Skip to content

feat(enhanced): add request to consume share #3307

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Dec 5, 2024
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
5 changes: 5 additions & 0 deletions .changeset/brown-badgers-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@module-federation/enhanced': minor
---

support request option on ConsumeSharePlugin. Allows matching requests like the object key of shared does
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,8 @@ export interface ConsumesConfig {
* Layer for the shared module.
*/
layer?: string;
/**
* The actual request to use for importing the module. If not specified, the property name/key will be used.
*/
request?: string;
}
4 changes: 4 additions & 0 deletions packages/enhanced/src/lib/sharing/ConsumeSharedModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ export type ConsumeOptions = {
* resolved fallback request
*/
importResolved?: string | undefined;
/**
* The actual request to use for importing the module. If not specified, the property name/key will be used.
*/
request?: string;
/**
* global share key
*/
Expand Down
119 changes: 60 additions & 59 deletions packages/enhanced/src/lib/sharing/ConsumeSharedPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import FederationRuntimePlugin from '../container/runtime/FederationRuntimePlugi
import ShareRuntimeModule from './ShareRuntimeModule';
import type { SemVerRange } from 'webpack/lib/util/semver';
import type { ResolveData } from 'webpack/lib/NormalModuleFactory';
import type { ModuleFactoryCreateDataContextInfo } from 'webpack/lib/ModuleFactory';

const ModuleNotFoundError = require(
normalizeWebpackPath('webpack/lib/ModuleNotFoundError'),
Expand Down Expand Up @@ -61,6 +62,17 @@ const RESOLVE_OPTIONS: ResolveOptionsWithDependencyType = {
dependencyType: 'esm',
};
const PLUGIN_NAME = 'ConsumeSharedPlugin';

// Helper function to create composite key
function createLookupKey(
request: string,
contextInfo: ModuleFactoryCreateDataContextInfo,
): string {
return contextInfo.issuerLayer
? `(${contextInfo.issuerLayer})${request}`
: request;
}

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

Expand Down Expand Up @@ -88,6 +100,7 @@ class ConsumeSharedPlugin {
eager: false,
issuerLayer: undefined,
layer: undefined,
request: key,
}
: // key is a request/key
// item is a version
Expand All @@ -103,26 +116,33 @@ class ConsumeSharedPlugin {
eager: false,
issuerLayer: undefined,
layer: undefined,
request: key,
};
return result;
},
(item, key) => ({
import: item.import === false ? undefined : item.import || key,
shareScope: item.shareScope || options.shareScope || 'default',
shareKey: item.shareKey || key,
// @ts-ignore webpack internal semver has some issue, use runtime semver , related issue: https://github.com/webpack/webpack/issues/17756
requiredVersion: item.requiredVersion,
strictVersion:
typeof item.strictVersion === 'boolean'
? item.strictVersion
: item.import !== false && !item.singleton,
//@ts-ignore
packageName: item.packageName,
singleton: !!item.singleton,
eager: !!item.eager,
issuerLayer: item.issuerLayer ? item.issuerLayer : undefined,
layer: item.layer ? item.layer : undefined,
}),
(item, key) => {
const request = item.request || key;
return {
import: item.import === false ? undefined : item.import || request,
shareScope: item.shareScope || options.shareScope || 'default',
shareKey: item.shareKey || request,
requiredVersion:
item.requiredVersion === false
? false
: // @ts-ignore webpack internal semver has some issue, use runtime semver , related issue: https://github.com/webpack/webpack/issues/17756
(item.requiredVersion as SemVerRange),
strictVersion:
typeof item.strictVersion === 'boolean'
? item.strictVersion
: item.import !== false && !item.singleton,
packageName: item.packageName,
singleton: !!item.singleton,
eager: !!item.eager,
issuerLayer: item.issuerLayer ? item.issuerLayer : undefined,
layer: item.layer ? item.layer : undefined,
request,
} as ConsumeOptions;
},
);
}

Expand Down Expand Up @@ -305,54 +325,35 @@ class ConsumeSharedPlugin {
) {
return;
}
const match = unresolvedConsumes.get(
createLookupKey(request, contextInfo),
);

