Skip to content

Commit db87cc1

Browse files
Support bundled dependencies in resolver plugin (#4903)
* [lookup-by-path] Return linked list of matches * [workspace-resolve-plugin] Handle hierarchical node_modules * [rush-resolve-plugin] Support "bundledDependencies" * Apply suggestions from code review Reformat change logs, add comments about numeric values. Co-authored-by: Ian Clanton-Thuon <[email protected]> --------- Co-authored-by: David Michon <[email protected]> Co-authored-by: Ian Clanton-Thuon <[email protected]>
1 parent f8eb2c2 commit db87cc1

20 files changed

+498
-1335
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@microsoft/rush",
5+
"comment": "Support `bundledDependencies` in rush-resolver-cache-plugin.",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@microsoft/rush"
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@rushstack/lookup-by-path",
5+
"comment": "Return a linked list of matches in `findLongestPrefixMatch` in the event that multiple prefixes match. The head of the list is the most specific match.",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "@rushstack/lookup-by-path"
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@rushstack/webpack-workspace-resolve-plugin",
5+
"comment": "Support hierarchical `node_modules` folders.",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "@rushstack/webpack-workspace-resolve-plugin"
10+
}

common/reviews/api/lookup-by-path.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
// @beta
88
export interface IPrefixMatch<TItem> {
99
index: number;
10+
lastMatch?: IPrefixMatch<TItem>;
1011
value: TItem;
1112
}
1213

