@module-federation/vite 1.21.5 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -242,6 +242,27 @@ You can specify the place the host initialization file is injected with the **ho
242
242
  The **moduleParseTimeout** option allows you to configure the maximum time to wait for module parsing during the build process.
243
243
  The **moduleParseIdleTimeout** option is an alternative that resets the timer on every parsed module. It only fires when there has been no module activity for the configured duration, making it suitable for large codebases where the total build time exceeds the fixed timeout.
244
244
 
245
+ ## SSR entry loading strategy
246
+
247
+ SSR hosts can choose how HTTP ESM remote entries are evaluated during build and preview:
248
+
249
+ ```ts
250
+ federation({
251
+ name: "host",
252
+ remotes: {
253
+ // ...
254
+ },
255
+ ssrEntryLoader: {
256
+ strategy: "vm",
257
+ },
258
+ });
259
+ ```
260
+
261
+ - `"temp-file"` (default) fetches the remote graph, rewrites imports to host-resolved shared packages, writes temporary files, and loads them with `import()`.
262
+ - `"vm"` evaluates the graph in memory with `vm.SourceTextModule` and resolves shared packages through the Module Federation share scope.
263
+
264
+ The `"vm"` strategy requires Node.js to run with `--experimental-vm-modules`. When unavailable, the loader warns once and falls back to `"temp-file"`. Vite 8 development uses `ModuleRunner`; this option primarily affects build and preview SSR entry loading.
265
+
245
266
  ## Runtime capability optimization
246
267
 
247
268
  Runtime features that a build never uses can be removed at build time:
@@ -326,9 +347,9 @@ This deployment step is separate from the local Vite build; the plugin only emit
326
347
 
327
348
  ## External runtime (`experiments`)
328
349
 
329
- Share one `@module-federation/runtime-core` instance from a pure consumer host so remotes do not bundle their own copy. Pair the flags — remotes with `externalRuntime` require a host that provides the global.
350
+ Share one `@module-federation/runtime-core` instance from the host so remotes do not bundle their own copy. Pair the flags — remotes with `externalRuntime` require a host that provides the global.
330
351
 
331
- **Host (pure consumer, no `exposes`):**
352
+ **Host:**
332
353
 
333
354
  ```ts
334
355
  federation({
@@ -361,14 +382,14 @@ federation({
361
382
  });
362
383
  ```
363
384
 
364
- `provideExternalRuntime` injects a local runtime plugin that publishes `runtime-core` on `globalThis._FEDERATION_RUNTIME_CORE`. `externalRuntime` rewrites imports of `@module-federation/runtime-core` to read that global. Using `provideExternalRuntime` together with `exposes` throws only pure consumers may provide the runtime.
385
+ `provideExternalRuntime` injects a local runtime plugin that publishes `runtime-core` on `globalThis._FEDERATION_RUNTIME_CORE`. `externalRuntime` rewrites imports of `@module-federation/runtime-core` to read that global. A container that also `exposes` (e.g. a host consumed by its own remotes) may provide the runtime too, as long as exactly one container on the page does and it is loaded before any `externalRuntime` remote evaluates (a second provider is ignored with a `Detect multiple module federation runtime!` warning; a remote evaluated before the provider throws `_FEDERATION_RUNTIME_CORE is missing`).
365
386
  The `externalRuntime` rewrite applies to the browser remote graph; SSR remote entries continue to resolve `@module-federation/runtime-core` from Node so they do not depend on the browser global.
366
387
 
367
388
  ## ⚠️ `codeSplitting` is managed by the plugin
368
389
 
369
390
  Do not set `build.rollupOptions.output.codeSplitting` or
370
391
  `build.rolldownOptions.output.codeSplitting` to `false` — it will be **ignored** (with a warning).
371
- Module Federation requires chunk splitting so `loadShare` and `runtimeInitStatus` stay isolated for correct bootstrap order.
392
+ Module Federation requires chunk splitting so `runtimeInitStatus` and deferred `loadShare` wrappers stay isolated for correct bootstrap order. Eager `loadShare` wrappers are coalesced into one `loadShare-eager` chunk on Vite 8+ to reduce startup requests.
372
393
 
373
394
  ### `codeSplitting.groups` (Vite 8+ / Rolldown)
374
395
 
