Skip to content

Commit 73eb07e

Browse files
fix(node): Improve chunk flushing (#1985)
1 parent 8972910 commit 73eb07e

File tree

14 files changed

+165
-111
lines changed

14 files changed

+165
-111
lines changed

.changeset/popular-spoons-grin.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@module-federation/nextjs-mf': patch
3+
'@module-federation/node': patch
4+
---
5+
6+
Rewrite chunk flushing and hot reloading to use federation runtime apis

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,4 @@ jobs:
6666
run: lsof -ti tcp:3005,3006,3007 | xargs kill
6767

6868
- name: Build Next.js Apps in Production Mode
69-
run: pnpm app:next:prod
69+
run: pnpm app:next:build

apps/3000-home/project.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
"executor": "@nx/next:build",
99
"defaultConfiguration": "production",
1010
"options": {
11-
"outputPath": "dist/apps/3000-home"
11+
"outputPath": "apps/3000-home"
1212
},
1313
"configurations": {
1414
"development": {
15-
"outputPath": "dist/apps/3000-home"
15+
"outputPath": "apps/3000-home"
1616
},
1717
"production": {}
1818
},

apps/3001-shop/pages/_document.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,20 @@ import {
88

99
class MyDocument extends Document {
1010
static async getInitialProps(ctx) {
11+
await revalidate().then((shouldUpdate) => {
12+
if (shouldUpdate) {
13+
ctx.res.writeHead(307, { Location: ctx.req.url });
14+
ctx.res.end();
15+
}
16+
});
1117
const initialProps = await Document.getInitialProps(ctx);
1218
const chunks = await flushChunks();
1319
ctx?.res?.on('finish', () => {
14-
revalidate().then((shouldUpdate) => {
15-
if (shouldUpdate) {
16-
console.log('should HMR', shouldUpdate);
17-
}
18-
});
20+
// revalidate().then((shouldUpdate) => {
21+
// if (shouldUpdate) {
22+
// console.log('should HMR', shouldUpdate);
23+
// }
24+
// });
1925
});
2026

2127
return {

apps/3001-shop/project.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
"executor": "@nx/next:build",
99
"defaultConfiguration": "production",
1010
"options": {
11-
"outputPath": "apps/3001-shop/dist"
11+
"outputPath": "apps/3001-shop"
1212
},
1313
"configurations": {
1414
"development": {
15-
"outputPath": "apps/3001-shop/dist"
15+
"outputPath": "apps/3001-shop"
1616
},
1717
"production": {}
1818
},

apps/3002-checkout/project.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
"executor": "@nx/next:build",
99
"defaultConfiguration": "production",
1010
"options": {
11-
"outputPath": "{options.outputPath}"
11+
"outputPath": "apps/3002-checkout"
1212
},
1313
"configurations": {
1414
"development": {
15-
"outputPath": "apps/3002-checkout/dist"
15+
"outputPath": "apps/3002-checkout"
1616
},
1717
"production": {}
1818
},

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
"extract-i18n:website": "nx run website:extract-i18n",
3232
"sync:pullMFTypes": "concurrently \"node ./packages/enhanced/pullts.js\"",
3333
"app:next:dev": "nx run-many --target=serve --configuration=development -p 3000-home,3001-shop,3002-checkout",
34-
"app:next:prod": "nx run-many --target=build --configuration=production -p 3000-home,3001-shop,3002-checkout",
34+
"app:next:build": "nx run-many --target=build --configuration=production -p 3000-home,3001-shop,3002-checkout",
35+
"app:next:prod": "nx run-many --target=serve --configuration=production -p 3000-home,3001-shop,3002-checkout",
3536
"app:node:dev": "nx run-many --target=serve --configuration=development -p node-host,node-local-remote,node-remote",
3637
"app:runtime:dev": "nx run-many --target=serve -p 3005-runtime-host,3006-runtime-remote,3007-runtime-remote",
3738
"commitlint": "commitlint --edit",

packages/nextjs-mf/src/federation-noop.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ require('next/amp');
99
require('styled-jsx');
1010
require('styled-jsx/style');
1111
require('next/image');
12-
require('react/jsx-dev-runtime');
12+
// require('react/jsx-dev-runtime');
1313
require('react/jsx-runtime');

packages/nextjs-mf/src/plugins/NextFederationPlugin/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@ export class NextFederationPlugin {
6767
// ContainerPlugin will get NextFederationPlugin._options, so NextFederationPlugin._options should be the same as normalFederationPluginOptions
6868
this._options = normalFederationPluginOptions;
6969
new ModuleFederationPlugin(normalFederationPluginOptions).apply(compiler);
70+
71+
const runtimeESMPath = require.resolve(
72+
'@module-federation/runtime/dist/index.esm.js',
73+
);
74+
compiler.hooks.afterPlugins.tap('PatchAliasWebpackPlugin', () => {
75+
compiler.options.resolve.alias = {
76+
...compiler.options.resolve.alias,
77+
'@module-federation/runtime$': runtimeESMPath,
78+
};
79+
});
7080
}
7181

7282
private validateOptions(compiler: Compiler): boolean {

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

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default function (): FederationRuntimePlugin {
3232
beforeInit(args) {
3333
const { userOptions, shareInfo } = args;
3434
const { shared } = userOptions;
35-
35+
if (!globalThis.usedChunks) globalThis.usedChunks = new Set();
3636
if (shared) {
3737
Object.keys(shared || {}).forEach((sharedKey) => {
3838
if (!shared[sharedKey].strategy) {
@@ -50,7 +50,7 @@ export default function (): FederationRuntimePlugin {
5050

5151
// if (__webpack_runtime_id__ && !__webpack_runtime_id__.startsWith('webpack')) return args;
5252
const { moduleCache, name } = args.origin;
53-
const gs = (globalThis as any) || new Function('return globalThis')();
53+
const gs = new Function('return globalThis')();
5454
const attachedRemote = gs[name];
5555
if (attachedRemote) {
5656
moduleCache.set(name, attachedRemote);
@@ -70,9 +70,35 @@ export default function (): FederationRuntimePlugin {
7070
afterResolve(args) {
7171
return args;
7272
},
73-
// onLoad(args) {
74-
// return args;
75-
// },
73+
onLoad(args) {
74+
const { exposeModuleFactory, exposeModule, id } = args;
75+
76+
const moduleOrFactory = exposeModuleFactory || exposeModule;
77+
const exposedModuleExports = moduleOrFactory();
78+
const handler = {
79+
//@ts-ignore
80+
get: function (target, prop, receiver) {
81+
const origMethod = target[prop];
82+
if (typeof origMethod === 'function') {
83+
//@ts-ignore
84+
return function (...args) {
85+
globalThis.usedChunks.add(
86+
//@ts-ignore
87+
id,
88+
);
89+
90+
// console.log(`function as called to ${prop}`, id);
91+
//@ts-ignore
92+
return origMethod.apply(this, args);
93+
};
94+
} else {
95+
return Reflect.get(target, prop, receiver);
96+
}
97+
},
98+
};
99+
100+
return () => new Proxy(exposedModuleExports, handler);
101+
},
76102
resolveShare(args) {
77103
if (
78104
args.pkgName !== 'react' &&

packages/nextjs-mf/utils/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,16 @@ export type { FlushedChunksProps } from './flushedChunks';
3333
* If the function is called on the server side, it imports the revalidate function from the module federation node utilities and returns the result of calling that function.
3434
* @returns {Promise<boolean>} A promise that resolves with a boolean.
3535
*/
36-
export const revalidate = () => {
36+
export const revalidate = (
37+
fetchModule: any = undefined,
38+
force: boolean = false,
39+
) => {
3740
if (typeof window !== 'undefined') {
3841
console.error('revalidate should only be called server-side');
3942
return Promise.resolve(false);
4043
}
4144
// @ts-ignore
4245
return import('@module-federation/node/utils').then((utils) => {
43-
return utils.revalidate();
46+
return utils.revalidate(fetchModule, force);
4447
});
4548
};

packages/node/global.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ declare global {
3333
};
3434
}
3535
}
36+
var usedChunks: Set<string>;
3637

3738
var __FEDERATION__: {
3839
__INSTANCES__: Array<{

packages/node/src/utils/flush-chunks.ts

Lines changed: 61 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -91,104 +91,80 @@ const createShareMap = () => {
9191
*/
9292
// @ts-ignore
9393
const processChunk = async (chunk, shareMap, hostStats) => {
94+
const chunks = new Set();
95+
const [remote, req] = chunk.split('/');
96+
const request = './' + req;
97+
const knownRemotes = getAllKnownRemotes();
98+
//@ts-ignore
99+
if (!knownRemotes[remote]) {
100+
console.error(
101+
`flush chunks: Remote ${remote} is not defined in the global config`,
102+
);
103+
return;
104+
}
105+
94106
try {
95-
// Create a set to store the chunks
96-
const chunks = new Set();
107+
//@ts-ignore
108+
const remoteName = new URL(knownRemotes[remote].entry).pathname
109+
.split('/')
110+
.pop();
111+
//@ts-ignore
97112

98-
// Split the chunk string into remote and request
99-
const [remote, request] = chunk.split('->');
100-
const knownRemotes = getAllKnownRemotes();
113+
const statsFile = knownRemotes[remote].entry
114+
.replace(remoteName, 'federated-stats.json')
115+
.replace('ssr', 'chunks');
116+
let stats = {};
101117

102-
// If the remote is not defined in the global config, return
103-
//@ts-ignore
104-
if (!knownRemotes[remote]) {
105-
console.error(
106-
`flush chunks:`,
107-
`Remote ${remote} is not defined in the global config`,
108-
);
109-
return;
118+
try {
119+
stats = await fetch(statsFile).then((res) => res.json());
120+
} catch (e) {
121+
console.error('flush error', e);
110122
}
123+
//@ts-ignore
111124

112-
try {
113-
// Extract the remote name from the URL
114-
//@ts-ignore
115-
const remoteName = new URL(
116-
//@ts-ignore
117-
globalThis.__remote_scope__._config[remote],
118-
).pathname
119-
.split('/')
120-
.pop();
125+
const [prefix] = knownRemotes[remote].entry.split('static/');
126+
//@ts-ignore
121127

122-
// Construct the stats file URL from the remote config
128+
if (stats.federatedModules) {
123129
//@ts-ignore
124-
const statsFile = globalThis.__remote_scope__._config[remote]
125-
.replace(remoteName, 'federated-stats.json')
126-
.replace('ssr', 'chunks');
127-
128-
let stats = {};
129-
try {
130-
// Fetch the remote config and stats file
131-
stats = await fetch(statsFile).then((res) => res.json());
132-
} catch (e) {
133-
console.error('flush error', e);
134-
}
135130

136-
// Add the main chunk to the chunks set
137-
//TODO: ensure host doesnt embed its own remote in ssr, this causes crash
138-
// chunks.add(
139-
// global.__remote_scope__._config[remote].replace('ssr', 'chunks')
140-
// );
131+
stats.federatedModules.forEach((modules) => {
132+
if (modules.exposes?.[request]) {
133+
//@ts-ignore
141134

142-
// Extract the prefix from the remote config
143-
const [prefix] =
144-
//@ts-ignore
145-
globalThis.__remote_scope__._config[remote].split('static/');
135+
modules.exposes[request].forEach((chunk) => {
136+
chunks.add([prefix, chunk].join(''));
146137

147-
// Process federated modules from the stats object
148-
// @ts-ignore
149-
if (stats.federatedModules) {
150-
// @ts-ignore
151-
stats.federatedModules.forEach((modules) => {
152-
// Process exposed modules
153-
if (modules.exposes?.[request]) {
154-
// @ts-ignore
155-
modules.exposes[request].forEach((chunk) => {
156-
chunks.add([prefix, chunk].join(''));
157-
158-
//TODO: reimplement this
159-
Object.values(chunk).forEach((chunk) => {
160-
// Add files to the chunks set
161-
// @ts-ignore
162-
if (chunk.files) {
163-
// @ts-ignore
164-
chunk.files.forEach((file) => {
165-
chunks.add(prefix + file);
166-
});
167-
}
168-
// Process required modules
169-
// @ts-ignore
170-
if (chunk.requiredModules) {
171-
// @ts-ignore
172-
chunk.requiredModules.forEach((module) => {
173-
// Check if the module is in the shareMap
174-
if (shareMap[module]) {
175-
// If the module is from the host, log the host stats
176-
}
177-
});
178-
}
179-
});
180-
});
181-
}
182-
});
183-
}
138+
Object.values(chunk).forEach((chunk) => {
139+
//@ts-ignore
184140

185-
// Return the array of chunks
186-
return Array.from(chunks);
187-
} catch (e) {
188-
console.error('flush error:', e);
141+
if (chunk.files) {
142+
//@ts-ignore
143+
144+
chunk.files.forEach((file) => {
145+
chunks.add(prefix + file);
146+
});
147+
}
148+
//@ts-ignore
149+
150+
if (chunk.requiredModules) {
151+
//@ts-ignore
152+
153+
chunk.requiredModules.forEach((module) => {
154+
if (shareMap[module]) {
155+
// If the module is from the host, log the host stats
156+
}
157+
});
158+
}
159+
});
160+
});
161+
}
162+
});
189163
}
164+
165+
return Array.from(chunks);
190166
} catch (e) {
191-
// catch just in case
167+
console.error('flush error:', e);
192168
}
193169
};
194170

0 commit comments

Comments
 (0)