Skip to content

Commit 1478f50

Browse files
fix(nextjs-mf): improve hot reloading (#3001)
1 parent 3d6b72c commit 1478f50

File tree

14 files changed

+257
-163
lines changed

14 files changed

+257
-163
lines changed

.changeset/ai-calm-cat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@module-federation/nextjs-mf": patch
3+
---
4+
- Added `globalThis.moduleGraphDirty = true` to mark the module graph as dirty when an error is detected.
5+
- Replaced `new Function('return globalThis')()` with a direct reference to `globalThis`.

.changeset/ai-eager-wolf.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@module-federation/node": patch
3+
---
4+
5+
Add global flag `moduleGraphDirty` to control forced revalidation in hot-reload.
6+
7+
- Introduced new global variable `moduleGraphDirty`.
8+
- Initialized `moduleGraphDirty` to `false` in the global scope.
9+
- Modified `revalidate` function to check `moduleGraphDirty` flag.
10+
- Forces revalidation if `moduleGraphDirty` is `true`.

.changeset/ai-noisy-lion.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@module-federation/node": minor
3+
---
4+
5+
Enhanced hot-reload functionality with module decaching and improved type safety.
6+
7+
- Added `callsite` package for resolving module paths.
8+
- Implemented `decache` and `searchCache` functions to remove modules from cache safely.
9+
- Ensure proper handling of relative module paths.
10+
- Avoid issues with native modules during decaching.
11+
- Refactored hot-reload logic to use the new decache functionality.
12+
- Improved type definitions and type safety throughout `hot-reload.ts`.
13+
- Properly typed function return values.
14+
- Added TypeScript annotations for better clarity.

.changeset/ai-noisy-owl.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@module-federation/nextjs-mf": minor
3+
---
4+
5+
Added the UniverseEntryChunkTrackerPlugin to track entry chunks in the server plugin.
6+
7+
- Applied UniverseEntryChunkTrackerPlugin in the applyServerPlugins function.
8+
- This change aims to enhance tracking of entry chunks in the server environment for hot reloading prod instances

.github/workflows/build-and-test.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,18 @@ jobs:
4343
run: npx nx run-many --targets=build --projects=tag:type:pkg --skip-nx-cache
4444

4545
- name: Run Affected Test
46-
run: npx nx affected -t test --parallel=2 --exclude='*,!tag:type:pkg' --skip-nx-cache
46+
uses: nick-fields/retry@v3
47+
with:
48+
max_attempts: 2
49+
timeout_minutes: 10
50+
command: npx nx affected -t test --parallel=3 --exclude='*,!tag:type:pkg' --skip-nx-cache
4751

4852
- name: Run Affected Experimental Tests
49-
run: npx nx affected -t test:experiments --parallel=2 --exclude='*,!tag:type:pkg' --skip-nx-cache
53+
uses: nick-fields/retry@v3
54+
with:
55+
max_attempts: 2
56+
timeout_minutes: 10
57+
command: npx nx affected -t test:experiments --parallel=1 --exclude='*,!tag:type:pkg' --skip-nx-cache
5058

5159
e2e-modern:
5260
needs: checkout-install

packages/enhanced/test/ConfigTestCases.basictest.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
if (globalThis.__FEDERATION__) {
2+
globalThis.__GLOBAL_LOADING_REMOTE_ENTRY__ = {};
3+
//@ts-ignore
4+
globalThis.__FEDERATION__.__INSTANCES__.map((i) => {
5+
i.moduleCache.clear();
6+
if (globalThis[i.name]) {
7+
delete globalThis[i.name];
8+
}
9+
});
10+
globalThis.__FEDERATION__.__INSTANCES__ = [];
11+
}
112
const { describeCases } = require('./ConfigTestCases.template');
213

314
describeCases({

packages/enhanced/test/ConfigTestCases.embedruntime.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
if (globalThis.__FEDERATION__) {
2+
globalThis.__GLOBAL_LOADING_REMOTE_ENTRY__ = {};
3+
//@ts-ignore
4+
globalThis.__FEDERATION__.__INSTANCES__.map((i) => {
5+
i.moduleCache.clear();
6+
if (globalThis[i.name]) {
7+
delete globalThis[i.name];
8+
}
9+
});
10+
globalThis.__FEDERATION__.__INSTANCES__ = [];
11+
}
112
const { describeCases } = require('./ConfigTestCases.template');
213
jest.resetModules();
314
describeCases({

packages/nextjs-mf/src/plugins/NextFederationPlugin/apply-server-plugins.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
import type { moduleFederationPlugin } from '@module-federation/sdk';
77
import path from 'path';
88
import InvertedContainerPlugin from '../container/InvertedContainerPlugin';
9+
import UniverseEntryChunkTrackerPlugin from '@module-federation/node/universe-entry-chunk-tracker-plugin';
910

1011
type EntryStaticNormalized = Awaited<
1112
ReturnType<Extract<WebpackOptionsNormalized['entry'], () => any>>
@@ -74,7 +75,7 @@ export function applyServerPlugins(
7475
suffix,
7576
);
7677
}
77-
78+
new UniverseEntryChunkTrackerPlugin().apply(compiler);
7879
new InvertedContainerPlugin().apply(compiler);
7980
}
8081

packages/nextjs-mf/src/plugins/container/runtimePlugin.ts

Lines changed: 38 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@ export default function (): FederationRuntimePlugin {
77
url: string;
88
attrs?: Record<string, any>;
99
}) {
10-
// Updated type
11-
var url = args.url;
12-
var attrs = args.attrs;
10+
const url = args.url;
11+
const attrs = args.attrs;
1312
if (typeof window !== 'undefined') {
14-
var script = document.createElement('script');
13+
const script = document.createElement('script');
1514
script.src = url;
1615
script.async = true;
1716
delete attrs?.['crossorigin'];
@@ -26,20 +25,21 @@ export default function (): FederationRuntimePlugin {
2625
from: string;
2726
origin: any;
2827
}) {
29-
var id = args.id;
30-
var error = args.error;
31-
var from = args.from;
28+
const id = args.id;
29+
const error = args.error;
30+
const from = args.from;
31+
//@ts-ignore
32+
globalThis.moduleGraphDirty = true;
3233
console.error(id, 'offline');
33-
var pg = function () {
34+
const pg = function () {
3435
console.error(id, 'offline', error);
3536
return null;
3637
};
3738

3839
(pg as any).getInitialProps = function (ctx: any) {
39-
// Type assertion to add getInitialProps
4040
return {};
4141
};
42-
var mod;
42+
let mod;
4343
if (from === 'build') {
4444
mod = function () {
4545
return {
@@ -70,15 +70,16 @@ export default function (): FederationRuntimePlugin {
7070
return args;
7171
}
7272

73-
var moduleCache = args.origin.moduleCache;
74-
var name = args.origin.name;
75-
var gs;
73+
const moduleCache = args.origin.moduleCache;
74+
const name = args.origin.name;
75+
let gs;
7676
try {
7777
gs = new Function('return globalThis')();
7878
} catch (e) {
7979
gs = globalThis; // fallback for browsers without 'unsafe-eval' CSP policy enabled
8080
}
81-
var attachedRemote = gs[name];
81+
//@ts-ignore
82+
const attachedRemote = gs[name];
8283
if (attachedRemote) {
8384
moduleCache.set(name, attachedRemote);
8485
}
@@ -89,10 +90,10 @@ export default function (): FederationRuntimePlugin {
8990
return args;
9091
},
9192
beforeRequest: function (args: any) {
92-
var options = args.options;
93-
var id = args.id;
94-
var remoteName = id.split('/').shift();
95-
var remote = options.remotes.find(function (remote: any) {
93+
const options = args.options;
94+
const id = args.id;
95+
const remoteName = id.split('/').shift();
96+
const remote = options.remotes.find(function (remote: any) {
9697
return remote.name === remoteName;
9798
});
9899
if (!remote) return args;
@@ -106,41 +107,41 @@ export default function (): FederationRuntimePlugin {
106107
return args;
107108
},
108109
onLoad: function (args: any) {
109-
var exposeModuleFactory = args.exposeModuleFactory;
110-
var exposeModule = args.exposeModule;
111-
var id = args.id;
112-
var moduleOrFactory = exposeModuleFactory || exposeModule;
113-
if (!moduleOrFactory) return args; // Ensure moduleOrFactory is defined
110+
const exposeModuleFactory = args.exposeModuleFactory;
111+
const exposeModule = args.exposeModule;
112+
const id = args.id;
113+
const moduleOrFactory = exposeModuleFactory || exposeModule;
114+
if (!moduleOrFactory) return args;
114115

115116
if (typeof window === 'undefined') {
116-
var exposedModuleExports: any;
117+
let exposedModuleExports: any;
117118
try {
118119
exposedModuleExports = moduleOrFactory();
119120
} catch (e) {
120121
exposedModuleExports = moduleOrFactory;
121122
}
122123

123-
var handler: ProxyHandler<any> = {
124+
const handler: ProxyHandler<any> = {
124125
get: function (target, prop, receiver) {
125-
// Check if accessing a static property of the function itself
126126
if (
127127
target === exposedModuleExports &&
128128
typeof exposedModuleExports[prop] === 'function'
129129
) {
130130
return function (this: unknown) {
131131
globalThis.usedChunks.add(id);
132+
//eslint-disable-next-line
132133
return exposedModuleExports[prop].apply(this, arguments);
133134
};
134135
}
135136

136-
var originalMethod = target[prop];
137+
const originalMethod = target[prop];
137138
if (typeof originalMethod === 'function') {
138-
var proxiedFunction = function (this: unknown) {
139+
const proxiedFunction = function (this: unknown) {
139140
globalThis.usedChunks.add(id);
141+
//eslint-disable-next-line
140142
return originalMethod.apply(this, arguments);
141143
};
142144

143-
// Copy all enumerable properties from the original method to the proxied function
144145
Object.keys(originalMethod).forEach(function (prop) {
145146
Object.defineProperty(proxiedFunction, prop, {
146147
value: originalMethod[prop],
@@ -158,12 +159,9 @@ export default function (): FederationRuntimePlugin {
158159
};
159160

160161
if (typeof exposedModuleExports === 'function') {
161-
// If the module export is a function, we create a proxy that can handle both its
162-
// call (as a function) and access to its properties (including static methods).
163162
exposedModuleExports = new Proxy(exposedModuleExports, handler);
164163

165-
// Proxy static properties specifically
166-
var staticProps = Object.getOwnPropertyNames(exposedModuleExports);
164+
const staticProps = Object.getOwnPropertyNames(exposedModuleExports);
167165
staticProps.forEach(function (prop) {
168166
if (typeof exposedModuleExports[prop] === 'function') {
169167
exposedModuleExports[prop] = new Proxy(
@@ -176,7 +174,6 @@ export default function (): FederationRuntimePlugin {
176174
return exposedModuleExports;
177175
};
178176
} else {
179-
// For objects, just wrap the exported object itself
180177
exposedModuleExports = new Proxy(exposedModuleExports, handler);
181178
}
182179

@@ -194,23 +191,22 @@ export default function (): FederationRuntimePlugin {
194191
) {
195192
return args;
196193
}
197-
var shareScopeMap = args.shareScopeMap;
198-
var scope = args.scope;
199-
var pkgName = args.pkgName;
200-
var version = args.version;
201-
var GlobalFederation = args.GlobalFederation;
202-
var host = GlobalFederation['__INSTANCES__'][0];
194+
const shareScopeMap = args.shareScopeMap;
195+
const scope = args.scope;
196+
const pkgName = args.pkgName;
197+
const version = args.version;
198+
const GlobalFederation = args.GlobalFederation;
199+
const host = GlobalFederation['__INSTANCES__'][0];
203200
if (!host) {
204201
return args;
205202
}
206203

207204
if (!host.options.shared[pkgName]) {
208205
return args;
209206
}
210-
//handle react host next remote, disable resolving when not next host
211207
args.resolver = function () {
212208
shareScopeMap[scope][pkgName][version] =
213-
host.options.shared[pkgName][0]; // replace local share scope manually with desired module
209+
host.options.shared[pkgName][0];
214210
return shareScopeMap[scope][pkgName][version];
215211
};
216212
return args;

packages/node/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export { default as StreamingTargetPlugin } from './plugins/StreamingTargetPlugin';
22
export { default as NodeFederationPlugin } from './plugins/NodeFederationPlugin';
33
export { default as UniversalFederationPlugin } from './plugins/UniversalFederationPlugin';
4+
//@ts-ignore
45
export { default as ChunkCorrelationPlugin } from './plugins/ChunkCorrelationPlugin';
56
export { default as RemotePublicPathPlugin } from './plugins/RemotePublicPathRuntimeModule';
67
export { default as EntryChunkTrackerPlugin } from './plugins/EntryChunkTrackerPlugin';

0 commit comments

Comments
 (0)