@@ -43,5 +43,24 @@ function isAbsoluteUrl(src) {
43
43
  if (/^[a-z]:[\\/]/i.test(src)) return false;
44
44
  return EXTERNAL_URL_RE.test(src);
45
45
  }
46
+ const HASH_PLACEHOLDER_RE = /(?:[._-]?\[hash(?::\d+)?\])/g;
47
+ function hasFileExtension(fileName) {
48
+ return fileName.slice(Math.max(fileName.lastIndexOf("/"), fileName.lastIndexOf("\\")) + 1).lastIndexOf(".") > 0;
49
+ }
50
+ /**
51
+ * Resolve a bundler `filename` template that still contains `[hash]` placeholders
52
+ * into the concrete, stable file name Module Federation serves.
53
+ *
54
+ * The federation entries are emitted by us rather than hashed by the bundler, so
55
+ * the placeholder is dropped instead of being substituted. When stripping it also
56
+ * removes the extension (`mf-[hash:8]` → `mf`), `.js` is appended so the result
57
+ * stays a loadable module. The extension check deliberately looks at the basename
58
+ * only — a dotted directory (`assets/v1.2/entry`) must not be mistaken for one.
59
+ */
60
+ function resolveHashPlaceholderFileName(fileName) {
61
+ if (!fileName.includes("[hash")) return fileName;
62
+ const normalized = fileName.replace(HASH_PLACEHOLDER_RE, "");
63
+ return hasFileExtension(normalized) ? normalized : `${normalized}.js`;
64
+ }
46
65
  //#endregion
47
- export { normalizePathForImport as n, rebaseImport as r, EXTERNAL_URL_RE as t };
66
+ export { resolveHashPlaceholderFileName as i, normalizePathForImport as n, rebaseImport as r, EXTERNAL_URL_RE as t };
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, readdirSync } from "fs";
2
2
  import { createRequire } from "module";
3
3
  import * as path$1 from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "url";
5
+ import { createHash } from "node:crypto";
5
6
  //#region src/utils/logger.ts
6
7
  const MODULE_FEDERATION_LOG_PREFIX = "[Module Federation]";
