@module-federation/vite 1.13.7 → 1.14.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/lib/index.cjs CHANGED
@@ -461,6 +461,191 @@ function PluginDevProxyModuleTopLevelAwait() {
461
461
  };
462
462
  }
463
463
  //#endregion
464
+ //#region src/plugins/pluginDevRemoteHmr.ts
465
+ const REMOTE_HMR_ENDPOINT = "__mf_hmr";
466
+ const REMOTE_HMR_EVENT = "mf:remote-update";
467
+ const REMOTE_HMR_CONNECT_RETRY_DELAY_MS = 1e3;
468
+ const REMOTE_HMR_CONNECT_MAX_RETRIES = 10;
469
+ function getBasePath(base) {
470
+ if (!base) return "/";
471
+ if (base.startsWith("http://") || base.startsWith("https://")) try {
472
+ return new URL(base).pathname || "/";
473
+ } catch {
474
+ return "/";
475
+ }
476
+ return base;
477
+ }
478
+ function getRemoteHmrPath(base) {
479
+ return `${getBasePath(base).replace(/\/?$/, "/")}${REMOTE_HMR_ENDPOINT}`.replace(/\/{2,}/g, "/");
480
+ }
481
+ function getHmrWsPath(base, hmrPath) {
482
+ const normalizedBase = getBasePath(base);
483
+ const normalizedPath = getBasePath(hmrPath || "");
484
+ if (!normalizedPath || normalizedPath === "/") return normalizedBase;
485
+ return `${normalizedBase.endsWith("/") ? normalizedBase.slice(0, -1) : normalizedBase}/${normalizedPath.startsWith("/") ? normalizedPath.slice(1) : normalizedPath}`;
486
+ }
487
+ function shouldIgnoreFile(file, options) {
488
+ return file.includes("/node_modules/") || file.includes("\\node_modules\\") || file.includes(`/${options.virtualModuleDir}/`) || file.includes(`\\${options.virtualModuleDir}\\`) || file.includes("/.vite/") || file.includes("\\.vite\\") || file.includes("/.__mf__temp/") || file.includes("\\.__mf__temp\\");
489
+ }
490
+ function getRemoteHmrWsUrl(server) {
491
+ const hmr = server.config.server.hmr;
492
+ return `${hmr && typeof hmr === "object" && hmr.protocol ? hmr.protocol : server.config.server.https ? "wss" : "ws"}://${hmr && typeof hmr === "object" && hmr.host ? hmr.host : typeof server.config.server.host === "string" && server.config.server.host !== "0.0.0.0" ? server.config.server.host : "localhost"}:${hmr && typeof hmr === "object" && (hmr.clientPort || hmr.port) ? hmr.clientPort || hmr.port : server.config.server.port}${getHmrWsPath(server.config.base, hmr && typeof hmr === "object" ? hmr.path : "")}?token=${server.config.webSocketToken}`;
493
+ }
494
+ function getLocalFallbackOrigin(server) {
495
+ return `${server.config.server.https ? "https" : "http"}://${typeof server.config.server.host === "string" && server.config.server.host !== "0.0.0.0" && server.config.server.host !== "::" ? server.config.server.host : "localhost"}:${server.config.server.port || 5173}`;
496
+ }
497
+ function getRemoteHmrEndpoint(remoteEntry, server) {
498
+ try {
499
+ const remoteManifestUrl = new URL(remoteEntry, getLocalFallbackOrigin(server));
500
+ remoteManifestUrl.pathname = `/${remoteManifestUrl.pathname.split("/").filter(Boolean).slice(0, -1).join("/")}`;
501
+ if (!remoteManifestUrl.pathname.endsWith("/")) remoteManifestUrl.pathname += "/";
502
+ remoteManifestUrl.search = "";
503
+ remoteManifestUrl.hash = "";
504
+ return new URL(REMOTE_HMR_ENDPOINT, remoteManifestUrl).toString();
505
+ } catch {
506
+ return null;
507
+ }
508
+ }
509
+ function parseRemoteHmrMessage(rawData) {
510
+ if (typeof rawData !== "string") return null;
511
+ try {
512
+ const parsed = JSON.parse(rawData);
513
+ if (parsed?.type !== "custom" || typeof parsed?.event !== "string") return null;
514
+ return parsed;
515
+ } catch {
516
+ return null;
517
+ }
518
+ }
519
+ function getStringPreview(value, max = 180) {
520
+ let rawValue = "";
521
+ if (typeof value === "string") rawValue = value;
522
+ else if (value instanceof Error) rawValue = `${value.name}: ${value.message}`;
523
+ else if (typeof value === "object" && value !== null) try {
524
+ rawValue = JSON.stringify(value);
525
+ } catch {}
526
+ return rawValue.slice(0, max);
527
+ }
528
+ function isRemoteHmrEnabled(dev) {
529
+ return typeof dev === "object" && dev !== null && dev.remoteHmr === true;
530
+ }
531
+ function pluginDevRemoteHmr(options) {
532
+ return {
533
+ name: "module-federation-dev-remote-hmr",
534
+ apply: "serve",
535
+ configureServer(server) {
536
+ if (!isRemoteHmrEnabled(options.dev)) return;
537
+ const isRemote = Object.keys(options.exposes).length > 0;
538
+ const isHost = Object.keys(options.remotes).length > 0;
539
+ if (isRemote) {
540
+ const endpointPath = getRemoteHmrPath(server.config.base);
541
+ const wsUrl = getRemoteHmrWsUrl(server);
542
+ server.middlewares.use((req, res, next) => {
543
+ if (req.url?.replace(/\?.*/, "") !== endpointPath) {
544
+ next();
545
+ return;
546
+ }
547
+ res.setHeader("Content-Type", "application/json");
548
+ res.setHeader("Access-Control-Allow-Origin", "*");
549
+ res.end(JSON.stringify({
550
+ remote: options.name,
551
+ event: REMOTE_HMR_EVENT,
552
+ wsUrl
553
+ }));
554
+ });
555
+ const broadcast = (file) => {
556
+ if (shouldIgnoreFile(file, options)) return;
557
+ server.ws.send({
558
+ type: "custom",
559
+ event: REMOTE_HMR_EVENT,
560
+ data: {
561
+ remote: options.name,
562
+ file,
563
+ ts: Date.now()
564
+ }
565
+ });
566
+ };
567
+ server.watcher.on("change", broadcast);
568
+ server.watcher.on("add", broadcast);
569
+ server.watcher.on("unlink", broadcast);
570
+ server.httpServer?.once("close", () => {
571
+ server.watcher.off("change", broadcast);
572
+ server.watcher.off("add", broadcast);
573
+ server.watcher.off("unlink", broadcast);
574
+ });
575
+ }
576
+ if (isHost) {
577
+ const connections = [];
578
+ const reconnectTimers = /* @__PURE__ */ new Map();
579
+ let isTearingDown = false;
580
+ const clearReconnectTimer = (remoteName) => {
581
+ const timer = reconnectTimers.get(remoteName);
582
+ if (!timer) return;
583
+ clearTimeout(timer);
584
+ reconnectTimers.delete(remoteName);
585
+ };
586
+ const scheduleReconnect = (remoteName, remote, attempt, reason) => {
587
+ if (isTearingDown) return;
588
+ if (attempt >= REMOTE_HMR_CONNECT_MAX_RETRIES) {
589
+ mfWarn(`Remote "${remoteName}" full HMR reconnect skipped after ${REMOTE_HMR_CONNECT_MAX_RETRIES} attempts: ${reason}`);
590
+ return;
591
+ }
592
+ clearReconnectTimer(remoteName);
593
+ const timer = setTimeout(() => {
594
+ reconnectTimers.delete(remoteName);
595
+ connectRemote(remoteName, remote, attempt + 1);
596
+ }, REMOTE_HMR_CONNECT_RETRY_DELAY_MS);
597
+ reconnectTimers.set(remoteName, timer);
598
+ };
599
+ const connectRemote = async (remoteName, remote, attempt = 0) => {
600
+ if (isTearingDown) return;
601
+ const endpoint = getRemoteHmrEndpoint(remote.entry, server);
602
+ if (!endpoint) {
603
+ mfWarn(`Failed to build HMR endpoint URL for remote "${remoteName}"`);
604
+ return;
605
+ }
606
+ try {
607
+ const metadataResponse = await fetch(endpoint);
608
+ if (!metadataResponse.ok) {
609
+ mfWarn(`Failed to fetch remote HMR metadata from "${remoteName}": ${metadataResponse.status}`);
610
+ scheduleReconnect(remoteName, remote, attempt, `HTTP ${metadataResponse.status}`);
611
+ return;
612
+ }
613
+ const metadata = await metadataResponse.json();
614
+ if (metadata.event !== REMOTE_HMR_EVENT || !metadata.wsUrl) {
615
+ mfWarn(`Remote "${remoteName}" returned unexpected HMR metadata shape`);
616
+ return;
617
+ }
618
+ const ws = new WebSocket(metadata.wsUrl, "vite-hmr");
619
+ ws.onmessage = (rawEvent) => {
620
+ const message = parseRemoteHmrMessage(rawEvent.data);
621
+ if (!message || message.event !== REMOTE_HMR_EVENT) return;
622
+ server.ws.send({ type: "full-reload" });
623
+ };
624
+ ws.onopen = () => clearReconnectTimer(remoteName);
625
+ ws.onerror = (error) => mfWarn(`Remote HMR socket error for "${remoteName}":`, error);
626
+ ws.onclose = () => scheduleReconnect(remoteName, remote, attempt, "socket closed");
627
+ connections.push(ws);
628
+ } catch (error) {
629
+ mfWarn(`Failed to connect remote HMR for "${remoteName}" on attempt ${attempt + 1}: ${getStringPreview(error)}`);
630
+ scheduleReconnect(remoteName, remote, attempt, getStringPreview(error));
631
+ }
632
+ };
633
+ const teardown = () => {
634
+ isTearingDown = true;
635
+ reconnectTimers.forEach((timer) => clearTimeout(timer));
636
+ reconnectTimers.clear();
637
+ connections.forEach((connection) => {
638
+ if (connection.readyState !== connection.CLOSING && connection.readyState !== connection.CLOSED) connection.close();
639
+ });
640
+ connections.length = 0;
641
+ };
642
+ for (const [remoteName, remote] of Object.entries(options.remotes)) connectRemote(remoteName, remote);
643
+ server.httpServer?.once("close", teardown);
644
+ }
645
+ }
646
+ };
647
+ }
648
+ //#endregion
464
649
  //#region src/plugins/pluginDts.ts
