Skip to content

Commit

Permalink
Merge pull request #4434 from Josmithr/regexp-bundledPackages
Browse files Browse the repository at this point in the history
api-extractor: Add glob support in `bundledPackages`
  • Loading branch information
octogonz authored Feb 29, 2024
2 parents aaa3a88 + 323c72e commit 043da3b
Show file tree
Hide file tree
Showing 42 changed files with 752 additions and 28 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ These GitHub repositories provide supplementary resources for Rush Stack:
| [/build-tests/api-extractor-lib1-test](./build-tests/api-extractor-lib1-test/) | Building this project is a regression test for api-extractor |
| [/build-tests/api-extractor-lib2-test](./build-tests/api-extractor-lib2-test/) | Building this project is a regression test for api-extractor |
| [/build-tests/api-extractor-lib3-test](./build-tests/api-extractor-lib3-test/) | Building this project is a regression test for api-extractor |
| [/build-tests/api-extractor-lib4-test](./build-tests/api-extractor-lib4-test/) | Building this project is a regression test for api-extractor |
| [/build-tests/api-extractor-lib5-test](./build-tests/api-extractor-lib5-test/) | Building this project is a regression test for api-extractor |
| [/build-tests/api-extractor-scenarios](./build-tests/api-extractor-scenarios/) | Building this project is a regression test for api-extractor |
| [/build-tests/api-extractor-test-01](./build-tests/api-extractor-test-01/) | Building this project is a regression test for api-extractor |
| [/build-tests/api-extractor-test-02](./build-tests/api-extractor-test-02/) | Building this project is a regression test for api-extractor |
Expand Down
2 changes: 2 additions & 0 deletions apps/api-extractor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@rushstack/terminal": "workspace:*",
"@rushstack/ts-command-line": "workspace:*",
"lodash": "~4.17.15",
"minimatch": "~3.0.3",
"resolve": "~1.22.1",
"semver": "~7.5.4",
"source-map": "~0.6.1",
Expand All @@ -55,6 +56,7 @@
"@rushstack/heft": "0.65.5",
"@types/heft-jest": "1.0.1",
"@types/lodash": "4.14.116",
"@types/minimatch": "3.0.5",
"@types/node": "18.17.15",
"@types/resolve": "1.20.2",
"@types/semver": "7.5.0",
Expand Down
8 changes: 3 additions & 5 deletions apps/api-extractor/src/api/ExtractorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -837,11 +837,9 @@ export class ExtractorConfig {
}

const bundledPackages: string[] = configObject.bundledPackages || [];
for (const bundledPackage of bundledPackages) {
if (!PackageName.isValidName(bundledPackage)) {
throw new Error(`The "bundledPackages" list contains an invalid package name: "${bundledPackage}"`);
}
}

// Note: we cannot fully validate package name patterns, as the strings may contain wildcards.
// We won't know if the entries are valid until we can compare them against the package.json "dependencies" contents.

