Skip to content

Commit 47fdbc2

Browse files
feat(enhanced): Recursive search for versions of shared dependencies (#3078)
Co-authored-by: Zack Jackson <[email protected]> Co-authored-by: ScriptedAlchemy <[email protected]>
1 parent 8712967 commit 47fdbc2

File tree

21 files changed

+190
-38
lines changed

21 files changed

+190
-38
lines changed

.changeset/smooth-goats-knock.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@module-federation/enhanced': patch
3+
---
4+
5+
Added recursively search for shared dependency versions

packages/enhanced/src/lib/sharing/ConsumeSharedPlugin.ts

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,6 @@ class ConsumeSharedPlugin {
129129
}
130130

131131
apply(compiler: Compiler): void {
132-
//@ts-ignore
133132
new FederationRuntimePlugin().apply(compiler);
134133
process.env['FEDERATION_WEBPACK_PATH'] =
135134
process.env['FEDERATION_WEBPACK_PATH'] || getWebpackPath(compiler);
@@ -167,7 +166,6 @@ class ConsumeSharedPlugin {
167166
`No required version specified and unable to automatically determine one. ${details}`,
168167
);
169168
error.file = `shared module ${request}`;
170-
//@ts-ignore
171169
compilation.warnings.push(error);
172170
};
173171
const directFallback =
@@ -198,7 +196,6 @@ class ConsumeSharedPlugin {
198196
);
199197
if (err) {
200198
compilation.errors.push(
201-
//@ts-ignore
202199
new ModuleNotFoundError(null, err, {
203200
name: `resolving fallback for shared module ${request}`,
204201
}),
@@ -235,39 +232,53 @@ class ConsumeSharedPlugin {
235232
compilation.inputFileSystem,
236233
context,
237234
['package.json'],
238-
(err, result) => {
235+
(err, result, checkedDescriptionFilePaths) => {
239236
if (err) {
240237
requiredVersionWarning(
241238
`Unable to read description file: ${err}`,
242239
);
243240
return resolve(undefined);
244241
}
245-
//@ts-ignore
246-
const { data, path: descriptionPath } = result;
242+
const { data } = /** @type {DescriptionFile} */ result || {};
247243
if (!data) {
248-
requiredVersionWarning(
249-
`Unable to find description file in ${context}.`,
250-
);
244+
if (checkedDescriptionFilePaths?.length) {
245+
requiredVersionWarning(
246+
[
247+
`Unable to find required version for "${packageName}" in description file/s`,
248+
checkedDescriptionFilePaths.join('\n'),
249+
'It need to be in dependencies, devDependencies or peerDependencies.',
250+
].join('\n'),
251+
);
252+
} else {
253+
requiredVersionWarning(
254+
`Unable to find description file in ${context}.`,
255+
);
256+
}
257+
251258
return resolve(undefined);
252259
}
253-
//@ts-ignore
254-
if (data.name === packageName) {
260+
if (data['name'] === packageName) {
255261
// Package self-referencing
256262
return resolve(undefined);
257263
}
258264
const requiredVersion = getRequiredVersionFromDescriptionFile(
259265
data,
260266
packageName,
261267
);
262-
if (typeof requiredVersion !== 'string') {
263-
requiredVersionWarning(
264-
`Unable to find required version for "${packageName}" in description file (${descriptionPath}). It need to be in dependencies, devDependencies or peerDependencies.`,
265-
);
266-
return resolve(undefined);
267-
}
268+
//TODO: align with webpck semver parser again
268269
// @ts-ignore webpack internal semver has some issue, use runtime semver , related issue: https://github.com/webpack/webpack/issues/17756
269270
resolve(requiredVersion);
270271
},
272+
(result) => {
273+
if (!result) return false;
274+
const { data } = result;
275+
const maybeRequiredVersion =
276+
getRequiredVersionFromDescriptionFile(data, packageName);
277+
return (
278+
data['name'] === packageName ||
279+
typeof maybeRequiredVersion === 'string'
280+
);
281+
},
271282
);
272283
}),
273284
]).then(([importResolved, requiredVersion]) => {
@@ -315,7 +326,6 @@ class ConsumeSharedPlugin {
315326
);
316327
normalModuleFactory.hooks.createModule.tapPromise(
317328
PLUGIN_NAME,
318-
//@ts-ignore
319329
({ resource }, { context, dependencies }) => {
320330
if (
321331
dependencies[0] instanceof ConsumeSharedFallbackDependency ||
@@ -343,7 +353,6 @@ class ConsumeSharedPlugin {
343353
set.add(RuntimeGlobals.hasOwnProperty);
344354
compilation.addRuntimeModule(
345355
chunk,
346-
//@ts-ignore
347356
new ConsumeSharedRuntimeModule(set),
348357
);
349358
// FIXME: need to remove webpack internal inject ShareRuntimeModule, otherwise there will be two ShareRuntimeModule

packages/enhanced/src/lib/sharing/utils.ts

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,35 +12,33 @@ const { join, dirname, readJson } = require(
1212
) as typeof import('webpack/lib/util/fs');
1313

1414
// Extreme shorthand only for github. eg: foo/bar
15-
const RE_URL_GITHUB_EXTREME_SHORT: RegExp =
16-
/^[^/@:.\s][^/@:\s]*\/[^@:\s]*[^/@:\s]#\S+/;
15+
const RE_URL_GITHUB_EXTREME_SHORT = /^[^/@:.\s][^/@:\s]*\/[^@:\s]*[^/@:\s]#\S+/;
1716

1817
// Short url with specific protocol. eg: github:foo/bar
19-
const RE_GIT_URL_SHORT: RegExp =
20-
/^(github|gitlab|bitbucket|gist):\/?[^/.]+\/?/i;
18+
const RE_GIT_URL_SHORT = /^(github|gitlab|bitbucket|gist):\/?[^/.]+\/?/i;
2119

2220
// Currently supported protocols
23-
const RE_PROTOCOL: RegExp =
21+
const RE_PROTOCOL =
2422
/^((git\+)?(ssh|https?|file)|git|github|gitlab|bitbucket|gist):$/i;
2523

2624
// Has custom protocol
27-
const RE_CUSTOM_PROTOCOL: RegExp = /^((git\+)?(ssh|https?|file)|git):\/\//i;
25+
const RE_CUSTOM_PROTOCOL = /^((git\+)?(ssh|https?|file)|git):\/\//i;
2826

2927
// Valid hash format for npm / yarn ...
30-
const RE_URL_HASH_VERSION: RegExp = /#(?:semver:)?(.+)/;
28+
const RE_URL_HASH_VERSION = /#(?:semver:)?(.+)/;
3129

3230
// Simple hostname validate
33-
const RE_HOSTNAME: RegExp = /^(?:[^/.]+(\.[^/]+)+|localhost)$/;
31+
const RE_HOSTNAME = /^(?:[^/.]+(\.[^/]+)+|localhost)$/;
3432

3533
// For hostname with colon. eg: ssh://user@github.com:foo/bar
36-
const RE_HOSTNAME_WITH_COLON: RegExp =
34+
const RE_HOSTNAME_WITH_COLON =
3735
/([^/@#:.]+(?:\.[^/@#:.]+)+|localhost):([^#/0-9]+)/;
3836

3937
// Reg for url without protocol
40-
const RE_NO_PROTOCOL: RegExp = /^([^/@#:.]+(?:\.[^/@#:.]+)+)/;
38+
const RE_NO_PROTOCOL = /^([^/@#:.]+(?:\.[^/@#:.]+)+)/;
4139

4240
// Specific protocol for short url without normal hostname
43-
const PROTOCOLS_FOR_SHORT: string[] = [
41+
const PROTOCOLS_FOR_SHORT = [
4442
'github:',
4543
'gitlab:',
4644
'bitbucket:',
@@ -49,7 +47,7 @@ const PROTOCOLS_FOR_SHORT: string[] = [
4947
];
5048

5149
// Default protocol for git url
52-
const DEF_GIT_PROTOCOL: string = 'git+ssh://';
50+
const DEF_GIT_PROTOCOL = 'git+ssh://';
5351

5452
// thanks to https://github.com/npm/hosted-git-info/blob/latest/git-host-info.js
5553
const extractCommithashByDomain: {
@@ -330,32 +328,59 @@ function normalizeVersion(versionDesc: string): string {
330328

331329
export { normalizeVersion };
332330

331+
/** @typedef {{ data: Record<string, any>, path: string }} DescriptionFile */
332+
333+
interface DescriptionFile {
334+
data: Record<string, any>;
335+
path: string;
336+
}
337+
333338
/**
334339
*
335340
* @param {InputFileSystem} fs file system
336341
* @param {string} directory directory to start looking into
337342
* @param {string[]} descriptionFiles possible description filenames
338-
* @param {function((Error | null)=, {data: object, path: string}=): void} callback callback
343+
* @param {function((Error | null)=, {data?: DescriptionFile, path?: string}=, (checkedDescriptionFilePaths: string[])=): void} callback callback
344+
* @param {function({data: DescriptionFile}=): boolean} satisfiesDescriptionFileData file data compliance check
345+
* @param {Set<string>} [checkedFilePaths] optional set to track checked file paths
339346
*/
340-
const getDescriptionFile = (
347+
function getDescriptionFile(
341348
fs: InputFileSystem,
342349
directory: string,
343350
descriptionFiles: string[],
344-
callback: (err: Error | null, data?: { data: object; path: string }) => void,
345-
) => {
351+
callback: (
352+
err: Error | null,
353+
data?: DescriptionFile,
354+
checkedDescriptionFilePaths?: string[],
355+
) => void,
356+
satisfiesDescriptionFileData?: (data?: DescriptionFile) => boolean,
357+
checkedFilePaths: Set<string> = new Set<string>(), // Default to a new Set if not provided
358+
) {
346359
let i = 0;
360+
361+
// Create an object to hold the function and the shared checkedFilePaths
362+
const satisfiesDescriptionFileDataInternal = {
363+
check: satisfiesDescriptionFileData,
364+
checkedFilePaths: checkedFilePaths, // Use the passed Set instance
365+
};
366+
347367
const tryLoadCurrent = () => {
348368
if (i >= descriptionFiles.length) {
349369
const parentDirectory = dirname(fs, directory);
350370
if (!parentDirectory || parentDirectory === directory) {
351-
//@ts-ignore
352-
return callback();
371+
return callback(
372+
null,
373+
undefined,
374+
Array.from(satisfiesDescriptionFileDataInternal.checkedFilePaths),
375+
);
353376
}
354377
return getDescriptionFile(
355378
fs,
356379
parentDirectory,
357380
descriptionFiles,
358381
callback,
382+
satisfiesDescriptionFileDataInternal.check,
383+
satisfiesDescriptionFileDataInternal.checkedFilePaths, // Pass the same Set
359384
);
360385
}
361386
const filePath = join(fs, directory, descriptionFiles[i]);
@@ -372,11 +397,19 @@ const getDescriptionFile = (
372397
new Error(`Description file ${filePath} is not an object`),
373398
);
374399
}
400+
if (
401+
typeof satisfiesDescriptionFileDataInternal.check === 'function' &&
402+
!satisfiesDescriptionFileDataInternal.check({ data, path: filePath })
403+
) {
404+
i++;
405+
satisfiesDescriptionFileDataInternal.checkedFilePaths.add(filePath);
406+
return tryLoadCurrent();
407+
}
375408
callback(null, { data, path: filePath });
376409
});
377410
};
378411
tryLoadCurrent();
379-
};
412+
}
380413
export { getDescriptionFile };
381414
/**
382415
* Get required version from description file
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
it('should provide own dependency', async () => {
2+
expect(await import('lib')).toEqual(
3+
expect.objectContaining({
4+
5+
}),
6+
);
7+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"type": "commonjs"
3+
}

packages/enhanced/test/configCases/sharing/share-plugin-dual-mode/node_modules/lib/index.js

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/enhanced/test/configCases/sharing/share-plugin-dual-mode/node_modules/lib/package.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/enhanced/test/configCases/sharing/share-plugin-dual-mode/node_modules/transitive_lib/index.js

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/enhanced/test/configCases/sharing/share-plugin-dual-mode/node_modules/transitive_lib/package.json

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"dependencies": {
3+
"lib": "^1.0.0"
4+
}
5+
}

0 commit comments

Comments
 (0)