Skip to content

Commit f81bbea

Browse files
authored
fix: ensure compatibility for rspack by flattening scope array in customShareInfo (#4555)
1 parent a78f53c commit f81bbea

File tree

3 files changed

+223
-0
lines changed

3 files changed

+223
-0
lines changed

.changeset/shaggy-taxis-sneeze.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@module-federation/webpack-bundler-runtime': patch
3+
---
4+
5+
fix(webpack-bundler-runtime): ensure compatibility for rspack by flattening scope array in customShareInfo

packages/webpack-bundler-runtime/__tests__/consumes.spec.ts

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,216 @@ describe('consumes', () => {
371371
expect(moduleObj.exports).toBe('factory result');
372372
});
373373

374+
test('should unwrap nested array scope for rspack compatibility', async () => {
375+
// Setup
376+
const mockModuleId = 'moduleId1';
377+
const mockPromises: Promise<any>[] = [];
378+
const mockShareKey = 'testShareKey';
379+
// Rspack wraps scope in nested array: [['default']]
380+
const mockShareInfo = {
381+
scope: [['default']],
382+
shareConfig: {
383+
singleton: true,
384+
requiredVersion: '1.0.0',
385+
},
386+
};
387+
388+
const mockFactory = jest.fn().mockReturnValue('factory result');
389+
const mockLoadSharePromise = Promise.resolve(mockFactory);
390+
391+
const mockFederationInstance = {
392+
loadShare: jest.fn().mockReturnValue(mockLoadSharePromise),
393+
};
394+
395+
const mockWebpackRequire = {
396+
o: jest
397+
.fn()
398+
.mockImplementation((obj, key) =>
399+
Object.prototype.hasOwnProperty.call(obj, key),
400+
),
401+
m: {},
402+
c: {},
403+
federation: {
404+
instance: mockFederationInstance,
405+
},
406+
};
407+
408+
const mockOptions: ConsumesOptions = {
409+
chunkId: 'testChunkId',
410+
promises: mockPromises,
411+
chunkMapping: {
412+
testChunkId: [mockModuleId],
413+
},
414+
installedModules: {},
415+
moduleToHandlerMapping: {
416+
[mockModuleId]: {
417+
shareKey: mockShareKey,
418+
getter: jest.fn(),
419+
shareInfo: mockShareInfo,
420+
},
421+
},
422+
webpackRequire: mockWebpackRequire as any,
423+
};
424+
425+
// Execute
426+
consumes(mockOptions);
427+
428+
// Verify promise is added
429+
expect(mockPromises.length).toBe(1);
430+
431+
// Wait for promise to resolve
432+
await mockPromises[0];
433+
434+
// Verify loadShare was called with unwrapped scope
435+
expect(mockFederationInstance.loadShare).toHaveBeenCalledWith(
436+
mockShareKey,
437+
expect.objectContaining({
438+
customShareInfo: expect.objectContaining({
439+
scope: ['default'], // Should be unwrapped from [['default']]
440+
}),
441+
}),
442+
);
443+
444+
// Verify the original shareInfo was not mutated (customShareInfo is a copy)
445+
expect(mockShareInfo.scope).toEqual([['default']]);
446+
});
447+
448+
test('should not unwrap non-nested array scope', async () => {
449+
// Setup
450+
const mockModuleId = 'moduleId1';
451+
const mockPromises: Promise<any>[] = [];
452+
const mockShareKey = 'testShareKey';
453+
// Webpack style: scope is a flat array
454+
const mockShareInfo = {
455+
scope: ['default'],
456+
shareConfig: {
457+
singleton: true,
458+
requiredVersion: '1.0.0',
459+
},
460+
};
461+
462+
const mockFactory = jest.fn().mockReturnValue('factory result');
463+
const mockLoadSharePromise = Promise.resolve(mockFactory);
464+
465+
const mockFederationInstance = {
466+
loadShare: jest.fn().mockReturnValue(mockLoadSharePromise),
467+
};
468+
469+
const mockWebpackRequire = {
470+
o: jest
471+
.fn()
472+
.mockImplementation((obj, key) =>
473+
Object.prototype.hasOwnProperty.call(obj, key),
474+
),
475+
m: {},
476+
c: {},
477+
federation: {
478+
instance: mockFederationInstance,
479+
},
480+
};
481+
482+
const mockOptions: ConsumesOptions = {
483+
chunkId: 'testChunkId',
484+
promises: mockPromises,
485+
chunkMapping: {
486+
testChunkId: [mockModuleId],
487+
},
488+
installedModules: {},
489+
moduleToHandlerMapping: {
490+
[mockModuleId]: {
491+
shareKey: mockShareKey,
492+
getter: jest.fn(),
493+
shareInfo: mockShareInfo,
494+
},
495+
},
496+
webpackRequire: mockWebpackRequire as any,
497+
};
498+
499+
// Execute
500+
consumes(mockOptions);
501+
502+
// Wait for promise to resolve
503+
await mockPromises[0];
504+
505+
// Verify loadShare was called with unchanged scope
506+
expect(mockFederationInstance.loadShare).toHaveBeenCalledWith(
507+
mockShareKey,
508+
expect.objectContaining({
509+
customShareInfo: expect.objectContaining({
510+
scope: ['default'], // Should remain unchanged
511+
}),
512+
}),
513+
);
514+
});
515+
516+
test('should not unwrap scope when first element is not an array', async () => {
517+
// Setup
518+
const mockModuleId = 'moduleId1';
519+
const mockPromises: Promise<any>[] = [];
520+
const mockShareKey = 'testShareKey';
521+
// Edge case: scope is an array but first element is not an array
522+
const mockShareInfo = {
523+
scope: ['default', 'other'],
524+
shareConfig: {
525+
singleton: true,
526+
requiredVersion: '1.0.0',
527+
},
528+
};
529+
530+
const mockFactory = jest.fn().mockReturnValue('factory result');
531+
const mockLoadSharePromise = Promise.resolve(mockFactory);
532+
533+
const mockFederationInstance = {
534+
loadShare: jest.fn().mockReturnValue(mockLoadSharePromise),
535+
};
536+
537+
const mockWebpackRequire = {
538+
o: jest
539+
.fn()
540+
.mockImplementation((obj, key) =>
541+
Object.prototype.hasOwnProperty.call(obj, key),
542+
),
543+
m: {},
544+
c: {},
545+
federation: {
546+
instance: mockFederationInstance,
547+
},
548+
};
549+
550+
const mockOptions: ConsumesOptions = {
551+
chunkId: 'testChunkId',
552+
promises: mockPromises,
553+
chunkMapping: {
554+
testChunkId: [mockModuleId],
555+
},
556+
installedModules: {},
557+
moduleToHandlerMapping: {
558+
[mockModuleId]: {
559+
shareKey: mockShareKey,
560+
getter: jest.fn(),
561+
shareInfo: mockShareInfo,
562+
},
563+
},
564+
webpackRequire: mockWebpackRequire as any,
565+
};
566+
567+
// Execute
568+
consumes(mockOptions);
569+
570+
// Wait for promise to resolve
571+
await mockPromises[0];
572+
573+
// Verify loadShare was called with unchanged scope
574+
expect(mockFederationInstance.loadShare).toHaveBeenCalledWith(
575+
mockShareKey,
576+
expect.objectContaining({
577+
customShareInfo: expect.objectContaining({
578+
scope: ['default', 'other'], // Should remain unchanged
579+
}),
580+
}),
581+
);
582+
});
583+
374584
test('should handle promise rejection', async () => {
375585
// Setup
376586
const mockModuleId = 'moduleId1';

packages/webpack-bundler-runtime/src/consumes.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ export function consumes(options: ConsumesOptions) {
6464
moduleToHandlerMapping[id];
6565
const usedExports = getUsedExports(webpackRequire, shareKey);
6666
const customShareInfo: Partial<Shared> = { ...shareInfo };
67+
// compatibility for rspack, which will wrap scope with array, but webpack will not
68+
// https://github.com/web-infra-dev/rspack/blob/main/packages/rspack/src/runtime/moduleFederationDefaultRuntime.js#L95
69+
if (
70+
Array.isArray(customShareInfo.scope) &&
71+
Array.isArray(customShareInfo.scope[0])
72+
) {
73+
customShareInfo.scope = customShareInfo.scope[0];
74+
}
6775
if (usedExports) {
6876
customShareInfo.treeShaking = {
6977
usedExports,

0 commit comments

Comments
 (0)