Skip to content

Commit 043da3b

Browse files
authored
Merge pull request #4434 from Josmithr/regexp-bundledPackages
api-extractor: Add glob support in `bundledPackages`
2 parents aaa3a88 + 323c72e commit 043da3b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+752
-28
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ These GitHub repositories provide supplementary resources for Rush Stack:
140140
| [/build-tests/api-extractor-lib1-test](./build-tests/api-extractor-lib1-test/) | Building this project is a regression test for api-extractor |
141141
| [/build-tests/api-extractor-lib2-test](./build-tests/api-extractor-lib2-test/) | Building this project is a regression test for api-extractor |
142142
| [/build-tests/api-extractor-lib3-test](./build-tests/api-extractor-lib3-test/) | Building this project is a regression test for api-extractor |
143+
| [/build-tests/api-extractor-lib4-test](./build-tests/api-extractor-lib4-test/) | Building this project is a regression test for api-extractor |
144+
| [/build-tests/api-extractor-lib5-test](./build-tests/api-extractor-lib5-test/) | Building this project is a regression test for api-extractor |
143145
| [/build-tests/api-extractor-scenarios](./build-tests/api-extractor-scenarios/) | Building this project is a regression test for api-extractor |
144146
| [/build-tests/api-extractor-test-01](./build-tests/api-extractor-test-01/) | Building this project is a regression test for api-extractor |
145147
| [/build-tests/api-extractor-test-02](./build-tests/api-extractor-test-02/) | Building this project is a regression test for api-extractor |

apps/api-extractor/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"@rushstack/terminal": "workspace:*",
4646
"@rushstack/ts-command-line": "workspace:*",
4747
"lodash": "~4.17.15",
48+
"minimatch": "~3.0.3",
4849
"resolve": "~1.22.1",
4950
"semver": "~7.5.4",
5051
"source-map": "~0.6.1",
@@ -55,6 +56,7 @@
5556
"@rushstack/heft": "0.65.5",
5657
"@types/heft-jest": "1.0.1",
5758
"@types/lodash": "4.14.116",
59+
"@types/minimatch": "3.0.5",
5860
"@types/node": "18.17.15",
5961
"@types/resolve": "1.20.2",
6062
"@types/semver": "7.5.0",

apps/api-extractor/src/api/ExtractorConfig.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -837,11 +837,9 @@ export class ExtractorConfig {
837837
}
838838

839839
const bundledPackages: string[] = configObject.bundledPackages || [];
840-
for (const bundledPackage of bundledPackages) {
841-
if (!PackageName.isValidName(bundledPackage)) {
842-
throw new Error(`The "bundledPackages" list contains an invalid package name: "${bundledPackage}"`);
843-
}
844-
}
840+
841+
// Note: we cannot fully validate package name patterns, as the strings may contain wildcards.
842+
// We won't know if the entries are valid until we can compare them against the package.json "dependencies" contents.
845843