// First try to match with layer-specific request
if (contextInfo.issuerLayer) {
// Try to find a layer-specific match
for (const [key, options] of unresolvedConsumes) {
if (
options.issuerLayer === contextInfo.issuerLayer &&
(key === request ||
(options.import && options.import === request))
) {
return createConsumeSharedModule(context, request, {
...options,
layer: options.layer || contextInfo.issuerLayer,
});
}
}
}
// not sure if i need this with the `request` options passthrough
// if (match === undefined) {

// // fallback to using alias
// match = unresolvedConsumes.get(request);
// // check alias matches issuerLayer
// if (match && match.issuerLayer !== contextInfo.issuerLayer) {
// match = undefined;
// }
// }

// If no layer-specific match found, try regular matching
const match = unresolvedConsumes.get(request);
if (match !== undefined) {
// Only use non-layer-specific match if it doesn't have issuerLayer
if (!match.issuerLayer) {
return createConsumeSharedModule(context, request, {
...match,
layer: match.layer || contextInfo.issuerLayer,
});
}
return createConsumeSharedModule(context, request, match);
}

// Check prefixed consumes
for (const [prefix, options] of prefixedConsumes) {
if (request.startsWith(prefix)) {
// Only use prefixed consume if layer matches or no layer specified
if (
!options.issuerLayer ||
options.issuerLayer === contextInfo.issuerLayer
) {
const remainder = request.slice(prefix.length);
return createConsumeSharedModule(context, request, {
...options,
import: options.import
? options.import + remainder
: undefined,
shareKey: options.shareKey + remainder,
layer: options.layer || contextInfo.issuerLayer,
});
}
const remainder = request.slice(prefix.length);
return createConsumeSharedModule(context, request, {
...options,
import: options.import
? options.import + remainder
: undefined,
shareKey: options.shareKey + remainder,
layer: options.layer || contextInfo.issuerLayer,
});
}
}
});
Expand Down
46 changes: 33 additions & 13 deletions packages/enhanced/src/lib/sharing/resolveMatchedConfigs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path';
import type { Compilation } from 'webpack';
import type { ResolveOptionsWithDependencyType } from 'webpack/lib/ResolverFactory';
import type { SemVerRange } from 'webpack/lib/util/semver';
import type { ConsumeOptions } from './ConsumeSharedModule';

