@vercel/microfrontends
Version:
Defines configuration and utilities for microfrontends development
1 lines • 93.5 kB
Source Map (JSON)
{"version":3,"sources":["../../src/sveltekit/index.ts","../../src/bin/check-proxy.ts","../../src/config/microfrontends/server/index.ts","../../src/config/overrides/constants.ts","../../src/config/overrides/is-override-cookie.ts","../../src/config/overrides/get-override-from-cookie.ts","../../src/config/overrides/parse-overrides.ts","../../src/config/errors.ts","../../src/config/microfrontends-config/utils/get-config-from-env.ts","../../src/config/schema/utils/is-default-app.ts","../../src/config/microfrontends/utils/find-repository-root.ts","../../src/config/microfrontends/utils/find-default-package.ts","../../src/config/constants.ts","../../src/config/microfrontends/utils/is-monorepo.ts","../../src/config/microfrontends/utils/find-package-root.ts","../../src/config/microfrontends/utils/find-config.ts","../../src/config/microfrontends-config/isomorphic/index.ts","../../src/config/microfrontends-config/client/index.ts","../../src/config/microfrontends-config/isomorphic/validation.ts","../../src/config/microfrontends-config/isomorphic/utils/generate-asset-prefix.ts","../../src/config/microfrontends-config/isomorphic/utils/generate-port.ts","../../src/config/microfrontends-config/isomorphic/host.ts","../../src/config/microfrontends-config/isomorphic/utils/generate-automation-bypass-env-var-name.ts","../../src/config/microfrontends-config/isomorphic/application.ts","../../src/config/microfrontends-config/isomorphic/constants.ts","../../src/config/microfrontends/utils/get-application-context.ts","../../src/config/microfrontends/server/utils/get-output-file-path.ts","../../src/config/microfrontends/server/constants.ts","../../src/config/microfrontends/server/validation.ts","../../schema/schema.json","../../src/config/schema/utils/load.ts","../../src/sveltekit/config/index.ts"],"sourcesContent":["export { withMicrofrontends } from './config';\n","export function displayLocalProxyInfo(port: number): void {\n // TODO(olszewski): this is really icky, but since withMicroFrontends is called by two separate processes\n // we can't rely on some shared state so we instead use env to store state\n const { MFE_PROXY_MESSAGE_PRINTED, TURBO_TASK_HAS_MFE_PROXY } = process.env;\n if (\n TURBO_TASK_HAS_MFE_PROXY === 'true' &&\n MFE_PROXY_MESSAGE_PRINTED !== 'true'\n ) {\n process.env.MFE_PROXY_MESSAGE_PRINTED = 'true';\n // eslint-disable-next-line no-console\n console.log(`Microfrontends Proxy running on http://localhost:${port}`);\n }\n}\n","import fs from 'node:fs';\nimport { dirname } from 'node:path';\nimport type { Config } from '../../schema/types';\nimport { parseOverrides } from '../../overrides';\nimport type { OverridesConfig } from '../../overrides';\nimport { getConfigStringFromEnv } from '../../microfrontends-config/utils/get-config-from-env';\nimport { MicrofrontendError } from '../../errors';\nimport { isDefaultApp } from '../../schema/utils/is-default-app';\nimport { findRepositoryRoot } from '../utils/find-repository-root';\nimport { findDefaultMicrofrontendsPackage } from '../utils/find-default-package';\nimport { isMonorepo as isRepositoryMonorepo } from '../utils/is-monorepo';\nimport { findPackageRoot } from '../utils/find-package-root';\nimport { findConfig } from '../utils/find-config';\nimport { MicrofrontendConfigIsomorphic } from '../../microfrontends-config/isomorphic';\nimport { getApplicationContext } from '../utils/get-application-context';\nimport { getOutputFilePath } from './utils/get-output-file-path';\nimport { validateSchema } from './validation';\n\nexport class MicrofrontendsServer {\n config: MicrofrontendConfigIsomorphic;\n\n constructor({\n config,\n overrides,\n }: {\n config: Config;\n overrides?: OverridesConfig;\n }) {\n this.config = new MicrofrontendConfigIsomorphic({ config, overrides });\n }\n\n /**\n * Writes the configuration to a file.\n */\n writeConfig(\n opts: {\n pretty?: boolean;\n } = {\n pretty: true,\n },\n ): void {\n const outputPath = getOutputFilePath();\n\n // ensure the directory exists\n fs.mkdirSync(dirname(outputPath), { recursive: true });\n fs.writeFileSync(\n outputPath,\n JSON.stringify(\n this.config.toSchemaJson(),\n null,\n (opts.pretty ?? true) ? 2 : undefined,\n ),\n );\n }\n\n // --------- Static Methods ---------\n\n /**\n * Generates a MicrofrontendsServer instance from an unknown object.\n */\n static fromUnknown({\n config,\n cookies,\n }: {\n config: unknown;\n cookies?: { name: string; value: string }[];\n }): MicrofrontendsServer {\n const overrides = cookies ? parseOverrides(cookies) : undefined;\n if (typeof config === 'string') {\n return new MicrofrontendsServer({\n config: MicrofrontendsServer.validate(config),\n overrides,\n });\n }\n if (typeof config === 'object') {\n return new MicrofrontendsServer({\n config: config as Config,\n overrides,\n });\n }\n\n throw new MicrofrontendError(\n 'Invalid config: must be a string or an object',\n { type: 'config', subtype: 'does_not_match_schema' },\n );\n }\n\n /**\n * Generates a MicrofrontendsServer instance from the environment.\n * Uses additional validation that is only available when in a node runtime\n */\n static fromEnv({\n cookies,\n }: {\n cookies: { name: string; value: string }[];\n }): MicrofrontendsServer {\n return new MicrofrontendsServer({\n config: MicrofrontendsServer.validate(getConfigStringFromEnv()),\n overrides: parseOverrides(cookies),\n });\n }\n\n /**\n * Validates the configuration against the JSON schema\n */\n static validate(config: string | Config): Config {\n if (typeof config === 'string') {\n const c = validateSchema(config);\n return c;\n }\n return config;\n }\n\n /**\n * Looks up the configuration by inferring the package root and looking for a microfrontends config file. If a file is not found,\n * it will look for a package in the repository with a microfrontends file that contains the current application\n * and use that configuration.\n *\n * This can return either a Child or Main configuration.\n */\n static infer({\n directory,\n filePath,\n cookies,\n }: {\n directory?: string;\n filePath?: string;\n cookies?: { name: string; value: string }[];\n } = {}): MicrofrontendsServer {\n if (filePath) {\n return MicrofrontendsServer.fromFile({\n filePath,\n cookies,\n });\n }\n\n try {\n const packageRoot = findPackageRoot(directory);\n const { name: appName } = getApplicationContext({ packageRoot });\n\n // see if we have a config file at the package root\n const maybeConfig = findConfig({ dir: packageRoot });\n if (maybeConfig) {\n return MicrofrontendsServer.fromFile({\n filePath: maybeConfig,\n cookies,\n });\n }\n\n // if we don't have a microfrontends configuration file, see if we have another package in the repo that references this one\n const repositoryRoot = findRepositoryRoot();\n const isMonorepo = isRepositoryMonorepo({ repositoryRoot });\n if (isMonorepo) {\n // find the default package\n const defaultPackage = findDefaultMicrofrontendsPackage({\n repositoryRoot,\n applicationName: appName,\n });\n\n // see if we have a config file at the package root\n const maybeConfigFromDefault = findConfig({ dir: defaultPackage });\n if (maybeConfigFromDefault) {\n return MicrofrontendsServer.fromFile({\n filePath: maybeConfigFromDefault,\n cookies,\n });\n }\n }\n // will be caught below\n throw new Error('Unable to infer');\n } catch (e) {\n if (e instanceof MicrofrontendError) {\n throw e;\n }\n // we were unable to infer\n throw new MicrofrontendError(\n 'Unable to locate and parse microfrontends configuration',\n { cause: e, type: 'config', subtype: 'inference_failed' },\n );\n }\n }\n\n /*\n * Generates a MicrofrontendsServer instance from a file.\n */\n static fromFile({\n filePath,\n cookies,\n }: {\n filePath: string;\n cookies?: { name: string; value: string }[];\n }): MicrofrontendsServer {\n try {\n const configJson = fs.readFileSync(filePath, 'utf-8');\n const config = MicrofrontendsServer.validate(configJson);\n\n return new MicrofrontendsServer({\n config,\n overrides: cookies ? parseOverrides(cookies) : undefined,\n });\n } catch (e) {\n throw MicrofrontendError.handle(e, {\n fileName: filePath,\n });\n }\n }\n\n /*\n * Generates a MicrofrontendsServer instance from a file.\n */\n static fromMainConfigFile({\n filePath,\n overrides,\n }: {\n filePath: string;\n overrides?: OverridesConfig;\n }): MicrofrontendsServer {\n try {\n const config = fs.readFileSync(filePath, 'utf-8');\n const validatedConfig = MicrofrontendsServer.validate(config);\n const [defaultApplication] = Object.entries(validatedConfig.applications)\n .filter(([, app]) => isDefaultApp(app))\n .map(([name]) => name);\n // This should never get hit as MicrofrontendsServer.validate checks this if we're given a main config\n if (!defaultApplication) {\n throw new MicrofrontendError(\n 'No default application found. At least one application needs to be the default by omitting routing.',\n { type: 'config', subtype: 'no_default_application' },\n );\n }\n return new MicrofrontendsServer({\n config: validatedConfig,\n overrides,\n });\n } catch (e) {\n throw MicrofrontendError.handle(e, {\n fileName: filePath,\n });\n }\n }\n}\n","// cookie name needs to match proxy\n// https://github.com/vercel/proxy/blob/fb00d723136ad539a194e4a851dd272010527c35/lib/routing/micro_frontends_overrides.lua#L7\nexport const OVERRIDES_COOKIE_PREFIX = 'vercel-micro-frontends-override';\nexport const OVERRIDES_ENV_COOKIE_PREFIX = `${OVERRIDES_COOKIE_PREFIX}:env:`;\n","import { OVERRIDES_COOKIE_PREFIX } from './constants';\n\nexport function isOverrideCookie(cookie: { name?: string }): boolean {\n return Boolean(cookie.name?.startsWith(OVERRIDES_COOKIE_PREFIX));\n}\n","import { isOverrideCookie } from './is-override-cookie';\nimport { OVERRIDES_ENV_COOKIE_PREFIX } from './constants';\n\nexport function getOverrideFromCookie(cookie: {\n name: string;\n value?: string | null;\n}): { application: string; host: string } | undefined {\n if (!isOverrideCookie(cookie) || !cookie.value) return;\n return {\n application: cookie.name.replace(OVERRIDES_ENV_COOKIE_PREFIX, ''),\n host: cookie.value,\n };\n}\n","import type { OverridesConfig } from './types';\nimport { getOverrideFromCookie } from './get-override-from-cookie';\n\nexport function parseOverrides(\n cookies: { name: string; value?: string | null }[],\n): OverridesConfig {\n const overridesConfig: OverridesConfig = { applications: {} };\n\n cookies.forEach((cookie) => {\n const override = getOverrideFromCookie(cookie);\n if (!override) return;\n overridesConfig.applications[override.application] = {\n environment: { host: override.host },\n };\n });\n\n return overridesConfig;\n}\n","export type MicrofrontendErrorType =\n | 'config'\n | 'packageJson'\n | 'vercelJson'\n | 'application'\n | 'unknown';\n\nexport type MicrofrontendErrorSubtype =\n | 'not_found'\n | 'inference_failed'\n | 'not_found_in_env'\n | 'invalid_asset_prefix'\n | 'invalid_main_path'\n | 'does_not_match_schema'\n | 'unable_to_read_file'\n | 'unsupported_validation_env'\n | 'unsupported_version'\n | 'invalid_path'\n | 'invalid_permissions'\n | 'invalid_syntax'\n | 'missing_microfrontend_config_path'\n | 'unsupported_operation';\n\n// A mapping of error types to their subtypes.\ninterface TypeToSubtype {\n application:\n | 'invalid_asset_prefix'\n | 'invalid_path'\n | 'multiple_package_managers'\n | 'not_found';\n config:\n | 'conflicting_paths'\n | 'depcrecated_field'\n | 'does_not_match_schema'\n | 'invalid_main_path'\n | 'invalid_preview_deployment_suffix'\n | 'multiple_default_applications'\n | 'no_default_application'\n | 'not_found_in_env'\n | 'not_found'\n | 'inference_failed'\n | 'unable_to_read_file'\n | 'invalid_syntax'\n | 'invalid_permissions'\n | 'unsupported_operation'\n | 'unsupported_validation_env'\n | 'unsupported_version';\n packageJson:\n | 'missing_field_name'\n | 'unable_to_read_file'\n | 'invalid_permissions'\n | 'invalid_syntax';\n vercelJson:\n | 'missing_field_microfrontend_config_path'\n | 'unable_to_read_file'\n | 'invalid_permissions'\n | 'invalid_syntax';\n unknown: never;\n}\n\nexport type MicrofrontendErrorSource =\n | '@vercel/microfrontends'\n | '@vercel/microfrontends/next'\n | 'fs'\n | 'ajv';\n\nexport interface MicrofrontendErrorOptions<T extends MicrofrontendErrorType> {\n cause?: unknown;\n source?: MicrofrontendErrorSource;\n type?: T;\n subtype?: TypeToSubtype[T];\n}\n\ninterface HandleOptions {\n fileName?: string;\n}\n\nexport class MicrofrontendError<\n T extends MicrofrontendErrorType = 'unknown',\n> extends Error {\n public source: MicrofrontendErrorSource;\n public type: T;\n public subtype?: TypeToSubtype[T];\n\n constructor(message: string, opts?: MicrofrontendErrorOptions<T>) {\n super(message, { cause: opts?.cause });\n this.name = 'MicrofrontendsError';\n this.source = opts?.source ?? '@vercel/microfrontends';\n this.type = opts?.type ?? ('unknown' as T);\n this.subtype = opts?.subtype;\n Error.captureStackTrace(this, MicrofrontendError);\n }\n\n isKnown(): boolean {\n return this.type !== 'unknown';\n }\n\n isUnknown(): boolean {\n return !this.isKnown();\n }\n\n /**\n * Converts an error to a MicrofrontendsError.\n * @param original - The original error to convert.\n * @returns The converted MicrofrontendsError.\n */\n static convert(\n original: Error,\n opts?: HandleOptions,\n ): MicrofrontendError<MicrofrontendErrorType> {\n if (opts?.fileName) {\n const err = MicrofrontendError.convertFSError(original, opts.fileName);\n if (err) {\n return err;\n }\n }\n\n if (\n original.message.includes(\n 'Code generation from strings disallowed for this context',\n )\n ) {\n return new MicrofrontendError(original.message, {\n type: 'config',\n subtype: 'unsupported_validation_env',\n source: 'ajv',\n });\n }\n\n // unknown catch-all\n return new MicrofrontendError(original.message);\n }\n\n static convertFSError(\n original: Error,\n fileName: string,\n ): MicrofrontendError<MicrofrontendErrorType> | null {\n if (original instanceof Error && 'code' in original) {\n if (original.code === 'ENOENT') {\n return new MicrofrontendError(`Could not find \"${fileName}\"`, {\n type: 'config',\n subtype: 'unable_to_read_file',\n source: 'fs',\n });\n }\n if (original.code === 'EACCES') {\n return new MicrofrontendError(\n `Permission denied while accessing \"${fileName}\"`,\n {\n type: 'config',\n subtype: 'invalid_permissions',\n source: 'fs',\n },\n );\n }\n }\n\n if (original instanceof SyntaxError) {\n return new MicrofrontendError(\n `Failed to parse \"${fileName}\": Invalid JSON format.`,\n {\n type: 'config',\n subtype: 'invalid_syntax',\n source: 'fs',\n },\n );\n }\n\n return null;\n }\n\n /**\n * Handles an unknown error and returns a MicrofrontendsError instance.\n * @param err - The error to handle.\n * @returns A MicrofrontendsError instance.\n */\n static handle(\n err: unknown,\n opts?: HandleOptions,\n ): MicrofrontendError<MicrofrontendErrorType> {\n if (err instanceof MicrofrontendError) {\n return err as MicrofrontendError<MicrofrontendErrorType>;\n }\n\n // handle Error instances\n if (err instanceof Error) {\n return MicrofrontendError.convert(err, opts);\n }\n\n // handle object errors\n if (typeof err === 'object' && err !== null) {\n if ('message' in err && typeof err.message === 'string') {\n return MicrofrontendError.convert(new Error(err.message), opts);\n }\n }\n\n return new MicrofrontendError('An unknown error occurred');\n }\n}\n","import { MicrofrontendError } from '../../errors';\n\n/**\n * Utility to fetch the microfrontend configuration string from the environment.\n */\nexport function getConfigStringFromEnv(): string {\n const config = process.env.MFE_CONFIG;\n if (!config) {\n throw new MicrofrontendError(`Missing \"MFE_CONFIG\" in environment.`, {\n type: 'config',\n subtype: 'not_found_in_env',\n });\n }\n return config;\n}\n","import type { Application, DefaultApplication } from '../types';\n\nexport function isDefaultApp(a: Application): a is DefaultApplication {\n return !('routing' in a);\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nconst GIT_DIRECTORY = '.git';\n\nfunction hasGitDirectory(dir: string): boolean {\n const gitPath = path.join(dir, GIT_DIRECTORY);\n\n // Check for a .git directory (not a file)\n return fs.existsSync(gitPath) && fs.statSync(gitPath).isDirectory();\n}\n\nfunction hasPnpmWorkspaces(dir: string): boolean {\n return fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'));\n}\n\n/**\n * Find the root of the repository by looking for a `.git` directory. This should\n * work with submodules as well as it verifies that the `.git` directory is a\n * directory and not a file.\n *\n * TODO: support finding the root via other mechanisms to avoid the need for git to already be initialized\n */\nexport function findRepositoryRoot(startDir?: string): string {\n if (process.env.NX_WORKSPACE_ROOT) {\n // Trust NX's workspace root here so we don't have to rely on finding a .git\n // directory. There are some places (like the `vercel deploy` CLI command)\n // where the .git directory doesn't exist.\n // TODO: Figure out a way to find the repo root in non-NX `vercel deploy`s.\n return process.env.NX_WORKSPACE_ROOT;\n }\n\n let currentDir = startDir || process.cwd();\n\n while (currentDir !== path.parse(currentDir).root) {\n if (hasGitDirectory(currentDir) || hasPnpmWorkspaces(currentDir)) {\n return currentDir;\n }\n\n currentDir = path.dirname(currentDir);\n }\n\n throw new Error(\n 'Repository root not found. Specify the root of the repository with the `repository.root` option.',\n );\n}\n","import { dirname } from 'node:path';\nimport { readFileSync } from 'node:fs';\nimport { parse } from 'jsonc-parser';\nimport fg from 'fast-glob';\nimport type { Config } from '../../schema/types';\nimport { CONFIGURATION_FILENAMES } from '../../constants';\n\n// cache the path to default configuration to avoid having to walk the file system multiple times\nconst configCache: Record<string, string> = {};\n\ninterface FindDefaultMicrofrontendPackageArgs {\n repositoryRoot: string;\n applicationName: string;\n}\n\n/**\n * Given a repository root and a package name, find the path to the package.json file with the\n * given name.\n *\n * This method uses globby to find all package.json files and then reads them in parallel\n */\nfunction findDefaultMicrofrontendsPackages({\n repositoryRoot,\n applicationName,\n}: FindDefaultMicrofrontendPackageArgs): string | null {\n try {\n // eslint-disable-next-line import/no-named-as-default-member\n const microfrontendsJsonPaths = fg.globSync(\n `**/{${CONFIGURATION_FILENAMES.join(',')}}`,\n {\n cwd: repositoryRoot,\n absolute: true,\n onlyFiles: true,\n followSymbolicLinks: false,\n ignore: ['**/node_modules/**', '**/.git/**'],\n },\n );\n\n const matchingPaths: string[] = [];\n for (const microfrontendsJsonPath of microfrontendsJsonPaths) {\n try {\n const microfrontendsJsonContent = readFileSync(\n microfrontendsJsonPath,\n 'utf-8',\n );\n const microfrontendsJson = parse(microfrontendsJsonContent) as Config;\n\n if (microfrontendsJson.applications[applicationName]) {\n matchingPaths.push(microfrontendsJsonPath);\n }\n } catch (error) {\n // malformed json most likely, skip this file\n }\n }\n\n if (matchingPaths.length > 1) {\n throw new Error(\n `Found multiple default applications referencing \"${applicationName}\" in the repository, but only one is allowed.\\n${matchingPaths.join('\\n • ')}`,\n );\n }\n\n if (matchingPaths.length === 0) {\n throw new Error(\n `Could not find default application with \"applications.${applicationName}\"`,\n );\n }\n\n const [packageJsonPath] = matchingPaths as [string];\n return dirname(packageJsonPath);\n } catch (error) {\n return null;\n }\n}\n\n/**\n * Given a repository root and a package name, find the path to the package directory with\n * a microfrontends config that contains the given name in its applications.\n */\nexport function findDefaultMicrofrontendsPackage(\n opts: FindDefaultMicrofrontendPackageArgs,\n): string {\n // cache this with name to support multiple configurations in the same repository\n const cacheKey = `${opts.repositoryRoot}-${opts.applicationName}`;\n\n // Check if we have a cached result\n if (configCache[cacheKey]) {\n return configCache[cacheKey];\n }\n\n const result = findDefaultMicrofrontendsPackages(opts);\n\n if (!result) {\n throw new Error(\n 'Error trying to resolve the main microfrontends configuration',\n );\n }\n\n // Cache the result\n configCache[cacheKey] = result;\n return result;\n}\n","export const CONFIGURATION_FILENAMES = [\n 'microfrontends.jsonc',\n 'microfrontends.json',\n] as const;\n","import fs from 'node:fs';\nimport path from 'node:path';\n\n/**\n * Given a repository root, determine if the repository is using the workspace feature of any package manager.\n *\n * Supports npm, yarn, pnpm, bun, and vlt\n */\nexport function isMonorepo({\n repositoryRoot,\n}: {\n repositoryRoot: string;\n}): boolean {\n try {\n // pnpm can be validated just by the existence of the pnpm-workspace.yaml file\n if (fs.existsSync(path.join(repositoryRoot, 'pnpm-workspace.yaml'))) {\n return true;\n }\n\n // vlt can be validated just by the existence of the vlt-workspaces.json file\n if (fs.existsSync(path.join(repositoryRoot, 'vlt-workspaces.json'))) {\n return true;\n }\n\n // NX can be validated by checking the environment variable.\n if (process.env.NX_WORKSPACE_ROOT === path.resolve(repositoryRoot)) {\n return true;\n }\n\n // all the rest need packages defined in root package.json\n const packageJsonPath = path.join(repositoryRoot, 'package.json');\n if (!fs.existsSync(packageJsonPath)) {\n return false;\n }\n\n const packageJson = JSON.parse(\n fs.readFileSync(packageJsonPath, 'utf-8'),\n ) as {\n workspaces?: string[] | Record<string, string>;\n };\n\n return packageJson.workspaces !== undefined;\n } catch (error) {\n // eslint-disable-next-line no-console\n console.error('Error determining if repository is a monorepo', error);\n return false;\n }\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nconst PACKAGE_JSON = 'package.json';\n\n/**\n * Find the package root by looking for the closest package.json.\n *\n */\nexport function findPackageRoot(startDir?: string): string {\n let currentDir = startDir || process.cwd();\n\n while (currentDir !== path.parse(currentDir).root) {\n const pkgJsonPath = path.join(currentDir, PACKAGE_JSON);\n\n // Check for a .git directory (not a file)\n if (fs.existsSync(pkgJsonPath)) {\n return currentDir;\n }\n\n currentDir = path.dirname(currentDir);\n }\n\n throw new Error(\n 'Package root not found. Specify the root of the package with the `package.root` option.',\n );\n}\n","import fs from 'node:fs';\nimport { join } from 'node:path';\nimport { CONFIGURATION_FILENAMES } from '../../constants';\n\nexport function findConfig({ dir }: { dir: string }): string | null {\n for (const filename of CONFIGURATION_FILENAMES) {\n const maybeConfig = join(dir, filename);\n if (fs.existsSync(maybeConfig)) {\n return maybeConfig;\n }\n }\n\n return null;\n}\n","import { parse } from 'jsonc-parser';\nimport { getConfigStringFromEnv } from '../utils/get-config-from-env';\nimport { isDefaultApp } from '../../schema/utils/is-default-app';\nimport type { Config } from '../../schema/types';\nimport type { ClientConfig } from '../client/types';\nimport { MicrofrontendError } from '../../errors';\nimport { MicrofrontendConfigClient } from '../client';\nimport { type OverridesConfig, parseOverrides } from '../../overrides';\nimport { DefaultApplication, ChildApplication } from './application';\nimport { DEFAULT_LOCAL_PROXY_PORT } from './constants';\nimport {\n validateConfigDefaultApplication,\n validateConfigPaths,\n validateDeprecatedFields,\n} from './validation';\n\ninterface ValidationOptions {\n skipValidation?: 'deprecatedFields'[];\n}\n\n/**\n * A class to manage the microfrontends configuration.\n */\nexport class MicrofrontendConfigIsomorphic {\n config: Config;\n defaultApplication: DefaultApplication;\n childApplications: Record<string, ChildApplication> = {};\n overrides?: OverridesConfig;\n options?: Config['options'];\n\n private readonly serialized: {\n config: Config;\n overrides?: OverridesConfig;\n };\n\n constructor({\n config,\n overrides,\n opts,\n }: {\n config: Config;\n overrides?: OverridesConfig;\n opts?: ValidationOptions;\n }) {\n // run validation on init\n MicrofrontendConfigIsomorphic.validate(config, opts);\n\n const disableOverrides = config.options?.disableOverrides ?? false;\n this.overrides = overrides && !disableOverrides ? overrides : undefined;\n\n let defaultApplication: DefaultApplication | undefined;\n // create applications\n for (const [appId, appConfig] of Object.entries(config.applications)) {\n const appOverrides = !disableOverrides\n ? this.overrides?.applications[appId]\n : undefined;\n\n if (isDefaultApp(appConfig)) {\n defaultApplication = new DefaultApplication(appId, {\n app: appConfig,\n overrides: appOverrides,\n });\n } else {\n this.childApplications[appId] = new ChildApplication(appId, {\n app: appConfig,\n overrides: appOverrides,\n });\n }\n }\n\n // validate that this.defaultApplication is defined\n if (!defaultApplication) {\n throw new MicrofrontendError(\n 'Could not find default application in microfrontends configuration',\n {\n type: 'application',\n subtype: 'not_found',\n },\n );\n }\n this.defaultApplication = defaultApplication;\n\n this.config = config;\n this.options = config.options;\n this.serialized = {\n config,\n overrides,\n };\n }\n\n static validate(config: string | Config, opts?: ValidationOptions): Config {\n const skipValidation: ValidationOptions['skipValidation'] =\n opts?.skipValidation ?? [];\n // let this throw if it's not valid JSON\n const c = typeof config === 'string' ? (parse(config) as Config) : config;\n\n validateConfigPaths(c.applications);\n validateConfigDefaultApplication(c.applications);\n\n if (!skipValidation.includes('deprecatedFields')) {\n validateDeprecatedFields(c);\n }\n\n return c;\n }\n\n static fromEnv({\n cookies,\n }: {\n cookies?: { name: string; value: string }[];\n }): MicrofrontendConfigIsomorphic {\n return new MicrofrontendConfigIsomorphic({\n config: parse(getConfigStringFromEnv()) as Config,\n overrides: parseOverrides(cookies ?? []),\n });\n }\n\n isOverridesDisabled(): boolean {\n return this.options?.disableOverrides ?? false;\n }\n\n getConfig(): Config {\n return this.config;\n }\n\n getApplicationsByType(): {\n defaultApplication?: DefaultApplication;\n applications: ChildApplication[];\n } {\n return {\n defaultApplication: this.defaultApplication,\n applications: Object.values(this.childApplications),\n };\n }\n\n getChildApplications(): ChildApplication[] {\n return Object.values(this.childApplications);\n }\n\n getAllApplications(): (DefaultApplication | ChildApplication)[] {\n return [\n this.defaultApplication,\n ...Object.values(this.childApplications),\n ].filter(Boolean);\n }\n\n getApplication(name: string): DefaultApplication | ChildApplication {\n // check the default\n if (\n this.defaultApplication.name === name ||\n this.defaultApplication.packageName === name\n ) {\n return this.defaultApplication;\n }\n const app =\n this.childApplications[name] ||\n Object.values(this.childApplications).find(\n (child) => child.packageName === name,\n );\n if (!app) {\n throw new MicrofrontendError(\n `Could not find microfrontends configuration for application \"${name}\"`,\n {\n type: 'application',\n subtype: 'not_found',\n },\n );\n }\n\n return app;\n }\n\n hasApplication(name: string): boolean {\n try {\n this.getApplication(name);\n return true;\n } catch {\n return false;\n }\n }\n\n getApplicationByProjectId(\n projectId: string,\n ): DefaultApplication | ChildApplication | undefined {\n // check the default\n if (this.defaultApplication.projectId === projectId) {\n return this.defaultApplication;\n }\n\n return Object.values(this.childApplications).find(\n (app) => app.projectId === projectId,\n );\n }\n\n /**\n * Returns the default application.\n */\n getDefaultApplication(): DefaultApplication {\n return this.defaultApplication;\n }\n\n /**\n * Returns the configured port for the local proxy\n */\n getLocalProxyPort(): number {\n return this.config.options?.localProxyPort ?? DEFAULT_LOCAL_PROXY_PORT;\n }\n\n /**\n * Serializes the class back to the Schema type.\n *\n * NOTE: This is used when writing the config to disk and must always match the input Schema\n */\n toSchemaJson(): Config {\n return this.serialized.config;\n }\n\n toClientConfig(): MicrofrontendConfigClient {\n const applications: ClientConfig['applications'] = Object.fromEntries(\n Object.entries(this.childApplications).map(([name, application]) => [\n name,\n {\n default: false,\n routing: application.routing,\n },\n ]),\n );\n\n // add the default application\n applications[this.defaultApplication.name] = {\n default: true,\n };\n\n return new MicrofrontendConfigClient({\n applications,\n });\n }\n\n serialize(): {\n config: Config;\n overrides?: OverridesConfig;\n } {\n return this.serialized;\n }\n}\n","import { pathToRegexp } from 'path-to-regexp';\nimport type { ClientConfig } from './types';\n\ninterface MicrofrontendConfigClientOptions {\n removeFlaggedPaths?: boolean;\n}\n\nexport class MicrofrontendConfigClient {\n applications: ClientConfig['applications'];\n pathCache: Record<string, string> = {};\n private readonly serialized: ClientConfig;\n\n constructor(config: ClientConfig, opts?: MicrofrontendConfigClientOptions) {\n this.serialized = config;\n if (opts?.removeFlaggedPaths) {\n for (const app of Object.values(config.applications)) {\n if (app.routing) {\n app.routing = app.routing.filter((match) => !match.flag);\n }\n }\n }\n this.applications = config.applications;\n }\n\n /**\n * Create a new `MicrofrontendConfigClient` from a JSON string.\n * Config must be passed in to remain framework agnostic\n */\n static fromEnv(\n config: string | undefined,\n opts?: MicrofrontendConfigClientOptions,\n ): MicrofrontendConfigClient {\n if (!config) {\n throw new Error('No microfrontends configuration found');\n }\n return new MicrofrontendConfigClient(\n JSON.parse(config) as ClientConfig,\n opts,\n );\n }\n\n isEqual(other: MicrofrontendConfigClient): boolean {\n return (\n JSON.stringify(this.applications) === JSON.stringify(other.applications)\n );\n }\n\n getApplicationNameForPath(path: string): string | null {\n if (!path.startsWith('/')) {\n throw new Error(`Path must start with a /`);\n }\n\n if (this.pathCache[path]) {\n return this.pathCache[path];\n }\n\n const pathname = new URL(path, 'https://example.com').pathname;\n for (const [name, application] of Object.entries(this.applications)) {\n if (application.routing) {\n for (const group of application.routing) {\n for (const childPath of group.paths) {\n const regexp = pathToRegexp(childPath);\n if (regexp.test(pathname)) {\n this.pathCache[path] = name;\n return name;\n }\n }\n }\n }\n }\n const defaultApplication = Object.entries(this.applications).find(\n ([, application]) => application.default,\n );\n if (!defaultApplication) {\n return null;\n }\n\n this.pathCache[path] = defaultApplication[0];\n return defaultApplication[0];\n }\n\n serialize(): ClientConfig {\n return this.serialized;\n }\n}\n","import { pathToRegexp, parse as parsePathRegexp } from 'path-to-regexp';\nimport type {\n ApplicationId,\n PathGroup,\n ApplicationRouting,\n ChildApplication as ChildApplicationConfig,\n Config,\n} from '../../schema/types';\nimport { MicrofrontendError } from '../../errors';\nimport { isDefaultApp } from '../../schema/utils/is-default-app';\n\nconst LIST_FORMATTER = new Intl.ListFormat('en', {\n style: 'long',\n type: 'conjunction',\n});\n\n/**\n * Validate all paths in a configuration - ensures paths do not overlap\n */\nexport const validateConfigPaths = (\n applicationConfigsById?: ApplicationRouting,\n): void => {\n if (!applicationConfigsById) {\n return;\n }\n\n const pathsByApplicationId = new Map<\n PathGroup['paths'][number],\n {\n applications: ApplicationId[];\n matcher: RegExp;\n applicationId?: ApplicationId;\n }\n >();\n const errors: string[] = [];\n\n for (const [id, app] of Object.entries(applicationConfigsById)) {\n if (isDefaultApp(app)) {\n // default applications do not have routing\n continue;\n }\n\n for (const pathMatch of app.routing) {\n for (const path of pathMatch.paths) {\n const maybeError = validatePathExpression(path);\n if (maybeError) {\n errors.push(maybeError);\n } else {\n const existing = pathsByApplicationId.get(path);\n if (existing) {\n existing.applications.push(id);\n } else {\n pathsByApplicationId.set(path, {\n applications: [id],\n matcher: pathToRegexp(path),\n applicationId: id,\n });\n }\n }\n }\n }\n }\n const entries = Array.from(pathsByApplicationId.entries());\n\n for (const [path, { applications: ids, matcher, applicationId }] of entries) {\n if (ids.length > 1) {\n errors.push(\n `Duplicate path \"${path}\" for applications \"${ids.join(', ')}\"`,\n );\n }\n\n for (const [\n matchPath,\n { applications: matchIds, applicationId: matchApplicationId },\n ] of entries) {\n if (path === matchPath) {\n // we're comparing to ourselves, so skip\n continue;\n }\n\n if (applicationId === matchApplicationId) {\n // we're comparing to paths within our own application, which are allowed to overlap, so skip\n continue;\n }\n\n if (matcher.test(matchPath)) {\n const source = `\"${path}\" of application${ids.length > 0 ? 's' : ''} ${ids.join(', ')}`;\n const destination = `\"${matchPath}\" of application${matchIds.length > 0 ? 's' : ''} ${matchIds.join(', ')}`;\n\n errors.push(\n `Overlapping path detected between ${source} and ${destination}`,\n );\n }\n }\n }\n\n if (errors.length) {\n throw new MicrofrontendError(\n `Invalid paths: ${errors.join(', ')}. See supported paths in the documentation https://vercel.com/docs/microfrontends/path-routing#supported-path-expressions.`,\n {\n type: 'config',\n subtype: 'conflicting_paths',\n },\n );\n }\n};\n\n// From https://github.com/pillarjs/path-to-regexp/blob/b0778f5e8e6c6e9ee4e2f5b34e877cc5229f8036/src/index.ts#L143\nconst PATH_DEFAULT_PATTERN = '[^\\\\/#\\\\?]+?';\n\nfunction validatePathExpression(path: string): string | undefined {\n try {\n const tokens = parsePathRegexp(path);\n if (/(?<!\\\\)\\{/.test(path)) {\n return `Optional paths are not supported: ${path}`;\n }\n if (/(?<!\\\\|\\()\\?/.test(path)) {\n return `Optional paths are not supported: ${path}`;\n }\n if (/\\/[^/]*(?<!\\\\):[^/]*(?<!\\\\):[^/]*/.test(path)) {\n return `Only one wildcard is allowed per path segment: ${path}`;\n }\n for (let i = 0; i < tokens.length; i++) {\n const token = tokens[i];\n if (token === undefined) {\n return `token ${i} in ${path} is undefined, this shouldn't happen`;\n }\n if (typeof token !== 'string') {\n if (!token.name) {\n return `Only named wildcards are allowed: ${path} (hint: add \":path\" to the wildcard)`;\n }\n if (\n token.pattern !== PATH_DEFAULT_PATTERN &&\n // Allows (a|b|c) and ((?!a|b|c).*) regex\n // Only limited regex is supported for now, due to performance considerations\n !/^(?<allowed>[\\w]+(?:\\|[^:|()]+)+)$|^\\(\\?!(?<disallowed>[\\w]+(?:\\|[^:|()]+)*)\\)\\.\\*$/.test(\n token.pattern,\n )\n ) {\n return `Path ${path} cannot use unsupported regular expression wildcard`;\n }\n if (token.modifier && i !== tokens.length - 1) {\n return `Modifier ${token.modifier} is not allowed on wildcard :${token.name} in ${path}. Modifiers are only allowed in the last path component`;\n }\n }\n }\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n return `Path ${path} could not be parsed into regexp: ${message}`;\n }\n return undefined;\n}\n\n/**\n * Validate all paths in an application - ensures paths are the correct format\n */\nexport const validateAppPaths = (\n name: string,\n app: ChildApplicationConfig,\n): void => {\n // validate routes\n for (const group of app.routing) {\n for (const p of group.paths) {\n if (p === '/') {\n continue;\n }\n if (p.endsWith('/')) {\n throw new MicrofrontendError(\n `Invalid path for application \"${name}\". ${p} must not end with a slash.`,\n { type: 'application', subtype: 'invalid_path' },\n );\n }\n\n if (!p.startsWith('/')) {\n throw new MicrofrontendError(\n `Invalid path for application \"${name}\". ${p} must start with a slash.`,\n { type: 'application', subtype: 'invalid_path' },\n );\n }\n }\n }\n};\n\n/**\n * Make sure only one `Application` defines routing\n * */\nexport const validateConfigDefaultApplication = (\n applicationConfigsById?: ApplicationRouting,\n): void => {\n if (!applicationConfigsById) {\n return;\n }\n\n const applicationsWithoutRouting = Object.entries(\n applicationConfigsById,\n ).filter(([, app]) => isDefaultApp(app));\n const numApplicationsWithoutRouting = applicationsWithoutRouting.reduce(\n (acc) => {\n return acc + 1;\n },\n 0,\n );\n\n if (numApplicationsWithoutRouting === 0) {\n throw new MicrofrontendError(\n 'No default application found. At least one application needs to be the default by omitting routing.',\n { type: 'config', subtype: 'no_default_application' },\n );\n }\n\n if (numApplicationsWithoutRouting > 1) {\n const applicationNamesMissingRouting = applicationsWithoutRouting.map(\n ([name]) => name,\n );\n throw new MicrofrontendError(\n `All applications except for the default app must contain the \"routing\" field. Applications that are missing routing: ${LIST_FORMATTER.format(applicationNamesMissingRouting)}.`,\n { type: 'config', subtype: 'multiple_default_applications' },\n );\n }\n};\n\n// TODO: Remove this after 4 May 2025 when the new schema has been fully in use for 30 days and thus we can delete the deprecated fields.\nexport const validateDeprecatedFields = (config: Config): void => {\n const errors = [];\n\n for (const [applicationId, application] of Object.entries(\n config.applications,\n )) {\n if (application.development?.localPort) {\n errors.push(\n `Application '${applicationId}' cannot contain deprecated field 'development.localPort'. Use 'developement.local' instead.`,\n );\n }\n if (application.projectId) {\n // This will be an error in the next release\n }\n }\n\n if (errors.length) {\n throw new MicrofrontendError(\n `Microfrontends configuration file errors:\\n- ${errors.join('\\n- ')}`,\n {\n type: 'config',\n subtype: 'depcrecated_field',\n },\n );\n }\n};\n","const PREFIX = 'vc-ap';\n\nexport function generateAssetPrefixFromName({\n name,\n}: {\n name: string;\n}): string {\n if (!name) {\n throw new Error('Name is required to generate an asset prefix');\n }\n\n return `${PREFIX}-${name}`;\n}\n","export function generatePortFromName({\n name,\n minPort = 3000,\n maxPort = 8000,\n}: {\n name: string;\n minPort?: number;\n maxPort?: number;\n}): number {\n if (!name) {\n throw new Error('Name is required to generate a port');\n }\n\n // hash the name\n let hash = 0;\n for (let i = 0; i < name.length; i++) {\n // eslint-disable-next-line no-bitwise\n hash = (hash << 5) - hash + name.charCodeAt(i);\n // Convert to 32-bit\n // eslint-disable-next-line no-bitwise\n hash |= 0;\n }\n hash = Math.abs(hash);\n\n // Map the hash to the port range\n const range = maxPort - minPort;\n const port = minPort + (hash % range);\n\n return port;\n}\n","import type {\n HostConfig as RemoteHostConfigSchema,\n LocalHostConfig as LocalHostConfigSchema,\n} from '../../../bin/types';\nimport { generatePortFromName } from './utils/generate-port';\n\ninterface HostOptions {\n isLocal?: boolean;\n}\n\nexport class Host {\n protocol: 'http' | 'https';\n host: string;\n port?: number;\n local: boolean | undefined;\n\n constructor(\n hostConfig: RemoteHostConfigSchema | string,\n options?: HostOptions,\n ) {\n if (typeof hostConfig === 'string') {\n ({\n protocol: this.protocol,\n host: this.host,\n port: this.port,\n } = Host.parseUrl(hostConfig));\n } else {\n const { protocol = 'https', host, port } = hostConfig;\n this.protocol = protocol;\n this.host = host;\n this.port = port;\n }\n this.local = options?.isLocal;\n }\n\n protected static parseUrl(\n url: string,\n defaultProtocol = 'https',\n ): {\n protocol: Host['protocol'];\n host: string;\n port?: number;\n } {\n let hostToParse = url;\n if (!/^https?:\\/\\//.exec(hostToParse)) {\n hostToParse = `${defaultProtocol}://${hostToParse}`;\n }\n const parsed = new URL(hostToParse);\n if (!parsed.hostname) {\n throw new Error(Host.getMicrofrontendsError(url, 'requires a host'));\n }\n if (parsed.hash) {\n throw new Error(\n Host.getMicrofrontendsError(url, 'cannot have a fragment'),\n );\n }\n if (parsed.username || parsed.password) {\n throw new Error(\n Host.getMicrofrontendsError(\n url,\n 'cannot have authentication credentials (username and/or password)',\n ),\n );\n }\n if (parsed.pathname !== '/') {\n throw new Error(Host.getMicrofrontendsError(url, 'cannot have a path'));\n }\n if (parsed.search) {\n throw new Error(\n Host.getMicrofrontendsError(url, 'cannot have query parameters'),\n );\n }\n const protocol = parsed.protocol.slice(0, -1) as Host['protocol'];\n return {\n protocol,\n host: parsed.hostname,\n port: parsed.port ? Number.parseInt(parsed.port) : undefined,\n };\n }\n\n private static getMicrofrontendsError(url: string, message: string): string {\n return `Microfrontends configuration error: the URL ${url} in your microfrontends.json ${message}.`;\n }\n\n isLocal(): boolean {\n return this.local || this.host === 'localhost' || this.host === '127.0.0.1';\n }\n\n toString(): string {\n const url = this.toUrl();\n // strip the trailing slash\n return url.toString().replace(/\\/$/, '');\n }\n\n toUrl(): URL {\n const url = `${this.protocol}://${this.host}${this.port ? `:${this.port}` : ''}`;\n return new URL(url);\n }\n}\n\n/**\n * A Host subclass with defaults for locally running applications\n */\nexport class LocalHost extends Host {\n constructor({\n appName,\n localPort,\n local,\n }: {\n appName: string;\n localPort?: number;\n local?: string | number | LocalHostConfigSchema;\n }) {\n if (localPort && local) {\n throw new Error(\n `Microfrontends configuration error: '${appName}' has both the 'development.local' and 'development.localPort' fields set. Please remove the 'development.localPort' field and ensure the 'development.local' field has the correct port.`,\n );\n }\n let protocol: RemoteHostConfigSchema['protocol'];\n let host: string | undefined;\n let port: number | undefined;\n if (localPort) {\n port = localPort;\n } else if (typeof local === 'number') {\n port = local;\n } else if (typeof local === 'string') {\n if (/^\\d+$/.test(local)) {\n port = Number.parseInt(local);\n } else {\n const parsed = Host.parseUrl(local, 'http');\n protocol = parsed.protocol;\n host = parsed.host;\n port = parsed.port;\n }\n } else if (local) {\n protocol = local.protocol;\n host = local.host;\n port = local.port;\n }\n // set defaults for local\n super({\n protocol: protocol ?? 'http',\n host: host ?? 'localhost',\n port: port ?? generatePortFromName({ name: appName }),\n });\n }\n}\n","export function generateAutomationBypassEnvVarName({\n name,\n}: {\n name: string;\n}): string {\n return `AUTOMATION_BYPASS_${name.toUpperCase().replace(/[^a-zA-Z0-9]/g, '_')}`;\n}\n","import type {\n Application as ApplicationConfig,\n DefaultApplication as DefaultApplicationConfig,\n ChildApplication as ChildApplicationConfig,\n PathGroup,\n} from '../../schema/types';\nimport type { ApplicationOverrideConfig } from '../../overrides';\nimport { validateAppPaths } from './validation';\nimport { generateAssetPrefixFromName } from './utils/generate-asset-prefix';\nimport { Host, LocalHost } from './host';\nimport { generateAutomationBypassEnvVarName } from './utils/generate-automation-bypass-env-var-name';\n\nexport class Application {\n readonly default: boolean;\n name: string;\n development: {\n local: LocalHost;\n fallback?: Host;\n };\n fallback?: Host;\n projectId?: string;\n packageName?: string;\n overrides?: {\n environment?: Host;\n };\n readonly serialized: ApplicationConfig;\n\n constructor(\n name: string,\n {\n app,\n overrides,\n isDefault,\n }: {\n app: ApplicationConfig;\n overrides?: ApplicationOverrideConfig;\n isDefault?: boolean;\n },\n ) {\n this.name = name;\n this.development = {\n local: new LocalHost({\n appName: name,\n localPort: app.development?.localPort,\n local: app.development?.local,\n }),\n fallback: app.development?.fallback\n ? new Host(app.development.fallback)\n : undefined,\n };\n if (app.development?.fallback) {\n this.fallback = new Host(app.development.fallback);\n }\n this.projectId = app.projectId;\n this.packageName = app.packageName;\n this.overrides = overrides?.environment\n ? {\n environment: new Host(overrides.environment),\n }\n : undefined;\n this.default = isDefault ?? false;\n this.serialized = app;\n }\n\n isDefault(): boolean {\n return this.default;\n }\n\n getAssetPrefix(): string {\n return generateAssetPrefixFromName({ name: this.name });\n }\n\n getAutomationBypassEnvVarName(): string {\n return generateAutomationBypassEnvVarName({ name: this.name });\n }\n\n serialize(): ApplicationConfig {\n return this.serialized;\n }\n}\n\nexport class DefaultApplication extends Application {\n readonly default = true;\n fallback: Host;\n\n constructor(\n name: string,\n {\n app,\n overrides,\n }: {\n app: DefaultApplicationConfig;\n overrides?: ApplicationOverrideConfig;\n },\n ) {\n super(name, {\n app,\n overrides,\n isDefault: true,\n });\n\