+Update the paths logic in the api reports command to support complex subpaths

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2024-04-24 13:21:23 +02:00
parent e60f318e62
commit 9ae9bb2022
6 changed files with 118 additions and 69 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/repo-tools': patch
---
Update the paths logic in the api reports command to support complex subpaths
@@ -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<string>();
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,
`<unscopedPackageName>${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)/);
@@ -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<void>} 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(
@@ -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);
@@ -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;
}
@@ -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',
},
];
}