From 9ae9bb2022db86256c55fbfc7d72024b38f4b221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 24 Apr 2024 13:21:23 +0200 Subject: [PATCH] +Update the paths logic in the api reports command to support complex subpaths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/curly-shirts-flow.md | 5 ++ .../src/commands/api-reports/api-extractor.ts | 65 +++++++++++------- .../api-reports/generateTypeDeclarations.ts | 2 +- .../src/commands/type-deps/type-deps.ts | 8 +-- packages/repo-tools/src/lib/entryPoints.ts | 39 ----------- .../src/lib/getPackageExportDetails.ts | 68 +++++++++++++++++++ 6 files changed, 118 insertions(+), 69 deletions(-) create mode 100644 .changeset/curly-shirts-flow.md delete mode 100644 packages/repo-tools/src/lib/entryPoints.ts create mode 100644 packages/repo-tools/src/lib/getPackageExportDetails.ts diff --git a/.changeset/curly-shirts-flow.md b/.changeset/curly-shirts-flow.md new file mode 100644 index 0000000000..b9a73809ba --- /dev/null +++ b/.changeset/curly-shirts-flow.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +Update the paths logic in the api reports command to support complex subpaths diff --git a/packages/repo-tools/src/commands/api-reports/api-extractor.ts b/packages/repo-tools/src/commands/api-reports/api-extractor.ts index 7e36b08b0a..6a523af6a9 100644 --- a/packages/repo-tools/src/commands/api-reports/api-extractor.ts +++ b/packages/repo-tools/src/commands/api-reports/api-extractor.ts @@ -62,7 +62,7 @@ import { IMarkdownEmitterContext } from '@microsoft/api-documenter/lib/markdown/ import { AstDeclaration } from '@microsoft/api-extractor/lib/analyzer/AstDeclaration'; import { paths as cliPaths } from '../../lib/paths'; import { minimatch } from 'minimatch'; -import { getPackageExportNames } from '../../lib/entryPoints'; +import { getPackageExportDetails } from '../../lib/getPackageExportDetails'; import { createBinRunner } from '../util'; const tmpDir = cliPaths.resolveTargetRoot( @@ -303,8 +303,15 @@ function logApiReportInstructions() { async function findPackageEntryPoints(packageDirs: string[]): Promise< Array<{ + // package dir relative to root, e.g. "packages/backend-app-api" packageDir: string; + // the name of the export, e.g. "index" or "alpha" name: string; + // the path within the dist directory for this export, e.g. "alpha.d.ts" + distPath: string; + // the path within the dist-types directory of this package for this export, + // e.g. "src/entrypoints/foo/index.d.ts" + distTypesPath: string; }> > { return Promise.all( @@ -313,12 +320,9 @@ async function findPackageEntryPoints(packageDirs: string[]): Promise< cliPaths.resolveTargetRoot(packageDir, 'package.json'), ); - return ( - getPackageExportNames(pkg)?.map(name => ({ packageDir, name })) ?? { - packageDir, - name: 'index', - } - ); + return getPackageExportDetails(pkg).map(details => { + return { packageDir, ...details }; + }); }), ).then(results => results.flat()); } @@ -344,13 +348,23 @@ export async function runApiExtraction({ }: ApiExtractionOptions) { await fs.remove(outputDir); - const packageEntryPoints = await findPackageEntryPoints(packageDirs); + // The collection of all entry points of all packages, as a single list + const allEntryPoints = await findPackageEntryPoints(packageDirs); - const entryPoints = packageEntryPoints.map(({ packageDir, name }) => { - return cliPaths.resolveTargetRoot( - `./dist-types/${packageDir}/src/${name}.d.ts`, - ); - }); + // The path (relative to the root) to ALL dist-types entry points (e.g. + // "dist-types/packages/backend-app-api/src/index.d.ts"). These are used as + // "extra"/contextual entry points for the extractor so that it can see the + // full context of things that are required by the local entry point being + // inspected. + const allDistTypesEntryPointPaths = allEntryPoints.map( + ({ packageDir, distTypesPath }) => { + return cliPaths.resolveTargetRoot( + './dist-types', + packageDir, + distTypesPath, + ); + }, + ); let compilerState: CompilerState | undefined = undefined; @@ -364,8 +378,8 @@ export async function runApiExtraction({ } const warnings = new Array(); - for (const [packageDir, group] of Object.entries( - groupBy(packageEntryPoints, ep => ep.packageDir), + for (const [packageDir, packageEntryPoints] of Object.entries( + groupBy(allEntryPoints, ep => ep.packageDir), )) { console.log(`## Processing ${packageDir}`); const noBail = Array.isArray(allowWarnings) @@ -377,7 +391,6 @@ export async function runApiExtraction({ './dist-types', packageDir, ); - const names = group.map(ep => ep.name); const remainingReportFiles = new Set( fs @@ -389,8 +402,9 @@ export async function runApiExtraction({ ), ); - for (const name of names) { - const suffix = name === 'index' ? '' : `-${name}`; + for (const packageEntryPoint of packageEntryPoints) { + const suffix = + packageEntryPoint.name === 'index' ? '' : `-${packageEntryPoint.name}`; const reportFileName = `api-report${suffix}.md`; const reportPath = resolvePath(projectFolder, reportFileName); remainingReportFiles.delete(reportFileName); @@ -401,7 +415,7 @@ export async function runApiExtraction({ configObject: { mainEntryPointFilePath: resolvePath( packageFolder, - `src/${name}.d.ts`, + packageEntryPoint.distTypesPath, ), bundledPackages: [], @@ -422,7 +436,7 @@ export async function runApiExtraction({ docModel: { // TODO(Rugvip): This skips docs for non-index entry points. We can try to work around it, but // most likely it makes sense to wait for API Extractor to natively support exports. - enabled: name === 'index', + enabled: packageEntryPoint.name === 'index', apiJsonFilePath: resolvePath( outputDir, `${suffix}.api.json`, @@ -482,7 +496,7 @@ export async function runApiExtraction({ if (!compilerState) { compilerState = CompilerState.create(extractorConfig, { - additionalEntryPoints: entryPoints, + additionalEntryPoints: allDistTypesEntryPointPaths, }); } @@ -519,18 +533,21 @@ export async function runApiExtraction({ }); // This release tag validation makes sure that the release tag of known entry points match expectations. - // The root index entrypoint is only allowed @public exports, while /alpha and /beta only allow @alpha and @beta. + // The root index entry point is only allowed @public exports, while /alpha and /beta only allow @alpha and @beta. if ( validateReleaseTags && fs.pathExistsSync(extractorConfig.reportFilePath) ) { - if (['index', 'alpha', 'beta'].includes(name)) { + if (['index', 'alpha', 'beta'].includes(packageEntryPoint.name)) { const report = await fs.readFile( extractorConfig.reportFilePath, 'utf8', ); const lines = report.split(/\r?\n/); - const expectedTag = name === 'index' ? 'public' : name; + const expectedTag = + packageEntryPoint.name === 'index' + ? 'public' + : packageEntryPoint.name; for (let i = 0; i < lines.length; i += 1) { const line = lines[i]; const match = line.match(/^\/\/ @(alpha|beta|public)/); diff --git a/packages/repo-tools/src/commands/api-reports/generateTypeDeclarations.ts b/packages/repo-tools/src/commands/api-reports/generateTypeDeclarations.ts index 58f4117bc2..43ee2aa85f 100644 --- a/packages/repo-tools/src/commands/api-reports/generateTypeDeclarations.ts +++ b/packages/repo-tools/src/commands/api-reports/generateTypeDeclarations.ts @@ -13,6 +13,7 @@ * 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'; @@ -27,7 +28,6 @@ import { paths as cliPaths } from '../../lib/paths'; * @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 generateTypeDeclarations(tsconfigFilePath: string) { await fs.remove(cliPaths.resolveTargetRoot('dist-types')); const { status } = spawnSync( diff --git a/packages/repo-tools/src/commands/type-deps/type-deps.ts b/packages/repo-tools/src/commands/type-deps/type-deps.ts index 17164cea39..7b75ac37ee 100644 --- a/packages/repo-tools/src/commands/type-deps/type-deps.ts +++ b/packages/repo-tools/src/commands/type-deps/type-deps.ts @@ -20,7 +20,7 @@ import { resolve as resolvePath } from 'path'; // eslint-disable-next-line @backstage/no-undeclared-imports import chalk from 'chalk'; import { getPackages, Package } from '@manypkg/get-packages'; -import { getPackageExportNames } from '../../lib/entryPoints'; +import { getPackageExportDetails } from '../../lib/getPackageExportDetails'; export default async () => { const { packages } = await getPackages(resolvePath('.')); @@ -97,11 +97,9 @@ function findAllDeps(declSrc: string) { * missing or incorrect in package.json */ function checkTypes(pkg: Package) { - const entryPointNames = getPackageExportNames(pkg.packageJson) ?? ['index']; - - const allDeps = entryPointNames.flatMap(name => { + const allDeps = getPackageExportDetails(pkg.packageJson).flatMap(exp => { const typeDecl = fs.readFileSync( - resolvePath(pkg.dir, `dist/${name}.d.ts`), + resolvePath(pkg.dir, 'dist', exp.distPath), 'utf8', ); return findAllDeps(typeDecl); diff --git a/packages/repo-tools/src/lib/entryPoints.ts b/packages/repo-tools/src/lib/entryPoints.ts deleted file mode 100644 index 4a412d410b..0000000000 --- a/packages/repo-tools/src/lib/entryPoints.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2023 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 { extname } from 'path'; -import type { JsonObject } from '@backstage/types'; - -export function getPackageExportNames(pkg: JsonObject): string[] | undefined { - if (pkg.exports && typeof pkg.exports !== 'string') { - return Object.entries(pkg.exports).flatMap(([mount, path]) => { - const ext = extname(String(path)); - if (!['.ts', '.tsx', '.cts', '.mts'].includes(ext)) { - return []; // Ignore non-TS entry points - } - let name = mount; - if (name.startsWith('./')) { - name = name.slice(2); - } - if (!name || name === '.') { - return ['index']; - } - return [name]; - }); - } - - return undefined; -} diff --git a/packages/repo-tools/src/lib/getPackageExportDetails.ts b/packages/repo-tools/src/lib/getPackageExportDetails.ts new file mode 100644 index 0000000000..6889bb2e77 --- /dev/null +++ b/packages/repo-tools/src/lib/getPackageExportDetails.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2023 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 { extname } from 'path'; +import type { JsonObject } from '@backstage/types'; + +export function getPackageExportDetails(pkg: JsonObject): Array<{ + // the name of the export, e.g. "index" or "alpha" + name: string; + // the path within the dist directory for this export, e.g. "alpha.d.ts" + distPath: string; + // the path within the dist-types directory of this package for this export, + // e.g. "src/entrypoints/foo/index.d.ts" + distTypesPath: string; +}> { + if (pkg.exports && typeof pkg.exports !== 'string') { + return Object.entries(pkg.exports).flatMap( + ([mount, path]: [string, string]) => { + const ext = extname(path); + if (!['.ts', '.tsx', '.cts', '.mts'].includes(ext)) { + return []; // Ignore non-TS entry points + } + + let name = mount; + if (name.startsWith('./')) { + name = name.slice(2); + } + if (!name || name === '.') { + name = 'index'; + } + + const distPath = `${name}.d.ts`; + const distTypesPath = path + .replace(/^\.\//, '') // Remove leading "./" + .replace(/\.[^.]+$/, '.d.ts'); // Replace .extension with .d.ts + + return [ + { + name, + distPath, + distTypesPath, + }, + ]; + }, + ); + } + + return [ + { + name: 'index', + distPath: 'index.d.ts', + distTypesPath: 'src/index.d.ts', + }, + ]; +}