Skip to content

Commit cd3a71a

Browse files
committed
fix(node): enhance error messaging for fetchAndRun with url
1 parent f323928 commit cd3a71a

File tree

5 files changed

+71
-12
lines changed

5 files changed

+71
-12
lines changed

packages/node/src/__tests__/runtimePlugin.test.ts

+53-5
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ jest.mock('vm', () => ({
4141
},
4242
}));
4343

44-
global.fetch = jest.fn().mockResolvedValue({
44+
(global as unknown as any).fetch = jest.fn().mockResolvedValue({
4545
text: jest.fn().mockResolvedValue('// mock chunk content'),
4646
});
47+
const globalFetch = (global as unknown as any).fetch as jest.Mock;
4748

4849
const mockWebpackRequire = {
4950
u: jest.fn((chunkId: string) => `/chunks/${chunkId}.js`),
@@ -77,7 +78,7 @@ const mockNonWebpackRequire = jest.fn().mockImplementation((id: string) => {
7778
if (id === 'path') return require('path');
7879
if (id === 'fs') return require('fs');
7980
if (id === 'vm') return require('vm');
80-
if (id === 'node-fetch') return { default: global.fetch };
81+
if (id === 'node-fetch') return { default: globalFetch };
8182
return {};
8283
});
8384

@@ -343,11 +344,11 @@ describe('runtimePlugin', () => {
343344

344345
describe('fetchAndRun', () => {
345346
beforeEach(() => {
346-
(global.fetch as jest.Mock).mockReset();
347+
(globalFetch as jest.Mock).mockReset();
347348
});
348349

349350
it('should fetch and execute remote content', async () => {
350-
(global.fetch as jest.Mock).mockResolvedValue({
351+
(globalFetch as jest.Mock).mockResolvedValue({
351352
text: jest.fn().mockResolvedValue('// mock script content'),
352353
});
353354

@@ -381,7 +382,7 @@ describe('runtimePlugin', () => {
381382

382383
it('should handle fetch errors', async () => {
383384
const fetchError = new Error('Fetch failed');
384-
(global.fetch as jest.Mock).mockRejectedValue(fetchError);
385+
(globalFetch as jest.Mock).mockRejectedValue(fetchError);
385386

386387
const url = new URL('http://example.com/chunk.js');
387388
const callback = jest.fn();
@@ -403,6 +404,9 @@ describe('runtimePlugin', () => {
403404
await new Promise((resolve) => setTimeout(resolve, 10));
404405

405406
expect(callback).toHaveBeenCalledWith(expect.any(Error), null);
407+
expect(callback.mock.calls[0][0].message.includes(url.href)).toEqual(
408+
true,
409+
);
406410
});
407411
});
408412

@@ -746,6 +750,50 @@ describe('runtimePlugin', () => {
746750
// The original promise should be reused
747751
expect(promises[0]).toBe(originalPromise);
748752
});
753+
754+
it('should delete chunks from the installedChunks when loadChunk fails', async () => {
755+
// mock loadChunk to fail
756+
jest
757+
.spyOn(runtimePluginModule, 'loadChunk')
758+
.mockImplementationOnce((strategy, chunkId, root, callback, args) => {
759+
Promise.resolve().then(() => {
760+
callback(new Error('failed to load'), undefined);
761+
});
762+
});
763+
764+
jest
765+
.spyOn(runtimePluginModule, 'installChunk')
766+
.mockImplementationOnce((chunk, installedChunks) => {
767+
// Mock implementation that doesn't rely on iterating chunk.ids
768+
installedChunks['test-chunk'] = 0;
769+
});
770+
771+
// Mock installedChunks
772+
const installedChunks: Record<string, any> = {};
773+
774+
// Call the function under test - returns the handler function, doesn't set webpack_require.f.require
775+
const handler = setupChunkHandler(installedChunks, {});
776+
777+
const promises: Promise<any>[] = [];
778+
let res, err;
779+
780+
try {
781+
// Call the handler with mock chunk ID and promises array
782+
handler('test-chunk', promises);
783+
// Verify that installedChunks has test-chunk before the promise rejects
784+
expect(installedChunks['test-chunk']).toBeDefined();
785+
res = await promises[0];
786+
} catch (e) {
787+
err = e;
788+
}
789+
790+
// Verify that an error was thrown, and the response is undefined
791+
expect(res).not.toBeDefined();
792+
expect(err instanceof Error).toEqual(true);
793+
794+
// Verify the chunk data was properly removed
795+
expect(installedChunks['test-chunk']).not.toBeDefined();
796+
});
749797
});
750798

751799
describe('setupWebpackRequirePatching', () => {

packages/node/src/runtimePlugin.ts

+11-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type {
22
FederationRuntimePlugin,
33
FederationHost,
44
} from '@module-federation/runtime';
5+
import { Response } from 'node-fetch';
6+
57
type WebpackRequire = {
68
(id: string): any;
79
u: (chunkId: string) => string;
@@ -125,26 +127,29 @@ export const loadFromFs = (
125127
callback(new Error(`File ${filename} does not exist`), null);
126128
}
127129
};
128-
129130
// Hoisted utility function to fetch and execute chunks from remote URLs
130131
export const fetchAndRun = (
131132
url: URL,
132133
chunkName: string,
133134
callback: (err: Error | null, chunk: any) => void,
134135
args: any,
135136
): void => {
136-
(typeof fetch === 'undefined'
137+
const createFetchError = (e: Error) =>
138+
new Error(`Error while fetching from URL: ${url}`, { cause: e });
139+
(typeof (global as any).fetch === 'undefined'
137140
? importNodeModule<typeof import('node-fetch')>('node-fetch').then(
138141
(mod) => mod.default,
139142
)
140-
: Promise.resolve(fetch)
143+
: Promise.resolve((global as any).fetch)
141144
)
142145
.then((fetchFunction) => {
143146
return args.origin.loaderHook.lifecycle.fetch
144147
.emit(url.href, {})
145148
.then((res: Response | null) => {
146149
if (!res || !(res instanceof Response)) {
147-
return fetchFunction(url.href).then((response) => response.text());
150+
return fetchFunction(url.href).then((response: Response) =>
151+
response.text(),
152+
);
148153
}
149154
return res.text();
150155
});
@@ -160,10 +165,10 @@ export const fetchAndRun = (
160165
);
161166
callback(null, chunk);
162167
} catch (e) {
163-
callback(e as Error, null);
168+
callback(createFetchError(e as Error), null);
164169
}
165170
})
166-
.catch((err: Error) => callback(err, null));
171+
.catch((err: Error) => callback(createFetchError(err as Error), null));
167172
};
168173

169174
// Hoisted utility function to resolve URLs for chunks

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/* eslint-disable no-undef */
22

3+
import { Response } from 'node-fetch';
4+
35
// @ts-ignore
46
if (!globalThis.usedChunks) {
57
// @ts-ignore
@@ -121,7 +123,9 @@ const processChunk = async (chunk, shareMap, hostStats) => {
121123
let stats = {};
122124

123125
try {
124-
stats = await fetch(statsFile).then((res) => res.json());
126+
stats = await (global as any)
127+
.fetch(statsFile)
128+
.then((res: Response) => res.json());
125129
} catch (e) {
126130
console.error('flush error', e);
127131
}

packages/node/src/utils/hot-reload.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { getAllKnownRemotes } from './flush-chunks';
22
import crypto from 'crypto';
33
import helpers from '@module-federation/runtime/helpers';
44
import path from 'path';
5+
import { Response } from 'node-fetch';
56

67
declare global {
78
var mfHashMap: Record<string, string> | undefined;

packages/node/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"extends": "../../tsconfig.base.json",
33
"compilerOptions": {
44
"target": "ES2021",
5+
"lib": ["es2022.error"],
56
"moduleResolution": "node",
67
"resolveJsonModule": true,
78
"skipLibCheck": true,

0 commit comments

Comments
 (0)