@vercel/microfrontends
Version:
Defines configuration and utilities for microfrontends development
1,019 lines (999 loc) • 33.1 kB
JavaScript
// src/next/testing/index.ts
import { readFileSync } from "node:fs";
import {
NextRequest
} from "next/server.js";
import { match, pathToRegexp as pathToRegexp3 } from "path-to-regexp";
import { parse as parse2 } from "jsonc-parser";
// src/config/microfrontends-config/isomorphic/index.ts
import { parse } from "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
import { pathToRegexp } from "path-to-regexp";
var regexpCache = /* @__PURE__ */ new Map();
var getRegexp = (path) => {
const existing = regexpCache.get(path);
if (existing) {
return existing;
}
const 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
import { pathToRegexp as pathToRegexp2, parse as parsePathRegexp } from "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: pathToRegexp2(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 = parsePathRegexp(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" ? parse(config) : config;
validateConfigPaths(c.applications);
validateConfigDefaultApplication(c.applications);
return c;
}
static fromEnv({
cookies
}) {
return new MicrofrontendConfigIsomorphic({
config: 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 = parse2(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 (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 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 (pathToRegexp3(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- ")}`
);
}
}
export {
expandWildcards,
getAllChildApplicationNames,
getAllMicrofrontendPaths,
getFlaggedPathsForApp,
getLaunchedPathsForApp,
loadMicrofrontendConfigForEdge,
validateMiddlewareConfig,
validateMiddlewareOnFlaggedPaths,
validateRouting
};
//# sourceMappingURL=testing.js.map