UNPKG

firebase-tools

Version:
139 lines (138 loc) 6.35 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.runUniversalMaker = runUniversalMaker; exports.localBuild = localBuild; const childProcess = require("child_process"); const fs = require("fs-extra"); const path = require("path"); const index_1 = require("./secrets/index"); const prompt_1 = require("../prompt"); const error_1 = require("../error"); const logger_1 = require("../logger"); const utils_1 = require("../utils"); const universalMakerDownload_1 = require("./universalMakerDownload"); async function runUniversalMaker(projectRoot, addedEnv) { const universalMakerBinary = await (0, universalMakerDownload_1.getOrDownloadUniversalMaker)(); executeUniversalMakerBinary(universalMakerBinary, projectRoot, addedEnv); return processUniversalMakerOutput(projectRoot); } function executeUniversalMakerBinary(universalMakerBinary, projectRoot, addedEnv) { try { const targetAppHosting = path.join(projectRoot, ".apphosting"); fs.removeSync(targetAppHosting); fs.ensureDirSync(targetAppHosting); const res = childProcess.spawnSync(universalMakerBinary, ["-application_dir", projectRoot, "-output_dir", projectRoot, "-output_format", "json"], { cwd: projectRoot, env: { ...process.env, ...addedEnv, X_GOOGLE_TARGET_PLATFORM: "fah", FIREBASE_OUTPUT_BUNDLE_DIR: targetAppHosting, }, stdio: "pipe", }); if (res.stdout) { logger_1.logger.debug("[Universal Maker stdout]:\n" + res.stdout.toString()); } if (res.stderr) { logger_1.logger.debug("[Universal Maker stderr]:\n" + res.stderr.toString()); } if (res.error) { throw res.error; } if (res.status !== 0) { throw new error_1.FirebaseError(`Universal Maker failed with exit code ${res.status ?? "unknown"}.`); } } catch (e) { if (e && typeof e === "object" && "code" in e && e.code === "EACCES") { throw new error_1.FirebaseError(`Failed to execute the Universal Maker binary at ${universalMakerBinary} due to permission constraints. Please assure you have set execution permissions (e.g., chmod +x) on the file.`); } throw e; } } function parseBundleYaml(projectRoot, defaultRunCommand) { const bundleYamlPath = path.join(projectRoot, ".apphosting", "bundle.yaml"); if (!fs.existsSync(bundleYamlPath)) { throw new error_1.FirebaseError("Failed to resolve build artifacts. Ensure Universal Maker produced a valid bundle.yaml with outputFiles."); } const bundleRaw = fs.readFileSync(bundleYamlPath, "utf-8"); const bundleData = (0, utils_1.wrappedSafeLoad)(bundleRaw); const runCommand = bundleData?.runConfig?.runCommand ?? defaultRunCommand; const outputFiles = bundleData?.outputFiles?.serverApp?.include ?? []; return { runCommand, outputFiles }; } function processUniversalMakerOutput(projectRoot) { const outputFilePath = path.join(projectRoot, "build_output.json"); if (!fs.existsSync(outputFilePath)) { throw new error_1.FirebaseError(`Universal Maker did not produce the expected output file at ${outputFilePath}`); } const outputRaw = fs.readFileSync(outputFilePath, "utf-8"); fs.unlinkSync(outputFilePath); let umOutput; try { umOutput = JSON.parse(outputRaw); } catch (e) { throw new error_1.FirebaseError(`Failed to parse build_output.json: ${(0, error_1.getErrMsg)(e)}`); } const defaultRunCommand = `${umOutput.command} ${umOutput.args.join(" ")}`; const { runCommand: finalRunCommand, outputFiles: finalOutputFiles } = parseBundleYaml(projectRoot, defaultRunCommand); return { runConfig: { runCommand: finalRunCommand, environmentVariables: Object.entries(umOutput.envVars || {}) .filter(([k]) => k !== "FIREBASE_OUTPUT_BUNDLE_DIR") .map(([k, v]) => ({ variable: k, value: String(v), availability: ["RUNTIME"], })), }, outputFiles: { serverApp: { include: finalOutputFiles, }, }, }; } async function localBuild(projectId, projectRoot, env = {}, options) { const hasBuildAvailableSecrets = Object.values(env).some((v) => v.secret && (!v.availability || v.availability.includes("BUILD"))); if (hasBuildAvailableSecrets && !options?.allowLocalBuildSecrets) { if (options?.nonInteractive) { throw new error_1.FirebaseError("Using build-available secrets during a local build in non-interactive mode requires the --allow-local-build-secrets flag."); } if (!(await (0, prompt_1.confirm)({ message: "Your build includes secrets that are available to the build environment. Using secrets in local builds may leave sensitive values in local artifacts/temporary files. Do you want to continue?", default: false, }))) { throw new error_1.FirebaseError("Cancelled local build due to BUILD-available secrets."); } } const addedEnv = await toProcessEnv(projectId, env); const apphostingBuildOutput = await runUniversalMaker(projectRoot, addedEnv); const discoveredEnv = apphostingBuildOutput.runConfig.environmentVariables?.map(({ variable, value, availability }) => ({ variable, value, availability: availability, })); return { outputFiles: apphostingBuildOutput.outputFiles?.serverApp.include ?? [], buildConfig: { runCommand: apphostingBuildOutput.runConfig.runCommand, env: discoveredEnv ?? [], }, }; } async function toProcessEnv(projectId, env) { const buildVars = Object.entries(env).filter(([, value]) => { return !value.availability || value.availability.includes("BUILD"); }); const resolvedEntries = await Promise.all(buildVars.map(async ([key, value]) => { const resolvedValue = value.secret ? await (0, index_1.loadSecret)(projectId, value.secret) : value.value || ""; return [key, resolvedValue]; })); return Object.fromEntries(resolvedEntries); }