const tsconfigFilePath: string = ExtractorConfig._resolvePathWithTokens(
'tsconfigFilePath',
Expand Down
11 changes: 10 additions & 1 deletion apps/api-extractor/src/api/IConfigFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,8 +378,17 @@ export interface IConfigFile {
* A list of NPM package names whose exports should be treated as part of this package.
*
* @remarks
* Also supports glob patterns.
* Note: glob patterns will **only** be resolved against dependencies listed in the project's package.json file.
*
* For example, suppose that Webpack is used to generate a distributed bundle for the project `library1`,
* * This is both a safety and a performance precaution.
*
* Exact package names will be applied against any dependency encountered while walking the type graph, regardless of
* dependencies listed in the package.json.
*
* @example
*
* Suppose that Webpack is used to generate a distributed bundle for the project `library1`,
* and another NPM package `library2` is embedded in this bundle. Some types from `library2` may become part
* of the exported API for `library1`, but by default API Extractor would generate a .d.ts rollup that explicitly
* imports `library2`. To avoid this, we can specify:
Expand Down
64 changes: 62 additions & 2 deletions apps/api-extractor/src/collector/Collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@

import * as ts from 'typescript';
import * as tsdoc from '@microsoft/tsdoc';
import { PackageJsonLookup, Sort, InternalError } from '@rushstack/node-core-library';
import {
PackageJsonLookup,
Sort,
InternalError,
type INodePackageJson,
PackageName
} from '@rushstack/node-core-library';
import { ReleaseTag } from '@microsoft/api-extractor-model';
import minimatch from 'minimatch';

import { ExtractorMessageId } from '../api/ExtractorMessageId';

Expand Down Expand Up @@ -132,7 +139,11 @@ export class Collector {

this._tsdocParser = new tsdoc.TSDocParser(this.extractorConfig.tsdocConfiguration);

this.bundledPackageNames = new Set<string>(this.extractorConfig.bundledPackages);
// Resolve package name patterns and store concrete set of bundled package dependency names
this.bundledPackageNames = Collector._resolveBundledPackagePatterns(
this.extractorConfig.bundledPackages,
this.extractorConfig.packageJson
);

this.astSymbolTable = new AstSymbolTable(
this.program,
Expand All @@ -147,6 +158,55 @@ export class Collector {
}

/**
* Resolve provided `bundledPackages` names and glob patterns to a list of explicit package names.
*
* @remarks
* Explicit package names will be included in the output unconditionally. However, wildcard patterns will
* only be matched against the various dependencies listed in the provided package.json (if there was one).
* Patterns will be matched against `dependencies`, `devDependencies`, `optionalDependencies`, and `peerDependencies`.
*
* @param bundledPackages - The list of package names and/or glob patterns to resolve.
* @param packageJson - The package.json of the package being processed (if there is one).
* @returns The set of resolved package names to be bundled during analysis.
*/
private static _resolveBundledPackagePatterns(
bundledPackages: string[],
packageJson: INodePackageJson | undefined
): ReadonlySet<string> {
if (bundledPackages.length === 0) {
// If no `bundledPackages` were specified, then there is nothing to resolve.
// Return an empty set.
return new Set<string>();
}

// Accumulate all declared dependencies.
// Any wildcard patterns in `bundledPackages` will be resolved against these.
const dependencyNames: Set<string> = new Set<string>();
Object.keys(packageJson?.dependencies ?? {}).forEach((dep) => dependencyNames.add(dep));
Object.keys(packageJson?.devDependencies ?? {}).forEach((dep) => dependencyNames.add(dep));
Object.keys(packageJson?.peerDependencies ?? {}).forEach((dep) => dependencyNames.add(dep));
Object.keys(packageJson?.optionalDependencies ?? {}).forEach((dep) => dependencyNames.add(dep));

// The set of resolved package names to be populated and returned
const resolvedPackageNames: Set<string> = new Set<string>();

for (const packageNameOrPattern of bundledPackages) {
// If the string is an exact package name, use it regardless of package.json contents
if (PackageName.isValidName(packageNameOrPattern)) {
resolvedPackageNames.add(packageNameOrPattern);
} else {
// If the entry isn't an exact package name, assume glob pattern and search for matches
for (const dependencyName of dependencyNames) {
if (minimatch(dependencyName, packageNameOrPattern)) {
resolvedPackageNames.add(dependencyName);
}
}
}
}
return resolvedPackageNames;
}

/**a
* Returns a list of names (e.g. "example-library") that should appear in a reference like this:
*
* ```
Expand Down
2 changes: 1 addition & 1 deletion apps/api-extractor/src/generators/DtsRollupGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export class DtsRollupGenerator {
if (entity.astEntity instanceof AstImport) {
// Note: it isn't valid to trim imports based on their release tags.
// E.g. class Foo (`@public`) extends interface Bar (`@beta`) from some external library.
// API-Extractor cannot trim `import { Bar } from "externa-library"` when generating its public rollup,
// API-Extractor cannot trim `import { Bar } from "external-library"` when generating its public rollup,
// or the export of `Foo` would include a broken reference to `Bar`.
const astImport: AstImport = entity.astEntity;
DtsEmitHelpers.emitImport(writer, entity, astImport);
Expand Down
9 changes: 8 additions & 1 deletion apps/api-extractor/src/schemas/api-extractor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,19 @@
* For example, suppose that Webpack is used to generate a distributed bundle for the project "library1",
* and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part
* of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly
* imports library2. To avoid this, we can specify:
* imports library2. To avoid this, we might specify:
*
* "bundledPackages": [ "library2" ],
*
* This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been
* local files for library1.
*
* The "bundledPackages" elements may specify glob patterns using minimatch syntax. To ensure deterministic
* output, globs are expanded by matching explicitly declared top-level dependencies only. For example,
* the pattern below will NOT match "@my-company/example" unless it appears in a field such as "dependencies"
* or "devDependencies" of the project's package.json file:
*
* "bundledPackages": [ "@my-company/*" ],
*/
"bundledPackages": [],

Expand Down
2 changes: 1 addition & 1 deletion apps/api-extractor/src/schemas/api-extractor.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
},

"bundledPackages": {
"description": "A list of NPM package names whose exports should be treated as part of this package.",
"description": "A list of NPM package names whose exports should be treated as part of this package. Also supports glob patterns.",
"type": "array",
"items": {
"type": "string"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,9 @@ import { Lib1Class } from 'api-extractor-lib1-test';

export { Lib1Class }

/** @public */
export declare class Lib3Class {
prop: boolean;
}

export { }
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,10 @@ import { Lib1Class } from 'api-extractor-lib1-test';

export { Lib1Class }

// @public (undocumented)
export class Lib3Class {
// (undocumented)
prop: boolean;
}

```
5 changes: 5 additions & 0 deletions build-tests/api-extractor-lib3-test/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@
*/

export { Lib1Class } from 'api-extractor-lib1-test';

/** @public */
export class Lib3Class {
prop: boolean;
}
4 changes: 4 additions & 0 deletions build-tests/api-extractor-lib4-test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# This project's outputs are tracked to surface changes to API Extractor rollups during PRs
!dist
dist/*
!dist/*.d.ts
27 changes: 27 additions & 0 deletions build-tests/api-extractor-lib4-test/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const fsx = require('fs-extra');
const child_process = require('child_process');
const path = require('path');
const process = require('process');

function executeCommand(command) {
console.log('---> ' + command);
child_process.execSync(command, { stdio: 'inherit' });
}

// Clean the old build outputs
console.log(`==> Starting build.js for ${path.basename(process.cwd())}`);
fsx.emptyDirSync('dist');
fsx.emptyDirSync('lib');
fsx.emptyDirSync('temp');

// Run the TypeScript compiler
executeCommand('node node_modules/typescript/lib/tsc');

// Run the API Extractor command-line
if (process.argv.indexOf('--production') >= 0) {
executeCommand('node node_modules/@microsoft/api-extractor/lib/start run');
} else {
executeCommand('node node_modules/@microsoft/api-extractor/lib/start run --local');
}

console.log(`==> Finished build.js for ${path.basename(process.cwd())}`);
19 changes: 19 additions & 0 deletions build-tests/api-extractor-lib4-test/config/api-extractor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",

"mainEntryPointFilePath": "<projectFolder>/lib/index.d.ts",

"apiReport": {
"enabled": true
},

"docModel": {
"enabled": true
},

"dtsRollup": {
"enabled": true
},

"testMode": true
}
10 changes: 10 additions & 0 deletions build-tests/api-extractor-lib4-test/config/rush-project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-project.schema.json",

"operationSettings": [
{
"operationName": "_phase:build",
"outputFolderNames": ["lib"]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* api-extractor-lib4-test
*
* @remarks
* This library is consumed by api-extractor-scenarios.
*
* @packageDocumentation
*/

/** @public */
export declare enum Lib4Enum {
Foo = "Foo",
Bar = "Bar",
Baz = "Baz"
}

export { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## API Report File for "api-extractor-lib4-test"

> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts

// @public (undocumented)
export enum Lib4Enum {
// (undocumented)
Bar = "Bar",
// (undocumented)
Baz = "Baz",
// (undocumented)
Foo = "Foo"
}

```
19 changes: 19 additions & 0 deletions build-tests/api-extractor-lib4-test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "api-extractor-lib4-test",
"description": "Building this project is a regression test for api-extractor",
"version": "1.0.0",
"private": true,
"main": "lib/index.js",
"typings": "dist/api-extractor-lib3-test.d.ts",
"scripts": {
"build": "node build.js",
"_phase:build": "node build.js"
},
"devDependencies": {
"@microsoft/api-extractor": "workspace:*",
"@types/jest": "29.2.5",
"@types/node": "18.17.15",
"fs-extra": "~7.0.1",
"typescript": "~5.3.3"
}
}
18 changes: 18 additions & 0 deletions build-tests/api-extractor-lib4-test/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

/**
* api-extractor-lib4-test
*
* @remarks
* This library is consumed by api-extractor-scenarios.
*
* @packageDocumentation
*/

/** @public */
export enum Lib4Enum {
Foo = 'Foo',
Bar = 'Bar',
Baz = 'Baz'
}
16 changes: 16 additions & 0 deletions build-tests/api-extractor-lib4-test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es6",
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"declaration": true,
"sourceMap": true,
"declarationMap": true,
"experimentalDecorators": true,
"strictNullChecks": true,
"types": ["node", "jest"],
"lib": ["es5", "scripthost", "es2015.collection", "es2015.promise", "es2015.iterable", "dom"],
"outDir": "lib"
},
"include": ["src/**/*.ts", "typings/tsd.d.ts"]
}
4 changes: 4 additions & 0 deletions build-tests/api-extractor-lib5-test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# This project's outputs are tracked to surface changes to API Extractor rollups during PRs
!dist
dist/*
!dist/*.d.ts
Loading

0 comments on commit 043da3b

Please sign in to comment.