firebase-tools
Version:
Command-Line Interface for Firebase
710 lines (709 loc) • 32.9 kB
JavaScript
;
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,
};
}