Skip to content
Open
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/worker-federation-runtime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@module-federation/enhanced': patch
---

Workers created with `new Worker(new URL(...))` and other async entrypoints now get the federation runtime entry in their own runtime chunk, so a remote or shared module loaded from a worker no longer throws on `bundlerRuntime`. Modules hoisted into runtime chunks are only removed from other chunks when every runtime of that chunk received them.
136 changes: 48 additions & 88 deletions packages/enhanced/src/lib/container/HoistContainerReferencesPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,114 +85,74 @@ class HoistContainerReferences implements WebpackPluginInstance {
remoteDependencies: Set<Dependency>,
): void {
const { chunkGraph, moduleGraph } = compilation;
const { forEachRuntime } = compilation.compiler.webpack.util.runtime;
const allModulesToHoist = new Set<Module>();

// Process container entry dependencies (needed for nextjs-mf exposed modules)
for (const dep of containerEntryDependencies) {
const containerEntryModule = moduleGraph.getModule(dep);
if (!containerEntryModule) continue;
const referencedModules = getAllReferencedModules(
compilation,
containerEntryModule,
'initial',
);
referencedModules.forEach((m: Module) => allModulesToHoist.add(m));
const moduleRuntimes = chunkGraph.getModuleRuntimes(containerEntryModule);
const runtimes = new Set<string>();
for (const runtimeSpec of moduleRuntimes) {
compilation.compiler.webpack.util.runtime.forEachRuntime(
runtimeSpec,
(runtimeKey) => {
if (runtimeKey) {
runtimes.add(runtimeKey);
}
},
);
}
for (const runtime of runtimes) {
const runtimeChunk = compilation.namedChunks.get(runtime);
if (!runtimeChunk) continue;
for (const module of referencedModules) {
if (!chunkGraph.isModuleInChunk(module, runtimeChunk)) {
chunkGraph.connectChunkAndModule(runtimeChunk, module);
}
}
}
const runtimeChunkByKey = new Map<string, Chunk>();
for (const chunk of runtimeChunks) {
forEachRuntime(chunk.runtime, (runtimeKey) => {
if (runtimeKey) runtimeChunkByKey.set(runtimeKey, chunk);
});
}

// Federation Runtime Dependencies: use 'initial' (not 'all')
for (const dep of federationRuntimeDependencies) {
const runtimeModule = moduleGraph.getModule(dep);
if (!runtimeModule) continue;
const runtimeChunksOf = (module: Module): Set<Chunk> => {
const chunks = new Set<Chunk>();
for (const runtimeSpec of chunkGraph.getModuleRuntimes(module)) {
forEachRuntime(runtimeSpec, (runtimeKey) => {
const runtimeChunk = runtimeKey && runtimeChunkByKey.get(runtimeKey);
if (runtimeChunk) chunks.add(runtimeChunk);
});
}
return chunks;
};

for (const dep of [
...containerEntryDependencies,
...federationRuntimeDependencies,
...remoteDependencies,
]) {
const module = moduleGraph.getModule(dep);
if (!module) continue;
const referencedModules = getAllReferencedModules(
compilation,
runtimeModule,
module,
'initial',
);
referencedModules.forEach((m: Module) => allModulesToHoist.add(m));
const moduleRuntimes = chunkGraph.getModuleRuntimes(runtimeModule);
const runtimes = new Set<string>();
for (const runtimeSpec of moduleRuntimes) {
compilation.compiler.webpack.util.runtime.forEachRuntime(
runtimeSpec,
(runtimeKey) => {
if (runtimeKey) {
runtimes.add(runtimeKey);
}
},
);
}
for (const runtime of runtimes) {
const runtimeChunk = compilation.namedChunks.get(runtime);
if (!runtimeChunk) continue;
for (const module of referencedModules) {
if (!chunkGraph.isModuleInChunk(module, runtimeChunk)) {
chunkGraph.connectChunkAndModule(runtimeChunk, module);
for (const runtimeChunk of runtimeChunksOf(module)) {
for (const referenced of referencedModules) {
if (!chunkGraph.isModuleInChunk(referenced, runtimeChunk)) {
chunkGraph.connectChunkAndModule(runtimeChunk, referenced);
}
}
}
}

// Process remote dependencies
for (const remoteDep of remoteDependencies) {
const remoteModule = moduleGraph.getModule(remoteDep);
if (!remoteModule) continue;
const referencedRemoteModules = getAllReferencedModules(
compilation,
remoteModule,
'initial',
);
referencedRemoteModules.forEach((m: Module) => allModulesToHoist.add(m));
const remoteModuleRuntimes = chunkGraph.getModuleRuntimes(remoteModule);
const remoteRuntimes = new Set<string>();
for (const runtimeSpec of remoteModuleRuntimes) {
compilation.compiler.webpack.util.runtime.forEachRuntime(
runtimeSpec,
(runtimeKey) => {
if (runtimeKey) remoteRuntimes.add(runtimeKey);
},
);
}
for (const runtime of remoteRuntimes) {
const runtimeChunk = compilation.namedChunks.get(runtime);
if (!runtimeChunk) continue;
for (const module of referencedRemoteModules) {
if (!chunkGraph.isModuleInChunk(module, runtimeChunk)) {
chunkGraph.connectChunkAndModule(runtimeChunk, module);
}
}
}
}

this.cleanUpChunks(compilation, allModulesToHoist);
this.cleanUpChunks(compilation, allModulesToHoist, runtimeChunkByKey);
}

// Method to clean up chunks by disconnecting unused modules
private cleanUpChunks(compilation: Compilation, modules: Set<Module>): void {
private cleanUpChunks(
compilation: Compilation,
modules: Set<Module>,
runtimeChunkByKey: Map<string, Chunk>,
): void {
const { chunkGraph } = compilation;
const { forEachRuntime } = compilation.compiler.webpack.util.runtime;
for (const module of modules) {
for (const chunk of chunkGraph.getModuleChunks(module)) {
if (!chunk.hasRuntime()) {
if (chunk.hasRuntime()) continue;
let hoistedToAllRuntimes = true;
forEachRuntime(chunk.runtime, (runtimeKey) => {
const runtimeChunk = runtimeKey && runtimeChunkByKey.get(runtimeKey);
if (
!runtimeChunk ||
!chunkGraph.isModuleInChunk(module, runtimeChunk)
) {
hoistedToAllRuntimes = false;
}
});
if (hoistedToAllRuntimes) {
chunkGraph.disconnectChunkAndModule(chunk, module);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path';

const ModuleDependency = require(
normalizeWebpackPath('webpack/lib/dependencies/ModuleDependency'),
) as typeof import('webpack/lib/dependencies/ModuleDependency');
const NullDependency = require(
normalizeWebpackPath('webpack/lib/dependencies/NullDependency'),
) as typeof import('webpack/lib/dependencies/NullDependency');
const makeSerializable = require(
normalizeWebpackPath('webpack/lib/util/makeSerializable'),
) as typeof import('webpack/lib/util/makeSerializable');

class AsyncEntrypointRuntimeDependency extends ModuleDependency {
Comment thread
ScriptedAlchemy marked this conversation as resolved.
static override Template = NullDependency.Template;

override get type() {
return 'federation runtime async entrypoint dependency';
}
}

makeSerializable(
AsyncEntrypointRuntimeDependency,
'enhanced/lib/container/runtime/AsyncEntrypointRuntimeDependency',
);

export default AsyncEntrypointRuntimeDependency;
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,11 @@ class EmbedFederationRuntimeModule extends RuntimeModule {
return null;
}
let found;
if (chunk.name) {
for (const dep of this.containerEntrySet) {
const mod = compilation.moduleGraph.getModule(dep);
if (mod && compilation.chunkGraph.isModuleInChunk(mod, chunk)) {
found = mod as NormalModuleType;
break;
}
for (const dep of this.containerEntrySet) {
const mod = compilation.moduleGraph.getModule(dep);
if (mod && compilation.chunkGraph.isModuleInChunk(mod, chunk)) {
found = mod as NormalModuleType;
break;
}
}
if (!found) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
WebpackPluginInstance,
Compilation,
Chunk,
javascript,
} from 'webpack';
import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path';
import { moduleFederationPlugin } from '@module-federation/sdk';
Expand All @@ -16,10 +17,16 @@ import {
normalizeToPosixPath,
} from './utils';
import { TEMP_DIR } from '../constant';
import {
JAVASCRIPT_MODULE_TYPE_AUTO,
JAVASCRIPT_MODULE_TYPE_DYNAMIC,
JAVASCRIPT_MODULE_TYPE_ESM,
} from '../../Constants';
import EmbedFederationRuntimePlugin from './EmbedFederationRuntimePlugin';
import FederationModulesPlugin from './FederationModulesPlugin';
import HoistContainerReferences from '../HoistContainerReferencesPlugin';
import FederationRuntimeDependency from './FederationRuntimeDependency';
import AsyncEntrypointRuntimeDependency from './AsyncEntrypointRuntimeDependency';

const ModuleDependency = require(
normalizeWebpackPath('webpack/lib/dependencies/ModuleDependency'),
Expand Down Expand Up @@ -352,6 +359,40 @@ class FederationRuntimePlugin {
FederationRuntimeDependency,
new ModuleDependency.Template(),
);
compilation.dependencyFactories.set(
AsyncEntrypointRuntimeDependency,
normalModuleFactory,
);
compilation.dependencyTemplates.set(
AsyncEntrypointRuntimeDependency,
new AsyncEntrypointRuntimeDependency.Template(),
);

// addInclude only reaches static entrypoints.
const addEntryToAsyncEntrypoints = (
parser: javascript.JavascriptParser,
) => {
parser.hooks.finish.tap(this.constructor.name, () => {
for (const block of parser.state.module.blocks) {
if (block.groupOptions?.entryOptions) {
block.addDependency(
new AsyncEntrypointRuntimeDependency(
this.getDependency(compiler).request,
),
);
}
}
});
};
for (const type of [
JAVASCRIPT_MODULE_TYPE_AUTO,
JAVASCRIPT_MODULE_TYPE_DYNAMIC,
JAVASCRIPT_MODULE_TYPE_ESM,
]) {
normalModuleFactory.hooks.parser
.for(type)
.tap(this.constructor.name, addEntryToAsyncEntrypoints);
}
},
);
compiler.hooks.make.tapAsync(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { label } from './helper.js';

export function init() {}

export function get(request) {
return Promise.resolve(() => `${label} ${request}`);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const label = 'remote';
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const { fileURLToPath } = __non_webpack_require__('url');
const { basename } = __non_webpack_require__('path');

it('should initialize the federation runtime in a worker chunk', async () => {
const worker = new Worker(new URL('./worker.js', import.meta.url));
__non_webpack_require__(`./${basename(fileURLToPath(worker.url))}`);
expect(await globalThis.workerRemote).toBe('remote ./x');
});

it('should keep a hoisted module in a chunk whose runtime did not receive it', async () => {
__non_webpack_require__('./other.js');
expect(await globalThis.otherHelper).toBe('remote');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
globalThis.otherHelper = import('./helper.js').then((m) => m.label);
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = {
findBundle() {
return './main.js';
},
moduleScope(scope) {
scope.Worker = class Worker {
constructor(url) {
this.url = url;
}
};
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const { ModuleFederationPlugin } = require('../../../../dist/src');

/** @type {import("../../../../").Configuration} */
module.exports = {
entry: {
main: './index.js',
other: './other.js',
},
output: {
filename: '[name].js',
},
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
remote: 'internal ./container.js',
},
}),
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
globalThis.workerRemote = import('remote/x').then((m) => m.default);
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path';
import AsyncEntrypointRuntimeDependency from '../../../../src/lib/container/runtime/AsyncEntrypointRuntimeDependency';

const { buffersSerializer } = require(
normalizeWebpackPath('webpack/lib/util/serialization'),
) as typeof import('webpack/lib/util/serialization');

describe('AsyncEntrypointRuntimeDependency', () => {
it('round-trips through the webpack object serializer', async () => {
const dep = new AsyncEntrypointRuntimeDependency('./federation-entry.js');
dep.loc = { name: 'worker' };

const data = await buffersSerializer.serialize(dep, {});
const restored = await buffersSerializer.deserialize(data, {});

expect(restored).toBeInstanceOf(AsyncEntrypointRuntimeDependency);
expect(restored.request).toBe('./federation-entry.js');
expect(restored.type).toBe(
'federation runtime async entrypoint dependency',
);
});
});
Loading