diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 16a4fd8..c344d52 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,5 +28,6 @@ jobs: cache: 'npm' - run: npm ci - run: npm run build --if-present + - run: npm run build:cli - run: npm test - run: npm run lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1cc5bd3..0139905 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,5 +22,7 @@ jobs: registry-url: https://registry.npmjs.org/ - name: Build dist run: npm build + - name: Build CLI + run: npm run build:cli - name: Release to NPM run: npx semantic-release diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..737a4e8 --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +__tests__ \ No newline at end of file diff --git a/README.md b/README.md index 6dd50a1..7625c93 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,38 @@ Patternfly documentation core contains the base packages needed to build and release the PatternFly org website. -## Development +## Consuming this repo as a package + +### Setup + +Using this package for your documentation is accomplished in just a few simple steps: + +1. Run `npx patternfly-doc-core@latest setup` from the root of your repo. This will: + - add the documentation core as a dependency in your package + - add the relevant scripts for using the documentation core to your package scripts + - create the configuration file for customizing the documentation core +1. Install the documentation core using your projects dependency manager, e.g. `npm install` or `yarn install` +1. Run the initialization script using your script runner, e.g. `npm run init:docs` or `yarn init:docs` + - this will update a Vite config in the documentation so that it can access the files in your repo when running the development server +1. Edit the `pf-docs.config.mjs` file in your project root to point the documentation core to your documentation files + +### Use + +Once setup is complete you can start the dev server with the `start` script, and create production builds using the `build:docs` script! + +## Running this repo directly + +### Development The website is built using [Astro](https://astro.build). Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. -The `src/components/` folder contains Astro and React components that can be used to build the websites pages. +The `src/components/` folder contains Astro and React components that can be used to build the websites pages. 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). -## 🧞 Commands +### 🧞 Commands All commands are run from the root of the project, from a terminal: @@ -24,3 +45,5 @@ All commands are run from the root of the project, from a terminal: | `npm run preview` | Preview your build locally, before deploying | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | | `npm run astro -- --help` | Get help using the Astro CLI | +| `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 | diff --git a/astro.config.mjs b/astro.config.mjs index 2d2b88b..2a306d8 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -8,6 +8,11 @@ export default defineConfig({ vite: { ssr: { noExternal: ["@patternfly/*", "react-dropzone"], + }, + server: { + fs: { + allow: ['./'] + } } } }); \ No newline at end of file diff --git a/cli/__tests__/createCollectionContent.test.ts b/cli/__tests__/createCollectionContent.test.ts new file mode 100644 index 0000000..e9f0a88 --- /dev/null +++ b/cli/__tests__/createCollectionContent.test.ts @@ -0,0 +1,85 @@ +import { createCollectionContent } from '../createCollectionContent' +import { getConfig } from '../getConfig' +import { writeFile } from 'fs/promises' + +jest.mock('../getConfig') +jest.mock('fs/promises') + +// suppress console.log so that it doesn't clutter the test output +jest.spyOn(console, 'log').mockImplementation(() => {}) + +it('should call getConfig with the passed config file location', async () => { + await createCollectionContent('/foo/', 'bar', false) + + expect(getConfig).toHaveBeenCalledWith('bar') +}) + +it('should not proceed if config is not found', async () => { + ;(getConfig as jest.Mock).mockResolvedValue(undefined) + + await createCollectionContent('/foo/', 'bar', false) + + expect(writeFile).not.toHaveBeenCalled() +}) + +it('should log error if content is not found in config', async () => { + ;(getConfig as jest.Mock).mockResolvedValue({ foo: 'bar' }) + + const mockConsoleError = jest.fn() + jest.spyOn(console, 'error').mockImplementation(mockConsoleError) + + await createCollectionContent('/foo/', 'bar', false) + + expect(mockConsoleError).toHaveBeenCalledWith('No content found in config') + expect(writeFile).not.toHaveBeenCalled() +}) + +it('should call writeFile with the expected file location and content without throwing any errors', async () => { + ;(getConfig as jest.Mock).mockResolvedValue({ content: { key: 'value' } }) + + const mockConsoleError = jest.fn() + jest.spyOn(console, 'error').mockImplementation(mockConsoleError) + + await createCollectionContent('/foo/', 'bar', false) + + expect(writeFile).toHaveBeenCalledWith( + '/foo/src/content.ts', + `export const content = ${JSON.stringify({ key: 'value' })}`, + ) + expect(mockConsoleError).not.toHaveBeenCalled() +}) + +it('should log error if writeFile throws an error', async () => { + ;(getConfig as jest.Mock).mockResolvedValue({ content: { key: 'value' } }) + + const mockConsoleError = jest.fn() + jest.spyOn(console, 'error').mockImplementation(mockConsoleError) + + const error = new Error('error') + ;(writeFile as jest.Mock).mockRejectedValue(error) + + await createCollectionContent('/foo/', 'bar', false) + + expect(mockConsoleError).toHaveBeenCalledWith( + 'Error writing content file', + error, + ) +}) + +it('should log that content file was created when run in verbose mode', async () => { + const mockConsoleLog = jest.fn() + jest.spyOn(console, 'log').mockImplementation(mockConsoleLog) + + await createCollectionContent('/foo/', 'bar', true) + + expect(mockConsoleLog).toHaveBeenCalledWith('Content file created') +}) + +it('should not log that content file was created when not run in verbose mode', async () => { + const mockConsoleLog = jest.fn() + jest.spyOn(console, 'log').mockImplementation(mockConsoleLog) + + await createCollectionContent('/foo/', 'bar', false) + + expect(mockConsoleLog).not.toHaveBeenCalled() +}) diff --git a/cli/__tests__/createConfigFile.test.ts b/cli/__tests__/createConfigFile.test.ts new file mode 100644 index 0000000..c870d01 --- /dev/null +++ b/cli/__tests__/createConfigFile.test.ts @@ -0,0 +1,52 @@ +/* eslint-disable no-console */ + +import { createConfigFile } from '../createConfigFile.ts' +import { access, copyFile } from 'fs/promises' + +jest.mock('fs/promises') + +afterEach(() => { + jest.clearAllMocks() +}) + +// suppress console calls so that it doesn't clutter the test output +jest.spyOn(console, 'log').mockImplementation(() => {}) +jest.spyOn(console, 'error').mockImplementation(() => {}) + +it('should log a message and not call copyFile if the config file already exists', async () => { + ;(access as jest.Mock).mockResolvedValue(true) + + await createConfigFile('/astro', '/consumer') + + expect(copyFile).not.toHaveBeenCalled() + expect(console.log).toHaveBeenCalledWith( + 'pf-docs.config.mjs already exists, proceeding to next setup step', + ) +}) + +it('should copy the template file if the config file does not exist', async () => { + ;(access as jest.Mock).mockRejectedValue(new Error()) + ;(copyFile as jest.Mock).mockResolvedValue(undefined) + + const from = '/astro/cli/templates/pf-docs.config.mjs' + const to = '/consumer/pf-docs.config.mjs' + + await createConfigFile('/astro', '/consumer') + + expect(copyFile).toHaveBeenCalledWith(from, to) + expect(console.log).toHaveBeenCalledWith( + 'pf-docs.config.mjs has been created in /consumer', + ) +}) + +it('should log an error if copyFile fails', async () => { + ;(access as jest.Mock).mockRejectedValue(new Error()) + ;(copyFile as jest.Mock).mockRejectedValue(new Error('copy failed')) + + await createConfigFile('/astro', '/consumer') + + expect(console.error).toHaveBeenCalledWith( + 'Error creating pf-docs.config.mjs in /consumer.', + ) + expect(console.error).toHaveBeenCalledWith(new Error('copy failed')) +}) diff --git a/cli/__tests__/getConfig.test.ts b/cli/__tests__/getConfig.test.ts new file mode 100644 index 0000000..1d466e0 --- /dev/null +++ b/cli/__tests__/getConfig.test.ts @@ -0,0 +1,30 @@ +import { getConfig } from '../getConfig' +import { resolve } from 'path' + +it('should return the config when pf-docs.config.mjs exists', async () => { + const config = await getConfig(resolve('./cli/testData/good.config.js')) + expect(config).toEqual({ + config: { + content: [ + { + base: 'base-path', + packageName: 'package-name', + pattern: 'pattern', + name: 'name', + }, + ], + }, + }) +}) + +it('should return undefined and log error when pf-docs.config.mjs does not exist', async () => { + const consoleErrorMock = jest.fn() + + jest.spyOn(console, 'error').mockImplementation(consoleErrorMock) + + const config = await getConfig('foo') + expect(config).toBeUndefined() + expect(consoleErrorMock).toHaveBeenCalledWith( + 'pf-docs.config.mjs not found, have you created it at the root of your package?', + ) +}) diff --git a/cli/__tests__/setFsRootDir.test.ts b/cli/__tests__/setFsRootDir.test.ts new file mode 100644 index 0000000..dcee9a1 --- /dev/null +++ b/cli/__tests__/setFsRootDir.test.ts @@ -0,0 +1,58 @@ +import { readFile, writeFile } from 'fs/promises' +import { setFsRootDir } from '../setFsRootDir' + +jest.mock('fs/promises') + +// suppress console.log so that it doesn't clutter the test output +jest.spyOn(console, 'log').mockImplementation(() => {}) + +it('should attempt to read the astro config file', async () => { + ;(readFile as jest.Mock).mockResolvedValue("{ fs: { allow: ['/bar/'] } }") + + await setFsRootDir('/foo/', '/bar') + + expect(readFile).toHaveBeenCalledWith('/foo/astro.config.mjs', 'utf8') +}) + +it('should not modify the file if the default allow list is not present', async () => { + ;(readFile as jest.Mock).mockResolvedValue("{ fs: { allow: ['/bar/'] } }") + + await setFsRootDir('/foo/', '/bar') + + expect(writeFile).not.toHaveBeenCalled() +}) + +it('should modify the file if the default allow list is present', async () => { + ;(readFile as jest.Mock).mockResolvedValue("{ fs: { allow: ['./'] } }") + + await setFsRootDir('/foo/', '/bar') + + expect(writeFile).toHaveBeenCalledWith( + '/foo/astro.config.mjs', + "{ fs: { allow: ['/bar/'] } }", + ) +}) + +it('should log an error if writing the file fails', async () => { + ;(readFile as jest.Mock).mockResolvedValue("{ fs: { allow: ['./'] } }") + ;(writeFile as jest.Mock).mockRejectedValue(new Error('write error')) + const consoleErrorSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}) + + await setFsRootDir('/foo/', '/bar') + + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Error setting the server allow list in /foo/`, + expect.any(Error), + ) +}) + +it('should log a success message after attempting to write the file', async () => { + ;(readFile as jest.Mock).mockResolvedValue("{ fs: { allow: ['./'] } }") + const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {}) + + await setFsRootDir('/foo/', '/bar') + + expect(consoleLogSpy).toHaveBeenCalledWith('fs value set created') +}) diff --git a/cli/__tests__/updatePackageFile.test.ts b/cli/__tests__/updatePackageFile.test.ts new file mode 100644 index 0000000..a974833 --- /dev/null +++ b/cli/__tests__/updatePackageFile.test.ts @@ -0,0 +1,102 @@ +/* eslint-disable no-console */ + +import { updatePackageFile } from '../updatePackageFile' +import { copyFile, readFile, writeFile } from 'fs/promises' + +jest.mock('fs/promises') + +// suppress console calls so that it doesn't clutter the test output +jest.spyOn(console, 'log').mockImplementation(() => {}) + +const consumerPackageFilePath = '/consumer/package.json' +const templateFilePath = '/astro/cli/templates/package.json' +const docsCorePackageFilePath = '/astro/package.json' + +beforeEach(() => { + jest.clearAllMocks() +}) + +it('should update the package.json with new scripts and devDependencies', async () => { + const docsCorePackageJson = { + name: 'patternfly-doc-core', + version: '1.0.0', + } + const packageJson = { + scripts: { + test: 'jest', + }, + dependencies: { + react: '1.0.0', + }, + devDependencies: { + jest: '1.0.0', + }, + } + const templateJson = { + scripts: { + start: 'patternfly-doc-core start', + }, + } + + ;(readFile as jest.Mock).mockImplementation((path: string) => { + if (path === docsCorePackageFilePath) { + return Promise.resolve(JSON.stringify(docsCorePackageJson)) + } else if (path === consumerPackageFilePath) { + return Promise.resolve(JSON.stringify(packageJson)) + } else if (path === templateFilePath) { + return Promise.resolve(JSON.stringify(templateJson)) + } + return Promise.reject(new Error('File not found')) + }) + + await updatePackageFile('/astro', '/consumer') + + expect(readFile).toHaveBeenCalledWith(docsCorePackageFilePath, 'utf8') + expect(readFile).toHaveBeenCalledWith(consumerPackageFilePath, 'utf8') + expect(readFile).toHaveBeenCalledWith(templateFilePath, 'utf8') + expect(writeFile).toHaveBeenCalledWith( + consumerPackageFilePath, + JSON.stringify( + { + scripts: { + test: 'jest', + start: 'patternfly-doc-core start', + }, + dependencies: { + react: '1.0.0', + }, + devDependencies: { + jest: '1.0.0', + 'patternfly-doc-core': '^1.0.0', + }, + }, + null, + 2, + ), + ) + + expect(console.log).toHaveBeenCalledWith( + `${consumerPackageFilePath} has been updated with new scripts and devDependencies.`, + ) + expect(copyFile).not.toHaveBeenCalled() +}) + +it('should create a new package.json using the template file if the consumer package.json does not exist', async () => { + ;(readFile as jest.Mock).mockImplementation((path: string) => { + if (path === consumerPackageFilePath) { + return Promise.reject(new Error('File not found')) + } else if (path === docsCorePackageFilePath) { + return Promise.resolve('{}') + } + }) + + await updatePackageFile('/astro', '/consumer') + + expect(copyFile).toHaveBeenCalledWith( + templateFilePath, + consumerPackageFilePath, + ) + expect(console.log).toHaveBeenCalledWith( + `${consumerPackageFilePath} has been created using the template file.`, + ) +}) diff --git a/cli/cli.ts b/cli/cli.ts new file mode 100755 index 0000000..598a76c --- /dev/null +++ b/cli/cli.ts @@ -0,0 +1,81 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ +import { Command } from 'commander' +import { build, dev, preview, sync } from 'astro' +import { join } from 'path' +import { createCollectionContent } from './createCollectionContent.js' +import { setFsRootDir } from './setFsRootDir.js' +import { createConfigFile } from './createConfigFile.js' +import { updatePackageFile } from './updatePackageFile.js' +import { getConfig } from './getConfig.js' + +function updateContent(program: Command) { + const { verbose } = program.opts() + + if (verbose) { + console.log('Verbose mode enabled') + } + + createCollectionContent( + astroRoot, + `${process.cwd()}/pf-docs.config.mjs`, + verbose, + ) +} + +const astroRoot = import.meta + .resolve('patternfly-doc-core') + .replace('dist/cli/cli.js', '') + .replace('file://', '') +const currentDir = process.cwd() + +const program = new Command() +program.name('pf-doc-core') + +program.option('--verbose', 'verbose mode', false) + +program.command('setup').action(async () => { + await Promise.all([ + updatePackageFile(astroRoot, currentDir), + createConfigFile(astroRoot, currentDir), + ]) + + console.log( + '\nSetup complete, next install dependencies with your package manager of choice and then run the `init:docs` script', + ) +}) + +program.command('init').action(async () => { + await setFsRootDir(astroRoot, currentDir) + console.log( + '\nInitialization complete, next update your pf-docs.config.mjs file and then run the `start` script to start the dev server', + ) +}) + +program.command('start').action(async () => { + updateContent(program) + dev({ mode: 'development', root: astroRoot }) +}) + +program.command('build').action(async () => { + updateContent(program) + const config = await getConfig(`${currentDir}/pf-docs.config.mjs`) + if (!config) { + console.error( + 'No config found, please run the `setup` command or manually create a pf-docs.config.mjs file', + ) + return + } + build({ root: astroRoot, outDir: join(currentDir, config.outputDir) }) +}) + +program.command('serve').action(async () => { + updateContent(program) + preview({ root: astroRoot }) +}) + +program.command('sync').action(async () => { + sync({ root: astroRoot }) +}) + +program.parse(process.argv) diff --git a/cli/createCollectionContent.ts b/cli/createCollectionContent.ts new file mode 100644 index 0000000..368f092 --- /dev/null +++ b/cli/createCollectionContent.ts @@ -0,0 +1,31 @@ +/* eslint-disable no-console */ +import { writeFile } from 'fs/promises' +import { getConfig } from './getConfig.js' + +export async function createCollectionContent(rootDir: string, configFile: string, verbose: boolean) { + const config = await getConfig(configFile) + if (!config) { + return + } + + const { content } = config + if (!content) { + console.error('No content found in config') + return + } + + const contentFile = rootDir + 'src/content.ts' + + try { + await writeFile( + contentFile, + `export const content = ${JSON.stringify(content)}`, + ) + } catch (e: any) { + console.error('Error writing content file', e) + } finally { + if (verbose) { + console.log('Content file created') + } + } +} diff --git a/cli/createConfigFile.ts b/cli/createConfigFile.ts new file mode 100644 index 0000000..b23a5d2 --- /dev/null +++ b/cli/createConfigFile.ts @@ -0,0 +1,36 @@ +/* eslint-disable no-console */ +import { copyFile, access } from 'fs/promises' +import { join } from 'path' + +export async function createConfigFile( + astroRootDir: string, + consumerRootDir: string, +) { + const configFileName = 'pf-docs.config.mjs' + const configFilePath = join(consumerRootDir, configFileName) + const templateFilePath = join( + astroRootDir, + 'cli', + 'templates', + configFileName, + ) + + const fileExists = await access(configFilePath) + .then(() => true) + .catch(() => false) + + if (fileExists) { + console.log( + `${configFileName} already exists, proceeding to next setup step`, + ) + } else { + await copyFile(templateFilePath, configFilePath) + .then(() => + console.log(`${configFileName} has been created in ${consumerRootDir}`), + ) + .catch((error) => { + console.error(`Error creating ${configFileName} in ${consumerRootDir}.`) + console.error(error) + }) + } +} diff --git a/cli/getConfig.ts b/cli/getConfig.ts new file mode 100644 index 0000000..dd9ad5d --- /dev/null +++ b/cli/getConfig.ts @@ -0,0 +1,28 @@ +/* eslint-disable no-console */ +export interface CollectionDefinition { + base?: string + packageName?: string + pattern: string + name: string +} + +export interface DocsConfig { + content: CollectionDefinition[]; + outputDir: string; +} + +export async function getConfig(fileLocation: string): Promise { + try { + const { config } = await import(fileLocation) + return config as DocsConfig + } catch (e: any) { + if (['ERR_MODULE_NOT_FOUND', 'MODULE_NOT_FOUND'].includes(e.code)) { + console.error( + 'pf-docs.config.mjs not found, have you created it at the root of your package?', + ) + return + } + console.error(e) + return + } +} diff --git a/cli/setFsRootDir.ts b/cli/setFsRootDir.ts new file mode 100644 index 0000000..a880876 --- /dev/null +++ b/cli/setFsRootDir.ts @@ -0,0 +1,29 @@ +/* eslint-disable no-console */ +import { readFile, writeFile } from 'fs/promises' + +export async function setFsRootDir( + astroRootDir: string, + consumerRootDir: string, +) { + const astroConfigFile = astroRootDir + 'astro.config.mjs' + const astroConfigFileContent = await readFile(astroConfigFile, 'utf8') + + const defaultAllowList = "allow: ['./']" + + if (!astroConfigFileContent.includes(defaultAllowList)) { + return + } + + const newAstroConfigFileContent = astroConfigFileContent.replace( + defaultAllowList, + `allow: ['${consumerRootDir}/']`, + ) + + try { + await writeFile(astroConfigFile, newAstroConfigFileContent) + } catch (e: any) { + console.error(`Error setting the server allow list in ${astroRootDir}`, e) + } finally { + console.log('fs value set created') + } +} diff --git a/cli/templates/package.json b/cli/templates/package.json new file mode 100644 index 0000000..f16218d --- /dev/null +++ b/cli/templates/package.json @@ -0,0 +1,19 @@ +{ + "name": "add-package-name", + "version": "1.0.0", + "main": "index.js", + "type": "module", + "scripts": { + "start": "patternfly-doc-core start", + "build:docs": "patternfly-doc-core build", + "serve": "patternfly-doc-core serve", + "sync": "patternfly-doc-core sync", + "init:docs": "patternfly-doc-core init" + }, + "author": "", + "license": "", + "description": "", + "devDependencies": { + "patternfly-doc-core": "latest" + } +} diff --git a/cli/templates/pf-docs.config.mjs b/cli/templates/pf-docs.config.mjs new file mode 100644 index 0000000..dc09463 --- /dev/null +++ b/cli/templates/pf-docs.config.mjs @@ -0,0 +1,20 @@ +export const config = { + content: [ + // example content entry for local content, this would feed all markdown files in the content directory to the + // documentation core with a content identifier of 'content': + // { + // base: 'content', + // pattern: "*.md", + // name: 'content' + // }, + // + // example content entry for remote content, this would fetch all markdown files matching the glob in 'pattern' + // from the specified npm package and serve them with a content identifier of 'react-component-docs': + // { + // packageName: "@patternfly/react-core", + // pattern: "**/components/**/*.md", + // name: "react-component-docs", + // }, + ], + outputDir: "./dist/docs" +}; diff --git a/cli/testData/good.config.js b/cli/testData/good.config.js new file mode 100644 index 0000000..55803a1 --- /dev/null +++ b/cli/testData/good.config.js @@ -0,0 +1,12 @@ +export const config = { + config: { + content: [ + { + base: 'base-path', + packageName: 'package-name', + pattern: 'pattern', + name: 'name', + }, + ], + }, +} diff --git a/cli/tsconfig.json b/cli/tsconfig.json new file mode 100644 index 0000000..f7f4b90 --- /dev/null +++ b/cli/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "outDir": "../dist/cli", + "skipLibCheck": true, + "module": "NodeNext", + "allowJs": true + }, + "exclude": ["__tests__", "testData"], +} diff --git a/cli/updatePackageFile.ts b/cli/updatePackageFile.ts new file mode 100644 index 0000000..ce537ca --- /dev/null +++ b/cli/updatePackageFile.ts @@ -0,0 +1,55 @@ +/* eslint-disable no-console */ +import { copyFile, readFile, writeFile } from 'fs/promises' +import { join } from 'path' + +export async function updatePackageFile( + astroRootDir: string, + consumerRootDir: string, +) { + const packageFilePath = join(consumerRootDir, 'package.json') + const templateFilePath = join( + astroRootDir, + 'cli', + 'templates', + 'package.json', + ) + const docsCorePackageFilePath = join(astroRootDir, 'package.json') + const docsCorePackageFileContent = await readFile( + docsCorePackageFilePath, + 'utf8', + ) + const docsCorePackageJson = JSON.parse(docsCorePackageFileContent) + const docsCorePackageName = docsCorePackageJson.name + const docsCorePackageVersion = docsCorePackageJson.version + + const fileContent = await readFile(packageFilePath, 'utf8') + .then((content) => content) + .catch(() => false) + + if (typeof fileContent === 'string') { + const packageJson = JSON.parse(fileContent) + const templateContent = await readFile(templateFilePath, 'utf8') + const templateJson = JSON.parse(templateContent) + + packageJson.scripts = { + ...packageJson.scripts, + ...templateJson.scripts, + } + + packageJson.devDependencies = { + ...packageJson.devDependencies, + [docsCorePackageName]: `^${docsCorePackageVersion}`, + } + + await writeFile(packageFilePath, JSON.stringify(packageJson, null, 2)) + console.log( + `${packageFilePath} has been updated with new scripts and devDependencies.`, + ) + } else { + console.log( + `${packageFilePath} not found, creating a new one using the template file.`, + ) + await copyFile(templateFilePath, packageFilePath) + console.log(`${packageFilePath} has been created using the template file.`) + } +} diff --git a/jest.config.ts b/jest.config.ts index 55cfd5c..1e37ec3 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -7,7 +7,7 @@ import type { Config } from 'jest' const config: Config = { preset: 'ts-jest', testEnvironment: 'jsdom', - roots: ['/src'], + roots: ['/src', '/cli'], transform: { '^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.jest.json' }], '^.+\\.m?jsx?$': 'babel-jest', @@ -16,6 +16,7 @@ const config: Config = { moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], moduleNameMapper: { '\\.(css|less)$': '/src/__mocks__/styleMock.ts', + '(.+)\\.js': '$1', }, setupFilesAfterEnv: ['/test.setup.ts'], transformIgnorePatterns: [ diff --git a/package-lock.json b/package-lock.json index 2879940..fcdbb49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,12 +17,17 @@ "@types/react-dom": "^18.3.1", "astro": "^5.1.7", "change-case": "5.4.4", + "commander": "^13.0.0", + "glob": "^11.0.1", "nanostores": "^0.11.3", "react": "^18.3.1", "react-dom": "^18.3.1", "sass": "^1.81.0", "typescript": "^5.6.3" }, + "bin": { + "patternfly-doc-core": "cli/cli.ts" + }, "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.26.3", "@babel/preset-react": "^7.26.3", @@ -2078,6 +2083,63 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -2519,6 +2581,28 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@jest/reporters/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -5988,7 +6072,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/base-64": { @@ -6711,6 +6794,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/commander": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.0.0.tgz", + "integrity": "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/common-ancestor-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz", @@ -6936,7 +7028,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -7402,6 +7493,12 @@ "readable-stream": "^2.0.2" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -8843,6 +8940,22 @@ "is-callable": "^1.1.3" } }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", @@ -9065,22 +9178,23 @@ "license": "ISC" }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", + "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": "*" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -9097,6 +9211,30 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "15.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-15.12.0.tgz", @@ -10374,7 +10512,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/issue-parser": { @@ -10488,6 +10625,21 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", + "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jake": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", @@ -10911,6 +11063,28 @@ "node": ">=8" } }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jest-config/node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -11940,6 +12114,28 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jest-runtime/node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -13707,6 +13903,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", @@ -17111,6 +17316,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -17267,7 +17478,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -17280,6 +17490,31 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -18777,7 +19012,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -18790,7 +19024,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -18835,7 +19068,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "engines": { "node": ">=14" }, @@ -19161,6 +19393,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.11", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", @@ -19278,6 +19552,28 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -19485,6 +19781,28 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -20962,7 +21280,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -21134,6 +21451,80 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index c25aa26..334203f 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,15 @@ "name": "patternfly-doc-core", "type": "module", "version": "0.0.1", + "publishConfig": { + "access": "public" + }, "scripts": { "dev": "astro dev", "start": "astro dev", "build": "astro check && astro build", + "build:cli": "tsc --build ./cli/tsconfig.json", + "build:cli:watch": "tsc --build --watch ./cli/tsconfig.json", "preview": "astro preview", "astro": "astro", "prettier": "prettier --write ./src", @@ -13,6 +18,8 @@ "test": "jest", "test:watch": "jest --watch" }, + "main": "dist/cli/cli.js", + "bin": "./dist/cli/cli.js", "prettier": { "plugins": [ "prettier-plugin-astro" @@ -39,11 +46,13 @@ "@types/react-dom": "^18.3.1", "astro": "^5.1.7", "change-case": "5.4.4", + "commander": "^13.0.0", "nanostores": "^0.11.3", "react": "^18.3.1", "react-dom": "^18.3.1", "sass": "^1.81.0", - "typescript": "^5.6.3" + "typescript": "^5.6.3", + "glob": "^11.0.1" }, "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.26.3", @@ -77,5 +86,6 @@ "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "typescript-eslint": "^8.15.0" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/components/Navigation.astro b/src/components/Navigation.astro index 6c900c9..c07c7a0 100644 --- a/src/components/Navigation.astro +++ b/src/components/Navigation.astro @@ -3,7 +3,11 @@ import { getCollection } from 'astro:content' import { Navigation as ReactNav } from './Navigation.tsx' -const navEntries = await getCollection('textContent') +import { content } from "../content" + +const collections = await Promise.all(content.map(async (entry) => await getCollection(entry.name as 'textContent'))) + +const navEntries = collections.flat(); --- diff --git a/src/content.config.ts b/src/content.config.ts index 66cd8b4..c62c59a 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -1,15 +1,36 @@ import { defineCollection, z } from 'astro:content' import { glob } from 'astro/loaders' -const textContent = defineCollection({ - loader: glob({ pattern: '*.md', base: 'textContent' }), - schema: z.object({ - id: z.string(), - section: z.string(), - title: z.string().optional(), - }), -}) - -export const collections = { - textContent, +import { content } from './content' +import type { CollectionDefinition } from '../cli/getConfig' + +function defineContent(contentObj: CollectionDefinition) { + const { base, packageName, pattern, name } = contentObj + const dir = `${process.cwd()}/${base || `node_modules/${packageName}`}` + + if (!base && !packageName) { + // eslint-disable-next-line no-console + console.error('Either a base or packageName must be defined for ', name) + return + } + + return defineCollection({ + loader: glob({ base: dir, pattern }), + schema: z.object({ + id: z.string(), + section: z.string(), + title: z.string().optional(), + }), + }) } + +export const collections = content.reduce( + (acc, contentObj) => { + const def = defineContent(contentObj) + if (def) { + acc[contentObj.name] = def + } + return acc + }, + {} as Record>, +) diff --git a/src/content.ts b/src/content.ts new file mode 100644 index 0000000..6a079ae --- /dev/null +++ b/src/content.ts @@ -0,0 +1 @@ +export const content = [{ base: 'textContent', pattern: '*.md', name: "textContent" }, { base: 'dir', pattern: '*.md', name: "dir" }] diff --git a/src/pages/[section]/[...id].astro b/src/pages/[section]/[...id].astro index a2c04ca..74470a4 100644 --- a/src/pages/[section]/[...id].astro +++ b/src/pages/[section]/[...id].astro @@ -2,10 +2,12 @@ import { getCollection, render } from 'astro:content' import { Title } from '@patternfly/react-core' import MainLayout from '../../layouts/Main.astro' +import { content } from "../../content" export async function getStaticPaths() { - const content = await getCollection('textContent') - return content.map((entry) => ({ + const collections = await Promise.all(content.map(async (entry) => await getCollection(entry.name as 'textContent'))) + + return collections.flat().map((entry) => ({ params: { id: entry.id, section: entry.data.section }, props: { entry, title: entry.data.title }, })