UNPKG

@vercel/microfrontends

Version:

Defines configuration and utilities for microfrontends development

284 lines (273 loc) 9.29 kB
// src/config/microfrontends/utils/find-config.ts import fs from "node:fs"; import { join } from "node:path"; // src/config/microfrontends/utils/get-config-file-name.ts var DEFAULT_CONFIGURATION_FILENAMES = [ "microfrontends.json", "microfrontends.jsonc" ]; function getPossibleConfigurationFilenames({ customConfigFilename }) { if (customConfigFilename) { if (!customConfigFilename.endsWith(".json") && !customConfigFilename.endsWith(".jsonc")) { throw new Error( `Found VC_MICROFRONTENDS_CONFIG_FILE_NAME but the name is invalid. Received: ${customConfigFilename}. The file name must end with '.json' or '.jsonc'. It's also possible for the env var to include the path, eg microfrontends-dev.json or /path/to/microfrontends-dev.json.` ); } return Array.from( /* @__PURE__ */ new Set([customConfigFilename, ...DEFAULT_CONFIGURATION_FILENAMES]) ); } return DEFAULT_CONFIGURATION_FILENAMES; } // src/config/microfrontends/utils/find-config.ts function findConfig({ dir, customConfigFilename }) { for (const filename of getPossibleConfigurationFilenames({ customConfigFilename })) { const maybeConfig = join(dir, filename); if (fs.existsSync(maybeConfig)) { return maybeConfig; } } return null; } // src/config/microfrontends/utils/infer-microfrontends-location.ts import { dirname } from "node:path"; import { readFileSync } from "node:fs"; import { parse } from "jsonc-parser"; import fg from "fast-glob"; // 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/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/config/microfrontends/utils/infer-microfrontends-location.ts var configCache = {}; function findPackageWithMicrofrontendsConfig({ repositoryRoot, applicationContext, customConfigFilename }) { const applicationName = applicationContext.name; logger.debug( "[MFE Config] Searching repository for configs containing application:", applicationName ); try { const microfrontendsJsonPaths = fg.globSync( `**/{${getPossibleConfigurationFilenames({ customConfigFilename }).join(",")}}`, { cwd: repositoryRoot, absolute: true, onlyFiles: true, followSymbolicLinks: false, ignore: ["**/node_modules/**", "**/.git/**"] } ); logger.debug( "[MFE Config] Found", microfrontendsJsonPaths.length, "config file(s) in repository" ); const matchingPaths = []; for (const microfrontendsJsonPath of microfrontendsJsonPaths) { try { const microfrontendsJsonContent = readFileSync( microfrontendsJsonPath, "utf-8" ); const microfrontendsJson = parse(microfrontendsJsonContent); if (microfrontendsJson.applications[applicationName]) { logger.debug( "[MFE Config] Found application in config:", microfrontendsJsonPath ); matchingPaths.push(microfrontendsJsonPath); } else { for (const [_, app] of Object.entries( microfrontendsJson.applications )) { if (app.packageName === applicationName) { logger.debug( "[MFE Config] Found application via packageName in config:", microfrontendsJsonPath ); matchingPaths.push(microfrontendsJsonPath); } } } } catch (error2) { } } logger.debug( "[MFE Config] Total matching config files:", matchingPaths.length ); if (matchingPaths.length > 1) { throw new MicrofrontendError( `Found multiple \`microfrontends.json\` files in the repository referencing the application "${applicationName}", but only one is allowed. ${matchingPaths.join("\n \u2022 ")}`, { type: "config", subtype: "inference_failed" } ); } if (matchingPaths.length === 0) { let additionalErrorMessage = ""; if (microfrontendsJsonPaths.length > 0) { if (!applicationContext.projectName) { additionalErrorMessage = ` If the name in package.json (${applicationContext.packageJsonName}) 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.`; } else { additionalErrorMessage = ` Names of applications in \`microfrontends.json\` must match the Vercel Project name (${applicationContext.projectName}).`; } } throw new MicrofrontendError( `Could not find a \`microfrontends.json\` file in the repository that contains the "${applicationName}" application.${additionalErrorMessage} If your Vercel Microfrontends configuration is not in this repository, you can use the Vercel CLI to pull the Vercel Microfrontends configuration using the "vercel microfrontends pull" command, or you can specify the path manually using the VC_MICROFRONTENDS_CONFIG environment variable. If your Vercel Microfrontends configuration has a custom name, ensure the VC_MICROFRONTENDS_CONFIG_FILE_NAME environment variable is set, you can pull the vercel project environment variables using the "vercel env pull" command. If you suspect this is thrown in error, please reach out to the Vercel team.`, { type: "config", subtype: "inference_failed" } ); } const [packageJsonPath] = matchingPaths; return dirname(packageJsonPath); } catch (error2) { if (error2 instanceof MicrofrontendError) { throw error2; } return null; } } function inferMicrofrontendsLocation(opts) { const cacheKey = `${opts.repositoryRoot}-${opts.applicationContext.name}${opts.customConfigFilename ? `-${opts.customConfigFilename}` : ""}`; if (configCache[cacheKey]) { return configCache[cacheKey]; } const result = findPackageWithMicrofrontendsConfig(opts); if (!result) { throw new MicrofrontendError( `Could not infer the location of the \`microfrontends.json\` file for application "${opts.applicationContext.name}" starting in directory "${opts.repositoryRoot}".`, { type: "config", subtype: "inference_failed" } ); } configCache[cacheKey] = result; return result; } export { findConfig, getPossibleConfigurationFilenames, inferMicrofrontendsLocation }; //# sourceMappingURL=utils.js.map