From 38f2a980e08337f150c2840d961e44cc361aa37d Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Thu, 24 Mar 2022 12:16:35 +0100 Subject: [PATCH] fix(bazel): continue to support non-exports resolution in api-golden test This is needed since the CLI does not use exports yet. See: https://github.com/angular/angular-cli/issues/22889 --- bazel/api-golden/BUILD.bazel | 1 + bazel/api-golden/find_entry_points.ts | 68 ++++++++++++++----- bazel/api-golden/index_npm_packages.ts | 17 ++++- bazel/api-golden/path-normalize.ts | 14 ++++ bazel/api-golden/test/BUILD.bazel | 10 +++ bazel/api-golden/test/fixtures/BUILD.bazel | 6 +- .../fixtures/pkg_no_exports_field/index.d.ts | 1 + .../pkg_no_exports_field/package.json | 5 ++ .../pkg_no_exports_field/testing/index.d.ts | 5 ++ .../pkg_no_exports_field/testing/nested.d.ts | 1 + .../testing/nested/package.json | 3 + .../pkg_no_exports_field/testing/package.json | 3 + .../goldens/pkg_no_exports_field/index.md | 12 ++++ .../pkg_no_exports_field/testing/index.md | 15 ++++ .../testing/nested/index.md | 12 ++++ 15 files changed, 154 insertions(+), 19 deletions(-) create mode 100644 bazel/api-golden/path-normalize.ts create mode 100644 bazel/api-golden/test/fixtures/pkg_no_exports_field/index.d.ts create mode 100644 bazel/api-golden/test/fixtures/pkg_no_exports_field/package.json create mode 100644 bazel/api-golden/test/fixtures/pkg_no_exports_field/testing/index.d.ts create mode 100644 bazel/api-golden/test/fixtures/pkg_no_exports_field/testing/nested.d.ts create mode 100644 bazel/api-golden/test/fixtures/pkg_no_exports_field/testing/nested/package.json create mode 100644 bazel/api-golden/test/fixtures/pkg_no_exports_field/testing/package.json create mode 100644 bazel/api-golden/test/goldens/pkg_no_exports_field/index.md create mode 100644 bazel/api-golden/test/goldens/pkg_no_exports_field/testing/index.md create mode 100644 bazel/api-golden/test/goldens/pkg_no_exports_field/testing/nested/index.md diff --git a/bazel/api-golden/BUILD.bazel b/bazel/api-golden/BUILD.bazel index e44f467f2..e82f0282e 100644 --- a/bazel/api-golden/BUILD.bazel +++ b/bazel/api-golden/BUILD.bazel @@ -13,6 +13,7 @@ ts_library( "find_entry_points.ts", "index.ts", "index_npm_packages.ts", + "path-normalize.ts", "test_api_report.ts", ], # A tsconfig needs to be specified as otherwise `ts_library` will look for the config diff --git a/bazel/api-golden/find_entry_points.ts b/bazel/api-golden/find_entry_points.ts index 44c89244c..5ddad3fd4 100644 --- a/bazel/api-golden/find_entry_points.ts +++ b/bazel/api-golden/find_entry_points.ts @@ -6,43 +6,34 @@ * found in the LICENSE file at https://angular.io/license */ -import {join, normalize} from 'path'; +import {dirname, join, relative} from 'path'; +import {lstatSync, readFileSync, readdirSync} from 'fs'; -import {readFileSync} from 'fs'; +import {PackageJson} from './index_npm_packages'; /** Interface describing a resolved NPM package entry point. */ export interface PackageEntryPoint { typesEntryPointPath: string; subpath: string; - moduleName: string; -} - -/** Interface describing contents of a `package.json`. */ -interface PackageJson { - name: string; - exports?: Record; } /** Finds all entry points within a given NPM package directory. */ export function findEntryPointsWithinNpmPackage( dirPath: string, - packageJsonPath: string, + packageJson: PackageJson, ): PackageEntryPoint[] { - const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as PackageJson; const entryPoints: PackageEntryPoint[] = []; + // Legacy behavior to support Angular packages without the `exports` field. + // TODO: Remove when https://github.com/angular/angular-cli/issues/22889 is resolved. if (packageJson.exports === undefined) { - throw new Error( - `Expected top-level "package.json" in "${dirPath}" to declare entry-points ` + - `through conditional exports.`, - ); + return findEntryPointsByThroughNestedPackageFiles(dirPath); } for (const [subpath, conditions] of Object.entries(packageJson.exports)) { if (conditions.types !== undefined) { entryPoints.push({ subpath, - moduleName: normalize(join(packageJson.name, subpath)).replace(/\\/g, '/'), typesEntryPointPath: join(dirPath, conditions.types), }); } @@ -50,3 +41,48 @@ export function findEntryPointsWithinNpmPackage( return entryPoints; } + +/** + * Finds all entry points within a given NPM package directory by looking + * for nested `package.json` files. This is a legacy mechanism since we can + * consult the `package.json` `exports` information in most cases. + */ +function findEntryPointsByThroughNestedPackageFiles(dirPath: string): PackageEntryPoint[] { + const entryPoints: PackageEntryPoint[] = []; + + for (const packageJsonFilePath of findPackageJsonFilesInDirectory(dirPath)) { + const packageJson = JSON.parse(readFileSync(packageJsonFilePath, 'utf8')) as PackageJson; + const subpath = relative(dirPath, dirname(packageJsonFilePath)); + const typesFile = packageJson.types || packageJson.typings; + + if (typesFile) { + entryPoints.push({ + subpath, + typesEntryPointPath: join(dirname(packageJsonFilePath), typesFile), + }); + } + } + + 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 { + for (const fileName of readdirSync(directoryPath)) { + const fullPath = join(directoryPath, fileName); + if (isDirectory(fullPath)) { + yield* findPackageJsonFilesInDirectory(fullPath); + } else if (fileName === 'package.json') { + yield fullPath; + } + } +} diff --git a/bazel/api-golden/index_npm_packages.ts b/bazel/api-golden/index_npm_packages.ts index 1b8340f23..d1fae4fe4 100644 --- a/bazel/api-golden/index_npm_packages.ts +++ b/bazel/api-golden/index_npm_packages.ts @@ -10,9 +10,19 @@ import * as chalk from 'chalk'; import {findEntryPointsWithinNpmPackage} from './find_entry_points'; import {join} from 'path'; +import {normalizePathToPosix} from './path-normalize'; +import {readFileSync} from 'fs'; import {runfiles} from '@bazel/runfiles'; import {testApiGolden} from './test_api_report'; +/** Interface describing contents of a `package.json`. */ +export interface PackageJson { + name: string; + exports?: Record; + types?: string; + typings?: string; +} + /** * Entry point for the `api_golden_test_npm_package` Bazel rule. This function determines * all types within the specified NPM package and builds API reports that will be compared @@ -26,17 +36,20 @@ async function main( typeNames: string[], ) { const packageJsonPath = join(npmPackageDir, 'package.json'); - const entryPoints = findEntryPointsWithinNpmPackage(npmPackageDir, packageJsonPath); + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as PackageJson; + const entryPoints = findEntryPointsWithinNpmPackage(npmPackageDir, packageJson); const outdatedGoldens: string[] = []; + let allTestsSucceeding = true; - for (const {subpath, moduleName, typesEntryPointPath} of entryPoints) { + for (const {subpath, 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 defining NodeJS exports subpath in the NPM package, // See: https://api-extractor.com/pages/overview/demo_api_report/. const goldenName = join(subpath, 'index.md'); const goldenFilePath = join(goldenDir, goldenName); + const moduleName = normalizePathToPosix(join(packageJson.name, subpath)); const {succeeded, apiReportChanged} = await testApiGolden( goldenFilePath, diff --git a/bazel/api-golden/path-normalize.ts b/bazel/api-golden/path-normalize.ts new file mode 100644 index 000000000..c8cf44d01 --- /dev/null +++ b/bazel/api-golden/path-normalize.ts @@ -0,0 +1,14 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {normalize} from 'path'; + +/** Normalizes a path to use Posix forward-slash separators. */ +export function normalizePathToPosix(input: string): string { + return normalize(input).replace(/\\/g, '/'); +} diff --git a/bazel/api-golden/test/BUILD.bazel b/bazel/api-golden/test/BUILD.bazel index 7659fcc2a..6ee4aca0a 100644 --- a/bazel/api-golden/test/BUILD.bazel +++ b/bazel/api-golden/test/BUILD.bazel @@ -20,3 +20,13 @@ api_golden_test_npm_package( golden_dir = "dev-infra/bazel/api-golden/test/goldens/test_package", npm_package = "dev-infra/bazel/api-golden/test/fixtures/test_package", ) + +api_golden_test_npm_package( + name = "test_npm_package_no_exports_field", + data = [ + "goldens/pkg_no_exports_field", + "//bazel/api-golden/test/fixtures:pkg_no_exports_field", + ], + golden_dir = "dev-infra/bazel/api-golden/test/goldens/pkg_no_exports_field", + npm_package = "dev-infra/bazel/api-golden/test/fixtures/pkg_no_exports_field", +) diff --git a/bazel/api-golden/test/fixtures/BUILD.bazel b/bazel/api-golden/test/fixtures/BUILD.bazel index d16055d46..54b18ddea 100644 --- a/bazel/api-golden/test/fixtures/BUILD.bazel +++ b/bazel/api-golden/test/fixtures/BUILD.bazel @@ -2,7 +2,11 @@ load("//tools:defaults.bzl", "ts_library") package(default_visibility = ["//bazel/api-golden/test:__pkg__"]) -exports_files(["test_package"]) +# Expose these two fixture directories as tree artifacts. +exports_files([ + "test_package", + "pkg_no_exports_field", +]) ts_library( name = "test_lib", diff --git a/bazel/api-golden/test/fixtures/pkg_no_exports_field/index.d.ts b/bazel/api-golden/test/fixtures/pkg_no_exports_field/index.d.ts new file mode 100644 index 000000000..bc0e134d1 --- /dev/null +++ b/bazel/api-golden/test/fixtures/pkg_no_exports_field/index.d.ts @@ -0,0 +1 @@ +export declare const fromTopLevel: boolean; diff --git a/bazel/api-golden/test/fixtures/pkg_no_exports_field/package.json b/bazel/api-golden/test/fixtures/pkg_no_exports_field/package.json new file mode 100644 index 000000000..9673464b2 --- /dev/null +++ b/bazel/api-golden/test/fixtures/pkg_no_exports_field/package.json @@ -0,0 +1,5 @@ +{ + "name": "test-package", + "type": "module", + "types": "./index.d.ts" +} diff --git a/bazel/api-golden/test/fixtures/pkg_no_exports_field/testing/index.d.ts b/bazel/api-golden/test/fixtures/pkg_no_exports_field/testing/index.d.ts new file mode 100644 index 000000000..9098de978 --- /dev/null +++ b/bazel/api-golden/test/fixtures/pkg_no_exports_field/testing/index.d.ts @@ -0,0 +1,5 @@ +import {fromTopLevel} from 'test-package'; + +export declare const anotherVariable = fromTopLevel; + +export declare function rawStringFn(input: string): string; diff --git a/bazel/api-golden/test/fixtures/pkg_no_exports_field/testing/nested.d.ts b/bazel/api-golden/test/fixtures/pkg_no_exports_field/testing/nested.d.ts new file mode 100644 index 000000000..c6ee8f70b --- /dev/null +++ b/bazel/api-golden/test/fixtures/pkg_no_exports_field/testing/nested.d.ts @@ -0,0 +1 @@ +export declare const nestedFile = true; diff --git a/bazel/api-golden/test/fixtures/pkg_no_exports_field/testing/nested/package.json b/bazel/api-golden/test/fixtures/pkg_no_exports_field/testing/nested/package.json new file mode 100644 index 000000000..2b93c1774 --- /dev/null +++ b/bazel/api-golden/test/fixtures/pkg_no_exports_field/testing/nested/package.json @@ -0,0 +1,3 @@ +{ + "types": "../nested.d.ts" +} diff --git a/bazel/api-golden/test/fixtures/pkg_no_exports_field/testing/package.json b/bazel/api-golden/test/fixtures/pkg_no_exports_field/testing/package.json new file mode 100644 index 000000000..1704e6b78 --- /dev/null +++ b/bazel/api-golden/test/fixtures/pkg_no_exports_field/testing/package.json @@ -0,0 +1,3 @@ +{ + "types": "./index.d.ts" +} diff --git a/bazel/api-golden/test/goldens/pkg_no_exports_field/index.md b/bazel/api-golden/test/goldens/pkg_no_exports_field/index.md new file mode 100644 index 000000000..35adea5ae --- /dev/null +++ b/bazel/api-golden/test/goldens/pkg_no_exports_field/index.md @@ -0,0 +1,12 @@ +## API Report File for "test-package" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public (undocumented) +export const fromTopLevel: boolean; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/bazel/api-golden/test/goldens/pkg_no_exports_field/testing/index.md b/bazel/api-golden/test/goldens/pkg_no_exports_field/testing/index.md new file mode 100644 index 000000000..a5cf40207 --- /dev/null +++ b/bazel/api-golden/test/goldens/pkg_no_exports_field/testing/index.md @@ -0,0 +1,15 @@ +## API Report File for "test-package_testing" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public (undocumented) +export const anotherVariable = fromTopLevel; + +// @public (undocumented) +export function rawStringFn(input: string): string; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/bazel/api-golden/test/goldens/pkg_no_exports_field/testing/nested/index.md b/bazel/api-golden/test/goldens/pkg_no_exports_field/testing/nested/index.md new file mode 100644 index 000000000..493a3199d --- /dev/null +++ b/bazel/api-golden/test/goldens/pkg_no_exports_field/testing/nested/index.md @@ -0,0 +1,12 @@ +## API Report File for "test-package_testing_nested" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public (undocumented) +export const nestedFile = true; + +// (No @packageDocumentation comment for this package) + +```