const ModuleNotFoundError = require(
normalizeWebpackPath('webpack/lib/ModuleNotFoundError'),
Expand All @@ -13,6 +15,9 @@ const LazySet = require(
normalizeWebpackPath('webpack/lib/util/LazySet'),
) as typeof import('webpack/lib/util/LazySet');

const RELATIVE_REQUEST_REGEX = /^\.\.?(\/|$)/;
const ABSOLUTE_PATH_REGEX = /^(\/|[A-Za-z]:\\|\\\\)/;

interface MatchedConfigs<T> {
resolved: Map<string, T>;
unresolved: Map<string, T>;
Expand All @@ -23,7 +28,19 @@ const RESOLVE_OPTIONS: ResolveOptionsWithDependencyType = {
dependencyType: 'esm',
};

export async function resolveMatchedConfigs<T>(
function createCompositeKey(request: string, config: ConsumeOptions): string {
if (config.issuerLayer) {
return `(${config.issuerLayer})${request}`;
// layer unlikely to be used, issuerLayer is what factorize provides
// which is what we need to create a matching key for
} else if (config.layer) {
return `(${config.layer})${request}`;
} else {
return request;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

used for when there is no layer etc,

}
}
// TODO: look at passing dedicated request key instead of infer from object key
export async function resolveMatchedConfigs<T extends ConsumeOptions>(
compilation: Compilation,
configs: [string, T][],
): Promise<MatchedConfigs<T>> {
Expand All @@ -35,28 +52,26 @@ export async function resolveMatchedConfigs<T>(
contextDependencies: new LazySet<string>(),
missingDependencies: new LazySet<string>(),
};
// @ts-ignore
const resolver = compilation.resolverFactory.get('normal', RESOLVE_OPTIONS);
const context = compilation.compiler.context;

await Promise.all(
//@ts-ignore
configs.map(([request, config]) => {
if (/^\.\.?(\/|$)/.test(request)) {
const resolveRequest = config.request || request;
if (RELATIVE_REQUEST_REGEX.test(resolveRequest)) {
// relative request
return new Promise<void>((resolve) => {
resolver.resolve(
{},
context,
request,
resolveRequest,
resolveContext,
(err, result) => {
if (err || result === false) {
err = err || new Error(`Can't resolve ${request}`);
err = err || new Error(`Can't resolve ${resolveRequest}`);
compilation.errors.push(
// @ts-ignore
new ModuleNotFoundError(null, err, {
name: `shared module ${request}`,
name: `shared module ${resolveRequest}`,
}),
);
return resolve();
Expand All @@ -66,15 +81,20 @@ export async function resolveMatchedConfigs<T>(
},
);
});
} else if (/^(\/|[A-Za-z]:\\|\\\\)/.test(request)) {
} else if (ABSOLUTE_PATH_REGEX.test(resolveRequest)) {
// absolute path
resolved.set(request, config);
} else if (request.endsWith('/')) {
resolved.set(resolveRequest, config);
return undefined;
} else if (resolveRequest.endsWith('/')) {
// module request prefix
prefixed.set(request, config);
const key = createCompositeKey(resolveRequest, config);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with the request param, i can now support prefix shared etc without a problem

prefixed.set(key, config);
return undefined;
} else {
// module request
unresolved.set(request, config);
const key = createCompositeKey(resolveRequest, config);
unresolved.set(key, config);
return undefined;
}
}),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function r(
'singleton' !== t &&
'strictVersion' !== t &&
'issuerLayer' !== t &&
'request' !== t &&
'layer' !== t
)
return (r.errors = [{ params: { additionalProperty: t } }]), !1;
Expand Down
6 changes: 6 additions & 0 deletions packages/enhanced/src/schemas/sharing/ConsumeSharedPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ export default {
type: 'string',
minLength: 1,
},
request: {
description:
'The actual request to use for importing the module. If not specified, the property name/key will be used.',
type: 'string',
minLength: 1,
},
},
},
ConsumesItem: {
Expand Down
1 change: 0 additions & 1 deletion packages/enhanced/test/ConfigTestCases.template.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ const describeCases = (config) => {
for (const category of categories) {
// eslint-disable-next-line no-loop-func
describe(category.name, () => {
// category.tests = [category.tests[1]];
for (const testName of category.tests) {
// eslint-disable-next-line no-loop-func
describe(testName, function () {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Loader that injects the multi-pkg layer name as an export
*/
module.exports = function multiPkgLayerLoader(source) {
return [source, 'export const layer = "multi-pkg-layer";'].join('\n');
};

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
@@ -1,6 +1,7 @@
/**
* Main test entry point
*/
import '../tests/layer-inheritance.test';
import '../tests/unlayered-share.test';
import '../tests/different-layers.test';
import '../tests/lib-two.test';
import '../tests/prefixed-share.test';
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/**
* Tests for different layer configurations
* Tests for different layer configurations in shared module consumption
*/
it('Module graph should have a different layer', async () => {
it('should consume shared React module from differing-layer when test is in differing-layer', async () => {
const { version, layer } = await import('react');
expect(version).toBe('1.0.0');
expect(layer).toBe('differing-layer');
});

it('Module graph should have a layer set explicitly thats not the inherited issuerLayer', async () => {
it('should consume React with explicit-layer override when importing index2 from differing-layer', async () => {
const { dix, layer } = await import('react/index2');
expect(dix).toBe('1.0.0');
expect(layer).toBe('explicit-layer');
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Tests for lib-two module sharing with lib-two-required-layer configurations
*/

it('should consume lib-two v1.3.4 from lib-two-required-layer with eager loading', async () => {
const { version, layer } = await import('lib-two');
expect(version).toBe('1.3.4');
expect(layer).toBe('differing-layer'); // Using the layer from different-layer-loader
});

it('should consume lib-two-layered v1.3.4 from lib-two-required-layer with eager loading', async () => {
const { version, layer } = await import('lib-two-layered');
expect(version).toBe('1.3.4');
expect(layer).toBe('differing-layer'); // Using the layer from different-layer-loader
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Tests for prefixed module sharing with different layers
*/
it('should consume thing1 from multi-pkg with multi-pkg-layer', async () => {
const { version, layer } = await import('multi-pkg/thing1');
expect(version).toBe('2.0.0');
expect(layer).toBe('multi-pkg-layer');
});

it('should consume thing2 from multi-pkg with multi-pkg-layer', async () => {
const { version, layer } = await import('multi-pkg/thing2');
expect(version).toBe('2.0.0');
expect(layer).toBe('multi-pkg-layer');
});
Loading
Loading