Skip to content

Commit

Permalink
feat(bazel): api-golden test should leverage package exports informat…
Browse files Browse the repository at this point in the history
…ion for finding entries

Instead of looking for nested `package.json` files which currently only exist for
workaround-reasons in APF v13, we should consult the package NodeJS exports.

This will help with: angular/angular#45405.
  • Loading branch information
devversion committed Mar 22, 2022
1 parent be344b8 commit 6379fcf
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 48 deletions.
56 changes: 23 additions & 33 deletions bazel/api-golden/find_entry_points.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,57 +6,47 @@
* found in the LICENSE file at https://angular.io/license
*/

import {lstatSync, readdirSync, readFileSync} from 'fs';
import {dirname, join} from 'path';
import {join, normalize} from 'path';

import {readFileSync} from 'fs';

/** Interface describing a resolved NPM package entry point. */
export interface PackageEntryPoint {
typesEntryPointPath: string;
packageJsonPath: string;
subpath: string;
moduleName: string;
}

/** Interface describing contents of a `package.json`. */
interface PackageJson {
types?: string;
typings?: string;
name: string;
exports?: Record<string, {types?: string}>;
}

/** Finds all entry points within a given NPM package directory. */
export function findEntryPointsWithinNpmPackage(dirPath: string): PackageEntryPoint[] {
export function findEntryPointsWithinNpmPackage(
dirPath: string,
packageJsonPath: string,
): PackageEntryPoint[] {
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as PackageJson;
const entryPoints: PackageEntryPoint[] = [];

for (const packageJsonFilePath of findPackageJsonFilesInDirectory(dirPath)) {
const packageJson = JSON.parse(readFileSync(packageJsonFilePath, 'utf8')) as PackageJson;
const typesFile = packageJson.types || packageJson.typings;
if (packageJson.exports === undefined) {
throw new Error(
`Expected top-level "package.json" in "${dirPath}" to declare entry-points ` +
`through conditional exports.`,
);
}

if (typesFile) {
for (const [subpath, conditions] of Object.entries(packageJson.exports)) {
if (conditions.types !== undefined) {
entryPoints.push({
packageJsonPath: packageJsonFilePath,
typesEntryPointPath: join(dirname(packageJsonFilePath), typesFile),
subpath,
moduleName: normalize(join(packageJson.name, subpath)).replace(/\\/g, '/'),
typesEntryPointPath: join(dirPath, conditions.types),
});
}
}

return entryPoints;
}

/** Determine if the provided path is a directory. */
function isDirectory(dirPath: string) {
try {
return lstatSync(dirPath).isDirectory();
} catch {
return false;
}
}

/** Finds all `package.json` files within a directory. */
function* findPackageJsonFilesInDirectory(directoryPath: string): IterableIterator<string> {
for (const fileName of readdirSync(directoryPath)) {
const fullPath = join(directoryPath, fileName);
if (isDirectory(fullPath)) {
yield* findPackageJsonFilesInDirectory(fullPath);
} else if (fileName === 'package.json') {
yield fullPath;
}
}
}
16 changes: 8 additions & 8 deletions bazel/api-golden/index_npm_packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/

import {runfiles} from '@bazel/runfiles';
import * as chalk from 'chalk';
import {join, relative} from 'path';

import {findEntryPointsWithinNpmPackage} from './find_entry_points';
import {join} from 'path';
import {runfiles} from '@bazel/runfiles';
import {testApiGolden} from './test_api_report';

/**
Expand All @@ -25,18 +25,17 @@ async function main(
stripExportPattern: RegExp,
typeNames: string[],
) {
const entryPoints = findEntryPointsWithinNpmPackage(npmPackageDir);
const packageJsonPath = join(npmPackageDir, 'package.json');
const entryPoints = findEntryPointsWithinNpmPackage(npmPackageDir, packageJsonPath);
const outdatedGoldens: string[] = [];
let allTestsSucceeding = true;

for (const {packageJsonPath, typesEntryPointPath} of entryPoints) {
const pkgRelativeName = relative(npmPackageDir, typesEntryPointPath);
for (const {subpath, moduleName, typesEntryPointPath} of entryPoints) {
// API extractor generates API reports as markdown files. For each types
// entry-point we maintain a separate golden file. These golden files are
// based on the name of the entry-point `.d.ts` file in the NPM package,
// but with the proper `.md` file extension.
// based on the name of the defining NodeJS exports subpath in the NPM package,
// See: https://api-extractor.com/pages/overview/demo_api_report/.
const goldenName = pkgRelativeName.replace(/\.d\.ts$/, '.md');
const goldenName = join(subpath, 'index.md');
const goldenFilePath = join(goldenDir, goldenName);

const {succeeded, apiReportChanged} = await testApiGolden(
Expand All @@ -46,6 +45,7 @@ async function main(
stripExportPattern,
typeNames,
packageJsonPath,
moduleName,
);

// Keep track of outdated goldens.
Expand Down
22 changes: 15 additions & 7 deletions bazel/api-golden/test_api_report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/

import {runfiles} from '@bazel/runfiles';
import {
ConsoleMessageId,
Extractor,
Expand All @@ -17,9 +16,11 @@ import {
ExtractorResult,
IConfigFile,
} from '@microsoft/api-extractor';
import {basename, dirname} from 'path';

import {AstModule} from '@microsoft/api-extractor/lib/analyzer/AstModule';
import {ExportAnalyzer} from '@microsoft/api-extractor/lib/analyzer/ExportAnalyzer';
import {basename, dirname} from 'path';
import {runfiles} from '@bazel/runfiles';

/**
* Original definition of the `ExportAnalyzer#fetchAstModuleExportInfo` method.
Expand All @@ -42,6 +43,9 @@ const _origFetchAstModuleExportInfo = ExportAnalyzer.prototype.fetchAstModuleExp
* @param packageJsonPath Optional path to a `package.json` file that contains the entry
* point. Note that the `package.json` is currently only used by `api-extractor` to determine
* the package name displayed within the API golden.
* @param customPackageName A custom package name to be provided for the API report. This can be
* useful when the specified `package.json` is describing the whole package but the API report
* is scoped to a specific subpath/entry-point.
*/
export async function testApiGolden(
goldenFilePath: string,
Expand All @@ -50,6 +54,7 @@ export async function testApiGolden(
stripExportPattern: RegExp,
typeNames: string[] = [],
packageJsonPath = resolveWorkspacePackageJsonPath(),
customPackageName?: string,
): Promise<ExtractorResult> {
// If no `TEST_TMPDIR` is defined, then this script runs using `bazel run`. We use
// the runfile directory as temporary directory for API extractor.
Expand Down Expand Up @@ -90,11 +95,14 @@ export async function testApiGolden(
// compatible with the API extractor. This is a workaround for a bug in api-extractor.
// TODO remove once https://github.com/microsoft/rushstack/issues/2774 is resolved.
const packageJson = require(packageJsonPath);
const packageNameSegments = packageJson.name.split('/');
const packageName =
packageNameSegments.length === 1
? packageNameSegments[0]
: `${packageNameSegments[0]}/${packageNameSegments.slice(1).join('_')}`;
const packageNameSegments = (customPackageName ?? packageJson.name).split('/');
const isScopedPackage = packageNameSegments[0][0] === '@' && packageNameSegments.length > 1;
// API extractor allows one-slash when the package uses the scoped-package convention.
const slashConversionStartIndex = isScopedPackage ? 1 : 0;
const normalizedRest = packageNameSegments.slice(slashConversionStartIndex).join('_');
const packageName = isScopedPackage
? `${packageNameSegments[0]}/${normalizedRest}`
: normalizedRest;

const extractorConfig = ExtractorConfig.prepare({
configObject,
Expand Down

0 comments on commit 6379fcf

Please sign in to comment.