@vercel/microfrontends 2.0.0 → 2.1.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +1 -5
  3. package/dist/bin/cli.cjs +226 -79
  4. package/dist/config.cjs +5 -4
  5. package/dist/config.cjs.map +1 -1
  6. package/dist/config.js +5 -4
  7. package/dist/config.js.map +1 -1
  8. package/dist/experimental/sveltekit.cjs +51 -20
  9. package/dist/experimental/sveltekit.cjs.map +1 -1
  10. package/dist/experimental/sveltekit.js +51 -20
  11. package/dist/experimental/sveltekit.js.map +1 -1
  12. package/dist/experimental/vite.cjs +51 -20
  13. package/dist/experimental/vite.cjs.map +1 -1
  14. package/dist/experimental/vite.js +51 -20
  15. package/dist/experimental/vite.js.map +1 -1
  16. package/dist/microfrontends/server.cjs +51 -20
  17. package/dist/microfrontends/server.cjs.map +1 -1
  18. package/dist/microfrontends/server.js +51 -20
  19. package/dist/microfrontends/server.js.map +1 -1
  20. package/dist/microfrontends/utils.cjs +32 -7
  21. package/dist/microfrontends/utils.cjs.map +1 -1
  22. package/dist/microfrontends/utils.d.ts +8 -2
  23. package/dist/microfrontends/utils.js +31 -7
  24. package/dist/microfrontends/utils.js.map +1 -1
  25. package/dist/next/client.cjs +1 -1
  26. package/dist/next/client.cjs.map +1 -1
  27. package/dist/next/client.d.ts +15 -1
  28. package/dist/next/client.js +1 -1
  29. package/dist/next/client.js.map +1 -1
  30. package/dist/next/config.cjs +62 -20
  31. package/dist/next/config.cjs.map +1 -1
  32. package/dist/next/config.js +62 -20
  33. package/dist/next/config.js.map +1 -1
  34. package/dist/next/middleware.cjs +5 -4
  35. package/dist/next/middleware.cjs.map +1 -1
  36. package/dist/next/middleware.js +5 -4
  37. package/dist/next/middleware.js.map +1 -1
  38. package/dist/next/testing.cjs +5 -4
  39. package/dist/next/testing.cjs.map +1 -1
  40. package/dist/next/testing.js +5 -4
  41. package/dist/next/testing.js.map +1 -1
  42. package/dist/utils/mfe-port.cjs +66 -40
  43. package/dist/utils/mfe-port.cjs.map +1 -1
  44. package/dist/utils/mfe-port.js +66 -40
  45. package/dist/utils/mfe-port.js.map +1 -1
  46. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @vercel/microfrontends
2
2
 
3
+ ## 2.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c856a5d:
8
+ - Add support for custom `microfrontends.json` file names. This enables a single application to be deployed as multiple different vercel projects / microfrontends.
9
+ - Strip asset prefix from Vercel Firewall rate limit paths. Support Vercel Firewall rate limit requests when they go to a child application.
10
+
11
+ ## 2.0.1
12
+
13
+ ### Patch Changes
14
+
15
+ - 8c11bdc:
16
+ - Support hyphens and escaped special characters in supported path matching regex https://vercel.com/docs/microfrontends/path-routing#supported-path-expressions
17
+ - Improve error message for when local development proxy can't determine the port
18
+ - Update local proxy double slash routing behaviour to match the production proxy
19
+
3
20
  ## 2.0.0
4
21
 
