From 9fd87fb9049301c7d1f79dbf4b8bf54045fef5f1 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Fri, 5 Dec 2025 12:45:24 -0500 Subject: [PATCH 01/12] fix(API): address issues with text API in monorepos --- README.mdx | 1 + .../[version]/[section]/[page]/[tab].test.ts | 2 +- .../api/[version]/[section]/[page]/[tab].ts | 14 +++- src/utils/__tests__/packageUtils.test.ts | 69 ++++++++++++++++++- src/utils/apiIndex/generate.ts | 9 ++- src/utils/apiIndex/get.ts | 4 +- src/utils/getOutputDir.ts | 19 +++++ src/utils/packageUtils.ts | 8 ++- 8 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 src/utils/getOutputDir.ts diff --git a/README.mdx b/README.mdx index 26c5fce..139b7a5 100644 --- a/README.mdx +++ b/README.mdx @@ -33,6 +33,7 @@ Any static assets, like images, can be placed in the `public/` directory. To define the markdown schema this project uses a typescript based schema known as [Zod](https://zod.dev). Details of how this is integratred into Astro can be found in Astros documentation on [content creation using Zod](https://docs.astro.build/en/guides/content-collections/#defining-datatypes-with-zod). +Note: When running in dev mode locally, API endpoints are not available on a clean repo until either a build has been done or a tab route has been hit. ### 🧞 Commands All commands are run from the root of the project, from a terminal: diff --git a/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/[tab].test.ts b/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/[tab].test.ts index 656c68e..acd9784 100644 --- a/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/[tab].test.ts +++ b/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/[tab].test.ts @@ -86,7 +86,7 @@ jest.mock('../../../../../../../utils', () => ({ .replace(/[\s_]+/g, '-') .toLowerCase() }), - getDefaultTab: jest.fn((filePath?: string) => { + getDefaultTabForApi: jest.fn((filePath?: string) => { if (!filePath) { return 'react' } diff --git a/src/pages/api/[version]/[section]/[page]/[tab].ts b/src/pages/api/[version]/[section]/[page]/[tab].ts index 9af8e57..d770764 100644 --- a/src/pages/api/[version]/[section]/[page]/[tab].ts +++ b/src/pages/api/[version]/[section]/[page]/[tab].ts @@ -5,8 +5,8 @@ import { getCollection } from 'astro:content' import { content } from '../../../../../content' import { kebabCase, - getDefaultTab, addDemosOrDeprecated, + getDefaultTabForApi, } from '../../../../../utils' import { generateAndWriteApiIndex } from '../../../../../utils/apiIndex/generate' import { getApiIndex } from '../../../../../utils/apiIndex/get' @@ -44,7 +44,15 @@ export const getStaticPaths: GetStaticPaths = async () => { } } - return paths + // This shouldn't happen since we have a fallback tab value, but if it somehow does we need to alert the user + paths.forEach((path) => { + if (!path.params.tab) { + console.warn(`[API Warning] Tab not found for path: ${path.params.version}/${path.params.section}/${path.params.page}`) + } + }) + + // Again, this shouldn't happen since we have a fallback tab value, but if it somehow does and we don't filter out tabless paths it will crash the build + return paths.filter((path) => !!path.params.tab) } export const GET: APIRoute = async ({ params }) => { @@ -109,7 +117,7 @@ export const GET: APIRoute = async ({ params }) => { ...rest, data: { ...data, - tab: data.tab || data.source || getDefaultTab(filePath), + tab: data.tab || data.source || getDefaultTabForApi(filePath), }, })) diff --git a/src/utils/__tests__/packageUtils.test.ts b/src/utils/__tests__/packageUtils.test.ts index fbbc4fe..72faa81 100644 --- a/src/utils/__tests__/packageUtils.test.ts +++ b/src/utils/__tests__/packageUtils.test.ts @@ -1,4 +1,4 @@ -import { getPackageName, getTabBase, getDefaultTab, addDemosOrDeprecated } from '../packageUtils' +import { getPackageName, getTabBase, getDefaultTab, getDefaultTabForApi, addDemosOrDeprecated } from '../packageUtils' describe('getPackageName', () => { it('returns empty string for empty input', () => { @@ -192,3 +192,70 @@ describe('getDefaultTab', () => { expect(getDefaultTab(filePath)).toBe('') }) }) + +describe('getDefaultTabForApi', () => { + it('returns base tab for regular patternfly package path', () => { + const filePath = '/path/to/node_modules/@patternfly/patternfly/dist/index.js' + expect(getDefaultTabForApi(filePath)).toBe('html') + }) + + it('returns base tab for regular react-core package path', () => { + const filePath = '/path/to/node_modules/@patternfly/react-core/dist/index.js' + expect(getDefaultTabForApi(filePath)).toBe('react') + }) + + it('returns demos tab for demos path with patternfly package', () => { + const filePath = '/path/to/node_modules/@patternfly/patternfly/demos/Button.js' + expect(getDefaultTabForApi(filePath)).toBe('html-demos') + }) + + it('returns demos tab for demos path with react-core package', () => { + const filePath = '/path/to/node_modules/@patternfly/react-core/demos/Button.js' + expect(getDefaultTabForApi(filePath)).toBe('react-demos') + }) + + it('returns deprecated tab for deprecated path with patternfly package', () => { + const filePath = '/path/to/node_modules/@patternfly/patternfly/deprecated/OldButton.js' + expect(getDefaultTabForApi(filePath)).toBe('html-deprecated') + }) + + it('returns deprecated tab for deprecated path with react-core package', () => { + const filePath = '/path/to/node_modules/@patternfly/react-core/deprecated/OldButton.js' + expect(getDefaultTabForApi(filePath)).toBe('react-deprecated') + }) + + it('adds both demos and deprecated when both are in path', () => { + const filePath = '/path/to/node_modules/@patternfly/react-core/demos/deprecated/Button.js' + expect(getDefaultTabForApi(filePath)).toBe('react-demos-deprecated') + }) + + it('returns "text" fallback for unknown package', () => { + const filePath = '/path/to/node_modules/unknown-package/dist/index.js' + expect(getDefaultTabForApi(filePath)).toBe('text') + }) + + it('returns "text" fallback for path without node_modules', () => { + const filePath = '/path/to/some/file.js' + expect(getDefaultTabForApi(filePath)).toBe('text') + }) + + it('returns "text" fallback for empty input', () => { + expect(getDefaultTabForApi('')).toBe('text') + }) + + it('returns "text" fallback for null/undefined input', () => { + expect(getDefaultTabForApi(null as any)).toBe('text') + expect(getDefaultTabForApi(undefined)).toBe('text') + expect(getDefaultTabForApi()).toBe('text') + }) + + it('returns "text" fallback for unknown package with demos path', () => { + const filePath = '/path/to/node_modules/unknown-package/demos/Button.js' + expect(getDefaultTabForApi(filePath)).toBe('text') + }) + + it('returns "text" fallback for unknown package with deprecated path', () => { + const filePath = '/path/to/node_modules/unknown-package/deprecated/Button.js' + expect(getDefaultTabForApi(filePath)).toBe('text') + }) +}) diff --git a/src/utils/apiIndex/generate.ts b/src/utils/apiIndex/generate.ts index 6fab08a..46b9fd8 100644 --- a/src/utils/apiIndex/generate.ts +++ b/src/utils/apiIndex/generate.ts @@ -4,7 +4,9 @@ import { writeFile } from 'fs/promises' import { getCollection } from 'astro:content' import type { CollectionKey } from 'astro:content' import { content } from '../../content' -import { kebabCase, getDefaultTab, addDemosOrDeprecated } from '../index' +import { kebabCase, addDemosOrDeprecated } from '../index' +import { getDefaultTabForApi } from '../packageUtils' +import { getOutputDir } from '../getOutputDir' const SOURCE_ORDER: Record = { react: 1, @@ -107,7 +109,7 @@ export async function generateApiIndex(): Promise { // Collect tab const entryTab = - entry.data.tab || entry.data.source || getDefaultTab(entry.filePath) + entry.data.tab || entry.data.source || getDefaultTabForApi(entry.filePath) const tab = addDemosOrDeprecated(entryTab, entry.id) if (!pageTabs[pageKey]) { pageTabs[pageKey] = new Set() @@ -137,7 +139,8 @@ export async function generateApiIndex(): Promise { * @param index - The API index structure to write */ export async function writeApiIndex(index: ApiIndex): Promise { - const indexPath = join(process.cwd(), 'src', 'apiIndex.json') + const outputDir = await getOutputDir() + const indexPath = join(outputDir, 'apiIndex.json') try { await writeFile(indexPath, JSON.stringify(index, null, 2)) diff --git a/src/utils/apiIndex/get.ts b/src/utils/apiIndex/get.ts index 1f42458..333b273 100644 --- a/src/utils/apiIndex/get.ts +++ b/src/utils/apiIndex/get.ts @@ -1,6 +1,7 @@ import { join } from 'path' import { readFile } from 'fs/promises' import type { ApiIndex } from './generate' +import { getOutputDir } from '../getOutputDir' /** * Reads and parses the API index file @@ -10,7 +11,8 @@ import type { ApiIndex } from './generate' * @throws Error if index file is not found, contains invalid JSON, or has invalid structure */ export async function getApiIndex(): Promise { - const indexPath = join(process.cwd(), 'src', 'apiIndex.json') + const outputDir = await getOutputDir() + const indexPath = join(outputDir, 'apiIndex.json') try { const content = await readFile(indexPath, 'utf-8') diff --git a/src/utils/getOutputDir.ts b/src/utils/getOutputDir.ts new file mode 100644 index 0000000..698950c --- /dev/null +++ b/src/utils/getOutputDir.ts @@ -0,0 +1,19 @@ +import { join } from 'path' +import { getConfig } from '../../cli/getConfig' + +export async function getOutputDir(): Promise { + const config = await getConfig(join(process.cwd(), 'pf-docs.config.mjs')) + if (!config) { + throw new Error( + 'No config found, please run the `setup` command or manually create a pf-docs.config.mjs file', + ) + } + + if (!config.outputDir) { + throw new Error( + 'No outputDir found in config file, an output directory must be defined in your config file e.g. "dist"', + ) + } + + return config.outputDir +} diff --git a/src/utils/packageUtils.ts b/src/utils/packageUtils.ts index c47babe..154797b 100644 --- a/src/utils/packageUtils.ts +++ b/src/utils/packageUtils.ts @@ -61,7 +61,9 @@ export const getDefaultTab = (filePath?: string): string => { const packageName = getPackageName(filePath) const tabBase = getTabBase(packageName) - const tab = addDemosOrDeprecated(tabBase, filePath) - - return tab + return addDemosOrDeprecated(tabBase, filePath) } + +// This function is specifically for API routes where we need a fallback tab name +// to ensure content is always accessible even when the default tab logic doesn't apply +export const getDefaultTabForApi = (filePath?: string): string => getDefaultTab(filePath) || 'text' From d75e164195cd8392a04237b2718643385a6ef421 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Mon, 8 Dec 2025 15:56:20 -0500 Subject: [PATCH 02/12] Prevent the api index from being bundled, which can cause it to be outdated --- README.mdx | 2 +- astro.config.mjs | 5 +++++ package.json | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/README.mdx b/README.mdx index 139b7a5..bfb2d58 100644 --- a/README.mdx +++ b/README.mdx @@ -33,7 +33,7 @@ Any static assets, like images, can be placed in the `public/` directory. To define the markdown schema this project uses a typescript based schema known as [Zod](https://zod.dev). Details of how this is integratred into Astro can be found in Astros documentation on [content creation using Zod](https://docs.astro.build/en/guides/content-collections/#defining-datatypes-with-zod). -Note: When running in dev mode locally, API endpoints are not available on a clean repo until either a build has been done or a tab route has been hit. +Note: When running in dev mode locally, API endpoints are not available on a clean repo until a build has been done ### 🧞 Commands All commands are run from the root of the project, from a terminal: diff --git a/astro.config.mjs b/astro.config.mjs index 4a9622f..1ebad5c 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -13,6 +13,11 @@ export default defineConfig({ noExternal: ["@patternfly/*", "react-dropzone"], external: ["node:fs", "node:path"] }, + build: { + rollupOptions: { + external: [/apiIndex\.json$/] + } + }, server: { fs: { allow: ['./'] diff --git a/package.json b/package.json index 9fee6b4..b0031eb 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "test": "jest", "test:watch": "jest --watch", "semantic-release": "semantic-release", - "cf-typegen": "wrangler types" + "cf-typegen": "wrangler types", + "clean": "rm -rf dist .astro .wrangler" }, "main": "dist/cli/cli.js", "bin": "./dist/cli/cli.js", From 7b27a98fcb96e79ef834ddac86a557b812ec0764 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Mon, 8 Dec 2025 16:13:46 -0500 Subject: [PATCH 03/12] Update to output the apiIndex to the user specified output directory --- cli/cli.ts | 54 ++++++++++++++----- jest.config.ts | 3 +- .../pages/api/__tests__/[version].test.ts | 2 +- .../pages/api/__tests__/versions.test.ts | 2 +- src/pages/api/[version].ts | 2 +- src/pages/api/[version]/[section].ts | 2 +- src/pages/api/[version]/[section]/[page].ts | 2 +- src/pages/api/openapi.json.ts | 2 +- src/pages/api/versions.ts | 2 +- src/utils/apiIndex/generate.ts | 2 +- tsconfig.json | 16 ++++-- 11 files changed, 64 insertions(+), 25 deletions(-) diff --git a/cli/cli.ts b/cli/cli.ts index 95f250f..f1563fb 100755 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -12,7 +12,7 @@ import { symLinkConfig } from './symLinkConfig.js' import { buildPropsData } from './buildPropsData.js' import { hasFile } from './hasFile.js' import { convertToMDX } from './convertToMDX.js' -import { mkdir, copyFile } from 'fs/promises' +import { mkdir, copyFile, readFile, writeFile } from 'fs/promises' import { fileExists } from './fileExists.js' const currentDir = process.cwd() @@ -87,29 +87,52 @@ async function transformMDContentToMDX() { } } -async function initializeApiIndex() { - const templateIndexPath = join(astroRoot, 'cli', 'templates', 'apiIndex.json') - const targetIndexPath = join(astroRoot, 'src', 'apiIndex.json') +async function updateTsConfigOutputDirPath(program: Command) { + const { verbose } = program.opts() + const tsConfigPath = join(astroRoot, 'tsconfig.json') + + try { + const tsConfigFile = await readFile(tsConfigPath, 'utf-8') + const tsConfig = JSON.parse(tsConfigFile) + const formattedOutputDir = join(absoluteOutputDir, '*') + + tsConfig.compilerOptions.paths["outputDir/*"] = [formattedOutputDir] + + await writeFile(tsConfigPath, JSON.stringify(tsConfig, null, 2)) + + if (verbose) { + console.log(`Updated tsconfig.json with outputDir path: ${formattedOutputDir}`) + } + } catch (e: any) { + console.error('Error updating tsconfig.json with outputDir path:', e) + } +} +async function initializeApiIndex(program: Command) { + const { verbose } = program.opts() + const templateIndexPath = join(astroRoot, 'cli', 'templates', 'apiIndex.json') + const targetIndexPath = join(absoluteOutputDir, 'apiIndex.json') const indexExists = await fileExists(targetIndexPath) // early return if the file exists from a previous build if (indexExists) { - console.log('apiIndex.json already exists, skipping initialization') + if (verbose) { + console.log('apiIndex.json already exists, skipping initialization') + } return } try { await copyFile(templateIndexPath, targetIndexPath) - console.log('Initialized apiIndex.json') + if (verbose) { + console.log('Initialized apiIndex.json') + } } catch (e: any) { console.error('Error copying apiIndex.json template:', e) } } async function buildProject(): Promise { - await updateContent(program) - await generateProps(program, true) if (!config) { console.error( 'No config found, please run the `setup` command or manually create a pf-docs.config.mjs file', @@ -123,13 +146,17 @@ async function buildProject(): Promise { ) return config } - - await initializeApiIndex() + await updateTsConfigOutputDirPath(program) + await updateContent(program) + await generateProps(program, true) + await initializeApiIndex(program) await transformMDContentToMDX() - build({ + const docsOutputDir = join(absoluteOutputDir, 'docs') + + await build({ root: astroRoot, - outDir: join(absoluteOutputDir, 'docs'), + outDir: docsOutputDir, }) return config @@ -193,8 +220,9 @@ program.command('init').action(async () => { }) program.command('start').action(async () => { + await updateTsConfigOutputDirPath(program) await updateContent(program) - await initializeApiIndex() + await initializeApiIndex(program) // if a props file hasn't been generated yet, but the consumer has propsData, it will cause a runtime error so to // prevent that we're just creating a props file regardless of what they say if one doesn't exist yet diff --git a/jest.config.ts b/jest.config.ts index d9d5be4..cde6fa9 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -16,9 +16,10 @@ const config: Config = { testPathIgnorePatterns: ['/node_modules/', '/__tests__/helpers/'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], moduleNameMapper: { + '^outputDir/(.*)$': '/dist/$1', '\\.(css|less)$': '/src/__mocks__/styleMock.ts', - '(.+)\\.js': '$1', '^astro:content$': '/src/__mocks__/astro-content.ts', + '(.+)\\.js': '$1', }, setupFilesAfterEnv: ['/test.setup.ts'], transformIgnorePatterns: [ diff --git a/src/__tests__/pages/api/__tests__/[version].test.ts b/src/__tests__/pages/api/__tests__/[version].test.ts index 531f69c..e92369b 100644 --- a/src/__tests__/pages/api/__tests__/[version].test.ts +++ b/src/__tests__/pages/api/__tests__/[version].test.ts @@ -4,7 +4,7 @@ import { GET } from '../../../../pages/api/[version]' * Mock apiIndex.json with multiple versions (v5, v6) * to test section retrieval for different versions */ -jest.mock('../../../../apiIndex.json', () => ({ +jest.mock('outputDir/apiIndex.json', () => ({ versions: ['v5', 'v6'], sections: { v5: ['getting-started'], diff --git a/src/__tests__/pages/api/__tests__/versions.test.ts b/src/__tests__/pages/api/__tests__/versions.test.ts index 5a905c4..695ba5a 100644 --- a/src/__tests__/pages/api/__tests__/versions.test.ts +++ b/src/__tests__/pages/api/__tests__/versions.test.ts @@ -3,7 +3,7 @@ import { GET } from '../../../../pages/api/versions' /** * Mock apiIndex.json with multiple versions */ -jest.mock('../../../../apiIndex.json', () => ({ +jest.mock('outputDir/apiIndex.json', () => ({ versions: ['v5', 'v6'], sections: {}, pages: {}, diff --git a/src/pages/api/[version].ts b/src/pages/api/[version].ts index f103040..9d13dcc 100644 --- a/src/pages/api/[version].ts +++ b/src/pages/api/[version].ts @@ -1,6 +1,6 @@ import type { APIRoute } from 'astro' import { createJsonResponse, createIndexKey } from '../../utils/apiHelpers' -import { sections as sectionsData } from '../../apiIndex.json' +import { sections as sectionsData } from 'outputDir/apiIndex.json' export const prerender = false diff --git a/src/pages/api/[version]/[section].ts b/src/pages/api/[version]/[section].ts index cf35963..414ab83 100644 --- a/src/pages/api/[version]/[section].ts +++ b/src/pages/api/[version]/[section].ts @@ -1,6 +1,6 @@ import type { APIRoute } from 'astro' import { createJsonResponse, createIndexKey } from '../../../utils/apiHelpers' -import { pages as pagesData } from '../../../apiIndex.json' +import { pages as pagesData } from 'outputDir/apiIndex.json' export const prerender = false diff --git a/src/pages/api/[version]/[section]/[page].ts b/src/pages/api/[version]/[section]/[page].ts index 456e227..0679d6b 100644 --- a/src/pages/api/[version]/[section]/[page].ts +++ b/src/pages/api/[version]/[section]/[page].ts @@ -1,6 +1,6 @@ import type { APIRoute } from 'astro' import { createJsonResponse, createIndexKey } from '../../../../utils/apiHelpers' -import { tabs as tabsData } from '../../../../apiIndex.json' +import { tabs as tabsData } from 'outputDir/apiIndex.json' export const prerender = false diff --git a/src/pages/api/openapi.json.ts b/src/pages/api/openapi.json.ts index af94756..6db7b48 100644 --- a/src/pages/api/openapi.json.ts +++ b/src/pages/api/openapi.json.ts @@ -1,5 +1,5 @@ import type { APIRoute } from 'astro' -import index from '../../apiIndex.json' +import index from 'outputDir/apiIndex.json' export const prerender = false diff --git a/src/pages/api/versions.ts b/src/pages/api/versions.ts index 64e53f5..3e14320 100644 --- a/src/pages/api/versions.ts +++ b/src/pages/api/versions.ts @@ -1,6 +1,6 @@ import type { APIRoute } from 'astro' import { createJsonResponse } from '../../utils/apiHelpers' -import { versions as versionsData } from '../../apiIndex.json' +import { versions as versionsData } from 'outputDir/apiIndex.json' export const prerender = false diff --git a/src/utils/apiIndex/generate.ts b/src/utils/apiIndex/generate.ts index 46b9fd8..2333292 100644 --- a/src/utils/apiIndex/generate.ts +++ b/src/utils/apiIndex/generate.ts @@ -133,7 +133,7 @@ export async function generateApiIndex(): Promise { } /** - * Writes API index to src/apiIndex.json + * Writes API index to a apiIndex.json file in the user defined output directory * This file is used by server-side API routes to avoid runtime getCollection() calls * * @param index - The API index structure to write diff --git a/tsconfig.json b/tsconfig.json index c6b4d69..def1af6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,17 @@ "moduleResolution": "bundler", "importHelpers": true, "verbatimModuleSyntax": false, + "paths": { + "outputDir/*": [ + "/Users/ausulliv/repos/patternfly-doc-core/dist/*" + ] + } }, - "include": [".astro/types.d.ts", "**/*"], - "exclude": ["dist"] -} + "include": [ + ".astro/types.d.ts", + "**/*" + ], + "exclude": [ + "dist" + ] +} \ No newline at end of file From 9bced60364524e555feec4c2d114b3d4aa33da3e Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Tue, 9 Dec 2025 17:45:34 -0500 Subject: [PATCH 04/12] update api routes to fetch the apiIndex at runtime rather than import it --- .gitignore | 2 + astro.config.mjs | 5 -- cli/cli.ts | 60 ++++++++++++--------- package.json | 2 +- src/pages/api/[version].ts | 25 +++++---- src/pages/api/[version]/[section].ts | 26 +++++---- src/pages/api/[version]/[section]/[page].ts | 30 +++++++---- src/pages/api/openapi.json.ts | 12 +++-- src/pages/api/versions.ts | 16 ++++-- src/utils/apiIndex/fetch.ts | 19 +++++++ src/utils/apiIndex/generate.ts | 3 +- 11 files changed, 131 insertions(+), 69 deletions(-) create mode 100644 src/utils/apiIndex/fetch.ts diff --git a/.gitignore b/.gitignore index 7c0cb99..db06f95 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ src/apiIndex.json textContent/*.mdx coverage/ + +.wrangler/ \ No newline at end of file diff --git a/astro.config.mjs b/astro.config.mjs index 1ebad5c..4a9622f 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -13,11 +13,6 @@ export default defineConfig({ noExternal: ["@patternfly/*", "react-dropzone"], external: ["node:fs", "node:path"] }, - build: { - rollupOptions: { - external: [/apiIndex\.json$/] - } - }, server: { fs: { allow: ['./'] diff --git a/cli/cli.ts b/cli/cli.ts index f1563fb..1559d4a 100755 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -96,12 +96,14 @@ async function updateTsConfigOutputDirPath(program: Command) { const tsConfig = JSON.parse(tsConfigFile) const formattedOutputDir = join(absoluteOutputDir, '*') - tsConfig.compilerOptions.paths["outputDir/*"] = [formattedOutputDir] + tsConfig.compilerOptions.paths['outputDir/*'] = [formattedOutputDir] await writeFile(tsConfigPath, JSON.stringify(tsConfig, null, 2)) if (verbose) { - console.log(`Updated tsconfig.json with outputDir path: ${formattedOutputDir}`) + console.log( + `Updated tsconfig.json with outputDir path: ${formattedOutputDir}`, + ) } } catch (e: any) { console.error('Error updating tsconfig.json with outputDir path:', e) @@ -132,7 +134,9 @@ async function initializeApiIndex(program: Command) { } } -async function buildProject(): Promise { +async function buildProject(program: Command): Promise { + const { verbose } = program.opts() + if (!config) { console.error( 'No config found, please run the `setup` command or manually create a pf-docs.config.mjs file', @@ -159,35 +163,40 @@ async function buildProject(): Promise { outDir: docsOutputDir, }) + // copy the apiIndex.json file to the docs directory so it can be served as a static asset + const apiIndexPath = join(absoluteOutputDir, 'apiIndex.json') + const docsApiIndexPath = join(absoluteOutputDir, 'docs', 'apiIndex.json') + await copyFile(apiIndexPath, docsApiIndexPath) + + if (verbose) { + console.log('Copied apiIndex.json to docs directory') + } + return config } -async function deploy() { - const { verbose } = program.opts() +async function deploy(program: Command) { + const { verbose, dryRun } = program.opts() if (verbose) { console.log('Starting Cloudflare deployment...') } + if (dryRun) { + console.log('Dry run mode enabled, skipping deployment') + return + } + try { - // First build the project - const config = await buildProject() - if (config) { - if (verbose) { - console.log('Build complete, deploying to Cloudflare...') - } - - // Deploy using Wrangler - const { execSync } = await import('child_process') - const outputPath = join(absoluteOutputDir, 'docs') - - execSync(`npx wrangler pages deploy ${outputPath}`, { - stdio: 'inherit', - cwd: currentDir, - }) - - console.log('Successfully deployed to Cloudflare Pages!') - } + // Deploy using Wrangler + const { execSync } = await import('child_process') + + execSync(`wrangler pages deploy`, { + stdio: 'inherit', + cwd: currentDir, + }) + + console.log('Successfully deployed to Cloudflare Pages!') } catch (error) { console.error('Deployment failed:', error) process.exit(1) @@ -199,6 +208,7 @@ program.name('pf-doc-core') program.option('--verbose', 'verbose mode', false) program.option('--props', 'generate props data', false) +program.option('--dry-run', 'dry run mode', false) program.command('setup').action(async () => { await Promise.all([ @@ -232,7 +242,7 @@ program.command('start').action(async () => { }) program.command('build').action(async () => { - await buildProject() + await buildProject(program) }) program.command('generate-props').action(async () => { @@ -257,7 +267,7 @@ program }) program.command('deploy').action(async () => { - await deploy() + await deploy(program) }) program.parse(process.argv) diff --git a/package.json b/package.json index b0031eb..da68046 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "build:props": "npm run build:cli && node ./dist/cli/cli.js generate-props", "preview": "wrangler pages dev", "astro": "astro", - "deploy": "wrangler pages deploy", + "deploy": "npm run build:cli && node ./dist/cli/cli.js deploy", "versions:upload": "wrangler versions upload", "prettier": "prettier --write ./src", "lint": "eslint . --cache --cache-strategy content", diff --git a/src/pages/api/[version].ts b/src/pages/api/[version].ts index 9d13dcc..95fc549 100644 --- a/src/pages/api/[version].ts +++ b/src/pages/api/[version].ts @@ -1,10 +1,10 @@ import type { APIRoute } from 'astro' -import { createJsonResponse, createIndexKey } from '../../utils/apiHelpers' -import { sections as sectionsData } from 'outputDir/apiIndex.json' +import { createJsonResponse } from '../../utils/apiHelpers' +import { fetchApiIndex } from '../../utils/apiIndex/fetch' export const prerender = false -export const GET: APIRoute = async ({ params }) => { +export const GET: APIRoute = async ({ params, url }) => { const { version } = params if (!version) { @@ -14,12 +14,19 @@ export const GET: APIRoute = async ({ params }) => { ) } - const key = createIndexKey(version) - const sections = sectionsData[key as keyof typeof sectionsData] + try { + const index = await fetchApiIndex(url) + const sections = index.sections[version] - if (!sections) { - return createJsonResponse({ error: `Version '${version}' not found` }, 404) - } + if (!sections) { + return createJsonResponse({ error: `Version '${version}' not found` }, 404) + } - return createJsonResponse(sections) + return createJsonResponse(sections) + } catch (error) { + return createJsonResponse( + { error: 'Failed to load API index' }, + 500 + ) + } } diff --git a/src/pages/api/[version]/[section].ts b/src/pages/api/[version]/[section].ts index 414ab83..12a570d 100644 --- a/src/pages/api/[version]/[section].ts +++ b/src/pages/api/[version]/[section].ts @@ -1,10 +1,10 @@ import type { APIRoute } from 'astro' import { createJsonResponse, createIndexKey } from '../../../utils/apiHelpers' -import { pages as pagesData } from 'outputDir/apiIndex.json' +import { fetchApiIndex } from '../../../utils/apiIndex/fetch' export const prerender = false -export const GET: APIRoute = async ({ params }) => { +export const GET: APIRoute = async ({ params, url }) => { const { version, section } = params if (!version || !section) { @@ -14,15 +14,23 @@ export const GET: APIRoute = async ({ params }) => { ) } - const key = createIndexKey(version, section) - const pages = pagesData[key as keyof typeof pagesData] + try { + const index = await fetchApiIndex(url) + const key = createIndexKey(version, section) + const pages = index.pages[key] - if (!pages) { + if (!pages) { + return createJsonResponse( + { error: `Section '${section}' not found for version '${version}'` }, + 404, + ) + } + + return createJsonResponse(pages) + } catch (error) { return createJsonResponse( - { error: `Section '${section}' not found for version '${version}'` }, - 404, + { error: 'Failed to load API index' }, + 500 ) } - - return createJsonResponse(pages) } diff --git a/src/pages/api/[version]/[section]/[page].ts b/src/pages/api/[version]/[section]/[page].ts index 0679d6b..e6b67f5 100644 --- a/src/pages/api/[version]/[section]/[page].ts +++ b/src/pages/api/[version]/[section]/[page].ts @@ -1,10 +1,10 @@ import type { APIRoute } from 'astro' import { createJsonResponse, createIndexKey } from '../../../../utils/apiHelpers' -import { tabs as tabsData } from 'outputDir/apiIndex.json' +import { fetchApiIndex } from '../../../../utils/apiIndex/fetch' export const prerender = false -export const GET: APIRoute = async ({ params }) => { +export const GET: APIRoute = async ({ params, url }) => { const { version, section, page } = params if (!version || !section || !page) { @@ -14,17 +14,25 @@ export const GET: APIRoute = async ({ params }) => { ) } - const key = createIndexKey(version, section, page) - const tabs = tabsData[key as keyof typeof tabsData] + try { + const index = await fetchApiIndex(url) + const key = createIndexKey(version, section, page) + const tabs = index.tabs[key] - if (!tabs) { + if (!tabs) { + return createJsonResponse( + { + error: `Page '${page}' not found in section '${section}' for version '${version}'`, + }, + 404, + ) + } + + return createJsonResponse(tabs) + } catch (error) { return createJsonResponse( - { - error: `Page '${page}' not found in section '${section}' for version '${version}'`, - }, - 404, + { error: 'Failed to load API index' }, + 500 ) } - - return createJsonResponse(tabs) } diff --git a/src/pages/api/openapi.json.ts b/src/pages/api/openapi.json.ts index 6db7b48..a5caf0b 100644 --- a/src/pages/api/openapi.json.ts +++ b/src/pages/api/openapi.json.ts @@ -1,10 +1,16 @@ import type { APIRoute } from 'astro' -import index from 'outputDir/apiIndex.json' +import { fetchApiIndex } from '../../utils/apiIndex/fetch' export const prerender = false -export const GET: APIRoute = async () => { - const versions = index.versions +export const GET: APIRoute = async ({ url }) => { + let versions: string[] + try { + const index = await fetchApiIndex(url) + versions = index.versions + } catch (error) { + versions = [] + } const openApiSpec = { openapi: '3.0.0', diff --git a/src/pages/api/versions.ts b/src/pages/api/versions.ts index 3e14320..44758ac 100644 --- a/src/pages/api/versions.ts +++ b/src/pages/api/versions.ts @@ -1,11 +1,17 @@ import type { APIRoute } from 'astro' import { createJsonResponse } from '../../utils/apiHelpers' -import { versions as versionsData } from 'outputDir/apiIndex.json' +import { fetchApiIndex } from '../../utils/apiIndex/fetch' export const prerender = false -export const GET: APIRoute = async () => { - const versions = versionsData - - return createJsonResponse(versions) +export const GET: APIRoute = async ({ url }) => { + try { + const index = await fetchApiIndex(url) + return createJsonResponse(index.versions) + } catch (error) { + return createJsonResponse( + { error: 'Failed to load API index' }, + 500 + ) + } } diff --git a/src/utils/apiIndex/fetch.ts b/src/utils/apiIndex/fetch.ts new file mode 100644 index 0000000..4a627c7 --- /dev/null +++ b/src/utils/apiIndex/fetch.ts @@ -0,0 +1,19 @@ +import type { ApiIndex } from './generate' + +/** + * Fetches the API index from the server as a static asset + * Used by API routes at runtime instead of importing the JSON statically + * + * @param url - The URL object from the API route context + * @returns Promise resolving to the API index structure + */ +export async function fetchApiIndex(url: URL): Promise { + const apiIndexUrl = new URL('/apiIndex.json', url.origin) + const response = await fetch(apiIndexUrl) + + if (!response.ok) { + throw new Error(`Failed to load API index: ${response.status} ${response.statusText}`) + } + + return response.json() +} diff --git a/src/utils/apiIndex/generate.ts b/src/utils/apiIndex/generate.ts index 2333292..6e47e28 100644 --- a/src/utils/apiIndex/generate.ts +++ b/src/utils/apiIndex/generate.ts @@ -1,6 +1,6 @@ /* eslint-disable no-console */ import { join } from 'path' -import { writeFile } from 'fs/promises' +import { writeFile, mkdir } from 'fs/promises' import { getCollection } from 'astro:content' import type { CollectionKey } from 'astro:content' import { content } from '../../content' @@ -143,6 +143,7 @@ export async function writeApiIndex(index: ApiIndex): Promise { const indexPath = join(outputDir, 'apiIndex.json') try { + await mkdir(outputDir, { recursive: true }) await writeFile(indexPath, JSON.stringify(index, null, 2)) console.log(`✓ Generated API index with ${index.versions.length} versions`) } catch (error) { From 06a90f423bdeed16d1175056da3a6d7d1e02f18e Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Wed, 10 Dec 2025 09:57:12 -0500 Subject: [PATCH 05/12] fix fetching the apiIndex when running the dev server --- src/pages/apiIndex.json.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/pages/apiIndex.json.ts diff --git a/src/pages/apiIndex.json.ts b/src/pages/apiIndex.json.ts new file mode 100644 index 0000000..8d702ba --- /dev/null +++ b/src/pages/apiIndex.json.ts @@ -0,0 +1,27 @@ +import type { APIRoute } from 'astro' +import { getApiIndex } from '../utils/apiIndex/get' + +// Prerender at build time so this doesn't run in the Cloudflare Worker +export const prerender = true + +export const GET: APIRoute = async () => { + try { + const index = await getApiIndex() + return new Response(JSON.stringify(index), { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }) + } catch (error) { + return new Response( + JSON.stringify({ error: 'Failed to load API index' }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + } + ) + } +} From 76e52701b6f9e311668f199e2635abb0908d05c4 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Wed, 10 Dec 2025 12:11:05 -0500 Subject: [PATCH 06/12] fix unit tests --- .../pages/api/__tests__/[version].test.ts | 78 +++++++++++++++++-- .../pages/api/__tests__/versions.test.ts | 46 +++++++++-- 2 files changed, 110 insertions(+), 14 deletions(-) diff --git a/src/__tests__/pages/api/__tests__/[version].test.ts b/src/__tests__/pages/api/__tests__/[version].test.ts index e92369b..1ac71b0 100644 --- a/src/__tests__/pages/api/__tests__/[version].test.ts +++ b/src/__tests__/pages/api/__tests__/[version].test.ts @@ -1,10 +1,6 @@ import { GET } from '../../../../pages/api/[version]' -/** - * Mock apiIndex.json with multiple versions (v5, v6) - * to test section retrieval for different versions - */ -jest.mock('outputDir/apiIndex.json', () => ({ +const mockApiIndex = { versions: ['v5', 'v6'], sections: { v5: ['getting-started'], @@ -12,11 +8,19 @@ jest.mock('outputDir/apiIndex.json', () => ({ }, pages: {}, tabs: {}, -})) +} it('returns all sections for a valid version', 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'), } as any) const body = await response.json() @@ -26,41 +30,81 @@ it('returns all sections for a valid version', async () => { expect(body).toContain('components') expect(body).toContain('layouts') expect(body).toContain('utilities') + + jest.restoreAllMocks() }) it('returns only sections for the requested version', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response) + ) + const response = await GET({ params: { version: 'v5' }, + url: new URL('http://localhost:4321/api/v5'), } as any) const body = await response.json() expect(response.status).toBe(200) expect(body).toContain('getting-started') + + jest.restoreAllMocks() }) it('sorts sections alphabetically', 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'), } as any) const body = await response.json() const sorted = [...body].sort() expect(body).toEqual(sorted) + + jest.restoreAllMocks() }) it('deduplicates sections from multiple collections', 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'), } as any) const body = await response.json() const unique = [...new Set(body)] expect(body).toEqual(unique) + + 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'), } as any) const body = await response.json() @@ -68,25 +112,47 @@ it('returns 404 error for nonexistent version', async () => { 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/'), } 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('excludes content entries that have no section field', 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'), } as any) const body = await response.json() // Should only include sections from entries that have data.section expect(body.length).toBeGreaterThan(0) + + jest.restoreAllMocks() }) diff --git a/src/__tests__/pages/api/__tests__/versions.test.ts b/src/__tests__/pages/api/__tests__/versions.test.ts index 695ba5a..026ca61 100644 --- a/src/__tests__/pages/api/__tests__/versions.test.ts +++ b/src/__tests__/pages/api/__tests__/versions.test.ts @@ -1,36 +1,66 @@ import { GET } from '../../../../pages/api/versions' -/** - * Mock apiIndex.json with multiple versions - */ -jest.mock('outputDir/apiIndex.json', () => ({ +const mockApiIndex = { versions: ['v5', 'v6'], sections: {}, pages: {}, tabs: {}, -})) +} it('returns unique versions as sorted array', async () => { - const response = await GET({} as any) + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response) + ) + + const response = await GET({ + url: new URL('http://localhost:4321/api/versions'), + } 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).toEqual(['v5', 'v6']) + + jest.restoreAllMocks() }) it('sorts versions alphabetically', async () => { - const response = await GET({} as any) + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response) + ) + + const response = await GET({ + url: new URL('http://localhost:4321/api/versions'), + } as any) const body = await response.json() expect(body).toEqual(['v5', 'v6']) + + jest.restoreAllMocks() }) it('returns only the versions from the index', async () => { - const response = await GET({} as any) + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response) + ) + + const response = await GET({ + url: new URL('http://localhost:4321/api/versions'), + } as any) const body = await response.json() // Should return exactly the versions from the mocked apiIndex.json expect(body).toEqual(['v5', 'v6']) expect(body).toHaveLength(2) + + jest.restoreAllMocks() }) From fc8173c415100e6f1214ed1378b97081f67bf212 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Wed, 10 Dec 2025 13:28:36 -0500 Subject: [PATCH 07/12] fix lint errors --- src/pages/api/[version].ts | 2 +- src/pages/api/[version]/[section].ts | 2 +- src/pages/api/[version]/[section]/[page].ts | 2 +- src/pages/api/openapi.json.ts | 6 +++++- src/pages/api/versions.ts | 2 +- src/pages/apiIndex.json.ts | 2 +- 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/pages/api/[version].ts b/src/pages/api/[version].ts index 95fc549..8397aee 100644 --- a/src/pages/api/[version].ts +++ b/src/pages/api/[version].ts @@ -25,7 +25,7 @@ export const GET: APIRoute = async ({ params, url }) => { return createJsonResponse(sections) } catch (error) { return createJsonResponse( - { error: 'Failed to load API index' }, + { error: 'Failed to load API index', details: error }, 500 ) } diff --git a/src/pages/api/[version]/[section].ts b/src/pages/api/[version]/[section].ts index 12a570d..563702b 100644 --- a/src/pages/api/[version]/[section].ts +++ b/src/pages/api/[version]/[section].ts @@ -29,7 +29,7 @@ export const GET: APIRoute = async ({ params, url }) => { return createJsonResponse(pages) } catch (error) { return createJsonResponse( - { error: 'Failed to load API index' }, + { error: 'Failed to load API index', details: error }, 500 ) } diff --git a/src/pages/api/[version]/[section]/[page].ts b/src/pages/api/[version]/[section]/[page].ts index e6b67f5..9ccbf6e 100644 --- a/src/pages/api/[version]/[section]/[page].ts +++ b/src/pages/api/[version]/[section]/[page].ts @@ -31,7 +31,7 @@ export const GET: APIRoute = async ({ params, url }) => { return createJsonResponse(tabs) } catch (error) { return createJsonResponse( - { error: 'Failed to load API index' }, + { error: 'Failed to load API index', details: error }, 500 ) } diff --git a/src/pages/api/openapi.json.ts b/src/pages/api/openapi.json.ts index a5caf0b..fba081b 100644 --- a/src/pages/api/openapi.json.ts +++ b/src/pages/api/openapi.json.ts @@ -1,5 +1,6 @@ import type { APIRoute } from 'astro' import { fetchApiIndex } from '../../utils/apiIndex/fetch' +import { createJsonResponse } from '../../utils/apiHelpers' export const prerender = false @@ -9,7 +10,10 @@ export const GET: APIRoute = async ({ url }) => { const index = await fetchApiIndex(url) versions = index.versions } catch (error) { - versions = [] + return createJsonResponse( + { error: 'Failed to load API index', details: error }, + 500 + ) } const openApiSpec = { diff --git a/src/pages/api/versions.ts b/src/pages/api/versions.ts index 44758ac..3b21f82 100644 --- a/src/pages/api/versions.ts +++ b/src/pages/api/versions.ts @@ -10,7 +10,7 @@ export const GET: APIRoute = async ({ url }) => { return createJsonResponse(index.versions) } catch (error) { return createJsonResponse( - { error: 'Failed to load API index' }, + { error: 'Failed to load API index', details: error }, 500 ) } diff --git a/src/pages/apiIndex.json.ts b/src/pages/apiIndex.json.ts index 8d702ba..aeb1107 100644 --- a/src/pages/apiIndex.json.ts +++ b/src/pages/apiIndex.json.ts @@ -15,7 +15,7 @@ export const GET: APIRoute = async () => { }) } catch (error) { return new Response( - JSON.stringify({ error: 'Failed to load API index' }), + JSON.stringify({ error: 'Failed to load API index', details: error }), { status: 500, headers: { From cd5c2a0b9d528f252e96a6d61a767bce56fce3e9 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Thu, 11 Dec 2025 16:51:26 -0500 Subject: [PATCH 08/12] Remove custom outputDir pathing now that we're fetching the apiIndex at --- .gitignore | 4 ++- cli/cli.ts | 26 +------------------ jest.config.ts | 1 - package-lock.json | 2 +- package.json | 2 +- .../pages/api/__tests__/[version].test.ts | 8 +++--- src/pages/api/[version]/[section].ts | 3 ++- src/pages/api/openapi.json.ts | 3 ++- tsconfig.json | 7 +---- 9 files changed, 16 insertions(+), 40 deletions(-) diff --git a/.gitignore b/.gitignore index db06f95..5efbdc5 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,6 @@ textContent/*.mdx coverage/ -.wrangler/ \ No newline at end of file +.wrangler/ + +temp \ No newline at end of file diff --git a/cli/cli.ts b/cli/cli.ts index 1559d4a..d09006a 100755 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -34,6 +34,7 @@ try { .replace('file://', '') } catch (e: any) { if (e.code === 'ERR_MODULE_NOT_FOUND') { + console.log('@patternfly/patternfly-doc-core not found, using current directory as astroRoot') astroRoot = process.cwd() } else { console.error('Error resolving astroRoot', e) @@ -87,29 +88,6 @@ async function transformMDContentToMDX() { } } -async function updateTsConfigOutputDirPath(program: Command) { - const { verbose } = program.opts() - const tsConfigPath = join(astroRoot, 'tsconfig.json') - - try { - const tsConfigFile = await readFile(tsConfigPath, 'utf-8') - const tsConfig = JSON.parse(tsConfigFile) - const formattedOutputDir = join(absoluteOutputDir, '*') - - tsConfig.compilerOptions.paths['outputDir/*'] = [formattedOutputDir] - - await writeFile(tsConfigPath, JSON.stringify(tsConfig, null, 2)) - - if (verbose) { - console.log( - `Updated tsconfig.json with outputDir path: ${formattedOutputDir}`, - ) - } - } catch (e: any) { - console.error('Error updating tsconfig.json with outputDir path:', e) - } -} - async function initializeApiIndex(program: Command) { const { verbose } = program.opts() const templateIndexPath = join(astroRoot, 'cli', 'templates', 'apiIndex.json') @@ -150,7 +128,6 @@ async function buildProject(program: Command): Promise { ) return config } - await updateTsConfigOutputDirPath(program) await updateContent(program) await generateProps(program, true) await initializeApiIndex(program) @@ -230,7 +207,6 @@ program.command('init').action(async () => { }) program.command('start').action(async () => { - await updateTsConfigOutputDirPath(program) await updateContent(program) await initializeApiIndex(program) diff --git a/jest.config.ts b/jest.config.ts index cde6fa9..8348831 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -16,7 +16,6 @@ const config: Config = { testPathIgnorePatterns: ['/node_modules/', '/__tests__/helpers/'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], moduleNameMapper: { - '^outputDir/(.*)$': '/dist/$1', '\\.(css|less)$': '/src/__mocks__/styleMock.ts', '^astro:content$': '/src/__mocks__/astro-content.ts', '(.+)\\.js': '$1', diff --git a/package-lock.json b/package-lock.json index 2623c98..9bc293c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "@patternfly/react-tokens": "^6.0.0", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", - "astro": "5.15.9", + "astro": "^5.15.9", "change-case": "5.4.4", "commander": "^13.1.0", "glob": "^11.0.3", diff --git a/package.json b/package.json index da68046..bfe7cf6 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@patternfly/quickstarts": "^6.0.0", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", - "astro": "5.15.9", + "astro": "^5.15.9", "change-case": "5.4.4", "commander": "^13.1.0", "glob": "^11.0.3", diff --git a/src/__tests__/pages/api/__tests__/[version].test.ts b/src/__tests__/pages/api/__tests__/[version].test.ts index 1ac71b0..ab1a511 100644 --- a/src/__tests__/pages/api/__tests__/[version].test.ts +++ b/src/__tests__/pages/api/__tests__/[version].test.ts @@ -137,7 +137,7 @@ it('returns 400 error when version parameter is missing', async () => { jest.restoreAllMocks() }) -it('excludes content entries that have no section field', async () => { +it('returns sections array that matches the API index', async () => { global.fetch = jest.fn(() => Promise.resolve({ ok: true, @@ -151,8 +151,10 @@ it('excludes content entries that have no section field', async () => { } as any) const body = await response.json() - // Should only include sections from entries that have data.section - expect(body.length).toBeGreaterThan(0) + // Verify the returned sections exactly match the indexed sections + // The API index generation process filters out entries without section fields + expect(body).toEqual(mockApiIndex.sections.v6) + expect(body).toEqual(['components', 'layouts', 'utilities']) jest.restoreAllMocks() }) diff --git a/src/pages/api/[version]/[section].ts b/src/pages/api/[version]/[section].ts index 563702b..1db5e95 100644 --- a/src/pages/api/[version]/[section].ts +++ b/src/pages/api/[version]/[section].ts @@ -28,8 +28,9 @@ export const GET: APIRoute = async ({ params, url }) => { return createJsonResponse(pages) } catch (error) { + const details = error instanceof Error ? error.message : String(error) return createJsonResponse( - { error: 'Failed to load API index', details: error }, + { error: 'Failed to load API index', details }, 500 ) } diff --git a/src/pages/api/openapi.json.ts b/src/pages/api/openapi.json.ts index fba081b..8dd3c07 100644 --- a/src/pages/api/openapi.json.ts +++ b/src/pages/api/openapi.json.ts @@ -10,8 +10,9 @@ export const GET: APIRoute = async ({ url }) => { const index = await fetchApiIndex(url) versions = index.versions } catch (error) { + const details = error instanceof Error ? error.message : String(error) return createJsonResponse( - { error: 'Failed to load API index', details: error }, + { error: 'Failed to load API index', details }, 500 ) } diff --git a/tsconfig.json b/tsconfig.json index def1af6..157f333 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,12 +7,7 @@ "module": "ESNext", "moduleResolution": "bundler", "importHelpers": true, - "verbatimModuleSyntax": false, - "paths": { - "outputDir/*": [ - "/Users/ausulliv/repos/patternfly-doc-core/dist/*" - ] - } + "verbatimModuleSyntax": false }, "include": [ ".astro/types.d.ts", From 062ae90fd85ee43813a79d6d4fba298f0a882fcf Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Thu, 11 Dec 2025 16:55:23 -0500 Subject: [PATCH 09/12] remove unused imports --- cli/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/cli.ts b/cli/cli.ts index d09006a..b17ee63 100755 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -12,7 +12,7 @@ import { symLinkConfig } from './symLinkConfig.js' import { buildPropsData } from './buildPropsData.js' import { hasFile } from './hasFile.js' import { convertToMDX } from './convertToMDX.js' -import { mkdir, copyFile, readFile, writeFile } from 'fs/promises' +import { mkdir, copyFile } from 'fs/promises' import { fileExists } from './fileExists.js' const currentDir = process.cwd() From f41a39ed9179520a4074585942aaceab35456bf1 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Thu, 11 Dec 2025 17:08:34 -0500 Subject: [PATCH 10/12] improve readme clarity --- README.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.mdx b/README.mdx index bfb2d58..5d9e7ea 100644 --- a/README.mdx +++ b/README.mdx @@ -33,7 +33,7 @@ Any static assets, like images, can be placed in the `public/` directory. To define the markdown schema this project uses a typescript based schema known as [Zod](https://zod.dev). Details of how this is integratred into Astro can be found in Astros documentation on [content creation using Zod](https://docs.astro.build/en/guides/content-collections/#defining-datatypes-with-zod). -Note: When running in dev mode locally, API endpoints are not available on a clean repo until a build has been done +Note: When running in dev mode locally on a clean repository, API endpoints will not be available until you run `npm run build` to generate the API index. ### 🧞 Commands All commands are run from the root of the project, from a terminal: From 43c2194a4f9f328398a40aa71034a0c685bd1ede Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Thu, 11 Dec 2025 17:09:47 -0500 Subject: [PATCH 11/12] add error handling for the apiIndex copy operation --- cli/cli.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/cli/cli.ts b/cli/cli.ts index b17ee63..58143cc 100755 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -141,12 +141,17 @@ async function buildProject(program: Command): Promise { }) // copy the apiIndex.json file to the docs directory so it can be served as a static asset - const apiIndexPath = join(absoluteOutputDir, 'apiIndex.json') - const docsApiIndexPath = join(absoluteOutputDir, 'docs', 'apiIndex.json') - await copyFile(apiIndexPath, docsApiIndexPath) + try { + const apiIndexPath = join(absoluteOutputDir, 'apiIndex.json') + const docsApiIndexPath = join(absoluteOutputDir, 'docs', 'apiIndex.json') + await copyFile(apiIndexPath, docsApiIndexPath) - if (verbose) { - console.log('Copied apiIndex.json to docs directory') + if (verbose) { + console.log('Copied apiIndex.json to docs directory') + } + } catch (error) { + console.error('Failed to copy apiIndex.json to docs directory:', error) + throw error } return config From 8e9d674e4550dcf361de96da8bb5bb0d733d4e05 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Thu, 11 Dec 2025 17:10:14 -0500 Subject: [PATCH 12/12] Improve typing for the apiIndex fetch util --- src/utils/apiIndex/fetch.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/apiIndex/fetch.ts b/src/utils/apiIndex/fetch.ts index 4a627c7..ec1a3dc 100644 --- a/src/utils/apiIndex/fetch.ts +++ b/src/utils/apiIndex/fetch.ts @@ -15,5 +15,6 @@ export async function fetchApiIndex(url: URL): Promise { throw new Error(`Failed to load API index: ${response.status} ${response.statusText}`) } - return response.json() + const data = (await response.json()) as ApiIndex + return data }