UNPKG

firebase-tools

Version:
160 lines (159 loc) • 6.52 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.askQuestions = askQuestions; exports.actuate = actuate; exports.upsertRunConfig = upsertRunConfig; const path = require("path"); const deploy_1 = require("../../deploy"); const prereqs_1 = require("../../deploy/run/prereqs"); const util_1 = require("../../deploy/run/util"); const error_1 = require("../../error"); const fsutils_1 = require("../../fsutils"); const run = require("../../gcp/run"); const runv2 = require("../../gcp/runv2"); const prompt_1 = require("../../prompt"); const requirePermissions_1 = require("../../requirePermissions"); const utils_1 = require("../../utils"); const DEFAULT_IGNORE = ["node_modules", ".git", "firebase-debug.log", "firebase-debug.*.log"]; const SERVICE_ID_REGEX = /^[a-z]([-a-z0-9]{0,47}[a-z0-9])?$/; async function askQuestions(setup, config, options) { const projectId = setup.projectId; if (!projectId) { throw new error_1.FirebaseError("Cloud Run requires a Firebase project. Run firebase use --add first."); } await (0, requirePermissions_1.requirePermissions)({ ...options, projectId }, prereqs_1.RUN_PERMISSIONS); await (0, prereqs_1.prereqs)(projectId); let existing; const action = await (0, prompt_1.select)({ message: "Please select an option", choices: [ { name: "Create a new service and deploy", value: "create" }, { name: "Update an existing service and deploy", value: "update" }, ], default: "create", }); if (action === "update") { existing = await promptExistingService(projectId); } let serviceId; let region; if (existing) { ({ region, serviceId } = parseServiceName(existing.name)); } else { region = await (0, prompt_1.select)({ message: "Which region should this service be deployed to?", choices: await run.listLocations(projectId), default: "us-central1", }); serviceId = await promptNewServiceId(projectId, region); } const savedEntry = findRunEntry(getRunEntries(config), serviceId, region); const localBuild = await (0, prompt_1.select)({ message: "Would you like to build your app locally or remotely?", choices: [ { name: "Build remotely on Cloud Build", value: false }, { name: "Build locally", value: true }, ], default: savedEntry?.localBuild === true, }); const rawBaseImage = await (0, prompt_1.input)({ message: localBuild ? "Which base image should your app use? (e.g. nodejs20, nodejs22)" : "Which base image should your app use, if any? (e.g. nodejs20, nodejs22)", default: (0, util_1.mainContainer)(existing?.template)?.baseImageUri, validate: (img) => localBuild && !img.trim() ? "Local builds require a base image." : true, }); const baseImage = (rawBaseImage || "").trim(); const rootDir = await promptRootDir(config.projectDir, savedEntry?.rootDir ?? "/"); const runInfo = { serviceId, region, baseImage, rootDir }; if (localBuild) { runInfo.localBuild = true; } setup.featureInfo = { ...setup.featureInfo, run: runInfo }; } async function promptExistingService(projectId) { const allServices = await runv2.listServices(projectId, false); const services = allServices.filter((s) => !s.labels?.[runv2.CLIENT_NAME_LABEL]); if (services.length === 0) { (0, utils_1.logBullet)("No Cloud Run services to update. Creating a new service instead."); return undefined; } const choices = services.map((service) => { const { serviceId, region } = parseServiceName(service.name); return { name: `${serviceId} (${region})`, value: service }; }); return (0, prompt_1.select)({ message: "Which service would you like to update?", choices, }); } function parseServiceName(name) { const parts = name.split("/"); return { region: parts[3], serviceId: parts[5] }; } async function promptNewServiceId(projectId, region) { return (0, prompt_1.input)({ message: "Please enter a unique ID for your service", validate: async (id) => { if (!SERVICE_ID_REGEX.test(id)) { return "Use up to 49 lowercase letters, digits, and hyphens, starting with a letter and not ending with a hyphen."; } const existing = await (0, util_1.getExistingService)(projectId, region, id); if (existing) { return `A service named ${id} already exists in ${region}.`; } return true; }, }); } async function promptRootDir(projectDir, defaultDir) { return (0, prompt_1.input)({ message: "Specify your app's root directory relative to your firebase.json directory", default: defaultDir, validate: (dir) => { const absPath = path.join(projectDir, dir); if (!(0, fsutils_1.dirExistsSync)(absPath)) { return `Directory ${absPath} does not exist. Please enter a valid directory.`; } return true; }, }); } async function actuate(setup, config, options) { const info = setup.featureInfo?.run; if (!info) { return; } const service = { serviceId: info.serviceId, rootDir: info.rootDir, region: info.region }; if (info.localBuild) { upsertRunConfig({ ...service, localBuild: true }, config); } else { upsertRunConfig(service, config); } await (0, deploy_1.deploy)(["run"], { ...options, projectId: setup.projectId, config, only: `run:${(0, util_1.fullServiceName)(info)}` }, { baseImage: info.baseImage || null }); } function upsertRunConfig(service, config) { const entries = getRunEntries(config); const existing = findRunEntry(entries, service.serviceId, service.region); if (existing) { existing.rootDir = service.rootDir; if (service.localBuild) { existing.localBuild = true; } else { delete existing.localBuild; } } else { entries.push({ ...service, ignore: DEFAULT_IGNORE }); } config.set("run", entries.length === 1 ? entries[0] : entries); } function getRunEntries(config) { return [config.src.run ?? []].flat(); } function findRunEntry(entries, serviceId, region) { return entries.find((e) => e.serviceId === serviceId && e.region === region); }