@module-federation/vite 1.17.0 → 1.17.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.
@@ -262,6 +262,7 @@ const sharedCacheHelperCode = `const __mfGetSharedCacheDescriptor = (pkg, single
262
262
  if (value !== undefined) return value;
263
263
  const aliases = descriptor.aliases || [];
264
264
  for (const alias of aliases) {
265
+ if (!Object.prototype.hasOwnProperty.call(cache, alias)) continue;
265
266
  const aliasValue = cache[alias];
266
267
  if (aliasValue !== undefined) {
267
268
  cache[descriptor.canonical] = aliasValue;
@@ -270,11 +271,60 @@ const sharedCacheHelperCode = `const __mfGetSharedCacheDescriptor = (pkg, single
270
271
  }
271
272
  return undefined;
272
273
  };
273
- const __mfWriteSharedCache = (cache, descriptor, value) => {
274
+ const __mfSharedCacheListenersKey = Symbol.for("module-federation.shared-cache-listeners");
275
+ const __mfGetSharedCacheListeners = (cache) => {
276
+ let listeners = cache[__mfSharedCacheListenersKey];
277
+ if (listeners === undefined) {
278
+ listeners = Object.create(null);
279
+ Object.defineProperty(cache, __mfSharedCacheListenersKey, {
280
+ value: listeners,
281
+ enumerable: false,
282
+ configurable: false,
283
+ writable: false
284
+ });
285
+ }
286
+ return listeners;
287
+ };
288
+ const __mfSubscribeSharedCache = (cache, descriptor, listener) => {
289
+ const listeners = __mfGetSharedCacheListeners(cache);
290
+ (listeners[descriptor.canonical] ||= new Set()).add(listener);
291
+ };
292
+ const __mfSharedCacheOwnersKey = Symbol.for("module-federation.shared-cache-owners");
293
+ const __mfGetSharedCacheOwners = (cache) => {
294
+ let owners = cache[__mfSharedCacheOwnersKey];
295
+ if (owners === undefined) {
296
+ owners = Object.create(null);
297
+ Object.defineProperty(cache, __mfSharedCacheOwnersKey, {
298
+ value: owners,
299
+ enumerable: false,
300
+ configurable: false,
301
+ writable: false
302
+ });
303
+ }
304
+ return owners;
305
+ };
306
+ const __mfReadSharedCacheOwner = (cache, descriptor) =>
307
+ cache[__mfSharedCacheOwnersKey]?.[descriptor.canonical];
308
+ const __mfWriteSharedCache = (cache, descriptor, value, owner) => {
274
309
  cache[descriptor.canonical] = value;
275
310
  const aliases = descriptor.aliases || [];
276
311
  for (const alias of aliases) {
277
- if (cache[alias] === undefined) cache[alias] = value;
312
+ Object.defineProperty(cache, alias, {
313
+ value,
314
+ enumerable: true,
315
+ configurable: true,
316
+ writable: true
317
+ });
318
+ }
319
+ const owners = cache[__mfSharedCacheOwnersKey];
320
+ if (owner === undefined) {
321
+ if (owners) delete owners[descriptor.canonical];
322
+ } else {
323
+ __mfGetSharedCacheOwners(cache)[descriptor.canonical] = owner;
324
+ }
325
+ const listeners = cache[__mfSharedCacheListenersKey]?.[descriptor.canonical];
326
+ if (listeners) {
327
+ for (const listener of listeners) listener(value);
278
328
  }
279
329
  return value;
280
330
  };
@@ -0,0 +1,553 @@
1
+ //#region src/utils/fetchWithTimeout.ts
2
+ const DEFAULT_SSR_FETCH_TIMEOUT_MS = 1e4;
3
+ /** Fetch with a bounded wait. Set timeoutMs to 0 to disable the timeout. */
4
+ function fetchWithTimeout(input, init = {}, timeoutMs = DEFAULT_SSR_FETCH_TIMEOUT_MS) {
5
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return fetch(input, init);
6
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
7
+ const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
8
+ return fetch(input, {
9
+ ...init,
10
+ signal
11
+ });
12
+ }
13
+ //#endregion
14
+ //#region src/utils/ssrEntryLoader.ts
15
+ /**
16
+ * MF runtime plugin that intercepts the `loadEntry` lifecycle hook on the
17
+ * server and loads the SSR-compatible remote entry instead of the browser one.
18
+ *
19
+ * This completely replaces the need for any `@module-federation/sdk` patches.
20
+ * The `loadEntry` hook is emitted by `runtime-core` before it falls through to
21
+ * `loadScriptNode` — if the hook returns a value, the runtime uses it directly.
22
+ *
23
+ * Strategy:
24
+ * - In Node (detected through process.versions.node), fetch the remote's mf-manifest.json
25
+ * to discover the ssrRemoteEntry URL and its type.
26
+ * - ESM entry: use a dynamic `import()` — the SSR entry has no browser
27
+ * globals and all shared packages are external.
28
+ * - Dev mode (Vite 8+ only): use `ModuleRunner` with an HTTP transport backed
29
+ * by the remote's `/__mf_runner__` endpoint. This fetches fully-transformed
30
+ * module source through Vite's plugin pipeline, avoiding serialisation which
31
+ * cannot faithfully represent React components or closures.
32
+ *
33
+ * Dev mode on Vite < 8 is NOT supported — `ModuleRunner` and
34
+ * `FetchableDevEnvironment` are Vite 8+ APIs. If you need dev-mode SSR on
35
+ * an older Vite version, implement an alternative loader in `loadSSRRemoteEntry`
36
+ * for the `isDevSsrEntry` branch and expose a corresponding server endpoint
37
+ * from `pluginSSRRemoteEntry.configureServer`.
38
+ *
39
+ * Exported as a plain factory function so it can be serialised into the
40
+ * generated runtimePlugins list in virtualRemotes.ts.
41
+ */
42
+ const importCache = /* @__PURE__ */ new Map();
43
+ async function nodeImport(id) {
44
+ if (!importCache.has(id)) importCache.set(id, import(
45
+ /* @vite-ignore */
46
+ id
47
+ ));
48
+ return importCache.get(id);
49
+ }
50
+ const isNodeServer = () => typeof globalThis.process?.versions?.node === "string";
51
+ const runnerCache = /* @__PURE__ */ new Map();
52
+ /**
53
+ * Import `vite/module-runner` dynamically. Returns null on Vite < 8 where the
54
+ * subpath doesn't exist. Uses a plain dynamic import (not nodeImport) so that
55
+ * Vitest can intercept it with vi.mock in tests.
56
+ */
57
+ async function getModuleRunnerModule() {
58
+ try {
59
+ return await import("vite/module-runner");
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+ /**
65
+ * Create a ModuleRunner that fetches modules from a remote Vite dev server's
66
+ * `/__mf_runner__` endpoint. Each HTTP POST carries a `fetchModule` invoke
67
+ * payload; the remote responds with the transformed module source as JSON.
68
+ *
69
+ * This is Vite 8+ only — older versions don't expose `vite/module-runner` or
70
+ * the `/__mf_runner__` proxy endpoint.
71
+ */
72
+ async function getOrCreateRunner(remoteOrigin, fetchTimeoutMs) {
73
+ const cacheKey = `${fetchTimeoutMs}::${remoteOrigin}`;
74
+ if (runnerCache.has(cacheKey)) return runnerCache.get(cacheKey);
75
+ const promise = (async () => {
76
+ const viteRunner = await getModuleRunnerModule();
77
+ if (!viteRunner) return null;
78
+ const { ModuleRunner, ESModulesEvaluator } = viteRunner;
79
+ const runnerEndpoint = `${remoteOrigin}/__mf_runner__`;
80
+ try {
81
+ return new ModuleRunner({
82
+ hmr: false,
83
+ transport: { async invoke(payload) {
84
+ return await (await fetchWithTimeout(runnerEndpoint, {
85
+ method: "POST",
86
+ headers: { "Content-Type": "application/json" },
87
+ body: JSON.stringify(payload)
88
+ }, fetchTimeoutMs)).json();
89
+ } }
90
+ }, new ESModulesEvaluator());
91
+ } catch {
92
+ return null;
93
+ }
94
+ })();
95
+ runnerCache.set(cacheKey, promise);
96
+ return promise;
97
+ }
98
+ const _path = () => nodeImport("path");
99
+ const _fs = () => nodeImport("fs");
100
+ const _crypto = () => nodeImport("crypto");
101
+ const _module = () => nodeImport("module");
102
+ /**
103
+ * Version key for a resolved SSR entry. Derived from the remote's manifest
104
+ * content so a redeploy at the same URL produces a different key, which in
105
+ * turn produces different temp-file names — busting both our caches and
106
+ * Node's ESM module cache. Convention-resolved entries (no manifest) get a
107
+ * stable placeholder key and cannot be revalidated automatically.
108
+ */
109
+ const UNVERSIONED = "unversioned";
110
+ function hashString(value) {
111
+ let hash = 2166136261;
112
+ for (let i = 0; i < value.length; i++) {
113
+ hash ^= value.charCodeAt(i);
114
+ hash = Math.imul(hash, 16777619);
115
+ }
116
+ return (hash >>> 0).toString(16).padStart(8, "0");
117
+ }
118
+ function computeManifestVersionKey(manifest) {
119
+ const buildVersion = manifest.metaData?.buildInfo?.buildVersion;
120
+ const contentHash = hashString(JSON.stringify(manifest));
121
+ return buildVersion ? `${buildVersion}-${contentHash}` : contentHash;
122
+ }
123
+ const ssrEntryCache = /* @__PURE__ */ new Map();
124
+ const manifestFetchCache = /* @__PURE__ */ new Map();
125
+ function makeUrlCacheKey(url, fetchTimeoutMs) {
126
+ return `${fetchTimeoutMs}::${url}`;
127
+ }
128
+ var SsrEntryHttpError = class extends Error {
129
+ constructor(url, status, statusText, bodyPreview) {
130
+ super(`Failed to fetch SSR module "${url}": ${status} ${statusText}` + (bodyPreview ? `\npreview: ${bodyPreview}` : ""));
131
+ this.url = url;
132
+ this.status = status;
133
+ this.statusText = statusText;
134
+ this.bodyPreview = bodyPreview;
135
+ this.name = "SsrEntryHttpError";
136
+ }
137
+ };
138
+ function getBodyPreview(body) {
139
+ return body.slice(0, 240).replace(/\s+/g, " ").trim();
140
+ }
141
+ function isSsrEntryHttpError(error) {
142
+ return error instanceof SsrEntryHttpError;
143
+ }
144
+ async function fetchManifest(manifestUrl, fetchTimeoutMs) {
145
+ try {
146
+ const res = await fetchWithTimeout(manifestUrl, {}, fetchTimeoutMs);
147
+ if (!res.ok) return null;
148
+ return await res.json();
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
153
+ async function fetchManifestCached(manifestUrl, fetchTimeoutMs) {
154
+ const cacheKey = makeUrlCacheKey(manifestUrl, fetchTimeoutMs);
155
+ if (!manifestFetchCache.has(cacheKey)) {
156
+ const promise = fetchManifest(manifestUrl, fetchTimeoutMs);
157
+ manifestFetchCache.set(cacheKey, promise);
158
+ promise.then((manifest) => {
159
+ if (!manifest && manifestFetchCache.get(cacheKey) === promise) manifestFetchCache.delete(cacheKey);
160
+ });
161
+ }
162
+ return manifestFetchCache.get(cacheKey);
163
+ }
164
+ /** True when the host configured a manifest URL as the remote entry (any .json name). */
165
+ function isManifestEntry(remoteEntryUrl) {
166
+ try {
167
+ const { pathname } = new URL(remoteEntryUrl);
168
+ return /\.json$/i.test(pathname);
169
+ } catch {
170
+ return /\.json(?:[?#]|$)/i.test(remoteEntryUrl);
171
+ }
172
+ }
173
+ function isSsrEntry(remoteEntryUrl) {
174
+ return /\.ssr\.js(?:[?#].*)?$/.test(remoteEntryUrl);
175
+ }
176
+ function getManifestUrl(remoteEntryUrl) {
177
+ if (isManifestEntry(remoteEntryUrl)) return remoteEntryUrl;
178
+ return remoteEntryUrl.replace(/\/[^/]+$/, "/mf-manifest.json");
179
+ }
180
+ function getEntryFilename(entryUrl) {
181
+ return entryUrl.split("/").pop()?.replace(/[?#].*$/, "").replace(/\.[^.]+$/, "") ?? "remoteEntry";
182
+ }
183
+ function resolveEntryAssetUrl(entry, manifestUrl) {
184
+ const base = manifestUrl.replace(/\/[^/]+$/, "/");
185
+ return new URL(`${entry.path || ""}${entry.name}`, base).href;
186
+ }
187
+ function resolveSSREntryUrl(manifest, manifestUrl) {
188
+ const meta = manifest?.metaData;
189
+ if (!meta?.ssrRemoteEntry?.name) return null;
190
+ const base = manifestUrl.replace(/\/[^/]+$/, "/");
191
+ const entryPath = (meta.ssrRemoteEntry.path || "") + meta.ssrRemoteEntry.name;
192
+ return {
193
+ url: new URL(entryPath, base).href,
194
+ type: meta.ssrRemoteEntry.type || "module",
195
+ versionKey: computeManifestVersionKey(manifest)
196
+ };
197
+ }
198
+ /**
199
+ * Derive the SSR entry URL by convention when no manifest is available.
200
+ * remoteEntry.js → remoteEntry.ssr.js
201
+ * remoteEntry.js → /__mf_ssr__/remoteEntry.ssr.js (dev middleware)
202
+ * Returns the first URL that responds with a 200.
203
+ */
204
+ async function headCheckSsrEntry(candidate, fetchTimeoutMs) {
205
+ try {
206
+ const res = await fetchWithTimeout(candidate.url, { method: "HEAD" }, fetchTimeoutMs);
207
+ const ct = res.headers.get("content-type") ?? "";
208
+ if (res.ok && !ct.includes("text/html")) return candidate;
209
+ } catch {}
210
+ return null;
211
+ }
212
+ function resolveAssetBaseUrl(entryUrl, manifest, manifestUrl) {
213
+ const remoteEntry = manifest?.metaData?.remoteEntry;
214
+ if (remoteEntry?.name) return resolveEntryAssetUrl(remoteEntry, manifestUrl);
215
+ if (!isManifestEntry(entryUrl)) return entryUrl;
216
+ return new URL("remoteEntry.js", manifestUrl.replace(/\/[^/]+$/, "/")).href;
217
+ }
218
+ async function buildEntryContext(entryUrl, fetchTimeoutMs) {
219
+ const manifestUrl = getManifestUrl(entryUrl);
220
+ const manifest = await fetchManifestCached(manifestUrl, fetchTimeoutMs);
221
+ const assetBaseUrl = resolveAssetBaseUrl(entryUrl, manifest, manifestUrl);
222
+ return {
223
+ entryUrl,
224
+ manifestUrl,
225
+ manifest,
226
+ assetBaseUrl,
227
+ filename: getEntryFilename(assetBaseUrl),
228
+ remoteOrigin: assetBaseUrl.replace(/\/[^/]+$/, "")
229
+ };
230
+ }
231
+ function buildSsrEntryCandidates(ctx, options = {}) {
232
+ const { assetBaseUrl, filename, remoteOrigin } = ctx;
233
+ const base = assetBaseUrl.replace(/\.[^.]+$/, "");
234
+ const candidates = [];
235
+ if (!options.skipServerBuild) candidates.push({
236
+ url: `${remoteOrigin}/__mf_server__/${filename}.ssr.js`,
237
+ type: "module",
238
+ versionKey: UNVERSIONED
239
+ });
240
+ candidates.push({
241
+ url: `${base}.ssr.js`,
242
+ type: "module",
243
+ versionKey: UNVERSIONED
244
+ }, {
245
+ url: `${remoteOrigin}/__mf_ssr__/${filename}.ssr.js`,
246
+ type: "module",
247
+ versionKey: UNVERSIONED
248
+ });
249
+ return candidates;
250
+ }
251
+ async function resolveFirstReachableCandidate(candidates, fetchTimeoutMs) {
252
+ for (const candidate of candidates) {
253
+ const hit = await headCheckSsrEntry(candidate, fetchTimeoutMs);
254
+ if (hit) return hit;
255
+ }
256
+ return null;
257
+ }
258
+ async function resolveSSREntryImpl(remoteEntryUrl, fetchTimeoutMs) {
259
+ if (isSsrEntry(remoteEntryUrl)) return {
260
+ url: remoteEntryUrl,
261
+ type: "module",
262
+ versionKey: UNVERSIONED
263
+ };
264
+ if (!isManifestEntry(remoteEntryUrl)) {
265
+ const filename = getEntryFilename(remoteEntryUrl);
266
+ const fromServerBuild = await headCheckSsrEntry({
267
+ url: `${remoteEntryUrl.replace(/\/[^/]+$/, "")}/__mf_server__/${filename}.ssr.js`,
268
+ type: "module",
269
+ versionKey: UNVERSIONED
270
+ }, fetchTimeoutMs);
271
+ if (fromServerBuild) return fromServerBuild;
272
+ }
273
+ const ctx = await buildEntryContext(remoteEntryUrl, fetchTimeoutMs);
274
+ if (ctx.manifest) {
275
+ const fromManifest = resolveSSREntryUrl(ctx.manifest, ctx.manifestUrl);
276
+ if (fromManifest) return fromManifest;
277
+ }
278
+ return resolveFirstReachableCandidate(buildSsrEntryCandidates(ctx, { skipServerBuild: !isManifestEntry(remoteEntryUrl) }), fetchTimeoutMs);
279
+ }
280
+ function setSsrEntryCache(remoteEntryUrl, fetchTimeoutMs) {
281
+ const cacheKey = makeUrlCacheKey(remoteEntryUrl, fetchTimeoutMs);
282
+ const record = {
283
+ promise: resolveSSREntryImpl(remoteEntryUrl, fetchTimeoutMs),
284
+ resolvedAt: Date.now()
285
+ };
286
+ ssrEntryCache.set(cacheKey, record);
287
+ record.promise.then((entry) => {
288
+ if (!entry && ssrEntryCache.get(cacheKey) === record) ssrEntryCache.delete(cacheKey);
289
+ });
290
+ return record;
291
+ }
292
+ async function getSSREntry(remoteEntryUrl, maxAgeMs, fetchTimeoutMs) {
293
+ const cacheKey = makeUrlCacheKey(remoteEntryUrl, fetchTimeoutMs);
294
+ const cached = ssrEntryCache.get(cacheKey);
295
+ if (!cached) return setSsrEntryCache(remoteEntryUrl, fetchTimeoutMs).promise;
296
+ if (!(typeof maxAgeMs === "number" && maxAgeMs >= 0 && Date.now() - cached.resolvedAt >= maxAgeMs)) return cached.promise;
297
+ const previous = await cached.promise.catch(() => null);
298
+ manifestFetchCache.delete(makeUrlCacheKey(getManifestUrl(remoteEntryUrl), fetchTimeoutMs));
299
+ const record = setSsrEntryCache(remoteEntryUrl, fetchTimeoutMs);
300
+ const next = await record.promise.catch(() => null);
301
+ if (previous && next && previous.versionKey !== next.versionKey) dropRemoteCaches(remoteEntryUrl);
302
+ return record.promise;
303
+ }
304
+ /**
305
+ * Drop per-remote caches after a version change so old artifacts stop being
306
+ * reused. Temp-file cache keys hold SSR entry/chunk URLs (not the browser
307
+ * entry URL), so scope the invalidation by origin.
308
+ */
309
+ function dropRemoteCaches(remoteEntryUrl) {
310
+ let origin;
311
+ try {
312
+ origin = new URL(remoteEntryUrl).origin;
313
+ } catch {
314
+ return;
315
+ }
316
+ for (const [key] of tempFileCache) if (JSON.parse(key)[2].startsWith(origin)) {
317
+ tempFileCache.delete(key);
318
+ tempFilePathCache.delete(key);
319
+ }
320
+ }
321
+ /**
322
+ * Drop the loader's caches so the next `loadEntry` re-resolves and re-fetches
323
+ * remote SSR entries. Pass a remote entry URL to scope the invalidation to one
324
+ * remote; call with no arguments to invalidate everything.
325
+ *
326
+ * Note: the MF runtime keeps its own container/module caches per federation
327
+ * instance. This function best-effort clears the module caches of all global
328
+ * federation instances so re-renders load fresh remote modules, but hosts that
329
+ * hold direct references to previously loaded modules keep those references.
330
+ */
331
+ function revalidate(remoteEntryUrl) {
332
+ if (remoteEntryUrl) {
333
+ for (const key of ssrEntryCache.keys()) if (key.endsWith(`::${remoteEntryUrl}`)) ssrEntryCache.delete(key);
334
+ const manifestUrl = getManifestUrl(remoteEntryUrl);
335
+ for (const key of manifestFetchCache.keys()) if (key.endsWith(`::${manifestUrl}`)) manifestFetchCache.delete(key);
336
+ dropRemoteCaches(remoteEntryUrl);
337
+ } else {
338
+ ssrEntryCache.clear();
339
+ manifestFetchCache.clear();
340
+ tempFileCache.clear();
341
+ tempFilePathCache.clear();
342
+ }
343
+ const federation = globalThis.__FEDERATION__;
344
+ for (const instance of federation?.__INSTANCES__ ?? []) try {
345
+ instance?.moduleCache?.clear?.();
346
+ } catch {}
347
+ }
348
+ const tempFileCache = /* @__PURE__ */ new Map();
349
+ const tempFilePathCache = /* @__PURE__ */ new Map();
350
+ let ssrCacheDirPromise;
351
+ async function getSSRCacheDir() {
352
+ if (!ssrCacheDirPromise) ssrCacheDirPromise = (async () => {
353
+ const { join } = await _path();
354
+ const { rmSync } = await _fs();
355
+ const dir = join(process.cwd(), "node_modules", ".ssr-cache");
356
+ process.once("exit", () => {
357
+ try {
358
+ rmSync(dir, {
359
+ recursive: true,
360
+ force: true
361
+ });
362
+ } catch {}
363
+ });
364
+ return dir;
365
+ })();
366
+ return ssrCacheDirPromise;
367
+ }
368
+ /**
369
+ * Neutralize browser-only preload machinery in Vite/Rolldown output so the
370
+ * code can evaluate in Node. Shared by the temp-file and vm strategies.
371
+ */
372
+ function neutralizeBrowserPreloadHelpers(code) {
373
+ code = code.replace(/import\s*\{([^}]*)\}\s*from\s*["'][^"']*preload-helper[^"']*["'];?/g, (_m, bindings) => {
374
+ return bindings.split(",").map((b) => {
375
+ const parts = b.trim().split(/\s+as\s+/);
376
+ return (parts[1] ?? parts[0]).trim();
377
+ }).filter(Boolean).map((l) => `const ${l} = (fn) => fn();`).join("\n");
378
+ });
379
+ code = code.replace(/__vite__mapDeps\([^)]+\)/g, "[]");
380
+ code = code.replace(/\b([A-Za-z_$][\w$]*)\s*\(\s*\(\s*\)\s*=>\s*import\(([^)]*)\)\s*,\s*\[\]\s*\)/g, "import($2)");
381
+ return code;
382
+ }
383
+ function transformSsrCode(code, base, sharedPkgMap) {
384
+ code = code.replace(/((?:from|export\s*\*\s*from)\s*)(["'`])(\.\.?\/[^"'`\s][^"'`]*)["'`]/g, (_m, prefix, _q, specifier) => `${prefix}"${new URL(specifier, base).href}"`);
385
+ code = code.replace(/(import\s*)(["'`])(\.\.?\/[^"'`\s][^"'`]*)["'`]/g, (_m, prefix, _q, specifier) => `${prefix}"${new URL(specifier, base).href}"`);
386
+ code = code.replace(/(import\s*\(\s*)(["'`])(\.\.?\/[^"'`\s][^"'`]*)["'`](\s*\))/g, (_m, prefix, _q, specifier, suffix) => `${prefix}"${new URL(specifier, base).href}"${suffix}`);
387
+ if (sharedPkgMap && sharedPkgMap.size > 0) code = code.replace(/(?:from|import\s*\()\s*(["'`])([^"'`./][^"'`]*)["'`]/g, (m, _q, specifier) => {
388
+ const resolved = sharedPkgMap.get(specifier);
389
+ return resolved ? m.replace(specifier, `file://${resolved}`) : m;
390
+ });
391
+ return neutralizeBrowserPreloadHelpers(code);
392
+ }
393
+ function isVitePreloadHelperSpecifier(specifier) {
394
+ return specifier.includes("preload-helper");
395
+ }
396
+ /**
397
+ * Fetch an HTTP ESM module, transform it, write it to a temp .js file and
398
+ * return the file path. Recursively does the same for HTTP transitive imports
399
+ * so that `import('file:///...temp.js')` can resolve them.
400
+ *
401
+ * `versionKey` participates in both the cache key and the temp file name, so
402
+ * a remote redeploy (new manifest → new key) produces new files and bypasses
403
+ * Node's ESM module cache instead of serving the stale build.
404
+ */
405
+ async function fetchEsmToTempFile(url, tmpDir, visited, pending, sharedPkgMap, versionKey = UNVERSIONED, fetchTimeoutMs = DEFAULT_SSR_FETCH_TIMEOUT_MS) {
406
+ const cacheKey = JSON.stringify([
407
+ fetchTimeoutMs,
408
+ versionKey,
409
+ url
410
+ ]);
411
+ if (visited.has(url)) return visited.get(url);
412
+ const cached = tempFileCache.get(cacheKey);
413
+ if (cached) {
414
+ pending.add(cached);
415
+ const tmpFile = await tempFilePathCache.get(cacheKey);
416
+ visited.set(url, tmpFile);
417
+ return tmpFile;
418
+ }
419
+ const tmpFilePromise = (async () => {
420
+ const { createHash } = await _crypto();
421
+ const { join } = await _path();
422
+ return join(tmpDir, `${createHash("sha1").update(cacheKey).digest("hex").slice(0, 12)}.js`);
423
+ })();
424
+ tempFilePathCache.set(cacheKey, tmpFilePromise);
425
+ const promise = (async () => {
426
+ const tmpFile = await tmpFilePromise;
427
+ visited.set(url, tmpFile);
428
+ const res = await fetchWithTimeout(url, {}, fetchTimeoutMs);
429
+ let code = await res.text();
430
+ if (!res.ok) throw new SsrEntryHttpError(url, res.status, res.statusText, getBodyPreview(code));
431
+ const base = url.replace(/\/[^/]*$/, "/");
432
+ const relImports = [];
433
+ const relRegex = /(?:from|export\s*\*\s*from|import\s*(?:\(|\s))\s*["'`]([^"'`\s]+)["'`]/g;
434
+ let m;
435
+ while ((m = relRegex.exec(code)) !== null) if ((m[1].startsWith("./") || m[1].startsWith("../")) && !isVitePreloadHelperSpecifier(m[1])) relImports.push(new URL(m[1], base).href);
436
+ const subMap = /* @__PURE__ */ new Map();
437
+ await Promise.all([...new Set(relImports)].filter((u) => u.startsWith("http://") || u.startsWith("https://")).map(async (u) => {
438
+ const tmpPath = await fetchEsmToTempFile(u, tmpDir, visited, pending, sharedPkgMap, versionKey, fetchTimeoutMs);
439
+ subMap.set(u, `file://${tmpPath}`);
440
+ }));
441
+ code = transformSsrCode(code, base, sharedPkgMap);
442
+ for (const [httpUrl, fileUrl] of subMap) code = code.split(httpUrl).join(fileUrl);
443
+ const { writeFileSync } = await _fs();
444
+ writeFileSync(tmpFile, code, "utf8");
445
+ return tmpFile;
446
+ })();
447
+ tempFileCache.set(cacheKey, promise);
448
+ pending.add(promise);
449
+ promise.catch(() => {
450
+ if (tempFileCache.get(cacheKey) === promise) tempFileCache.delete(cacheKey);
451
+ if (tempFilePathCache.get(cacheKey) === tmpFilePromise) tempFilePathCache.delete(cacheKey);
452
+ });
453
+ return promise;
454
+ }
455
+ async function fetchEsmGraphToTempFile(url, tmpDir, sharedPkgMap, versionKey = UNVERSIONED, fetchTimeoutMs = DEFAULT_SSR_FETCH_TIMEOUT_MS) {
456
+ const pending = /* @__PURE__ */ new Set();
457
+ const rootFile = await fetchEsmToTempFile(url, tmpDir, /* @__PURE__ */ new Map(), pending, sharedPkgMap, versionKey, fetchTimeoutMs);
458
+ await Promise.all(pending);
459
+ return rootFile;
460
+ }
461
+ async function importTempModule(filePath, versionKey) {
462
+ return await import(`${filePath}?v=${encodeURIComponent(versionKey)}`);
463
+ }
464
+ let warnedVmUnavailable = false;
465
+ async function tryVmStrategy(ssrEntry, options) {
466
+ const { loadViaVmStrategy, isVmStrategyAvailable } = await import("./ssrVmStrategy-By_N71Dl.js");
467
+ if (!await isVmStrategyAvailable()) {
468
+ if (!warnedVmUnavailable) {
469
+ warnedVmUnavailable = true;
470
+ console.warn("[mf-vite:ssr-entry-loader] strategy \"vm\" requires vm.SourceTextModule (run Node with --experimental-vm-modules); falling back to the temp-file strategy.");
471
+ }
472
+ return null;
473
+ }
474
+ return await loadViaVmStrategy(ssrEntry.url, {
475
+ resolvedShared: options.resolvedShared,
476
+ shareScopeName: options.shareScopeName,
477
+ versionKey: ssrEntry.versionKey,
478
+ fetchTimeoutMs: options.fetchTimeoutMs
479
+ });
480
+ }
481
+ async function loadSSRRemoteEntry(ssrEntry, options) {
482
+ const { url, type, versionKey } = ssrEntry;
483
+ const { resolvedShared } = options;
484
+ if (type === "commonjs-module" || type === "commonjs") {
485
+ const { createRequire } = await _module();
486
+ const req = createRequire(import.meta.url);
487
+ try {
488
+ return req(url);
489
+ } catch {}
490
+ }
491
+ if (url.startsWith("http://") || url.startsWith("https://")) {
492
+ const urlObj = new URL(url);
493
+ if (urlObj.pathname.includes("/__mf_ssr__/")) {
494
+ const remoteOrigin = urlObj.origin;
495
+ const runner = await getOrCreateRunner(remoteOrigin, options.fetchTimeoutMs);
496
+ if (!runner) {
497
+ if (process.env.NODE_ENV !== "production") return null;
498
+ } else try {
499
+ const mod = await runner.import(urlObj.pathname);
500
+ if (mod && typeof mod === "object" && "init" in mod) return mod;
501
+ if (process.env.NODE_ENV !== "production") return null;
502
+ } catch {
503
+ if (process.env.NODE_ENV !== "production") return null;
504
+ }
505
+ }
506
+ if (options.strategy === "vm") try {
507
+ const fromVm = await tryVmStrategy(ssrEntry, options);
508
+ if (fromVm) return fromVm;
509
+ } catch (error) {
510
+ if (isSsrEntryHttpError(error)) throw error;
511
+ }
512
+ const { mkdirSync } = await _fs();
513
+ const cacheDir = await getSSRCacheDir();
514
+ mkdirSync(cacheDir, { recursive: true });
515
+ const sharedPkgMap = new Map(Object.entries(resolvedShared));
516
+ try {
517
+ return await importTempModule(await fetchEsmGraphToTempFile(url, cacheDir, sharedPkgMap, versionKey, options.fetchTimeoutMs), versionKey);
518
+ } catch (error) {
519
+ if (isSsrEntryHttpError(error)) throw error;
520
+ return null;
521
+ }
522
+ }
523
+ try {
524
+ return await import(
525
+ /* @vite-ignore */
526
+ url
527
+ );
528
+ } catch {
529
+ return null;
530
+ }
531
+ }
532
+ function ssrEntryLoaderPlugin(options = {}) {
533
+ const resolved = {
534
+ resolvedShared: options.resolvedShared ?? {},
535
+ strategy: options.strategy ?? "temp-file",
536
+ shareScopeName: options.shareScopeName ?? "default",
537
+ maxAgeMs: options.maxAgeMs,
538
+ fetchTimeoutMs: options.fetchTimeoutMs ?? 1e4
539
+ };
540
+ return {
541
+ name: "mf-vite:ssr-entry-loader",
542
+ async loadEntry({ remoteInfo }) {
543
+ if (!isNodeServer()) return;
544
+ const ssrEntry = await getSSREntry(remoteInfo.entry, resolved.maxAgeMs, resolved.fetchTimeoutMs);
545
+ if (!ssrEntry) return;
546
+ const mod = await loadSSRRemoteEntry(ssrEntry, resolved);
547
+ if (!mod) return;
548
+ return mod;
549
+ }
550
+ };
551
+ }
552
+ //#endregion
553
+ export { DEFAULT_SSR_FETCH_TIMEOUT_MS as a, ssrEntryLoaderPlugin as i, neutralizeBrowserPreloadHelpers as n, fetchWithTimeout as o, revalidate as r, SsrEntryHttpError as t };
@@ -1,4 +1,4 @@
1
- import { SsrEntryHttpError, neutralizeBrowserPreloadHelpers } from "./utils/ssrEntryLoader.js";
1
+ import { n as neutralizeBrowserPreloadHelpers, o as fetchWithTimeout, t as SsrEntryHttpError } from "./ssrEntryLoader-D_sUES94.js";
2
2
  //#region src/utils/ssrVmStrategy.ts
3
3
  /**
4
4
  * vm.SourceTextModule strategy for loading remote SSR entries.
@@ -83,8 +83,8 @@ const namespaceCache = /* @__PURE__ */ new Map();
83
83
  function getBodyPreview(body) {
84
84
  return body.slice(0, 240).replace(/\s+/g, " ").trim();
85
85
  }
86
- async function fetchModuleSource(url) {
87
- const res = await fetch(url);
86
+ async function fetchModuleSource(url, fetchTimeoutMs) {
87
+ const res = await fetchWithTimeout(url, {}, fetchTimeoutMs);
88
88
  const text = await res.text();
89
89
  if (!res.ok) throw new SsrEntryHttpError(url, res.status, res.statusText, getBodyPreview(text));
90
90
  return neutralizeBrowserPreloadHelpers(text);
@@ -99,9 +99,13 @@ function resolveSpecifierUrl(specifier, referencerUrl) {
99
99
  return null;
100
100
  }
101
101
  function getHttpModule(vm, url, options) {
102
- const cacheKey = `${options.versionKey}::${url}`;
102
+ const cacheKey = JSON.stringify([
103
+ options.fetchTimeoutMs ?? 1e4,
104
+ options.versionKey,
105
+ url
106
+ ]);
103
107
  if (!httpModuleCache.has(cacheKey)) httpModuleCache.set(cacheKey, (async () => {
104
- const code = await fetchModuleSource(url);
108
+ const code = await fetchModuleSource(url, options.fetchTimeoutMs);
105
109
  return new vm.SourceTextModule(code, {
106
110
  identifier: url,
107
111
  initializeImportMeta(meta) {
@@ -100,6 +100,11 @@ interface SsrEntryLoaderOptions {
100
100
  * source.
101
101
  */
102
102
  maxAgeMs?: number;
103
+ /**
104
+ * Maximum time in milliseconds for each SSR network request. Defaults to
105
+ * 10 seconds. Set to `0` to disable the timeout.
106
+ */
107
+ fetchTimeoutMs?: number;
103
108
  }
104
109
  declare function ssrEntryLoaderPlugin(options?: SsrEntryLoaderOptions): {
105
110
  name: string;