From adc7eae668093fd0d3b09e359b279c9b77330e26 Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Wed, 21 Jan 2026 15:45:38 -0500 Subject: [PATCH 1/6] feat(API): Added CSS API endpoint for react tokens. Added code to get and create the index at build time. Updated css.ts to retrieve code from apiIndex.json added unit tests for extracting the patternfly css. --- .../api/[version]/[section]/[page]/css.ts | 39 ++ src/pages/api/index.ts | 36 ++ .../__tests__/extractReactTokens.test.ts | 498 ++++++++++++++++++ src/utils/apiIndex/generate.ts | 28 + src/utils/apiIndex/get.ts | 23 + src/utils/extractReactTokens.ts | 119 +++++ 6 files changed, 743 insertions(+) create mode 100644 src/pages/api/[version]/[section]/[page]/css.ts create mode 100644 src/utils/__tests__/extractReactTokens.test.ts create mode 100644 src/utils/extractReactTokens.ts diff --git a/src/pages/api/[version]/[section]/[page]/css.ts b/src/pages/api/[version]/[section]/[page]/css.ts new file mode 100644 index 0000000..e6c6faf --- /dev/null +++ b/src/pages/api/[version]/[section]/[page]/css.ts @@ -0,0 +1,39 @@ +import type { APIRoute } from 'astro' +import { createJsonResponse, createIndexKey } from '../../../../../utils/apiHelpers' +import { fetchApiIndex } from '../../../../../utils/apiIndex/fetch' + +export const prerender = false + +export const GET: APIRoute = async ({ params, url }) => { + const { version, section, page } = params + + if (!version || !section || !page) { + return createJsonResponse( + { error: 'Version, section, and page parameters are required' }, + 400, + ) + } + + try { + const index = await fetchApiIndex(url) + const pageKey = createIndexKey(version, section, page) + const cssTokens = index.css[pageKey] || [] + + if (cssTokens.length === 0) { + return createJsonResponse( + { + error: `No CSS tokens found for page '${page}' in section '${section}' for version '${version}'. CSS tokens are only available for content with a cssPrefix in the front matter.`, + }, + 404, + ) + } + + return createJsonResponse(cssTokens) + } catch (error) { + const details = error instanceof Error ? error.message : String(error) + return createJsonResponse( + { error: 'Failed to load API index', details }, + 500, + ) + } +} diff --git a/src/pages/api/index.ts b/src/pages/api/index.ts index cead58f..6b5b94a 100644 --- a/src/pages/api/index.ts +++ b/src/pages/api/index.ts @@ -132,6 +132,42 @@ export const GET: APIRoute = async () => example: ['react', 'react-demos', 'html'], }, }, + { + path: '/api/{version}/{section}/{page}/css', + method: 'GET', + description: 'Get CSS tokens for a specific page', + parameters: [ + { + name: 'version', + in: 'path', + required: true, + type: 'string', + example: 'v6', + }, + { + name: 'section', + in: 'path', + required: true, + type: 'string', + example: 'components', + }, + { + name: 'page', + in: 'path', + required: true, + type: 'string', + example: 'alert', + }, + ], + returns: { + type: 'array', + items: 'object', + description: 'Array of CSS token objects with tokenName, value, and variableName', + example: [ + { tokenName: 'c_alert__Background', value: '#000000', variableName: 'c_alert__Background' }, + ], + }, + }, { path: '/api/{version}/{section}/{page}/{tab}', method: 'GET', diff --git a/src/utils/__tests__/extractReactTokens.test.ts b/src/utils/__tests__/extractReactTokens.test.ts new file mode 100644 index 0000000..eb395a9 --- /dev/null +++ b/src/utils/__tests__/extractReactTokens.test.ts @@ -0,0 +1,498 @@ +import { readdir, readFile } from 'fs/promises' +import { existsSync } from 'fs' +import { join } from 'path' +import { extractReactTokens } from '../extractReactTokens' + +// Mock fs/promises +jest.mock('fs/promises', () => ({ + readdir: jest.fn(), + readFile: jest.fn(), +})) + +// Mock fs +jest.mock('fs', () => ({ + existsSync: jest.fn(), +})) + +// Mock path +jest.mock('path', () => ({ + join: jest.fn((...args) => args.join('/')), +})) + +// Mock process.cwd +const originalCwd = process.cwd +beforeAll(() => { + process.cwd = jest.fn(() => '/test/project') +}) + +afterAll(() => { + process.cwd = originalCwd +}) + +describe('extractReactTokens', () => { + beforeEach(() => { + jest.clearAllMocks() + // Reset console methods + jest.spyOn(console, 'error').mockImplementation(() => {}) + jest.spyOn(console, 'warn').mockImplementation(() => {}) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + describe('CSS prefix to token prefix conversion', () => { + it('converts single CSS prefix correctly', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([]) + + await extractReactTokens('pf-v6-c-accordion') + + expect(join).toHaveBeenCalledWith( + '/test/project', + 'node_modules', + '@patternfly', + 'react-tokens', + 'dist', + 'esm', + ) + }) + + it('converts array of CSS prefixes correctly', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([]) + + await extractReactTokens(['pf-v6-c-accordion', 'pf-v6-c-button']) + + expect(join).toHaveBeenCalledWith( + '/test/project', + 'node_modules', + '@patternfly', + 'react-tokens', + 'dist', + 'esm', + ) + }) + + it('handles CSS prefix without pf-v6- prefix', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([]) + + await extractReactTokens('c-accordion') + + expect(join).toHaveBeenCalled() + }) + }) + + describe('directory existence check', () => { + it('returns empty array when tokens directory does not exist', async () => { + ;(existsSync as jest.Mock).mockReturnValue(false) + + const result = await extractReactTokens('pf-v6-c-accordion') + + expect(result).toEqual([]) + expect(console.error).toHaveBeenCalledWith( + expect.stringContaining('Tokens directory not found'), + ) + expect(readdir).not.toHaveBeenCalled() + }) + + it('returns empty array when tokens directory exists', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([]) + + const result = await extractReactTokens('pf-v6-c-accordion') + + expect(result).toEqual([]) + expect(readdir).toHaveBeenCalled() + }) + }) + + describe('file filtering', () => { + it('filters out non-JS files', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([ + 'c_accordion__toggle_FontFamily.js', + 'c_accordion.ts', + 'c_accordion.json', + 'c_accordion.css', + ]) + ;(readFile as jest.Mock).mockResolvedValue( + 'export const token = { "name": "test", "value": "test", "var": "--test" };', + ) + + const result = await extractReactTokens('pf-v6-c-accordion') + + expect(readFile).toHaveBeenCalledTimes(1) + expect(readFile).toHaveBeenCalledWith( + expect.stringContaining('c_accordion__toggle_FontFamily.js'), + 'utf8', + ) + }) + + it('filters out componentIndex.js', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([ + 'componentIndex.js', + 'c_accordion__toggle_FontFamily.js', + ]) + ;(readFile as jest.Mock).mockResolvedValue( + 'export const token = { "name": "test", "value": "test", "var": "--test" };', + ) + + const result = await extractReactTokens('pf-v6-c-accordion') + + expect(readFile).toHaveBeenCalledTimes(1) + expect(readFile).not.toHaveBeenCalledWith( + expect.stringContaining('componentIndex.js'), + expect.anything(), + ) + }) + + it('filters out main component file (e.g., c_accordion.js)', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([ + 'c_accordion.js', + 'c_accordion__toggle_FontFamily.js', + 'c_accordion__header_BackgroundColor.js', + ]) + ;(readFile as jest.Mock).mockResolvedValue( + 'export const token = { "name": "test", "value": "test", "var": "--test" };', + ) + + const result = await extractReactTokens('pf-v6-c-accordion') + + expect(readFile).toHaveBeenCalledTimes(2) + expect(readFile).not.toHaveBeenCalledWith( + expect.stringContaining('c_accordion.js'), + expect.anything(), + ) + expect(readFile).toHaveBeenCalledWith( + expect.stringContaining('c_accordion__toggle_FontFamily.js'), + 'utf8', + ) + expect(readFile).toHaveBeenCalledWith( + expect.stringContaining('c_accordion__header_BackgroundColor.js'), + 'utf8', + ) + }) + + it('includes files that start with token prefix but are not the main component file', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([ + 'c_accordion__toggle_FontFamily.js', + 'c_accordion__header_BackgroundColor.js', + 'c_accordion__section_PaddingTop.js', + ]) + ;(readFile as jest.Mock).mockResolvedValue( + 'export const token = { "name": "test", "value": "test", "var": "--test" };', + ) + + const result = await extractReactTokens('pf-v6-c-accordion') + + expect(readFile).toHaveBeenCalledTimes(3) + }) + + it('handles multiple prefixes and matches files for any prefix', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([ + 'c_accordion__toggle_FontFamily.js', + 'c_button__primary_BackgroundColor.js', + 'c_other_component.js', + ]) + ;(readFile as jest.Mock).mockResolvedValue( + 'export const token = { "name": "test", "value": "test", "var": "--test" };', + ) + + const result = await extractReactTokens([ + 'pf-v6-c-accordion', + 'pf-v6-c-button', + ]) + + expect(readFile).toHaveBeenCalledTimes(2) + expect(readFile).toHaveBeenCalledWith( + expect.stringContaining('c_accordion__toggle_FontFamily.js'), + 'utf8', + ) + expect(readFile).toHaveBeenCalledWith( + expect.stringContaining('c_button__primary_BackgroundColor.js'), + 'utf8', + ) + }) + }) + + describe('token extraction from files', () => { + it('extracts token object from valid file content', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([ + 'c_accordion__toggle_FontFamily.js', + ]) + ;(readFile as jest.Mock).mockResolvedValue( + 'export const c_accordion_toggle_FontFamily = { "name": "c-accordion-toggle-FontFamily", "value": "1rem", "var": "--pf-v6-c-accordion--toggle--FontFamily"\n};', + ) + + const result = await extractReactTokens('pf-v6-c-accordion') + + expect(result).toEqual([ + { + name: 'c-accordion-toggle-FontFamily', + value: '1rem', + var: '--pf-v6-c-accordion--toggle--FontFamily', + }, + ]) + }) + + it('extracts multiple token objects from multiple files', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([ + 'c_accordion__toggle_FontFamily.js', + 'c_accordion__header_BackgroundColor.js', + ]) + ;(readFile as jest.Mock) + .mockResolvedValueOnce( + 'export const token1 = { "name": "c-accordion-toggle-FontFamily", "value": "1rem", "var": "--pf-v6-c-accordion--toggle--FontFamily"\n};', + ) + .mockResolvedValueOnce( + 'export const token2 = { "name": "c-accordion-header-BackgroundColor", "value": "#fff", "var": "--pf-v6-c-accordion--header--BackgroundColor"\n};', + ) + + const result = await extractReactTokens('pf-v6-c-accordion') + + expect(result).toHaveLength(2) + expect(result).toEqual([ + { + name: 'c-accordion-header-BackgroundColor', + value: '#fff', + var: '--pf-v6-c-accordion--header--BackgroundColor', + }, + { + name: 'c-accordion-toggle-FontFamily', + value: '1rem', + var: '--pf-v6-c-accordion--toggle--FontFamily', + }, + ]) + }) + + it('handles file content with multiline object definition', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([ + 'c_accordion__toggle_FontFamily.js', + ]) + ;(readFile as jest.Mock).mockResolvedValue(`export const token = { + "name": "c-accordion-toggle-FontFamily", + "value": "1rem", + "var": "--pf-v6-c-accordion--toggle--FontFamily" +};`) + + const result = await extractReactTokens('pf-v6-c-accordion') + + expect(result).toEqual([ + { + name: 'c-accordion-toggle-FontFamily', + value: '1rem', + var: '--pf-v6-c-accordion--toggle--FontFamily', + }, + ]) + }) + + it('handles file content with whitespace and comments', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([ + 'c_accordion__toggle_FontFamily.js', + ]) + ;(readFile as jest.Mock).mockResolvedValue( + '// Some comment\nexport const token = { "name": "test", "value": "test", "var": "--test"\n};// Another comment', + ) + + const result = await extractReactTokens('pf-v6-c-accordion') + + expect(result).toEqual([ + { + name: 'test', + value: 'test', + var: '--test', + }, + ]) + }) + + it('skips files that do not match the export pattern', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([ + 'c_accordion__toggle_FontFamily.js', + 'c_accordion__header_BackgroundColor.js', + ]) + ;(readFile as jest.Mock) + .mockResolvedValueOnce('const token = { "name": "test" };') // No export + .mockResolvedValueOnce( + 'export const token = { "name": "test", "value": "test", "var": "--test"\n};', + ) + + const result = await extractReactTokens('pf-v6-c-accordion') + + expect(result).toHaveLength(1) + expect(result[0].name).toBe('test') + }) + + it('validates token object has required properties', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([ + 'c_accordion__toggle_FontFamily.js', + ]) + ;(readFile as jest.Mock).mockResolvedValue( + 'export const token = { "name": "test" };', // Missing value and var + ) + + const result = await extractReactTokens('pf-v6-c-accordion') + + expect(result).toEqual([]) + }) + + it('validates token object properties are strings', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([ + 'c_accordion__toggle_FontFamily.js', + ]) + ;(readFile as jest.Mock).mockResolvedValue( + 'export const token = { "name": 123, "value": "test", "var": "--test" };', // name is not a string + ) + + const result = await extractReactTokens('pf-v6-c-accordion') + + expect(result).toEqual([]) + }) + }) + + describe('error handling', () => { + it('handles file read errors gracefully', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([ + 'c_accordion__toggle_FontFamily.js', + 'c_accordion__header_BackgroundColor.js', + ]) + ;(readFile as jest.Mock) + .mockRejectedValueOnce(new Error('Permission denied')) + .mockResolvedValueOnce( + 'export const token = { "name": "test", "value": "test", "var": "--test"\n};', + ) + + const result = await extractReactTokens('pf-v6-c-accordion') + + expect(console.warn).toHaveBeenCalledWith( + expect.stringContaining('Failed to read file'), + expect.any(Error), + ) + expect(result).toHaveLength(1) + }) + + it('handles object parsing errors gracefully', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([ + 'c_accordion__toggle_FontFamily.js', + ]) + ;(readFile as jest.Mock).mockResolvedValue( + 'export const token = { invalid syntax\n};', // Invalid JavaScript + ) + + const result = await extractReactTokens('pf-v6-c-accordion') + + expect(console.warn).toHaveBeenCalledWith( + expect.stringContaining('Failed to parse object'), + expect.any(Error), + ) + expect(result).toEqual([]) + }) + + it('handles readdir errors gracefully', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockRejectedValue(new Error('Directory read failed')) + + await expect(extractReactTokens('pf-v6-c-accordion')).rejects.toThrow( + 'Directory read failed', + ) + }) + }) + + describe('sorting', () => { + it('sorts tokens by name alphabetically', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([ + 'c_accordion__z_token.js', + 'c_accordion__a_token.js', + 'c_accordion__m_token.js', + ]) + ;(readFile as jest.Mock) + .mockResolvedValueOnce( + 'export const token1 = { "name": "z-token", "value": "test", "var": "--test"\n};', + ) + .mockResolvedValueOnce( + 'export const token2 = { "name": "a-token", "value": "test", "var": "--test"\n};', + ) + .mockResolvedValueOnce( + 'export const token3 = { "name": "m-token", "value": "test", "var": "--test"\n};', + ) + + const result = await extractReactTokens('pf-v6-c-accordion') + + expect(result).toEqual([ + { name: 'a-token', value: 'test', var: '--test' }, + { name: 'm-token', value: 'test', var: '--test' }, + { name: 'z-token', value: 'test', var: '--test' }, + ]) + }) + }) + + describe('edge cases', () => { + it('handles empty file list', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([]) + + const result = await extractReactTokens('pf-v6-c-accordion') + + expect(result).toEqual([]) + }) + + it('handles files with no matching prefix', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([ + 'other_component__token.js', + 'unrelated_file.js', + ]) + + const result = await extractReactTokens('pf-v6-c-accordion') + + expect(result).toEqual([]) + expect(readFile).not.toHaveBeenCalled() + }) + + it('handles CSS prefix with multiple hyphens', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([ + 'c_data_list__item_row_BackgroundColor.js', + ]) + ;(readFile as jest.Mock).mockResolvedValue( + 'export const token = { "name": "test", "value": "test", "var": "--test"\n};', + ) + + const result = await extractReactTokens('pf-v6-c-data-list') + + expect(readFile).toHaveBeenCalled() + expect(result).toHaveLength(1) + }) + + it('handles file content with multiple export statements', async () => { + ;(existsSync as jest.Mock).mockReturnValue(true) + ;(readdir as jest.Mock).mockResolvedValue([ + 'c_accordion__toggle_FontFamily.js', + ]) + ;(readFile as jest.Mock).mockResolvedValue( + 'export const token1 = { "name": "first", "value": "test", "var": "--test"\n};export const token2 = { "name": "second", "value": "test", "var": "--test"\n};', + ) + + const result = await extractReactTokens('pf-v6-c-accordion') + + // Should only extract the first matching export + expect(result).toHaveLength(1) + expect(result[0].name).toBe('first') + }) + }) +}) diff --git a/src/utils/apiIndex/generate.ts b/src/utils/apiIndex/generate.ts index 4a92211..8745d13 100644 --- a/src/utils/apiIndex/generate.ts +++ b/src/utils/apiIndex/generate.ts @@ -7,6 +7,7 @@ import { content } from '../../content' import { kebabCase, addDemosOrDeprecated } from '../index' import { getDefaultTabForApi } from '../packageUtils' import { getOutputDir } from '../getOutputDir' +import { extractReactTokens } from '../extractReactTokens' const SOURCE_ORDER: Record = { react: 1, @@ -45,6 +46,8 @@ export interface ApiIndex { tabs: Record /** Examples by version::section::page::tab with titles (e.g., { 'v6::components::alert::react': [{exampleName: 'AlertDefault', title: 'Default alert'}] }) */ examples: Record + /** CSS token objects by version::section::page (e.g., { 'v6::components::accordion': [{name: '--pf-v6-c-accordion--...', value: '...', var: '...'}] }) */ + css: Record> } /** @@ -102,6 +105,7 @@ export async function generateApiIndex(): Promise { pages: {}, tabs: {}, examples: {}, + css: {}, } // Get all versions @@ -130,6 +134,8 @@ export async function generateApiIndex(): Promise { const sectionPages: Record> = {} const pageTabs: Record> = {} const tabExamples: Record = {} + const pageCss: Record> = {} + const pageCssPrefixes: Record = {} flatEntries.forEach((entry: any) => { if (!entry.data.section) { @@ -165,8 +171,25 @@ export async function generateApiIndex(): Promise { if (examplesWithTitles.length > 0) { tabExamples[tabKey] = examplesWithTitles } + + // Collect CSS prefixes for pages - we'll extract tokens later + if (entry.data.cssPrefix && !pageCssPrefixes[pageKey]) { + pageCssPrefixes[pageKey] = entry.data.cssPrefix + } }) + // Extract CSS tokens for pages that have cssPrefix + for (const [pageKey, cssPrefix] of Object.entries(pageCssPrefixes)) { + try { + const tokens = await extractReactTokens(cssPrefix) + if (tokens.length > 0) { + pageCss[pageKey] = tokens + } + } catch (error) { + console.warn(`Failed to extract CSS tokens for ${pageKey}:`, error) + } + } + // Convert sets to sorted arrays index.sections[version] = Array.from(sections).sort() @@ -181,6 +204,11 @@ export async function generateApiIndex(): Promise { Object.entries(tabExamples).forEach(([key, examples]) => { index.examples[key] = examples }) + + // Add CSS token objects to index + Object.entries(pageCss).forEach(([key, tokens]) => { + index.css[key] = tokens + }) } return index diff --git a/src/utils/apiIndex/get.ts b/src/utils/apiIndex/get.ts index a20c3b8..e35a662 100644 --- a/src/utils/apiIndex/get.ts +++ b/src/utils/apiIndex/get.ts @@ -27,6 +27,10 @@ export async function getApiIndex(): Promise { throw new Error('Invalid API index structure: missing or invalid "examples" object') } + if (!parsed.css || typeof parsed.css !== 'object') { + throw new Error('Invalid API index structure: missing or invalid "css" object') + } + return parsed as ApiIndex } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { @@ -117,3 +121,22 @@ export async function getExamples( const key = createIndexKey(version, section, page, tab) return index.examples[key] || [] } + +/** + * Gets CSS token objects for a specific page + * + * @param version - The documentation version (e.g., 'v6') + * @param section - The section name (e.g., 'components') + * @param page - The page slug (e.g., 'accordion') + * @returns Promise resolving to array of token objects, or empty array if not found + */ +export async function getCssTokens( + version: string, + section: string, + page: string, +): Promise> { + const index = await getApiIndex() + const { createIndexKey } = await import('../apiHelpers') + const key = createIndexKey(version, section, page) + return index.css[key] || [] +} diff --git a/src/utils/extractReactTokens.ts b/src/utils/extractReactTokens.ts new file mode 100644 index 0000000..305708f --- /dev/null +++ b/src/utils/extractReactTokens.ts @@ -0,0 +1,119 @@ +import { readdir, readFile } from 'fs/promises' +import { join } from 'path' +import { existsSync } from 'fs' + +/** + * Converts a CSS prefix (e.g., "pf-v6-c-accordion") to a token prefix (e.g., "c_accordion") + * + * @param cssPrefix - The CSS prefix from front matter + * @returns The token prefix used in file names + */ +function cssPrefixToTokenPrefix(cssPrefix: string): string { + // Remove "pf-v6-" prefix and replace hyphens with underscores to match the tokens. + return cssPrefix.replace(/^pf-v6-/, '').replace(/-+/g, '_') +} + +/** + * Extracts all token objects from @patternfly/react-tokens that match a given CSS prefix + * + * @param cssPrefix - The CSS prefix (e.g., "pf-v6-c-accordion") + * @returns Array of token objects with name, value, and var properties + */ +export async function extractReactTokens( + cssPrefix: string | string[], +): Promise> { + // Handle both single prefix and array of prefixes to support the subcomponents. + const prefixes = Array.isArray(cssPrefix) ? cssPrefix : [cssPrefix] + const tokenPrefixes = prefixes.map(cssPrefixToTokenPrefix) + + // Path to the react-tokens esm directory. + const tokensDir = join( + process.cwd(), + 'node_modules', + '@patternfly', + 'react-tokens', + 'dist', + 'esm', + ) + + if (!existsSync(tokensDir)) { + console.error(`Tokens directory not found: ${tokensDir}`) + return [] + } + + // Get all files in the directory + const files = await readdir(tokensDir) + + // Filter for .js files that match any of the token prefixes + // Exclude componentIndex.js and main component files (like c_accordion.js without underscores after the prefix) + const matchingFiles = files.filter((file) => { + if (!file.endsWith('.js') || file === 'componentIndex.js') { + return false + } + // Check if file starts with any of the token prefixes + // We want individual token files (e.g., c_accordion__toggle_FontFamily.js) + // but not the main component index file (e.g., c_accordion.js) + return tokenPrefixes.some((prefix) => { + if (file === `${prefix}.js`) { + // This is the main component file, skip it + return false + } + return file.startsWith(prefix) + }) + }) + + // Import and extract objects from each matching file + const tokenObjects: Array<{ name: string; value: string; var: string }> = [] + + for (const file of matchingFiles) { + try { + const filePath = join(tokensDir, file) + const fileContent = await readFile(filePath, 'utf8') + + // Extract the exported object using regex + // Pattern: export const variableName = { "name": "...", "value": "...", "var": "..." }; + // Use non-greedy match to get just the first exported const object + const objectMatch = fileContent.match( + /export const \w+ = \{[\s\S]*?\n\};/, + ) + + if (objectMatch) { + // Parse the object string to extract the JSON-like object + const objectContent = objectMatch[0] + .replace(/export const \w+ = /, '') + .replace(/;$/, '') + + try { + // Use Function constructor for safe evaluation + // The object content is valid JavaScript, so we can evaluate it + const tokenObject = new Function(`return ${objectContent}`)() as { + name: string + value: string + var: string + } + + if ( + tokenObject && + typeof tokenObject === 'object' && + typeof tokenObject.name === 'string' && + typeof tokenObject.value === 'string' && + typeof tokenObject.var === 'string' + ) { + tokenObjects.push({ + name: tokenObject.name, + value: tokenObject.value, + var: tokenObject.var, + }) + } + } catch (evalError) { + console.warn(`Failed to parse object from ${file}:`, evalError) + } + } + } catch (error) { + console.warn(`Failed to read file ${file}:`, error) + } + } + + // Sort by name for consistent ordering + return tokenObjects.sort((a, b) => a.name.localeCompare(b.name)) +} From 51c7eb360641d7fa0ecc409f6a22d44ee1a814f3 Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Thu, 22 Jan 2026 14:10:00 -0500 Subject: [PATCH 2/6] Added endpoint tests. --- .../[version]/[section]/[page]/css.test.ts | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 src/__tests__/pages/api/__tests__/[version]/[section]/[page]/css.test.ts diff --git a/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/css.test.ts b/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/css.test.ts new file mode 100644 index 0000000..bb835d6 --- /dev/null +++ b/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/css.test.ts @@ -0,0 +1,236 @@ +import { GET } from '../../../../../../../pages/api/[version]/[section]/[page]/css' + +/** + * Mock fetchApiIndex to return API index with CSS tokens + */ +jest.mock('../../../../../../../utils/apiIndex/fetch', () => ({ + fetchApiIndex: jest.fn().mockResolvedValue({ + versions: ['v6'], + sections: { + v6: ['components'], + }, + pages: { + 'v6::components': ['alert', 'button'], + }, + tabs: { + 'v6::components::alert': ['react', 'html'], + 'v6::components::button': ['react'], + }, + css: { + 'v6::components::alert': [ + { + name: '--pf-v6-c-alert--BackgroundColor', + value: '#ffffff', + description: 'Alert background color', + }, + { + name: '--pf-v6-c-alert--Color', + value: '#151515', + description: 'Alert text color', + }, + ], + 'v6::components::button': [ + { + name: '--pf-v6-c-button--BackgroundColor', + value: '#0066cc', + description: 'Button background color', + }, + ], + }, + }), +})) + +beforeEach(() => { + jest.clearAllMocks() +}) + +it('returns CSS tokens for a valid page', async () => { + const response = await GET({ + params: { + version: 'v6', + section: 'components', + page: 'alert', + }, + url: new URL('http://localhost/api/v6/components/alert/css'), + } 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(Array.isArray(body)).toBe(true) + expect(body).toHaveLength(2) + expect(body[0]).toHaveProperty('name') + expect(body[0]).toHaveProperty('value') + expect(body[0]).toHaveProperty('description') + expect(body[0].name).toBe('--pf-v6-c-alert--BackgroundColor') + expect(body[0].value).toBe('#ffffff') +}) + +it('returns CSS tokens for different pages', async () => { + const buttonResponse = await GET({ + params: { + version: 'v6', + section: 'components', + page: 'button', + }, + url: new URL('http://localhost/api/v6/components/button/css'), + } as any) + const buttonBody = await buttonResponse.json() + + expect(buttonResponse.status).toBe(200) + expect(Array.isArray(buttonBody)).toBe(true) + expect(buttonBody).toHaveLength(1) + expect(buttonBody[0].name).toBe('--pf-v6-c-button--BackgroundColor') +}) + +it('returns 404 error when no CSS tokens are found', async () => { + const response = await GET({ + params: { + version: 'v6', + section: 'components', + page: 'nonexistent', + }, + url: new URL('http://localhost/api/v6/components/nonexistent/css'), + } as any) + const body = await response.json() + + expect(response.status).toBe(404) + expect(body).toHaveProperty('error') + expect(body.error).toContain('No CSS tokens found') + expect(body.error).toContain('nonexistent') + expect(body.error).toContain('components') + expect(body.error).toContain('v6') + expect(body.error).toContain('cssPrefix') +}) + +it('returns 400 error when version parameter is missing', async () => { + const response = await GET({ + params: { + section: 'components', + page: 'alert', + }, + url: new URL('http://localhost/api/components/alert/css'), + } as any) + const body = await response.json() + + expect(response.status).toBe(400) + expect(body).toHaveProperty('error') + expect(body.error).toContain('Version, section, and page parameters are required') +}) + +it('returns 400 error when section parameter is missing', async () => { + const response = await GET({ + params: { + version: 'v6', + page: 'alert', + }, + url: new URL('http://localhost/api/v6/alert/css'), + } as any) + const body = await response.json() + + expect(response.status).toBe(400) + expect(body).toHaveProperty('error') + expect(body.error).toContain('Version, section, and page parameters are required') +}) + +it('returns 400 error when page parameter is missing', async () => { + const response = await GET({ + params: { + version: 'v6', + section: 'components', + }, + url: new URL('http://localhost/api/v6/components/css'), + } as any) + const body = await response.json() + + expect(response.status).toBe(400) + expect(body).toHaveProperty('error') + expect(body.error).toContain('Version, section, and page parameters are required') +}) + +it('returns 400 error when all parameters are missing', async () => { + const response = await GET({ + params: {}, + url: new URL('http://localhost/api/css'), + } as any) + const body = await response.json() + + expect(response.status).toBe(400) + expect(body).toHaveProperty('error') + expect(body.error).toContain('Version, section, and page parameters are required') +}) + +it('returns 500 error when fetchApiIndex fails', async () => { + const { fetchApiIndex } = require('../../../../../../../utils/apiIndex/fetch') + fetchApiIndex.mockRejectedValueOnce(new Error('Network error')) + + const response = await GET({ + params: { + version: 'v6', + section: 'components', + page: 'alert', + }, + url: new URL('http://localhost/api/v6/components/alert/css'), + } as any) + const body = await response.json() + + expect(response.status).toBe(500) + expect(body).toHaveProperty('error') + expect(body).toHaveProperty('details') + expect(body.error).toBe('Failed to load API index') + expect(body.details).toBe('Network error') +}) + +it('returns 500 error when fetchApiIndex throws a non-Error object', async () => { + const { fetchApiIndex } = require('../../../../../../../utils/apiIndex/fetch') + fetchApiIndex.mockRejectedValueOnce('String error') + + const response = await GET({ + params: { + version: 'v6', + section: 'components', + page: 'alert', + }, + url: new URL('http://localhost/api/v6/components/alert/css'), + } as any) + const body = await response.json() + + expect(response.status).toBe(500) + expect(body).toHaveProperty('error') + expect(body).toHaveProperty('details') + expect(body.error).toBe('Failed to load API index') + expect(body.details).toBe('String error') +}) + +it('returns empty array when CSS tokens array exists but is empty', async () => { + const { fetchApiIndex } = require('../../../../../../../utils/apiIndex/fetch') + fetchApiIndex.mockResolvedValueOnce({ + versions: ['v6'], + sections: { + v6: ['components'], + }, + pages: { + 'v6::components': ['empty'], + }, + tabs: { + 'v6::components::empty': ['react'], + }, + css: { + 'v6::components::empty': [], + }, + }) + + const response = await GET({ + params: { + version: 'v6', + section: 'components', + page: 'empty', + }, + url: new URL('http://localhost/api/v6/components/empty/css'), + } as any) + const body = await response.json() + + expect(response.status).toBe(404) + expect(body).toHaveProperty('error') + expect(body.error).toContain('No CSS tokens found') +}) From 4197aefb86ccc7a8688d230461e68942645c78bb Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Thu, 22 Jan 2026 16:06:16 -0500 Subject: [PATCH 3/6] Fixed styles and linting errors. --- src/utils/extractReactTokens.ts | 91 +++++++++++++++++---------------- 1 file changed, 48 insertions(+), 43 deletions(-) diff --git a/src/utils/extractReactTokens.ts b/src/utils/extractReactTokens.ts index 305708f..75f352c 100644 --- a/src/utils/extractReactTokens.ts +++ b/src/utils/extractReactTokens.ts @@ -21,7 +21,7 @@ function cssPrefixToTokenPrefix(cssPrefix: string): string { */ export async function extractReactTokens( cssPrefix: string | string[], -): Promise> { +): Promise<{ name: string; value: string; var: string }[]> { // Handle both single prefix and array of prefixes to support the subcomponents. const prefixes = Array.isArray(cssPrefix) ? cssPrefix : [cssPrefix] const tokenPrefixes = prefixes.map(cssPrefixToTokenPrefix) @@ -37,6 +37,7 @@ export async function extractReactTokens( ) if (!existsSync(tokensDir)) { + // eslint-disable-next-line no-console console.error(`Tokens directory not found: ${tokensDir}`) return [] } @@ -63,56 +64,60 @@ export async function extractReactTokens( }) // Import and extract objects from each matching file - const tokenObjects: Array<{ name: string; value: string; var: string }> = [] + const tokenObjects: { name: string; value: string; var: string }[] = [] - for (const file of matchingFiles) { - try { - const filePath = join(tokensDir, file) - const fileContent = await readFile(filePath, 'utf8') + await Promise.all( + matchingFiles.map(async (file) => { + try { + const filePath = join(tokensDir, file) + const fileContent = await readFile(filePath, 'utf8') - // Extract the exported object using regex - // Pattern: export const variableName = { "name": "...", "value": "...", "var": "..." }; - // Use non-greedy match to get just the first exported const object - const objectMatch = fileContent.match( - /export const \w+ = \{[\s\S]*?\n\};/, - ) + // Extract the exported object using regex + // Pattern: export const variableName = { "name": "...", "value": "...", "var": "..." }; + // Use non-greedy match to get just the first exported const object + const objectMatch = fileContent.match( + /export const \w+ = \{[\s\S]*?\n\};/, + ) - if (objectMatch) { - // Parse the object string to extract the JSON-like object - const objectContent = objectMatch[0] - .replace(/export const \w+ = /, '') - .replace(/;$/, '') + if (objectMatch) { + // Parse the object string to extract the JSON-like object + const objectContent = objectMatch[0] + .replace(/export const \w+ = /, '') + .replace(/;$/, '') - try { - // Use Function constructor for safe evaluation - // The object content is valid JavaScript, so we can evaluate it - const tokenObject = new Function(`return ${objectContent}`)() as { - name: string - value: string - var: string - } + try { + // Use Function constructor for safe evaluation + // The object content is valid JavaScript, so we can evaluate it + const tokenObject = new Function(`return ${objectContent}`)() as { + name: string + value: string + var: string + } - if ( - tokenObject && - typeof tokenObject === 'object' && - typeof tokenObject.name === 'string' && - typeof tokenObject.value === 'string' && - typeof tokenObject.var === 'string' - ) { - tokenObjects.push({ - name: tokenObject.name, - value: tokenObject.value, - var: tokenObject.var, - }) + if ( + tokenObject && + typeof tokenObject === 'object' && + typeof tokenObject.name === 'string' && + typeof tokenObject.value === 'string' && + typeof tokenObject.var === 'string' + ) { + tokenObjects.push({ + name: tokenObject.name, + value: tokenObject.value, + var: tokenObject.var, + }) + } + } catch (evalError) { + // eslint-disable-next-line no-console + console.warn(`Failed to parse object from ${file}:`, evalError) } - } catch (evalError) { - console.warn(`Failed to parse object from ${file}:`, evalError) } + } catch (error) { + // eslint-disable-next-line no-console + console.warn(`Failed to read file ${file}:`, error) } - } catch (error) { - console.warn(`Failed to read file ${file}:`, error) - } - } + }), + ) // Sort by name for consistent ordering return tokenObjects.sort((a, b) => a.name.localeCompare(b.name)) From b0277f11993f9dde49fd39d2bd8a795707a1e915 Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Thu, 22 Jan 2026 16:14:32 -0500 Subject: [PATCH 4/6] Removed non standard code. --- .../[version]/[section]/[page]/css.test.ts | 3 + .../__tests__/extractReactTokens.test.ts | 55 ++----------- src/utils/apiIndex/generate.ts | 4 +- src/utils/apiIndex/get.ts | 2 +- src/utils/extractReactTokens.ts | 80 ++++++++----------- 5 files changed, 45 insertions(+), 99 deletions(-) diff --git a/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/css.test.ts b/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/css.test.ts index bb835d6..278fbd1 100644 --- a/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/css.test.ts +++ b/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/css.test.ts @@ -161,6 +161,7 @@ it('returns 400 error when all parameters are missing', async () => { }) it('returns 500 error when fetchApiIndex fails', async () => { + // eslint-disable-next-line @typescript-eslint/no-require-imports const { fetchApiIndex } = require('../../../../../../../utils/apiIndex/fetch') fetchApiIndex.mockRejectedValueOnce(new Error('Network error')) @@ -182,6 +183,7 @@ it('returns 500 error when fetchApiIndex fails', async () => { }) it('returns 500 error when fetchApiIndex throws a non-Error object', async () => { + // eslint-disable-next-line @typescript-eslint/no-require-imports const { fetchApiIndex } = require('../../../../../../../utils/apiIndex/fetch') fetchApiIndex.mockRejectedValueOnce('String error') @@ -203,6 +205,7 @@ it('returns 500 error when fetchApiIndex throws a non-Error object', async () => }) it('returns empty array when CSS tokens array exists but is empty', async () => { + // eslint-disable-next-line @typescript-eslint/no-require-imports const { fetchApiIndex } = require('../../../../../../../utils/apiIndex/fetch') fetchApiIndex.mockResolvedValueOnce({ versions: ['v6'], diff --git a/src/utils/__tests__/extractReactTokens.test.ts b/src/utils/__tests__/extractReactTokens.test.ts index eb395a9..abf1517 100644 --- a/src/utils/__tests__/extractReactTokens.test.ts +++ b/src/utils/__tests__/extractReactTokens.test.ts @@ -32,9 +32,6 @@ afterAll(() => { describe('extractReactTokens', () => { beforeEach(() => { jest.clearAllMocks() - // Reset console methods - jest.spyOn(console, 'error').mockImplementation(() => {}) - jest.spyOn(console, 'warn').mockImplementation(() => {}) }) afterEach(() => { @@ -91,9 +88,6 @@ describe('extractReactTokens', () => { const result = await extractReactTokens('pf-v6-c-accordion') expect(result).toEqual([]) - expect(console.error).toHaveBeenCalledWith( - expect.stringContaining('Tokens directory not found'), - ) expect(readdir).not.toHaveBeenCalled() }) @@ -121,7 +115,7 @@ describe('extractReactTokens', () => { 'export const token = { "name": "test", "value": "test", "var": "--test" };', ) - const result = await extractReactTokens('pf-v6-c-accordion') + await extractReactTokens('pf-v6-c-accordion') expect(readFile).toHaveBeenCalledTimes(1) expect(readFile).toHaveBeenCalledWith( @@ -140,7 +134,7 @@ describe('extractReactTokens', () => { 'export const token = { "name": "test", "value": "test", "var": "--test" };', ) - const result = await extractReactTokens('pf-v6-c-accordion') + await extractReactTokens('pf-v6-c-accordion') expect(readFile).toHaveBeenCalledTimes(1) expect(readFile).not.toHaveBeenCalledWith( @@ -160,7 +154,7 @@ describe('extractReactTokens', () => { 'export const token = { "name": "test", "value": "test", "var": "--test" };', ) - const result = await extractReactTokens('pf-v6-c-accordion') + await extractReactTokens('pf-v6-c-accordion') expect(readFile).toHaveBeenCalledTimes(2) expect(readFile).not.toHaveBeenCalledWith( @@ -188,7 +182,7 @@ describe('extractReactTokens', () => { 'export const token = { "name": "test", "value": "test", "var": "--test" };', ) - const result = await extractReactTokens('pf-v6-c-accordion') + await extractReactTokens('pf-v6-c-accordion') expect(readFile).toHaveBeenCalledTimes(3) }) @@ -204,7 +198,7 @@ describe('extractReactTokens', () => { 'export const token = { "name": "test", "value": "test", "var": "--test" };', ) - const result = await extractReactTokens([ + await extractReactTokens([ 'pf-v6-c-accordion', 'pf-v6-c-button', ]) @@ -363,45 +357,6 @@ describe('extractReactTokens', () => { }) describe('error handling', () => { - it('handles file read errors gracefully', async () => { - ;(existsSync as jest.Mock).mockReturnValue(true) - ;(readdir as jest.Mock).mockResolvedValue([ - 'c_accordion__toggle_FontFamily.js', - 'c_accordion__header_BackgroundColor.js', - ]) - ;(readFile as jest.Mock) - .mockRejectedValueOnce(new Error('Permission denied')) - .mockResolvedValueOnce( - 'export const token = { "name": "test", "value": "test", "var": "--test"\n};', - ) - - const result = await extractReactTokens('pf-v6-c-accordion') - - expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining('Failed to read file'), - expect.any(Error), - ) - expect(result).toHaveLength(1) - }) - - it('handles object parsing errors gracefully', async () => { - ;(existsSync as jest.Mock).mockReturnValue(true) - ;(readdir as jest.Mock).mockResolvedValue([ - 'c_accordion__toggle_FontFamily.js', - ]) - ;(readFile as jest.Mock).mockResolvedValue( - 'export const token = { invalid syntax\n};', // Invalid JavaScript - ) - - const result = await extractReactTokens('pf-v6-c-accordion') - - expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining('Failed to parse object'), - expect.any(Error), - ) - expect(result).toEqual([]) - }) - it('handles readdir errors gracefully', async () => { ;(existsSync as jest.Mock).mockReturnValue(true) ;(readdir as jest.Mock).mockRejectedValue(new Error('Directory read failed')) diff --git a/src/utils/apiIndex/generate.ts b/src/utils/apiIndex/generate.ts index 8745d13..76a83ca 100644 --- a/src/utils/apiIndex/generate.ts +++ b/src/utils/apiIndex/generate.ts @@ -47,7 +47,7 @@ export interface ApiIndex { /** Examples by version::section::page::tab with titles (e.g., { 'v6::components::alert::react': [{exampleName: 'AlertDefault', title: 'Default alert'}] }) */ examples: Record /** CSS token objects by version::section::page (e.g., { 'v6::components::accordion': [{name: '--pf-v6-c-accordion--...', value: '...', var: '...'}] }) */ - css: Record> + css: Record } /** @@ -134,7 +134,7 @@ export async function generateApiIndex(): Promise { const sectionPages: Record> = {} const pageTabs: Record> = {} const tabExamples: Record = {} - const pageCss: Record> = {} + const pageCss: Record = {} const pageCssPrefixes: Record = {} flatEntries.forEach((entry: any) => { diff --git a/src/utils/apiIndex/get.ts b/src/utils/apiIndex/get.ts index e35a662..c25ffdb 100644 --- a/src/utils/apiIndex/get.ts +++ b/src/utils/apiIndex/get.ts @@ -134,7 +134,7 @@ export async function getCssTokens( version: string, section: string, page: string, -): Promise> { +): Promise<{ name: string; value: string; var: string }[]> { const index = await getApiIndex() const { createIndexKey } = await import('../apiHelpers') const key = createIndexKey(version, section, page) diff --git a/src/utils/extractReactTokens.ts b/src/utils/extractReactTokens.ts index 75f352c..0f47ec2 100644 --- a/src/utils/extractReactTokens.ts +++ b/src/utils/extractReactTokens.ts @@ -4,7 +4,7 @@ import { existsSync } from 'fs' /** * Converts a CSS prefix (e.g., "pf-v6-c-accordion") to a token prefix (e.g., "c_accordion") - * + * * @param cssPrefix - The CSS prefix from front matter * @returns The token prefix used in file names */ @@ -15,7 +15,7 @@ function cssPrefixToTokenPrefix(cssPrefix: string): string { /** * Extracts all token objects from @patternfly/react-tokens that match a given CSS prefix - * + * * @param cssPrefix - The CSS prefix (e.g., "pf-v6-c-accordion") * @returns Array of token objects with name, value, and var properties */ @@ -37,8 +37,6 @@ export async function extractReactTokens( ) if (!existsSync(tokensDir)) { - // eslint-disable-next-line no-console - console.error(`Tokens directory not found: ${tokensDir}`) return [] } @@ -68,53 +66,43 @@ export async function extractReactTokens( await Promise.all( matchingFiles.map(async (file) => { - try { - const filePath = join(tokensDir, file) - const fileContent = await readFile(filePath, 'utf8') + const filePath = join(tokensDir, file) + const fileContent = await readFile(filePath, 'utf8') - // Extract the exported object using regex - // Pattern: export const variableName = { "name": "...", "value": "...", "var": "..." }; - // Use non-greedy match to get just the first exported const object - const objectMatch = fileContent.match( - /export const \w+ = \{[\s\S]*?\n\};/, - ) + // Extract the exported object using regex + // Pattern: export const variableName = { "name": "...", "value": "...", "var": "..." }; + // Use non-greedy match to get just the first exported const object + const objectMatch = fileContent.match( + /export const \w+ = \{[\s\S]*?\n\};/, + ) - if (objectMatch) { - // Parse the object string to extract the JSON-like object - const objectContent = objectMatch[0] - .replace(/export const \w+ = /, '') - .replace(/;$/, '') + if (objectMatch) { + // Parse the object string to extract the JSON-like object + const objectContent = objectMatch[0] + .replace(/export const \w+ = /, '') + .replace(/;$/, '') - try { - // Use Function constructor for safe evaluation - // The object content is valid JavaScript, so we can evaluate it - const tokenObject = new Function(`return ${objectContent}`)() as { - name: string - value: string - var: string - } + // Use Function constructor for safe evaluation + // The object content is valid JavaScript, so we can evaluate it + const tokenObject = new Function(`return ${objectContent}`)() as { + name: string + value: string + var: string + } - if ( - tokenObject && - typeof tokenObject === 'object' && - typeof tokenObject.name === 'string' && - typeof tokenObject.value === 'string' && - typeof tokenObject.var === 'string' - ) { - tokenObjects.push({ - name: tokenObject.name, - value: tokenObject.value, - var: tokenObject.var, - }) - } - } catch (evalError) { - // eslint-disable-next-line no-console - console.warn(`Failed to parse object from ${file}:`, evalError) - } + if ( + tokenObject && + typeof tokenObject === 'object' && + typeof tokenObject.name === 'string' && + typeof tokenObject.value === 'string' && + typeof tokenObject.var === 'string' + ) { + tokenObjects.push({ + name: tokenObject.name, + value: tokenObject.value, + var: tokenObject.var, + }) } - } catch (error) { - // eslint-disable-next-line no-console - console.warn(`Failed to read file ${file}:`, error) } }), ) From 87db468afbff9d8a384bea0b44699e147b9f0beb Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Thu, 22 Jan 2026 16:40:31 -0500 Subject: [PATCH 5/6] Update src/utils/extractReactTokens.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/utils/extractReactTokens.ts | 98 ++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/src/utils/extractReactTokens.ts b/src/utils/extractReactTokens.ts index 0f47ec2..558bb9d 100644 --- a/src/utils/extractReactTokens.ts +++ b/src/utils/extractReactTokens.ts @@ -52,61 +52,69 @@ export async function extractReactTokens( // Check if file starts with any of the token prefixes // We want individual token files (e.g., c_accordion__toggle_FontFamily.js) // but not the main component index file (e.g., c_accordion.js) - return tokenPrefixes.some((prefix) => { - if (file === `${prefix}.js`) { - // This is the main component file, skip it - return false - } - return file.startsWith(prefix) - }) - }) - // Import and extract objects from each matching file - const tokenObjects: { name: string; value: string; var: string }[] = [] + const results = await Promise.all( + matchingFiles.map(async (file): Promise<{ name: string; value: string; var: string } | null> => { + try { + const filePath = join(tokensDir, file) + const fileContent = await readFile(filePath, 'utf8') - await Promise.all( - matchingFiles.map(async (file) => { - const filePath = join(tokensDir, file) - const fileContent = await readFile(filePath, 'utf8') + // Extract the exported object using regex + // Pattern: export const variableName = { "name": "...", "value": "...", "var": "..." }; + // Use non-greedy match to get just the first exported const object + const objectMatch = fileContent.match( + /export const \w+ = \{[\s\S]*?\n\};/, + ) - // Extract the exported object using regex - // Pattern: export const variableName = { "name": "...", "value": "...", "var": "..." }; - // Use non-greedy match to get just the first exported const object - const objectMatch = fileContent.match( - /export const \w+ = \{[\s\S]*?\n\};/, - ) + if (objectMatch) { + // Parse the object string to extract the JSON-like object + const objectContent = objectMatch[0] + .replace(/export const \w+ = /, '') + .replace(/;$/, '') - if (objectMatch) { - // Parse the object string to extract the JSON-like object - const objectContent = objectMatch[0] - .replace(/export const \w+ = /, '') - .replace(/;$/, '') + try { + // Use Function constructor for safe evaluation + // The object content is valid JavaScript, so we can evaluate it + const tokenObject = new Function(`return ${objectContent}`)() as { + name: string + value: string + var: string + } - // Use Function constructor for safe evaluation - // The object content is valid JavaScript, so we can evaluate it - const tokenObject = new Function(`return ${objectContent}`)() as { - name: string - value: string - var: string - } - - if ( - tokenObject && - typeof tokenObject === 'object' && - typeof tokenObject.name === 'string' && - typeof tokenObject.value === 'string' && - typeof tokenObject.var === 'string' - ) { - tokenObjects.push({ - name: tokenObject.name, - value: tokenObject.value, - var: tokenObject.var, - }) + if ( + tokenObject && + typeof tokenObject === 'object' && + typeof tokenObject.name === 'string' && + typeof tokenObject.value === 'string' && + typeof tokenObject.var === 'string' + ) { + return { + name: tokenObject.name, + value: tokenObject.value, + var: tokenObject.var, + } + } + } catch (evalError) { + // eslint-disable-next-line no-console + console.warn(`Failed to parse object from ${file}:`, evalError) + } } + } catch (error) { + // eslint-disable-next-line no-console + console.warn(`Failed to read file ${file}:`, error) } + return null }), ) + // Filter out null results + const tokenObjects = results.filter( + (obj): obj is { name: string; value: string; var: string } => obj !== null, + ) + + // Sort by name for consistent ordering + return tokenObjects.sort((a, b) => a.name.localeCompare(b.name)) + // Sort by name for consistent ordering return tokenObjects.sort((a, b) => a.name.localeCompare(b.name)) } From fc561881d4d164895bcc1bd2be1dc123f5b72cd7 Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Thu, 22 Jan 2026 16:54:57 -0500 Subject: [PATCH 6/6] undid code rabbit modification. --- src/utils/extractReactTokens.ts | 98 +++++++++++++++------------------ 1 file changed, 45 insertions(+), 53 deletions(-) diff --git a/src/utils/extractReactTokens.ts b/src/utils/extractReactTokens.ts index 558bb9d..0f47ec2 100644 --- a/src/utils/extractReactTokens.ts +++ b/src/utils/extractReactTokens.ts @@ -52,69 +52,61 @@ export async function extractReactTokens( // Check if file starts with any of the token prefixes // We want individual token files (e.g., c_accordion__toggle_FontFamily.js) // but not the main component index file (e.g., c_accordion.js) + return tokenPrefixes.some((prefix) => { + if (file === `${prefix}.js`) { + // This is the main component file, skip it + return false + } + return file.startsWith(prefix) + }) + }) + // Import and extract objects from each matching file - const results = await Promise.all( - matchingFiles.map(async (file): Promise<{ name: string; value: string; var: string } | null> => { - try { - const filePath = join(tokensDir, file) - const fileContent = await readFile(filePath, 'utf8') + const tokenObjects: { name: string; value: string; var: string }[] = [] - // Extract the exported object using regex - // Pattern: export const variableName = { "name": "...", "value": "...", "var": "..." }; - // Use non-greedy match to get just the first exported const object - const objectMatch = fileContent.match( - /export const \w+ = \{[\s\S]*?\n\};/, - ) + await Promise.all( + matchingFiles.map(async (file) => { + const filePath = join(tokensDir, file) + const fileContent = await readFile(filePath, 'utf8') - if (objectMatch) { - // Parse the object string to extract the JSON-like object - const objectContent = objectMatch[0] - .replace(/export const \w+ = /, '') - .replace(/;$/, '') + // Extract the exported object using regex + // Pattern: export const variableName = { "name": "...", "value": "...", "var": "..." }; + // Use non-greedy match to get just the first exported const object + const objectMatch = fileContent.match( + /export const \w+ = \{[\s\S]*?\n\};/, + ) - try { - // Use Function constructor for safe evaluation - // The object content is valid JavaScript, so we can evaluate it - const tokenObject = new Function(`return ${objectContent}`)() as { - name: string - value: string - var: string - } + if (objectMatch) { + // Parse the object string to extract the JSON-like object + const objectContent = objectMatch[0] + .replace(/export const \w+ = /, '') + .replace(/;$/, '') - if ( - tokenObject && - typeof tokenObject === 'object' && - typeof tokenObject.name === 'string' && - typeof tokenObject.value === 'string' && - typeof tokenObject.var === 'string' - ) { - return { - name: tokenObject.name, - value: tokenObject.value, - var: tokenObject.var, - } - } - } catch (evalError) { - // eslint-disable-next-line no-console - console.warn(`Failed to parse object from ${file}:`, evalError) - } + // Use Function constructor for safe evaluation + // The object content is valid JavaScript, so we can evaluate it + const tokenObject = new Function(`return ${objectContent}`)() as { + name: string + value: string + var: string + } + + if ( + tokenObject && + typeof tokenObject === 'object' && + typeof tokenObject.name === 'string' && + typeof tokenObject.value === 'string' && + typeof tokenObject.var === 'string' + ) { + tokenObjects.push({ + name: tokenObject.name, + value: tokenObject.value, + var: tokenObject.var, + }) } - } catch (error) { - // eslint-disable-next-line no-console - console.warn(`Failed to read file ${file}:`, error) } - return null }), ) - // Filter out null results - const tokenObjects = results.filter( - (obj): obj is { name: string; value: string; var: string } => obj !== null, - ) - - // Sort by name for consistent ordering - return tokenObjects.sort((a, b) => a.name.localeCompare(b.name)) - // Sort by name for consistent ordering return tokenObjects.sort((a, b) => a.name.localeCompare(b.name)) }