846844
const tsconfigFilePath: string = ExtractorConfig._resolvePathWithTokens(
847845
'tsconfigFilePath',

apps/api-extractor/src/api/IConfigFile.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,8 +378,17 @@ export interface IConfigFile {
378378
* A list of NPM package names whose exports should be treated as part of this package.
379379
*
380380
* @remarks
381+
* Also supports glob patterns.
382+
* Note: glob patterns will **only** be resolved against dependencies listed in the project's package.json file.
381383
*
382-
* For example, suppose that Webpack is used to generate a distributed bundle for the project `library1`,
384+
* * This is both a safety and a performance precaution.
385+
*
386+
* Exact package names will be applied against any dependency encountered while walking the type graph, regardless of
387+
* dependencies listed in the package.json.
388+
*
389+
* @example
390+
*
391+
* Suppose that Webpack is used to generate a distributed bundle for the project `library1`,
383392
* and another NPM package `library2` is embedded in this bundle. Some types from `library2` may become part
384393
* of the exported API for `library1`, but by default API Extractor would generate a .d.ts rollup that explicitly
385394
* imports `library2`. To avoid this, we can specify:

apps/api-extractor/src/collector/Collector.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,15 @@
33

44
import * as ts from 'typescript';
55
import * as tsdoc from '@microsoft/tsdoc';
6-
import { PackageJsonLookup, Sort, InternalError } from '@rushstack/node-core-library';
6+
import {
7+
PackageJsonLookup,
8+
Sort,
9+
InternalError,
10+
type INodePackageJson,
11+
PackageName
12+
} from '@rushstack/node-core-library';
713
import { ReleaseTag } from '@microsoft/api-extractor-model';
14+
import minimatch from 'minimatch';
815

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

@@ -132,7 +139,11 @@ export class Collector {
132139

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

135-
this.bundledPackageNames = new Set<string>(this.extractorConfig.bundledPackages);
142+
// Resolve package name patterns and store concrete set of bundled package dependency names
143+
this.bundledPackageNames = Collector._resolveBundledPackagePatterns(
144+
this.extractorConfig.bundledPackages,
145+
this.extractorConfig.packageJson
146+
);
136147

137148
this.astSymbolTable = new AstSymbolTable(
138149
this.program,
@@ -147,6 +158,55 @@ export class Collector {
147158
}
148159

149160
/**
161+
* Resolve provided `bundledPackages` names and glob patterns to a list of explicit package names.
162+
*
163+
* @remarks
164+
* Explicit package names will be included in the output unconditionally. However, wildcard patterns will
165+
* only be matched against the various dependencies listed in the provided package.json (if there was one).
166+
* Patterns will be matched against `dependencies`, `devDependencies`, `optionalDependencies`, and `peerDependencies`.
167+
*
168+
* @param bundledPackages - The list of package names and/or glob patterns to resolve.
169+
* @param packageJson - The package.json of the package being processed (if there is one).
170+
* @returns The set of resolved package names to be bundled during analysis.
171+
*/
172+
private static _resolveBundledPackagePatterns(
173+
bundledPackages: string[],
174+
packageJson: INodePackageJson | undefined
175+
): ReadonlySet<string> {
176+
if (bundledPackages.length === 0) {
177+
// If no `bundledPackages` were specified, then there is nothing to resolve.
178+
// Return an empty set.
179+
return new Set<string>();
180+
}
181+
182+
// Accumulate all declared dependencies.
183+
// Any wildcard patterns in `bundledPackages` will be resolved against these.
184+
const dependencyNames: Set<string> = new Set<string>();
185+
Object.keys(packageJson?.dependencies ?? {}).forEach((dep) => dependencyNames.add(dep));
186+
Object.keys(packageJson?.devDependencies ?? {}).forEach((dep) => dependencyNames.add(dep));
187+
Object.keys(packageJson?.peerDependencies ?? {}).forEach((dep) => dependencyNames.add(dep));
188+
Object.keys(packageJson?.optionalDependencies ?? {}).forEach((dep) => dependencyNames.add(dep));
189+
190+
// The set of resolved package names to be populated and returned
191+
const resolvedPackageNames: Set<string> = new Set<string>();
192+
193+
for (const packageNameOrPattern of bundledPackages) {
194+
// If the string is an exact package name, use it regardless of package.json contents
195+
if (PackageName.isValidName(packageNameOrPattern)) {
196+
resolvedPackageNames.add(packageNameOrPattern);
197+
} else {
198+
// If the entry isn't an exact package name, assume glob pattern and search for matches
199+
for (const dependencyName of dependencyNames) {
200+
if (minimatch(dependencyName, packageNameOrPattern)) {
201+
resolvedPackageNames.add(dependencyName);
202+
}
203+
}
204+
}
205+
}
206+
return resolvedPackageNames;
207+
}
208+
209+
/**a
150210
* Returns a list of names (e.g. "example-library") that should appear in a reference like this:
151211
*
152212
* ```

apps/api-extractor/src/generators/DtsRollupGenerator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export class DtsRollupGenerator {
107107
if (entity.astEntity instanceof AstImport) {
108108
// Note: it isn't valid to trim imports based on their release tags.
109109
// E.g. class Foo (`@public`) extends interface Bar (`@beta`) from some external library.
110-
// API-Extractor cannot trim `import { Bar } from "externa-library"` when generating its public rollup,
110+
// API-Extractor cannot trim `import { Bar } from "external-library"` when generating its public rollup,
111111
// or the export of `Foo` would include a broken reference to `Bar`.
112112
const astImport: AstImport = entity.astEntity;
113113
DtsEmitHelpers.emitImport(writer, entity, astImport);

apps/api-extractor/src/schemas/api-extractor-template.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,19 @@
5353
* For example, suppose that Webpack is used to generate a distributed bundle for the project "library1",
5454
* and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part
5555
* of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly
56-
* imports library2. To avoid this, we can specify:
56+
* imports library2. To avoid this, we might specify:
5757
*
5858
* "bundledPackages": [ "library2" ],
5959
*
6060
* This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been
6161
* local files for library1.
62+
*
63+
* The "bundledPackages" elements may specify glob patterns using minimatch syntax. To ensure deterministic
64+
* output, globs are expanded by matching explicitly declared top-level dependencies only. For example,
65+
* the pattern below will NOT match "@my-company/example" unless it appears in a field such as "dependencies"
66+
* or "devDependencies" of the project's package.json file:
67+
*
68+
* "bundledPackages": [ "@my-company/*" ],
6269
*/
6370
"bundledPackages": [],
6471

apps/api-extractor/src/schemas/api-extractor.schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
},
2525

2626
"bundledPackages": {
27-
"description": "A list of NPM package names whose exports should be treated as part of this package.",
27+
"description": "A list of NPM package names whose exports should be treated as part of this package. Also supports glob patterns.",
2828
"type": "array",
2929
"items": {
3030
"type": "string"

build-tests/api-extractor-lib3-test/dist/api-extractor-lib3-test.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,9 @@ import { Lib1Class } from 'api-extractor-lib1-test';
1111

1212
export { Lib1Class }
1313

14+
/** @public */
15+
export declare class Lib3Class {
16+
prop: boolean;
17+
}
18+
1419
export { }

build-tests/api-extractor-lib3-test/etc/api-extractor-lib3-test.api.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,10 @@ import { Lib1Class } from 'api-extractor-lib1-test';
88

99
export { Lib1Class }
1010

11+
// @public (undocumented)
12+
export class Lib3Class {
13+
// (undocumented)
14+
prop: boolean;
15+
}
16+
1117
```

0 commit comments

Comments
 (0)