UNPKG

firebase-tools

Version:
710 lines (709 loc) 32.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FUNCTION_KITS_DIR = exports.DEFAULT_TEMPLATE = exports.TEMPLATES = void 0; exports.generateUniqueId = generateUniqueId; exports.parseNpmPackageSpecifier = parseNpmPackageSpecifier; exports.validateNpmPackageName = validateNpmPackageName; exports.sanitizePackageNameToKitName = sanitizePackageNameToKitName; exports.isThirdPartyPackage = isThirdPartyPackage; exports.checkPackageHasShrinkwrap = checkPackageHasShrinkwrap; exports.isKitConfiguredForProject = isKitConfiguredForProject; exports.extractExistingFunctionsInfo = extractExistingFunctionsInfo; exports.promptKitInstanceId = promptKitInstanceId; exports.promptKitId = promptKitId; exports.promptSecurityConfirmation = promptSecurityConfirmation; exports.promptExistingInstanceForProject = promptExistingInstanceForProject; exports.writeKitPackageJson = writeKitPackageJson; exports.writeKitIndexTs = writeKitIndexTs; exports.scaffoldKitFiles = scaffoldKitFiles; exports.buildAndInstallKit = buildAndInstallKit; exports.addKitToConfig = addKitToConfig; exports.validateAndResolveDirectoryKit = validateAndResolveDirectoryKit; exports.buildAndInstallDirectoryKit = buildAndInstallDirectoryKit; exports.findKitConfig = findKitConfig; exports.addInstanceToKitConfig = addInstanceToKitConfig; exports.scaffoldKit = scaffoldKit; exports.addInstanceToKit = addInstanceToKit; exports.discoverKitBuild = discoverKitBuild; exports.printKitFirstDeployReport = printKitFirstDeployReport; exports.addKitInstanceOrConfigureProject = addKitInstanceOrConfigureProject; exports.findExistingKit = findExistingKit; exports.resolvePackageSource = resolvePackageSource; exports.resolveDirectorySource = resolveDirectorySource; exports.installKitOrInstance = installKitOrInstance; const crypto = require("crypto"); const path = require("path"); const clc = require("colorette"); const fs = require("fs-extra"); const error_1 = require("../../error"); const projectUtils_1 = require("../../projectUtils"); const utils_1 = require("../../utils"); const projectConfig_1 = require("../projectConfig"); const logger_1 = require("../../logger"); const prompt_1 = require("../../prompt"); const spawn_1 = require("../../init/spawn"); const templates_1 = require("../../templates"); const supported = require("../../deploy/functions/runtimes/supported"); const runtimes = require("../../deploy/functions/runtimes"); const build = require("../../deploy/functions/build"); const iam = require("../../gcp/iam"); const env_1 = require("../env"); const env_2 = require("./env"); exports.TEMPLATES = { installation: "init/functions/typescript/index-kit.ts", migration: "init/functions/typescript/index-kit-migration.ts", }; exports.DEFAULT_TEMPLATE = "installation"; exports.FUNCTION_KITS_DIR = "function-kits"; function generateUniqueId(baseId, existingIds) { if (!existingIds.has(baseId)) { return baseId; } const prefix = baseId.slice(0, 35); let candidate = ""; do { const randomSuffix = crypto.randomBytes(2).toString("hex"); candidate = `${prefix}-${randomSuffix}`; } while (existingIds.has(candidate)); return candidate; } function parseNpmPackageSpecifier(rawPkg) { const lastAt = rawPkg.lastIndexOf("@"); if (lastAt > 0) { return { packageName: rawPkg.substring(0, lastAt), version: rawPkg.substring(lastAt + 1), }; } return { packageName: rawPkg }; } function validateNpmPackageName(packageName) { const npmPackageRegex = /^(?:@[a-z0-9_.-]+\/[a-z0-9_.-]+|[a-z0-9_.-]+)$/i; if (!packageName || packageName.length > 214 || !npmPackageRegex.test(packageName)) { throw new error_1.FirebaseError(`Invalid NPM package name '${packageName}'. Package names must be valid npm package specifiers (e.g. 'my-kit' or '@scope/my-kit').`); } } function sanitizePackageNameToKitName(packageName) { const parts = packageName.split("/"); const nameWithoutScope = parts[parts.length - 1] || packageName; const sanitized = nameWithoutScope.toLowerCase().replace(/[^a-z0-9_-]/g, ""); return sanitized.slice(0, 40); } function isThirdPartyPackage(packageName) { return !packageName.startsWith("@firebase-function-kits/"); } async function checkPackageHasShrinkwrap(rawPkgName) { try { const output = await (0, spawn_1.spawnWithOutput)("npm", ["pack", rawPkgName, "--dry-run", "--json"]); const parsed = JSON.parse(output); if (Array.isArray(parsed) && parsed.length > 0) { const pkgInfo = parsed[0]; if (pkgInfo.hasShrinkwrap) { return true; } if (Array.isArray(pkgInfo.files)) { return pkgInfo.files.some((f) => f.path === "npm-shrinkwrap.json" || f.path === "npm-shrinkwrap.js"); } } } catch (err) { logger_1.logger.debug(`Failed to inspect package shrinkwrap for ${rawPkgName}: ${(0, error_1.getErrMsg)(err)}`); } return false; } function isKitConfiguredForProject(config, kit, projectId, projectAlias) { return Object.values(kit.instances || {}).some((configDir) => (0, env_1.hasProjectEnv)(config.path(configDir), projectId, projectAlias)); } function extractExistingFunctionsInfo(configFunctions) { const existingFunctions = configFunctions && (!Array.isArray(configFunctions) || configFunctions.length > 0) ? (0, projectConfig_1.normalizeAndValidate)(configFunctions) : []; const existingKitIds = new Set(); const existingCodebases = new Set(); const existingInstanceIds = new Set(); for (const c of existingFunctions) { if ((0, projectConfig_1.isKitConfig)(c)) { if (c.kit) { existingKitIds.add(c.kit); } if (c.instances) { for (const instId of Object.keys(c.instances)) { existingInstanceIds.add(instId); } } } else if (c.codebase) { existingCodebases.add(c.codebase); } } return { existingFunctions, existingKitIds, existingCodebases, existingInstanceIds, }; } async function promptKitInstanceId(baseKitId, existingInstanceIds, existingCodebases, nonInteractive, customInstanceId) { const instanceCollisions = new Set([...existingInstanceIds, ...existingCodebases]); const defaultInstanceId = generateUniqueId(baseKitId, instanceCollisions); if (customInstanceId) { (0, projectConfig_1.validateKitInstanceId)(customInstanceId); if (existingInstanceIds.has(customInstanceId)) { throw new error_1.FirebaseError(`functions kit instance ID must be unique across all kits, but '${customInstanceId}' was used more than once.`); } if (existingCodebases.has(customInstanceId)) { throw new error_1.FirebaseError(`functions codebase name and kit instance ID must be mutually exclusive, but '${customInstanceId}' was used as both a codebase name and a kit instance ID.`); } return customInstanceId; } const instanceId = await (0, prompt_1.input)({ message: "What would you like to name this instance?", default: defaultInstanceId, nonInteractive, validate: (val) => { try { (0, projectConfig_1.validateKitInstanceId)(val); } catch (err) { return (0, error_1.getErrMsg)(err); } if (existingInstanceIds.has(val)) { return `functions kit instance ID must be unique across all kits, but '${val}' was used more than once.`; } if (existingCodebases.has(val)) { return `functions codebase name and kit instance ID must be mutually exclusive, but '${val}' was used as both a codebase name and a kit instance ID.`; } return true; }, }); (0, projectConfig_1.validateKitInstanceId)(instanceId); if (existingInstanceIds.has(instanceId)) { throw new error_1.FirebaseError(`functions kit instance ID must be unique across all kits, but '${instanceId}' was used more than once.`); } if (existingCodebases.has(instanceId)) { throw new error_1.FirebaseError(`functions codebase name and kit instance ID must be mutually exclusive, but '${instanceId}' was used as both a codebase name and a kit instance ID.`); } return instanceId; } async function promptKitId(packageName, existingKitIds, nonInteractive, customKitId) { const baseKitId = sanitizePackageNameToKitName(packageName) || "kit"; const defaultKitId = generateUniqueId(baseKitId, existingKitIds); if (customKitId) { (0, projectConfig_1.validateKit)(customKitId); if (existingKitIds.has(customKitId)) { throw new error_1.FirebaseError(`functions.kit must be unique but '${customKitId}' was used more than once.`); } return customKitId; } const kitId = await (0, prompt_1.input)({ message: "What would you like to name this kit?", default: defaultKitId, nonInteractive, validate: (val) => { try { (0, projectConfig_1.validateKit)(val); } catch (err) { return (0, error_1.getErrMsg)(err); } if (existingKitIds.has(val)) { return `functions.kit must be unique but '${val}' was used more than once.`; } return true; }, }); (0, projectConfig_1.validateKit)(kitId); if (existingKitIds.has(kitId)) { throw new error_1.FirebaseError(`functions.kit must be unique but '${kitId}' was used more than once.`); } return kitId; } async function promptSecurityConfirmation(rawPkgName, packageName, nonInteractive) { const isThirdParty = isThirdPartyPackage(packageName); if (isThirdParty) { (0, utils_1.logLabeledWarning)("functions", `Package ${clc.bold(packageName)} is a third-party kit (outside the @firebase-function-kits scope).`); } const hasShrinkwrap = await checkPackageHasShrinkwrap(rawPkgName); if (!hasShrinkwrap) { (0, utils_1.logLabeledWarning)("functions", `Package ${clc.bold(packageName)} does not have an npm-shrinkwrap.json file. npm-shrinkwrap guarantees that you deploy the same version of dependencies that the publisher tested against. Since this kit does not have an npm-shrinkwrap, it is possible that deploys or updates may introduce bugs or vulnerabilities in newer dependency versions that the publisher did not test against.`); } if (isThirdParty || !hasShrinkwrap) { let confirmMessage; if (isThirdParty && !hasShrinkwrap) { confirmMessage = `Are you sure you want to install the third-party kit ${packageName} without locked dependencies?`; } else if (isThirdParty) { confirmMessage = `Are you sure you want to install the third-party kit ${packageName}?`; } else { confirmMessage = `Are you sure you want to install ${packageName} without locked dependencies?`; } const confirmInstallation = await (0, prompt_1.confirm)({ message: confirmMessage, default: false, nonInteractive, }); if (!confirmInstallation) { throw new error_1.FirebaseError("Installation cancelled."); } } return isThirdParty; } async function promptExistingInstanceForProject(options, existingKit) { const instanceIds = Object.keys(existingKit.instances || {}); if (instanceIds.length === 0) { throw new error_1.FirebaseError(`Kit '${existingKit.kit}' has no instances configured.`); } let selectedInstanceId = "<instance-name>"; if (instanceIds.length === 1) { selectedInstanceId = instanceIds[0]; } else if (!options.nonInteractive) { selectedInstanceId = await (0, prompt_1.select)({ message: "Which instance would you like to configure for this project?", choices: instanceIds.map((id) => ({ name: id, value: id })), }); } const targetProject = (0, projectUtils_1.getProjectId)(options) || options.project || "<project-name>"; (0, utils_1.logLabeledBullet)("functions", `To create a new instance in this project, deploy the instance dedicated to this project using\n` + clc.bold(`firebase deploy --only functions:${selectedInstanceId} --project ${targetProject}`)); return selectedInstanceId; } async function writeKitPackageJson(config, sourcePath, kitId) { const relPackageJsonPath = path.join(sourcePath, "package.json"); const absPackageJsonPath = config.path(relPackageJsonPath); let pkgJson = {}; if (await fs.pathExists(absPackageJsonPath)) { try { pkgJson = (await fs.readJson(absPackageJsonPath)); } catch (err) { logger_1.logger.debug(`Failed to read existing package.json: ${(0, error_1.getErrMsg)(err)}`); } } else { const latestNodeVersion = supported.latest("nodejs").replace("nodejs", ""); const packageNoLintingTemplate = (0, templates_1.readTemplateSync)("init/functions/typescript/package.nolint.json"); const subbedTemplate = packageNoLintingTemplate.replace(/{{RUNTIME}}/g, latestNodeVersion); try { pkgJson = JSON.parse(subbedTemplate); } catch (err) { throw new error_1.FirebaseError("Failed to parse package.nolint.json template: " + (0, error_1.getErrMsg)(err)); } } pkgJson.name = `${kitId}-wrapper`; await config.askWriteProjectFile(relPackageJsonPath, pkgJson); } async function writeKitIndexTs(config, sourcePath, packageName, templateType) { const relIndexTsPath = path.join(sourcePath, "src", "index.ts"); const absIndexTsPath = config.path(relIndexTsPath); if (!(await fs.pathExists(absIndexTsPath))) { const template = (0, templates_1.readTemplateSync)(exports.TEMPLATES[templateType]); const indexContent = template.replace(/{{PACKAGE_NAME}}/g, packageName); await config.askWriteProjectFile(relIndexTsPath, indexContent); } } async function scaffoldKitFiles(config, kitId, instanceId, packageName, templateType = exports.DEFAULT_TEMPLATE) { const sourcePath = path.join(exports.FUNCTION_KITS_DIR, kitId, "source"); const configDirPath = path.join(exports.FUNCTION_KITS_DIR, kitId, `config-${instanceId}`); const absSourcePath = config.path(sourcePath); const absConfigDirPath = config.path(configDirPath); await fs.ensureDir(absSourcePath); await fs.ensureDir(absConfigDirPath); await writeKitPackageJson(config, sourcePath, kitId); await config.askWriteProjectFile(path.join(sourcePath, "tsconfig.json"), (0, templates_1.readTemplateSync)("init/functions/typescript/tsconfig.json")); await config.askWriteProjectFile(path.join(sourcePath, ".gitignore"), (0, templates_1.readTemplateSync)("init/functions/typescript/_gitignore")); await writeKitIndexTs(config, sourcePath, packageName, templateType); return { sourcePath, configDirPath, absSourcePath, absConfigDirPath }; } async function buildAndInstallKit(absSourcePath, rawPkgName, isThirdParty) { const installArgs = ["install", rawPkgName, "--save-prefix=^"]; if (isThirdParty) { installArgs.push("--ignore-scripts"); } (0, utils_1.logLabeledBullet)("functions", `Running npm ${installArgs.join(" ")}...`); try { await (0, spawn_1.wrapSpawn)("npm", installArgs, absSourcePath); } catch (err) { throw new error_1.FirebaseError(`NPM install failed: ${(0, error_1.getErrMsg)(err)}`); } (0, utils_1.logLabeledBullet)("functions", "Building TypeScript source..."); try { await (0, spawn_1.wrapSpawn)("npm", ["run", "build"], absSourcePath); } catch (err) { throw new error_1.FirebaseError(`TypeScript build failed: ${(0, error_1.getErrMsg)(err)}`); } } function addKitToConfig(config, options) { const configSrc = config.src; const newKitConfig = { kit: options.kitId, ...(options.packageName ? { sourcePackage: { name: options.packageName } } : {}), source: options.sourcePath, instances: { [options.instanceId]: options.configDirPath, }, ...(options.hasBuildScript ?? true ? { predeploy: ['npm --prefix "$RESOURCE_DIR" run build'] } : {}), }; const functionsRaw = configSrc.functions; if (!functionsRaw) { configSrc.functions = [newKitConfig]; } else if (Array.isArray(functionsRaw)) { functionsRaw.push(newKitConfig); } else { configSrc.functions = [functionsRaw, newKitConfig]; } config.writeProjectFile("firebase.json", configSrc); } async function validateAndResolveDirectoryKit(projectDir, rawDir) { const absDirectoryPath = (0, utils_1.resolveWithin)(projectDir, rawDir, `Directory '${rawDir}' is outside of project directory. Function kit directory must be inside the project directory.`); if (!(await fs.pathExists(absDirectoryPath))) { throw new error_1.FirebaseError(`Directory '${rawDir}' does not exist.`); } const stat = await fs.stat(absDirectoryPath); if (!stat.isDirectory()) { throw new error_1.FirebaseError(`Directory '${rawDir}' is not a directory.`); } const packageJsonPath = path.join(absDirectoryPath, "package.json"); if (!(await fs.pathExists(packageJsonPath))) { throw new error_1.FirebaseError(`Directory '${rawDir}' must contain a package.json file to be installed as a function kit.`); } let packageJson = {}; try { packageJson = (await fs.readJson(packageJsonPath)); } catch (err) { throw new error_1.FirebaseError(`Failed to parse package.json in '${rawDir}': ${(0, error_1.getErrMsg)(err)}`); } const hasBuildScript = Boolean(packageJson.scripts && packageJson.scripts.build); let relSourcePath = path.relative(projectDir, absDirectoryPath); if (!relSourcePath) { relSourcePath = "."; } relSourcePath = relSourcePath.split(path.sep).join("/"); return { absDirectoryPath, relSourcePath, hasBuildScript, }; } async function buildAndInstallDirectoryKit(absSourcePath, hasBuildScript) { (0, utils_1.logLabeledBullet)("functions", "Running npm install..."); try { await (0, spawn_1.wrapSpawn)("npm", ["install"], absSourcePath); } catch (err) { throw new error_1.FirebaseError(`NPM install failed: ${(0, error_1.getErrMsg)(err)}`); } if (hasBuildScript) { (0, utils_1.logLabeledBullet)("functions", "Building TypeScript source..."); try { await (0, spawn_1.wrapSpawn)("npm", ["run", "build"], absSourcePath); } catch (err) { throw new error_1.FirebaseError(`TypeScript build failed: ${(0, error_1.getErrMsg)(err)}`); } } } function findKitConfig(config, kitId) { const functions = config.src.functions; if (!functions) { return undefined; } if (Array.isArray(functions)) { return functions.find((f) => typeof f === "object" && f !== null && "kit" in f && f.kit === kitId); } if (typeof functions === "object" && "kit" in functions && functions.kit === kitId) { return functions; } return undefined; } function addInstanceToKitConfig(config, kitId, instanceId, configDirPath) { const target = findKitConfig(config, kitId); if (!target) { throw new error_1.FirebaseError(`Kit '${kitId}' not found in firebase.json.`); } target.instances = target.instances || {}; target.instances[instanceId] = configDirPath; config.writeProjectFile("firebase.json", config.src); } async function scaffoldKit(options) { const paths = await scaffoldKitFiles(options.config, options.kitId, options.instanceId, options.packageName, options.templateType); if (options.seedEnv?.envs && Object.keys(options.seedEnv.envs).length > 0) { (0, env_2.seedKitInstanceEnv)({ configDir: paths.absConfigDirPath, functionsSource: paths.absSourcePath, projectDir: options.config.projectDir, projectId: options.seedEnv.projectId, projectAlias: options.seedEnv.projectAlias, envs: options.seedEnv.envs, }); } addKitToConfig(options.config, { kitId: options.kitId, instanceId: options.instanceId, packageName: options.packageName, sourcePath: paths.sourcePath, configDirPath: paths.configDirPath, }); return paths; } async function addInstanceToKit(options) { const target = findKitConfig(options.config, options.kitId); if (!target) { throw new error_1.FirebaseError(`Kit '${options.kitId}' not found in firebase.json.`); } const configDirPath = path.join(exports.FUNCTION_KITS_DIR, options.kitId, `config-${options.instanceId}`); const absConfigDirPath = options.config.path(configDirPath); await fs.ensureDir(absConfigDirPath); if (options.seedEnv?.envs && Object.keys(options.seedEnv.envs).length > 0) { (0, env_2.seedKitInstanceEnv)({ configDir: absConfigDirPath, functionsSource: options.config.path(target.source), projectDir: options.config.projectDir, projectId: options.seedEnv.projectId, projectAlias: options.seedEnv.projectAlias, envs: options.seedEnv.envs, }); } addInstanceToKitConfig(options.config, options.kitId, options.instanceId, configDirPath); return { configDirPath, absConfigDirPath }; } async function discoverKitBuild(options, absSourcePath) { const projectId = (0, projectUtils_1.getProjectId)(options) || ""; const delegateContext = { projectId, sourceDir: absSourcePath, projectDir: options.config?.projectDir || "", runtime: supported.latest("nodejs"), }; const runtimeDelegate = await runtimes.getRuntimeDelegate(delegateContext); return runtimeDelegate.discoverBuild({}, {}); } async function printKitFirstDeployReport(options, instanceId, absSourcePath) { let discoveredBuild; const prefix = (0, projectConfig_1.addKitPrefix)(instanceId); try { discoveredBuild = await discoverKitBuild(options, absSourcePath); build.applyPrefix(discoveredBuild, prefix); } catch (err) { logger_1.logger.debug(`Could not discover kit build for reporting: ${(0, error_1.getErrMsg)(err)}`); return; } const formatScopedName = (name) => { const fullPrefix = `${prefix}-`; if (name.startsWith(fullPrefix)) { return `${fullPrefix}${clc.bold(name.slice(fullPrefix.length))}`; } return clc.bold(name); }; const functions = Object.keys(discoveredBuild.endpoints).sort().map(formatScopedName); const taskQueues = Object.entries(discoveredBuild.endpoints) .filter(([, endpoint]) => build.isTaskQueueTriggered(endpoint)) .map(([id]) => id) .sort() .map(formatScopedName); const channelSet = new Set(); for (const endpoint of Object.values(discoveredBuild.endpoints)) { if (build.isEventTriggered(endpoint) && endpoint.eventTrigger?.channel) { channelSet.add(endpoint.eventTrigger.channel); } } const eventarcChannels = Array.from(channelSet).sort(); const apis = (discoveredBuild.requiredAPIs || []).map((a) => a.api).sort(); const rawRoles = discoveredBuild.requiredRoles || []; const roles = (await Promise.all(rawRoles.map((r) => iam.getRoleName(r)))).sort(); const printSection = (heading, items) => { if (items.length > 0) { (0, utils_1.logLabeledBullet)("functions", `${heading}\n` + items.map((item) => `- ${item}`).join("\n")); } }; printSection("At the first deploy, the following functions will be created in your project:", functions); printSection("At the first deploy, the following Task Queues will be created in your project:", taskQueues); printSection("At the first deploy, the following Eventarc channels will be created in your project:", eventarcChannels); printSection("At the first deploy, the following APIs will be enabled in your project:", apis); printSection("At the first deploy, the following roles will be granted to the kit service account:", roles); const hasItems = functions.length > 0 || taskQueues.length > 0 || eventarcChannels.length > 0 || apis.length > 0 || roles.length > 0; if (hasItems) { (0, utils_1.logLabeledWarning)("functions", `${clc.bold("Please review the changes above. If you do not want them applied to your project, uninstall this kit before running firebase deploy.")}`); } } async function addKitInstanceOrConfigureProject(options, existingKit, existingFunctionsInfo) { const projectId = (0, projectUtils_1.getProjectId)(options) || options.projectId; const projectAlias = options.rc?.hasProjects && options.project && options.rc.hasProjectAlias(options.project) ? options.project : undefined; const isConfiguredForProject = isKitConfiguredForProject(options.config, existingKit, projectId, projectAlias); let action; if (!isConfiguredForProject && !options.nonInteractive) { const existingInstances = Object.keys(existingKit.instances || {}).join(", "); action = await (0, prompt_1.select)({ message: `The following instances already exist, but are not configured for this project: ${existingInstances}. What would you like to do?`, choices: [ { name: "Add an instance to the existing kit", value: "addInstance", }, { name: "Configure an existing instance for this project", value: "addEnv", }, ], }); } else { if (isConfiguredForProject) { (0, utils_1.logLabeledBullet)("functions", `This package is already installed as kit ${existingKit.kit}, creating a new instance.`); } action = "addInstance"; } if (action === "addInstance") { const instanceId = await promptKitInstanceId(existingKit.kit, existingFunctionsInfo.existingInstanceIds, existingFunctionsInfo.existingCodebases, options.nonInteractive, options.instanceId); const result = await addInstanceToKit({ config: options.config, kitId: existingKit.kit, instanceId, seedEnv: options.seedEnv, }); (0, utils_1.logLabeledSuccess)("functions", `Function kit instance ${clc.bold(instanceId)} successfully added to kit ${clc.bold(existingKit.kit)}.`); await printKitFirstDeployReport(options, instanceId, options.config.path(existingKit.source)); return { action: "addedInstance", kitId: existingKit.kit, instanceId, sourcePath: existingKit.source, configDirPath: result.configDirPath, }; } const selectedInstanceId = await promptExistingInstanceForProject(options, existingKit); return { action: "configuredEnv", kitId: existingKit.kit, instanceId: selectedInstanceId, }; } function findExistingKit(existingFunctions, options) { if (options.package) { const { packageName } = parseNpmPackageSpecifier(options.package); return existingFunctions.find((c) => (0, projectConfig_1.isKitConfig)(c) && c.sourcePackage?.name === packageName); } if (options.directory) { const absDir = path.isAbsolute(options.directory) ? options.directory : path.resolve(options.config.projectDir, options.directory); return existingFunctions.find((c) => (0, projectConfig_1.isKitConfig)(c) && !c.sourcePackage && path.resolve(options.config.projectDir, c.source) === absDir); } return undefined; } async function resolvePackageSource(options) { const templateType = options.template || exports.DEFAULT_TEMPLATE; if (!(templateType in exports.TEMPLATES)) { const validTemplates = Object.keys(exports.TEMPLATES) .map((t) => `'${t}'`) .join(" or "); throw new error_1.FirebaseError(`Invalid template '${templateType}'. Template must be ${validTemplates}.`); } const rawPkgName = options.package; if (!rawPkgName) { throw new error_1.FirebaseError("Set the --package option to a valid NPM package and try again."); } const { packageName } = parseNpmPackageSpecifier(rawPkgName); validateNpmPackageName(packageName); const isThirdParty = await promptSecurityConfirmation(rawPkgName, packageName, options.nonInteractive); return { defaultKitName: packageName, sourcePackageName: packageName, hasBuildScript: true, setup: (kitId, instanceId) => scaffoldKitFiles(options.config, kitId, instanceId, packageName, templateType), buildAndInstall: (absSourcePath) => buildAndInstallKit(absSourcePath, rawPkgName, isThirdParty), }; } async function resolveDirectorySource(options) { if (!options.directory) { throw new error_1.FirebaseError("Must specify --directory."); } const { absDirectoryPath, relSourcePath, hasBuildScript } = await validateAndResolveDirectoryKit(options.config.projectDir, options.directory); return { defaultKitName: path.basename(absDirectoryPath), sourcePackageName: undefined, hasBuildScript, setup: async (kitId, instanceId) => { const configDirPath = path.join(exports.FUNCTION_KITS_DIR, kitId, `config-${instanceId}`); const absConfigDirPath = options.config.path(configDirPath); await fs.ensureDir(absConfigDirPath); return { sourcePath: relSourcePath, configDirPath, absSourcePath: absDirectoryPath, absConfigDirPath, }; }, buildAndInstall: (absSourcePath) => buildAndInstallDirectoryKit(absSourcePath, hasBuildScript), }; } async function installKitOrInstance(options) { if (options.package && options.directory) { throw new error_1.FirebaseError("Cannot specify both --package and --directory. Please choose one."); } if (!options.package && !options.directory) { throw new error_1.FirebaseError("Must specify either --package or --directory."); } if (options.directory && options.template) { throw new error_1.FirebaseError("Cannot specify --template with --directory."); } const existingFunctionsInfo = extractExistingFunctionsInfo(options.config.src.functions); const existingKit = findExistingKit(existingFunctionsInfo.existingFunctions, options); if (existingKit) { return addKitInstanceOrConfigureProject(options, existingKit, existingFunctionsInfo); } const source = options.directory ? await resolveDirectorySource(options) : await resolvePackageSource(options); const kitId = await promptKitId(source.defaultKitName, existingFunctionsInfo.existingKitIds, options.nonInteractive, options.kitId); const instanceId = await promptKitInstanceId(kitId, existingFunctionsInfo.existingInstanceIds, existingFunctionsInfo.existingCodebases, options.nonInteractive, options.instanceId); const { sourcePath, configDirPath, absSourcePath, absConfigDirPath } = await source.setup(kitId, instanceId); if (options.seedEnv?.envs && Object.keys(options.seedEnv.envs).length > 0) { (0, env_2.seedKitInstanceEnv)({ configDir: absConfigDirPath, functionsSource: absSourcePath, projectDir: options.config.projectDir, projectId: options.seedEnv.projectId, projectAlias: options.seedEnv.projectAlias, envs: options.seedEnv.envs, }); } await source.buildAndInstall(absSourcePath); addKitToConfig(options.config, { kitId, instanceId, packageName: source.sourcePackageName, sourcePath, configDirPath, hasBuildScript: source.hasBuildScript, }); (0, utils_1.logLabeledSuccess)("functions", `Function kit ${clc.bold(kitId)} successfully installed.`); await printKitFirstDeployReport(options, instanceId, absSourcePath); return { action: "installedKit", kitId, instanceId, sourcePath, configDirPath, }; }