465
650
  const DEFAULT_DEV_OPTIONS = {
466
651
  disableLiveReload: true,
@@ -1323,24 +1508,49 @@ function getPackageEsmEntryPath(pkg) {
1323
1508
  }
1324
1509
  function getEsmNamedExports(pkg) {
1325
1510
  let source = "";
1511
+ let entryPath;
1326
1512
  try {
1327
- const entryPath = getPackageEsmEntryPath(pkg);
1513
+ entryPath = getPackageEsmEntryPath(pkg);
1328
1514
  if (!entryPath) return [];
1329
1515
  const { initSync, parse } = localRequire("es-module-lexer");
1330
1516
  initSync();
1331
1517
  source = (0, fs.readFileSync)(entryPath, "utf-8");
1332
1518
  const [, exports] = parse(source, entryPath);
1333
1519
  const names = exports.map((item) => item.n).filter((name) => isValidEsmExportName(name));
1334
- const regexNames = getNamedExportsViaRegex(source);
1520
+ const regexNames = getNamedExportsViaRegex(source, entryPath);
1335
1521
  const filteredNames = names.filter((name) => name !== "type" || regexNames.includes(name));
1336
1522
  if (filteredNames.length > 0) return [...new Set([...filteredNames, ...regexNames])];
1337
1523
  return regexNames;
1338
1524
  } catch {
1339
- return source ? getNamedExportsViaRegex(source) : [];
1525
+ return source ? getNamedExportsViaRegex(source, entryPath) : [];
1526
+ }
1527
+ }
1528
+ function resolveRelativeModule(filePath, specifier) {
1529
+ const dir = pathe.default.dirname(filePath);
1530
+ const exact = pathe.default.resolve(dir, specifier);
1531
+ if ((0, fs.existsSync)(exact) && !(0, fs.statSync)(exact).isDirectory()) return exact;
1532
+ const extensions = [
1533
+ ".ts",
1534
+ ".tsx",
1535
+ ".js",
1536
+ ".jsx",
1537
+ ".mjs",
1538
+ ".mts"
1539
+ ];
1540
+ for (const ext of extensions) {
1541
+ const candidate = pathe.default.resolve(dir, specifier + ext);
1542
+ if ((0, fs.existsSync)(candidate) && !(0, fs.statSync)(candidate).isDirectory()) return candidate;
1543
+ }
1544
+ const resolved = pathe.default.resolve(dir, specifier);
1545
+ for (const ext of extensions) {
1546
+ const candidate = pathe.default.join(resolved, "index" + ext);
1547
+ if ((0, fs.existsSync)(candidate)) return candidate;
1340
1548
  }
1341
1549
  }
1342
- function getNamedExportsViaRegex(source) {
1550
+ function getNamedExportsViaRegex(source, filePath, visited) {
1343
1551
  const names = /* @__PURE__ */ new Set();
1552
+ visited = visited || /* @__PURE__ */ new Set();
1553
+ if (filePath) visited.add(filePath);
1344
1554
  const declRegex = new RegExp(`export\\s+(?:async\\s+)?(?:function(?:\\*\\s*|\\s+\\*?\\s*)|const\\s+|let\\s+|var\\s+|class\\s+)(${JS_IDENTIFIER_PATTERN})`, "gu");
1345
1555
  let match;
1346
1556
  while ((match = declRegex.exec(source)) !== null) {
@@ -1361,6 +1571,19 @@ function getNamedExportsViaRegex(source) {
1361
1571
  if (isValidEsmExportName(name)) names.add(name);
1362
1572
  }
1363
1573
  }
1574
+ if (filePath) {
1575
+ const starExportRegex = /export\s+\*\s+from\s+['"]([^'"]+)['"]/g;
1576
+ while ((match = starExportRegex.exec(source)) !== null) {
1577
+ const specifier = match[1];
1578
+ if (!specifier.startsWith(".")) continue;
1579
+ const resolvedPath = resolveRelativeModule(filePath, specifier);
1580
+ if (!resolvedPath || visited.has(resolvedPath)) continue;
1581
+ try {
1582
+ const reExportNames = getNamedExportsViaRegex((0, fs.readFileSync)(resolvedPath, "utf-8"), resolvedPath, visited);
1583
+ for (const name of reExportNames) names.add(name);
1584
+ } catch {}
1585
+ }
1586
+ }
1364
1587
  return [...names];
1365
1588
  }
1366
1589
  function getPackageNamedExports(pkg) {
@@ -1522,6 +1745,10 @@ function generateLocalSharedImportMap() {
1522
1745
  const isAstro = hasPackageDependency("astro");
1523
1746
  const useDirectReactImport = isVinext || isAstro;
1524
1747
  const options = getNormalizeModuleFederationOptions();
1748
+ const getPackagePath = (pkg, shareItem) => {
1749
+ if (useDirectReactImport && pkg === "react") return "react";
1750
+ return getConcreteSharedImportSource(pkg, shareItem) || getLocalProviderImportPath(pkg) || getSharedImportSource(pkg, shareItem);
1751
+ };
1525
1752
  return `
1526
1753
  import {loadShare} from "@module-federation/runtime";
1527
1754
  const importMap = {
@@ -1529,8 +1756,7 @@ function generateLocalSharedImportMap() {
1529
1756
  const shareItem = getNormalizeShareItem(pkg);
1530
1757
  return `
1531
1758
  ${JSON.stringify(pkg)}: async () => {
1532
- ${shareItem?.shareConfig.import === false ? `throw new Error(\`[Module Federation] Shared module '\${${JSON.stringify(pkg)}}' must be provided by host\`);` : useDirectReactImport && pkg === "react" ? `let pkg = await import("react");
1533
- return pkg;` : `let pkg = await import(${JSON.stringify(getSharedImportSource(pkg, shareItem))});
1759
+ ${shareItem?.shareConfig.import === false ? `throw new Error(\`[Module Federation] Shared module '\${${JSON.stringify(pkg)}}' must be provided by host\`);` : `let pkg = await import(${JSON.stringify(getPackagePath(pkg, shareItem))});
1534
1760
  return pkg;`}
1535
1761
  }
1536
1762
  `;
@@ -1975,7 +2201,7 @@ const Manifest = () => {
1975
2201
  root = config.root;
1976
2202
  let base = config.base;
1977
2203
  if (_command === "serve") base = (config.server.origin || "") + config.base;
1978
- publicPath = resolvePublicPath(mfOptions, base, _originalConfigBase);
2204
+ publicPath = mfOptions.publicPath === "auto" ? "auto" : resolvePublicPath(mfOptions, base, _originalConfigBase);
1979
2205
  },
1980
2206
  async generateBundle(options, bundle) {
1981
2207
  if (!mfManifestName) return;
@@ -3078,6 +3304,7 @@ function federation(mfUserOptions) {
3078
3304
  checkAliasConflicts({ shared }),
3079
3305
  normalizeOptimizeDeps_default,
3080
3306
  ...pluginDts(options),
3307
+ pluginDevRemoteHmr(options),
3081
3308
  ...addEntry({
3082
3309
  entryName: "remoteEntry",
3083
3310
  entryPath: remoteEntryId,
package/lib/index.d.cts CHANGED
@@ -84,6 +84,7 @@ interface PluginDevOptions {
84
84
  disableLiveReload?: boolean;
85
85
  disableHotTypesReload?: boolean;
86
86
  disableDynamicRemoteTypeHints?: boolean;
87
+ remoteHmr?: boolean;
87
88
  }
88
89
  interface RemoteTypeUrl {
89
90
  alias?: string;
package/lib/index.d.mts CHANGED
@@ -84,6 +84,7 @@ interface PluginDevOptions {
84
84
  disableLiveReload?: boolean;
85
85
  disableHotTypesReload?: boolean;
86
86
  disableDynamicRemoteTypeHints?: boolean;
87
+ remoteHmr?: boolean;
87
88
  }
88
89
  interface RemoteTypeUrl {
89
90
  alias?: string;
package/lib/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import defu from "defu";
3
3
  import * as fs from "fs";
4
- import { existsSync, mkdirSync, readFileSync, writeFile, writeFileSync } from "fs";
4
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFile, writeFileSync } from "fs";
5
5
  import { createRequire as createRequire$1 } from "module";
6
6
  import * as path$1 from "pathe";
7
7
  import path, { basename, dirname, join, parse, resolve } from "pathe";
@@ -439,6 +439,191 @@ function PluginDevProxyModuleTopLevelAwait() {
439
439
  };
440
440
  }
441
441
  //#endregion
442
+ //#region src/plugins/pluginDevRemoteHmr.ts
443
+ const REMOTE_HMR_ENDPOINT = "__mf_hmr";
444
+ const REMOTE_HMR_EVENT = "mf:remote-update";
445
+ const REMOTE_HMR_CONNECT_RETRY_DELAY_MS = 1e3;
446
+ const REMOTE_HMR_CONNECT_MAX_RETRIES = 10;
447
+ function getBasePath(base) {
448
+ if (!base) return "/";
449
+ if (base.startsWith("http://") || base.startsWith("https://")) try {
450
+ return new URL(base).pathname || "/";
451
+ } catch {
452
+ return "/";
453
+ }
454
+ return base;
455
+ }
456
+ function getRemoteHmrPath(base) {
457
+ return `${getBasePath(base).replace(/\/?$/, "/")}${REMOTE_HMR_ENDPOINT}`.replace(/\/{2,}/g, "/");
458
+ }
459
+ function getHmrWsPath(base, hmrPath) {
460
+ const normalizedBase = getBasePath(base);
461
+ const normalizedPath = getBasePath(hmrPath || "");
462
+ if (!normalizedPath || normalizedPath === "/") return normalizedBase;
463
+ return `${normalizedBase.endsWith("/") ? normalizedBase.slice(0, -1) : normalizedBase}/${normalizedPath.startsWith("/") ? normalizedPath.slice(1) : normalizedPath}`;
464
+ }
465
+ function shouldIgnoreFile(file, options) {
466
+ return file.includes("/node_modules/") || file.includes("\\node_modules\\") || file.includes(`/${options.virtualModuleDir}/`) || file.includes(`\\${options.virtualModuleDir}\\`) || file.includes("/.vite/") || file.includes("\\.vite\\") || file.includes("/.__mf__temp/") || file.includes("\\.__mf__temp\\");
467
+ }
468
+ function getRemoteHmrWsUrl(server) {
469
+ const hmr = server.config.server.hmr;
470
+ return `${hmr && typeof hmr === "object" && hmr.protocol ? hmr.protocol : server.config.server.https ? "wss" : "ws"}://${hmr && typeof hmr === "object" && hmr.host ? hmr.host : typeof server.config.server.host === "string" && server.config.server.host !== "0.0.0.0" ? server.config.server.host : "localhost"}:${hmr && typeof hmr === "object" && (hmr.clientPort || hmr.port) ? hmr.clientPort || hmr.port : server.config.server.port}${getHmrWsPath(server.config.base, hmr && typeof hmr === "object" ? hmr.path : "")}?token=${server.config.webSocketToken}`;
471
+ }
472
+ function getLocalFallbackOrigin(server) {
473
+ return `${server.config.server.https ? "https" : "http"}://${typeof server.config.server.host === "string" && server.config.server.host !== "0.0.0.0" && server.config.server.host !== "::" ? server.config.server.host : "localhost"}:${server.config.server.port || 5173}`;
474
+ }
475
+ function getRemoteHmrEndpoint(remoteEntry, server) {
476
+ try {
477
+ const remoteManifestUrl = new URL(remoteEntry, getLocalFallbackOrigin(server));
478
+ remoteManifestUrl.pathname = `/${remoteManifestUrl.pathname.split("/").filter(Boolean).slice(0, -1).join("/")}`;
479
+ if (!remoteManifestUrl.pathname.endsWith("/")) remoteManifestUrl.pathname += "/";
480
+ remoteManifestUrl.search = "";
481
+ remoteManifestUrl.hash = "";
482
+ return new URL(REMOTE_HMR_ENDPOINT, remoteManifestUrl).toString();
483
+ } catch {
484
+ return null;
485
+ }
486
+ }
487
+ function parseRemoteHmrMessage(rawData) {
488
+ if (typeof rawData !== "string") return null;
489
+ try {
490
+ const parsed = JSON.parse(rawData);
491
+ if (parsed?.type !== "custom" || typeof parsed?.event !== "string") return null;
492
+ return parsed;
493
+ } catch {
494
+ return null;
495
+ }
496
+ }
497
+ function getStringPreview(value, max = 180) {
498
+ let rawValue = "";
499
+ if (typeof value === "string") rawValue = value;
500
+ else if (value instanceof Error) rawValue = `${value.name}: ${value.message}`;
501
+ else if (typeof value === "object" && value !== null) try {
502
+ rawValue = JSON.stringify(value);
503
+ } catch {}
504
+ return rawValue.slice(0, max);
505
+ }
506
+ function isRemoteHmrEnabled(dev) {
507
+ return typeof dev === "object" && dev !== null && dev.remoteHmr === true;
508
+ }
509
+ function pluginDevRemoteHmr(options) {
510
+ return {
511
+ name: "module-federation-dev-remote-hmr",
512
+ apply: "serve",
513
+ configureServer(server) {
514
+ if (!isRemoteHmrEnabled(options.dev)) return;
515
+ const isRemote = Object.keys(options.exposes).length > 0;
516
+ const isHost = Object.keys(options.remotes).length > 0;
517
+ if (isRemote) {
518
+ const endpointPath = getRemoteHmrPath(server.config.base);
519
+ const wsUrl = getRemoteHmrWsUrl(server);
520
+ server.middlewares.use((req, res, next) => {
521
+ if (req.url?.replace(/\?.*/, "") !== endpointPath) {
522
+ next();
523
+ return;
524
+ }
525
+ res.setHeader("Content-Type", "application/json");
526
+ res.setHeader("Access-Control-Allow-Origin", "*");
527
+ res.end(JSON.stringify({
528
+ remote: options.name,
529
+ event: REMOTE_HMR_EVENT,
530
+ wsUrl
531
+ }));
532
+ });
533
+ const broadcast = (file) => {
534
+ if (shouldIgnoreFile(file, options)) return;
535
+ server.ws.send({
536
+ type: "custom",
537
+ event: REMOTE_HMR_EVENT,
538
+ data: {
539
+ remote: options.name,
540
+ file,
541
+ ts: Date.now()
542
+ }
543
+ });
544
+ };
545
+ server.watcher.on("change", broadcast);
546
+ server.watcher.on("add", broadcast);
547
+ server.watcher.on("unlink", broadcast);
548
+ server.httpServer?.once("close", () => {
549
+ server.watcher.off("change", broadcast);
550
+ server.watcher.off("add", broadcast);
551
+ server.watcher.off("unlink", broadcast);
552
+ });
553
+ }
554
+ if (isHost) {
555
+ const connections = [];
556
+ const reconnectTimers = /* @__PURE__ */ new Map();
557
+ let isTearingDown = false;
558
+ const clearReconnectTimer = (remoteName) => {
559
+ const timer = reconnectTimers.get(remoteName);
560
+ if (!timer) return;
561
+ clearTimeout(timer);
562
+ reconnectTimers.delete(remoteName);
563
+ };
564
+ const scheduleReconnect = (remoteName, remote, attempt, reason) => {
565
+ if (isTearingDown) return;
566
+ if (attempt >= REMOTE_HMR_CONNECT_MAX_RETRIES) {
567
+ mfWarn(`Remote "${remoteName}" full HMR reconnect skipped after ${REMOTE_HMR_CONNECT_MAX_RETRIES} attempts: ${reason}`);
568
+ return;
569
+ }
570
+ clearReconnectTimer(remoteName);
571
+ const timer = setTimeout(() => {
572
+ reconnectTimers.delete(remoteName);
573
+ connectRemote(remoteName, remote, attempt + 1);
574
+ }, REMOTE_HMR_CONNECT_RETRY_DELAY_MS);
575
+ reconnectTimers.set(remoteName, timer);
576
+ };
577
+ const connectRemote = async (remoteName, remote, attempt = 0) => {
578
+ if (isTearingDown) return;
579
+ const endpoint = getRemoteHmrEndpoint(remote.entry, server);
580
+ if (!endpoint) {
581
+ mfWarn(`Failed to build HMR endpoint URL for remote "${remoteName}"`);
582
+ return;
583
+ }
584
+ try {
585
+ const metadataResponse = await fetch(endpoint);
586
+ if (!metadataResponse.ok) {
587
+ mfWarn(`Failed to fetch remote HMR metadata from "${remoteName}": ${metadataResponse.status}`);
588
+ scheduleReconnect(remoteName, remote, attempt, `HTTP ${metadataResponse.status}`);
589
+ return;
590
+ }
591
+ const metadata = await metadataResponse.json();
592
+ if (metadata.event !== REMOTE_HMR_EVENT || !metadata.wsUrl) {
593
+ mfWarn(`Remote "${remoteName}" returned unexpected HMR metadata shape`);
594
+ return;
595
+ }
596
+ const ws = new WebSocket(metadata.wsUrl, "vite-hmr");
597
+ ws.onmessage = (rawEvent) => {
598
+ const message = parseRemoteHmrMessage(rawEvent.data);
599
+ if (!message || message.event !== REMOTE_HMR_EVENT) return;
600
+ server.ws.send({ type: "full-reload" });
601
+ };
602
+ ws.onopen = () => clearReconnectTimer(remoteName);
603
+ ws.onerror = (error) => mfWarn(`Remote HMR socket error for "${remoteName}":`, error);
604
+ ws.onclose = () => scheduleReconnect(remoteName, remote, attempt, "socket closed");
605
+ connections.push(ws);
606
+ } catch (error) {
607
+ mfWarn(`Failed to connect remote HMR for "${remoteName}" on attempt ${attempt + 1}: ${getStringPreview(error)}`);
608
+ scheduleReconnect(remoteName, remote, attempt, getStringPreview(error));
609
+ }
610
+ };
611
+ const teardown = () => {
612
+ isTearingDown = true;
613
+ reconnectTimers.forEach((timer) => clearTimeout(timer));
614
+ reconnectTimers.clear();
615
+ connections.forEach((connection) => {
616
+ if (connection.readyState !== connection.CLOSING && connection.readyState !== connection.CLOSED) connection.close();
617
+ });
618
+ connections.length = 0;
619
+ };
620
+ for (const [remoteName, remote] of Object.entries(options.remotes)) connectRemote(remoteName, remote);
621
+ server.httpServer?.once("close", teardown);
622
+ }
623
+ }
624
+ };
625
+ }
626
+ //#endregion
442
627
  //#region src/plugins/pluginDts.ts
443
628
  const DEFAULT_DEV_OPTIONS = {
444
629
  disableLiveReload: true,
@@ -1300,24 +1485,49 @@ function getPackageEsmEntryPath(pkg) {
1300
1485
  }
1301
1486
  function getEsmNamedExports(pkg) {
1302
1487
  let source = "";
1488
+ let entryPath;
1303
1489
  try {
1304
- const entryPath = getPackageEsmEntryPath(pkg);
1490
+ entryPath = getPackageEsmEntryPath(pkg);
1305
1491
  if (!entryPath) return [];
1306
1492
  const { initSync, parse } = localRequire("es-module-lexer");
1307
1493
  initSync();
1308
1494
  source = readFileSync(entryPath, "utf-8");
1309
1495
  const [, exports] = parse(source, entryPath);
1310
1496
  const names = exports.map((item) => item.n).filter((name) => isValidEsmExportName(name));
1311
- const regexNames = getNamedExportsViaRegex(source);
1497
+ const regexNames = getNamedExportsViaRegex(source, entryPath);
1312
1498
  const filteredNames = names.filter((name) => name !== "type" || regexNames.includes(name));
1313
1499
  if (filteredNames.length > 0) return [...new Set([...filteredNames, ...regexNames])];
1314
1500
  return regexNames;
1315
1501
  } catch {
1316
- return source ? getNamedExportsViaRegex(source) : [];
1502
+ return source ? getNamedExportsViaRegex(source, entryPath) : [];
1503
+ }
1504
+ }
1505
+ function resolveRelativeModule(filePath, specifier) {
1506
+ const dir = path.dirname(filePath);
1507
+ const exact = path.resolve(dir, specifier);
1508
+ if (existsSync(exact) && !statSync(exact).isDirectory()) return exact;
1509
+ const extensions = [
1510
+ ".ts",
1511
+ ".tsx",
1512
+ ".js",
1513
+ ".jsx",
1514
+ ".mjs",
1515
+ ".mts"
1516
+ ];
1517
+ for (const ext of extensions) {
1518
+ const candidate = path.resolve(dir, specifier + ext);
1519
+ if (existsSync(candidate) && !statSync(candidate).isDirectory()) return candidate;
1520
+ }
1521
+ const resolved = path.resolve(dir, specifier);
1522
+ for (const ext of extensions) {
1523
+ const candidate = path.join(resolved, "index" + ext);
1524
+ if (existsSync(candidate)) return candidate;
1317
1525
  }
1318
1526
  }
1319
- function getNamedExportsViaRegex(source) {
1527
+ function getNamedExportsViaRegex(source, filePath, visited) {
1320
1528
  const names = /* @__PURE__ */ new Set();
1529
+ visited = visited || /* @__PURE__ */ new Set();
1530
+ if (filePath) visited.add(filePath);
1321
1531
  const declRegex = new RegExp(`export\\s+(?:async\\s+)?(?:function(?:\\*\\s*|\\s+\\*?\\s*)|const\\s+|let\\s+|var\\s+|class\\s+)(${JS_IDENTIFIER_PATTERN})`, "gu");
1322
1532
  let match;
1323
1533
  while ((match = declRegex.exec(source)) !== null) {
@@ -1338,6 +1548,19 @@ function getNamedExportsViaRegex(source) {
1338
1548
  if (isValidEsmExportName(name)) names.add(name);
1339
1549
  }
1340
1550
  }
1551
+ if (filePath) {
1552
+ const starExportRegex = /export\s+\*\s+from\s+['"]([^'"]+)['"]/g;
1553
+ while ((match = starExportRegex.exec(source)) !== null) {
1554
+ const specifier = match[1];
1555
+ if (!specifier.startsWith(".")) continue;
1556
+ const resolvedPath = resolveRelativeModule(filePath, specifier);
1557
+ if (!resolvedPath || visited.has(resolvedPath)) continue;
1558
+ try {
1559
+ const reExportNames = getNamedExportsViaRegex(readFileSync(resolvedPath, "utf-8"), resolvedPath, visited);
1560
+ for (const name of reExportNames) names.add(name);
1561
+ } catch {}
1562
+ }
1563
+ }
1341
1564
  return [...names];
1342
1565
  }
1343
1566
  function getPackageNamedExports(pkg) {
@@ -1499,6 +1722,10 @@ function generateLocalSharedImportMap() {
1499
1722
  const isAstro = hasPackageDependency("astro");
1500
1723
  const useDirectReactImport = isVinext || isAstro;
1501
1724
  const options = getNormalizeModuleFederationOptions();
1725
+ const getPackagePath = (pkg, shareItem) => {
1726
+ if (useDirectReactImport && pkg === "react") return "react";
1727
+ return getConcreteSharedImportSource(pkg, shareItem) || getLocalProviderImportPath(pkg) || getSharedImportSource(pkg, shareItem);
1728
+ };
1502
1729
  return `
1503
1730
  import {loadShare} from "@module-federation/runtime";
1504
1731
  const importMap = {
@@ -1506,8 +1733,7 @@ function generateLocalSharedImportMap() {
1506
1733
  const shareItem = getNormalizeShareItem(pkg);
1507
1734
  return `
1508
1735
  ${JSON.stringify(pkg)}: async () => {
1509
- ${shareItem?.shareConfig.import === false ? `throw new Error(\`[Module Federation] Shared module '\${${JSON.stringify(pkg)}}' must be provided by host\`);` : useDirectReactImport && pkg === "react" ? `let pkg = await import("react");
1510
- return pkg;` : `let pkg = await import(${JSON.stringify(getSharedImportSource(pkg, shareItem))});
1736
+ ${shareItem?.shareConfig.import === false ? `throw new Error(\`[Module Federation] Shared module '\${${JSON.stringify(pkg)}}' must be provided by host\`);` : `let pkg = await import(${JSON.stringify(getPackagePath(pkg, shareItem))});
1511
1737
  return pkg;`}
1512
1738
  }
1513
1739
  `;
@@ -1952,7 +2178,7 @@ const Manifest = () => {
1952
2178
  root = config.root;
1953
2179
  let base = config.base;
1954
2180
  if (_command === "serve") base = (config.server.origin || "") + config.base;
1955
- publicPath = resolvePublicPath(mfOptions, base, _originalConfigBase);
2181
+ publicPath = mfOptions.publicPath === "auto" ? "auto" : resolvePublicPath(mfOptions, base, _originalConfigBase);
1956
2182
  },
1957
2183
  async generateBundle(options, bundle) {
1958
2184
  if (!mfManifestName) return;
@@ -3055,6 +3281,7 @@ function federation(mfUserOptions) {
3055
3281
  checkAliasConflicts({ shared }),
3056
3282
  normalizeOptimizeDeps_default,
3057
3283
  ...pluginDts(options),
3284
+ pluginDevRemoteHmr(options),
3058
3285
  ...addEntry({
3059
3286
  entryName: "remoteEntry",
3060
3287
  entryPath: remoteEntryId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@module-federation/vite",
3
- "version": "1.13.7",
3
+ "version": "1.14.0",
4
4
  "description": "Vite plugin for Module Federation",
5
5
  "type": "module",
6
6
  "main": "./lib/index.cjs",