From 3b208636b65270cf26d2e3df5f18b7bb9d354abf Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Wed, 4 Feb 2026 12:45:44 -0500 Subject: [PATCH 01/23] feat: Added icon api endpoints. --- package-lock.json | 10 +++ package.json | 9 +- src/pages/api/icons/[iconName].ts | 47 ++++++++++ src/pages/api/icons/index.ts | 32 +++++++ src/pages/api/index.ts | 52 +++++++++++ src/pages/api/openapi.json.ts | 81 +++++++++++++++++ src/utils/apiHelpers.ts | 12 ++- src/utils/icons/reactIcons.ts | 144 ++++++++++++++++++++++++++++++ 8 files changed, 382 insertions(+), 5 deletions(-) create mode 100644 src/pages/api/icons/[iconName].ts create mode 100644 src/pages/api/icons/index.ts create mode 100644 src/utils/icons/reactIcons.ts diff --git a/package-lock.json b/package-lock.json index eb6c533..390441d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "react-docgen": "^7.1.1", "react-dom": "^18.3.1", "react-error-boundary": "^6.0.0", + "react-icons": "^5.5.0", "sass": "^1.90.0", "typescript": "^5.9.2" }, @@ -21902,6 +21903,15 @@ "react": ">=16.13.1" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index cf3fc79..c1fe88b 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@nanostores/react": "^0.8.4", "@patternfly/ast-helpers": "1.4.0-alpha.190", "@patternfly/patternfly": "^6.0.0", + "@patternfly/quickstarts": "^6.0.0", "@patternfly/react-code-editor": "^6.2.2", "@patternfly/react-core": "^6.0.0", "@patternfly/react-drag-drop": "^6.0.0", @@ -62,7 +63,6 @@ "@patternfly/react-styles": "^6.0.0", "@patternfly/react-table": "^6.0.0", "@patternfly/react-tokens": "^6.0.0", - "@patternfly/quickstarts": "^6.0.0", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "astro": "^5.15.9", @@ -74,6 +74,7 @@ "react-docgen": "^7.1.1", "react-dom": "^18.3.1", "react-error-boundary": "^6.0.0", + "react-icons": "^5.5.0", "sass": "^1.90.0", "typescript": "^5.9.2" }, @@ -82,6 +83,8 @@ "@babel/preset-react": "^7.26.3", "@babel/preset-typescript": "^7.26.0", "@eslint/js": "^9.16.0", + "@patternfly/react-data-view": "^6.0.0", + "@patternfly/react-user-feedback": "^6.0.0", "@semantic-release/git": "^10.0.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", @@ -110,9 +113,7 @@ "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "typescript-eslint": "^8.15.0", - "wrangler": "^4.20.0", - "@patternfly/react-user-feedback": "^6.0.0", - "@patternfly/react-data-view": "^6.0.0" + "wrangler": "^4.20.0" }, "config": { "commitizen": { diff --git a/src/pages/api/icons/[iconName].ts b/src/pages/api/icons/[iconName].ts new file mode 100644 index 0000000..297f36a --- /dev/null +++ b/src/pages/api/icons/[iconName].ts @@ -0,0 +1,47 @@ +import type { APIRoute } from 'astro' +import { + createJsonResponse, + createSvgResponse, +} from '../../../utils/apiHelpers' +import { getIconSvg, parseIconId } from '../../../utils/icons/reactIcons' + +export const prerender = false + +/** + * GET /api/icons/[icon-name] + * Returns actual SVG markup for the icon. + * Icon name format: {set}_{iconName} (e.g., fa_FaCircle, md_MdHome) + */ +export const GET: APIRoute = async ({ params }) => { + const iconId = params.iconName + + if (!iconId) { + return createJsonResponse( + { error: 'Icon name parameter is required' }, + 400, + ) + } + + const parsed = parseIconId(iconId) + if (!parsed) { + return createJsonResponse( + { + error: 'Invalid icon name format', + expected: 'Use format {set}_{iconName} (e.g., fa_FaCircle, md_MdHome)', + }, + 400, + ) + } + + const { setId, iconName } = parsed + const svg = await getIconSvg(setId, iconName) + + if (!svg) { + return createJsonResponse( + { error: `Icon '${iconName}' not found in set '${setId}'` }, + 404, + ) + } + + return createSvgResponse(svg) +} diff --git a/src/pages/api/icons/index.ts b/src/pages/api/icons/index.ts new file mode 100644 index 0000000..da2bfc2 --- /dev/null +++ b/src/pages/api/icons/index.ts @@ -0,0 +1,32 @@ +import type { APIRoute } from 'astro' +import { createJsonResponse } from '../../../utils/apiHelpers' +import { getAllIcons, filterIcons } from '../../../utils/icons/reactIcons' + +export const prerender = false + +/** + * GET /api/icons + * Returns list of all available icons with metadata. + * + * GET /api/icons?filter=circle + * Returns filtered list of icons matching the filter term (case-insensitive). + */ +export const GET: APIRoute = async ({ url }) => { + try { + const filter = url.searchParams.get('filter') ?? '' + const icons = await getAllIcons() + const filtered = filterIcons(icons, filter) + + return createJsonResponse({ + icons: filtered, + total: filtered.length, + filter: filter || undefined, + }) + } catch (error) { + const details = error instanceof Error ? error.message : String(error) + return createJsonResponse( + { error: 'Failed to load icons', details }, + 500, + ) + } +} diff --git a/src/pages/api/index.ts b/src/pages/api/index.ts index ae1abeb..c39762d 100644 --- a/src/pages/api/index.ts +++ b/src/pages/api/index.ts @@ -367,6 +367,58 @@ export const GET: APIRoute = async () => ], }, }, + { + path: '/api/icons', + method: 'GET', + description: 'List all available icons with metadata from react-icons', + parameters: [ + { + name: 'filter', + in: 'query', + required: false, + type: 'string', + description: 'Filter icons by name (case-insensitive)', + example: 'circle', + }, + ], + returns: { + type: 'object', + description: 'List of icons with name, reactName, style, usage, unicode', + example: { + icons: [ + { + name: 'circle', + reactName: 'FaCircle', + style: 'solid', + usage: "import { FaCircle } from 'react-icons/fa'", + unicode: '', + }, + ], + total: 1, + filter: 'circle', + }, + }, + }, + { + path: '/api/icons/{icon-name}', + method: 'GET', + description: 'Get SVG markup for a specific icon', + parameters: [ + { + name: 'icon-name', + in: 'path', + required: true, + type: 'string', + description: 'Icon identifier in format {set}_{iconName} (e.g., fa_FaCircle, md_MdHome)', + example: 'fa_FaCircle', + }, + ], + returns: { + type: 'string', + contentType: 'image/svg+xml', + description: 'SVG markup for the icon', + }, + }, { path: '/api/{version}/{section}/{page}/{tab}/examples/{example}', method: 'GET', diff --git a/src/pages/api/openapi.json.ts b/src/pages/api/openapi.json.ts index c3f02a7..fd2625b 100644 --- a/src/pages/api/openapi.json.ts +++ b/src/pages/api/openapi.json.ts @@ -104,6 +104,87 @@ export const GET: APIRoute = async ({ url }) => { }, }, }, + '/icons': { + get: { + summary: 'List available icons', + description: + 'Returns list of all available icons from react-icons with metadata. Use filter query param to filter by name.', + operationId: 'getIcons', + parameters: [ + { + name: 'filter', + in: 'query', + required: false, + schema: { type: 'string' }, + description: 'Filter icons by name (case-insensitive)', + example: 'circle', + }, + ], + responses: { + '200': { + description: 'List of icons with metadata', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + icons: { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string', example: 'circle' }, + reactName: { type: 'string', example: 'FaCircle' }, + style: { type: 'string', example: 'solid' }, + usage: { + type: 'string', + example: "import { FaCircle } from 'react-icons/fa'", + }, + unicode: { type: 'string', example: '' }, + }, + }, + }, + total: { type: 'integer' }, + filter: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + }, + '/icons/{icon-name}': { + get: { + summary: 'Get icon SVG markup', + description: + 'Returns actual SVG markup for the icon. Icon name format: {set}_{iconName} (e.g., fa_FaCircle, md_MdHome)', + operationId: 'getIconSvg', + parameters: [ + { + name: 'icon-name', + in: 'path', + required: true, + schema: { type: 'string' }, + description: 'Icon identifier: {set}_{iconName}', + example: 'fa_FaCircle', + }, + ], + responses: { + '200': { + description: 'SVG markup for the icon', + content: { + 'image/svg+xml': { + schema: { type: 'string' }, + }, + }, + }, + '404': { + description: 'Icon not found', + }, + }, + }, + }, '/openapi.json': { get: { summary: 'Get OpenAPI specification', diff --git a/src/utils/apiHelpers.ts b/src/utils/apiHelpers.ts index 8fa9c07..bb4cf4d 100644 --- a/src/utils/apiHelpers.ts +++ b/src/utils/apiHelpers.ts @@ -1,5 +1,5 @@ function getHeaders( - type: 'application/json' | 'text/plain', + type: 'application/json' | 'text/plain' | 'image/svg+xml', contentLength?: number, ): HeadersInit { const headers: HeadersInit = { @@ -36,6 +36,16 @@ export function createTextResponse( }) } +export function createSvgResponse( + content: string, + status: number = 200, +): Response { + return new Response(content, { + status, + headers: getHeaders('image/svg+xml', content.length), + }) +} + /** * Creates an index key by joining parts with '::' separator * Used to construct keys for looking up sections, pages, and tabs in the API index diff --git a/src/utils/icons/reactIcons.ts b/src/utils/icons/reactIcons.ts new file mode 100644 index 0000000..60c2946 --- /dev/null +++ b/src/utils/icons/reactIcons.ts @@ -0,0 +1,144 @@ +/** + * Utilities for working with react-icons from the ESM package. + * Icons are loaded from node_modules/react-icons icon set folders. + */ +import { renderToStaticMarkup } from 'react-dom/server' +import React from 'react' +import { IconsManifest } from 'react-icons/lib' + +export interface IconMetadata { + name: string + reactName: string + style: string + usage: string + unicode: string + /** Set id for SVG URL: /api/icons/{set}_{reactName} */ + set?: string +} + +const ICON_SET_IDS = IconsManifest.map((m) => m.id) + +/** Derive style from set id and react name (e.g., fa + FaRegCircle -> "regular") */ +function getStyle(setId: string, reactName: string): string { + if (setId === 'fa' || setId === 'fa6') { + if (reactName.startsWith('FaReg')) return 'regular' + if (reactName.startsWith('FaBrands')) return 'brands' + return 'solid' + } + return setId +} + +/** Convert PascalCase to kebab-case */ +function toKebabCase(str: string): string { + return str + .replace(/([a-z])([A-Z])/g, '$1-$2') + .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') + .toLowerCase() +} + +/** Derive base name from react name by removing set-specific prefixes */ +function getBaseName(setId: string, reactName: string): string { + let base = reactName + if (setId === 'fa' || setId === 'fa6') { + base = base.replace(/^Fa(Reg|Brands)?/, '') + } else if (setId === 'io' || setId === 'io5') { + base = base.replace(/^Io(5)?/, '') + } else if (setId === 'md') { + base = base.replace(/^Md/, '') + } else if (setId === 'hi' || setId === 'hi2') { + base = base.replace(/^Hi(2)?/, '') + } else { + const setPrefix = setId.charAt(0).toUpperCase() + setId.slice(1) + const prefix = new RegExp(`^${setPrefix}`, 'i') + base = base.replace(prefix, '') + } + return toKebabCase(base) || toKebabCase(reactName) +} + +/** + * Get all icons from all sets with metadata. + * Shape: { name, reactName, style, usage, unicode } + */ +export async function getAllIcons(): Promise { + const icons: IconMetadata[] = [] + + for (const setId of ICON_SET_IDS) { + try { + const module = await import(`react-icons/${setId}`) + const iconNames = Object.keys(module).filter( + (k) => typeof module[k] === 'function' && k !== 'default', + ) + + for (const reactName of iconNames) { + icons.push({ + name: getBaseName(setId, reactName), + reactName, + style: getStyle(setId, reactName), + usage: `import { ${reactName} } from 'react-icons/${setId}'`, + unicode: '', + set: setId, + }) + } + } catch { + // Skip sets that fail to load + } + } + + return icons +} + +/** + * Filter icons by search term (case-insensitive match on name or reactName) + */ +export function filterIcons( + icons: IconMetadata[], + filter: string, +): IconMetadata[] { + if (!filter || !filter.trim()) return icons + const term = filter.toLowerCase().trim() + return icons.filter( + (icon) => + icon.name.toLowerCase().includes(term) || + icon.reactName.toLowerCase().includes(term), + ) +} + +/** + * Get SVG markup for a specific icon. + * @param setId - Icon set id (e.g., "fa", "md") + * @param iconName - Icon component name (e.g., "FaCircle") + */ +export async function getIconSvg( + setId: string, + iconName: string, +): Promise { + if (!ICON_SET_IDS.includes(setId)) return null + + try { + const module = await import(`react-icons/${setId}`) + const IconComponent = module[iconName] + if (typeof IconComponent !== 'function') return null + + const element = React.createElement(IconComponent, { + size: '1em', + style: { verticalAlign: 'middle' }, + }) + return renderToStaticMarkup(element) + } catch { + return null + } +} + +/** + * Parse icon id "set_iconName" into { setId, iconName } + */ +export function parseIconId(iconId: string): { setId: string; iconName: string } | null { + const underscoreIndex = iconId.indexOf('_') + if (underscoreIndex <= 0) return null + + const setId = iconId.slice(0, underscoreIndex) + const iconName = iconId.slice(underscoreIndex + 1) + if (!setId || !iconName) return null + + return { setId, iconName } +} From a0d38552414f8a0ee09b3534313c6e7478ceb13e Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Wed, 4 Feb 2026 16:44:49 -0500 Subject: [PATCH 02/23] feat: Added icons end points. --- .../[version]/icons/[iconName].test.ts | 202 +++++++++++++++++ .../__tests__/[version]/icons/index.test.ts | 212 ++++++++++++++++++ .../api/{ => [version]}/icons/[iconName].ts | 27 ++- src/pages/api/{ => [version]}/icons/index.ts | 25 ++- src/pages/api/index.ts | 18 +- src/pages/api/openapi.json.ts | 18 +- 6 files changed, 488 insertions(+), 14 deletions(-) create mode 100644 src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts create mode 100644 src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts rename src/pages/api/{ => [version]}/icons/[iconName].ts (54%) rename src/pages/api/{ => [version]}/icons/index.ts (50%) diff --git a/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts b/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts new file mode 100644 index 0000000..de123d9 --- /dev/null +++ b/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts @@ -0,0 +1,202 @@ +import { GET } from '../../../../../../pages/api/[version]/icons/[iconName]' + +const mockApiIndex = { + versions: ['v5', 'v6'], + sections: {}, + pages: {}, + tabs: {}, +} + +const mockSvg = '' + +jest.mock('../../../../../../utils/icons/reactIcons', () => ({ + getIconSvg: jest.fn((setId: string, iconName: string) => { + if (setId === 'fa' && iconName === 'FaCircle') return Promise.resolve(mockSvg) + return Promise.resolve(null) + }), + parseIconId: jest.fn((iconId: string) => { + const underscoreIndex = iconId.indexOf('_') + if (underscoreIndex <= 0) return null + const setId = iconId.slice(0, underscoreIndex) + const iconName = iconId.slice(underscoreIndex + 1) + if (!setId || !iconName) return null + return { setId, iconName } + }), +})) + +it('returns SVG markup for valid icon', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6', iconName: 'fa_FaCircle' }, + url: new URL('http://localhost:4321/api/v6/icons/fa_FaCircle'), + } as any) + const body = await response.text() + + expect(response.status).toBe(200) + expect(response.headers.get('Content-Type')).toBe( + 'image/svg+xml; charset=utf-8', + ) + expect(body).toBe(mockSvg) + expect(body).toContain(' { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6', iconName: 'fa_FaNonExistent' }, + url: new URL('http://localhost:4321/api/v6/icons/fa_FaNonExistent'), + } as any) + const body = await response.json() + + expect(response.status).toBe(404) + expect(body).toHaveProperty('error') + expect(body.error).toContain('FaNonExistent') + expect(body.error).toContain('fa') + expect(body.error).toContain('not found') + + jest.restoreAllMocks() +}) + +it('returns 400 for invalid icon name format (no underscore)', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6', iconName: 'invalid' }, + url: new URL('http://localhost:4321/api/v6/icons/invalid'), + } as any) + const body = await response.json() + + expect(response.status).toBe(400) + expect(body).toHaveProperty('error') + expect(body.error).toBe('Invalid icon name format') + expect(body).toHaveProperty('expected') + expect(body.expected).toContain('fa_FaCircle') + + jest.restoreAllMocks() +}) + +it('returns 400 for invalid icon name format (leading underscore)', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6', iconName: '_FaCircle' }, + url: new URL('http://localhost:4321/api/v6/icons/_FaCircle'), + } as any) + const body = await response.json() + + expect(response.status).toBe(400) + expect(body).toHaveProperty('error') + expect(body.error).toBe('Invalid icon name format') + + jest.restoreAllMocks() +}) + +it('returns 400 when icon name parameter is missing', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6' }, + url: new URL('http://localhost:4321/api/v6/icons'), + } as any) + const body = await response.json() + + expect(response.status).toBe(400) + expect(body).toHaveProperty('error') + expect(body.error).toContain('Icon name parameter is required') + + jest.restoreAllMocks() +}) + +it('returns 404 for nonexistent version', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v99', iconName: 'fa_FaCircle' }, + url: new URL('http://localhost:4321/api/v99/icons/fa_FaCircle'), + } as any) + const body = await response.json() + + expect(response.status).toBe(404) + expect(body).toHaveProperty('error') + expect(body.error).toContain('v99') + expect(body.error).toContain('not found') + + jest.restoreAllMocks() +}) + +it('returns 400 when version parameter is missing', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { iconName: 'fa_FaCircle' }, + url: new URL('http://localhost:4321/api/icons/fa_FaCircle'), + } as any) + const body = await response.json() + + expect(response.status).toBe(400) + expect(body).toHaveProperty('error') + expect(body.error).toContain('Version parameter is required') + + jest.restoreAllMocks() +}) + +it('returns 500 when fetchApiIndex fails', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + } as Response), + ) + + const response = await GET({ + params: { version: 'v6', iconName: 'fa_FaCircle' }, + url: new URL('http://localhost:4321/api/v6/icons/fa_FaCircle'), + } as any) + const body = await response.json() + + expect(response.status).toBe(500) + expect(body).toHaveProperty('error') + expect(body.error).toBe('Failed to fetch API index') + + jest.restoreAllMocks() +}) diff --git a/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts b/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts new file mode 100644 index 0000000..613dc29 --- /dev/null +++ b/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts @@ -0,0 +1,212 @@ +import { GET } from '../../../../../../pages/api/[version]/icons/index' + +const mockApiIndex = { + versions: ['v5', 'v6'], + sections: {}, + pages: {}, + tabs: {}, +} + +const mockIcons = [ + { + name: 'circle', + reactName: 'FaCircle', + style: 'solid', + usage: "import { FaCircle } from 'react-icons/fa'", + unicode: '', + set: 'fa', + }, + { + name: 'home', + reactName: 'MdHome', + style: 'md', + usage: "import { MdHome } from 'react-icons/md'", + unicode: '', + set: 'md', + }, + { + name: 'circle-outline', + reactName: 'FaRegCircle', + style: 'regular', + usage: "import { FaRegCircle } from 'react-icons/fa'", + unicode: '', + set: 'fa', + }, +] + +jest.mock('../../../../../../utils/icons/reactIcons', () => ({ + getAllIcons: jest.fn(() => Promise.resolve(mockIcons)), + filterIcons: jest.fn((icons: typeof mockIcons, filter: string) => { + if (!filter || !filter.trim()) return icons + const term = filter.toLowerCase().trim() + return icons.filter( + (icon: (typeof mockIcons)[0]) => + icon.name.toLowerCase().includes(term) || + icon.reactName.toLowerCase().includes(term), + ) + }), +})) + +it('returns all icons with metadata', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6' }, + url: new URL('http://localhost:4321/api/v6/icons'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(response.headers.get('Content-Type')).toBe( + 'application/json; charset=utf-8', + ) + expect(body).toHaveProperty('icons') + expect(body).toHaveProperty('total') + expect(Array.isArray(body.icons)).toBe(true) + expect(body.icons).toHaveLength(3) + expect(body.total).toBe(3) + expect(body.icons[0]).toHaveProperty('name') + expect(body.icons[0]).toHaveProperty('reactName') + expect(body.icons[0]).toHaveProperty('style') + expect(body.icons[0]).toHaveProperty('usage') + expect(body.icons[0]).toHaveProperty('unicode') + + jest.restoreAllMocks() +}) + +it('filters icons when filter parameter is provided', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6' }, + url: new URL('http://localhost:4321/api/v6/icons?filter=circle'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(body.icons).toHaveLength(2) + expect(body.total).toBe(2) + expect(body.filter).toBe('circle') + expect(body.icons.every((i: { name: string }) => i.name.includes('circle'))) + + jest.restoreAllMocks() +}) + +it('filter is case-insensitive', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6' }, + url: new URL('http://localhost:4321/api/v6/icons?filter=CIRCLE'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(body.icons.length).toBeGreaterThan(0) + + jest.restoreAllMocks() +}) + +it('returns empty icons array when filter yields no matches', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6' }, + url: new URL('http://localhost:4321/api/v6/icons?filter=nonexistent'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(body.icons).toHaveLength(0) + expect(body.total).toBe(0) + + jest.restoreAllMocks() +}) + +it('returns 404 error for nonexistent version', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v99' }, + url: new URL('http://localhost:4321/api/v99/icons'), + } as any) + const body = await response.json() + + expect(response.status).toBe(404) + expect(body).toHaveProperty('error') + expect(body.error).toContain('v99') + expect(body.error).toContain('not found') + + jest.restoreAllMocks() +}) + +it('returns 400 error when version parameter is missing', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: {}, + url: new URL('http://localhost:4321/api/icons'), + } as any) + const body = await response.json() + + expect(response.status).toBe(400) + expect(body).toHaveProperty('error') + expect(body.error).toContain('Version parameter is required') + + jest.restoreAllMocks() +}) + +it('returns 500 error when getAllIcons throws', async () => { + const { getAllIcons } = require('../../../../../../utils/icons/reactIcons') + ;(getAllIcons as jest.Mock).mockRejectedValueOnce(new Error('Load failed')) + + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response), + ) + + const response = await GET({ + params: { version: 'v6' }, + url: new URL('http://localhost:4321/api/v6/icons'), + } as any) + const body = await response.json() + + expect(response.status).toBe(500) + expect(body).toHaveProperty('error') + expect(body.error).toBe('Failed to load icons') + expect(body).toHaveProperty('details') + + jest.restoreAllMocks() +}) diff --git a/src/pages/api/icons/[iconName].ts b/src/pages/api/[version]/icons/[iconName].ts similarity index 54% rename from src/pages/api/icons/[iconName].ts rename to src/pages/api/[version]/icons/[iconName].ts index 297f36a..b510ac3 100644 --- a/src/pages/api/icons/[iconName].ts +++ b/src/pages/api/[version]/icons/[iconName].ts @@ -2,18 +2,35 @@ import type { APIRoute } from 'astro' import { createJsonResponse, createSvgResponse, -} from '../../../utils/apiHelpers' -import { getIconSvg, parseIconId } from '../../../utils/icons/reactIcons' +} from '../../../../utils/apiHelpers' +import { fetchApiIndex } from '../../../../utils/apiIndex/fetch' +import { getIconSvg, parseIconId } from '../../../../utils/icons/reactIcons' export const prerender = false /** - * GET /api/icons/[icon-name] + * GET /api/{version}/icons/[icon-name] * Returns actual SVG markup for the icon. * Icon name format: {set}_{iconName} (e.g., fa_FaCircle, md_MdHome) */ -export const GET: APIRoute = async ({ params }) => { - const iconId = params.iconName +export const GET: APIRoute = async ({ params, url }) => { + const { version, iconName: iconId } = params + + if (!version) { + return createJsonResponse( + { error: 'Version parameter is required' }, + 400, + ) + } + + try { + const index = await fetchApiIndex(url) + if (!index.versions.includes(version)) { + return createJsonResponse({ error: `Version '${version}' not found` }, 404) + } + } catch { + return createJsonResponse({ error: 'Failed to fetch API index' }, 500) + } if (!iconId) { return createJsonResponse( diff --git a/src/pages/api/icons/index.ts b/src/pages/api/[version]/icons/index.ts similarity index 50% rename from src/pages/api/icons/index.ts rename to src/pages/api/[version]/icons/index.ts index da2bfc2..5a05d4c 100644 --- a/src/pages/api/icons/index.ts +++ b/src/pages/api/[version]/icons/index.ts @@ -1,18 +1,33 @@ import type { APIRoute } from 'astro' -import { createJsonResponse } from '../../../utils/apiHelpers' -import { getAllIcons, filterIcons } from '../../../utils/icons/reactIcons' +import { createJsonResponse } from '../../../../utils/apiHelpers' +import { fetchApiIndex } from '../../../../utils/apiIndex/fetch' +import { getAllIcons, filterIcons } from '../../../../utils/icons/reactIcons' export const prerender = false /** - * GET /api/icons + * GET /api/{version}/icons * Returns list of all available icons with metadata. * - * GET /api/icons?filter=circle + * GET /api/{version}/icons?filter=circle * Returns filtered list of icons matching the filter term (case-insensitive). */ -export const GET: APIRoute = async ({ url }) => { +export const GET: APIRoute = async ({ params, url }) => { + const { version } = params + + if (!version) { + return createJsonResponse( + { error: 'Version parameter is required' }, + 400, + ) + } + try { + const index = await fetchApiIndex(url) + if (!index.versions.includes(version)) { + return createJsonResponse({ error: `Version '${version}' not found` }, 404) + } + const filter = url.searchParams.get('filter') ?? '' const icons = await getAllIcons() const filtered = filterIcons(icons, filter) diff --git a/src/pages/api/index.ts b/src/pages/api/index.ts index c39762d..534022e 100644 --- a/src/pages/api/index.ts +++ b/src/pages/api/index.ts @@ -368,10 +368,17 @@ export const GET: APIRoute = async () => }, }, { - path: '/api/icons', + path: '/api/{version}/icons', method: 'GET', description: 'List all available icons with metadata from react-icons', parameters: [ + { + name: 'version', + in: 'path', + required: true, + type: 'string', + example: 'v6', + }, { name: 'filter', in: 'query', @@ -400,10 +407,17 @@ export const GET: APIRoute = async () => }, }, { - path: '/api/icons/{icon-name}', + path: '/api/{version}/icons/{icon-name}', method: 'GET', description: 'Get SVG markup for a specific icon', parameters: [ + { + name: 'version', + in: 'path', + required: true, + type: 'string', + example: 'v6', + }, { name: 'icon-name', in: 'path', diff --git a/src/pages/api/openapi.json.ts b/src/pages/api/openapi.json.ts index fd2625b..081bc52 100644 --- a/src/pages/api/openapi.json.ts +++ b/src/pages/api/openapi.json.ts @@ -104,13 +104,20 @@ export const GET: APIRoute = async ({ url }) => { }, }, }, - '/icons': { + '/{version}/icons': { get: { summary: 'List available icons', description: 'Returns list of all available icons from react-icons with metadata. Use filter query param to filter by name.', operationId: 'getIcons', parameters: [ + { + name: 'version', + in: 'path', + required: true, + schema: { type: 'string' }, + example: 'v6', + }, { name: 'filter', in: 'query', @@ -154,13 +161,20 @@ export const GET: APIRoute = async ({ url }) => { }, }, }, - '/icons/{icon-name}': { + '/{version}/icons/{icon-name}': { get: { summary: 'Get icon SVG markup', description: 'Returns actual SVG markup for the icon. Icon name format: {set}_{iconName} (e.g., fa_FaCircle, md_MdHome)', operationId: 'getIconSvg', parameters: [ + { + name: 'version', + in: 'path', + required: true, + schema: { type: 'string' }, + example: 'v6', + }, { name: 'icon-name', in: 'path', From da36b8d89b3f1c20f68beb1f254ea7f7d6738f86 Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Fri, 6 Feb 2026 13:14:24 -0500 Subject: [PATCH 03/23] chore: Updated with a few modifications. --- .../pages/api/__tests__/[version]/icons/index.test.ts | 2 +- src/pages/api/[version]/icons/[iconName].ts | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts b/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts index 613dc29..ce8aae2 100644 --- a/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts +++ b/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts @@ -97,7 +97,7 @@ it('filters icons when filter parameter is provided', async () => { expect(body.icons).toHaveLength(2) expect(body.total).toBe(2) expect(body.filter).toBe('circle') - expect(body.icons.every((i: { name: string }) => i.name.includes('circle'))) + expect(body.icons.every((i: { name: string }) => i.name.includes('circle'))).toBe(true) jest.restoreAllMocks() }) diff --git a/src/pages/api/[version]/icons/[iconName].ts b/src/pages/api/[version]/icons/[iconName].ts index b510ac3..4bb0d42 100644 --- a/src/pages/api/[version]/icons/[iconName].ts +++ b/src/pages/api/[version]/icons/[iconName].ts @@ -54,10 +54,7 @@ export const GET: APIRoute = async ({ params, url }) => { const svg = await getIconSvg(setId, iconName) if (!svg) { - return createJsonResponse( - { error: `Icon '${iconName}' not found in set '${setId}'` }, - 404, - ) + return createJsonResponse([]); } return createSvgResponse(svg) From 24375727131d08890647fbc540488044e44b7991 Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Fri, 6 Feb 2026 13:28:05 -0500 Subject: [PATCH 04/23] fixed broken test. --- src/pages/api/[version]/icons/[iconName].ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/api/[version]/icons/[iconName].ts b/src/pages/api/[version]/icons/[iconName].ts index 4bb0d42..b510ac3 100644 --- a/src/pages/api/[version]/icons/[iconName].ts +++ b/src/pages/api/[version]/icons/[iconName].ts @@ -54,7 +54,10 @@ export const GET: APIRoute = async ({ params, url }) => { const svg = await getIconSvg(setId, iconName) if (!svg) { - return createJsonResponse([]); + return createJsonResponse( + { error: `Icon '${iconName}' not found in set '${setId}'` }, + 404, + ) } return createSvgResponse(svg) From bc103e11ddf21c607fff012a6efe8cd7a9430f5b Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Fri, 6 Feb 2026 13:57:36 -0500 Subject: [PATCH 05/23] chore: fix lint errors. --- .../[version]/icons/[iconName].test.ts | 12 ++++++-- .../__tests__/[version]/icons/index.test.ts | 6 ++-- src/utils/icons/reactIcons.ts | 28 ++++++++++++++----- 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts b/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts index de123d9..2f5f714 100644 --- a/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts +++ b/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts @@ -11,15 +11,21 @@ const mockSvg = '< jest.mock('../../../../../../utils/icons/reactIcons', () => ({ getIconSvg: jest.fn((setId: string, iconName: string) => { - if (setId === 'fa' && iconName === 'FaCircle') return Promise.resolve(mockSvg) + if (setId === 'fa' && iconName === 'FaCircle') { + return Promise.resolve(mockSvg) + } return Promise.resolve(null) }), parseIconId: jest.fn((iconId: string) => { const underscoreIndex = iconId.indexOf('_') - if (underscoreIndex <= 0) return null + if (underscoreIndex <= 0) { + return null + } const setId = iconId.slice(0, underscoreIndex) const iconName = iconId.slice(underscoreIndex + 1) - if (!setId || !iconName) return null + if (!setId || !iconName) { + return null + } return { setId, iconName } }), })) diff --git a/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts b/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts index ce8aae2..81a94bc 100644 --- a/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts +++ b/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts @@ -1,4 +1,5 @@ import { GET } from '../../../../../../pages/api/[version]/icons/index' +import { getAllIcons } from '../../../../../../utils/icons/reactIcons' const mockApiIndex = { versions: ['v5', 'v6'], @@ -37,7 +38,9 @@ const mockIcons = [ jest.mock('../../../../../../utils/icons/reactIcons', () => ({ getAllIcons: jest.fn(() => Promise.resolve(mockIcons)), filterIcons: jest.fn((icons: typeof mockIcons, filter: string) => { - if (!filter || !filter.trim()) return icons + if (!filter || !filter.trim()) { + return icons + } const term = filter.toLowerCase().trim() return icons.filter( (icon: (typeof mockIcons)[0]) => @@ -187,7 +190,6 @@ it('returns 400 error when version parameter is missing', async () => { }) it('returns 500 error when getAllIcons throws', async () => { - const { getAllIcons } = require('../../../../../../utils/icons/reactIcons') ;(getAllIcons as jest.Mock).mockRejectedValueOnce(new Error('Load failed')) global.fetch = jest.fn(() => diff --git a/src/utils/icons/reactIcons.ts b/src/utils/icons/reactIcons.ts index 60c2946..99cf35b 100644 --- a/src/utils/icons/reactIcons.ts +++ b/src/utils/icons/reactIcons.ts @@ -21,8 +21,12 @@ const ICON_SET_IDS = IconsManifest.map((m) => m.id) /** Derive style from set id and react name (e.g., fa + FaRegCircle -> "regular") */ function getStyle(setId: string, reactName: string): string { if (setId === 'fa' || setId === 'fa6') { - if (reactName.startsWith('FaReg')) return 'regular' - if (reactName.startsWith('FaBrands')) return 'brands' + if (reactName.startsWith('FaReg')) { + return 'regular' + } + if (reactName.startsWith('FaBrands')) { + return 'brands' + } return 'solid' } return setId @@ -94,7 +98,9 @@ export function filterIcons( icons: IconMetadata[], filter: string, ): IconMetadata[] { - if (!filter || !filter.trim()) return icons + if (!filter || !filter.trim()) { + return icons + } const term = filter.toLowerCase().trim() return icons.filter( (icon) => @@ -112,12 +118,16 @@ export async function getIconSvg( setId: string, iconName: string, ): Promise { - if (!ICON_SET_IDS.includes(setId)) return null + if (!ICON_SET_IDS.includes(setId)) { + return null + } try { const module = await import(`react-icons/${setId}`) const IconComponent = module[iconName] - if (typeof IconComponent !== 'function') return null + if (typeof IconComponent !== 'function') { + return null + } const element = React.createElement(IconComponent, { size: '1em', @@ -134,11 +144,15 @@ export async function getIconSvg( */ export function parseIconId(iconId: string): { setId: string; iconName: string } | null { const underscoreIndex = iconId.indexOf('_') - if (underscoreIndex <= 0) return null + if (underscoreIndex <= 0) { + return null + } const setId = iconId.slice(0, underscoreIndex) const iconName = iconId.slice(underscoreIndex + 1) - if (!setId || !iconName) return null + if (!setId || !iconName) { + return null + } return { setId, iconName } } From e38d0a112f0add536ede486df483ee08b89dc427 Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Fri, 6 Feb 2026 15:33:55 -0500 Subject: [PATCH 06/23] fix: Added prerender step for cloudflare. --- .../__tests__/[version]/icons/index.test.ts | 71 +++++++------------ src/pages/api/[version]/icons/index.ts | 5 +- src/pages/iconsIndex.json.ts | 31 ++++++++ src/utils/icons/fetch.ts | 27 +++++++ 4 files changed, 87 insertions(+), 47 deletions(-) create mode 100644 src/pages/iconsIndex.json.ts create mode 100644 src/utils/icons/fetch.ts diff --git a/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts b/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts index 81a94bc..f5b50d5 100644 --- a/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts +++ b/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts @@ -1,5 +1,4 @@ import { GET } from '../../../../../../pages/api/[version]/icons/index' -import { getAllIcons } from '../../../../../../utils/icons/reactIcons' const mockApiIndex = { versions: ['v5', 'v6'], @@ -35,8 +34,18 @@ const mockIcons = [ }, ] +function createFetchMock(): typeof fetch { + return jest.fn((input: RequestInfo | URL) => { + const url = typeof input === 'string' ? input : input.toString() + const json = () => + Promise.resolve( + url.includes('iconsIndex.json') ? { icons: mockIcons } : mockApiIndex + ) + return Promise.resolve({ ok: true, json } as Response) + }) as typeof fetch +} + jest.mock('../../../../../../utils/icons/reactIcons', () => ({ - getAllIcons: jest.fn(() => Promise.resolve(mockIcons)), filterIcons: jest.fn((icons: typeof mockIcons, filter: string) => { if (!filter || !filter.trim()) { return icons @@ -51,12 +60,7 @@ jest.mock('../../../../../../utils/icons/reactIcons', () => ({ })) it('returns all icons with metadata', async () => { - global.fetch = jest.fn(() => - Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockApiIndex), - } as Response), - ) + global.fetch = createFetchMock() const response = await GET({ params: { version: 'v6' }, @@ -83,12 +87,7 @@ it('returns all icons with metadata', async () => { }) it('filters icons when filter parameter is provided', async () => { - global.fetch = jest.fn(() => - Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockApiIndex), - } as Response), - ) + global.fetch = createFetchMock() const response = await GET({ params: { version: 'v6' }, @@ -106,12 +105,7 @@ it('filters icons when filter parameter is provided', async () => { }) it('filter is case-insensitive', async () => { - global.fetch = jest.fn(() => - Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockApiIndex), - } as Response), - ) + global.fetch = createFetchMock() const response = await GET({ params: { version: 'v6' }, @@ -126,12 +120,7 @@ it('filter is case-insensitive', async () => { }) it('returns empty icons array when filter yields no matches', async () => { - global.fetch = jest.fn(() => - Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockApiIndex), - } as Response), - ) + global.fetch = createFetchMock() const response = await GET({ params: { version: 'v6' }, @@ -147,12 +136,7 @@ it('returns empty icons array when filter yields no matches', async () => { }) it('returns 404 error for nonexistent version', async () => { - global.fetch = jest.fn(() => - Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockApiIndex), - } as Response), - ) + global.fetch = createFetchMock() const response = await GET({ params: { version: 'v99' }, @@ -169,12 +153,7 @@ it('returns 404 error for nonexistent version', async () => { }) it('returns 400 error when version parameter is missing', async () => { - global.fetch = jest.fn(() => - Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockApiIndex), - } as Response), - ) + global.fetch = createFetchMock() const response = await GET({ params: {}, @@ -189,15 +168,17 @@ it('returns 400 error when version parameter is missing', async () => { jest.restoreAllMocks() }) -it('returns 500 error when getAllIcons throws', async () => { - ;(getAllIcons as jest.Mock).mockRejectedValueOnce(new Error('Load failed')) - - global.fetch = jest.fn(() => - Promise.resolve({ +it('returns 500 error when fetchIconsIndex throws', async () => { + global.fetch = jest.fn((input: RequestInfo | URL) => { + const url = typeof input === 'string' ? input : input.toString() + if (url.includes('iconsIndex.json')) { + return Promise.resolve({ ok: false, status: 500, statusText: 'Internal Server Error' } as Response) + } + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockApiIndex), - } as Response), - ) + } as Response) + }) as typeof fetch const response = await GET({ params: { version: 'v6' }, diff --git a/src/pages/api/[version]/icons/index.ts b/src/pages/api/[version]/icons/index.ts index 5a05d4c..93105f5 100644 --- a/src/pages/api/[version]/icons/index.ts +++ b/src/pages/api/[version]/icons/index.ts @@ -1,7 +1,8 @@ import type { APIRoute } from 'astro' import { createJsonResponse } from '../../../../utils/apiHelpers' import { fetchApiIndex } from '../../../../utils/apiIndex/fetch' -import { getAllIcons, filterIcons } from '../../../../utils/icons/reactIcons' +import { fetchIconsIndex } from '../../../../utils/icons/fetch' +import { filterIcons } from '../../../../utils/icons/reactIcons' export const prerender = false @@ -29,7 +30,7 @@ export const GET: APIRoute = async ({ params, url }) => { } const filter = url.searchParams.get('filter') ?? '' - const icons = await getAllIcons() + const icons = await fetchIconsIndex(url) const filtered = filterIcons(icons, filter) return createJsonResponse({ diff --git a/src/pages/iconsIndex.json.ts b/src/pages/iconsIndex.json.ts new file mode 100644 index 0000000..52e724f --- /dev/null +++ b/src/pages/iconsIndex.json.ts @@ -0,0 +1,31 @@ +import type { APIRoute } from 'astro' +import { getAllIcons } from '../utils/icons/reactIcons' + +/** + * Prerender at build time so this doesn't run in the Cloudflare Worker. + * getAllIcons() uses dynamic imports of react-icons which fail in Workers + * due to bundle size and Node.js compatibility. + */ +export const prerender = true + +export const GET: APIRoute = async () => { + try { + const icons = await getAllIcons() + return new Response(JSON.stringify({ icons }), { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }) + } catch (error) { + return new Response( + JSON.stringify({ error: 'Failed to load icons index', details: String(error) }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + } + ) + } +} diff --git a/src/utils/icons/fetch.ts b/src/utils/icons/fetch.ts new file mode 100644 index 0000000..ccb0b66 --- /dev/null +++ b/src/utils/icons/fetch.ts @@ -0,0 +1,27 @@ +import type { IconMetadata } from './reactIcons' + +export interface IconsIndex { + icons: IconMetadata[] +} + +/** + * Fetches the icons index from the server as a static asset. + * Used by API routes at runtime instead of calling getAllIcons() which uses + * dynamic imports that fail in Cloudflare Workers. + * + * @param url - The URL object from the API route context + * @returns Promise resolving to the icons index structure + */ +export async function fetchIconsIndex(url: URL): Promise { + const iconsIndexUrl = new URL('/iconsIndex.json', url.origin) + const response = await fetch(iconsIndexUrl) + + if (!response.ok) { + throw new Error( + `Failed to load icons index: ${response.status} ${response.statusText}` + ) + } + + const data = (await response.json()) as IconsIndex + return data.icons +} From 0d52a5a4d1af3f7918d4990d36cd89bba320e710 Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Fri, 6 Feb 2026 16:00:12 -0500 Subject: [PATCH 07/23] fix: Updated to prerender svgs as well for cloud flare. --- .../[version]/icons/[iconName].test.ts | 99 +++++++++---------- src/pages/api/[version]/icons/[iconName].ts | 6 +- src/pages/iconsSvgs/[setId].json.ts | 46 +++++++++ src/utils/icons/fetch.ts | 23 +++++ src/utils/icons/reactIcons.ts | 35 +++++++ 5 files changed, 152 insertions(+), 57 deletions(-) create mode 100644 src/pages/iconsSvgs/[setId].json.ts diff --git a/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts b/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts index 2f5f714..2b99aac 100644 --- a/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts +++ b/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts @@ -9,13 +9,30 @@ const mockApiIndex = { const mockSvg = '' -jest.mock('../../../../../../utils/icons/reactIcons', () => ({ - getIconSvg: jest.fn((setId: string, iconName: string) => { - if (setId === 'fa' && iconName === 'FaCircle') { - return Promise.resolve(mockSvg) +const mockIconSvgs: Record> = { + fa: { FaCircle: mockSvg }, +} + +function createFetchMock(): typeof fetch { + return jest.fn((input: RequestInfo | URL) => { + const url = typeof input === 'string' ? input : input.toString() + const match = url.match(/\/iconsSvgs\/([^/]+)\.json/) + if (match) { + const setId = match[1] + const svgs = mockIconSvgs[setId] ?? {} + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(svgs), + } as Response) } - return Promise.resolve(null) - }), + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response) + }) as typeof fetch +} + +jest.mock('../../../../../../utils/icons/reactIcons', () => ({ parseIconId: jest.fn((iconId: string) => { const underscoreIndex = iconId.indexOf('_') if (underscoreIndex <= 0) { @@ -31,12 +48,7 @@ jest.mock('../../../../../../utils/icons/reactIcons', () => ({ })) it('returns SVG markup for valid icon', async () => { - global.fetch = jest.fn(() => - Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockApiIndex), - } as Response), - ) + global.fetch = createFetchMock() const response = await GET({ params: { version: 'v6', iconName: 'fa_FaCircle' }, @@ -55,12 +67,7 @@ it('returns SVG markup for valid icon', async () => { }) it('returns 404 when icon is not found in set', async () => { - global.fetch = jest.fn(() => - Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockApiIndex), - } as Response), - ) + global.fetch = createFetchMock() const response = await GET({ params: { version: 'v6', iconName: 'fa_FaNonExistent' }, @@ -78,12 +85,7 @@ it('returns 404 when icon is not found in set', async () => { }) it('returns 400 for invalid icon name format (no underscore)', async () => { - global.fetch = jest.fn(() => - Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockApiIndex), - } as Response), - ) + global.fetch = createFetchMock() const response = await GET({ params: { version: 'v6', iconName: 'invalid' }, @@ -101,12 +103,7 @@ it('returns 400 for invalid icon name format (no underscore)', async () => { }) it('returns 400 for invalid icon name format (leading underscore)', async () => { - global.fetch = jest.fn(() => - Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockApiIndex), - } as Response), - ) + global.fetch = createFetchMock() const response = await GET({ params: { version: 'v6', iconName: '_FaCircle' }, @@ -122,12 +119,7 @@ it('returns 400 for invalid icon name format (leading underscore)', async () => }) it('returns 400 when icon name parameter is missing', async () => { - global.fetch = jest.fn(() => - Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockApiIndex), - } as Response), - ) + global.fetch = createFetchMock() const response = await GET({ params: { version: 'v6' }, @@ -143,12 +135,7 @@ it('returns 400 when icon name parameter is missing', async () => { }) it('returns 404 for nonexistent version', async () => { - global.fetch = jest.fn(() => - Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockApiIndex), - } as Response), - ) + global.fetch = createFetchMock() const response = await GET({ params: { version: 'v99', iconName: 'fa_FaCircle' }, @@ -165,12 +152,7 @@ it('returns 404 for nonexistent version', async () => { }) it('returns 400 when version parameter is missing', async () => { - global.fetch = jest.fn(() => - Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockApiIndex), - } as Response), - ) + global.fetch = createFetchMock() const response = await GET({ params: { iconName: 'fa_FaCircle' }, @@ -186,13 +168,20 @@ it('returns 400 when version parameter is missing', async () => { }) it('returns 500 when fetchApiIndex fails', async () => { - global.fetch = jest.fn(() => - Promise.resolve({ - ok: false, - status: 500, - statusText: 'Internal Server Error', - } as Response), - ) + global.fetch = jest.fn((input: RequestInfo | URL) => { + const url = typeof input === 'string' ? input : input.toString() + if (url.includes('apiIndex.json')) { + return Promise.resolve({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + } as Response) + } + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({}), + } as Response) + }) as typeof fetch const response = await GET({ params: { version: 'v6', iconName: 'fa_FaCircle' }, diff --git a/src/pages/api/[version]/icons/[iconName].ts b/src/pages/api/[version]/icons/[iconName].ts index b510ac3..bde1882 100644 --- a/src/pages/api/[version]/icons/[iconName].ts +++ b/src/pages/api/[version]/icons/[iconName].ts @@ -4,7 +4,8 @@ import { createSvgResponse, } from '../../../../utils/apiHelpers' import { fetchApiIndex } from '../../../../utils/apiIndex/fetch' -import { getIconSvg, parseIconId } from '../../../../utils/icons/reactIcons' +import { fetchIconSvgs } from '../../../../utils/icons/fetch' +import { parseIconId } from '../../../../utils/icons/reactIcons' export const prerender = false @@ -51,7 +52,8 @@ export const GET: APIRoute = async ({ params, url }) => { } const { setId, iconName } = parsed - const svg = await getIconSvg(setId, iconName) + const svgs = await fetchIconSvgs(url, setId) + const svg = svgs?.[iconName] ?? null if (!svg) { return createJsonResponse( diff --git a/src/pages/iconsSvgs/[setId].json.ts b/src/pages/iconsSvgs/[setId].json.ts new file mode 100644 index 0000000..9c150cc --- /dev/null +++ b/src/pages/iconsSvgs/[setId].json.ts @@ -0,0 +1,46 @@ +import type { APIRoute, GetStaticPaths } from 'astro' +import { IconsManifest } from 'react-icons/lib' +import { getIconSvgsForSet } from '../../utils/icons/reactIcons' + +/** + * Prerender at build time so this doesn't run in the Cloudflare Worker. + * getIconSvgsForSet() uses dynamic imports of react-icons which fail in Workers. + */ +export const prerender = true + +export const getStaticPaths: GetStaticPaths = async () => + IconsManifest.map((m) => ({ + params: { setId: m.id }, + })) + +export const GET: APIRoute = async ({ params }) => { + const { setId } = params + + if (!setId) { + return new Response( + JSON.stringify({ error: 'Set ID is required' }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ) + } + + try { + const svgs = await getIconSvgsForSet(setId) + return new Response(JSON.stringify(svgs), { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }) + } catch (error) { + return new Response( + JSON.stringify({ + error: 'Failed to load icon SVGs', + details: String(error), + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + } + ) + } +} diff --git a/src/utils/icons/fetch.ts b/src/utils/icons/fetch.ts index ccb0b66..5963772 100644 --- a/src/utils/icons/fetch.ts +++ b/src/utils/icons/fetch.ts @@ -25,3 +25,26 @@ export async function fetchIconsIndex(url: URL): Promise { const data = (await response.json()) as IconsIndex return data.icons } + +/** + * Fetches prerendered SVG markup for all icons in a set. + * Used by the icon SVG API route at runtime instead of getIconSvg() which + * uses dynamic imports that fail in Cloudflare Workers. + * + * @param url - The URL object from the API route context + * @param setId - Icon set id (e.g., "fa", "ci") + * @returns Promise resolving to Record of iconName -> SVG string, or null if fetch fails + */ +export async function fetchIconSvgs( + url: URL, + setId: string, +): Promise | null> { + const iconsSvgsUrl = new URL(`/iconsSvgs/${setId}.json`, url.origin) + const response = await fetch(iconsSvgsUrl) + + if (!response.ok) { + return null + } + + return (await response.json()) as Record +} diff --git a/src/utils/icons/reactIcons.ts b/src/utils/icons/reactIcons.ts index 99cf35b..575727c 100644 --- a/src/utils/icons/reactIcons.ts +++ b/src/utils/icons/reactIcons.ts @@ -109,6 +109,41 @@ export function filterIcons( ) } +/** + * Get SVG markup for all icons in a set. Used at build time for prerendering. + * @param setId - Icon set id (e.g., "fa", "md") + * @returns Record of iconName -> SVG string + */ +export async function getIconSvgsForSet( + setId: string, +): Promise> { + if (!ICON_SET_IDS.includes(setId)) { + return {} + } + + try { + const module = await import(`react-icons/${setId}`) + const svgs: Record = {} + + for (const iconName of Object.keys(module)) { + const IconComponent = module[iconName] + if (typeof IconComponent !== 'function' || iconName === 'default') { + continue + } + + const element = React.createElement(IconComponent, { + size: '1em', + style: { verticalAlign: 'middle' }, + }) + svgs[iconName] = renderToStaticMarkup(element) + } + + return svgs + } catch { + return {} + } +} + /** * Get SVG markup for a specific icon. * @param setId - Icon set id (e.g., "fa", "md") From a8654f0c0bcd2a7a0dd274d31c516b748263bb32 Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Wed, 11 Feb 2026 10:22:57 -0500 Subject: [PATCH 08/23] Updated to react name as the end point. --- .../[version]/icons/[iconName].test.ts | 75 +++++++------------ src/pages/api/[version]/icons/[iconName].ts | 28 +++---- src/pages/api/index.ts | 4 +- src/pages/api/openapi.json.ts | 6 +- src/utils/icons/reactIcons.ts | 2 +- 5 files changed, 45 insertions(+), 70 deletions(-) diff --git a/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts b/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts index 2b99aac..f912cab 100644 --- a/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts +++ b/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts @@ -13,9 +13,21 @@ const mockIconSvgs: Record> = { fa: { FaCircle: mockSvg }, } +const mockIconsIndex = { + icons: [ + { name: 'circle', reactName: 'FaCircle', style: 'solid', usage: '', unicode: '', set: 'fa' }, + ], +} + function createFetchMock(): typeof fetch { return jest.fn((input: RequestInfo | URL) => { const url = typeof input === 'string' ? input : input.toString() + if (url.includes('/iconsIndex.json')) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockIconsIndex), + } as Response) + } const match = url.match(/\/iconsSvgs\/([^/]+)\.json/) if (match) { const setId = match[1] @@ -32,27 +44,12 @@ function createFetchMock(): typeof fetch { }) as typeof fetch } -jest.mock('../../../../../../utils/icons/reactIcons', () => ({ - parseIconId: jest.fn((iconId: string) => { - const underscoreIndex = iconId.indexOf('_') - if (underscoreIndex <= 0) { - return null - } - const setId = iconId.slice(0, underscoreIndex) - const iconName = iconId.slice(underscoreIndex + 1) - if (!setId || !iconName) { - return null - } - return { setId, iconName } - }), -})) - it('returns SVG markup for valid icon', async () => { global.fetch = createFetchMock() const response = await GET({ - params: { version: 'v6', iconName: 'fa_FaCircle' }, - url: new URL('http://localhost:4321/api/v6/icons/fa_FaCircle'), + params: { version: 'v6', iconName: 'FaCircle' }, + url: new URL('http://localhost:4321/api/v6/icons/FaCircle'), } as any) const body = await response.text() @@ -66,25 +63,24 @@ it('returns SVG markup for valid icon', async () => { jest.restoreAllMocks() }) -it('returns 404 when icon is not found in set', async () => { +it('returns 404 when icon is not found', async () => { global.fetch = createFetchMock() const response = await GET({ - params: { version: 'v6', iconName: 'fa_FaNonExistent' }, - url: new URL('http://localhost:4321/api/v6/icons/fa_FaNonExistent'), + params: { version: 'v6', iconName: 'FaNonExistent' }, + url: new URL('http://localhost:4321/api/v6/icons/FaNonExistent'), } as any) const body = await response.json() expect(response.status).toBe(404) expect(body).toHaveProperty('error') expect(body.error).toContain('FaNonExistent') - expect(body.error).toContain('fa') expect(body.error).toContain('not found') jest.restoreAllMocks() }) -it('returns 400 for invalid icon name format (no underscore)', async () => { +it('returns 404 when icon name is not in index', async () => { global.fetch = createFetchMock() const response = await GET({ @@ -93,27 +89,10 @@ it('returns 400 for invalid icon name format (no underscore)', async () => { } as any) const body = await response.json() - expect(response.status).toBe(400) - expect(body).toHaveProperty('error') - expect(body.error).toBe('Invalid icon name format') - expect(body).toHaveProperty('expected') - expect(body.expected).toContain('fa_FaCircle') - - jest.restoreAllMocks() -}) - -it('returns 400 for invalid icon name format (leading underscore)', async () => { - global.fetch = createFetchMock() - - const response = await GET({ - params: { version: 'v6', iconName: '_FaCircle' }, - url: new URL('http://localhost:4321/api/v6/icons/_FaCircle'), - } as any) - const body = await response.json() - - expect(response.status).toBe(400) + expect(response.status).toBe(404) expect(body).toHaveProperty('error') - expect(body.error).toBe('Invalid icon name format') + expect(body.error).toContain('invalid') + expect(body.error).toContain('not found') jest.restoreAllMocks() }) @@ -138,8 +117,8 @@ it('returns 404 for nonexistent version', async () => { global.fetch = createFetchMock() const response = await GET({ - params: { version: 'v99', iconName: 'fa_FaCircle' }, - url: new URL('http://localhost:4321/api/v99/icons/fa_FaCircle'), + params: { version: 'v99', iconName: 'FaCircle' }, + url: new URL('http://localhost:4321/api/v99/icons/FaCircle'), } as any) const body = await response.json() @@ -155,8 +134,8 @@ it('returns 400 when version parameter is missing', async () => { global.fetch = createFetchMock() const response = await GET({ - params: { iconName: 'fa_FaCircle' }, - url: new URL('http://localhost:4321/api/icons/fa_FaCircle'), + params: { iconName: 'FaCircle' }, + url: new URL('http://localhost:4321/api/icons/FaCircle'), } as any) const body = await response.json() @@ -184,8 +163,8 @@ it('returns 500 when fetchApiIndex fails', async () => { }) as typeof fetch const response = await GET({ - params: { version: 'v6', iconName: 'fa_FaCircle' }, - url: new URL('http://localhost:4321/api/v6/icons/fa_FaCircle'), + params: { version: 'v6', iconName: 'FaCircle' }, + url: new URL('http://localhost:4321/api/v6/icons/FaCircle'), } as any) const body = await response.json() diff --git a/src/pages/api/[version]/icons/[iconName].ts b/src/pages/api/[version]/icons/[iconName].ts index bde1882..c377a28 100644 --- a/src/pages/api/[version]/icons/[iconName].ts +++ b/src/pages/api/[version]/icons/[iconName].ts @@ -4,18 +4,17 @@ import { createSvgResponse, } from '../../../../utils/apiHelpers' import { fetchApiIndex } from '../../../../utils/apiIndex/fetch' -import { fetchIconSvgs } from '../../../../utils/icons/fetch' -import { parseIconId } from '../../../../utils/icons/reactIcons' +import { fetchIconSvgs, fetchIconsIndex } from '../../../../utils/icons/fetch' export const prerender = false /** * GET /api/{version}/icons/[icon-name] * Returns actual SVG markup for the icon. - * Icon name format: {set}_{iconName} (e.g., fa_FaCircle, md_MdHome) + * Icon name: React component name (e.g., FaCircle, MdHome) */ export const GET: APIRoute = async ({ params, url }) => { - const { version, iconName: iconId } = params + const { version, iconName: reactName } = params if (!version) { return createJsonResponse( @@ -33,31 +32,28 @@ export const GET: APIRoute = async ({ params, url }) => { return createJsonResponse({ error: 'Failed to fetch API index' }, 500) } - if (!iconId) { + if (!reactName) { return createJsonResponse( { error: 'Icon name parameter is required' }, 400, ) } - const parsed = parseIconId(iconId) - if (!parsed) { + const icons = await fetchIconsIndex(url) + const icon = icons.find((i) => i.reactName === reactName) + if (!icon?.set) { return createJsonResponse( - { - error: 'Invalid icon name format', - expected: 'Use format {set}_{iconName} (e.g., fa_FaCircle, md_MdHome)', - }, - 400, + { error: `Icon '${reactName}' not found` }, + 404, ) } - const { setId, iconName } = parsed - const svgs = await fetchIconSvgs(url, setId) - const svg = svgs?.[iconName] ?? null + const svgs = await fetchIconSvgs(url, icon.set) + const svg = svgs?.[reactName] ?? null if (!svg) { return createJsonResponse( - { error: `Icon '${iconName}' not found in set '${setId}'` }, + { error: `Icon '${reactName}' not found in set '${icon.set}'` }, 404, ) } diff --git a/src/pages/api/index.ts b/src/pages/api/index.ts index 534022e..fbde11c 100644 --- a/src/pages/api/index.ts +++ b/src/pages/api/index.ts @@ -423,8 +423,8 @@ export const GET: APIRoute = async () => in: 'path', required: true, type: 'string', - description: 'Icon identifier in format {set}_{iconName} (e.g., fa_FaCircle, md_MdHome)', - example: 'fa_FaCircle', + description: 'Icon identifier: React component name (e.g., FaCircle, MdHome)', + example: 'FaCircle', }, ], returns: { diff --git a/src/pages/api/openapi.json.ts b/src/pages/api/openapi.json.ts index 081bc52..8e06b0a 100644 --- a/src/pages/api/openapi.json.ts +++ b/src/pages/api/openapi.json.ts @@ -165,7 +165,7 @@ export const GET: APIRoute = async ({ url }) => { get: { summary: 'Get icon SVG markup', description: - 'Returns actual SVG markup for the icon. Icon name format: {set}_{iconName} (e.g., fa_FaCircle, md_MdHome)', + 'Returns actual SVG markup for the icon. Icon name: React component name (e.g., FaCircle, MdHome)', operationId: 'getIconSvg', parameters: [ { @@ -180,8 +180,8 @@ export const GET: APIRoute = async ({ url }) => { in: 'path', required: true, schema: { type: 'string' }, - description: 'Icon identifier: {set}_{iconName}', - example: 'fa_FaCircle', + description: 'Icon identifier: React component name', + example: 'FaCircle', }, ], responses: { diff --git a/src/utils/icons/reactIcons.ts b/src/utils/icons/reactIcons.ts index 575727c..966d968 100644 --- a/src/utils/icons/reactIcons.ts +++ b/src/utils/icons/reactIcons.ts @@ -12,7 +12,7 @@ export interface IconMetadata { style: string usage: string unicode: string - /** Set id for SVG URL: /api/icons/{set}_{reactName} */ + /** Set id for SVG lookup (react name used in URL: /api/icons/{reactName}) */ set?: string } From b380631d1997b253ab6ba7fd437c0ea5d74434cb Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Mon, 16 Feb 2026 21:41:42 -0500 Subject: [PATCH 09/23] feat: Refactor icon utilities to use @patternfly/react-icons and update API endpoints accordingly --- .../[version]/icons/[iconName].test.ts | 26 +-- .../__tests__/[version]/icons/index.test.ts | 24 +-- src/pages/api/index.ts | 8 +- src/pages/api/openapi.json.ts | 8 +- src/pages/iconsIndex.json.ts | 3 +- src/pages/iconsSvgs/[setId].json.ts | 12 +- src/utils/icons/reactIcons.ts | 189 ++++++++---------- 7 files changed, 127 insertions(+), 143 deletions(-) diff --git a/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts b/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts index f912cab..d014199 100644 --- a/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts +++ b/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts @@ -10,12 +10,12 @@ const mockApiIndex = { const mockSvg = '' const mockIconSvgs: Record> = { - fa: { FaCircle: mockSvg }, + pf: { CircleIcon: mockSvg }, } const mockIconsIndex = { icons: [ - { name: 'circle', reactName: 'FaCircle', style: 'solid', usage: '', unicode: '', set: 'fa' }, + { name: 'circle', reactName: 'CircleIcon', style: 'pf', usage: '', unicode: '', set: 'pf' }, ], } @@ -48,8 +48,8 @@ it('returns SVG markup for valid icon', async () => { global.fetch = createFetchMock() const response = await GET({ - params: { version: 'v6', iconName: 'FaCircle' }, - url: new URL('http://localhost:4321/api/v6/icons/FaCircle'), + params: { version: 'v6', iconName: 'CircleIcon' }, + url: new URL('http://localhost:4321/api/v6/icons/CircleIcon'), } as any) const body = await response.text() @@ -67,14 +67,14 @@ it('returns 404 when icon is not found', async () => { global.fetch = createFetchMock() const response = await GET({ - params: { version: 'v6', iconName: 'FaNonExistent' }, - url: new URL('http://localhost:4321/api/v6/icons/FaNonExistent'), + params: { version: 'v6', iconName: 'NonExistentIcon' }, + url: new URL('http://localhost:4321/api/v6/icons/NonExistentIcon'), } as any) const body = await response.json() expect(response.status).toBe(404) expect(body).toHaveProperty('error') - expect(body.error).toContain('FaNonExistent') + expect(body.error).toContain('NonExistentIcon') expect(body.error).toContain('not found') jest.restoreAllMocks() @@ -117,8 +117,8 @@ it('returns 404 for nonexistent version', async () => { global.fetch = createFetchMock() const response = await GET({ - params: { version: 'v99', iconName: 'FaCircle' }, - url: new URL('http://localhost:4321/api/v99/icons/FaCircle'), + params: { version: 'v99', iconName: 'CircleIcon' }, + url: new URL('http://localhost:4321/api/v99/icons/CircleIcon'), } as any) const body = await response.json() @@ -134,8 +134,8 @@ it('returns 400 when version parameter is missing', async () => { global.fetch = createFetchMock() const response = await GET({ - params: { iconName: 'FaCircle' }, - url: new URL('http://localhost:4321/api/icons/FaCircle'), + params: { iconName: 'CircleIcon' }, + url: new URL('http://localhost:4321/api/icons/CircleIcon'), } as any) const body = await response.json() @@ -163,8 +163,8 @@ it('returns 500 when fetchApiIndex fails', async () => { }) as typeof fetch const response = await GET({ - params: { version: 'v6', iconName: 'FaCircle' }, - url: new URL('http://localhost:4321/api/v6/icons/FaCircle'), + params: { version: 'v6', iconName: 'CircleIcon' }, + url: new URL('http://localhost:4321/api/v6/icons/CircleIcon'), } as any) const body = await response.json() diff --git a/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts b/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts index f5b50d5..5b44790 100644 --- a/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts +++ b/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts @@ -10,27 +10,27 @@ const mockApiIndex = { const mockIcons = [ { name: 'circle', - reactName: 'FaCircle', - style: 'solid', - usage: "import { FaCircle } from 'react-icons/fa'", + reactName: 'CircleIcon', + style: 'pf', + usage: "import { CircleIcon } from '@patternfly/react-icons'", unicode: '', - set: 'fa', + set: 'pf', }, { name: 'home', - reactName: 'MdHome', - style: 'md', - usage: "import { MdHome } from 'react-icons/md'", + reactName: 'HomeIcon', + style: 'pf', + usage: "import { HomeIcon } from '@patternfly/react-icons'", unicode: '', - set: 'md', + set: 'pf', }, { name: 'circle-outline', - reactName: 'FaRegCircle', - style: 'regular', - usage: "import { FaRegCircle } from 'react-icons/fa'", + reactName: 'CircleOutlineIcon', + style: 'pf', + usage: "import { CircleOutlineIcon } from '@patternfly/react-icons'", unicode: '', - set: 'fa', + set: 'pf', }, ] diff --git a/src/pages/api/index.ts b/src/pages/api/index.ts index fbde11c..e0483bf 100644 --- a/src/pages/api/index.ts +++ b/src/pages/api/index.ts @@ -370,7 +370,7 @@ export const GET: APIRoute = async () => { path: '/api/{version}/icons', method: 'GET', - description: 'List all available icons with metadata from react-icons', + description: 'List all available icons with metadata from @patternfly/react-icons', parameters: [ { name: 'version', @@ -395,9 +395,9 @@ export const GET: APIRoute = async () => icons: [ { name: 'circle', - reactName: 'FaCircle', - style: 'solid', - usage: "import { FaCircle } from 'react-icons/fa'", + reactName: 'CircleIcon', + style: 'pf', + usage: "import { CircleIcon } from '@patternfly/react-icons'", unicode: '', }, ], diff --git a/src/pages/api/openapi.json.ts b/src/pages/api/openapi.json.ts index 8e06b0a..e71101f 100644 --- a/src/pages/api/openapi.json.ts +++ b/src/pages/api/openapi.json.ts @@ -108,7 +108,7 @@ export const GET: APIRoute = async ({ url }) => { get: { summary: 'List available icons', description: - 'Returns list of all available icons from react-icons with metadata. Use filter query param to filter by name.', + 'Returns list of all available icons from @patternfly/react-icons (dist/static) with metadata. Use filter query param to filter by name.', operationId: 'getIcons', parameters: [ { @@ -141,11 +141,11 @@ export const GET: APIRoute = async ({ url }) => { type: 'object', properties: { name: { type: 'string', example: 'circle' }, - reactName: { type: 'string', example: 'FaCircle' }, - style: { type: 'string', example: 'solid' }, + reactName: { type: 'string', example: 'CircleIcon' }, + style: { type: 'string', example: 'pf' }, usage: { type: 'string', - example: "import { FaCircle } from 'react-icons/fa'", + example: "import { CircleIcon } from '@patternfly/react-icons'", }, unicode: { type: 'string', example: '' }, }, diff --git a/src/pages/iconsIndex.json.ts b/src/pages/iconsIndex.json.ts index 52e724f..b10be0f 100644 --- a/src/pages/iconsIndex.json.ts +++ b/src/pages/iconsIndex.json.ts @@ -3,8 +3,7 @@ import { getAllIcons } from '../utils/icons/reactIcons' /** * Prerender at build time so this doesn't run in the Cloudflare Worker. - * getAllIcons() uses dynamic imports of react-icons which fail in Workers - * due to bundle size and Node.js compatibility. + * getAllIcons() reads from @patternfly/react-icons/dist/static (Node fs). */ export const prerender = true diff --git a/src/pages/iconsSvgs/[setId].json.ts b/src/pages/iconsSvgs/[setId].json.ts index 9c150cc..561beeb 100644 --- a/src/pages/iconsSvgs/[setId].json.ts +++ b/src/pages/iconsSvgs/[setId].json.ts @@ -1,17 +1,17 @@ import type { APIRoute, GetStaticPaths } from 'astro' -import { IconsManifest } from 'react-icons/lib' import { getIconSvgsForSet } from '../../utils/icons/reactIcons' +const PF_ICONS_SET_ID = 'pf' + /** * Prerender at build time so this doesn't run in the Cloudflare Worker. - * getIconSvgsForSet() uses dynamic imports of react-icons which fail in Workers. + * getIconSvgsForSet() reads from @patternfly/react-icons/dist/static. */ export const prerender = true -export const getStaticPaths: GetStaticPaths = async () => - IconsManifest.map((m) => ({ - params: { setId: m.id }, - })) +export const getStaticPaths: GetStaticPaths = async () => [ + { params: { setId: PF_ICONS_SET_ID } }, +] export const GET: APIRoute = async ({ params }) => { const { setId } = params diff --git a/src/utils/icons/reactIcons.ts b/src/utils/icons/reactIcons.ts index 966d968..e754dc9 100644 --- a/src/utils/icons/reactIcons.ts +++ b/src/utils/icons/reactIcons.ts @@ -1,10 +1,12 @@ /** - * Utilities for working with react-icons from the ESM package. - * Icons are loaded from node_modules/react-icons icon set folders. + * Utilities for working with @patternfly/react-icons. + * Icons are loaded from @patternfly/react-icons/dist/static (SVG files). */ -import { renderToStaticMarkup } from 'react-dom/server' -import React from 'react' -import { IconsManifest } from 'react-icons/lib' +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) export interface IconMetadata { name: string @@ -12,80 +14,68 @@ export interface IconMetadata { style: string usage: string unicode: string - /** Set id for SVG lookup (react name used in URL: /api/icons/{reactName}) */ + /** Set id for SVG lookup (used internally by API) */ set?: string } -const ICON_SET_IDS = IconsManifest.map((m) => m.id) - -/** Derive style from set id and react name (e.g., fa + FaRegCircle -> "regular") */ -function getStyle(setId: string, reactName: string): string { - if (setId === 'fa' || setId === 'fa6') { - if (reactName.startsWith('FaReg')) { - return 'regular' - } - if (reactName.startsWith('FaBrands')) { - return 'brands' - } - return 'solid' - } - return setId +const PF_ICONS_SET_ID = 'pf' + +/** Resolve path to @patternfly/react-icons/dist/static (from project root). */ +function getStaticIconsDir(): string { + const projectRoot = path.resolve(__dirname, '../..') + return path.join( + projectRoot, + 'node_modules', + '@patternfly', + 'react-icons', + 'dist', + 'static', + ) +} + +/** Convert kebab-case filename (no .svg) to PatternFly React component name (PascalCase + "Icon"). */ +function kebabToReactName(kebab: string): string { + const pascal = kebab + .split('-') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) + .join('') + return pascal + 'Icon' } -/** Convert PascalCase to kebab-case */ -function toKebabCase(str: string): string { - return str +/** Convert React component name back to kebab-case (without "Icon" suffix). */ +function reactNameToKebab(reactName: string): string { + const withoutIcon = reactName.replace(/Icon$/, '') + return withoutIcon .replace(/([a-z])([A-Z])/g, '$1-$2') .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') .toLowerCase() } -/** Derive base name from react name by removing set-specific prefixes */ -function getBaseName(setId: string, reactName: string): string { - let base = reactName - if (setId === 'fa' || setId === 'fa6') { - base = base.replace(/^Fa(Reg|Brands)?/, '') - } else if (setId === 'io' || setId === 'io5') { - base = base.replace(/^Io(5)?/, '') - } else if (setId === 'md') { - base = base.replace(/^Md/, '') - } else if (setId === 'hi' || setId === 'hi2') { - base = base.replace(/^Hi(2)?/, '') - } else { - const setPrefix = setId.charAt(0).toUpperCase() + setId.slice(1) - const prefix = new RegExp(`^${setPrefix}`, 'i') - base = base.replace(prefix, '') - } - return toKebabCase(base) || toKebabCase(reactName) -} - /** - * Get all icons from all sets with metadata. + * Get all icons from @patternfly/react-icons/dist/static with metadata. * Shape: { name, reactName, style, usage, unicode } */ export async function getAllIcons(): Promise { + const staticDir = getStaticIconsDir() + if (!fs.existsSync(staticDir)) { + return [] + } + + const files = fs.readdirSync(staticDir) + const svgFiles = files.filter((f) => f.endsWith('.svg')) const icons: IconMetadata[] = [] - for (const setId of ICON_SET_IDS) { - try { - const module = await import(`react-icons/${setId}`) - const iconNames = Object.keys(module).filter( - (k) => typeof module[k] === 'function' && k !== 'default', - ) - - for (const reactName of iconNames) { - icons.push({ - name: getBaseName(setId, reactName), - reactName, - style: getStyle(setId, reactName), - usage: `import { ${reactName} } from 'react-icons/${setId}'`, - unicode: '', - set: setId, - }) - } - } catch { - // Skip sets that fail to load - } + for (const file of svgFiles) { + const name = file.replace(/\.svg$/, '') + const reactName = kebabToReactName(name) + icons.push({ + name, + reactName, + style: PF_ICONS_SET_ID, + usage: `import { ${reactName} } from '@patternfly/react-icons'`, + unicode: '', + set: PF_ICONS_SET_ID, + }) } return icons @@ -110,74 +100,69 @@ export function filterIcons( } /** - * Get SVG markup for all icons in a set. Used at build time for prerendering. - * @param setId - Icon set id (e.g., "fa", "md") - * @returns Record of iconName -> SVG string + * Get SVG markup for all PatternFly icons (single set). + * Used at build time for prerendering. + * @param setId - Must be "pf" for PatternFly icons + * @returns Record of reactName -> SVG string */ export async function getIconSvgsForSet( setId: string, ): Promise> { - if (!ICON_SET_IDS.includes(setId)) { + if (setId !== PF_ICONS_SET_ID) { return {} } - try { - const module = await import(`react-icons/${setId}`) - const svgs: Record = {} - - for (const iconName of Object.keys(module)) { - const IconComponent = module[iconName] - if (typeof IconComponent !== 'function' || iconName === 'default') { - continue - } - - const element = React.createElement(IconComponent, { - size: '1em', - style: { verticalAlign: 'middle' }, - }) - svgs[iconName] = renderToStaticMarkup(element) - } - - return svgs - } catch { + const staticDir = getStaticIconsDir() + if (!fs.existsSync(staticDir)) { return {} } + + const files = fs.readdirSync(staticDir) + const svgFiles = files.filter((f) => f.endsWith('.svg')) + const svgs: Record = {} + + for (const file of svgFiles) { + const name = file.replace(/\.svg$/, '') + const reactName = kebabToReactName(name) + const filePath = path.join(staticDir, file) + const content = fs.readFileSync(filePath, 'utf-8') + svgs[reactName] = content.trim() + } + + return svgs } /** * Get SVG markup for a specific icon. - * @param setId - Icon set id (e.g., "fa", "md") - * @param iconName - Icon component name (e.g., "FaCircle") + * @param setId - Must be "pf" + * @param iconName - React component name (e.g., "AccessibleIconIcon") */ export async function getIconSvg( setId: string, iconName: string, ): Promise { - if (!ICON_SET_IDS.includes(setId)) { + if (setId !== PF_ICONS_SET_ID) { return null } - try { - const module = await import(`react-icons/${setId}`) - const IconComponent = module[iconName] - if (typeof IconComponent !== 'function') { - return null - } + const kebab = reactNameToKebab(iconName) + const fileName = `${kebab}.svg` + const staticDir = getStaticIconsDir() + const filePath = path.join(staticDir, fileName) - const element = React.createElement(IconComponent, { - size: '1em', - style: { verticalAlign: 'middle' }, - }) - return renderToStaticMarkup(element) - } catch { + if (!fs.existsSync(filePath)) { return null } + + return fs.readFileSync(filePath, 'utf-8').trim() } /** * Parse icon id "set_iconName" into { setId, iconName } */ -export function parseIconId(iconId: string): { setId: string; iconName: string } | null { +export function parseIconId( + iconId: string, +): { setId: string; iconName: string } | null { const underscoreIndex = iconId.indexOf('_') if (underscoreIndex <= 0) { return null From 2b8c40c633414af17917acc21067d780a3d8df72 Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Mon, 16 Feb 2026 21:42:34 -0500 Subject: [PATCH 10/23] updated to use prerelease to get static icons. --- package-lock.json | 77 +++++++++++++++++++++++++++++++++++++++++++++-- package.json | 2 +- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 390441d..cd74b9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@patternfly/react-code-editor": "^6.2.2", "@patternfly/react-core": "^6.0.0", "@patternfly/react-drag-drop": "^6.0.0", - "@patternfly/react-icons": "^6.0.0", + "@patternfly/react-icons": "6.5.0-prerelease.13", "@patternfly/react-styles": "^6.0.0", "@patternfly/react-table": "^6.0.0", "@patternfly/react-tokens": "^6.0.0", @@ -4085,6 +4085,16 @@ "react-dom": "^17 || ^18" } }, + "node_modules/@patternfly/react-code-editor/node_modules/@patternfly/react-icons": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.4.0.tgz", + "integrity": "sha512-SPjzatm73NUYv/BL6A/cjRA5sFQ15NkiyPAcT8gmklI7HY+ptd6/eg49uBDFmxTQcSwbb5ISW/R6wwCQBY2M+Q==", + "license": "MIT", + "peerDependencies": { + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + } + }, "node_modules/@patternfly/react-component-groups": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@patternfly/react-component-groups/-/react-component-groups-6.4.0.tgz", @@ -4104,6 +4114,17 @@ "react-dom": "^17 || ^18 || ^19" } }, + "node_modules/@patternfly/react-component-groups/node_modules/@patternfly/react-icons": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.4.0.tgz", + "integrity": "sha512-SPjzatm73NUYv/BL6A/cjRA5sFQ15NkiyPAcT8gmklI7HY+ptd6/eg49uBDFmxTQcSwbb5ISW/R6wwCQBY2M+Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + } + }, "node_modules/@patternfly/react-core": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.4.0.tgz", @@ -4122,6 +4143,16 @@ "react-dom": "^17 || ^18 || ^19" } }, + "node_modules/@patternfly/react-core/node_modules/@patternfly/react-icons": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.4.0.tgz", + "integrity": "sha512-SPjzatm73NUYv/BL6A/cjRA5sFQ15NkiyPAcT8gmklI7HY+ptd6/eg49uBDFmxTQcSwbb5ISW/R6wwCQBY2M+Q==", + "license": "MIT", + "peerDependencies": { + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + } + }, "node_modules/@patternfly/react-data-view": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@patternfly/react-data-view/-/react-data-view-6.4.0.tgz", @@ -4141,6 +4172,17 @@ "react-dom": "^17 || ^18 || ^19" } }, + "node_modules/@patternfly/react-data-view/node_modules/@patternfly/react-icons": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.4.0.tgz", + "integrity": "sha512-SPjzatm73NUYv/BL6A/cjRA5sFQ15NkiyPAcT8gmklI7HY+ptd6/eg49uBDFmxTQcSwbb5ISW/R6wwCQBY2M+Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + } + }, "node_modules/@patternfly/react-drag-drop": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/@patternfly/react-drag-drop/-/react-drag-drop-6.2.2.tgz", @@ -4160,7 +4202,7 @@ "react-dom": "^17 || ^18" } }, - "node_modules/@patternfly/react-icons": { + "node_modules/@patternfly/react-drag-drop/node_modules/@patternfly/react-icons": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.4.0.tgz", "integrity": "sha512-SPjzatm73NUYv/BL6A/cjRA5sFQ15NkiyPAcT8gmklI7HY+ptd6/eg49uBDFmxTQcSwbb5ISW/R6wwCQBY2M+Q==", @@ -4170,6 +4212,16 @@ "react-dom": "^17 || ^18 || ^19" } }, + "node_modules/@patternfly/react-icons": { + "version": "6.5.0-prerelease.13", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.5.0-prerelease.13.tgz", + "integrity": "sha512-40eSxfFytIAQkQ9EM6K4rqdDHIL9AwivqUbsYHZqJPNoipkL8RukxegPr7Lzvwt9kZ6OWGmTPtGySd4BkXzAqg==", + "license": "MIT", + "peerDependencies": { + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + } + }, "node_modules/@patternfly/react-styles": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.4.0.tgz", @@ -4194,6 +4246,16 @@ "react-dom": "^17 || ^18 || ^19" } }, + "node_modules/@patternfly/react-table/node_modules/@patternfly/react-icons": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.4.0.tgz", + "integrity": "sha512-SPjzatm73NUYv/BL6A/cjRA5sFQ15NkiyPAcT8gmklI7HY+ptd6/eg49uBDFmxTQcSwbb5ISW/R6wwCQBY2M+Q==", + "license": "MIT", + "peerDependencies": { + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + } + }, "node_modules/@patternfly/react-tokens": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.4.0.tgz", @@ -4215,6 +4277,17 @@ "react-dom": "^17 || ^18 || ^19" } }, + "node_modules/@patternfly/react-user-feedback/node_modules/@patternfly/react-icons": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.4.0.tgz", + "integrity": "sha512-SPjzatm73NUYv/BL6A/cjRA5sFQ15NkiyPAcT8gmklI7HY+ptd6/eg49uBDFmxTQcSwbb5ISW/R6wwCQBY2M+Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + } + }, "node_modules/@pkgr/core": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", diff --git a/package.json b/package.json index c1fe88b..9b695c5 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "@patternfly/react-code-editor": "^6.2.2", "@patternfly/react-core": "^6.0.0", "@patternfly/react-drag-drop": "^6.0.0", - "@patternfly/react-icons": "^6.0.0", + "@patternfly/react-icons": "6.5.0-prerelease.13", "@patternfly/react-styles": "^6.0.0", "@patternfly/react-table": "^6.0.0", "@patternfly/react-tokens": "^6.0.0", From 160618a24e66fdc97653d48de76a7e5d394369d2 Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Mon, 16 Feb 2026 21:59:28 -0500 Subject: [PATCH 11/23] Updates from review. --- astro.config.mjs | 6 ++++-- .../[version]/icons/[iconName].test.ts | 2 +- .../api/__tests__/[version]/icons/index.test.ts | 11 ----------- src/pages/api/[version]/icons/[iconName].ts | 6 +++--- src/pages/api/index.ts | 4 +--- src/pages/api/openapi.json.ts | 2 -- src/utils/icons/reactIcons.ts | 17 +++-------------- 7 files changed, 12 insertions(+), 36 deletions(-) diff --git a/astro.config.mjs b/astro.config.mjs index 7d5c233..21521de 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -11,7 +11,7 @@ export default defineConfig({ vite: { ssr: { noExternal: ["@patternfly/*", "react-dropzone"], - external: ["node:fs", "node:path", "fs/promises", "path"] + external: ["fs", "node:fs", "node:path", "path", "fs/promises"] }, server: { fs: { @@ -19,5 +19,7 @@ export default defineConfig({ } }, }, - adapter: cloudflare() + adapter: cloudflare({ + imageService: 'compile' + }) }); \ No newline at end of file diff --git a/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts b/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts index d014199..1ca8a10 100644 --- a/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts +++ b/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts @@ -15,7 +15,7 @@ const mockIconSvgs: Record> = { const mockIconsIndex = { icons: [ - { name: 'circle', reactName: 'CircleIcon', style: 'pf', usage: '', unicode: '', set: 'pf' }, + { name: 'circle', reactName: 'CircleIcon', usage: '' }, ], } diff --git a/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts b/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts index 5b44790..8a73faa 100644 --- a/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts +++ b/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts @@ -11,26 +11,17 @@ const mockIcons = [ { name: 'circle', reactName: 'CircleIcon', - style: 'pf', usage: "import { CircleIcon } from '@patternfly/react-icons'", - unicode: '', - set: 'pf', }, { name: 'home', reactName: 'HomeIcon', - style: 'pf', usage: "import { HomeIcon } from '@patternfly/react-icons'", - unicode: '', - set: 'pf', }, { name: 'circle-outline', reactName: 'CircleOutlineIcon', - style: 'pf', usage: "import { CircleOutlineIcon } from '@patternfly/react-icons'", - unicode: '', - set: 'pf', }, ] @@ -79,9 +70,7 @@ it('returns all icons with metadata', async () => { expect(body.total).toBe(3) expect(body.icons[0]).toHaveProperty('name') expect(body.icons[0]).toHaveProperty('reactName') - expect(body.icons[0]).toHaveProperty('style') expect(body.icons[0]).toHaveProperty('usage') - expect(body.icons[0]).toHaveProperty('unicode') jest.restoreAllMocks() }) diff --git a/src/pages/api/[version]/icons/[iconName].ts b/src/pages/api/[version]/icons/[iconName].ts index c377a28..5d26b07 100644 --- a/src/pages/api/[version]/icons/[iconName].ts +++ b/src/pages/api/[version]/icons/[iconName].ts @@ -41,19 +41,19 @@ export const GET: APIRoute = async ({ params, url }) => { const icons = await fetchIconsIndex(url) const icon = icons.find((i) => i.reactName === reactName) - if (!icon?.set) { + if (!icon) { return createJsonResponse( { error: `Icon '${reactName}' not found` }, 404, ) } - const svgs = await fetchIconSvgs(url, icon.set) + const svgs = await fetchIconSvgs(url, 'pf') const svg = svgs?.[reactName] ?? null if (!svg) { return createJsonResponse( - { error: `Icon '${reactName}' not found in set '${icon.set}'` }, + { error: `Icon '${reactName}' not found` }, 404, ) } diff --git a/src/pages/api/index.ts b/src/pages/api/index.ts index e0483bf..db0d70e 100644 --- a/src/pages/api/index.ts +++ b/src/pages/api/index.ts @@ -390,15 +390,13 @@ export const GET: APIRoute = async () => ], returns: { type: 'object', - description: 'List of icons with name, reactName, style, usage, unicode', + description: 'List of icons with name, reactName, usage', example: { icons: [ { name: 'circle', reactName: 'CircleIcon', - style: 'pf', usage: "import { CircleIcon } from '@patternfly/react-icons'", - unicode: '', }, ], total: 1, diff --git a/src/pages/api/openapi.json.ts b/src/pages/api/openapi.json.ts index e71101f..68a33cd 100644 --- a/src/pages/api/openapi.json.ts +++ b/src/pages/api/openapi.json.ts @@ -142,12 +142,10 @@ export const GET: APIRoute = async ({ url }) => { properties: { name: { type: 'string', example: 'circle' }, reactName: { type: 'string', example: 'CircleIcon' }, - style: { type: 'string', example: 'pf' }, usage: { type: 'string', example: "import { CircleIcon } from '@patternfly/react-icons'", }, - unicode: { type: 'string', example: '' }, }, }, }, diff --git a/src/utils/icons/reactIcons.ts b/src/utils/icons/reactIcons.ts index e754dc9..000518f 100644 --- a/src/utils/icons/reactIcons.ts +++ b/src/utils/icons/reactIcons.ts @@ -4,27 +4,19 @@ */ import fs from 'fs' import path from 'path' -import { fileURLToPath } from 'url' - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) export interface IconMetadata { name: string reactName: string - style: string usage: string - unicode: string - /** Set id for SVG lookup (used internally by API) */ - set?: string } const PF_ICONS_SET_ID = 'pf' -/** Resolve path to @patternfly/react-icons/dist/static (from project root). */ +/** Resolve path to @patternfly/react-icons/dist/static. Uses cwd so it works in dev and build. */ function getStaticIconsDir(): string { - const projectRoot = path.resolve(__dirname, '../..') return path.join( - projectRoot, + process.cwd(), 'node_modules', '@patternfly', 'react-icons', @@ -53,7 +45,7 @@ function reactNameToKebab(reactName: string): string { /** * Get all icons from @patternfly/react-icons/dist/static with metadata. - * Shape: { name, reactName, style, usage, unicode } + * Shape: { name, reactName, usage } */ export async function getAllIcons(): Promise { const staticDir = getStaticIconsDir() @@ -71,10 +63,7 @@ export async function getAllIcons(): Promise { icons.push({ name, reactName, - style: PF_ICONS_SET_ID, usage: `import { ${reactName} } from '@patternfly/react-icons'`, - unicode: '', - set: PF_ICONS_SET_ID, }) } From 9c287db55469172d6cdb4e77bf881f1d5df302ef Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Tue, 17 Feb 2026 16:28:30 -0500 Subject: [PATCH 12/23] Updated with review comments. --- .../[version]/icons/[iconName].test.ts | 2 +- src/pages/api/[version]/icons/[iconName].ts | 2 +- src/pages/api/[version]/icons/[setId].json.ts | 42 +++++++++++++++++ src/pages/iconsSvgs/[setId].json.ts | 46 ------------------- src/utils/apiIndex/get.ts | 39 ++++++++++++++++ src/utils/icons/fetch.ts | 12 +++-- 6 files changed, 91 insertions(+), 52 deletions(-) create mode 100644 src/pages/api/[version]/icons/[setId].json.ts delete mode 100644 src/pages/iconsSvgs/[setId].json.ts diff --git a/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts b/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts index 1ca8a10..c9c2840 100644 --- a/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts +++ b/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts @@ -28,7 +28,7 @@ function createFetchMock(): typeof fetch { json: () => Promise.resolve(mockIconsIndex), } as Response) } - const match = url.match(/\/iconsSvgs\/([^/]+)\.json/) + const match = url.match(/\/api\/[^/]+\/icons\/([^/]+)\.json/) if (match) { const setId = match[1] const svgs = mockIconSvgs[setId] ?? {} diff --git a/src/pages/api/[version]/icons/[iconName].ts b/src/pages/api/[version]/icons/[iconName].ts index 5d26b07..cc16c45 100644 --- a/src/pages/api/[version]/icons/[iconName].ts +++ b/src/pages/api/[version]/icons/[iconName].ts @@ -48,7 +48,7 @@ export const GET: APIRoute = async ({ params, url }) => { ) } - const svgs = await fetchIconSvgs(url, 'pf') + const svgs = await fetchIconSvgs(url, version, 'pf', assetsFetch) const svg = svgs?.[reactName] ?? null if (!svg) { diff --git a/src/pages/api/[version]/icons/[setId].json.ts b/src/pages/api/[version]/icons/[setId].json.ts new file mode 100644 index 0000000..e375d96 --- /dev/null +++ b/src/pages/api/[version]/icons/[setId].json.ts @@ -0,0 +1,42 @@ +import type { APIRoute, GetStaticPaths } from 'astro' +import { getVersionsFromIndexFile } from '../../../../utils/apiIndex/get' +import { createJsonResponse } from '../../../../utils/apiHelpers' +import { getIconSvgsForSet } from '../../../../utils/icons/reactIcons' + +/** + * Prerender at build time so this doesn't run in the Cloudflare Worker. + * getIconSvgsForSet() reads from @patternfly/react-icons/dist/static (Node fs). + * Serves JSON of all icon SVGs for a set (e.g. /api/v5/icons/pf.json). + */ +export const prerender = true + +export const getStaticPaths: GetStaticPaths = async () => { + const versions = await getVersionsFromIndexFile() + return versions.flatMap((version) => [ + { params: { version, setId: 'pf' } }, + ]) +} + +export const GET: APIRoute = async ({ params }) => { + const { version, setId } = params + if (!version) { + return createJsonResponse( + { error: 'Version parameter is required' }, + 400, + ) + } + if (!setId) { + return createJsonResponse({ error: 'Set ID is required' }, 400) + } + + try { + const svgs = await getIconSvgsForSet(setId) + return createJsonResponse(svgs) + } catch (error) { + const details = error instanceof Error ? error.message : String(error) + return createJsonResponse( + { error: 'Failed to load icon SVGs', details }, + 500, + ) + } +} diff --git a/src/pages/iconsSvgs/[setId].json.ts b/src/pages/iconsSvgs/[setId].json.ts deleted file mode 100644 index 561beeb..0000000 --- a/src/pages/iconsSvgs/[setId].json.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { APIRoute, GetStaticPaths } from 'astro' -import { getIconSvgsForSet } from '../../utils/icons/reactIcons' - -const PF_ICONS_SET_ID = 'pf' - -/** - * Prerender at build time so this doesn't run in the Cloudflare Worker. - * getIconSvgsForSet() reads from @patternfly/react-icons/dist/static. - */ -export const prerender = true - -export const getStaticPaths: GetStaticPaths = async () => [ - { params: { setId: PF_ICONS_SET_ID } }, -] - -export const GET: APIRoute = async ({ params }) => { - const { setId } = params - - if (!setId) { - return new Response( - JSON.stringify({ error: 'Set ID is required' }), - { status: 400, headers: { 'Content-Type': 'application/json' } } - ) - } - - try { - const svgs = await getIconSvgsForSet(setId) - return new Response(JSON.stringify(svgs), { - status: 200, - headers: { - 'Content-Type': 'application/json', - }, - }) - } catch (error) { - return new Response( - JSON.stringify({ - error: 'Failed to load icon SVGs', - details: String(error), - }), - { - status: 500, - headers: { 'Content-Type': 'application/json' }, - } - ) - } -} diff --git a/src/utils/apiIndex/get.ts b/src/utils/apiIndex/get.ts index 8428644..52cf9b2 100644 --- a/src/utils/apiIndex/get.ts +++ b/src/utils/apiIndex/get.ts @@ -52,6 +52,45 @@ export async function getApiIndex(): Promise { } } +/** + * Reads only the versions array from the API index file. + * Use this when the full index may not be generated yet or has a minimal structure + * (e.g. during getStaticPaths for icon routes). Does not validate examples/css. + * + * @returns Promise resolving to array of version strings (e.g., ['v5', 'v6']) + * @throws Error if the index file is missing or has no valid "versions" array + */ +export async function getVersionsFromIndexFile(): Promise { + const outputDir = await getOutputDir() + const indexPath = join(outputDir, 'apiIndex.json') + + try { + const content = await readFile(indexPath, 'utf-8') + const parsed = JSON.parse(content) + + if (!parsed.versions || !Array.isArray(parsed.versions)) { + throw new Error( + `Invalid API index structure at ${indexPath}: missing or invalid "versions" array`, + ) + } + + return parsed.versions + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + throw new Error( + `API index file not found at ${indexPath}. ` + + 'Please run the build process to generate the index.', + ) + } + if (error instanceof SyntaxError) { + throw new Error( + `API index contains invalid JSON at ${indexPath}. Please rebuild to regenerate the index file.`, + ) + } + throw error + } +} + /** * Gets all available documentation versions * diff --git a/src/utils/icons/fetch.ts b/src/utils/icons/fetch.ts index 5963772..beafe2f 100644 --- a/src/utils/icons/fetch.ts +++ b/src/utils/icons/fetch.ts @@ -32,15 +32,19 @@ export async function fetchIconsIndex(url: URL): Promise { * uses dynamic imports that fail in Cloudflare Workers. * * @param url - The URL object from the API route context - * @param setId - Icon set id (e.g., "fa", "ci") - * @returns Promise resolving to Record of iconName -> SVG string, or null if fetch fails + * @param version - Docs version (e.g. "v5") + * @param setId - Icon set id (e.g. "pf") + * @param assetsFetch - Optional; when provided (e.g. locals.runtime.env.ASSETS.fetch on Cloudflare), use it to fetch the asset */ export async function fetchIconSvgs( url: URL, + version: string, setId: string, ): Promise | null> { - const iconsSvgsUrl = new URL(`/iconsSvgs/${setId}.json`, url.origin) - const response = await fetch(iconsSvgsUrl) + const iconsSvgsUrl = new URL(`/api/${version}/icons/${setId}.json`, url.origin) + const response = assetsFetch + ? await assetsFetch(new Request(iconsSvgsUrl)) + : await fetch(iconsSvgsUrl) if (!response.ok) { return null From ce4583dce390c3d386cb0582608bba151b02dad2 Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Wed, 18 Feb 2026 16:22:20 -0500 Subject: [PATCH 13/23] Fix issue with fs on cloudflare for the /icons endpoint. --- .../icons/{[setId].json.ts => [iconSet].ts} | 12 +++---- src/pages/api/[version]/icons/index.ts | 20 +++++++++++- src/utils/icons/fetch.ts | 4 +-- src/utils/icons/icons.ts | 7 ++++ src/utils/icons/reactIcons.ts | 32 ++----------------- wrangler.jsonc | 2 +- 6 files changed, 38 insertions(+), 39 deletions(-) rename src/pages/api/[version]/icons/{[setId].json.ts => [iconSet].ts} (78%) create mode 100644 src/utils/icons/icons.ts diff --git a/src/pages/api/[version]/icons/[setId].json.ts b/src/pages/api/[version]/icons/[iconSet].ts similarity index 78% rename from src/pages/api/[version]/icons/[setId].json.ts rename to src/pages/api/[version]/icons/[iconSet].ts index e375d96..3a2e186 100644 --- a/src/pages/api/[version]/icons/[setId].json.ts +++ b/src/pages/api/[version]/icons/[iconSet].ts @@ -6,31 +6,31 @@ import { getIconSvgsForSet } from '../../../../utils/icons/reactIcons' /** * Prerender at build time so this doesn't run in the Cloudflare Worker. * getIconSvgsForSet() reads from @patternfly/react-icons/dist/static (Node fs). - * Serves JSON of all icon SVGs for a set (e.g. /api/v5/icons/pf.json). + * Serves JSON of all icon SVGs for a set (e.g. /api/v6/icons/pf). */ export const prerender = true export const getStaticPaths: GetStaticPaths = async () => { const versions = await getVersionsFromIndexFile() return versions.flatMap((version) => [ - { params: { version, setId: 'pf' } }, + { params: { version, iconSet: 'pf' } }, ]) } export const GET: APIRoute = async ({ params }) => { - const { version, setId } = params + const { version, iconSet } = params if (!version) { return createJsonResponse( { error: 'Version parameter is required' }, 400, ) } - if (!setId) { - return createJsonResponse({ error: 'Set ID is required' }, 400) + if (!iconSet) { + return createJsonResponse({ error: 'Icon set is required' }, 400) } try { - const svgs = await getIconSvgsForSet(setId) + const svgs = await getIconSvgsForSet(iconSet) return createJsonResponse(svgs) } catch (error) { const details = error instanceof Error ? error.message : String(error) diff --git a/src/pages/api/[version]/icons/index.ts b/src/pages/api/[version]/icons/index.ts index 93105f5..85be202 100644 --- a/src/pages/api/[version]/icons/index.ts +++ b/src/pages/api/[version]/icons/index.ts @@ -2,10 +2,28 @@ import type { APIRoute } from 'astro' import { createJsonResponse } from '../../../../utils/apiHelpers' import { fetchApiIndex } from '../../../../utils/apiIndex/fetch' import { fetchIconsIndex } from '../../../../utils/icons/fetch' -import { filterIcons } from '../../../../utils/icons/reactIcons' +import { IconMetadata } from '../../../../utils/icons/icons' export const prerender = false +/** + * Filter icons by search term (case-insensitive match on name or reactName) + */ +export function filterIcons( + icons: IconMetadata[], + filter: string, +): IconMetadata[] { + if (!filter || !filter.trim()) { + return icons + } + const term = filter.toLowerCase().trim() + return icons.filter( + (icon) => + icon.name.toLowerCase().includes(term) || + icon.reactName.toLowerCase().includes(term), + ) +} + /** * GET /api/{version}/icons * Returns list of all available icons with metadata. diff --git a/src/utils/icons/fetch.ts b/src/utils/icons/fetch.ts index beafe2f..7f13f2e 100644 --- a/src/utils/icons/fetch.ts +++ b/src/utils/icons/fetch.ts @@ -1,4 +1,4 @@ -import type { IconMetadata } from './reactIcons' +import type { IconMetadata } from './icons' export interface IconsIndex { icons: IconMetadata[] @@ -41,7 +41,7 @@ export async function fetchIconSvgs( version: string, setId: string, ): Promise | null> { - const iconsSvgsUrl = new URL(`/api/${version}/icons/${setId}.json`, url.origin) + const iconsSvgsUrl = new URL(`/api/${version}/icons/${setId}`, url.origin) const response = assetsFetch ? await assetsFetch(new Request(iconsSvgsUrl)) : await fetch(iconsSvgsUrl) diff --git a/src/utils/icons/icons.ts b/src/utils/icons/icons.ts new file mode 100644 index 0000000..c0b3069 --- /dev/null +++ b/src/utils/icons/icons.ts @@ -0,0 +1,7 @@ +export interface IconMetadata { + name: string + reactName: string + usage: string + } + + export const PF_ICONS_SET_ID = 'pf' \ No newline at end of file diff --git a/src/utils/icons/reactIcons.ts b/src/utils/icons/reactIcons.ts index 000518f..5f8e87c 100644 --- a/src/utils/icons/reactIcons.ts +++ b/src/utils/icons/reactIcons.ts @@ -2,17 +2,9 @@ * Utilities for working with @patternfly/react-icons. * Icons are loaded from @patternfly/react-icons/dist/static (SVG files). */ -import fs from 'fs' -import path from 'path' - -export interface IconMetadata { - name: string - reactName: string - usage: string -} - -const PF_ICONS_SET_ID = 'pf' - +import fs from 'node:fs' +import path from 'node:path' +import { IconMetadata, PF_ICONS_SET_ID } from './icons' /** Resolve path to @patternfly/react-icons/dist/static. Uses cwd so it works in dev and build. */ function getStaticIconsDir(): string { return path.join( @@ -70,24 +62,6 @@ export async function getAllIcons(): Promise { return icons } -/** - * Filter icons by search term (case-insensitive match on name or reactName) - */ -export function filterIcons( - icons: IconMetadata[], - filter: string, -): IconMetadata[] { - if (!filter || !filter.trim()) { - return icons - } - const term = filter.toLowerCase().trim() - return icons.filter( - (icon) => - icon.name.toLowerCase().includes(term) || - icon.reactName.toLowerCase().includes(term), - ) -} - /** * Get SVG markup for all PatternFly icons (single set). * Used at build time for prerendering. diff --git a/wrangler.jsonc b/wrangler.jsonc index f8e5f43..c86b9df 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -1,7 +1,7 @@ { "$schema": "node_modules/wrangler/config-schema.json", "name": "patternfly-docs-core", - "compatibility_date": "2025-06-17", + "compatibility_date": "2026-02-18", "compatibility_flags": ["nodejs_compat"], "observability": { "enabled": true From 695fcf7a6fffc42e46dc955f662adfbb23286b89 Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Fri, 17 Apr 2026 10:10:22 -0400 Subject: [PATCH 14/23] fixed borken test, and failing link. --- src/pages/api/[version]/icons/[iconName].ts | 9 +++++++-- src/utils/icons/fetch.ts | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pages/api/[version]/icons/[iconName].ts b/src/pages/api/[version]/icons/[iconName].ts index cc16c45..81b8b4e 100644 --- a/src/pages/api/[version]/icons/[iconName].ts +++ b/src/pages/api/[version]/icons/[iconName].ts @@ -13,7 +13,7 @@ export const prerender = false * Returns actual SVG markup for the icon. * Icon name: React component name (e.g., FaCircle, MdHome) */ -export const GET: APIRoute = async ({ params, url }) => { +export const GET: APIRoute = async ({ params, url, locals }) => { const { version, iconName: reactName } = params if (!version) { @@ -48,7 +48,12 @@ export const GET: APIRoute = async ({ params, url }) => { ) } - const svgs = await fetchIconSvgs(url, version, 'pf', assetsFetch) + const svgs = await fetchIconSvgs( + url, + version, + 'pf', + (locals as any)?.runtime?.env?.ASSETS?.fetch, + ) const svg = svgs?.[reactName] ?? null if (!svg) { diff --git a/src/utils/icons/fetch.ts b/src/utils/icons/fetch.ts index 7f13f2e..fda2858 100644 --- a/src/utils/icons/fetch.ts +++ b/src/utils/icons/fetch.ts @@ -40,8 +40,9 @@ export async function fetchIconSvgs( url: URL, version: string, setId: string, + assetsFetch?: (input: Request) => Promise, ): Promise | null> { - const iconsSvgsUrl = new URL(`/api/${version}/icons/${setId}`, url.origin) + const iconsSvgsUrl = new URL(`/api/${version}/icons/${setId}.json`, url.origin) const response = assetsFetch ? await assetsFetch(new Request(iconsSvgsUrl)) : await fetch(iconsSvgsUrl) From 9123ae74921b3d1899f709c8b8ea30f70ca6fa2e Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Wed, 3 Jun 2026 16:27:02 -0400 Subject: [PATCH 15/23] chore: fixed issue with end point that broke when switching to react name --- .../pages/api/__tests__/[version]/icons/[iconName].test.ts | 2 +- src/utils/apiIndex/generate.ts | 4 +++- src/utils/icons/fetch.ts | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts b/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts index c9c2840..1aa71a5 100644 --- a/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts +++ b/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts @@ -28,7 +28,7 @@ function createFetchMock(): typeof fetch { json: () => Promise.resolve(mockIconsIndex), } as Response) } - const match = url.match(/\/api\/[^/]+\/icons\/([^/]+)\.json/) + const match = url.match(/\/api\/[^/]+\/icons\/([^/]+)$/) if (match) { const setId = match[1] const svgs = mockIconSvgs[setId] ?? {} diff --git a/src/utils/apiIndex/generate.ts b/src/utils/apiIndex/generate.ts index 7f88688..e50493f 100644 --- a/src/utils/apiIndex/generate.ts +++ b/src/utils/apiIndex/generate.ts @@ -213,7 +213,9 @@ export async function generateApiIndex(): Promise { // Convert sets to sorted arrays // Sections are now always flat strings (subsections are in page names) - index.sections[version] = Array.from(sections).sort() + // Add hardcoded API endpoints (icons, tokens) to the sections list + const allSections = new Set([...sections, 'icons', 'tokens']) + index.sections[version] = Array.from(allSections).sort() Object.entries(sectionPages).forEach(([key, pages]) => { index.pages[key] = Array.from(pages).sort() diff --git a/src/utils/icons/fetch.ts b/src/utils/icons/fetch.ts index fda2858..9845af1 100644 --- a/src/utils/icons/fetch.ts +++ b/src/utils/icons/fetch.ts @@ -42,7 +42,7 @@ export async function fetchIconSvgs( setId: string, assetsFetch?: (input: Request) => Promise, ): Promise | null> { - const iconsSvgsUrl = new URL(`/api/${version}/icons/${setId}.json`, url.origin) + const iconsSvgsUrl = new URL(`/api/${version}/icons/${setId}`, url.origin) const response = assetsFetch ? await assetsFetch(new Request(iconsSvgsUrl)) : await fetch(iconsSvgsUrl) From 8702c57cdfb04e4e6b4969309d0efba492527fbf Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Mon, 8 Jun 2026 13:24:22 -0400 Subject: [PATCH 16/23] fix: resolve route collision between iconSet and iconName endpoints Moved the prerendered iconSet endpoint from /api/[version]/icons/[iconSet] to /api/[version]/iconsets/[iconSet] to prevent route collision with the dynamic iconName endpoint at /api/[version]/icons/[iconName]. The collision was causing Cloudflare Workers to route requests like /api/v6/icons/pf to the [iconName].ts handler instead of the prerendered [iconSet].ts file, resulting in 404 errors. This fix ensures: - /api/v6/iconsets/pf serves prerendered SVG data for all icons - /api/v6/icons/AddCircleOIcon serves individual icon SVGs - No ambiguity in routing on Cloudflare Workers Co-Authored-By: Claude Sonnet 4.5 --- .../pages/api/__tests__/[version]/icons/[iconName].test.ts | 2 +- src/pages/api/[version]/{icons => iconsets}/[iconSet].ts | 2 +- src/utils/icons/fetch.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/pages/api/[version]/{icons => iconsets}/[iconSet].ts (94%) diff --git a/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts b/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts index 1aa71a5..6c45c6b 100644 --- a/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts +++ b/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts @@ -28,7 +28,7 @@ function createFetchMock(): typeof fetch { json: () => Promise.resolve(mockIconsIndex), } as Response) } - const match = url.match(/\/api\/[^/]+\/icons\/([^/]+)$/) + const match = url.match(/\/api\/[^/]+\/iconsets\/([^/]+)$/) if (match) { const setId = match[1] const svgs = mockIconSvgs[setId] ?? {} diff --git a/src/pages/api/[version]/icons/[iconSet].ts b/src/pages/api/[version]/iconsets/[iconSet].ts similarity index 94% rename from src/pages/api/[version]/icons/[iconSet].ts rename to src/pages/api/[version]/iconsets/[iconSet].ts index 3a2e186..4c89395 100644 --- a/src/pages/api/[version]/icons/[iconSet].ts +++ b/src/pages/api/[version]/iconsets/[iconSet].ts @@ -6,7 +6,7 @@ import { getIconSvgsForSet } from '../../../../utils/icons/reactIcons' /** * Prerender at build time so this doesn't run in the Cloudflare Worker. * getIconSvgsForSet() reads from @patternfly/react-icons/dist/static (Node fs). - * Serves JSON of all icon SVGs for a set (e.g. /api/v6/icons/pf). + * Serves JSON of all icon SVGs for a set (e.g. /api/v6/iconsets/pf). */ export const prerender = true diff --git a/src/utils/icons/fetch.ts b/src/utils/icons/fetch.ts index 9845af1..c4ecca9 100644 --- a/src/utils/icons/fetch.ts +++ b/src/utils/icons/fetch.ts @@ -42,7 +42,7 @@ export async function fetchIconSvgs( setId: string, assetsFetch?: (input: Request) => Promise, ): Promise | null> { - const iconsSvgsUrl = new URL(`/api/${version}/icons/${setId}`, url.origin) + const iconsSvgsUrl = new URL(`/api/${version}/iconsets/${setId}`, url.origin) const response = assetsFetch ? await assetsFetch(new Request(iconsSvgsUrl)) : await fetch(iconsSvgsUrl) From 1f2d9bc841bc331a9ca2ac4d271062b30c6f39fa Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Tue, 9 Jun 2026 17:27:30 -0400 Subject: [PATCH 17/23] Fix icons API /[iconName] endpoints when deployed to Cloudflare Workers --- src/pages/api/[version]/icons/[iconName].ts | 47 ++++++++++++--------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/src/pages/api/[version]/icons/[iconName].ts b/src/pages/api/[version]/icons/[iconName].ts index 81b8b4e..5548455 100644 --- a/src/pages/api/[version]/icons/[iconName].ts +++ b/src/pages/api/[version]/icons/[iconName].ts @@ -39,29 +39,38 @@ export const GET: APIRoute = async ({ params, url, locals }) => { ) } - const icons = await fetchIconsIndex(url) - const icon = icons.find((i) => i.reactName === reactName) - if (!icon) { - return createJsonResponse( - { error: `Icon '${reactName}' not found` }, - 404, + try { + const icons = await fetchIconsIndex(url) + const icon = icons.find((i) => i.reactName === reactName) + if (!icon) { + return createJsonResponse( + { error: `Icon '${reactName}' not found` }, + 404, + ) + } + + const assets = (locals as any)?.runtime?.env?.ASSETS + const svgs = await fetchIconSvgs( + url, + version, + 'pf', + assets?.fetch?.bind(assets), ) - } + const svg = svgs?.[reactName] ?? null - const svgs = await fetchIconSvgs( - url, - version, - 'pf', - (locals as any)?.runtime?.env?.ASSETS?.fetch, - ) - const svg = svgs?.[reactName] ?? null + if (!svg) { + return createJsonResponse( + { error: `Icon '${reactName}' not found` }, + 404, + ) + } - if (!svg) { + return createSvgResponse(svg) + } catch (error) { + const details = error instanceof Error ? error.message : String(error) return createJsonResponse( - { error: `Icon '${reactName}' not found` }, - 404, + { error: 'Failed to load icon data', details }, + 500, ) } - - return createSvgResponse(svg) } From e5434ab71a3e327a6914657fa73f3b7c654c6216 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Tue, 9 Jun 2026 17:59:09 -0400 Subject: [PATCH 18/23] Fix build order issue causing /[iconName] api to fail when deployed --- src/pages/api/[version]/iconsets/[iconSet].ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/api/[version]/iconsets/[iconSet].ts b/src/pages/api/[version]/iconsets/[iconSet].ts index 4c89395..74a80b2 100644 --- a/src/pages/api/[version]/iconsets/[iconSet].ts +++ b/src/pages/api/[version]/iconsets/[iconSet].ts @@ -1,7 +1,7 @@ import type { APIRoute, GetStaticPaths } from 'astro' -import { getVersionsFromIndexFile } from '../../../../utils/apiIndex/get' import { createJsonResponse } from '../../../../utils/apiHelpers' import { getIconSvgsForSet } from '../../../../utils/icons/reactIcons' +import { content } from '../../../../content' /** * Prerender at build time so this doesn't run in the Cloudflare Worker. @@ -11,7 +11,7 @@ import { getIconSvgsForSet } from '../../../../utils/icons/reactIcons' export const prerender = true export const getStaticPaths: GetStaticPaths = async () => { - const versions = await getVersionsFromIndexFile() + const versions = [...new Set(content.map((entry: any) => entry.version))] return versions.flatMap((version) => [ { params: { version, iconSet: 'pf' } }, ]) From 3a6c91fe82eae57eac5e50063518133effbd47fd Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Tue, 9 Jun 2026 18:06:17 -0400 Subject: [PATCH 19/23] Revert "Fix icons API /[iconName] endpoints when deployed to Cloudflare Workers" This reverts commit 1f2d9bc841bc331a9ca2ac4d271062b30c6f39fa. --- src/pages/api/[version]/icons/[iconName].ts | 47 +++++++++------------ 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/src/pages/api/[version]/icons/[iconName].ts b/src/pages/api/[version]/icons/[iconName].ts index 5548455..81b8b4e 100644 --- a/src/pages/api/[version]/icons/[iconName].ts +++ b/src/pages/api/[version]/icons/[iconName].ts @@ -39,38 +39,29 @@ export const GET: APIRoute = async ({ params, url, locals }) => { ) } - try { - const icons = await fetchIconsIndex(url) - const icon = icons.find((i) => i.reactName === reactName) - if (!icon) { - return createJsonResponse( - { error: `Icon '${reactName}' not found` }, - 404, - ) - } - - const assets = (locals as any)?.runtime?.env?.ASSETS - const svgs = await fetchIconSvgs( - url, - version, - 'pf', - assets?.fetch?.bind(assets), + const icons = await fetchIconsIndex(url) + const icon = icons.find((i) => i.reactName === reactName) + if (!icon) { + return createJsonResponse( + { error: `Icon '${reactName}' not found` }, + 404, ) - const svg = svgs?.[reactName] ?? null + } - if (!svg) { - return createJsonResponse( - { error: `Icon '${reactName}' not found` }, - 404, - ) - } + const svgs = await fetchIconSvgs( + url, + version, + 'pf', + (locals as any)?.runtime?.env?.ASSETS?.fetch, + ) + const svg = svgs?.[reactName] ?? null - return createSvgResponse(svg) - } catch (error) { - const details = error instanceof Error ? error.message : String(error) + if (!svg) { return createJsonResponse( - { error: 'Failed to load icon data', details }, - 500, + { error: `Icon '${reactName}' not found` }, + 404, ) } + + return createSvgResponse(svg) } From 615255ea277b2d5442160f18e5dc986577b413ad Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Wed, 10 Jun 2026 10:38:34 -0400 Subject: [PATCH 20/23] Update deps to 6.5 releases --- package-lock.json | 247 ++++++++++++++++------------------------------ package.json | 25 +++-- 2 files changed, 97 insertions(+), 175 deletions(-) diff --git a/package-lock.json b/package-lock.json index cd74b9a..93a1640 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,16 +14,16 @@ "@astrojs/node": "^9.4.3", "@astrojs/react": "^4.3.0", "@nanostores/react": "^0.8.4", - "@patternfly/ast-helpers": "1.4.0-alpha.190", - "@patternfly/patternfly": "^6.0.0", - "@patternfly/quickstarts": "^6.0.0", - "@patternfly/react-code-editor": "^6.2.2", - "@patternfly/react-core": "^6.0.0", - "@patternfly/react-drag-drop": "^6.0.0", - "@patternfly/react-icons": "6.5.0-prerelease.13", - "@patternfly/react-styles": "^6.0.0", - "@patternfly/react-table": "^6.0.0", - "@patternfly/react-tokens": "^6.0.0", + "@patternfly/ast-helpers": "1.4.0-alpha.381", + "@patternfly/patternfly": "^6.5.2", + "@patternfly/quickstarts": "^6.5.0", + "@patternfly/react-code-editor": "^6.5.1", + "@patternfly/react-core": "^6.5.1", + "@patternfly/react-drag-drop": "^6.5.1", + "@patternfly/react-icons": "^6.5.1", + "@patternfly/react-styles": "^6.5.1", + "@patternfly/react-table": "^6.5.1", + "@patternfly/react-tokens": "^6.5.1", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "astro": "^5.15.9", @@ -35,7 +35,6 @@ "react-docgen": "^7.1.1", "react-dom": "^18.3.1", "react-error-boundary": "^6.0.0", - "react-icons": "^5.5.0", "sass": "^1.90.0", "typescript": "^5.9.2" }, @@ -47,8 +46,8 @@ "@babel/preset-react": "^7.26.3", "@babel/preset-typescript": "^7.26.0", "@eslint/js": "^9.16.0", - "@patternfly/react-data-view": "^6.0.0", - "@patternfly/react-user-feedback": "^6.0.0", + "@patternfly/react-data-view": "^6.5.0", + "@patternfly/react-user-feedback": "^6.3.0", "@semantic-release/git": "^10.0.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", @@ -4033,31 +4032,31 @@ } }, "node_modules/@patternfly/ast-helpers": { - "version": "1.4.0-alpha.190", - "resolved": "https://registry.npmjs.org/@patternfly/ast-helpers/-/ast-helpers-1.4.0-alpha.190.tgz", - "integrity": "sha512-NEVd593xs/qvjodvUQmyBretUeMkI4AX0dLe8flQnHYig5rhL65Bpfo+T/Nljv3M/HDV0CrRduJCMtnvpEDiNQ==", + "version": "1.4.0-alpha.381", + "resolved": "https://registry.npmjs.org/@patternfly/ast-helpers/-/ast-helpers-1.4.0-alpha.381.tgz", + "integrity": "sha512-i9Y+zdGZePR5Ko8b2pI5TjeG73oEMRqqH+Jbqn16NKLp3uVSXgP+NtxFX/8vmlWJzc6Nn9gDxWvX2OIBPfZL/g==", "license": "MIT", "dependencies": { - "acorn": "^8.4.1", + "acorn": "^8.16.0", "acorn-class-fields": "^1.0.0", "acorn-jsx": "^5.3.2", "acorn-static-class-features": "^1.0.0", - "astring": "^1.7.5" + "astring": "^1.9.0" } }, "node_modules/@patternfly/patternfly": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-6.2.3.tgz", - "integrity": "sha512-FR027W7JygcQpvlRU/Iom936Vm0apzfi2o5lvtlcWW6IaeZCCTtTaDxehoYuELHlemzkLziQAgu6LuCJEVayjw==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-6.5.2.tgz", + "integrity": "sha512-yZ71+1gt1VGzUN5amjDNd9NvttTnSOm9M0JeBL0YX1KaWXW1bmDPzTWEM+vQXuC4LVK0msHmR5hKB7KVpamAkA==", "license": "MIT" }, "node_modules/@patternfly/quickstarts": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@patternfly/quickstarts/-/quickstarts-6.3.1.tgz", - "integrity": "sha512-cuQ+m0K90vbGyNo4oR8UToXo1Jw24QDfCaIoAW0pbUkEcYuSPGqVvrOSf7w5hUMJ8jrXqE7g0T7JkcQXElMbHg==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@patternfly/quickstarts/-/quickstarts-6.5.0.tgz", + "integrity": "sha512-JYunrOwYF45uIZpcuMNVtoLyUKu1oB+qsUveL7lIduBVcWinw8rBXve+YnQTG7scis5kQONwJ8ZZh8W/5A37aw==", "license": "MIT", "dependencies": { - "dompurify": "^3.2.4", + "dompurify": "^3.3.2", "history": "^5.0.0" }, "peerDependencies": { @@ -4068,28 +4067,18 @@ } }, "node_modules/@patternfly/react-code-editor": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/@patternfly/react-code-editor/-/react-code-editor-6.2.2.tgz", - "integrity": "sha512-KPnkNP769afD2rvoNQtgCx+SYscamM5QSRmw2FJ9QPHVMksarwTsMvrdMxvu+n6Dhs/T40vQLU5UR7X2yPrURg==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-code-editor/-/react-code-editor-6.5.1.tgz", + "integrity": "sha512-Epd4iAX7/Uzo+Vl+6gX6f1BjhoX06DVV7pnZnQK4N4uknyXbfIGkmOJcE0Eq06z1cxiITV/h/JHI6WqzbOSaGA==", "license": "MIT", "dependencies": { - "@monaco-editor/react": "^4.6.0", - "@patternfly/react-core": "^6.2.2", - "@patternfly/react-icons": "^6.2.2", - "@patternfly/react-styles": "^6.2.2", + "@monaco-editor/react": "^4.7.0", + "@patternfly/react-core": "^6.5.1", + "@patternfly/react-icons": "^6.5.1", + "@patternfly/react-styles": "^6.5.1", "react-dropzone": "14.3.5", "tslib": "^2.8.1" }, - "peerDependencies": { - "react": "^17 || ^18", - "react-dom": "^17 || ^18" - } - }, - "node_modules/@patternfly/react-code-editor/node_modules/@patternfly/react-icons": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.4.0.tgz", - "integrity": "sha512-SPjzatm73NUYv/BL6A/cjRA5sFQ15NkiyPAcT8gmklI7HY+ptd6/eg49uBDFmxTQcSwbb5ISW/R6wwCQBY2M+Q==", - "license": "MIT", "peerDependencies": { "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" @@ -4114,27 +4103,16 @@ "react-dom": "^17 || ^18 || ^19" } }, - "node_modules/@patternfly/react-component-groups/node_modules/@patternfly/react-icons": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.4.0.tgz", - "integrity": "sha512-SPjzatm73NUYv/BL6A/cjRA5sFQ15NkiyPAcT8gmklI7HY+ptd6/eg49uBDFmxTQcSwbb5ISW/R6wwCQBY2M+Q==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "react": "^17 || ^18 || ^19", - "react-dom": "^17 || ^18 || ^19" - } - }, "node_modules/@patternfly/react-core": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.4.0.tgz", - "integrity": "sha512-zMgJmcFohp2FqgAoZHg7EXZS7gnaFESquk0qIavemYI0FsqspVlzV2/PUru7w+86+jXfqebRhgubPRsv1eJwEg==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.5.1.tgz", + "integrity": "sha512-fFZ0hcIyHJO27hxbf53W3m2R11l0O9WxR7CusJXuCEaNMP31ULrhf5Pv6ROdTrrs39Kl/yPv+2QuxQfe/4e72g==", "license": "MIT", "dependencies": { - "@patternfly/react-icons": "^6.4.0", - "@patternfly/react-styles": "^6.4.0", - "@patternfly/react-tokens": "^6.4.0", - "focus-trap": "7.6.4", + "@patternfly/react-icons": "^6.5.1", + "@patternfly/react-styles": "^6.5.1", + "@patternfly/react-tokens": "^6.5.1", + "focus-trap": "7.6.6", "react-dropzone": "^14.3.5", "tslib": "^2.8.1" }, @@ -4143,20 +4121,10 @@ "react-dom": "^17 || ^18 || ^19" } }, - "node_modules/@patternfly/react-core/node_modules/@patternfly/react-icons": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.4.0.tgz", - "integrity": "sha512-SPjzatm73NUYv/BL6A/cjRA5sFQ15NkiyPAcT8gmklI7HY+ptd6/eg49uBDFmxTQcSwbb5ISW/R6wwCQBY2M+Q==", - "license": "MIT", - "peerDependencies": { - "react": "^17 || ^18 || ^19", - "react-dom": "^17 || ^18 || ^19" - } - }, "node_modules/@patternfly/react-data-view": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-data-view/-/react-data-view-6.4.0.tgz", - "integrity": "sha512-AYIJvWLSoZaf3askvBjyyFQEvSCiquw5PFzEOiTsNoM2pDYkRagzppjclpI+MRJr44ZrfpljC6ZKE4f5Ni2p+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-data-view/-/react-data-view-6.5.0.tgz", + "integrity": "sha512-QTj8eg/pwchdxSpapqU3a6MZG7syiwzXy1La2hgAJtvQTR9ltkIg/GJJ4ota2O5wHzdSAVzTTdxiNuP33wmvbQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4172,50 +4140,29 @@ "react-dom": "^17 || ^18 || ^19" } }, - "node_modules/@patternfly/react-data-view/node_modules/@patternfly/react-icons": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.4.0.tgz", - "integrity": "sha512-SPjzatm73NUYv/BL6A/cjRA5sFQ15NkiyPAcT8gmklI7HY+ptd6/eg49uBDFmxTQcSwbb5ISW/R6wwCQBY2M+Q==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "react": "^17 || ^18 || ^19", - "react-dom": "^17 || ^18 || ^19" - } - }, "node_modules/@patternfly/react-drag-drop": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/@patternfly/react-drag-drop/-/react-drag-drop-6.2.2.tgz", - "integrity": "sha512-MBPkOz6VitwmRzjJIbS4i8/AML35PqSs5sBXZRk8ryCq/CgZ2j0zV2B5L7Z8UMwHRNdIlNJDIrUvTo+J1A9KsA==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-drag-drop/-/react-drag-drop-6.5.1.tgz", + "integrity": "sha512-10mo7ETrD4voyIUpzjtX8Z+cuQYnXPOHV5vuqhInArgSUiVi7OOqSJFG6iBnn7OCSKAMMakHdHkHRtOLiOO1Mw==", "license": "MIT", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", - "@patternfly/react-core": "^6.2.2", - "@patternfly/react-icons": "^6.2.2", - "@patternfly/react-styles": "^6.2.2", + "@patternfly/react-core": "^6.5.1", + "@patternfly/react-icons": "^6.5.1", + "@patternfly/react-styles": "^6.5.1", "resize-observer-polyfill": "^1.5.1" }, - "peerDependencies": { - "react": "^17 || ^18", - "react-dom": "^17 || ^18" - } - }, - "node_modules/@patternfly/react-drag-drop/node_modules/@patternfly/react-icons": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.4.0.tgz", - "integrity": "sha512-SPjzatm73NUYv/BL6A/cjRA5sFQ15NkiyPAcT8gmklI7HY+ptd6/eg49uBDFmxTQcSwbb5ISW/R6wwCQBY2M+Q==", - "license": "MIT", "peerDependencies": { "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" } }, "node_modules/@patternfly/react-icons": { - "version": "6.5.0-prerelease.13", - "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.5.0-prerelease.13.tgz", - "integrity": "sha512-40eSxfFytIAQkQ9EM6K4rqdDHIL9AwivqUbsYHZqJPNoipkL8RukxegPr7Lzvwt9kZ6OWGmTPtGySd4BkXzAqg==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.5.1.tgz", + "integrity": "sha512-CnuPvTTs4MMWx8CAUkmnY690ouN1bbHjunsyXu3QxvGOmzbztP+wS4BdiLS8TIXOIH80Yb7KPhnF8VkA+CduOA==", "license": "MIT", "peerDependencies": { "react": "^17 || ^18 || ^19", @@ -4223,22 +4170,22 @@ } }, "node_modules/@patternfly/react-styles": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.4.0.tgz", - "integrity": "sha512-EXmHA67s5sy+Wy/0uxWoUQ52jr9lsH2wV3QcgtvVc5zxpyBX89gShpqv4jfVqaowznHGDoL6fVBBrSe9BYOliQ==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.5.1.tgz", + "integrity": "sha512-yQMzUbbf6qYM/v3JbPvaCJTgxRbOKoEw229XZmnnM8gDvp8ECiI7LqihrAOK/NA6T6M3DDgsRMd2JurUBhPDEw==", "license": "MIT" }, "node_modules/@patternfly/react-table": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-6.4.0.tgz", - "integrity": "sha512-yv0sFOLGts8a2q9C1xUegjp50ayYyVRe0wKjMf+aMSNIK8sVYu8qu0yfBsCDybsUCldue7+qsYKRLFZosTllWQ==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-6.5.1.tgz", + "integrity": "sha512-JpX66ctLsg5snRRheDhuF2fHvXDG4UDKYfP5403kCAQ4Dz2dPu3PhlHi4AJol+egEBD2HfS/U37j7noQ1Y7mpA==", "license": "MIT", "dependencies": { - "@patternfly/react-core": "^6.4.0", - "@patternfly/react-icons": "^6.4.0", - "@patternfly/react-styles": "^6.4.0", - "@patternfly/react-tokens": "^6.4.0", - "lodash": "^4.17.21", + "@patternfly/react-core": "^6.5.1", + "@patternfly/react-icons": "^6.5.1", + "@patternfly/react-styles": "^6.5.1", + "@patternfly/react-tokens": "^6.5.1", + "lodash": "^4.18.1", "tslib": "^2.8.1" }, "peerDependencies": { @@ -4246,26 +4193,22 @@ "react-dom": "^17 || ^18 || ^19" } }, - "node_modules/@patternfly/react-table/node_modules/@patternfly/react-icons": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.4.0.tgz", - "integrity": "sha512-SPjzatm73NUYv/BL6A/cjRA5sFQ15NkiyPAcT8gmklI7HY+ptd6/eg49uBDFmxTQcSwbb5ISW/R6wwCQBY2M+Q==", - "license": "MIT", - "peerDependencies": { - "react": "^17 || ^18 || ^19", - "react-dom": "^17 || ^18 || ^19" - } + "node_modules/@patternfly/react-table/node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" }, "node_modules/@patternfly/react-tokens": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.4.0.tgz", - "integrity": "sha512-iZthBoXSGQ/+PfGTdPFJVulaJZI3rwE+7A/whOXPGp3Jyq3k6X52pr1+5nlO6WHasbZ9FyeZGqXf4fazUZNjbw==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.5.1.tgz", + "integrity": "sha512-zwepLsIQTL0Lf4R2/PIBOk1m+pm0hYVT3lktf2H4+Y87eRIifwMRb19c+pr4hj4ckGvHs+WxwjTfTj2Qqwn5rw==", "license": "MIT" }, "node_modules/@patternfly/react-user-feedback": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-user-feedback/-/react-user-feedback-6.2.0.tgz", - "integrity": "sha512-grhaZQwcESNZD2ifpbPoodCJ/NRTt4B24jVNYgDi23EVvQ0oGpyIXAvbUhX3siSkLjdqdYLsY9o5wNRJM78VkA==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-user-feedback/-/react-user-feedback-6.3.0.tgz", + "integrity": "sha512-Ff02JISZy//mWu31Y8dIulWJcR7RlniYm0o8VmZ74bZFauxE3eP2Tsfnw+IsLRz90jlGKgmGr/O8yHJHIOHFLg==", "dev": true, "license": "MIT", "dependencies": { @@ -4277,17 +4220,6 @@ "react-dom": "^17 || ^18 || ^19" } }, - "node_modules/@patternfly/react-user-feedback/node_modules/@patternfly/react-icons": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.4.0.tgz", - "integrity": "sha512-SPjzatm73NUYv/BL6A/cjRA5sFQ15NkiyPAcT8gmklI7HY+ptd6/eg49uBDFmxTQcSwbb5ISW/R6wwCQBY2M+Q==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "react": "^17 || ^18 || ^19", - "react-dom": "^17 || ^18 || ^19" - } - }, "node_modules/@pkgr/core": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", @@ -6475,9 +6407,9 @@ "license": "BSD-3-Clause" }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -9698,9 +9630,9 @@ } }, "node_modules/dompurify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", - "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.9.tgz", + "integrity": "sha512-4dPSRMRDqHvs0V4YDFCsaIZo4if5u0xM+llyxiM2fwuZFdKArUBAF3VtI2+n8NKg9P870WMdYk0UhqQNoWXbfQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -11447,12 +11379,12 @@ } }, "node_modules/focus-trap": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz", - "integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==", + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.6.tgz", + "integrity": "sha512-v/Z8bvMCajtx4mEXmOo7QEsIzlIOqRXTIwgUfsFOF9gEsespdbD0AkPIka1bSXZ8Y8oZ+2IVDQZePkTfEHZl7Q==", "license": "MIT", "dependencies": { - "tabbable": "^6.2.0" + "tabbable": "^6.3.0" } }, "node_modules/fontace": { @@ -21976,15 +21908,6 @@ "react": ">=16.13.1" } }, - "node_modules/react-icons": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", - "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", - "license": "MIT", - "peerDependencies": { - "react": "*" - } - }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -23992,9 +23915,9 @@ } }, "node_modules/tabbable": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", "license": "MIT" }, "node_modules/temp-dir": { diff --git a/package.json b/package.json index 9b695c5..304a3d9 100644 --- a/package.json +++ b/package.json @@ -53,16 +53,16 @@ "@astrojs/node": "^9.4.3", "@astrojs/react": "^4.3.0", "@nanostores/react": "^0.8.4", - "@patternfly/ast-helpers": "1.4.0-alpha.190", - "@patternfly/patternfly": "^6.0.0", - "@patternfly/quickstarts": "^6.0.0", - "@patternfly/react-code-editor": "^6.2.2", - "@patternfly/react-core": "^6.0.0", - "@patternfly/react-drag-drop": "^6.0.0", - "@patternfly/react-icons": "6.5.0-prerelease.13", - "@patternfly/react-styles": "^6.0.0", - "@patternfly/react-table": "^6.0.0", - "@patternfly/react-tokens": "^6.0.0", + "@patternfly/ast-helpers": "1.4.0-alpha.381", + "@patternfly/patternfly": "^6.5.2", + "@patternfly/quickstarts": "^6.5.0", + "@patternfly/react-code-editor": "^6.5.1", + "@patternfly/react-core": "^6.5.1", + "@patternfly/react-drag-drop": "^6.5.1", + "@patternfly/react-icons": "^6.5.1", + "@patternfly/react-styles": "^6.5.1", + "@patternfly/react-table": "^6.5.1", + "@patternfly/react-tokens": "^6.5.1", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "astro": "^5.15.9", @@ -74,7 +74,6 @@ "react-docgen": "^7.1.1", "react-dom": "^18.3.1", "react-error-boundary": "^6.0.0", - "react-icons": "^5.5.0", "sass": "^1.90.0", "typescript": "^5.9.2" }, @@ -83,8 +82,8 @@ "@babel/preset-react": "^7.26.3", "@babel/preset-typescript": "^7.26.0", "@eslint/js": "^9.16.0", - "@patternfly/react-data-view": "^6.0.0", - "@patternfly/react-user-feedback": "^6.0.0", + "@patternfly/react-data-view": "^6.5.0", + "@patternfly/react-user-feedback": "^6.3.0", "@semantic-release/git": "^10.0.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", From 44876af171f930442ab546270d31fc3afb828dd7 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Wed, 10 Jun 2026 13:49:54 -0400 Subject: [PATCH 21/23] Increase Node ram for build --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 304a3d9..7dec151 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "start": "npm run dev", "start:cli": "npm run build:cli && node ./dist/cli/cli.js start", "start:astro": "astro dev", - "build": "npm run build:cli && node --max-old-space-size=4096 ./dist/cli/cli.js build", + "build": "npm run build:cli && node --max-old-space-size=8192 ./dist/cli/cli.js build", "build:astro": "astro check && astro build", "build:cli": "tsc --build ./cli/tsconfig.json", "build:cli:watch": "tsc --build --watch ./cli/tsconfig.json", From 9ce4cf0f63fc25e8ab71e71a697c9b54fdb79222 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Wed, 10 Jun 2026 14:33:26 -0400 Subject: [PATCH 22/23] Update snapshots --- .../__snapshots__/NavEntry.test.tsx.snap | 2 +- .../__snapshots__/NavSection.test.tsx.snap | 12 ++++---- .../__snapshots__/Navigation.test.tsx.snap | 30 +++++++++---------- .../__snapshots__/PropsTable.test.tsx.snap | 10 +++---- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/components/__tests__/__snapshots__/NavEntry.test.tsx.snap b/src/components/__tests__/__snapshots__/NavEntry.test.tsx.snap index bcb40c6..7f49edf 100644 --- a/src/components/__tests__/__snapshots__/NavEntry.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/NavEntry.test.tsx.snap @@ -4,7 +4,7 @@ exports[`NavEntry matches snapshot 1`] = `
  • diff --git a/src/components/__tests__/__snapshots__/NavSection.test.tsx.snap b/src/components/__tests__/__snapshots__/NavSection.test.tsx.snap index dc408db..974e92b 100644 --- a/src/components/__tests__/__snapshots__/NavSection.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/NavSection.test.tsx.snap @@ -4,7 +4,7 @@ exports[`matches snapshot 1`] = `
  • @@ -26,11 +26,11 @@ exports[`matches snapshot 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 256 512" + viewBox="0 0 20 20" width="1em" > @@ -46,7 +46,7 @@ exports[`matches snapshot 1`] = ` >
  • @@ -66,7 +66,7 @@ exports[`matches snapshot 1`] = `
  • @@ -85,7 +85,7 @@ exports[`matches snapshot 1`] = `
  • diff --git a/src/components/__tests__/__snapshots__/Navigation.test.tsx.snap b/src/components/__tests__/__snapshots__/Navigation.test.tsx.snap index 2550ce9..0af3352 100644 --- a/src/components/__tests__/__snapshots__/Navigation.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Navigation.test.tsx.snap @@ -10,7 +10,7 @@ exports[`matches snapshot 1`] = `