firebase-tools
Version:
Command-Line Interface for Firebase
160 lines (159 loc) • 6.52 kB
JavaScript
;
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);
}