@vercel/microfrontends
Version:
Defines configuration and utilities for microfrontends development
1,013 lines (991 loc) • 30.7 kB
JavaScript
// src/next/middleware/middleware.ts
import { NextResponse } from "next/server";
import { pathToRegexp as pathToRegexp3 } from "path-to-regexp";
// src/config/well-known/endpoints.ts
async function getWellKnownClientData(config, flagValues = {}) {
const clientConfig = config.toClientConfig();
for (const [applicationName, application] of Object.entries(
clientConfig.applications
)) {
if (!application.routing) {
continue;
}
const allPaths = [];
for (const pathGroup of application.routing) {
if (pathGroup.flag) {
const flagName = pathGroup.flag;
const flagFn = flagValues[flagName];
if (!flagFn) {
throw new Error(
`Flag "${flagName}" was specified to control routing for path group "${pathGroup.group}" in application ${applicationName} but not found in provided flag values.`
);
}
const flagEnabled = await flagFn();
if (flagEnabled) {
allPaths.push(...pathGroup.paths);
}
} else {
allPaths.push(...pathGroup.paths);
}
}
application.routing = allPaths.length > 0 ? [{ paths: allPaths }] : [];
}
return {
config: clientConfig.serialize()
};
}
// src/bin/local-proxy-is-running.ts
function localProxyIsRunning() {
return process.env.TURBO_TASK_HAS_MFE_PROXY === "true";
}
// 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/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/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.hasFlaggedPaths = config.hasFlaggedPaths ?? false;
for (const app of Object.values(config.applications)) {
if (app.routing) {
if (app.routing.some((match) => match.flag)) {
this.hasFlaggedPaths = true;
}
const newRouting = [];
const pathsWithoutFlags = [];
for (const group of app.routing) {
if (group.flag) {
if (opts?.removeFlaggedPaths) {
continue;
}
if (group.group) {
delete group.group;
}
newRouting.push(group);
} else {
pathsWithoutFlags.push(...group.paths);
}
}
if (pathsWithoutFlags.length > 0) {
newRouting.push({ paths: pathsWithoutFlags });
}
app.routing = newRouting;
}
}
this.serialized = config;
if (this.hasFlaggedPaths) {
this.serialized.hasFlaggedPaths = this.hasFlaggedPaths;
}
this.applications = config.applications;
}
/**
* Create a new `MicrofrontendConfigClient` from a JSON string.
* Config must be passed in to remain framework agnostic
*/
static fromEnv(config) {
if (!config) {
throw new Error(
"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"
);
}
return new MicrofrontendConfigClient(JSON.parse(config));
}
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/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 VALID_ASSET_PREFIX_REGEXP = /^[a-z](?:[a-z0-9-]*[a-z0-9])?$/;
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
// Allows all letters, numbers, and hyphens. Other characters must be escaped.
!/^(?<allowed>[\w-~]+(?:\|[^:|()]+)+)$|^\(\?!(?<disallowed>[\w-~]+(?:\|[^:|()]+)*)\)\.\*$/.test(
token.pattern.replace(/\\./g, "")
)) {
return `Path ${path} cannot use unsupported regular expression wildcard. If the path includes special characters, they must be escaped with backslash (e.g. '\\(')`;
}
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" }
);
}
}
}
if (app.assetPrefix) {
if (!VALID_ASSET_PREFIX_REGEXP.test(app.assetPrefix)) {
throw new MicrofrontendError(
`Invalid asset prefix for application "${name}". ${app.assetPrefix} must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens.`,
{ type: "application", subtype: "invalid_asset_prefix" }
);
}
if (app.assetPrefix !== `vc-ap-${name}` && !app.routing.some(
(group) => group.paths.includes(`/${app.assetPrefix}/:path*`) && !group.flag
)) {
throw new MicrofrontendError(
`When \`assetPrefix\` is specified, \`/${app.assetPrefix}/:path*\` must be added the routing paths for the application. Changing the asset prefix is not a forwards and backwards compatible change, and the custom asset prefix should be added to \`paths\` and deployed before setting the \`assetPrefix\` field.`,
{ type: "application", subtype: "invalid_asset_prefix" }
);
}
}
};
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/hash-application-name.ts
import md5 from "md5";
function hashApplicationName(name) {
if (!name) {
throw new Error("Application name is required to generate hash");
}
return md5(name).substring(0, 6).padStart(6, "0");
}
// 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}-${hashApplicationName(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() {
const generatedAssetPrefix = generateAssetPrefixFromName({
name: this.name
});
if ("assetPrefix" in this.serialized) {
return this.serialized.assetPrefix ?? generatedAssetPrefix;
}
return generatedAssetPrefix;
}
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}". If the name in package.json differs from your Vercel Project name, set the \`packageName\` field for the application in \`microfrontends.json\` to ensure that the configuration can be found locally.`,
{
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;
}
toClientConfig(options) {
const applications = Object.fromEntries(
Object.entries(this.childApplications).map(([name, application]) => [
hashApplicationName(name),
{
default: false,
routing: application.routing
}
])
);
applications[hashApplicationName(this.defaultApplication.name)] = {
default: true
};
return new MicrofrontendConfigClient(
{
applications
},
{
removeFlaggedPaths: options?.removeFlaggedPaths
}
);
}
/**
* 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;
}
serialize() {
return this.serialized;
}
};
// src/bin/logger.ts
function debug(...args) {
if (process.env.MFE_DEBUG) {
console.log(...args);
}
}
function info(...args) {
console.log(...args);
}
function warn(...args) {
console.warn(...args);
}
function error(...args) {
console.error(...args);
}
var logger = {
debug,
info,
warn,
error
};
// src/next/middleware/middleware.ts
function getMfeFlagHeader(req) {
const flagValue = req.headers.get("x-vercel-mfe-flag-value");
if (flagValue === "true") {
return true;
}
if (flagValue === "false") {
return false;
}
return null;
}
function getFlagHandler({
application,
flagFn,
flagName,
pattern,
localProxyPort
}) {
return async (req) => {
try {
const pathname = req.nextUrl.pathname;
const localProxyRunning = localProxyIsRunning();
const flagValueFromHeader = localProxyRunning ? getMfeFlagHeader(req) : null;
if (pattern.test(pathname) && (flagValueFromHeader ?? await flagFn())) {
const headers = new Headers(req.headers);
headers.set("x-vercel-mfe-zone", application.name);
const middlewareResponseInit = {
request: {
headers
}
};
if (localProxyRunning) {
logger.debug(
`Routing flagged path "${pathname}" to local proxy for application "${application.name}"`
);
const url = req.nextUrl;
url.host = `localhost:${localProxyPort}`;
return NextResponse.rewrite(url, middlewareResponseInit);
}
logger.debug(
`Routing flagged path "${pathname}" to application "${application.name}"`
);
return NextResponse.next(middlewareResponseInit);
}
} catch (e) {
logger.error(
`An error occured in the microfrontends middleware evaluating the flag "${flagName}":`,
e
);
throw e;
}
};
}
function getWellKnownClientConfigMiddleware(config, flagValues) {
return async (_) => {
return NextResponse.json(
await getWellKnownClientData(config, flagValues)
);
};
}
function getMicrofrontendsMiddleware({
request,
flagValues
}) {
const microfrontends = MicrofrontendConfigIsomorphic.fromEnv({
cookies: request.cookies.getAll()
});
const middlewares = [];
if (!process.env.NEXT_PUBLIC_MFE_CURRENT_APPLICATION) {
throw new Error(
"The NEXT_PUBLIC_MFE_CURRENT_APPLICATION environment variable is not set. Did you run `withMicrofrontends` in your Next.js config?"
);
}
const currentApplication = microfrontends.getApplication(
process.env.NEXT_PUBLIC_MFE_CURRENT_APPLICATION
);
if (!currentApplication.isDefault()) {
return middlewares;
}
middlewares.push({
src: "/.well-known/vercel/microfrontends/client-config",
fn: getWellKnownClientConfigMiddleware(microfrontends, flagValues)
});
const localProxyPort = microfrontends.getLocalProxyPort();
for (const application of microfrontends.getChildApplications()) {
for (const pathGroup of application.routing) {
const flagName = pathGroup.flag;
if (flagName) {
const flagFn = flagValues[flagName];
if (!flagFn) {
throw new Error(
`Flag "${flagName}" was specified to control routing for path group "${pathGroup.group}" in application ${application.name} but not found in provided flag values. See https://vercel.com/docs/microfrontends/path-routing#add-microfrontends-middleware for more information.`
);
}
for (const path of pathGroup.paths) {
const pattern = pathToRegexp3(path);
middlewares.push({
src: pattern,
fn: getFlagHandler({
application,
flagFn,
flagName,
pattern,
localProxyPort
})
});
}
}
}
}
return middlewares;
}
async function runMicrofrontendsMiddleware({
request,
flagValues
}) {
const pathname = request.nextUrl.pathname;
const middlewares = getMicrofrontendsMiddleware({
request,
flagValues
});
for (const mware of middlewares) {
if (typeof mware.src === "string" ? pathname === mware.src : mware.src.test(pathname)) {
const response = await mware.fn(request);
if (response) {
return response;
}
}
}
}
export {
getMicrofrontendsMiddleware,
runMicrofrontendsMiddleware
};
//# sourceMappingURL=middleware.js.map