7
8
  function formatModuleFederationMessage(message) {
@@ -136,22 +137,37 @@ function getPackageExportsTarget(pkg, packageName, exportsField) {
136
137
  * - => 3
137
138
  * . => 4
138
139
  */
140
+ const MF_HASHED_NAME_THRESHOLD = 90;
141
+ const MF_HASHED_NAME_HASH_LENGTH = 16;
142
+ const MF_HASHED_NAME_PREFIX_LENGTH = MF_HASHED_NAME_THRESHOLD - MF_HASHED_NAME_HASH_LENGTH;
143
+ const mfHashedNameMap = /* @__PURE__ */ new Map();
139
144
  /**
140
- * Encodes a package name into a valid file name.
141
- * @param {string} name - The package name, e.g., "@scope/xx-xx.xx".
145
+ * Encodes a package name (or shared-module specifier, which may include a
146
+ * deep import subpath) into a valid file name, falling back to a
147
+ * readable-prefix + content-hash id when the plain encoding would be too
148
+ * long for a filesystem path segment.
149
+ * @param {string} name - The package name or specifier, e.g., "@scope/xx-xx.xx" or "@scope/pkg/deep/sub-path".
142
150
  * @returns {string} - The encoded file name.
143
151
  */
144
152
  function packageNameEncode(name) {
145
153
  if (typeof name !== "string") throw createModuleFederationError("A string package name is required");
146
- return name.replace(/@/g, "_mf_0_").replace(/\//g, "_mf_1_").replace(/-/g, "_mf_2_").replace(/\./g, "_mf_3_");
154
+ const encoded = name.replace(/@/g, "_mf_0_").replace(/\//g, "_mf_1_").replace(/-/g, "_mf_2_").replace(/\./g, "_mf_3_");
155
+ if (encoded.length <= MF_HASHED_NAME_THRESHOLD) return encoded;
156
+ const hashedName = `${encoded.slice(0, MF_HASHED_NAME_PREFIX_LENGTH)}${createHash("sha256").update(name).digest("hex").slice(0, MF_HASHED_NAME_HASH_LENGTH)}`;
157
+ mfHashedNameMap.set(hashedName, name);
158
+ return hashedName;
147
159
  }
148
160
  /**
149
- * Decodes an encoded file name back to the original package name.
161
+ * Decodes an encoded file name back to the original package name or
162
+ * shared-module specifier, whether it was plainly substituted or hashed
163
+ * down by `packageNameEncode`.
150
164
  * @param {string} encoded - The encoded file name, e.g., "_mf_0_scope_mf_1_xx_mf_2_xx_mf_3_xx".
151
- * @returns {string} - The decoded package name.
165
+ * @returns {string} - The decoded package name or specifier.
152
166
  */
153
167
  function packageNameDecode(encoded) {
154
168
  if (typeof encoded !== "string") throw createModuleFederationError("A string encoded file name is required");
169
+ const original = mfHashedNameMap.get(encoded);
170
+ if (original !== void 0) return original;
155
171
  return encoded.replace(/_mf_0_/g, "@").replace(/_mf_1_/g, "/").replace(/_mf_2_/g, "-").replace(/_mf_3_/g, ".");
156
172
  }
157
173
  /**
package/lib/index.d.ts CHANGED
@@ -126,6 +126,13 @@ type ModuleFederationOptions = {
126
126
  * add any other Node-only packages that should not be bundled into the SSR entry.
127
127
  */
128
128
  ssrExternals?: string[];
129
+ /**
130
+ * Options for the auto-injected `@module-federation/vite/ssrEntryLoader`.
131
+ * When omitted, the loader uses the `'temp-file'` strategy. Set
132
+ * `strategy: 'vm'` to opt into `vm.SourceTextModule` evaluation while keeping
133
+ * the computed `resolvedShared` map.
134
+ */
135
+ ssrEntryLoader?: SsrEntryLoaderConfig;
129
136
  /**
130
137
  * Experimental Module Federation capabilities.
131
138
  *
@@ -141,13 +148,28 @@ interface PluginExperimentsOptions {
141
148
  */
142
149
  externalRuntime?: boolean;
143
150
  /**
144
- * Pure-consumer only (no `exposes`). Injects a local runtime plugin that
145
- * publishes `runtime-core` on `globalThis._FEDERATION_RUNTIME_CORE`.
151
+ * Injects a local runtime plugin that publishes `runtime-core` on
152
+ * `globalThis._FEDERATION_RUNTIME_CORE`. Set it on exactly one container
153
+ * per page; that container may also `exposes`.
146
154
  */
147
155
  provideExternalRuntime?: boolean;
148
156
  /** Generate the React SSR/hydration island capability for eligible exposes. */
149
157
  ssrMode?: 'ISLAND';
150
158
  }
159
+ type SsrEntryLoaderStrategy = 'temp-file' | 'vm';
160
+ type SsrEntryLoaderConfig = {
161
+ /**
162
+ * How the auto-injected `@module-federation/vite/ssrEntryLoader` evaluates
163
+ * remote SSR entries.
164
+ *
165
+ * - `'temp-file'` (default when omitted): fetch the ESM graph, rewrite
166
+ * specifiers, write temp files and `import()` them.
167
+ * - `'vm'`: evaluate the graph with `vm.SourceTextModule`. Requires
168
+ * `--experimental-vm-modules`; the loader emits a single warning and
169
+ * falls back to `'temp-file'` when that API is unavailable.
170
+ */
171
+ strategy?: SsrEntryLoaderStrategy;
172
+ };
151
173
  type HostInitInjectLocationOptions = 'entry' | 'html';
152
174
  interface PluginDevOptions {
153
175
  disableLiveReload?: boolean;
@@ -227,4 +249,4 @@ interface DtsHostOptions {
227
249
  declare function federation(mfUserOptions: ModuleFederationOptions): any[];
228
250
  declare function createModuleFederationConfig<T extends ModuleFederationOptions>(options: T): T;
229
251
  //#endregion
230
- export { type ModuleFederationOptions, type PluginExperimentsOptions, type PluginManifestOptions, type TreeShakingConfig, createModuleFederationConfig, federation };
252
+ export { type ModuleFederationOptions, type PluginExperimentsOptions, type PluginManifestOptions, type SsrEntryLoaderConfig, type SsrEntryLoaderStrategy, type TreeShakingConfig, createModuleFederationConfig, federation };