@module-federation/vite 1.21.6 → 1.22.1

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 to reduce startup requests, on both Rolldown (Vite 8+) and Rollup (Vite 5–7).
372
393
 
373
394
  ### `codeSplitting.groups` (Vite 8+ / Rolldown)
374
395
 
@@ -1,7 +1,9 @@
1
+ import { l as getNodeModulesSuffix } from "./sharedKeyMatcher-BRzBCbuR.js";
1
2
  import { existsSync, readFileSync, readdirSync } from "fs";
2
3
  import { createRequire } from "module";
3
4
  import * as path$1 from "node:path";
4
5
  import { fileURLToPath, pathToFileURL } from "url";
6
+ import { createHash } from "node:crypto";
5
7
  //#region src/utils/logger.ts
6
8
  const MODULE_FEDERATION_LOG_PREFIX = "[Module Federation]";
7
9
  function formatModuleFederationMessage(message) {
@@ -128,6 +130,20 @@ function getPackageExportsTarget(pkg, packageName, exportsField) {
128
130
  if (subpath !== ".") return matchExportsSubpath(record, subpath);
129
131
  return record["."] ?? (!Object.keys(record).some((key) => key.startsWith(".")) ? record : void 0);
130
132
  }
133
+ /** Whether `pkg` can be located from `opts.cwd` (or the detection cwd). Aliased or non-hoisted packages may still resolve through Vite. */
134
+ function isPackageInstalled(pkg, opts = {}) {
135
+ return getInstalledPackageJson(getPackageName(pkg), opts) !== void 0;
136
+ }
137
+ /**
138
+ * Whether the installed package's `exports` field permits `pkg`. Lenient when the package cannot
139
+ * be found or has no `exports`: callers that need a hard "is installed" gate use `isPackageInstalled`.
140
+ */
141
+ function isPackageExportAvailable(pkg, opts = {}) {
142
+ const packageName = getPackageName(pkg);
143
+ const installed = getInstalledPackageJson(packageName, opts);
144
+ if (installed?.packageJson.exports == null) return true;
145
+ return resolveExportsEntry(getPackageExportsTarget(pkg, packageName, installed.packageJson.exports), opts.conditions) !== void 0;
146
+ }
131
147
  /**
132
148
  * Escaping rules:
133
149
  * Convert using the format __${mapping}__, where _ and $ are not allowed in npm package names but can be used in variable names.
@@ -136,22 +152,37 @@ function getPackageExportsTarget(pkg, packageName, exportsField) {
136
152
  * - => 3
137
153
  * . => 4
138
154
  */
155
+ const MF_HASHED_NAME_THRESHOLD = 90;
156
+ const MF_HASHED_NAME_HASH_LENGTH = 16;
157
+ const MF_HASHED_NAME_PREFIX_LENGTH = MF_HASHED_NAME_THRESHOLD - MF_HASHED_NAME_HASH_LENGTH;
158
+ const mfHashedNameMap = /* @__PURE__ */ new Map();
139
159
  /**
140
- * Encodes a package name into a valid file name.
141
- * @param {string} name - The package name, e.g., "@scope/xx-xx.xx".
160
+ * Encodes a package name (or shared-module specifier, which may include a
161
+ * deep import subpath) into a valid file name, falling back to a
162
+ * readable-prefix + content-hash id when the plain encoding would be too
163
+ * long for a filesystem path segment.
164
+ * @param {string} name - The package name or specifier, e.g., "@scope/xx-xx.xx" or "@scope/pkg/deep/sub-path".
142
165
  * @returns {string} - The encoded file name.
143
166
  */
144
167
  function packageNameEncode(name) {
145
168
  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_");
169
+ const encoded = name.replace(/@/g, "_mf_0_").replace(/\//g, "_mf_1_").replace(/-/g, "_mf_2_").replace(/\./g, "_mf_3_");
170
+ if (encoded.length <= MF_HASHED_NAME_THRESHOLD) return encoded;
171
+ const hashedName = `${encoded.slice(0, MF_HASHED_NAME_PREFIX_LENGTH)}${createHash("sha256").update(name).digest("hex").slice(0, MF_HASHED_NAME_HASH_LENGTH)}`;
172
+ mfHashedNameMap.set(hashedName, name);
173
+ return hashedName;
147
174
  }
148
175
  /**
149
- * Decodes an encoded file name back to the original package name.
176
+ * Decodes an encoded file name back to the original package name or
177
+ * shared-module specifier, whether it was plainly substituted or hashed
178
+ * down by `packageNameEncode`.
150
179
  * @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.
180
+ * @returns {string} - The decoded package name or specifier.
152
181
  */
153
182
  function packageNameDecode(encoded) {
154
183
  if (typeof encoded !== "string") throw createModuleFederationError("A string encoded file name is required");
184
+ const original = mfHashedNameMap.get(encoded);
185
+ if (original !== void 0) return original;
155
186
  return encoded.replace(/_mf_0_/g, "@").replace(/_mf_1_/g, "/").replace(/_mf_2_/g, "-").replace(/_mf_3_/g, ".");
156
187
  }
157
188
  /**
@@ -164,10 +195,9 @@ function getPackageName(packageString) {
164
195
  return match ? match[0] : packageString;
165
196
  }
166
197
  function getPackageNameFromNodeModulePath(source) {
167
- const normalized = source.replace(/\\/g, "/");
168
- const nodeModulesIndex = normalized.lastIndexOf("/node_modules/");
169
- if (nodeModulesIndex < 0) return;
170
- const parts = normalized.slice(nodeModulesIndex + 14).split("/");
198
+ const suffix = getNodeModulesSuffix(source);
199
+ if (!suffix) return;
200
+ const parts = suffix.split("/");
171
201
  if (!parts[0]) return;
172
202
  if (parts[0].startsWith("@")) return parts[1] ? `${parts[0]}/${parts[1]}` : void 0;
173
203
  return parts[0];
@@ -483,4 +513,4 @@ function hasPackageDependency(dependencyName, cwd = packageDetectionCwd || proce
483
513
  //#region src/utils/dtsConstants.ts
484
514
  const DEFAULT_PUBLIC_TYPES_FOLDER = "@mf-types";
485
515
  //#endregion
486
- export { mfError as _, getPackageDetectionCwd as a, getSharedCacheDescriptor as c, packageNameDecode as d, packageNameEncode as f, createModuleFederationError as g, sharedCacheHelperCode as h, getIsRolldown as i, hasPackageDependency as l, setPackageDetectionCwd as m, getInstalledPackageEntry as n, getPackageName as o, resolveImportPath as p, getInstalledPackageJson as r, getPackageNameFromNodeModulePath as s, DEFAULT_PUBLIC_TYPES_FOLDER as t, isNuxtProjectRoot as u, mfWarn as v };
516
+ export { sharedCacheHelperCode as _, getPackageDetectionCwd as a, mfWarn as b, getSharedCacheDescriptor as c, isPackageExportAvailable as d, isPackageInstalled as f, setPackageDetectionCwd as g, resolveImportPath as h, getIsRolldown as i, hasPackageDependency as l, packageNameEncode as m, getInstalledPackageEntry as n, getPackageName as o, packageNameDecode as p, getInstalledPackageJson as r, getPackageNameFromNodeModulePath as s, DEFAULT_PUBLIC_TYPES_FOLDER as t, isNuxtProjectRoot as u, createModuleFederationError as v, mfError as y };
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 };