Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
it('should be able to handle spaces in path to exposes', async () => {
const { default: test1 } = await import('./test 1');
const { default: test2 } = await import('./path with spaces/test-2');
expect(test1()).toBe('test 1');
expect(test2()).toBe('test 2');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function test() {
return 'test 2';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function test() {
return 'test 1';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const { ModuleFederationPlugin } = require('../../../../dist/src');

module.exports = {
mode: 'development',
devtool: false,
output: {
publicPath: 'http://localhost:3000/',
},
plugins: [
new ModuleFederationPlugin({
name: 'remote',
filename: 'remoteEntry.js',
manifest: true,
exposes: {
'./test-1': './test 1.js',
'./test-2': './path with spaces/test-2.js',
},
}),
],
};
20 changes: 19 additions & 1 deletion packages/manifest/src/ModuleHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,25 @@ class ModuleHandler {
// identifier: container entry (default) [[".",{"import":["./src/routes/page.tsx"],"name":"__federation_expose_default_export"}]]'
const data = identifier.split(' ');

JSON.parse(data[3]).forEach(([prefixedName, file]) => {
let entries: Array<
[exposeKey: string, { import: string[]; name?: string }]
> = [];

try {
// Prefer parsing exposes from stats first
entries = JSON.parse(data[3]);
} catch {
// If that fails, fallback to the original options
const exposes = this._options.exposes;

if (!exposes || typeof exposes !== 'object') {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my investigations this.options.exposes in ModuleHandler is always parsed into

interface ExposesObject {
    [k: string]: ExposesConfig;
}

interface ExposesConfig {
    import: ExposesItem | ExposesItems;
    name?: string;
}

by ContainerManager.containerPluginExposesOptions. But due to how everything is currently typed, TS thinks this could be the broader "unparsed" Exposes shape.

type Exposes = (ExposesItem | ExposesObject)[] | ExposesObject;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how it looks like, for both enhanced/ModuleFederationPlugin and rspack/ModuleFederationPlugin

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now, we could maintain this 100% decoupled and idempotently "re-parse" the entries ModuleHandler to ensure the type safety, but I would wait for a review first before increasing the complexity here. cc @ScriptedAlchemy @2heal1

return;
}

entries = Object.entries(exposes);
}

entries.forEach(([prefixedName, file]) => {
Comment on lines +295 to +313
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm opting to maintain the current JSON parsing logic and wrap it in a try catch block. There are probably good reason why it was implemented this way in the first place, but I lack the context, and I rather be as safe/backwards compatible as possible. If it fails the introduced logic just attempts to recover exposes from another source.

@2heal1 @ScriptedAlchemy As you mentioned in the issue, there are probably better ways of solving this, but again, I lack the context to propose a long-term solution. You mentioned a short-term solution, but since the issue was closed, I figured it wouldn't hurt proposing this patch 😅.

Also note I'm open to contribute and implement this in a different way if I can get some high level guidance (Just a couple of quick notes would be really helpful).

// TODO: support multiple import
exposesMap[getFileNameWithOutExt(file.import[0])] = getExposeItem({
exposeKey: prefixedName,
Expand Down