Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/consume-version-filter-package-name.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@module-federation/enhanced': patch
---

Read the version for `include.version` and `exclude.version` on consumed
shared modules from the `package.json` whose `name` matches the consumed
package. Prefix consumes such as `'lib/'` matched against `lib/sub`, scoped
packages, and `node_modules` suffix matches previously skipped the filter
and were always consumed. Relative, absolute, and aliased consume configs
are unchanged unless `packageName` is set.
232 changes: 94 additions & 138 deletions packages/enhanced/src/lib/sharing/ConsumeSharedPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import ProvideForSharedDependency from './ProvideForSharedDependency';
import FederationRuntimePlugin from '../container/runtime/FederationRuntimePlugin';
import ShareRuntimeModule from './ShareRuntimeModule';
import type { SemVerRange } from 'webpack/lib/util/semver';
import type { InputFileSystem } from 'webpack/lib/util/fs';
import type { ResolveData } from 'webpack/lib/NormalModuleFactory';
import type { ModuleFactoryCreateDataContextInfo } from 'webpack/lib/ModuleFactory';
import type { ConsumeOptions } from '@module-federation/sdk';
Expand Down Expand Up @@ -80,6 +81,25 @@ const RESOLVE_OPTIONS: ResolveOptionsWithDependencyType = {
};
const PLUGIN_NAME = 'ConsumeSharedPlugin';

function getPackageVersion(
fs: InputFileSystem,
resource: string,
packageName: string,
): Promise<string | undefined> {
return new Promise((resolve) => {
getDescriptionFile(
fs,
path.dirname(resource),
['package.json'],
(err, result) => resolve(err ? undefined : result?.data['version']),
(result) =>
!!result &&
typeof result.data['version'] === 'string' &&
result.data['name'] === packageName,
);
});
}

