From cea0262f6b17dd6656676c46e3a09a3def8a3cc4 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Tue, 10 Mar 2026 14:38:54 -0400 Subject: [PATCH 1/5] fix(api): resolve issue causing the props API to fail when deployed --- .../[version]/[section]/[page]/props.test.ts | 128 +++--------------- .../[version]/[section]/names.test.ts | 123 +++-------------- .../api/[version]/[section]/[page]/props.ts | 19 +-- src/pages/api/[version]/[section]/names.ts | 16 +-- src/pages/props.json.ts | 32 +++++ src/pages/props.ts | 15 +- src/utils/propsData/fetch.ts | 17 +++ 7 files changed, 93 insertions(+), 257 deletions(-) create mode 100644 src/pages/props.json.ts create mode 100644 src/utils/propsData/fetch.ts diff --git a/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/props.test.ts b/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/props.test.ts index 28cc3f7..41317d6 100644 --- a/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/props.test.ts +++ b/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/props.test.ts @@ -1,30 +1,12 @@ import { GET } from '../../../../../../../pages/api/[version]/[section]/[page]/props' -import { getConfig } from '../../../../../../../../cli/getConfig' import { sentenceCase, removeSubsection } from '../../../../../../../utils/case' /** - * Mock getConfig to return a test configuration + * Mock fetchProps to return props data */ -jest.mock('../../../../../../../../cli/getConfig', () => ({ - getConfig: jest.fn().mockResolvedValue({ - outputDir: '/mock/output/dir', - }), -})) - -/** - * Mock node:path join function - */ -const mockJoin = jest.fn((...paths: string[]) => paths.join('/')) -jest.mock('node:path', () => ({ - join: (...args: any[]) => mockJoin(...args), -})) - -/** - * Mock node:fs readFileSync function - */ -const mockReadFileSync = jest.fn() -jest.mock('node:fs', () => ({ - readFileSync: (...args: any[]) => mockReadFileSync(...args), +const mockFetchProps = jest.fn() +jest.mock('../../../../../../../utils/propsData/fetch', () => ({ + fetchProps: (...args: any[]) => mockFetchProps(...args), })) /** @@ -109,10 +91,7 @@ const mockData = { beforeEach(() => { jest.clearAllMocks() - // Reset process.cwd mock - process.cwd = jest.fn(() => '/mock/workspace') - // Reset mockReadFileSync to return default mock data - mockReadFileSync.mockReturnValue(JSON.stringify(mockData)) + mockFetchProps.mockResolvedValue(mockData) }) it('returns props data for a valid page', async () => { @@ -181,12 +160,8 @@ it('returns 400 error when page parameter is missing', async () => { expect(body.error).toContain('Page parameter is required') }) -it('returns 500 error when props.json file is not found', async () => { - mockReadFileSync.mockImplementation(() => { - const error = new Error('ENOENT: no such file or directory') - ; (error as any).code = 'ENOENT' - throw error - }) +it('returns 500 error when fetchProps fails', async () => { + mockFetchProps.mockRejectedValueOnce(new Error('Network error')) const response = await GET({ params: { version: 'v6', section: 'components', page: 'alert' }, @@ -196,13 +171,13 @@ it('returns 500 error when props.json file is not found', async () => { expect(response.status).toBe(500) expect(body).toHaveProperty('error') - expect(body.error).toBe('Props data not found') + expect(body.error).toBe('Failed to load props data') expect(body).toHaveProperty('details') - expect(body.details).toContain('ENOENT') + expect(body.details).toBe('Network error') }) -it('returns 500 error when props.json contains invalid JSON', async () => { - mockReadFileSync.mockReturnValue('invalid json content') +it('returns 500 error when fetchProps throws a non-Error object', async () => { + mockFetchProps.mockRejectedValueOnce('String error') const response = await GET({ params: { version: 'v6', section: 'components', page: 'alert' }, @@ -212,73 +187,9 @@ it('returns 500 error when props.json contains invalid JSON', async () => { expect(response.status).toBe(500) expect(body).toHaveProperty('error') - expect(body.error).toBe('Props data not found') + expect(body.error).toBe('Failed to load props data') expect(body).toHaveProperty('details') -}) - -it('returns 500 error when file read throws an error', async () => { - mockReadFileSync.mockImplementation(() => { - throw new Error('Permission denied') - }) - - const response = await GET({ - params: { version: 'v6', section: 'components', page: 'alert' }, - url: new URL('http://localhost:4321/api/v6/components/alert/props'), - } as any) - const body = await response.json() - - expect(response.status).toBe(500) - expect(body).toHaveProperty('error') - expect(body.error).toBe('Props data not found') - expect(body).toHaveProperty('details') - expect(body.details).toContain('Permission denied') -}) - -it('uses default outputDir when config does not provide one', async () => { - jest.mocked(getConfig).mockResolvedValueOnce({ - content: [], - propsGlobs: [], - outputDir: '', - }) - - const response = await GET({ - params: { version: 'v6', section: 'components', page: 'alert' }, - url: new URL('http://localhost:4321/api/v6/components/alert/props'), - } as any) - const body = await response.json() - - expect(response.status).toBe(200) - expect(body).toHaveProperty('name') - expect(mockJoin).toHaveBeenCalledWith('/mock/workspace/dist', 'props.json') -}) - -it('uses custom outputDir from config when provided', async () => { - jest.mocked(getConfig).mockResolvedValueOnce({ - outputDir: '/custom/output/path', - content: [], - propsGlobs: [], - }) - - const response = await GET({ - params: { version: 'v6', section: 'components', page: 'alert' }, - url: new URL('http://localhost:4321/api/v6/components/alert/props'), - } as any) - const body = await response.json() - - expect(response.status).toBe(200) - expect(body).toHaveProperty('name') - // Verify that join was called with custom outputDir - expect(mockJoin).toHaveBeenCalledWith('/custom/output/path', 'props.json') -}) - -it('reads props.json from the correct file path', async () => { - await GET({ - params: { version: 'v6', section: 'components', page: 'alert' }, - url: new URL('http://localhost:4321/api/v6/components/alert/props'), - } as any) - - // Verify readFileSync was called with the correct path - expect(mockReadFileSync).toHaveBeenCalledWith('/mock/output/dir/props.json') + expect(body.details).toBe('String error') }) it('returns full props structure with all fields', async () => { @@ -328,14 +239,13 @@ it('handles props with required field', async () => { }) it('handles components with empty props array', async () => { - const emptyPropsData = { + mockFetchProps.mockResolvedValueOnce({ 'Empty Component': { name: 'EmptyComponent', description: '', props: [], }, - } - mockReadFileSync.mockReturnValueOnce(JSON.stringify(emptyPropsData)) + }) const response = await GET({ params: { version: 'v6', section: 'components', page: 'empty-component' }, @@ -350,8 +260,6 @@ it('handles components with empty props array', async () => { }) it('handles request when tab is in URL path but not in params', async () => { - // Note: props.ts route is at [page] level, so tab parameter is not available - // This test verifies the route works correctly with just page parameter const response = await GET({ params: { version: 'v6', section: 'components', page: 'alert' }, url: new URL('http://localhost:4321/api/v6/components/alert/react/props'), @@ -364,8 +272,7 @@ it('handles request when tab is in URL path but not in params', async () => { }) it('removes subsection from page name before looking up props', async () => { - // Add test data for checkbox component - const dataWithSubsection = { + mockFetchProps.mockResolvedValueOnce({ ...mockData, Checkbox: { name: 'Checkbox', @@ -378,8 +285,7 @@ it('removes subsection from page name before looking up props', async () => { }, ], }, - } - mockReadFileSync.mockReturnValueOnce(JSON.stringify(dataWithSubsection)) + }) const response = await GET({ params: { version: 'v6', section: 'components', page: 'forms_checkbox' }, diff --git a/src/__tests__/pages/api/__tests__/[version]/[section]/names.test.ts b/src/__tests__/pages/api/__tests__/[version]/[section]/names.test.ts index 23edfde..332685b 100644 --- a/src/__tests__/pages/api/__tests__/[version]/[section]/names.test.ts +++ b/src/__tests__/pages/api/__tests__/[version]/[section]/names.test.ts @@ -1,29 +1,11 @@ import { GET } from '../../../../../../pages/api/[version]/[section]/names' -import { getConfig } from '../../../../../../../cli/getConfig' /** - * Mock getConfig to return a test configuration + * Mock fetchProps to return props data */ -jest.mock('../../../../../../../cli/getConfig', () => ({ - getConfig: jest.fn().mockResolvedValue({ - outputDir: '/mock/output/dir', - }), -})) - -/** - * Mock node:path join function - */ -const mockJoin = jest.fn((...paths: string[]) => paths.join('/')) -jest.mock('node:path', () => ({ - join: (...args: any[]) => mockJoin(...args), -})) - -/** - * Mock node:fs readFileSync function - */ -const mockReadFileSync = jest.fn() -jest.mock('node:fs', () => ({ - readFileSync: (...args: any[]) => mockReadFileSync(...args), +const mockFetchProps = jest.fn() +jest.mock('../../../../../../utils/propsData/fetch', () => ({ + fetchProps: (...args: any[]) => mockFetchProps(...args), })) const mockData = { @@ -86,10 +68,7 @@ const mockData = { beforeEach(() => { jest.clearAllMocks() - // Reset process.cwd mock - process.cwd = jest.fn(() => '/mock/workspace') - // Reset mockReadFileSync to return default mock data - mockReadFileSync.mockReturnValue(JSON.stringify(mockData)) + mockFetchProps.mockResolvedValue(mockData) }) it('returns filtered component names from props.json data', async () => { @@ -110,7 +89,7 @@ it('returns filtered component names from props.json data', async () => { }) it('filters out all keys containing "Props" case-insensitively', async () => { - const testData = { + mockFetchProps.mockResolvedValueOnce({ Alert: {}, Button: {}, AlertProps: {}, @@ -118,9 +97,7 @@ it('filters out all keys containing "Props" case-insensitively', async () => { alertprops: {}, ComponentProps: {}, SomeComponentProps: {}, - } - - mockReadFileSync.mockReturnValue(JSON.stringify(testData)) + }) const response = await GET({ params: { version: 'v6', section: 'components' }, @@ -138,13 +115,11 @@ it('filters out all keys containing "Props" case-insensitively', async () => { }) it('returns empty array when props.json has no valid component names', async () => { - const testData = { + mockFetchProps.mockResolvedValueOnce({ AlertProps: {}, ButtonProps: {}, ComponentProps: {}, - } - - mockReadFileSync.mockReturnValue(JSON.stringify(testData)) + }) const response = await GET({ params: { version: 'v6', section: 'components' }, @@ -158,7 +133,7 @@ it('returns empty array when props.json has no valid component names', async () }) it('returns empty array when props.json is empty', async () => { - mockReadFileSync.mockReturnValue(JSON.stringify({})) + mockFetchProps.mockResolvedValueOnce({}) const response = await GET({ params: { version: 'v6', section: 'components' }, @@ -171,12 +146,8 @@ it('returns empty array when props.json is empty', async () => { expect(body).toEqual([]) }) -it('returns 500 error when props.json file is not found', async () => { - mockReadFileSync.mockImplementation(() => { - const error = new Error('ENOENT: no such file or directory') - ; (error as any).code = 'ENOENT' - throw error - }) +it('returns 500 error when fetchProps fails', async () => { + mockFetchProps.mockRejectedValueOnce(new Error('Network error')) const response = await GET({ params: { version: 'v6', section: 'components' }, @@ -188,11 +159,11 @@ it('returns 500 error when props.json file is not found', async () => { expect(body).toHaveProperty('error') expect(body.error).toBe('Component names data not found') expect(body).toHaveProperty('details') - expect(body.details).toContain('ENOENT') + expect(body.details).toBe('Network error') }) -it('returns 500 error when props.json contains invalid JSON', async () => { - mockReadFileSync.mockReturnValue('invalid json content') +it('returns 500 error when fetchProps throws a non-Error object', async () => { + mockFetchProps.mockRejectedValueOnce('String error') const response = await GET({ params: { version: 'v6', section: 'components' }, @@ -204,67 +175,5 @@ it('returns 500 error when props.json contains invalid JSON', async () => { expect(body).toHaveProperty('error') expect(body.error).toBe('Component names data not found') expect(body).toHaveProperty('details') -}) - -it('returns 500 error when file read throws an error', async () => { - mockReadFileSync.mockImplementation(() => { - throw new Error('Permission denied') - }) - - const response = await GET({ - params: { version: 'v6', section: 'components' }, - url: new URL('http://localhost:4321/api/v6/components/names'), - } as any) - const body = await response.json() - - expect(response.status).toBe(500) - expect(body).toHaveProperty('error') - expect(body.error).toBe('Component names data not found') - expect(body).toHaveProperty('details') - expect(body.details).toContain('Permission denied') -}) - -it('uses default outputDir when config does not provide one', async () => { - jest.mocked(getConfig).mockResolvedValueOnce({ - content: [], - propsGlobs: [], - outputDir: '', - }) - - const response = await GET({ - params: { version: 'v6', section: 'components' }, - url: new URL('http://localhost:4321/api/v6/components/names'), - } as any) - const body = await response.json() - - expect(response.status).toBe(200) - expect(Array.isArray(body)).toBe(true) - expect(mockJoin).toHaveBeenCalledWith('/mock/workspace/dist', 'props.json') -}) - -it('uses custom outputDir from config when provided', async () => { - jest.mocked(getConfig).mockResolvedValueOnce({ - outputDir: '/custom/output/path', - content: [], - propsGlobs: [], - }) - - const response = await GET({ - params: { version: 'v6', section: 'components' }, - url: new URL('http://localhost:4321/api/v6/components/names'), - } as any) - const body = await response.json() - - expect(response.status).toBe(200) - expect(Array.isArray(body)).toBe(true) - expect(mockJoin).toHaveBeenCalledWith('/custom/output/path', 'props.json') -}) - -it('reads props.json from the correct file path', async () => { - await GET({ - params: { version: 'v6', section: 'components' }, - url: new URL('http://localhost:4321/api/v6/components/names'), - } as any) - - expect(mockReadFileSync).toHaveBeenCalledWith('/mock/output/dir/props.json') + expect(body.details).toBe('String error') }) diff --git a/src/pages/api/[version]/[section]/[page]/props.ts b/src/pages/api/[version]/[section]/[page]/props.ts index 15333be..4a34515 100644 --- a/src/pages/api/[version]/[section]/[page]/props.ts +++ b/src/pages/api/[version]/[section]/[page]/props.ts @@ -1,13 +1,11 @@ import type { APIRoute } from 'astro' import { createJsonResponse } from '../../../../../utils/apiHelpers' -import { getConfig } from '../../../../../../cli/getConfig' -import { join } from 'node:path' -import { readFileSync } from 'node:fs' +import { fetchProps } from '../../../../../utils/propsData/fetch' import { sentenceCase, removeSubsection } from '../../../../../utils/case' export const prerender = false -export const GET: APIRoute = async ({ params }) => { +export const GET: APIRoute = async ({ params, url }) => { const { page } = params if (!page) { @@ -18,13 +16,7 @@ export const GET: APIRoute = async ({ params }) => { } try { - const config = await getConfig(`${process.cwd()}/pf-docs.config.mjs`) - const outputDir = config?.outputDir || join(process.cwd(), 'dist') - - const propsFilePath = join(outputDir, 'props.json') - const propsDataFile = readFileSync(propsFilePath) - const props = JSON.parse(propsDataFile.toString()) - + const props = await fetchProps(url) const propsData = props[sentenceCase(removeSubsection(page))] if (propsData === undefined) { @@ -39,11 +31,8 @@ export const GET: APIRoute = async ({ params }) => { } catch (error) { const details = error instanceof Error ? error.message : String(error) return createJsonResponse( - { error: 'Props data not found', details }, + { error: 'Failed to load props data', details }, 500, ) } } - - - diff --git a/src/pages/api/[version]/[section]/names.ts b/src/pages/api/[version]/[section]/names.ts index af41ded..5354fd9 100644 --- a/src/pages/api/[version]/[section]/names.ts +++ b/src/pages/api/[version]/[section]/names.ts @@ -1,19 +1,12 @@ import type { APIRoute } from 'astro' import { createJsonResponse } from '../../../../utils/apiHelpers' -import { getConfig } from '../../../../../cli/getConfig' -import { join } from 'node:path' -import { readFileSync } from 'node:fs' +import { fetchProps } from '../../../../utils/propsData/fetch' export const prerender = false -export const GET: APIRoute = async ({ }) => { +export const GET: APIRoute = async ({ url }) => { try { - const config = await getConfig(`${process.cwd()}/pf-docs.config.mjs`) - const outputDir = config?.outputDir || join(process.cwd(), 'dist') - - const propsFilePath = join(outputDir, 'props.json') - const propsDataFile = readFileSync(propsFilePath) - const props = JSON.parse(propsDataFile.toString()) + const props = await fetchProps(url) const propsKey = new RegExp("Props", 'i'); // ignore ComponentProps objects const names = Object.keys(props).filter(name => !propsKey.test(name)) @@ -27,6 +20,3 @@ export const GET: APIRoute = async ({ }) => { ) } } - - - diff --git a/src/pages/props.json.ts b/src/pages/props.json.ts new file mode 100644 index 0000000..c22a586 --- /dev/null +++ b/src/pages/props.json.ts @@ -0,0 +1,32 @@ +import type { APIRoute } from 'astro' +import { readFile } from 'fs/promises' +import { join } from 'path' +import { getOutputDir } from '../utils/getOutputDir' + +// Prerender at build time so this doesn't run in the Cloudflare Worker +export const prerender = true + +export const GET: APIRoute = async () => { + try { + const outputDir = await getOutputDir() + const propsFilePath = join(outputDir, 'props.json') + const propsData = await readFile(propsFilePath, 'utf-8') + + return new Response(propsData, { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }) + } catch (error) { + return new Response( + JSON.stringify({ error: 'Failed to load props data', details: error }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + } + ) + } +} diff --git a/src/pages/props.ts b/src/pages/props.ts index 8b8373f..6533ec0 100644 --- a/src/pages/props.ts +++ b/src/pages/props.ts @@ -1,19 +1,12 @@ -import { readFileSync } from 'node:fs' -import { join } from 'node:path' -import { getConfig } from '../../cli/getConfig' +import { fetchProps } from '../utils/propsData/fetch' export const prerender = false export async function GET({ request }: { request: Request }) { - const config = await getConfig(`${process.cwd()}/pf-docs.config.mjs`) - const outputDir = config?.outputDir || join(process.cwd(), 'dist') + const url = new URL(request.url) + const props = await fetchProps(url) - const propsFilePath = join(outputDir, 'props.json') - const propsDataFile = readFileSync(propsFilePath) - const props = JSON.parse(propsDataFile.toString()) - - const queryParams = new URL(request.url).searchParams - const components = queryParams.get('components') + const components = url.searchParams.get('components') const componentsArray = components?.split(',') const propsData = componentsArray?.map((component) => props[component]) diff --git a/src/utils/propsData/fetch.ts b/src/utils/propsData/fetch.ts new file mode 100644 index 0000000..451eec7 --- /dev/null +++ b/src/utils/propsData/fetch.ts @@ -0,0 +1,17 @@ +/** + * Fetches the props data from the server as a static asset + * Used by API routes at runtime instead of reading from the filesystem + * + * @param url - The URL object from the API route context + * @returns Promise resolving to the props data structure + */ +export async function fetchProps(url: URL): Promise> { + const propsUrl = new URL('/props.json', url.origin) + const response = await fetch(propsUrl) + + if (!response.ok) { + throw new Error(`Failed to load props data: ${response.status} ${response.statusText}`) + } + + return await response.json() +} From 9938e8297315e8e3db104338699081b604a2fbef Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Tue, 10 Mar 2026 15:26:10 -0400 Subject: [PATCH 2/5] chore(api): throw error when props.json fails to prerender --- src/pages/props.json.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/pages/props.json.ts b/src/pages/props.json.ts index c22a586..77d21a0 100644 --- a/src/pages/props.json.ts +++ b/src/pages/props.json.ts @@ -19,14 +19,7 @@ export const GET: APIRoute = async () => { }, }) } catch (error) { - return new Response( - JSON.stringify({ error: 'Failed to load props data', details: error }), - { - status: 500, - headers: { - 'Content-Type': 'application/json', - }, - } - ) + const details = error instanceof Error ? error.message : String(error) + throw new Error(`Failed to prerender /props.json: ${details}`) } } From d1589af7808ab2dc17553f71fe42c129b6f4559a Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Tue, 10 Mar 2026 15:26:23 -0400 Subject: [PATCH 3/5] chore(api): add error handling to /props API --- src/pages/props.ts | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/pages/props.ts b/src/pages/props.ts index 6533ec0..d3b29c9 100644 --- a/src/pages/props.ts +++ b/src/pages/props.ts @@ -1,14 +1,31 @@ import { fetchProps } from '../utils/propsData/fetch' +import { createJsonResponse } from '../utils/apiHelpers' export const prerender = false export async function GET({ request }: { request: Request }) { const url = new URL(request.url) - const props = await fetchProps(url) const components = url.searchParams.get('components') - const componentsArray = components?.split(',') - const propsData = componentsArray?.map((component) => props[component]) + if (!components) { + return createJsonResponse( + { error: 'components query parameter is required' }, + 400, + ) + } - return new Response(JSON.stringify(propsData)) + try { + const props = await fetchProps(url) + const propsData = components + .split(',') + .map((component) => props[component.trim()]) + + return createJsonResponse(propsData) + } catch (error) { + const details = error instanceof Error ? error.message : String(error) + return createJsonResponse( + { error: 'Failed to load props data', details }, + 500, + ) + } } From e346cf3efc77722a93fb8b7874256fc5398f32f9 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Mon, 16 Mar 2026 16:11:13 -0400 Subject: [PATCH 4/5] fix(api): remove sentence casing when fetching prop data --- src/pages/api/[version]/[section]/[page]/props.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/api/[version]/[section]/[page]/props.ts b/src/pages/api/[version]/[section]/[page]/props.ts index 4a34515..76dc8a9 100644 --- a/src/pages/api/[version]/[section]/[page]/props.ts +++ b/src/pages/api/[version]/[section]/[page]/props.ts @@ -1,7 +1,7 @@ import type { APIRoute } from 'astro' import { createJsonResponse } from '../../../../../utils/apiHelpers' import { fetchProps } from '../../../../../utils/propsData/fetch' -import { sentenceCase, removeSubsection } from '../../../../../utils/case' +import { removeSubsection } from '../../../../../utils/case' export const prerender = false @@ -17,7 +17,7 @@ export const GET: APIRoute = async ({ params, url }) => { try { const props = await fetchProps(url) - const propsData = props[sentenceCase(removeSubsection(page))] + const propsData = props[removeSubsection(page)] if (propsData === undefined) { return createJsonResponse( From 18005f1dc00aca32f53f9e2a60d7f2458c999605 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Mon, 16 Mar 2026 17:45:10 -0400 Subject: [PATCH 5/5] fix(api): update props API to pascal case page names --- .../[version]/[section]/[page]/props.test.ts | 34 ++++++------------- .../api/[version]/[section]/[page]/props.ts | 4 ++- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/props.test.ts b/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/props.test.ts index 41317d6..687dc7e 100644 --- a/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/props.test.ts +++ b/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/props.test.ts @@ -1,5 +1,5 @@ import { GET } from '../../../../../../../pages/api/[version]/[section]/[page]/props' -import { sentenceCase, removeSubsection } from '../../../../../../../utils/case' +import { removeSubsection } from '../../../../../../../utils/case' /** * Mock fetchProps to return props data @@ -13,13 +13,6 @@ jest.mock('../../../../../../../utils/propsData/fetch', () => ({ * Mock sentenceCase and removeSubsection utilities */ jest.mock('../../../../../../../utils/case', () => ({ - sentenceCase: jest.fn((id: string) => - // Simple mock: convert kebab-case to Sentence Case - id - .split('-') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' ') - ), removeSubsection: jest.fn((page: string) => { // Simple mock: remove subsection prefix from page name if (page.includes('_')) { @@ -52,7 +45,7 @@ const mockData = { }, ], }, - 'Sample Data Row': { + SampleDataRow: { name: 'SampleDataRow', description: '', props: [ @@ -64,7 +57,7 @@ const mockData = { }, ], }, - 'Dashboard Wrapper': { + DashboardWrapper: { name: 'DashboardWrapper', description: '', props: [ @@ -75,7 +68,7 @@ const mockData = { }, ], }, - 'Keyboard Handler': { + KeyboardHandler: { name: 'KeyboardHandler', description: '', props: [ @@ -87,6 +80,11 @@ const mockData = { }, ], }, + EmptyComponent: { + name: 'EmptyComponent', + description: '', + props: [], + }, } beforeEach(() => { @@ -108,10 +106,9 @@ it('returns props data for a valid page', async () => { expect(body).toHaveProperty('props') expect(body.name).toBe('Alert') expect(Array.isArray(body.props)).toBe(true) - expect(sentenceCase).toHaveBeenCalledWith('alert') }) -it('converts kebab-case page name to sentence case for lookup', async () => { +it('converts kebab-case page name to pascal case for lookup', async () => { const response = await GET({ params: { version: 'v6', section: 'components', page: 'sample-data-row' }, url: new URL('http://localhost:4321/api/v6/components/sample-data-row/props'), @@ -120,7 +117,6 @@ it('converts kebab-case page name to sentence case for lookup', async () => { expect(response.status).toBe(200) expect(body.name).toBe('SampleDataRow') - expect(sentenceCase).toHaveBeenCalledWith('sample-data-row') }) it('handles multi-word page names correctly', async () => { @@ -132,7 +128,6 @@ it('handles multi-word page names correctly', async () => { expect(response.status).toBe(200) expect(body.name).toBe('DashboardWrapper') - expect(sentenceCase).toHaveBeenCalledWith('dashboard-wrapper') }) it('returns 404 error when props data is not found', async () => { @@ -239,14 +234,6 @@ it('handles props with required field', async () => { }) it('handles components with empty props array', async () => { - mockFetchProps.mockResolvedValueOnce({ - 'Empty Component': { - name: 'EmptyComponent', - description: '', - props: [], - }, - }) - const response = await GET({ params: { version: 'v6', section: 'components', page: 'empty-component' }, url: new URL('http://localhost:4321/api/v6/components/empty-component/props'), @@ -296,5 +283,4 @@ it('removes subsection from page name before looking up props', async () => { expect(response.status).toBe(200) expect(body.name).toBe('Checkbox') expect(removeSubsection).toHaveBeenCalledWith('forms_checkbox') - expect(sentenceCase).toHaveBeenCalledWith('checkbox') }) diff --git a/src/pages/api/[version]/[section]/[page]/props.ts b/src/pages/api/[version]/[section]/[page]/props.ts index 76dc8a9..e4fac86 100644 --- a/src/pages/api/[version]/[section]/[page]/props.ts +++ b/src/pages/api/[version]/[section]/[page]/props.ts @@ -1,4 +1,6 @@ import type { APIRoute } from 'astro' +import { pascalCase } from 'change-case' + import { createJsonResponse } from '../../../../../utils/apiHelpers' import { fetchProps } from '../../../../../utils/propsData/fetch' import { removeSubsection } from '../../../../../utils/case' @@ -17,7 +19,7 @@ export const GET: APIRoute = async ({ params, url }) => { try { const props = await fetchProps(url) - const propsData = props[removeSubsection(page)] + const propsData = props[pascalCase(removeSubsection(page))] if (propsData === undefined) { return createJsonResponse(