Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 0 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@
"type": "node",
"request": "launch",
"preLaunchTask": "pnpm-build-enhanced",
"runtimeExecutable": "/Users/bytedance/.nvm/versions/node/v18.20.8/bin/node",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this hardcoded path really necessary?

Copy link
Member

Choose a reason for hiding this comment

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

probbably not

"runtimeArgs": [
"${workspaceFolder}/node_modules/jest/bin/jest.js",
"test/ConfigTestCases.basictest.js",
Expand Down
26 changes: 26 additions & 0 deletions packages/enhanced/src/lib/sharing/ConsumeSharedModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,37 @@ class ConsumeSharedModule extends Module {
);
if (this.options.eager) {
this.addDependency(dep);
// We'll need to get the module from the fallback dependency
// later to copy its buildMeta and buildInfo
} else {
const block = new AsyncDependenciesBlock({});
block.addDependency(dep);
this.addBlock(block);
// Same here for async case
}

// We need to hook into the compilation process to copy metadata
// after the fallback is fully built
compilation.hooks.finishModules.tap('ConsumeSharedModule', () => {
let dependency;

if (this.options.eager) {
// For eager mode, get the fallback directly
dependency = this.dependencies[0];
} else {
// For async mode, get it from the block
dependency = this.blocks[0]?.dependencies[0];
}

if (dependency) {
const fallback = compilation.moduleGraph.getModule(dependency);
if (fallback) {
// Copy its buildMeta and buildInfo from fallback module
this.buildMeta = { ...fallback.buildMeta };
this.buildInfo = { ...fallback.buildInfo };
}
}
});
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Following @ScriptedAlchemy hints 🙇, this is how I got buildMeta to be properly populated. finishModules seemed like the earliest hook to tap into, but there could be better ways of doing it. I'm open to any suggestions.

Copy link
Member

@ScriptedAlchemy ScriptedAlchemy Sep 5, 2025

Choose a reason for hiding this comment

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

Almost.
should not do this in the build method as it will cause massive hook registration per module built and rebuild, instead we should do it in the consume share plugin. Also compilation is not always guaranteed to be available in the build method of modules. For example when using loader.importModule or const plugin who evaluate macros, there is no compilation attached.

Ill push to this branch with the corrections for you to review.

Copy link
Contributor Author

@vinicius-at-webflow vinicius-at-webflow Sep 5, 2025

Choose a reason for hiding this comment

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

Nice! This all makes sense to me. I had a feeling registering in the plugin was the right approach - nowhere across the entire codebase we hook from within a Module, or anything other than a Plugin. I also overlooked the fact that we could

   (modules, callback) => {
            for (const module of modules) {
              // Only process ConsumeSharedModule instances with fallback dependencies
              if (
                !(module instanceof ConsumeSharedModule) ||
                !module.options.import
              ) {
                continue;
              }

Thanks for pushing. I will pull and test everything E2E and report back.

}
callback();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
it('should be able to consume nested modules', async () => {
const { default: main } = await import('package-1');
expect(main('test')).toEqual('test package-1 package-2');
});

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,7 @@
{
"version": "1.0.0",
"dependencies": {
"package-2": "1.0.0",
"package-1": "1.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const { ModuleFederationPlugin } = require('../../../../dist/src');

module.exports = {
mode: 'development',
devtool: false,
plugins: [
new ModuleFederationPlugin({
name: 'consume-nested',
filename: 'remoteEntry.js',
shared: {
'package-2': { version: '1.0.0' },
'package-1': { version: '1.0.0' },
},
}),
],
};
108 changes: 105 additions & 3 deletions packages/enhanced/test/unit/sharing/ConsumeSharedModule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,17 @@ describe('ConsumeSharedModule', () => {

describe('build', () => {
it('should add fallback dependency when import exists and eager=true', () => {
const mockCompilation = {
hooks: {
finishModules: {
tap: jest.fn(),
},
},
moduleGraph: {
getModule: jest.fn(),
},
};

const module = new ConsumeSharedModule('/context', {
...testModuleOptions.eager,
import: './react',
Expand All @@ -274,13 +285,30 @@ describe('ConsumeSharedModule', () => {
function buildCallback() {
// Empty callback needed for the build method
}
module.build({} as any, {} as any, {} as any, {} as any, buildCallback);
module.build(
{} as any,
mockCompilation as any,
{} as any,
{} as any,
buildCallback,
);

expect(module.dependencies.length).toBe(1);
expect(module.blocks.length).toBe(0);
});

it('should add fallback in async block when import exists and eager=false', () => {
const mockCompilation = {
hooks: {
finishModules: {
tap: jest.fn(),
},
},
moduleGraph: {
getModule: jest.fn(),
},
};

const module = new ConsumeSharedModule('/context', {
...testModuleOptions.basic,
import: './react',
Expand All @@ -290,13 +318,30 @@ describe('ConsumeSharedModule', () => {
function buildCallback() {
// Empty callback needed for the build method
}
module.build({} as any, {} as any, {} as any, {} as any, buildCallback);
module.build(
{} as any,
mockCompilation as any,
{} as any,
{} as any,
buildCallback,
);

expect(module.dependencies.length).toBe(0);
expect(module.blocks.length).toBe(1);
});

it('should not add fallback when import does not exist', () => {
const mockCompilation = {
hooks: {
finishModules: {
tap: jest.fn(),
},
},
moduleGraph: {
getModule: jest.fn(),
},
};

const module = new ConsumeSharedModule('/context', {
...testModuleOptions.basic,
import: undefined,
Expand All @@ -306,11 +351,68 @@ describe('ConsumeSharedModule', () => {
function buildCallback() {
// Empty callback needed for the build method
}
module.build({} as any, {} as any, {} as any, {} as any, buildCallback);
module.build(
{} as any,
mockCompilation as any,
{} as any,
{} as any,
buildCallback,
);

expect(module.dependencies.length).toBe(0);
expect(module.blocks.length).toBe(0);
});

it('should copy buildMeta and buildInfo from fallback', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since I had to update a few tests on this suite, I'm also adding this unit one to cover the new logic.

const mockFinishModulesHook = {
tap: jest.fn(),
};

const mockFallbackModule = {
buildMeta: { testMeta: 'value' },
buildInfo: { testInfo: 'data' },
};

const mockCompilation = {
hooks: {
finishModules: mockFinishModulesHook,
},
moduleGraph: {
getModule: jest.fn().mockReturnValue(mockFallbackModule),
},
};

const module = new ConsumeSharedModule('/context', {
...testModuleOptions.eager,
import: './react',
} as any as ConsumeOptions);

// Named callback function to satisfy linter
function buildCallback() {
// Empty callback needed for the build method
}
module.build(
{} as any,
mockCompilation as any,
{} as any,
{} as any,
buildCallback,
);

// Verify the hook was tapped
expect(mockFinishModulesHook.tap).toHaveBeenCalledWith(
'ConsumeSharedModule',
expect.any(Function),
);

// Simulate the hook being called
const hookCallback = mockFinishModulesHook.tap.mock.calls[0][1];
hookCallback();

// Verify buildMeta and buildInfo were copied
expect(module.buildMeta).toEqual({ testMeta: 'value' });
expect(module.buildInfo).toEqual({ testInfo: 'data' });
});
});

describe('codeGeneration', () => {
Expand Down