UNPKG

@vercel/microfrontends

Version:

Defines configuration and utilities for microfrontends development

1,050 lines (1,029 loc) 34.8 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/next/testing/index.ts var testing_exports = {}; __export(testing_exports, { expandWildcards: () => expandWildcards, getAllChildApplicationNames: () => getAllChildApplicationNames, getAllMicrofrontendPaths: () => getAllMicrofrontendPaths, getFlaggedPathsForApp: () => getFlaggedPathsForApp, getLaunchedPathsForApp: () => getLaunchedPathsForApp, loadMicrofrontendConfigForEdge: () => loadMicrofrontendConfigForEdge, validateMiddlewareConfig: () => validateMiddlewareConfig, validateMiddlewareOnFlaggedPaths: () => validateMiddlewareOnFlaggedPaths, validateRouting: () => validateRouting }); module.exports = __toCommonJS(testing_exports); var import_node_fs = require("fs"); var import_server = require("next/server.js"); var import_path_to_regexp3 = require("path-to-regexp"); var import_jsonc_parser2 = require("jsonc-parser"); // src/config/microfrontends-config/isomorphic/index.ts var import_jsonc_parser = require("jsonc-parser"); // src/config/errors.ts var MicrofrontendError = class extends Error { constructor(message, opts) { super(message, { cause: opts?.cause }); this.name = "MicrofrontendsError"; this.source = opts?.source ?? "@vercel/microfrontends"; this.type = opts?.type ?? "unknown"; this.subtype = opts?.subtype; Error.captureStackTrace(this, MicrofrontendError); } isKnown() { return this.type !== "unknown"; } isUnknown() { return !this.isKnown(); } /** * Converts an error to a MicrofrontendsError. * @param original - The original error to convert. * @returns The converted MicrofrontendsError. */ static convert(original, opts) { if (opts?.fileName) { const err = MicrofrontendError.convertFSError(original, opts.fileName); if (err) { return err; } } if (original.message.includes( "Code generation from strings disallowed for this context" )) { return new MicrofrontendError(original.message, { type: "config", subtype: "unsupported_validation_env", source: "ajv" }); } return new MicrofrontendError(original.message); } static convertFSError(original, fileName) { if (original instanceof Error && "code" in original) { if (original.code === "ENOENT") { return new MicrofrontendError(`Could not find "${fileName}"`, { type: "config", subtype: "unable_to_read_file", source: "fs" }); } if (original.code === "EACCES") { return new MicrofrontendError( `Permission denied while accessing "${fileName}"`, { type: "config", subtype: "invalid_permissions", source: "fs" } ); } } if (original instanceof SyntaxError) { return new MicrofrontendError( `Failed to parse "${fileName}": Invalid JSON format.`, { type: "config", subtype: "invalid_syntax", source: "fs" } ); } return null; } /** * Handles an unknown error and returns a MicrofrontendsError instance. * @param err - The error to handle. * @returns A MicrofrontendsError instance. */ static handle(err, opts) { if (err instanceof MicrofrontendError) { return err; } if (err instanceof Error) { return MicrofrontendError.convert(err, opts); } if (typeof err === "object" && err !== null) { if ("message" in err && typeof err.message === "string") { return MicrofrontendError.convert(new Error(err.message), opts); } } return new MicrofrontendError("An unknown error occurred"); } }; // src/config/microfrontends-config/utils/get-config-from-env.ts function getConfigStringFromEnv() { const config = process.env.MFE_CONFIG; if (!config) { throw new MicrofrontendError(`Missing "MFE_CONFIG" in environment.`, { type: "config", subtype: "not_found_in_env" }); } return config; } // src/config/schema/utils/is-default-app.ts function isDefaultApp(a) { return !("routing" in a); } // src/config/microfrontends-config/client/index.ts var import_path_to_regexp = require("path-to-regexp"); var regexpCache = /* @__PURE__ */ new Map(); var getRegexp = (path) => { const existing = regexpCache.get(path); if (existing) { return existing; } const regexp = (0, import_path_to_regexp.pathToRegexp)(path); regexpCache.set(path, regexp); return regexp; }; var MicrofrontendConfigClient = class { constructor(config, opts) { this.pathCache = {}; this.serialized = config; if (opts?.removeFlaggedPaths) { for (const app of Object.values(config.applications)) { if (app.routing) { app.routing = app.routing.filter((match2) => !match2.flag); } } } this.applications = config.applications; } /** * Create a new `MicrofrontendConfigClient` from a JSON string. * Config must be passed in to remain framework agnostic */ static fromEnv(config, opts) { if (!config) { throw new Error( "Could not construct MicrofrontendConfigClient: configuration is empty or undefined. Did you set up your application with `withMicrofrontends`?" ); } return new MicrofrontendConfigClient( JSON.parse(config), opts ); } isEqual(other) { return this === other || JSON.stringify(this.applications) === JSON.stringify(other.applications); } getApplicationNameForPath(path) { if (!path.startsWith("/")) { throw new Error(`Path must start with a /`); } if (this.pathCache[path]) { return this.pathCache[path]; } const pathname = new URL(path, "https://example.com").pathname; for (const [name, application] of Object.entries(this.applications)) { if (application.routing) { for (const group of application.routing) { for (const childPath of group.paths) { const regexp = getRegexp(childPath); if (regexp.test(pathname)) { this.pathCache[path] = name; return name; } } } } } const defaultApplication = Object.entries(this.applications).find( ([, application]) => application.default ); if (!defaultApplication) { return null; } this.pathCache[path] = defaultApplication[0]; return defaultApplication[0]; } serialize() { return this.serialized; } }; // src/config/overrides/constants.ts var OVERRIDES_COOKIE_PREFIX = "vercel-micro-frontends-override"; var OVERRIDES_ENV_COOKIE_PREFIX = `${OVERRIDES_COOKIE_PREFIX}:env:`; // src/config/overrides/is-override-cookie.ts function isOverrideCookie(cookie) { return Boolean(cookie.name?.startsWith(OVERRIDES_COOKIE_PREFIX)); } // src/config/overrides/get-override-from-cookie.ts function getOverrideFromCookie(cookie) { if (!isOverrideCookie(cookie) || !cookie.value) return; return { application: cookie.name.replace(OVERRIDES_ENV_COOKIE_PREFIX, ""), host: cookie.value }; } // src/config/overrides/parse-overrides.ts function parseOverrides(cookies) { const overridesConfig = { applications: {} }; cookies.forEach((cookie) => { const override = getOverrideFromCookie(cookie); if (!override) return; overridesConfig.applications[override.application] = { environment: { host: override.host } }; }); return overridesConfig; } // src/config/microfrontends-config/isomorphic/validation.ts var import_path_to_regexp2 = require("path-to-regexp"); var LIST_FORMATTER = new Intl.ListFormat("en", { style: "long", type: "conjunction" }); var validateConfigPaths = (applicationConfigsById) => { if (!applicationConfigsById) { return; } const pathsByApplicationId = /* @__PURE__ */ new Map(); const errors = []; for (const [id, app] of Object.entries(applicationConfigsById)) { if (isDefaultApp(app)) { continue; } for (const pathMatch of app.routing) { for (const path of pathMatch.paths) { const maybeError = validatePathExpression(path); if (maybeError) { errors.push(maybeError); } else { const existing = pathsByApplicationId.get(path); if (existing) { existing.applications.push(id); } else { pathsByApplicationId.set(path, { applications: [id], matcher: (0, import_path_to_regexp2.pathToRegexp)(path), applicationId: id }); } } } } } const entries = Array.from(pathsByApplicationId.entries()); for (const [path, { applications: ids, matcher, applicationId }] of entries) { if (ids.length > 1) { errors.push( `Duplicate path "${path}" for applications "${ids.join(", ")}"` ); } for (const [ matchPath, { applications: matchIds, applicationId: matchApplicationId } ] of entries) { if (path === matchPath) { continue; } if (applicationId === matchApplicationId) { continue; } if (matcher.test(matchPath)) { const source = `"${path}" of application${ids.length > 0 ? "s" : ""} ${ids.join(", ")}`; const destination = `"${matchPath}" of application${matchIds.length > 0 ? "s" : ""} ${matchIds.join(", ")}`; errors.push( `Overlapping path detected between ${source} and ${destination}` ); } } } if (errors.length) { throw new MicrofrontendError( `Invalid paths: ${errors.join(", ")}. See supported paths in the documentation https://vercel.com/docs/microfrontends/path-routing#supported-path-expressions.`, { type: "config", subtype: "conflicting_paths" } ); } }; var PATH_DEFAULT_PATTERN = "[^\\/#\\?]+?"; function validatePathExpression(path) { try { const tokens = (0, import_path_to_regexp2.parse)(path); if (/(?<!\\)\{/.test(path)) { return `Optional paths are not supported: ${path}`; } if (/(?<!\\|\()\?/.test(path)) { return `Optional paths are not supported: ${path}`; } if (/\/[^/]*(?<!\\):[^/]*(?<!\\):[^/]*/.test(path)) { return `Only one wildcard is allowed per path segment: ${path}`; } for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; if (token === void 0) { return `token ${i} in ${path} is undefined, this shouldn't happen`; } if (typeof token !== "string") { if (!token.name) { return `Only named wildcards are allowed: ${path} (hint: add ":path" to the wildcard)`; } if (token.pattern !== PATH_DEFAULT_PATTERN && // Allows (a|b|c) and ((?!a|b|c).*) regex // Only limited regex is supported for now, due to performance considerations !/^(?<allowed>[\w]+(?:\|[^:|()]+)+)$|^\(\?!(?<disallowed>[\w]+(?:\|[^:|()]+)*)\)\.\*$/.test( token.pattern )) { return `Path ${path} cannot use unsupported regular expression wildcard`; } if (token.modifier && i !== tokens.length - 1) { return `Modifier ${token.modifier} is not allowed on wildcard :${token.name} in ${path}. Modifiers are only allowed in the last path component`; } } } } catch (e) { const message = e instanceof Error ? e.message : String(e); return `Path ${path} could not be parsed into regexp: ${message}`; } return void 0; } var validateAppPaths = (name, app) => { for (const group of app.routing) { for (const p of group.paths) { if (p === "/") { continue; } if (p.endsWith("/")) { throw new MicrofrontendError( `Invalid path for application "${name}". ${p} must not end with a slash.`, { type: "application", subtype: "invalid_path" } ); } if (!p.startsWith("/")) { throw new MicrofrontendError( `Invalid path for application "${name}". ${p} must start with a slash.`, { type: "application", subtype: "invalid_path" } ); } } } }; var validateConfigDefaultApplication = (applicationConfigsById) => { if (!applicationConfigsById) { return; } const applicationsWithoutRouting = Object.entries( applicationConfigsById ).filter(([, app]) => isDefaultApp(app)); const numApplicationsWithoutRouting = applicationsWithoutRouting.reduce( (acc) => { return acc + 1; }, 0 ); if (numApplicationsWithoutRouting === 0) { throw new MicrofrontendError( "No default application found. At least one application needs to be the default by omitting routing.", { type: "config", subtype: "no_default_application" } ); } if (numApplicationsWithoutRouting > 1) { const applicationNamesMissingRouting = applicationsWithoutRouting.map( ([name]) => name ); throw new MicrofrontendError( `All applications except for the default app must contain the "routing" field. Applications that are missing routing: ${LIST_FORMATTER.format(applicationNamesMissingRouting)}.`, { type: "config", subtype: "multiple_default_applications" } ); } }; // src/config/microfrontends-config/isomorphic/utils/generate-asset-prefix.ts var PREFIX = "vc-ap"; function generateAssetPrefixFromName({ name }) { if (!name) { throw new Error("Name is required to generate an asset prefix"); } return `${PREFIX}-${name}`; } // src/config/microfrontends-config/isomorphic/utils/generate-port.ts function generatePortFromName({ name, minPort = 3e3, maxPort = 8e3 }) { if (!name) { throw new Error("Name is required to generate a port"); } let hash = 0; for (let i = 0; i < name.length; i++) { hash = (hash << 5) - hash + name.charCodeAt(i); hash |= 0; } hash = Math.abs(hash); const range = maxPort - minPort; const port = minPort + hash % range; return port; } // src/config/microfrontends-config/isomorphic/host.ts var Host = class { constructor(hostConfig, options) { if (typeof hostConfig === "string") { ({ protocol: this.protocol, host: this.host, port: this.port } = Host.parseUrl(hostConfig)); } else { const { protocol = "https", host, port } = hostConfig; this.protocol = protocol; this.host = host; this.port = port; } this.local = options?.isLocal; } static parseUrl(url, defaultProtocol = "https") { let hostToParse = url; if (!/^https?:\/\//.exec(hostToParse)) { hostToParse = `${defaultProtocol}://${hostToParse}`; } const parsed = new URL(hostToParse); if (!parsed.hostname) { throw new Error(Host.getMicrofrontendsError(url, "requires a host")); } if (parsed.hash) { throw new Error( Host.getMicrofrontendsError(url, "cannot have a fragment") ); } if (parsed.username || parsed.password) { throw new Error( Host.getMicrofrontendsError( url, "cannot have authentication credentials (username and/or password)" ) ); } if (parsed.pathname !== "/") { throw new Error(Host.getMicrofrontendsError(url, "cannot have a path")); } if (parsed.search) { throw new Error( Host.getMicrofrontendsError(url, "cannot have query parameters") ); } const protocol = parsed.protocol.slice(0, -1); return { protocol, host: parsed.hostname, port: parsed.port ? Number.parseInt(parsed.port) : void 0 }; } static getMicrofrontendsError(url, message) { return `Microfrontends configuration error: the URL ${url} in your microfrontends.json ${message}.`; } isLocal() { return this.local || this.host === "localhost" || this.host === "127.0.0.1"; } toString() { const url = this.toUrl(); return url.toString().replace(/\/$/, ""); } toUrl() { const url = `${this.protocol}://${this.host}${this.port ? `:${this.port}` : ""}`; return new URL(url); } }; var LocalHost = class extends Host { constructor({ appName, local }) { let protocol; let host; let port; if (typeof local === "number") { port = local; } else if (typeof local === "string") { if (/^\d+$/.test(local)) { port = Number.parseInt(local); } else { const parsed = Host.parseUrl(local, "http"); protocol = parsed.protocol; host = parsed.host; port = parsed.port; } } else if (local) { protocol = local.protocol; host = local.host; port = local.port; } super({ protocol: protocol ?? "http", host: host ?? "localhost", port: port ?? generatePortFromName({ name: appName }) }); } }; // src/config/microfrontends-config/isomorphic/utils/generate-automation-bypass-env-var-name.ts function generateAutomationBypassEnvVarName({ name }) { return `AUTOMATION_BYPASS_${name.toUpperCase().replace(/[^a-zA-Z0-9]/g, "_")}`; } // src/config/microfrontends-config/isomorphic/application.ts var Application = class { constructor(name, { app, overrides, isDefault }) { this.name = name; this.development = { local: new LocalHost({ appName: name, local: app.development?.local }), fallback: app.development?.fallback ? new Host(app.development.fallback) : void 0 }; if (app.development?.fallback) { this.fallback = new Host(app.development.fallback); } this.packageName = app.packageName; this.overrides = overrides?.environment ? { environment: new Host(overrides.environment) } : void 0; this.default = isDefault ?? false; this.serialized = app; } isDefault() { return this.default; } getAssetPrefix() { return generateAssetPrefixFromName({ name: this.name }); } getAutomationBypassEnvVarName() { return generateAutomationBypassEnvVarName({ name: this.name }); } serialize() { return this.serialized; } }; var DefaultApplication = class extends Application { constructor(name, { app, overrides }) { super(name, { app, overrides, isDefault: true }); this.default = true; this.fallback = new Host(app.development.fallback); } getAssetPrefix() { return ""; } }; var ChildApplication = class extends Application { constructor(name, { app, overrides }) { ChildApplication.validate(name, app); super(name, { app, overrides, isDefault: false }); this.default = false; this.routing = app.routing; } static validate(name, app) { validateAppPaths(name, app); } }; // src/config/microfrontends-config/isomorphic/constants.ts var DEFAULT_LOCAL_PROXY_PORT = 3024; // src/config/microfrontends-config/isomorphic/index.ts var MicrofrontendConfigIsomorphic = class { constructor({ config, overrides }) { this.childApplications = {}; MicrofrontendConfigIsomorphic.validate(config); const disableOverrides = config.options?.disableOverrides ?? false; this.overrides = overrides && !disableOverrides ? overrides : void 0; let defaultApplication; for (const [appId, appConfig] of Object.entries(config.applications)) { const appOverrides = !disableOverrides ? this.overrides?.applications[appId] : void 0; if (isDefaultApp(appConfig)) { defaultApplication = new DefaultApplication(appId, { app: appConfig, overrides: appOverrides }); } else { this.childApplications[appId] = new ChildApplication(appId, { app: appConfig, overrides: appOverrides }); } } if (!defaultApplication) { throw new MicrofrontendError( "Could not find default application in microfrontends configuration", { type: "application", subtype: "not_found" } ); } this.defaultApplication = defaultApplication; this.config = config; this.options = config.options; this.serialized = { config, overrides }; } static validate(config) { const c = typeof config === "string" ? (0, import_jsonc_parser.parse)(config) : config; validateConfigPaths(c.applications); validateConfigDefaultApplication(c.applications); return c; } static fromEnv({ cookies }) { return new MicrofrontendConfigIsomorphic({ config: (0, import_jsonc_parser.parse)(getConfigStringFromEnv()), overrides: parseOverrides(cookies ?? []) }); } isOverridesDisabled() { return this.options?.disableOverrides ?? false; } getConfig() { return this.config; } getApplicationsByType() { return { defaultApplication: this.defaultApplication, applications: Object.values(this.childApplications) }; } getChildApplications() { return Object.values(this.childApplications); } getAllApplications() { return [ this.defaultApplication, ...Object.values(this.childApplications) ].filter(Boolean); } getApplication(name) { if (this.defaultApplication.name === name || this.defaultApplication.packageName === name) { return this.defaultApplication; } const app = this.childApplications[name] || Object.values(this.childApplications).find( (child) => child.packageName === name ); if (!app) { throw new MicrofrontendError( `Could not find microfrontends configuration for application "${name}"`, { type: "application", subtype: "not_found" } ); } return app; } hasApplication(name) { try { this.getApplication(name); return true; } catch { return false; } } getApplicationByProjectName(projectName) { if (this.defaultApplication.name === projectName) { return this.defaultApplication; } return Object.values(this.childApplications).find( (app) => app.name === projectName ); } /** * Returns the default application. */ getDefaultApplication() { return this.defaultApplication; } /** * Returns the configured port for the local proxy */ getLocalProxyPort() { return this.config.options?.localProxyPort ?? DEFAULT_LOCAL_PROXY_PORT; } /** * Serializes the class back to the Schema type. * * NOTE: This is used when writing the config to disk and must always match the input Schema */ toSchemaJson() { return this.serialized.config; } toClientConfig() { const applications = Object.fromEntries( Object.entries(this.childApplications).map(([name, application]) => [ name, { default: false, routing: application.routing } ]) ); applications[this.defaultApplication.name] = { default: true }; return new MicrofrontendConfigClient({ applications }); } serialize() { return this.serialized; } }; // src/next/testing/index.ts function expandWildcards(path) { if (path.includes("/:path*") || path.includes("/:slug*")) { return [ path === "/:path*" || path === "/:slug*" ? "/" : path.replace("/:path*", "").replace("/:slug*", ""), path.replace("/:path*", "/foo").replace("/:slug*", "/foo"), path.replace("/:path*", "/foo/bar").replace("/:slug*", "/foo/bar") ]; } if (path.includes("/:path+") || path.includes("/:slug+")) { return [ path.replace("/:path+", "/foo").replace("/:slug+", "/foo"), path.replace("/:path+", "/foo/bar").replace("/:slug+", "/foo/bar") ]; } if (path.includes("/:path") || path.includes("/:slug")) { return [path.replace("/:path", "/foo").replace("/:slug", "/foo")]; } return [path]; } function loadMicrofrontendConfigForEdge(path) { const rawMfConfig = (0, import_jsonc_parser2.parse)((0, import_node_fs.readFileSync)(path, "utf-8")); return new MicrofrontendConfigIsomorphic({ config: rawMfConfig }); } function getAllChildApplicationNames(mfConfig) { return mfConfig.getChildApplications().map((app) => app.name); } function getLaunchedPathsForApp(mfConfig, appName) { const app = mfConfig.getApplication(appName); if (app instanceof DefaultApplication) { return []; } return [ `/${app.getAssetPrefix()}/_next/static`, ...app.routing.filter((group) => !group.flag).flatMap((group) => group.paths.flatMap(expandWildcards)) ]; } function getFlaggedPathsForApp(mfConfig, appName) { const app = mfConfig.getApplication(appName); if (app instanceof DefaultApplication) { return []; } return app.routing.filter((group) => Boolean(group.flag)).flatMap((group) => group.paths.flatMap(expandWildcards)); } function getAllMicrofrontendPaths(mfConfig) { return mfConfig.getChildApplications().flatMap((app) => { return app.routing.flatMap((group) => group.paths.flatMap(expandWildcards)); }); } function urlMatches(middlewareConfig, path, doNotMatchWithHasOrMissing) { if (!middlewareConfig.matcher) { return false; } const matchers = Array.isArray(middlewareConfig.matcher) ? middlewareConfig.matcher : [middlewareConfig.matcher]; for (let matcher of matchers) { matcher = typeof matcher === "string" ? { source: matcher } : matcher; if ((0, import_path_to_regexp3.match)(matcher.source)(path)) { if (doNotMatchWithHasOrMissing && (matcher.has || matcher.missing)) { return false; } return true; } } return false; } function validateMiddlewareConfig(middlewareConfig, microfrontendConfigOrPath, extraProductionMatches) { const microfrontendConfig = typeof microfrontendConfigOrPath === "string" ? loadMicrofrontendConfigForEdge(microfrontendConfigOrPath) : microfrontendConfigOrPath; const errors = []; const usedExtraProductionMatches = /* @__PURE__ */ new Set(); const wellKnownEndpoint = "/.well-known/vercel/microfrontends/client-config"; if (!urlMatches(middlewareConfig, wellKnownEndpoint)) { errors.push( `Middleware must be configured to match ${wellKnownEndpoint}. This path is used by the client to know which flagged paths are routed to which microfrontend based on the flag values for the session. See https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher for more information.` ); } for (const application of microfrontendConfig.getChildApplications()) { const matches = [...application.routing]; matches.push({ paths: [`/${application.getAssetPrefix()}/_next/:path+`] }); for (const aMatch of matches) { const isFlagged = Boolean(aMatch.flag); for (const path of aMatch.paths) { const pathsToTest = expandWildcards(path); for (const testPath of pathsToTest) { const pathForDisplay = `${testPath}${path === testPath ? "" : ` (synthesized from ${path})`}`; const productionUrlMatches = urlMatches(middlewareConfig, testPath); if (isFlagged) { if (!urlMatches(middlewareConfig, testPath, true)) { errors.push( `Middleware should be configured to match ${pathForDisplay}. Middleware config matchers for flagged paths should ALWAYS match.` ); break; } } else if (productionUrlMatches) { if (extraProductionMatches?.includes(path)) { usedExtraProductionMatches.add(path); } else { errors.push( `Middleware should not match ${pathForDisplay}. This path is routed to a microfrontend and will never reach the middleware for the default application.` ); } break; } } } } } const unusedExtraProductionMatches = extraProductionMatches?.filter( (x) => !usedExtraProductionMatches.has(x) ); if (unusedExtraProductionMatches?.length) { errors.push( `The following paths were passed to the extraProductionMatches parameter but were unused. You probably want to remove them from the extraProductionMatches parameter: ${unusedExtraProductionMatches.join(", ")}` ); } if (errors.length > 0) { const message = `Found the following inconsistencies between your microfrontend config ${typeof microfrontendConfigOrPath === "string" ? `(at ${microfrontendConfigOrPath}) ` : ""}and middleware config: - `; throw new Error(message + errors.join("\n\n- ")); } } async function validateMiddlewareOnFlaggedPaths(microfrontendConfigOrPath, middleware) { const initialEnv = process.env.VERCEL_ENV; const initialMfePreviewDomains = process.env.MFE_PREVIEW_DOMAINS; try { const microfrontendConfig = typeof microfrontendConfigOrPath === "string" ? loadMicrofrontendConfigForEdge(microfrontendConfigOrPath) : microfrontendConfigOrPath; const allAppNames = getAllChildApplicationNames(microfrontendConfig); const errors = []; for (const appName of allAppNames) { const flaggedPaths = getFlaggedPathsForApp(microfrontendConfig, appName); if (flaggedPaths.length) { for (const path of flaggedPaths) { const host = microfrontendConfig.defaultApplication.fallback.toString(); const requestPath = `${host}${path}`; const request = new import_server.NextRequest(requestPath); const response = await middleware( request, {} ); const routedZone = response?.headers.get( "x-middleware-request-x-vercel-mfe-zone" ); if (!response) { errors.push( `middleware did not action for ${requestPath}. Expected to route to ${appName}` ); } else if (response.status !== 200) { errors.push( `expected 200 status for ${requestPath} but got ${response.status}` ); } else if (routedZone !== appName) { errors.push( `expected ${requestPath} to route to ${appName}, but got ${routedZone}` ); } } } } if (errors.length) { throw new Error(errors.join("\n")); } } finally { process.env.VERCEL_ENV = initialEnv; process.env.MFE_PREVIEW_DOMAINS = initialMfePreviewDomains; } } function validateRouting(microfrontendConfigOrPath, routesToTest) { const microfrontendConfig = typeof microfrontendConfigOrPath === "string" ? loadMicrofrontendConfigForEdge(microfrontendConfigOrPath) : microfrontendConfigOrPath; const matches = /* @__PURE__ */ new Map(); for (const application of microfrontendConfig.getChildApplications()) { for (const route of application.routing) { for (const path of route.paths) { matches.set({ path, flag: route.flag }, application.name); } } } const errors = []; for (const [applicationName, paths] of Object.entries(routesToTest)) { if (!microfrontendConfig.hasApplication(applicationName)) { errors.push( `Application ${applicationName} does not exist in the microfrontends config. The applications in the config are: ${microfrontendConfig.getAllApplications().map((app) => app.name).join(", ")}` ); continue; } for (const expected of paths) { const path = typeof expected === "string" ? expected : expected.path; const flag = typeof expected === "string" ? void 0 : expected.flag; const matchedApplications = /* @__PURE__ */ new Map(); const matchesWithoutFlags = /* @__PURE__ */ new Map(); for (const [matcher, applicationMatched] of matches.entries()) { if ((0, import_path_to_regexp3.pathToRegexp)(matcher.path).test(path)) { if (!matcher.flag || matcher.flag === flag) { const existingMatches = matchedApplications.get(applicationMatched) ?? []; existingMatches.push(matcher.path); matchedApplications.set(applicationMatched, existingMatches); } else { matchesWithoutFlags.set(applicationName, matcher.flag); } } } if (matchedApplications.size === 0) { matchedApplications.set( microfrontendConfig.getDefaultApplication().name, ["fallback to default application"] ); } if (matchedApplications.size > 1) { const formattedMatches = Array.from( matchedApplications.entries().map( ([matchedApplication, matchers]) => `${matchedApplication} (on ${matchers.join(", ")})` ) ).join(", "); errors.push( `${path} can only match one application, but it matched multiple: ${formattedMatches}` ); } else if (!matchedApplications.has(applicationName)) { const actualMatch = matchedApplications.entries().next().value; if (!actualMatch) { throw new Error("this shouldn't happen"); } const [matchedApplication, matchers] = actualMatch; let extraMessage = ""; if (matchesWithoutFlags.has(applicationName)) { const flagToSet = matchesWithoutFlags.get(applicationName); extraMessage = ` It would've matched ${applicationName} if the ${flagToSet} flag was set. If this is what you want, replace ${path} in the paths-to-test list with {path: '${path}', flag: '${flagToSet}'}.`; } errors.push( `Expected ${path}${flag ? ` (with flag ${flag})` : ""} to match ${applicationName}, but it matched ${matchedApplication} (on ${matchers.join(", ")}).${extraMessage}` ); } } } if (errors.length) { throw new Error( `Incorrect microfrontends routing detected: - ${errors.join("\n- ")}` ); } } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { expandWildcards, getAllChildApplicationNames, getAllMicrofrontendPaths, getFlaggedPathsForApp, getLaunchedPathsForApp, loadMicrofrontendConfigForEdge, validateMiddlewareConfig, validateMiddlewareOnFlaggedPaths, validateRouting }); //# sourceMappingURL=testing.cjs.map