Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dev-entries-official-mf-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@callstack/repack": patch
---

Fix `DevelopmentPlugin` crashing with `Cannot read properties of undefined (reading 'exposes')` when an official Module Federation plugin (from `@module-federation/enhanced`, or the built-in `container.ModuleFederationPlugin` of Rspack or webpack) is used with the dev server. The check for official plugins read `exposes` from `plugin.config`, which only Re.Pack's own Module Federation plugins have, instead of `plugin._options`.
2 changes: 1 addition & 1 deletion packages/repack/src/plugins/DevelopmentPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class DevelopmentPlugin {
}

// official MF plugins expose _options property
if ('_options' in plugin && plugin.config.exposes) {
if ('_options' in plugin && plugin._options.exposes) {
return plugin._options.name;
}

Expand Down
79 changes: 79 additions & 0 deletions packages/repack/src/plugins/__tests__/DevelopmentPlugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { ModuleFederationPlugin as MFPluginRspack } from '@module-federation/enhanced/rspack';
import type { Compiler, EntryNormalized } from '@rspack/core';
import { DevelopmentPlugin } from '../DevelopmentPlugin.js';
import { ModuleFederationPluginV2 } from '../ModuleFederationPluginV2.js';

const createMockCompiler = (plugins: unknown[]) => {
const EntryPlugin = jest.fn(() => ({ apply: jest.fn() }));
let onEntryOption: (context: string, entry: EntryNormalized) => void =
() => {};

const compiler = {
context: __dirname,
options: {
devServer: { hot: true, host: 'localhost', port: 8081 },
output: { uniqueName: 'test' },
resolve: { alias: {} },
module: { rules: [] },
plugins,
},
hooks: {
entryOption: {
tap: (_: unknown, callback: typeof onEntryOption) => {
onEntryOption = callback;
},
},
},
webpack: {
rspackVersion: '1.0.0',
DefinePlugin: jest.fn(() => ({ apply: jest.fn() })),
HotModuleReplacementPlugin: jest.fn(() => ({ apply: jest.fn() })),
ProvidePlugin: jest.fn(() => ({ apply: jest.fn() })),
Template: { toIdentifier: (name: string) => name },
EntryPlugin,
},
} as unknown as Compiler;

return {
compiler,
getEntryNames: () => {
onEntryOption(__dirname, { main: { import: ['./index.js'] } });
const calls = EntryPlugin.mock.calls as unknown as Array<
[string, string, { name: string }]
>;
return [...new Set(calls.map(([, , options]) => options.name))];
},
};
};

describe('DevelopmentPlugin', () => {
it('should add development entries to the container of an official MF plugin with exposes', () => {
const { compiler, getEntryNames } = createMockCompiler([
new MFPluginRspack({ name: 'remote', exposes: { './App': './App.js' } }),
]);
new DevelopmentPlugin({ platform: 'ios' }).apply(compiler);

expect(getEntryNames()).toEqual(['main', 'remote']);
});

it('should skip an official MF plugin without exposes', () => {
const { compiler, getEntryNames } = createMockCompiler([
new MFPluginRspack({ name: 'host' }),
]);
new DevelopmentPlugin({ platform: 'ios' }).apply(compiler);

expect(getEntryNames()).toEqual(['main']);
});

it('should add development entries to the container of a Re.Pack MF plugin with exposes', () => {
const { compiler, getEntryNames } = createMockCompiler([
new ModuleFederationPluginV2({
name: 'remote',
exposes: { './App': './App.js' },
}),
]);
new DevelopmentPlugin({ platform: 'ios' }).apply(compiler);

expect(getEntryNames()).toEqual(['main', 'remote']);
});
});