diff --git a/packages/repo-tools/src/commands/api-reports/api-reports.test.ts b/packages/repo-tools/src/commands/api-reports/api-reports.test.ts new file mode 100644 index 0000000000..fa5bfd6973 --- /dev/null +++ b/packages/repo-tools/src/commands/api-reports/api-reports.test.ts @@ -0,0 +1,505 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import mockFs from 'mock-fs'; +import { resolve as resolvePath } from 'path'; +import * as pathsLib from '../../lib/paths'; + +import { + buildDocs, + runCliExtraction, + runApiExtraction, + categorizePackageDirs, +} from './api-extractor'; + +import { buildApiReports } from './api-reports'; +import { generateTSC } from './generateTSC'; + +jest.mock('./generateTSC'); +// create mocks for the dependencies of the `buildApiReports` function +jest.mock('./api-extractor', () => ({ + createTemporaryTsConfig: jest.fn(), + categorizePackageDirs: jest.fn().mockImplementation(async (p: string[]) => { + console.log('categorizePackageDirs', p); + return { + tsPackageDirs: p, + cliPackageDirs: p, + }; + }), + runApiExtraction: jest.fn(), + runCliExtraction: jest.fn(), + buildDocs: jest.fn(), +})); + +const paths = pathsLib.paths; + +jest.spyOn(paths, 'targetRoot', 'get').mockReturnValue('/root'); +jest.spyOn(paths, 'resolveTargetRoot').mockImplementation((...path) => { + return resolvePath('/root', ...path); +}); + +describe('buildApiReports', () => { + beforeEach(() => { + mockFs({ + [paths.targetRoot]: { + 'package.json': JSON.stringify({ + workspaces: { packages: ['packages/*', 'plugins/*'] }, + }), + packages: { + 'package-a': { + 'package.json': '{}', + }, + 'package-b': { + 'package.json': '{}', + }, + 'package-c': {}, + 'README.md': 'Hello World', + }, + plugins: { + 'plugin-a': { + 'package.json': '{}', + }, + 'plugin-b': { + 'package.json': '{}', + }, + 'plugin-c': { + 'package.json': '{}', + }, + }, + }, + }); + }); + + afterEach(() => { + mockFs.restore(); + jest.clearAllMocks(); + }); + it('should run whitout any options', async () => { + const opts = {}; + + await buildApiReports(opts); + + expect(categorizePackageDirs).toHaveBeenCalledWith([ + 'packages/package-a', + 'packages/package-b', + 'plugins/plugin-a', + 'plugins/plugin-b', + 'plugins/plugin-c', + ]); + + expect(generateTSC).not.toHaveBeenCalled(); + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: [ + 'packages/package-a', + 'packages/package-b', + 'plugins/plugin-a', + 'plugins/plugin-b', + 'plugins/plugin-c', + ], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + expect(runCliExtraction).toHaveBeenCalledWith({ + packageDirs: [ + 'packages/package-a', + 'packages/package-b', + 'plugins/plugin-a', + 'plugins/plugin-b', + + 'plugins/plugin-c', + ], + isLocalBuild: true, + }); + + expect(buildDocs).not.toHaveBeenCalled(); + }); + + describe('paths', () => { + it('should generate API reports for one specific package', async () => { + const opts = { + paths: ['packages/package-a'], + }; + + await buildApiReports(opts); + + expect(categorizePackageDirs).toHaveBeenCalledWith([ + 'packages/package-a', + ]); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + expect(runCliExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a'], + isLocalBuild: true, + }); + + expect(buildDocs).not.toHaveBeenCalled(); + }); + it('should generate API reports for multiple specific packages', async () => { + const opts = { + paths: ['packages/package-a', 'packages/package-b'], + }; + + await buildApiReports(opts); + + expect(categorizePackageDirs).toHaveBeenCalledWith([ + 'packages/package-a', + 'packages/package-b', + ]); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + expect(runCliExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + isLocalBuild: true, + }); + + expect(buildDocs).not.toHaveBeenCalled(); + }); + it('should generate API reports for all packages matching the glob pattern', async () => { + const opts = { + paths: ['packages/*'], + }; + + await buildApiReports(opts); + + expect(categorizePackageDirs).toHaveBeenCalledWith([ + 'packages/package-a', + 'packages/package-b', + ]); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + expect(runCliExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + isLocalBuild: true, + }); + + expect(buildDocs).not.toHaveBeenCalled(); + }); + + it('should generate API reports for all packages matching multiple glob patterns', async () => { + const opts = { + paths: ['packages/*', 'plugins/*a'], + }; + + await buildApiReports(opts); + + expect(categorizePackageDirs).toHaveBeenCalledWith([ + 'packages/package-a', + 'packages/package-b', + 'plugins/plugin-a', + ]); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: [ + 'packages/package-a', + 'packages/package-b', + 'plugins/plugin-a', + ], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + expect(runCliExtraction).toHaveBeenCalledWith({ + packageDirs: [ + 'packages/package-a', + 'packages/package-b', + 'plugins/plugin-a', + ], + isLocalBuild: true, + }); + + expect(buildDocs).not.toHaveBeenCalled(); + }); + + it('should generate API reports for specific packages and glob pattern', async () => { + const opts = { + paths: ['packages/package-a', 'plugins/*'], + }; + + await buildApiReports(opts); + + expect(categorizePackageDirs).toHaveBeenCalledWith([ + 'packages/package-a', + 'plugins/plugin-a', + 'plugins/plugin-b', + 'plugins/plugin-c', + ]); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: [ + 'packages/package-a', + 'plugins/plugin-a', + 'plugins/plugin-b', + 'plugins/plugin-c', + ], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + expect(runCliExtraction).toHaveBeenCalledWith({ + packageDirs: [ + 'packages/package-a', + 'plugins/plugin-a', + 'plugins/plugin-b', + 'plugins/plugin-c', + ], + isLocalBuild: true, + }); + + expect(buildDocs).not.toHaveBeenCalled(); + }); + }); + describe('allowWarnings', () => { + it('should accept boolean values', async () => { + const opts = { + paths: ['packages/*'], + allowWarnings: true, + }; + + await buildApiReports(opts); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: true, + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + }); + + it('should accept single path value', async () => { + const opts = { + paths: ['packages/*'], + allowWarnings: ['packages/package-a'], + }; + + await buildApiReports(opts); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: ['packages/package-a'], + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + }); + + it('should accept multiple path values as array', async () => { + const opts = { + paths: ['packages/*'], + allowWarnings: ['packages/package-a', 'packages/package-b'], + }; + + await buildApiReports(opts); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: ['packages/package-a', 'packages/package-b'], + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + }); + + it('should accept multiple path values as comma separated string', async () => { + const opts = { + paths: ['packages/*'], + allowWarnings: ['packages/package-a,packages/package-b'], + }; + + await buildApiReports(opts); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: ['packages/package-a', 'packages/package-b'], + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + }); + + it('should accept multiple path values as comma separated string with spaces', async () => { + const opts = { + paths: ['packages/*'], + allowWarnings: ['packages/package-a, packages/package-b'], + }; + + await buildApiReports(opts); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: ['packages/package-a', 'packages/package-b'], + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + }); + }); + describe('omitMessages', () => { + it('should accept single message value', async () => { + const opts = { + paths: ['packages/*'], + omitMessages: ['ae-missing-release-tag'], + }; + + await buildApiReports(opts); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: ['ae-missing-release-tag'], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + }); + + it('should accept multiple message values as array', async () => { + const opts = { + paths: ['packages/*'], + omitMessages: ['ae-missing-release-tag', 'ae-missing-annotations'], + }; + + await buildApiReports(opts); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: ['ae-missing-release-tag', 'ae-missing-annotations'], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + }); + it('should accept multiple message values as comma separated string', async () => { + const opts = { + paths: ['packages/*'], + omitMessages: ['ae-missing-release-tag,ae-missing-annotations'], + }; + + await buildApiReports(opts); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: ['ae-missing-release-tag', 'ae-missing-annotations'], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + }); + + it('should accept multiple message values as comma separated string with spaces', async () => { + const opts = { + paths: ['packages/*'], + omitMessages: ['ae-missing-release-tag, ae-missing-annotations'], + }; + + await buildApiReports(opts); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: ['ae-missing-release-tag', 'ae-missing-annotations'], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + }); + }); + describe('isCI', () => { + it('should set localBuild to false if CI option is passed', async () => { + const opts = { + paths: ['packages/*'], + ci: true, + }; + + await buildApiReports(opts); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: [], + isLocalBuild: false, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + expect(runCliExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + isLocalBuild: false, + }); + }); + }); + describe('docs', () => { + it('should run typedoc if docs option is passed', async () => { + const opts = { + paths: ['packages/*'], + docs: true, + }; + + await buildApiReports(opts); + + expect(buildDocs).toHaveBeenCalledWith({ + inputDir: '/root/node_modules/.cache/api-extractor', + outputDir: '/root/docs/reference', + }); + }); + }); + describe('tsc', () => { + it('should run tsc if tsc option is passed', async () => { + const opts = { + paths: ['packages/*'], + tsc: true, + }; + + await buildApiReports(opts); + + expect(generateTSC).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/repo-tools/src/commands/api-reports/api-reports.ts b/packages/repo-tools/src/commands/api-reports/api-reports.ts index 11b65823c7..b59f498d7b 100644 --- a/packages/repo-tools/src/commands/api-reports/api-reports.ts +++ b/packages/repo-tools/src/commands/api-reports/api-reports.ts @@ -16,7 +16,6 @@ import { OptionValues } from 'commander'; import fs from 'fs-extra'; -import { spawnSync } from 'child_process'; import { createTemporaryTsConfig, categorizePackageDirs, @@ -25,8 +24,18 @@ import { buildDocs, } from './api-extractor'; import { findPackageDirs, paths as cliPaths } from '../../lib/paths'; +import { generateTSC } from './generateTSC'; -export default async (opts: OptionValues) => { +type Options = { + ci?: boolean; + docs?: boolean; + tsc?: boolean; + paths?: string[]; + allowWarnings?: string[] | boolean; + omitMessages?: string[]; +} & OptionValues; + +export const buildApiReports = async (opts: Options) => { const tmpDir = cliPaths.resolveTargetRoot( './node_modules/.cache/api-extractor', ); @@ -90,6 +99,7 @@ export default async (opts: OptionValues) => { }); } + console.log(isDocsBuild); if (isDocsBuild) { console.log('# Generating package documentation'); await buildDocs({ @@ -99,37 +109,6 @@ export default async (opts: OptionValues) => { } }; -/** - * Generates the TypeScript declaration files for the specified project, using the provided `tsconfig.json` file. - * - * Any existing declaration files in the `dist-types` directory will be deleted before generating the new ones. - * - * If the `tsc` command exits with a non-zero exit code, the process will be terminated with the same exit code. - * - * @param tsconfigFilePath {string} The path to the `tsconfig.json` file to use for generating the declaration files. - * @returns {Promise} A promise that resolves when the declaration files have been generated. - */ -export async function generateTSC(tsconfigFilePath: string) { - await fs.remove(cliPaths.resolveTargetRoot('dist-types')); - const { status } = spawnSync( - 'yarn', - [ - 'tsc', - ['--project', tsconfigFilePath], - ['--skipLibCheck', 'false'], - ['--incremental', 'false'], - ].flat(), - { - stdio: 'inherit', - shell: true, - cwd: cliPaths.targetRoot, - }, - ); - if (status !== 0) { - process.exit(status || undefined); - } -} - /** * Retrieves the list of package names in the "workspaces" field of the `package.json` file in the current workspace root. * @@ -173,6 +152,6 @@ function parseArrayOption(value: string[] | boolean | undefined) { return value; } return value?.flatMap((str: string) => - str.includes(',') ? str.split(',') : str, + str.includes(',') ? str.split(',').map(s => s.trim()) : str, ); } diff --git a/packages/repo-tools/src/commands/api-reports/generateTSC.ts b/packages/repo-tools/src/commands/api-reports/generateTSC.ts new file mode 100644 index 0000000000..5c10c5085f --- /dev/null +++ b/packages/repo-tools/src/commands/api-reports/generateTSC.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import fs from 'fs-extra'; +import { spawnSync } from 'child_process'; +import { paths as cliPaths } from '../../lib/paths'; + +/** + * Generates the TypeScript declaration files for the specified project, using the provided `tsconfig.json` file. + * + * Any existing declaration files in the `dist-types` directory will be deleted before generating the new ones. + * + * If the `tsc` command exits with a non-zero exit code, the process will be terminated with the same exit code. + * + * @param tsconfigFilePath {string} The path to the `tsconfig.json` file to use for generating the declaration files. + * @returns {Promise} A promise that resolves when the declaration files have been generated. + */ + +export async function generateTSC(tsconfigFilePath: string) { + await fs.remove(cliPaths.resolveTargetRoot('dist-types')); + const { status } = spawnSync( + 'yarn', + [ + 'tsc', + ['--project', tsconfigFilePath], + ['--skipLibCheck', 'false'], + ['--incremental', 'false'], + ].flat(), + { + stdio: 'inherit', + shell: true, + cwd: cliPaths.targetRoot, + }, + ); + if (status !== 0) { + process.exit(status || undefined); + } +} diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index 5742ce7241..3f3b2cdbe7 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -39,7 +39,9 @@ export function registerCommands(program: Command) { ) .description('Generate an API report for selected packages') .action( - lazy(() => import('./api-reports/api-reports').then(m => m.default)), + lazy(() => + import('./api-reports/api-reports').then(m => m.buildApiReports), + ), ); program