class ConsumeSharedPlugin {
private _consumes: [string, ConsumeOptions][];

Expand Down Expand Up @@ -170,7 +190,7 @@ class ConsumeSharedPlugin {
context: string,
request: string,
config: ConsumeOptions,
): Promise<ConsumeSharedModule> {
): Promise<ConsumeSharedModule | undefined> {
const requiredVersionWarning = (details: string) => {
const error = new WebpackError(
`No required version specified and unable to automatically determine one. ${details}`,
Expand All @@ -180,6 +200,11 @@ class ConsumeSharedPlugin {
};
const directFallback =
config.import && DIRECT_FALLBACK_REGEX.test(config.import);
const packageName =
config.packageName ??
(ABSOLUTE_PATH_REGEX.test(request)
? undefined
: PACKAGE_NAME_REGEX.exec(request)?.[0]);

const resolver: ResolverWithOptions = compilation.resolverFactory.get(
'normal',
Expand Down Expand Up @@ -226,21 +251,13 @@ class ConsumeSharedPlugin {
if (config.requiredVersion !== undefined) {
return resolve(config.requiredVersion);
}
let packageName = config.packageName;
if (packageName === undefined) {
if (ABSOLUTE_PATH_REGEX.test(request)) {
// For relative or absolute requests we don't automatically use a packageName.
// If wished one can specify one with the packageName option.
return resolve(undefined);
}
const match = PACKAGE_NAME_REGEX.exec(request);
if (!match) {
if (!ABSOLUTE_PATH_REGEX.test(request)) {
requiredVersionWarning(
'Unable to extract the package name from request.',
);
return resolve(undefined);
}
packageName = match[0];
return resolve(undefined);
}

getDescriptionFile(
Expand Down Expand Up @@ -308,145 +325,84 @@ class ConsumeSharedPlugin {
currentConfig,
);

// Check for include version first
if (config.include && typeof config.include.version === 'string') {
if (!importResolved) {
const { include, exclude } = config;
if (include && typeof include.version === 'string') {
if (!importResolved || packageName === undefined) {
return consumedModule;
}

return new Promise((resolveFilter) => {
getDescriptionFile(
compilation.inputFileSystem,
path.dirname(importResolved as string),
['package.json'],
(err, result) => {
if (err) {
return resolveFilter(consumedModule);
}
const { data } = result || {};
if (!data || !data['version'] || data['name'] !== request) {
return resolveFilter(consumedModule);
}

// Only include if version satisfies the include constraint
if (
config.include &&
satisfy(
parseRange(config.include.version as string),
data['version'],
)
) {
// Validate singleton usage with include.version
if (
config.include &&
config.include.version &&
config.singleton
) {
addSingletonFilterWarning(
compilation,
config.shareKey || request,
'include',
'version',
config.include.version,
request, // moduleRequest
importResolved, // moduleResource (might be undefined)
);
}

return resolveFilter(consumedModule);
}

// Check fallback version
if (
config.include &&
typeof config.include.fallbackVersion === 'string' &&
config.include.fallbackVersion
) {
if (
satisfy(
parseRange(config.include.version as string),
config.include.fallbackVersion,
)
) {
return resolveFilter(consumedModule);
}
return resolveFilter(
undefined as unknown as ConsumeSharedModule,
);
}

return resolveFilter(undefined as unknown as ConsumeSharedModule);
},
);
const includeVersion = include.version;
return getPackageVersion(
compilation.inputFileSystem,
importResolved,
packageName,
).then((version) => {
if (!version) {
return consumedModule;
}
if (satisfy(parseRange(includeVersion), version)) {
if (config.singleton) {
addSingletonFilterWarning(
compilation,
config.shareKey || request,
'include',
'version',
includeVersion,
request,
importResolved,
);
}
return consumedModule;
}
if (
typeof include.fallbackVersion === 'string' &&
include.fallbackVersion &&
satisfy(parseRange(includeVersion), include.fallbackVersion)
) {
return consumedModule;
}
return undefined;
});
}

// Check for exclude version (existing logic)
if (config.exclude && typeof config.exclude.version === 'string') {
if (exclude && typeof exclude.version === 'string') {
if (!importResolved) {
return consumedModule;
}

const excludeVersion = exclude.version;
if (
config.exclude &&
typeof config.exclude.fallbackVersion === 'string' &&
config.exclude.fallbackVersion
typeof exclude.fallbackVersion === 'string' &&
exclude.fallbackVersion
) {
if (
satisfy(
parseRange(config.exclude.version),
config.exclude.fallbackVersion,
)
) {
return undefined as unknown as ConsumeSharedModule;
}
return satisfy(parseRange(excludeVersion), exclude.fallbackVersion)
? undefined
: consumedModule;
}
if (packageName === undefined) {
return consumedModule;
}

return new Promise((resolveFilter) => {
getDescriptionFile(
compilation.inputFileSystem,
path.dirname(importResolved as string),
['package.json'],
(err, result) => {
if (err) {
return resolveFilter(consumedModule);
}
const { data } = result || {};
if (!data || !data['version'] || data['name'] !== request) {
return resolveFilter(consumedModule);
}

if (
config.exclude &&
typeof config.exclude.version === 'string' &&
satisfy(parseRange(config.exclude.version), data['version'])
) {
return resolveFilter(
undefined as unknown as ConsumeSharedModule,
);
}

// Validate singleton usage with exclude.version
if (
config.exclude &&
config.exclude.version &&
config.singleton
) {
addSingletonFilterWarning(
compilation,
config.shareKey || request,
'exclude',
'version',
config.exclude.version,
request, // moduleRequest
importResolved, // moduleResource (might be undefined)
);
}

return resolveFilter(consumedModule);
},
);
return getPackageVersion(
compilation.inputFileSystem,
importResolved,
packageName,
).then((version) => {
if (!version) {
return consumedModule;
}
if (satisfy(parseRange(excludeVersion), version)) {
return undefined;
}
if (config.singleton) {
addSingletonFilterWarning(
compilation,
config.shareKey || request,
'exclude',
'version',
excludeVersion,
request,
importResolved,
);
}
return consumedModule;
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const consumedShareKeys = () =>
Object.values(
__webpack_require__.consumesLoadingData.moduleIdToConsumeDataMapping,
).map((data) => data.shareKey);

it('consumes a prefix match whose package version satisfies include.version', async () => {
const mod = await import('lib-included/sub');
expect(mod.default).toBe('lib-included/sub');
expect(consumedShareKeys()).toContain('lib-included/sub');
});

it('does not consume a prefix match whose package version fails include.version', async () => {
const mod = await import('lib/sub');
expect(mod.default).toBe('lib/sub');
expect(consumedShareKeys()).not.toContain('lib/sub');
});

it('does not consume a prefix match whose package version matches exclude.version', async () => {
const mod = await import('lib-excluded/sub');
expect(mod.default).toBe('lib-excluded/sub');
expect(consumedShareKeys()).not.toContain('lib-excluded/sub');
});

it('consumes a relative request with include.version because it has no package name', async () => {
const mod = await import('./src/local');
expect(mod.default).toBe('local');
expect(consumedShareKeys()).toContain('./src/local');
});

it('does not consume a relative request whose exclude.fallbackVersion matches exclude.version', async () => {
const mod = await import('./src/local2');
expect(mod.default).toBe('local2');
expect(consumedShareKeys()).not.toContain('./src/local2');
});

it('does not consume an aliased request whose packageName version fails include.version', async () => {
const mod = await import('my-lib');
expect(mod.default).toBe('lib');
expect(consumedShareKeys()).not.toContain('my-lib');
});

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "consume-filters-prefix-version-test",
"version": "1.0.0",
"private": true,
"dependencies": {
"lib": "^1.0.0",
"lib-included": "^1.0.0",
"lib-excluded": "^1.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'local';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'local2';
Loading
Loading