firebase-tools
Version:
Command-Line Interface for Firebase
1,058 lines (1,057 loc) • 48.8 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.validateNpmPackageExists = validateNpmPackageExists;
exports.sanitizePackageNameToKitName = sanitizePackageNameToKitName;
exports.isThirdPartyPackage = isThirdPartyPackage;
exports.checkPackageHasShrinkwrap = checkPackageHasShrinkwrap;
exports.getUnconfiguredInstancesForProject = getUnconfiguredInstancesForProject;
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.resolveSdkSpecifierToSave = resolveSdkSpecifierToSave;
exports.getKitPackagesToSave = getKitPackagesToSave;
exports.buildAndInstallKit = buildAndInstallKit;
exports.addKitToConfig = addKitToConfig;
exports.validateAndResolveDirectoryKit = validateAndResolveDirectoryKit;
exports.buildAndInstallDirectoryKit = buildAndInstallDirectoryKit;
exports.findKitConfig = findKitConfig;
exports.addInstanceToKitConfig = addInstanceToKitConfig;
exports.revertFunctionsConfig = revertFunctionsConfig;
exports.scaffoldKit = scaffoldKit;
exports.addInstanceToKit = addInstanceToKit;
exports.discoverKitBuild = discoverKitBuild;
exports.promptAndWriteKitParams = promptAndWriteKitParams;
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 semver = require("semver");
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 params = require("../../deploy/functions/params");
const experiments = require("../../experiments");
const iam = require("../../gcp/iam");
const functionsEnv = require("../env");
const functionsConfig = require("../../functionsConfig");
const prepare_1 = require("../../deploy/functions/prepare");
const functional_1 = require("../../functional");
const env_1 = require("../env");
const env_2 = require("./env");
const fsutils_1 = require("../../fsutils");
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.includes(baseId)) {
return baseId;
}
const prefix = baseId.slice(0, 35);
let candidate = "";
do {
const randomSuffix = crypto.randomBytes(2).toString("hex");
candidate = `${prefix}-${randomSuffix}`;
} while (existingIds.includes(candidate));
return candidate;
}
function parseNpmPackageSpecifier(rawPkg) {
const lastAt = rawPkg.lastIndexOf("@");
if (lastAt > 0) {
const version = rawPkg.substring(lastAt + 1);
return {
packageName: rawPkg.substring(0, lastAt),
...(version ? { version } : {}),
};
}
return { packageName: rawPkg };
}
function validateNpmPackageName(packageNameOrSpecifier) {
const { packageName, version } = parseNpmPackageSpecifier(packageNameOrSpecifier);
const npmPackageRegex = /^(?:@[a-z0-9_.-]+\/[a-z0-9_.-]+|[a-z0-9_.-]+)$/i;
if (!packageName ||
packageName.length > 214 ||
!npmPackageRegex.test(packageName) ||
(packageNameOrSpecifier.lastIndexOf("@") > 0 && !version)) {
throw new error_1.FirebaseError(`Invalid NPM package name '${packageNameOrSpecifier}'. Package names must be valid npm package specifiers (e.g. 'my-kit' or '@scope/my-kit').`);
}
}
async function validateNpmPackageExists(rawPkgName) {
validateNpmPackageName(rawPkgName);
try {
const output = await (0, spawn_1.spawnWithOutput)("npm", ["view", rawPkgName, "version"]);
if (!output.trim()) {
throw new Error(`No version found for '${rawPkgName}'`);
}
}
catch (err) {
throw new error_1.FirebaseError(`NPM package '${rawPkgName}' could not be found in the npm registry. Please verify the package name and version.`, { original: err instanceof Error ? err : undefined });
}
}
function sanitizePackageNameToKitName(packageNameOrSpecifier) {
const { packageName } = parseNpmPackageSpecifier(packageNameOrSpecifier);
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(packageNameOrSpecifier) {
const { packageName } = parseNpmPackageSpecifier(packageNameOrSpecifier);
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 getUnconfiguredInstancesForProject(config, kit, projectId, projectAlias) {
return Object.entries(kit.instances || {})
.filter(([, configDir]) => !(0, env_1.hasProjectEnv)(config.path(configDir), projectId, projectAlias))
.map(([instanceId]) => instanceId);
}
function extractExistingFunctionsInfo(configFunctions) {
const existingFunctions = configFunctions && (!Array.isArray(configFunctions) || configFunctions.length > 0)
? (0, projectConfig_1.normalizeAndValidate)(configFunctions)
: [];
const existingKitIds = [];
const existingCodebases = [];
const existingInstanceIds = [];
for (const c of existingFunctions) {
if ((0, projectConfig_1.isKitConfig)(c)) {
if (c.kit) {
existingKitIds.push(c.kit);
}
if (c.instances) {
existingInstanceIds.push(...Object.keys(c.instances));
}
}
else if (c.codebase) {
existingCodebases.push(c.codebase);
}
}
return {
existingFunctions,
existingKitIds,
existingCodebases,
existingInstanceIds,
};
}
async function promptKitInstanceId(baseKitId, existingInstanceIds, existingCodebases, nonInteractive, customInstanceId) {
const instanceCollisions = [...existingInstanceIds, ...existingCodebases];
const defaultInstanceId = generateUniqueId(baseKitId, instanceCollisions);
if (customInstanceId) {
(0, projectConfig_1.validateKitInstanceId)(customInstanceId);
if (existingInstanceIds.includes(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.includes(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.includes(val)) {
return `functions kit instance ID must be unique across all kits, but '${val}' was used more than once.`;
}
if (existingCodebases.includes(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.includes(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.includes(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.includes(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.includes(val)) {
return `functions.kit must be unique but '${val}' was used more than once.`;
}
return true;
},
});
(0, projectConfig_1.validateKit)(kitId);
if (existingKitIds.includes(kitId)) {
throw new error_1.FirebaseError(`functions.kit must be unique but '${kitId}' was used more than once.`);
}
return kitId;
}
async function promptSecurityConfirmation(options) {
const { rawPkgName, packageName, nonInteractive, force } = options;
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,
force,
});
if (!confirmInstallation) {
throw new error_1.FirebaseError("Installation cancelled.");
}
}
return isThirdParty;
}
async function promptExistingInstanceForProject(options) {
const allInstanceIds = Object.keys(options.existingKit.instances || {});
if (allInstanceIds.length === 0) {
throw new error_1.FirebaseError(`Kit '${options.existingKit.kit}' has no instances configured.`);
}
if (options.instanceId) {
if (!allInstanceIds.includes(options.instanceId)) {
throw new error_1.FirebaseError(`Instance '${options.instanceId}' does not exist in kit '${options.existingKit.kit}'. Available instances: ${allInstanceIds.join(", ")}`);
}
if (!options.unconfiguredInstanceIds.includes(options.instanceId)) {
throw new error_1.FirebaseError(`Instance '${options.instanceId}' is already configured for this project.`);
}
return options.instanceId;
}
if (options.unconfiguredInstanceIds.length === 0) {
throw new error_1.FirebaseError(`Kit '${options.existingKit.kit}' has no unconfigured instances for this project.`);
}
if (options.unconfiguredInstanceIds.length === 1) {
const instanceId = options.unconfiguredInstanceIds[0];
(0, utils_1.logLabeledBullet)("functions", `${clc.bold(instanceId)} is the only instance without a configuration. Configuring...`);
return instanceId;
}
return (0, prompt_1.select)({
message: "Which instance would you like to configure for this project?",
choices: options.unconfiguredInstanceIds,
nonInteractive: options.nonInteractive,
});
}
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 packageKitTemplate = (0, templates_1.readTemplateSync)("init/functions/typescript/package-kit.json");
const subbedTemplate = packageKitTemplate.replace(/{{RUNTIME}}/g, latestNodeVersion);
try {
pkgJson = JSON.parse(subbedTemplate);
}
catch (err) {
throw new error_1.FirebaseError("Failed to parse package-kit.json template: " + (0, error_1.getErrMsg)(err), {
original: err instanceof Error ? err : undefined,
});
}
}
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 };
}
function resolveSdkSpecifierToSave(packageName, declaredVer, installedVer) {
const cleanVer = semver.clean(declaredVer);
if (cleanVer) {
return `${packageName}@=${installedVer || cleanVer}`;
}
if (declaredVer.trim().startsWith("~")) {
return installedVer ? `${packageName}@~${installedVer}` : `${packageName}@${declaredVer}`;
}
if (installedVer) {
return `${packageName}@^${installedVer}`;
}
return `${packageName}@${declaredVer}`;
}
async function getKitPackagesToSave(absSourcePath, packageName) {
const lockfilePath = path.join(absSourcePath, "package-lock.json");
let peerDeps = {};
let directDeps = {};
let installedFunctionsVer;
let installedAdminVer;
if (await fs.pathExists(lockfilePath)) {
try {
const lockfile = (await fs.readJson(lockfilePath));
const packages = lockfile.packages || {};
const kitEntry = packages[`node_modules/${packageName}`] || {};
peerDeps = kitEntry.peerDependencies || {};
directDeps = kitEntry.dependencies || {};
installedFunctionsVer = packages["node_modules/firebase-functions"]?.version;
installedAdminVer = packages["node_modules/firebase-admin"]?.version;
}
catch (err) {
logger_1.logger.debug(`Failed to read package-lock.json: ${(0, error_1.getErrMsg)(err)}`);
}
}
if (Object.keys(peerDeps).length === 0 && Object.keys(directDeps).length === 0) {
const kitPkgJsonPath = path.join(absSourcePath, "node_modules", packageName, "package.json");
if (await fs.pathExists(kitPkgJsonPath)) {
try {
const kitPkgJson = (await fs.readJson(kitPkgJsonPath));
peerDeps = kitPkgJson.peerDependencies || {};
directDeps = kitPkgJson.dependencies || {};
}
catch (err) {
logger_1.logger.debug(`Failed to read kit package.json: ${(0, error_1.getErrMsg)(err)}`);
}
}
}
const declaredFunctionsVer = peerDeps["firebase-functions"] || directDeps["firebase-functions"];
if (!declaredFunctionsVer) {
throw new error_1.FirebaseError(`Package '${packageName}' is not a valid Function Kit: it must declare 'firebase-functions' as a dependency or peer dependency.`);
}
const packagesToSave = [
resolveSdkSpecifierToSave("firebase-functions", declaredFunctionsVer, installedFunctionsVer),
];
const declaredAdminVer = peerDeps["firebase-admin"] || directDeps["firebase-admin"];
if (declaredAdminVer) {
packagesToSave.push(resolveSdkSpecifierToSave("firebase-admin", declaredAdminVer, installedAdminVer));
}
return packagesToSave;
}
async function buildAndInstallKit(absSourcePath, rawPkgName, isThirdParty) {
const { packageName } = parseNpmPackageSpecifier(rawPkgName);
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)}`);
}
const packagesToSave = await getKitPackagesToSave(absSourcePath, packageName);
if (packagesToSave.length > 0) {
(0, utils_1.logLabeledBullet)("functions", `Saving resolved SDK dependencies: ${clc.bold(packagesToSave.join(", "))}...`);
const saveArgs = ["install", ...packagesToSave, "--save-prefix=^"];
if (isThirdParty) {
saveArgs.push("--ignore-scripts");
}
try {
await (0, spawn_1.wrapSpawn)("npm", saveArgs, absSourcePath);
}
catch (err) {
throw new error_1.FirebaseError(`Failed to install required SDK dependencies (${packagesToSave.join(", ")}): ${(0, error_1.getErrMsg)(err)}. ` +
"Verify that the kit's dependencies are compatible with the resolved Firebase SDK versions.", { original: err instanceof Error ? err : undefined });
}
}
(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);
}
function revertFunctionsConfig(options) {
try {
if (!(0, utils_1.deepEqual)(options.config.src.functions, options.originalFunctions)) {
if (options.originalFunctions === undefined) {
delete options.config.src.functions;
}
else {
options.config.src.functions = options.originalFunctions;
}
options.config.writeProjectFile("firebase.json", options.config.src);
}
}
catch (err) {
logger_1.logger.debug(`Failed to revert firebase.json: ${(0, error_1.getErrMsg)(err)}`);
}
}
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);
const firebaseEnvs = functionsEnv.loadFirebaseEnvs({ projectId }, projectId, options.instanceId);
return runtimeDelegate.discoverBuild({}, firebaseEnvs);
}
async function promptAndWriteKitParams(options) {
if (!options.params || options.params.length === 0) {
return;
}
if (!options.projectId) {
logger_1.logger.debug("Skipping functions kit parameter prompt: no active project ID.");
return;
}
const userEnvOpt = {
configDir: options.absConfigDirPath,
functionsSource: options.absSourcePath,
projectDir: options.config.projectDir,
projectId: options.projectId,
projectAlias: options.projectAlias,
};
const rawUserEnvs = functionsEnv.loadUserEnvs(userEnvOpt);
const { userEnvs, secretRefs } = (0, prepare_1.partitionUserEnvs)(rawUserEnvs);
const parsedSecretRefs = (0, functional_1.mapObject)(secretRefs, (unparsed) => build.parseSecretRef(unparsed));
const secretPrefixForInstance = (0, projectConfig_1.addKitPrefix)(options.instanceId);
for (const secretParam of options.params.filter((p) => params.isSecretParam(p))) {
secretParam.resourceId = `${secretPrefixForInstance}-${secretParam.name}`;
}
build.applyEnvSecretBindingsToParams(options.params, parsedSecretRefs);
let firebaseConfig = { projectId: options.projectId };
try {
firebaseConfig = await functionsConfig.getFirebaseConfig({ projectId: options.projectId });
}
catch (err) {
logger_1.logger.debug(`Could not fetch Firebase config for project ${options.projectId}: ${(0, error_1.getErrMsg)(err)}`);
}
const typedUserEnvs = build.envWithTypes(options.params, userEnvs);
const { paramValues: resolvedEnvs, secretRefs: resolvedSecretRefs } = await params.resolveParams({
params: options.params,
firebaseConfig: firebaseConfig,
userEnvs: typedUserEnvs,
codebase: options.instanceId,
nonInteractive: options.nonInteractive,
});
functionsEnv.writeResolvedParams(resolvedEnvs, userEnvs, userEnvOpt);
if (experiments.isEnabled("secretEnvParams") &&
experiments.isEnabled("writeDefaultSecretBindings")) {
functionsEnv.writeResolvedSecretRefs(resolvedSecretRefs, secretRefs, userEnvOpt);
}
}
async function printKitFirstDeployReport(options) {
let discoveredBuild;
const prefix = (0, projectConfig_1.addKitPrefix)(options.instanceId);
try {
discoveredBuild = options.preDiscoveredBuild
? (0, utils_1.cloneDeep)(options.preDiscoveredBuild)
: await discoverKitBuild(options, options.absSourcePath);
build.applyEndpointPrefix(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) {
const { existingKit, existingFunctionsInfo, createdPaths = [] } = options;
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 unconfiguredInstances = getUnconfiguredInstancesForProject(options.config, existingKit, projectId, projectAlias);
const isConfiguredForProject = Object.keys(existingKit.instances || {}).length > 0 && unconfiguredInstances.length === 0;
let action;
if (options.instanceId &&
existingKit.instances &&
Object.prototype.hasOwnProperty.call(existingKit.instances, options.instanceId)) {
if (!unconfiguredInstances.includes(options.instanceId)) {
throw new error_1.FirebaseError(`Instance '${options.instanceId}' is already configured for this project.`);
}
action = "addEnv";
}
else if (options.instanceId) {
action = "addInstance";
}
else if (unconfiguredInstances.length > 0 && !options.nonInteractive) {
const unconfiguredInstancesList = unconfiguredInstances.join(", ");
action = await (0, prompt_1.select)({
message: `The following instances already exist, but are not configured for this project: ${unconfiguredInstancesList}. 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";
}
let resultAction;
let instanceId;
let configDirPath;
let absConfigDirPath;
if (action === "addInstance") {
resultAction = "addedInstance";
instanceId = await promptKitInstanceId(options.defaultInstanceId ?? existingKit.kit, existingFunctionsInfo.existingInstanceIds, existingFunctionsInfo.existingCodebases, options.nonInteractive, options.instanceId);
const expectedConfigDirPath = path.join(exports.FUNCTION_KITS_DIR, existingKit.kit, `config-${instanceId}`);
absConfigDirPath = options.config.path(expectedConfigDirPath);
createdPaths.push(absConfigDirPath);
const result = await addInstanceToKit({
config: options.config,
kitId: existingKit.kit,
instanceId,
seedEnv: options.seedEnv,
});
configDirPath = result.configDirPath;
absConfigDirPath = result.absConfigDirPath;
}
else if (action === "addEnv") {
resultAction = "configuredEnv";
instanceId = await promptExistingInstanceForProject({
...options,
existingKit,
unconfiguredInstanceIds: unconfiguredInstances,
});
configDirPath = existingKit.instances?.[instanceId];
if (!configDirPath) {
throw new error_1.FirebaseError(`Configuration directory for instance '${instanceId}' not found in kit '${existingKit.kit}'.`);
}
absConfigDirPath = options.config.path(configDirPath);
if (projectId) {
const envPath = path.join(absConfigDirPath, `.env.${projectId}`);
if (!(await fs.pathExists(envPath))) {
createdPaths.push(envPath);
}
}
if (options.seedEnv?.envs && Object.keys(options.seedEnv.envs).length > 0) {
await fs.ensureDir(absConfigDirPath);
(0, env_2.seedKitInstanceEnv)({
configDir: absConfigDirPath,
functionsSource: options.config.path(existingKit.source),
projectDir: options.config.projectDir,
projectId: options.seedEnv.projectId,
projectAlias: options.seedEnv.projectAlias,
envs: options.seedEnv.envs,
});
}
}
else {
throw new error_1.FirebaseError(`Unexpected action '${String(action)}' for kit installation.`);
}
if (projectId) {
fs.ensureFileSync(path.join(absConfigDirPath, `.env.${projectId}`));
}
const absSourcePath = options.config.path(existingKit.source);
let discoveredBuild;
const shouldConfigure = options.configure !== false;
if (shouldConfigure) {
try {
discoveredBuild = await discoverKitBuild({ ...options, instanceId }, absSourcePath);
}
catch (err) {
logger_1.logger.debug(`Could not discover kit build for params prompting: ${(0, error_1.getErrMsg)(err)}`);
}
if (discoveredBuild?.params && discoveredBuild.params.length > 0) {
await promptAndWriteKitParams({
config: options.config,
projectId,
projectAlias,
absConfigDirPath,
absSourcePath,
instanceId,
nonInteractive: options.nonInteractive,
params: discoveredBuild.params,
});
}
}
if (action === "addInstance") {
(0, utils_1.logLabeledSuccess)("functions", `Function kit instance ${clc.bold(instanceId)} successfully added to kit ${clc.bold(existingKit.kit)}.`);
}
else if (action === "addEnv") {
const projectDisplay = projectId || options.project || "project";
(0, utils_1.logLabeledSuccess)("functions", `Function kit instance ${clc.bold(instanceId)} successfully configured for ${clc.bold(projectDisplay)}.`);
}
await printKitFirstDeployReport({
config: options.config,
project: options.project,
projectId: options.projectId,
instanceId,
absSourcePath,
preDiscoveredBuild: discoveredBuild,
});
return {
action: resultAction,
kitId: existingKit.kit,
instanceId,
sourcePath: existingKit.source,
configDirPath,
};
}
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.");
}
await validateNpmPackageExists(rawPkgName);
const { packageName } = parseNpmPackageSpecifier(rawPkgName);
const isThirdParty = await promptSecurityConfirmation({
rawPkgName,
packageName,
nonInteractive: options.nonInteractive,
force: options.force,
});
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.");
}
if (options.package) {
validateNpmPackageName(options.package);
}
const originalFunctions = (0, utils_1.cloneDeep)(options.config.src.functions);
const createdPaths = [];
let kitId;
try {
const existingFunctionsInfo = extractExistingFunctionsInfo(options.config.src.functions);
const existingKit = findExistingKit(existingFunctionsInfo.existingFunctions, options);
if (existingKit) {
kitId = existingKit.kit;
return await addKitInstanceOrConfigureProject({
...options,
existingKit,
existingFunctionsInfo,
createdPaths,
});
}
const source = options.directory
? await resolveDirectorySource(options)
: await resolvePackageSource(options);
kitId = await promptKitId(source.defaultKitName, existingFunctionsInfo.existingKitIds, options.nonInteractive, options.kitId);
const instanceId = await promptKitInstanceId(options.defaultInstanceId ?? kitId, existingFunctionsInfo.existingInstanceIds, existingFunctionsInfo.existingCodebases, options.nonInteractive, options.instanceId);
const isPackageKit = !options.directory;
const absKitDir = options.config.path(path.join(exports.FUNCTION_KITS_DIR, kitId));
const expectedConfigDirPath = path.join(exports.FUNCTION_KITS_DIR, kitId, `config-${instanceId}`);
const absConfigDirPath = options.config.path(expectedConfigDirPath);
if (isPackageKit) {
createdPaths.push(absKitDir);
}
else {
createdPaths.push(absConfigDirPath);
}
const setupResult = await source.setup(kitId, instanceId);
const { sourcePath, configDirPath, absSourcePath } = setupResult;
if (options.seedEnv?.envs && Object.keys(options.seedEnv.envs).length > 0) {
(0, env_2.seedKitInstanceEnv)({
configDir: setupResult.absConfigDirPath,
functionsSource: absSourcePath,
projectDir: options.config.projectDir,
projectId: options.seedEnv.projectId,
projectAlias: options.seedEnv.projectAlias,
envs: options.seedEnv.envs,
});
}
await source.buildAndInstall(absSourcePath);
const projectId = (0, projectUtils_1.getProjectId)(options) || options.projectId;
const projectAlias = options.rc?.hasProjects && options.project && options.rc.hasProjectAlias(options.project)
? options.project
: undefined;
if (projectId) {
fs.ensureFileSync(path.join(setupResult.absConfigDirPath, `.env.${projectId}`));
}
let discoveredBuild;
const shouldConfigure = options.configure !== false;
if (shouldConfigure) {
try {
discoveredBuild = await discoverKitBuild({ ...options, instanceId }, absSourcePath);
}
catch (err) {
logger_1.logger.debug(`Could not discover kit build for params prompting: ${(0, error_1.getErrMsg)(err)}`);
}
if (discoveredBuild?.params && discoveredBuild.params.length > 0) {
await promptAndWriteKitParams({
config: options.config,
projectId,
projectAlias,
absConfigDirPath: setupResult.absConfigDirPath,
absSourcePath,
instanceId,
nonInteractive: options.nonInteractive,
params: discoveredBuild.params,
});
}
}
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({
config: options.config,
project: options.project,
projectId: options.projectId,
instanceId,
absSourcePath,
preDiscoveredBuild: discoveredBuild,
});
return {
action: "installedKit",
kitId,
instanceId,
sourcePath,
configDirPath,
};
}
catch (err) {
await Promise.all(createdPaths.map((targetPath) => fs.remove(targetPath).catch((cleanupErr) => {
logger_1.logger.debug(`Failed to clean up path '${targetPath}': ${(0, error_1.getErrMsg)(cleanupErr)}`);
})));
if (kitId) {
await (0, fsutils_1.removeDirectoryIfEmpty)(options.config.path(path.join(exports.FUNCTION_KITS_DIR, kitId)));
}
await (0, fsutils_1.removeDirectoryIfEmpty)(options.config.path(exports.FUNCTION_KITS_DIR));
revertFunctionsConfig({ config: options.config, originalFunctions });
throw err;
}
}