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