5
22
  > **Check out our [Public Beta](https://vercel.com/changelog/microfrontends-support-is-now-in-public-beta) changelog to learn more about this release.**
package/README.md CHANGED
@@ -1,8 +1,4 @@
1
- <picture>
2
- <source srcset="https://assets.vercel.com/image/upload/v1689795055/docs-assets/static/docs/microfrontends/mfe-package-banner-dark.png" media="(prefers-color-scheme: dark)">
3
- <source srcset="https://assets.vercel.com/image/upload/v1689795055/docs-assets/static/docs/microfrontends/mfe-package-banner-light.png" media="(prefers-color-scheme: light)">
4
- <img src="https://assets.vercel.com/image/upload/v1689795055/docs-assets/static/docs/microfrontends/mfe-package-banner-dark.png" alt="hero banner">
5
- </picture>
1
+ ![hero banner](./assets/mfe-package-banner-dark.png)
6
2
 
7
3
  # @vercel/microfrontends
8
4
 
package/dist/bin/cli.cjs CHANGED
@@ -30,7 +30,7 @@ var import_env = require("@next/env");
30
30
  // package.json
31
31
  var package_default = {
32
32
  name: "@vercel/microfrontends",
33
- version: "2.0.0",
33
+ version: "2.1.0",
34
34
  private: false,
35
35
  description: "Defines configuration and utilities for microfrontends development",
36
36
  keywords: [
@@ -169,11 +169,11 @@ var package_default = {
169
169
  typecheck: "tsc --noEmit"
170
170
  },
171
171
  dependencies: {
172
- "@next/env": "15.4.0-canary.41",
172
+ "@next/env": "15.5.4",
173
173
  "@types/md5": "^2.3.5",
174
174
  ajv: "^8.17.1",
175
175
  commander: "^12.1.0",
176
- cookie: "0.4.0",
176
+ cookie: "1.0.2",
177
177
  "fast-glob": "^3.3.2",
178
178
  "http-proxy": "^1.18.1",
179
179
  "jsonc-parser": "^3.3.1",
@@ -198,7 +198,7 @@ var package_default = {
198
198
  "eslint-config-custom": "workspace:*",
199
199
  jest: "^29.7.0",
200
200
  "jest-environment-jsdom": "29.2.2",
201
- next: "15.4.0-canary.41",
201
+ next: "15.5.4",
202
202
  react: "19.0.0",
203
203
  "react-dom": "19.0.0",
204
204
  "ts-config": "workspace:*",
@@ -452,7 +452,7 @@ var MicrofrontendConfigClient = class {
452
452
  static fromEnv(config) {
453
453
  if (!config) {
454
454
  throw new Error(
455
- "Could not construct MicrofrontendConfigClient: configuration is empty or undefined. Did you set up your application with `withMicrofrontends`?"
455
+ "Could not construct MicrofrontendConfigClient: configuration is empty or undefined. Did you set up your application with `withMicrofrontends`? Is the local proxy running and this application is being accessed via the proxy port? See https://vercel.com/docs/microfrontends/local-development#setting-up-microfrontends-proxy"
456
456
  );
457
457
  }
458
458
  return new MicrofrontendConfigClient(JSON.parse(config));
@@ -592,10 +592,11 @@ function validatePathExpression(path7) {
592
592
  }
593
593
  if (token.pattern !== PATH_DEFAULT_PATTERN && // Allows (a|b|c) and ((?!a|b|c).*) regex
594
594
  // Only limited regex is supported for now, due to performance considerations
595
- !/^(?<allowed>[\w]+(?:\|[^:|()]+)+)$|^\(\?!(?<disallowed>[\w]+(?:\|[^:|()]+)*)\)\.\*$/.test(
596
- token.pattern
595
+ // Allows all letters, numbers, and hyphens. Other characters must be escaped.
596
+ !/^(?<allowed>[\w-~]+(?:\|[^:|()]+)+)$|^\(\?!(?<disallowed>[\w-~]+(?:\|[^:|()]+)*)\)\.\*$/.test(
597
+ token.pattern.replace(/\\./g, "")
597
598
  )) {
598
- return `Path ${path7} cannot use unsupported regular expression wildcard`;
599
+ return `Path ${path7} cannot use unsupported regular expression wildcard. If the path includes special characters, they must be escaped with backslash (e.g. '\\(')`;
599
600
  }
600
601
  if (token.modifier && i !== tokens.length - 1) {
601
602
  return `Modifier ${token.modifier} is not allowed on wildcard :${token.name} in ${path7}. Modifiers are only allowed in the last path component`;
@@ -1111,22 +1112,38 @@ var import_node_fs2 = require("fs");
1111
1112
  var import_jsonc_parser2 = require("jsonc-parser");
1112
1113
  var import_fast_glob = __toESM(require("fast-glob"), 1);
1113
1114
 
1114
- // src/config/constants.ts
1115
- var CONFIGURATION_FILENAMES = [
1115
+ // src/config/microfrontends/utils/get-config-file-name.ts
1116
+ var DEFAULT_CONFIGURATION_FILENAMES = [
1116
1117
  "microfrontends.jsonc",
1117
1118
  "microfrontends.json"
1118
1119
  ];
1120
+ function getPossibleConfigurationFilenames({
1121
+ customConfigFilename
1122
+ }) {
1123
+ if (customConfigFilename) {
1124
+ if (!customConfigFilename.endsWith(".json") && !customConfigFilename.endsWith(".jsonc")) {
1125
+ throw new Error(
1126
+ `The VC_MICROFRONTENDS_CONFIG_FILE_NAME environment variable must end with '.json' or '.jsonc'. Received: ${customConfigFilename}`
1127
+ );
1128
+ }
1129
+ return Array.from(
1130
+ /* @__PURE__ */ new Set([customConfigFilename, ...DEFAULT_CONFIGURATION_FILENAMES])
1131
+ );
1132
+ }
1133
+ return DEFAULT_CONFIGURATION_FILENAMES;
1134
+ }
1119
1135
 
1120
1136
  // src/config/microfrontends/utils/infer-microfrontends-location.ts
1121
1137
  var configCache = {};
1122
1138
  function findPackageWithMicrofrontendsConfig({
1123
1139
  repositoryRoot,
1124
- applicationContext
1140
+ applicationContext,
1141
+ customConfigFilename
1125
1142
  }) {
1126
1143
  const applicationName = applicationContext.name;
1127
1144
  try {
1128
1145
  const microfrontendsJsonPaths = import_fast_glob.default.globSync(
1129
- `**/{${CONFIGURATION_FILENAMES.join(",")}}`,
1146
+ `**/{${getPossibleConfigurationFilenames({ customConfigFilename }).join(",")}}`,
1130
1147
  {
1131
1148
  cwd: repositoryRoot,
1132
1149
  absolute: true,
@@ -1182,6 +1199,8 @@ Names of applications in \`microfrontends.json\` must match the Vercel Project n
1182
1199
 
1183
1200
  If your Vercel Microfrontends configuration is not in this repository, you can use the Vercel CLI to pull the Vercel Microfrontends configuration using the "vercel microfrontends pull" command, or you can specify the path manually using the VC_MICROFRONTENDS_CONFIG environment variable.
1184
1201
 
1202
+ If your Vercel Microfrontends configuration has a custom name, ensure the VC_MICROFRONTENDS_CONFIG_FILE_NAME environment variable is set, you can pull the vercel project environment variables using the "vercel env pull" command.
1203
+
1185
1204
  If you suspect this is thrown in error, please reach out to the Vercel team.`,
1186
1205
  { type: "config", subtype: "inference_failed" }
1187
1206
  );
@@ -1196,7 +1215,7 @@ If you suspect this is thrown in error, please reach out to the Vercel team.`,
1196
1215
  }
1197
1216
  }
1198
1217
  function inferMicrofrontendsLocation(opts) {
1199
- const cacheKey = `${opts.repositoryRoot}-${opts.applicationContext.name}`;
1218
+ const cacheKey = `${opts.repositoryRoot}-${opts.applicationContext.name}${opts.customConfigFilename ? `-${opts.customConfigFilename}` : ""}`;
1200
1219
  if (configCache[cacheKey]) {
1201
1220
  return configCache[cacheKey];
1202
1221
  }
@@ -1262,8 +1281,13 @@ function findPackageRoot(startDir) {
1262
1281
  // src/config/microfrontends/utils/find-config.ts
1263
1282
  var import_node_fs5 = __toESM(require("fs"), 1);
1264
1283
  var import_node_path5 = require("path");
1265
- function findConfig({ dir }) {
1266
- for (const filename of CONFIGURATION_FILENAMES) {
1284
+ function findConfig({
1285
+ dir,
1286
+ customConfigFilename
1287
+ }) {
1288
+ for (const filename of getPossibleConfigurationFilenames({
1289
+ customConfigFilename
1290
+ })) {
1267
1291
  const maybeConfig = (0, import_node_path5.join)(dir, filename);
1268
1292
  if (import_node_fs5.default.existsSync(maybeConfig)) {
1269
1293
  return maybeConfig;
@@ -1695,7 +1719,11 @@ var MicrofrontendsServer = class {
1695
1719
  appName,
1696
1720
  packageRoot
1697
1721
  });
1698
- const maybeConfig = findConfig({ dir: packageRoot });
1722
+ const customConfigFilename = process.env.VC_MICROFRONTENDS_CONFIG_FILE_NAME;
1723
+ const maybeConfig = findConfig({
1724
+ dir: packageRoot,
1725
+ customConfigFilename
1726
+ });
1699
1727
  if (maybeConfig) {
1700
1728
  return MicrofrontendsServer.fromFile({
1701
1729
  filePath: maybeConfig,
@@ -1704,11 +1732,9 @@ var MicrofrontendsServer = class {
1704
1732
  }
1705
1733
  const repositoryRoot = findRepositoryRoot();
1706
1734
  const isMonorepo2 = isMonorepo({ repositoryRoot });
1707
- if (typeof process.env.VC_MICROFRONTENDS_CONFIG === "string") {
1708
- const maybeConfigFromEnv = (0, import_node_path8.resolve)(
1709
- packageRoot,
1710
- process.env.VC_MICROFRONTENDS_CONFIG
1711
- );
1735
+ const configFromEnv = process.env.VC_MICROFRONTENDS_CONFIG;
1736
+ if (typeof configFromEnv === "string") {
1737
+ const maybeConfigFromEnv = (0, import_node_path8.resolve)(packageRoot, configFromEnv);
1712
1738
  if (maybeConfigFromEnv) {
1713
1739
  return MicrofrontendsServer.fromFile({
1714
1740
  filePath: maybeConfigFromEnv,
@@ -1717,7 +1743,8 @@ var MicrofrontendsServer = class {
1717
1743
  }
1718
1744
  } else {
1719
1745
  const maybeConfigFromVercel = findConfig({
1720
- dir: (0, import_node_path8.join)(packageRoot, ".vercel")
1746
+ dir: (0, import_node_path8.join)(packageRoot, ".vercel"),
1747
+ customConfigFilename
1721
1748
  });
1722
1749
  if (maybeConfigFromVercel) {
1723
1750
  return MicrofrontendsServer.fromFile({
@@ -1728,9 +1755,13 @@ var MicrofrontendsServer = class {
1728
1755
  if (isMonorepo2) {
1729
1756
  const defaultPackage = inferMicrofrontendsLocation({
1730
1757
  repositoryRoot,
1731
- applicationContext
1758
+ applicationContext,
1759
+ customConfigFilename
1760
+ });
1761
+ const maybeConfigFromDefault = findConfig({
1762
+ dir: defaultPackage,
1763
+ customConfigFilename
1732
1764
  });
1733
- const maybeConfigFromDefault = findConfig({ dir: defaultPackage });
1734
1765
  if (maybeConfigFromDefault) {
1735
1766
  return MicrofrontendsServer.fromFile({
1736
1767
  filePath: maybeConfigFromDefault,
@@ -2112,12 +2143,6 @@ var localAuthHtml = ({
2112
2143
  var MFE_LOCAL_PROXY_HEADER = "x-vercel-mfe-local-proxy-origin";
2113
2144
  var MFE_FLAG_VALUE = "vercel-mfe-flag-value";
2114
2145
  var MFE_FLAG_VALUE_HEADER = `x-${MFE_FLAG_VALUE}`;
2115
- var MFE_DEBUG = process.env.MFE_DEBUG;
2116
- var mfeDebug = (message) => {
2117
- if (MFE_DEBUG === "true" || MFE_DEBUG === "1") {
2118
- console.log(message);
2119
- }
2120
- };
2121
2146
  var ProxyRequestRouter = class {
2122
2147
  constructor(config, {
2123
2148
  localApps
@@ -2130,8 +2155,10 @@ var ProxyRequestRouter = class {
2130
2155
  return this.getApplicationTarget(defaultApp);
2131
2156
  }
2132
2157
  getApplicationTarget(application) {
2133
- const useDev = this.localApps.find(
2134
- (name) => name === application.name || name === application.packageName
2158
+ const useDev = Boolean(
2159
+ this.localApps.find(
2160
+ (name) => name === application.name || name === application.packageName
2161
+ )
2135
2162
  );
2136
2163
  let applicationName = application.name;
2137
2164
  let host = useDev ? application.development.local : application.fallback;
@@ -2151,7 +2178,9 @@ var ProxyRequestRouter = class {
2151
2178
  protocol,
2152
2179
  hostname,
2153
2180
  port,
2154
- application: applicationName
2181
+ application: applicationName,
2182
+ isLocal: useDev,
2183
+ originalApplication: application.name
2155
2184
  };
2156
2185
  }
2157
2186
  /**
@@ -2225,8 +2254,8 @@ var ProxyRequestRouter = class {
2225
2254
  if (target)
2226
2255
  return target;
2227
2256
  const defaultHost = this.getDefaultHost(config);
2228
- mfeDebug(
2229
- `no matching routes, routing ${path7} to default application: ${JSON.stringify(defaultHost)}`
2257
+ console.log(
2258
+ ` ${path7} - Did not match any routes. Routing to default app: ${formatProxyTarget(defaultHost)}`
2230
2259
  );
2231
2260
  return { path: path7, ...defaultHost };
2232
2261
  }
@@ -2242,8 +2271,8 @@ var ProxyRequestRouter = class {
2242
2271
  const target = this.getApplicationTarget(application);
2243
2272
  if (middlewareMfeZone) {
2244
2273
  if (middlewareMfeZone === application.name) {
2245
- mfeDebug(
2246
- `routing ${path7} to '${target.application}' at ${target.hostname} according to 'x-vercel-mfe-zone' header`
2274
+ console.log(
2275
+ ` ${path7} - Routing to ${formatProxyTarget(target)} according to 'x-vercel-mfe-zone' header`
2247
2276
  );
2248
2277
  return { path: path7, ...target };
2249
2278
  }
@@ -2262,15 +2291,12 @@ var ProxyRequestRouter = class {
2262
2291
  for (const childPath of group.paths) {
2263
2292
  const regexp = (0, import_path_to_regexp3.pathToRegexp)(childPath);
2264
2293
  if (regexp.test(url.pathname)) {
2265
- mfeDebug(
2266
- `routing ${path7} to '${target.application}' at ${target.hostname}`
2267
- );
2268
2294
  if (group.flag) {
2269
2295
  if (mfeFlagValue === true) {
2270
2296
  } else if (mfeFlagValue === false) {
2271
2297
  continue;
2272
2298
  } else {
2273
- mfeDebug(
2299
+ console.log(
2274
2300
  "Routing group is behind flag. Routing to default app to check flag via middleware."
2275
2301
  );
2276
2302
  if (!this.isDefaultAppLocal()) {
@@ -2282,6 +2308,9 @@ var ProxyRequestRouter = class {
2282
2308
  return null;
2283
2309
  }
2284
2310
  }
2311
+ console.log(
2312
+ ` ${path7} - Matched ${childPath}. Routing to ${formatProxyTarget(target)}`
2313
+ );
2285
2314
  return { path: path7, ...target };
2286
2315
  }
2287
2316
  }
@@ -2310,8 +2339,8 @@ var ProxyRequestRouter = class {
2310
2339
  for (const rewrite of rewrites) {
2311
2340
  for (const assetPrefix of assetPrefixes) {
2312
2341
  if ((0, import_path_to_regexp3.pathToRegexp)(`/${assetPrefix}${rewrite}`).test(pathname)) {
2313
- mfeDebug(
2314
- `routing ${pathname} to '${target.application}' at ${target.hostname}`
2342
+ console.log(
2343
+ ` ${pathname} - Matched asset prefix. Routing to ${formatProxyTarget(target)}`
2315
2344
  );
2316
2345
  return {
2317
2346
  path: path7,
@@ -2339,13 +2368,16 @@ var ProxyRequestRouter = class {
2339
2368
  url: refererURL,
2340
2369
  applications
2341
2370
  });
2342
- mfeDebug(
2343
- `routing nextjs stack frame request to ${refererApp?.application}`
2371
+ if (!refererApp) {
2372
+ return null;
2373
+ }
2374
+ console.log(
2375
+ ` ${refererURL.pathname} - Routing nextjs stack frame request to ${formatProxyTarget(refererApp)}`
2344
2376
  );
2345
- return refererApp ? {
2377
+ return {
2346
2378
  ...refererApp,
2347
2379
  path: `${url.pathname}${url.search}`
2348
- } : null;
2380
+ };
2349
2381
  }
2350
2382
  checkNextSourceMap({ url }) {
2351
2383
  const isSourceMap = (0, import_path_to_regexp3.pathToRegexp)("/__nextjs_source-map").test(url.pathname);
@@ -2354,11 +2386,15 @@ var ProxyRequestRouter = class {
2354
2386
  }
2355
2387
  const localApp = this.getArbitraryLocalApp();
2356
2388
  if (!localApp) {
2357
- mfeDebug("no locally running application to route request to");
2389
+ console.error(
2390
+ ` ${url.pathname} - No locally running application to route request to`
2391
+ );
2358
2392
  return null;
2359
2393
  }
2360
2394
  const target = this.getApplicationTarget(localApp);
2361
- mfeDebug(`routing nextjs source map request to ${target.application}`);
2395
+ console.log(
2396
+ ` ${url.pathname} - Routing nextjs source map request to randomly selected local application: ${formatProxyTarget(target)}`
2397
+ );
2362
2398
  return {
2363
2399
  ...target,
2364
2400
  path: `${url.pathname}${url.search}`
@@ -2374,7 +2410,9 @@ var ProxyRequestRouter = class {
2374
2410
  }
2375
2411
  const imageUrl = url.searchParams.get("url");
2376
2412
  if (!imageUrl) {
2377
- mfeDebug("no url parameter found in _next/image request");
2413
+ console.error(
2414
+ ` ${url.pathname}?${url.search} - No url parameter found in _next/image request`
2415
+ );
2378
2416
  return null;
2379
2417
  }
2380
2418
  const decodedPath = decodeURIComponent(imageUrl);
@@ -2384,11 +2422,16 @@ var ProxyRequestRouter = class {
2384
2422
  url: imageURL,
2385
2423
  applications
2386
2424
  });
2387
- mfeDebug(`routing nextjs image request to ${imageApp?.application}`);
2388
- return imageApp ? {
2425
+ if (!imageApp) {
2426
+ return null;
2427
+ }
2428
+ console.log(
2429
+ ` ${url.pathname}?${url.search} - Routing nextjs image request to ${formatProxyTarget(imageApp)}`
2430
+ );
2431
+ return {
2389
2432
  ...imageApp,
2390
2433
  path: `${url.pathname}${url.search}`
2391
- } : null;
2434
+ };
2392
2435
  }
2393
2436
  isDefaultAppLocal() {
2394
2437
  const defaultApp = this.config.getDefaultApplication();
@@ -2410,10 +2453,12 @@ var ProxyRequestRouter = class {
2410
2453
  var LocalProxy = class {
2411
2454
  constructor(config, {
2412
2455
  localApps,
2413
- proxyPort
2456
+ proxyPort,
2457
+ configFilePath
2414
2458
  }) {
2415
2459
  this.router = new ProxyRequestRouter(config, { localApps });
2416
2460
  this.proxyPort = proxyPort ?? this.router.config.getLocalProxyPort();
2461
+ this.configFilePath = configFilePath;
2417
2462
  this.proxy = import_http_proxy.default.createProxyServer({ secure: true });
2418
2463
  this.proxy.on("error", (err, req, res) => {
2419
2464
  if (res instanceof http.ServerResponse) {
@@ -2423,9 +2468,12 @@ var LocalProxy = class {
2423
2468
  }
2424
2469
  const target = this.router.getTarget(req);
2425
2470
  res.end(
2426
- `Error proxying request to ${target.application}. Is the server running locally on port ${target.port}?`
2471
+ `Error proxying request to ${formatProxyTarget(target)}. Is the server running locally on port ${target.port}?`
2472
+ );
2473
+ console.error(
2474
+ `Error proxying request for ${formatProxyTarget(target)}: `,
2475
+ err
2427
2476
  );
2428
- console.error(`Error proxying request for ${target.application}: `, err);
2429
2477
  });
2430
2478
  }
2431
2479
  static fromFile(filePath, {
@@ -2441,7 +2489,11 @@ var LocalProxy = class {
2441
2489
  microfrontends = MicrofrontendsServer.infer();
2442
2490
  }
2443
2491
  LocalProxy.validateLocalApps(localApps, microfrontends.config);
2444
- return new LocalProxy(microfrontends.config, { localApps, proxyPort });
2492
+ return new LocalProxy(microfrontends.config, {
2493
+ localApps,
2494
+ proxyPort,
2495
+ configFilePath: filePath
2496
+ });
2445
2497
  }
2446
2498
  static validateLocalApps(localApps, config) {
2447
2499
  const unknownApps = [];
@@ -2478,13 +2530,37 @@ var LocalProxy = class {
2478
2530
  }
2479
2531
  });
2480
2532
  httpServer.listen(this.proxyPort, () => {
2481
- console.log(`Microfrontends Proxy: http://localhost:${this.proxyPort}`);
2533
+ this.displayStartupMessage();
2482
2534
  });
2483
2535
  }
2484
2536
  handleRequest(req, res) {
2485
2537
  if (this.handleProxyInfoRequest(req.url, res)) {
2486
2538
  return;
2487
2539
  }
2540
+ if (req.url?.includes("//")) {
2541
+ const originalUrl = req.url;
2542
+ if (originalUrl) {
2543
+ const normalizedUrl = originalUrl.replaceAll(/\/[\\/]+/g, "/");
2544
+ if (normalizedUrl !== originalUrl) {
2545
+ res.writeHead(307, {
2546
+ Location: normalizedUrl,
2547
+ // Copy incoming request headers except hop-by-hop headers and Location
2548
+ ...Object.fromEntries(
2549
+ Object.entries(req.headers).filter(
2550
+ ([key]) => ![
2551
+ "connection",
2552
+ "content-length",
2553
+ "transfer-encoding",
2554
+ "location"
2555
+ ].includes(key.toLowerCase())
2556
+ )
2557
+ )
2558
+ });
2559
+ res.end();
2560
+ return;
2561
+ }
2562
+ }
2563
+ }
2488
2564
  const target = this.router.getTarget(req);
2489
2565
  const { req: strippedReq, mfeFlagValue } = removeMfeFlagQuery(req);
2490
2566
  if (target.protocol === "https") {
@@ -2518,7 +2594,7 @@ var LocalProxy = class {
2518
2594
  })
2519
2595
  );
2520
2596
  }
2521
- if (realRes.statusCode === 307) {
2597
+ if (realRes.statusCode === 307 || realRes.statusCode === 308 || realRes.statusCode === 302 || realRes.statusCode === 301) {
2522
2598
  const locationHeader = realRes.headers.location;
2523
2599
  if (locationHeader) {
2524
2600
  const redirectUrl = new import_node_url.URL(
@@ -2579,6 +2655,79 @@ var LocalProxy = class {
2579
2655
  }
2580
2656
  return false;
2581
2657
  }
2658
+ displayStartupMessage() {
2659
+ const allApps = this.router.config.getAllApplications();
2660
+ const localApps = [];
2661
+ const fallbackApps = [];
2662
+ const defaultApp = this.router.config.getDefaultApplication();
2663
+ const defaultFallback = defaultApp.fallback.host;
2664
+ for (const app of allApps) {
2665
+ const isLocal = this.router.localApps.find(
2666
+ (name) => name === app.name || name === app.packageName
2667
+ );
2668
+ if (isLocal) {
2669
+ localApps.push({
2670
+ name: app.name,
2671
+ port: app.development.local.port
2672
+ });
2673
+ } else {
2674
+ const target = this.router.getApplicationTarget(app);
2675
+ let fallbackHost = target.hostname;
2676
+ if (!app.fallback) {
2677
+ fallbackHost = defaultFallback;
2678
+ }
2679
+ fallbackApps.push({
2680
+ name: app.name,
2681
+ fallback: fallbackHost
2682
+ });
2683
+ }
2684
+ }
2685
+ console.log(`
2686
+ \u25B2 Microfrontends Proxy (${package_default.version}) Started`);
2687
+ console.log(` - Proxy URL: http://localhost:${this.proxyPort}`);
2688
+ if (this.configFilePath) {
2689
+ console.log(` - Config: ${this.configFilePath}`);
2690
+ }
2691
+ if (localApps.length > 0) {
2692
+ console.log(" - Local Applications:");
2693
+ const displayLocalApps = localApps.length > 5 ? [
2694
+ ...localApps.slice(0, 5),
2695
+ { name: `... and ${localApps.length - 5} more`, port: void 0 }
2696
+ ] : localApps;
2697
+ for (const app of displayLocalApps) {
2698
+ if (app.port !== void 0) {
2699
+ console.log(` \u2022 ${app.name} (port ${app.port})`);
2700
+ } else {
2701
+ console.log(` \u2022 ${app.name}`);
2702
+ }
2703
+ }
2704
+ }
2705
+ if (fallbackApps.length > 0) {
2706
+ console.log(" - Fallback Applications:");
2707
+ const displayFallbackApps = fallbackApps.length > 5 ? [
2708
+ ...fallbackApps.slice(0, 5),
2709
+ { name: `... and ${fallbackApps.length - 5} more`, fallback: "" }
2710
+ ] : fallbackApps;
2711
+ for (const app of displayFallbackApps) {
2712
+ if (app.fallback) {
2713
+ console.log(` \u2022 ${app.name} \u2192 ${app.fallback}`);
2714
+ } else {
2715
+ console.log(` \u2022 ${app.name}`);
2716
+ }
2717
+ }
2718
+ }
2719
+ if (localApps.length === 0 && fallbackApps.length === 0) {
2720
+ console.log(" - No applications configured\n");
2721
+ }
2722
+ if (localApps.length > 0) {
2723
+ console.log(
2724
+ "\nRequests directly to the ports of these applications may be automatically\nredirected to this proxy. Set the MFE_DISABLE_LOCAL_PROXY_REWRITE=1\nenvironment variable to disable this behavior."
2725
+ );
2726
+ }
2727
+ console.log(`
2728
+ ${"\u2500".repeat(50)}
2729
+ `);
2730
+ }
2582
2731
  };
2583
2732
  function extractMfeFlagValue(path7) {
2584
2733
  const host = "http://example.com";
@@ -2608,6 +2757,9 @@ function removeMfeFlagQuery(req) {
2608
2757
  req.url = path7;
2609
2758
  return { req, mfeFlagValue };
2610
2759
  }
2760
+ function formatProxyTarget(target) {
2761
+ return `${target.originalApplication} (${target.isLocal ? "local" : "fallback"})`;
2762
+ }
2611
2763
 
2612
2764
  // src/bin/port.ts
2613
2765
  var import_node_process = require("process");
@@ -2617,19 +2769,19 @@ var import_node_path9 = __toESM(require("path"), 1);
2617
2769
  var import_node_fs8 = __toESM(require("fs"), 1);
2618
2770
  function mfePort(packageDir) {
2619
2771
  const { name: appName, version } = getPackageJson(packageDir);
2620
- const result = loadConfig({ packageDir, appName });
2621
- if (!result) {
2622
- throw new MicrofrontendError(
2623
- `Unable to determine configured port for ${appName}`,
2624
- { type: "config", subtype: "not_found" }
2625
- );
2772
+ try {
2773
+ const result = loadConfig({ packageDir, appName });
2774
+ const { port } = result;
2775
+ return {
2776
+ name: appName,
2777
+ version,
2778
+ port
2779
+ };
2780
+ } catch (e) {
2781
+ throw new Error(`Unable to determine configured port for ${appName}`, {
2782
+ cause: e
2783
+ });
2626
2784
  }
2627
- const { port } = result;
2628
- return {
2629
- name: appName,
2630
- version,
2631
- port
2632
- };
2633
2785
  }
2634
2786
  function getPackageJson(packageDir) {
2635
2787
  const filePath = import_node_path9.default.join(packageDir, "package.json");
@@ -2639,14 +2791,9 @@ function loadConfig({
2639
2791
  packageDir,
2640
2792
  appName
2641
2793
  }) {
2642
- let config;
2643
- try {
2644
- config = MicrofrontendsServer.infer({
2645
- directory: packageDir
2646
- });
2647
- } catch (e) {
2648
- return void 0;
2649
- }
2794
+ const config = MicrofrontendsServer.infer({
2795
+ directory: packageDir
2796
+ });
2650
2797
  const app = config.config.getApplication(appName);
2651
2798
  const port = app.development.local.port ?? (app.development.local.protocol === "https" ? 443 : 80);
2652
2799
  return { port };
package/dist/config.cjs CHANGED
@@ -232,7 +232,7 @@ var MicrofrontendConfigClient = class {
232
232
  static fromEnv(config) {
233
233
  if (!config) {
234
234
  throw new Error(
235
- "Could not construct MicrofrontendConfigClient: configuration is empty or undefined. Did you set up your application with `withMicrofrontends`?"
235
+ "Could not construct MicrofrontendConfigClient: configuration is empty or undefined. Did you set up your application with `withMicrofrontends`? Is the local proxy running and this application is being accessed via the proxy port? See https://vercel.com/docs/microfrontends/local-development#setting-up-microfrontends-proxy"
236
236
  );
237
237
  }
238
238
  return new MicrofrontendConfigClient(JSON.parse(config));
@@ -372,10 +372,11 @@ function validatePathExpression(path) {
372
372
  }
373
373
  if (token.pattern !== PATH_DEFAULT_PATTERN && // Allows (a|b|c) and ((?!a|b|c).*) regex
374
374
  // Only limited regex is supported for now, due to performance considerations
375
- !/^(?<allowed>[\w]+(?:\|[^:|()]+)+)$|^\(\?!(?<disallowed>[\w]+(?:\|[^:|()]+)*)\)\.\*$/.test(
376
- token.pattern
375
+ // Allows all letters, numbers, and hyphens. Other characters must be escaped.
376
+ !/^(?<allowed>[\w-~]+(?:\|[^:|()]+)+)$|^\(\?!(?<disallowed>[\w-~]+(?:\|[^:|()]+)*)\)\.\*$/.test(
377
+ token.pattern.replace(/\\./g, "")
377
378
  )) {
378
- return `Path ${path} cannot use unsupported regular expression wildcard`;
379
+ return `Path ${path} cannot use unsupported regular expression wildcard. If the path includes special characters, they must be escaped with backslash (e.g. '\\(')`;
379
380
  }
380
381
  if (token.modifier && i !== tokens.length - 1) {
381
382
  return `Modifier ${token.modifier} is not allowed on wildcard :${token.name} in ${path}. Modifiers are only allowed in the last path component`;