common/reviews/api/webpack-workspace-resolve-plugin.api.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export interface IResolverCacheFile {
2525

2626
// @beta
2727
export interface ISerializedResolveContext {
28-
deps: Record<string, number>;
28+
deps?: Record<string, number>;
2929
dirInfoFiles?: string[];
3030
name: string;
3131
root: string;
@@ -46,7 +46,7 @@ export interface IWorkspaceResolvePluginOptions {
4646
// @beta
4747
export class WorkspaceLayoutCache {
4848
constructor(options: IWorkspaceLayoutCacheOptions);
49-
readonly contextForPackage: WeakMap<object, IResolveContext>;
49+
readonly contextForPackage: WeakMap<object, IPrefixMatch<IResolveContext>>;
5050
readonly contextLookup: LookupByPath<IResolveContext>;
5151
// (undocumented)
5252
readonly normalizeToPlatform: IPathNormalizationFunction;

libraries/lookup-by-path/src/LookupByPath.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,11 @@ describe(LookupByPath.prototype.findLongestPrefixMatch.name, () => {
133133
['foo/bar', 4]
134134
]);
135135

136-
expect(tree.findLongestPrefixMatch('foo/bar')).toEqual({ value: 4, index: 7 });
136+
expect(tree.findLongestPrefixMatch('foo/bar')).toEqual({
137+
value: 4,
138+
index: 7,
139+
lastMatch: { value: 1, index: 3 }
140+
});
137141
expect(tree.findLongestPrefixMatch('barbar/baz')).toEqual({ value: 2, index: 6 });
138142
expect(tree.findLongestPrefixMatch('baz/foo')).toEqual({ value: 3, index: 3 });
139143
expect(tree.findLongestPrefixMatch('foo/foo')).toEqual({ value: 1, index: 3 });

libraries/lookup-by-path/src/LookupByPath.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ export interface IPrefixMatch<TItem> {
4040
* The index of the first character after the matched prefix
4141
*/
4242
index: number;
43+
/**
44+
* The last match found (with a shorter prefix), if any
45+
*/
46+
lastMatch?: IPrefixMatch<TItem>;
4347
}
4448

4549
/**
@@ -255,7 +259,8 @@ export class LookupByPath<TItem> {
255259
let best: IPrefixMatch<TItem> | undefined = node.value
256260
? {
257261
value: node.value,
258-
index: 0
262+
index: 0,
263+
lastMatch: undefined
259264
}
260265
: undefined;
261266
// Trivial cases
@@ -269,7 +274,8 @@ export class LookupByPath<TItem> {
269274
if (node.value !== undefined) {
270275
best = {
271276
value: node.value,
272-
index
277+
index,
278+
lastMatch: best
273279
};
274280
}
275281
if (!node.children) {

libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ export interface IPnpmShrinkwrapDependencyYaml {
5959
/** The name of the tarball, if this was from a TGZ file */
6060
tarball?: string;
6161
};
62+
/** The list of bundled dependencies in this package */
63+
bundledDependencies?: ReadonlyArray<string>;
6264
/** The list of dependencies and the resolved version */
6365
dependencies?: Record<string, IPnpmVersionSpecifier>;
6466
/** The list of optional dependencies and the resolved version */

rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,12 @@ export async function afterInstallAsync(
117117

118118
const filteredFiles: string[] = Object.keys(files).filter((file) => file.endsWith('/package.json'));
119119
if (filteredFiles.length > 0) {
120-
// eslint-disable-next-line require-atomic-updates
121-
context.files = filteredFiles.map((x) => x.slice(0, -13));
120+
const nestedPackageDirs: string[] = filteredFiles.map((x) => x.slice(0, /* -'/package.json'.length */ -13));
121+
122+
if (nestedPackageDirs.length > 0) {
123+
// eslint-disable-next-line require-atomic-updates
124+
context.nestedPackageDirs = nestedPackageDirs;
125+
}
122126
}
123127
} catch (error) {
124128
if (!context.optional) {

rush-plugins/rush-resolver-cache-plugin/src/computeResolverCacheFromLockfileAsync.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,59 @@ function isPackageCompatible(
4545
return true;
4646
}
4747

48+
function extractBundledDependencies(
49+
contexts: Map<string, IResolverContext>,
50+
context: IResolverContext
51+
): void {
52+
const { nestedPackageDirs } = context;
53+
if (!nestedPackageDirs) {
54+
return;
55+
}
56+
57+
for (let i: number = nestedPackageDirs.length - 1; i >= 0; i--) {
58+
const nestedDir: string = nestedPackageDirs[i];
59+
if (!nestedDir.startsWith('node_modules/')) {
60+
continue;
61+
}
62+
63+
const isScoped: boolean = nestedDir.charAt(/* 'node_modules/'.length */ 13) === '@';
64+
let index: number = nestedDir.indexOf('/', 13);
65+
if (isScoped) {
66+
index = nestedDir.indexOf('/', index + 1);
67+
}
68+
69+
const name: string = index === -1 ? nestedDir.slice(13) : nestedDir.slice(13, index);
70+
if (name.startsWith('.')) {
71+
continue;
72+
}
73+
74+
// Remove this nested package from the list
75+
nestedPackageDirs.splice(i, 1);
76+
77+
const remainder: string = index === -1 ? '' : nestedDir.slice(index + 1);
78+
const nestedRoot: string = `${context.descriptionFileRoot}/node_modules/${name}`;
79+
let nestedContext: IResolverContext | undefined = contexts.get(nestedRoot);
80+
if (!nestedContext) {
81+
nestedContext = {
82+
descriptionFileRoot: nestedRoot,
83+
descriptionFileHash: undefined,
84+
isProject: false,
85+
name,
86+
deps: new Map(),
87+
ordinal: -1
88+
};
89+
contexts.set(nestedRoot, nestedContext);
90+
}
91+
92+
context.deps.set(name, nestedRoot);
93+
94+
if (remainder) {
95+
nestedContext.nestedPackageDirs ??= [];
96+
nestedContext.nestedPackageDirs.push(remainder);
97+
}
98+
}
99+
}
100+
48101
/**
49102
* Options for computing the resolver cache from a lockfile.
50103
*/
@@ -130,7 +183,7 @@ export async function computeResolverCacheFromLockfileAsync(
130183
isProject: false,
131184
name,
132185
deps: new Map(),
133-
ordinal: contexts.size,
186+
ordinal: -1,
134187
optional: pack.optional
135188
};
136189

@@ -148,6 +201,12 @@ export async function computeResolverCacheFromLockfileAsync(
148201
await afterExternalPackagesAsync(contexts, missingOptionalDependencies);
149202
}
150203

204+
for (const context of contexts.values()) {
205+
if (context.nestedPackageDirs) {
206+
extractBundledDependencies(contexts, context);
207+
}
208+
}
209+
151210
// Add the data for workspace projects
152211
for (const [importerPath, importer] of lockfile.importers) {
153212
// Ignore the root project. This plugin assumes you don't have one.
@@ -167,7 +226,7 @@ export async function computeResolverCacheFromLockfileAsync(
167226
name: project.packageJson.name,
168227
isProject: true,
169228
deps: new Map(),
170-
ordinal: contexts.size
229+
ordinal: -1
171230
};
172231

173232
contexts.set(project.projectFolder, context);
@@ -183,6 +242,11 @@ export async function computeResolverCacheFromLockfileAsync(
183242
}
184243
}
185244

245+
let ordinal: number = 0;
246+
for (const context of contexts.values()) {
247+
context.ordinal = ordinal++;
248+
}
249+
186250
// Convert the intermediate representation to the final cache file
187251
const serializedContexts: ISerializedResolveContext[] = Array.from(
188252
contexts,

0 commit comments

Comments
 (0)