Skip to content

Commit 7d46c22

Browse files
authored
Merge branch 'main' into codex/fix-modernjs-splitchunks-warning
2 parents c454fc3 + 9958086 commit 7d46c22

5 files changed

Lines changed: 394 additions & 21 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@module-federation/sdk": patch
3+
---
4+
5+
Handle Node.js built-in imports in the Node ESM remote loader.

apps/router-demo/router-host-2000/cypress/e2e/memory-router.cy.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ describe('router-host-2000/memory-router', () => {
1515
});
1616

1717
describe('memory-router', () => {
18-
Cypress.on('uncaught:exception', () => false);
19-
2018
it('remote1', () => {
2119
cy.verifyContent('Remote1 home page');
2220
cy.clickByClass('.self-remote1-detail-link');
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { jest } from '@jest/globals';
2+
import { sep as pathSeparator } from 'node:path';
3+
import { pathToFileURL } from 'node:url';
4+
5+
const DEFAULT_REMOTE_ENTRY_URL = 'http://example.com/remoteEntry.js';
6+
7+
const createResponse = (body: string) => ({
8+
text: async () => body,
9+
});
10+
11+
const moduleSource = (source: TemplateStringsArray, ...values: string[]) =>
12+
String.raw(source, ...values).trim();
13+
14+
const setFetchMock = (
15+
handler: (url: string) => ReturnType<typeof createResponse>,
16+
) => {
17+
const fetchMock = jest.fn(async (url: string) => handler(url));
18+
globalThis.fetch = fetchMock as unknown as typeof fetch;
19+
20+
return fetchMock;
21+
};
22+
23+
const setRemoteEntryFetchMock = (remoteEntryUrl: string, body: string) =>
24+
setFetchMock((url) => {
25+
if (url !== remoteEntryUrl) {
26+
throw new Error(`${url} should not be fetched`);
27+
}
28+
29+
return createResponse(body);
30+
});
31+
32+
const loadNodeEsmScript = async <T = unknown>(
33+
url = DEFAULT_REMOTE_ENTRY_URL,
34+
): Promise<T> => {
35+
const { createScriptNode } = await import('../src/node');
36+
37+
return new Promise<T>((resolve, reject) => {
38+
createScriptNode(
39+
url,
40+
(error, scriptContext) => {
41+
if (error) {
42+
reject(error);
43+
return;
44+
}
45+
46+
resolve(scriptContext as T);
47+
},
48+
{ type: 'module' },
49+
);
50+
});
51+
};
52+
53+
describe('Node ESM builtin loading', () => {
54+
const originalFetch = globalThis.fetch;
55+
56+
beforeEach(() => {
57+
jest.resetModules();
58+
jest.clearAllMocks();
59+
});
60+
61+
afterEach(() => {
62+
globalThis.fetch = originalFetch;
63+
});
64+
65+
it('loads node: builtin imports without fetching them as remote chunks', async () => {
66+
const fetchMock = setRemoteEntryFetchMock(
67+
DEFAULT_REMOTE_ENTRY_URL,
68+
moduleSource`
69+
import { pathToFileURL } from 'node:url';
70+
71+
export const marker = pathToFileURL('/tmp/module-federation').href;
72+
export default {};
73+
`,
74+
);
75+
76+
const scriptContext = await loadNodeEsmScript<{
77+
marker: string;
78+
}>();
79+
80+
expect(scriptContext.marker).toBe('file:///tmp/module-federation');
81+
expect(fetchMock).toHaveBeenCalledWith(DEFAULT_REMOTE_ENTRY_URL);
82+
expect(fetchMock).not.toHaveBeenCalledWith('node:url');
83+
});
84+
85+
it('loads bare Node.js builtin imports without fetching them as remote chunks', async () => {
86+
const fetchMock = setRemoteEntryFetchMock(
87+
DEFAULT_REMOTE_ENTRY_URL,
88+
moduleSource`
89+
import { sep } from 'path';
90+
91+
export const separator = sep;
92+
export default {};
93+
`,
94+
);
95+
96+
const scriptContext = await loadNodeEsmScript<{
97+
separator: string;
98+
}>();
99+
100+
expect(scriptContext.separator).toBe(pathSeparator);
101+
expect(fetchMock).toHaveBeenCalledWith(DEFAULT_REMOTE_ENTRY_URL);
102+
expect(fetchMock).not.toHaveBeenCalledWith('path');
103+
});
104+
105+
it('bases import.meta.url under the current workspace for createRequire package resolution', async () => {
106+
const remoteEntryUrl =
107+
'http://example.com/server/remoteEntry.js?v=123#entry';
108+
const fetchMock = setRemoteEntryFetchMock(
109+
remoteEntryUrl,
110+
moduleSource`
111+
import { createRequire } from 'node:module';
112+
113+
const require = createRequire(import.meta.url);
114+
const webpack = require('webpack');
115+
116+
export const webpackType = typeof webpack;
117+
export const metaUrl = import.meta.url;
118+
export default {};
119+
`,
120+
);
121+
122+
const scriptContext = await loadNodeEsmScript<{
123+
metaUrl: string;
124+
webpackType: string;
125+
}>(remoteEntryUrl);
126+
const cwdFileUrl = pathToFileURL(process.cwd()).href;
127+
const cwdBaseUrl = cwdFileUrl.endsWith('/') ? cwdFileUrl : `${cwdFileUrl}/`;
128+
129+
expect(['function', 'object']).toContain(scriptContext.webpackType);
130+
expect(scriptContext.metaUrl).toContain(
131+
'__module_federation_remote__/http/example.com/server/remoteEntry.js/%3Fv%3D123%23entry',
132+
);
133+
expect(scriptContext.metaUrl.startsWith(cwdBaseUrl)).toBe(true);
134+
expect(fetchMock).toHaveBeenCalledWith(remoteEntryUrl);
135+
});
136+
137+
it('evaluates dynamically imported ESM chunks before their namespace is consumed', async () => {
138+
const remoteEntryUrl = 'http://example.com/server/remoteEntry.js';
139+
const chunkUrl = 'http://example.com/server/chunk.mjs';
140+
const fetchMock = setFetchMock((url) => {
141+
if (url === remoteEntryUrl) {
142+
return createResponse(
143+
moduleSource`
144+
export const chunkValuePromise = import('./chunk.mjs').then(
145+
(chunk) => chunk.value,
146+
);
147+
export default {};
148+
`,
149+
);
150+
}
151+
152+
if (url === chunkUrl) {
153+
return createResponse(
154+
moduleSource`
155+
export const value = 'loaded chunk';
156+
`,
157+
);
158+
}
159+
160+
throw new Error(`${url} should not be fetched`);
161+
});
162+
163+
const scriptContext = await loadNodeEsmScript<{
164+
chunkValuePromise: Promise<string>;
165+
}>(remoteEntryUrl);
166+
167+
await expect(scriptContext.chunkValuePromise).resolves.toBe('loaded chunk');
168+
expect(fetchMock).toHaveBeenCalledWith(remoteEntryUrl);
169+
expect(fetchMock).toHaveBeenCalledWith(chunkUrl);
170+
});
171+
172+
it('rejects absolute non-http module URLs without fetching them', async () => {
173+
const remoteEntryUrl = 'http://example.com/server/remoteEntry.js';
174+
const fileChunkUrl = 'file:///tmp/chunk.mjs';
175+
const fetchMock = setRemoteEntryFetchMock(
176+
remoteEntryUrl,
177+
moduleSource`
178+
import value from '${fileChunkUrl}';
179+
180+
export default value;
181+
`,
182+
);
183+
184+
await expect(loadNodeEsmScript(remoteEntryUrl)).rejects.toThrow(
185+
`Unsupported ESM module specifier "${fileChunkUrl}"`,
186+
);
187+
expect(fetchMock).toHaveBeenCalledTimes(1);
188+
expect(fetchMock).toHaveBeenCalledWith(remoteEntryUrl);
189+
expect(fetchMock).not.toHaveBeenCalledWith(fileChunkUrl);
190+
});
191+
192+
it('rejects bare non-builtin imports instead of fetching them as relative chunks', async () => {
193+
const remoteEntryUrl = 'http://example.com/server/remoteEntry.js';
194+
const fetchMock = setRemoteEntryFetchMock(
195+
remoteEntryUrl,
196+
moduleSource`
197+
import React from 'react';
198+
199+
export default React;
200+
`,
201+
);
202+
203+
await expect(loadNodeEsmScript(remoteEntryUrl)).rejects.toThrow(
204+
'Unsupported ESM module specifier "react"',
205+
);
206+
expect(fetchMock).toHaveBeenCalledTimes(1);
207+
expect(fetchMock).toHaveBeenCalledWith(remoteEntryUrl);
208+
});
209+
});

packages/sdk/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@
7676
"scripts": {
7777
"build": "tsdown --config tsdown.config.ts",
7878
"lint": "ESLINT_USE_FLAT_CONFIG=false pnpm exec eslint --ignore-pattern node_modules \"**/*.ts\" \"package.json\"",
79-
"test": "pnpm exec jest --config jest.config.cjs --passWithNoTests",
80-
"test:ci": "pnpm exec jest --config jest.config.cjs --passWithNoTests --ci --coverage",
79+
"test": "NODE_OPTIONS=--experimental-vm-modules pnpm exec jest --config jest.config.cjs --passWithNoTests",
80+
"test:ci": "NODE_OPTIONS=--experimental-vm-modules pnpm exec jest --config jest.config.cjs --passWithNoTests --ci --coverage",
8181
"pre-release": "pnpm run test && pnpm run build"
8282
}
8383
}

0 commit comments

Comments
 (0)