From 8d9179269cb7b4525cc2b8eb406b142adbe0d117 Mon Sep 17 00:00:00 2001 From: Donald Labaj Date: Wed, 9 Sep 2026 23:53:29 -0400 Subject: [PATCH] feat: add package props metadata generation --- README.mdx | 1 + cli/__tests__/buildPropsData.test.ts | 39 ++++++++++++- cli/buildPropsData.ts | 83 ++++++++++++++++++++++++---- cli/cli.ts | 23 +++++++- 4 files changed, 133 insertions(+), 13 deletions(-) diff --git a/README.mdx b/README.mdx index 5d9e7ea..9d5cfc8 100644 --- a/README.mdx +++ b/README.mdx @@ -49,3 +49,4 @@ All commands are run from the root of the project, from a terminal: | `npm run build:cli` | Create a JS build of the documentation core CLI | | `npm run build:cli:watch` | Run the CLI builder in watch mode | | `npm run build:props` | Create a json file of your TsDoc compatible in code documentation | +| `patternfly-doc-core generate-package-props` | Create a package metadata artifact at `schema/props.json` containing its format version, package identity, and components | diff --git a/cli/__tests__/buildPropsData.test.ts b/cli/__tests__/buildPropsData.test.ts index 7bcf349..03087fa 100644 --- a/cli/__tests__/buildPropsData.test.ts +++ b/cli/__tests__/buildPropsData.test.ts @@ -1,6 +1,9 @@ -import { writeFile } from 'fs/promises' +import { mkdir, readFile, writeFile } from 'fs/promises' import { glob } from 'glob' -import { buildPropsData } from '../buildPropsData' +import { + buildPackagePropsData, + buildPropsData, +} from '../buildPropsData' import { getConfig } from '../getConfig' import { tsDocgen } from '../tsDocGen' @@ -208,3 +211,35 @@ it('should not log verbose messages when not run in verbose mode', async () => { // Should not have any verbose logging calls expect(mockConsoleLog).not.toHaveBeenCalled() }) + +it('should write package metadata with package identity and components', async () => { + ;(getConfig as jest.Mock).mockResolvedValue(validConfigResponse) + ;(glob as unknown as jest.Mock).mockResolvedValueOnce(['files/one']) + ;(glob as unknown as jest.Mock).mockResolvedValueOnce([]) + ;(tsDocgen as jest.Mock).mockResolvedValue(validTsDocGenResponseOne) + ;(readFile as jest.Mock).mockResolvedValue( + JSON.stringify({ name: '@patternfly/test-package', version: '1.2.3' }), + ) + + const result = await buildPackagePropsData({ + rootDir: '/root', + configFile: '/config', + outputFile: 'schema/props.json', + verbose: false, + }) + + expect(result).toEqual({ + formatVersion: 1, + package: '@patternfly/test-package', + packageVersion: '1.2.3', + components: { + ComponentOne: validTsDocGenResponseOne[0], + ComponentTwo: validTsDocGenResponseOne[1], + }, + }) + expect(mkdir).toHaveBeenCalledWith('/root/schema', { recursive: true }) + expect(writeFile).toHaveBeenCalledWith( + '/root/schema/props.json', + `${JSON.stringify(result, null, 2)}\n`, + ) +}) diff --git a/cli/buildPropsData.ts b/cli/buildPropsData.ts index aa2ba49..de4b25a 100644 --- a/cli/buildPropsData.ts +++ b/cli/buildPropsData.ts @@ -1,13 +1,13 @@ /* eslint-disable no-console */ import { glob } from 'glob' -import { writeFile } from 'fs/promises' -import { join } from 'path' +import { mkdir, readFile, writeFile } from 'fs/promises' +import { dirname, join, resolve } from 'path' import { tsDocgen } from './tsDocGen.js' import { getConfig, PropsGlobs } from './getConfig.js' -interface Prop { +export interface Prop { name: string type: string description?: string @@ -15,15 +15,22 @@ interface Prop { defaultValue?: string hide?: boolean } -interface TsDoc { +export interface TsDoc { name: string description: string props: Prop[] } -interface PropsData { +export interface PropsData { [key: string]: TsDoc } +export interface PackagePropsData { + formatVersion: 1 + package: string + packageVersion: string + components: PropsData +} + // Build unique names for components with a "variant" extension type TsDocVariants = 'next' | 'deprecated' | undefined function getTsDocName(name: string, variant: TsDocVariants) { @@ -50,7 +57,7 @@ async function getFiles(root: string, globs: PropsGlobs[]) { return files.flat() } -async function getPropsData(files: string[], verbose: boolean) { +async function getPropsData(files: string[], verbose: boolean): Promise { const perFilePropsData = await Promise.all( files.map(async (file) => { if (verbose) { @@ -83,7 +90,14 @@ async function getPropsData(files: string[], verbose: boolean) { return combinedPropsData } -export async function buildPropsData( +interface BuildPropsOptions { + rootDir: string + configFile: string + outputFile: string + verbose: boolean +} + +async function getConfiguredPropsData( rootDir: string, configFile: string, verbose: boolean, @@ -102,7 +116,7 @@ export async function buildPropsData( return } - const { propsGlobs, outputDir } = config + const { propsGlobs } = config if (!propsGlobs) { console.error('No props data found in config') return @@ -111,12 +125,61 @@ export async function buildPropsData( const files = await getFiles(rootDir, propsGlobs) verboseModeLog(`Found ${files.length} files to parse`) - const propsData = await getPropsData(files, verbose) + return { + propsData: await getPropsData(files, verbose), + outputDir: config.outputDir, + verboseModeLog, + } +} - const propsFile = join(outputDir, 'props.json') +export async function buildPropsData( + rootDir: string, + configFile: string, + verbose: boolean, +) { + const configuredData = await getConfiguredPropsData(rootDir, configFile, verbose) + if (!configuredData) { + return + } + const { propsData, outputDir, verboseModeLog } = configuredData + const propsFile = join(outputDir, 'props.json') const absolutePropsFilePath = join(process.cwd(), propsFile) verboseModeLog(`Writing props data to ${absolutePropsFilePath}`) await writeFile(propsFile, JSON.stringify(propsData)) } + +export async function buildPackagePropsData({ + rootDir, + configFile, + outputFile, + verbose, +}: BuildPropsOptions): Promise { + const configuredData = await getConfiguredPropsData(rootDir, configFile, verbose) + if (!configuredData) { + return + } + + const packageJson = JSON.parse( + await readFile(join(rootDir, 'package.json'), 'utf8'), + ) as { name?: string; version?: string } + + if (!packageJson.name || !packageJson.version) { + throw new Error('Package metadata must include name and version in package.json') + } + + const packagePropsData: PackagePropsData = { + formatVersion: 1, + package: packageJson.name, + packageVersion: packageJson.version, + components: configuredData.propsData, + } + + const outputPath = resolve(rootDir, outputFile) + await mkdir(dirname(outputPath), { recursive: true }) + configuredData.verboseModeLog(`Writing package props data to ${outputPath}`) + await writeFile(outputPath, `${JSON.stringify(packagePropsData, null, 2)}\n`) + + return packagePropsData +} diff --git a/cli/cli.ts b/cli/cli.ts index 7745d93..e9bccb4 100755 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -9,7 +9,10 @@ import { createConfigFile } from './createConfigFile.js' import { updatePackageFile } from './updatePackageFile.js' import { DocsConfig, getConfig } from './getConfig.js' import { symLinkConfig } from './symLinkConfig.js' -import { buildPropsData } from './buildPropsData.js' +import { + buildPackagePropsData, + buildPropsData, +} from './buildPropsData.js' import { hasFile } from './hasFile.js' import { convertToMDX } from './convertToMDX.js' import { mkdir, copyFile } from 'fs/promises' @@ -244,6 +247,24 @@ program.command('generate-props').action(async () => { console.log('\nProps data generated') }) +program + .command('generate-package-props') + .argument('[outputFile]', 'package metadata output path', 'schema/props.json') + .action(async (outputFile: string) => { + const { verbose } = program.opts() + const { repoRoot } = config + const rootDir = repoRoot ? resolve(currentDir, repoRoot) : currentDir + + await buildPackagePropsData({ + rootDir, + configFile: `${currentDir}/pf-docs.config.mjs`, + outputFile, + verbose, + }) + + console.log(`\nPackage props data generated at ${outputFile}`) + }) + program.command('serve').action(async () => { await updateContent(program) preview({ root: astroRoot })