Skip to content

Commit 871f411

Browse files
authored
Merge branch 'main' into feature/var-remote-entry
2 parents 64c3586 + 4307b7a commit 871f411

File tree

6 files changed

+173
-9
lines changed

6 files changed

+173
-9
lines changed

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@module-federation/vite",
3-
"version": "1.9.2",
3+
"version": "1.9.4",
44
"description": "Vite plugin for Module Federation",
55
"type": "module",
66
"source": "src/index.ts",
@@ -45,6 +45,9 @@
4545
},
4646
"homepage": "https://github.com/module-federation/vite#readme",
4747
"packageManager": "[email protected]",
48+
"peerDependencies": {
49+
"vite": "<=6"
50+
},
4851
"dependencies": {
4952
"@module-federation/runtime": "^0.21.6",
5053
"@module-federation/sdk": "^0.21.6",

src/plugins/pluginAddEntry.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,11 @@ const addEntry = ({
135135
generateBundle(options, bundle) {
136136
if (!injectHtml()) return;
137137
const file = this.getFileName(emitFileId);
138+
const path = viteConfig.experimental?.renderBuiltUrl
139+
? viteConfig.experimental?.renderBuiltUrl(file)
140+
: viteConfig.base + file;
138141
const scriptContent = `
139-
<script type="module" src="${viteConfig.base + file}"></script>
142+
<script type="module" src="${path}"></script>
140143
`;
141144

142145
for (const fileName in bundle) {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { serializeRuntimeOptions } from '../serializeRuntimeOptions';
2+
3+
describe('generateRuntimePluginOption - safe JS literal', () => {
4+
it('should serialize complex megaObject', () => {
5+
const emptyArray: any[] = [];
6+
const emptyObject: any = {};
7+
8+
const megaObject = {
9+
arrInsideObj: [{ x: 10 }, new Set(['s1', 's2'])],
10+
mapInsideObj: new Map([
11+
['mapKey', new Set([12321])],
12+
['mapSet', new Set([1, 2, 3])],
13+
]),
14+
objInsideSet: new Set([{ a: 1 }, new Map([['mk', { nested: 'value' }]])]),
15+
};
16+
17+
const input = {
18+
numberVal: 123,
19+
stringVal: 'hello world',
20+
booleanVal: true,
21+
nullVal: null,
22+
undefinedVal: undefined,
23+
symbolVal: Symbol('prod'),
24+
funcVal: function greet(name: string) {
25+
return 'hello ' + name;
26+
},
27+
dateVal: new Date('2023-10-10T10:00:00.000Z'),
28+
regexVal: /prod-test/gi,
29+
arrayVal: [1, 'a', true, [2, 3, [4, 5]]],
30+
nestedObj: { level1: { level2: { value: 'deep' } } },
31+
mapVal: new Map([['key1', 100]]),
32+
setVal: new Set([1, 'x', new Date('2020-01-01')]),
33+
emptyArray,
34+
emptyObject,
35+
megaObject,
36+
};
37+
38+
console.time('bench-test');
39+
const code = serializeRuntimeOptions(input);
40+
console.timeEnd('bench-test');
41+
42+
// Simple checks that key features exist
43+
expect(code)
44+
.toContain(`{"numberVal": 123, "stringVal": "hello world", "booleanVal": true, "nullVal": null, "undefinedVal": undefined, "symbolVal": Symbol("prod"), "funcVal": function greet(name) {
45+
return "hello " + name;
46+
}, "dateVal": new Date("2023-10-10T10:00:00.000Z"), "regexVal": new RegExp("prod-test", "gi"), "arrayVal": [1, "a", true, [2, 3, [4, 5]]], "nestedObj": {"level1": {"level2": {"value": "deep"}}}, "mapVal": new Map([["key1", 100]]), "setVal": new Set([1, "x", new Date("2020-01-01T00:00:00.000Z")]), "emptyArray": [], "emptyObject": {}, "megaObject": {"arrInsideObj": [{"x": 10}, new Set(["s1", "s2"])], "mapInsideObj": new Map([["mapKey", new Set([12321])], ["mapSet", new Set([1, 2, 3])]]), "objInsideSet": new Set([{"a": 1}, new Map([["mk", {"nested": "value"}]])])}}`);
47+
});
48+
});

src/utils/normalizeModuleFederationOptions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ export type ModuleFederationOptions = {
307307
}
308308
>
309309
| undefined;
310-
runtimePlugins?: string[];
310+
runtimePlugins?: Array<string | [string, Record<string, unknown>]>;
311311
getPublicPath?: string;
312312
implementation?: string;
313313
manifest?: ManifestOptions | boolean;
@@ -333,7 +333,7 @@ export interface NormalizedModuleFederationOptions {
333333
runtime: any;
334334
shareScope: string;
335335
shared: NormalizedShared;
336-
runtimePlugins: string[];
336+
runtimePlugins: Array<string | [string, Record<string, unknown>]>;
337337
implementation: string;
338338
manifest: ManifestOptions | boolean;
339339
dev?: boolean | PluginDevOptions;
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* Serializes a JavaScript object into a string of source code that can be evaluated.
3+
* This function is used to create runtime plugin options without relying solely on JSON.stringify,
4+
* allowing support for non-JSON types like RegExp, Date, Map, Set, and Functions.
5+
* It also safely handles circular references.
6+
*
7+
* @param {Record<string, unknown>} options - The options object to serialize.
8+
* @returns {string} The resulting JavaScript source code string.
9+
*/
10+
export function serializeRuntimeOptions(options: Record<string, unknown>): string {
11+
// Use a WeakSet to track objects already encountered, which helps in detecting circular references.
12+
const seenObjects = new WeakSet<any>();
13+
14+
/**
15+
* Recursive inner function to serialize any value into a source code string.
16+
*/
17+
function valueToCode(val: any): string {
18+
// 1. Handle primitive values
19+
if (val === null) return 'null';
20+
21+
const type = typeof val;
22+
23+
if (type === 'string') return JSON.stringify(val);
24+
if (type === 'number' || type === 'boolean') return String(val);
25+
if (type === 'undefined') return 'undefined';
26+
27+
// Handle Symbol
28+
if (type === 'symbol') {
29+
const desc = val.description ?? '';
30+
return `Symbol(${JSON.stringify(desc)})`;
31+
}
32+
33+
// Handle Function (returns the function's source code)
34+
if (type === 'function') return val.toString();
35+
36+
// 2. Handle special built-in objects
37+
if (val instanceof Date) return `new Date(${JSON.stringify(val.toISOString())})`;
38+
if (val instanceof RegExp) {
39+
return `new RegExp(${JSON.stringify(val.source)}, ${JSON.stringify(val.flags)})`;
40+
}
41+
42+
// 3. Check for circular references and mark object as seen
43+
// This applies to objects, arrays, maps, and sets.
44+
if (type === 'object') {
45+
if (seenObjects.has(val)) {
46+
// This object has been seen previously in the recursion path
47+
return `"__circular__"`;
48+
}
49+
seenObjects.add(val);
50+
}
51+
52+
// 4. Handle Array, Map, Set
53+
if (Array.isArray(val)) {
54+
// Recursively serialize each element
55+
return `[${val.map(valueToCode).join(', ')}]`;
56+
}
57+
58+
if (val instanceof Map) {
59+
// Serialize Map entries into an array of [key, value] pairs
60+
const entries = Array.from(val.entries()).map(
61+
([k, v]) => `[${valueToCode(k)}, ${valueToCode(v)}]`
62+
);
63+
return `new Map([${entries.join(', ')}])`;
64+
}
65+
66+
if (val instanceof Set) {
67+
// Serialize Set values into an array
68+
const items = Array.from(val.values()).map(valueToCode);
69+
return `new Set([${items.join(', ')}])`;
70+
}
71+
72+
// 5. Handle plain objects (the default object type)
73+
if (type === 'object') {
74+
const properties: string[] = [];
75+
76+
// Iterate over the object's own enumerable properties
77+
for (const key in val) {
78+
if (Object.prototype.hasOwnProperty.call(val, key)) {
79+
// Wrap the key in JSON.stringify to handle non-identifier keys
80+
properties.push(`${JSON.stringify(key)}: ${valueToCode(val[key])}`);
81+
}
82+
}
83+
return `{${properties.join(', ')}}`;
84+
}
85+
86+
// 6. Fallback case (e.g., BigInt, other object types)
87+
// Coerce to string and then JSON.stringify that string for safety
88+
return JSON.stringify(String(val));
89+
}
90+
91+
// Start serialization for the top-level object
92+
const topLevelProps: string[] = [];
93+
94+
// Iterate over the properties of the root 'options' object
95+
for (const key in options) {
96+
if (Object.prototype.hasOwnProperty.call(options, key)) {
97+
topLevelProps.push(`${JSON.stringify(key)}: ${valueToCode(options[key])}`);
98+
}
99+
}
100+
101+
return `{${topLevelProps.join(', ')}}`;
102+
}

src/virtualModules/virtualRemoteEntry.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
getNormalizeShareItem,
88
NormalizedModuleFederationOptions,
99
} from '../utils/normalizeModuleFederationOptions';
10+
import { serializeRuntimeOptions } from '../utils/serializeRuntimeOptions';
1011
import VirtualModule from '../utils/VirtualModule';
1112
import { VIRTUAL_EXPOSES } from './virtualExposes';
1213
import { getUsedRemotesMap } from './virtualRemotes';
@@ -124,10 +125,17 @@ export function generateLocalSharedImportMap() {
124125

125126
export const REMOTE_ENTRY_ID = 'virtual:mf-REMOTE_ENTRY_ID';
126127
export function generateRemoteEntry(options: NormalizedModuleFederationOptions): string {
127-
const pluginImportNames = options.runtimePlugins.map((p, i) => [
128-
`$runtimePlugin_${i}`,
129-
`import $runtimePlugin_${i} from "${p}";`,
130-
]);
128+
const pluginImportNames = options.runtimePlugins.map((p, i) => {
129+
if (typeof p === 'string') {
130+
return [`$runtimePlugin_${i}`, `import $runtimePlugin_${i} from "${p}";`, `undefined`];
131+
} else {
132+
return [
133+
`$runtimePlugin_${i}`,
134+
`import $runtimePlugin_${i} from "${p[0]}";`,
135+
serializeRuntimeOptions(p[1]),
136+
];
137+
}
138+
});
131139

132140
return `
133141
import {init as runtimeInit, loadRemote} from "@module-federation/runtime";
@@ -145,7 +153,7 @@ export function generateRemoteEntry(options: NormalizedModuleFederationOptions):
145153
name: mfName,
146154
remotes: usedRemotes,
147155
shared: usedShared,
148-
plugins: [${pluginImportNames.map((item) => `${item[0]}()`).join(', ')}],
156+
plugins: [${pluginImportNames.map((item) => `${item[0]}(${item[2]})`).join(', ')}],
149157
${options.shareStrategy ? `shareStrategy: '${options.shareStrategy}'` : ''}
150158
});
151159
// handling circular init calls

0 commit comments

Comments
 (0)