firebase-tools
Version:
Command-Line Interface for Firebase
428 lines (427 loc) • 24 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.prepareFrameworks = exports.generateSSRCodebaseId = exports.discover = exports.WebFrameworks = void 0;
const path_1 = require("path");
const process_1 = require("process");
const child_process_1 = require("child_process");
const cross_spawn_1 = require("cross-spawn");
const promises_1 = require("fs/promises");
const fs_extra_1 = require("fs-extra");
const glob_1 = require("glob");
const process = require("node:process");
const projectUtils_1 = require("../projectUtils");
const config_1 = require("../hosting/config");
const api_1 = require("../hosting/api");
const apps_1 = require("../management/apps");
const prompt_1 = require("../prompt");
const types_1 = require("../emulator/types");
const defaultCredentials_1 = require("../defaultCredentials");
const auth_1 = require("../auth");
const functionsEmulatorShared_1 = require("../emulator/functionsEmulatorShared");
const constants_1 = require("../emulator/constants");
const error_1 = require("../error");
const requireHostingSite_1 = require("../requireHostingSite");
const experiments = require("../experiments");
const implicitInit_1 = require("../hosting/implicitInit");
const utils_1 = require("./utils");
const constants_2 = require("./constants");
const utils_2 = require("../utils");
const ensureTargeted_1 = require("../functions/ensureTargeted");
const util_1 = require("util");
const projectPath_1 = require("../projectPath");
const logger_1 = require("../logger");
const frameworks_1 = require("./frameworks");
Object.defineProperty(exports, "WebFrameworks", { enumerable: true, get: function () { return frameworks_1.WebFrameworks; } });
const fetchWebSetup_1 = require("../fetchWebSetup");
async function discover(dir, warn = true) {
const allFrameworkTypes = [
...new Set(Object.values(frameworks_1.WebFrameworks).map(({ type }) => type)),
].sort();
for (const discoveryType of allFrameworkTypes) {
const frameworksDiscovered = [];
for (const framework in frameworks_1.WebFrameworks) {
if (frameworks_1.WebFrameworks[framework]) {
const { discover, type } = frameworks_1.WebFrameworks[framework];
if (type !== discoveryType)
continue;
const result = await discover(dir);
if (result)
frameworksDiscovered.push(Object.assign({ framework }, result));
}
}
if (frameworksDiscovered.length > 1) {
if (warn)
console.error("Multiple conflicting frameworks discovered.");
return;
}
if (frameworksDiscovered.length === 1)
return frameworksDiscovered[0];
}
if (warn)
console.warn("Could not determine the web framework in use.");
return;
}
exports.discover = discover;
const BUILD_MEMO = new Map();
function memoizeBuild(dir, build, deps, target, context) {
const key = [dir, ...deps];
for (const existingKey of BUILD_MEMO.keys()) {
if ((0, util_1.isDeepStrictEqual)(existingKey, key)) {
return BUILD_MEMO.get(existingKey);
}
}
const value = build(dir, target, context);
BUILD_MEMO.set(key, value);
return value;
}
function generateSSRCodebaseId(site) {
return `firebase-frameworks-${site}`;
}
exports.generateSSRCodebaseId = generateSSRCodebaseId;
async function prepareFrameworks(purpose, targetNames, context, options, emulators = []) {
var _a, _b, _c, _d, _e, _f;
var _g, _h, _j, _k, _l;
const project = (0, projectUtils_1.needProjectId)(context || options);
const isDemoProject = constants_1.Constants.isDemoProject(project);
const projectRoot = (0, projectPath_1.resolveProjectPath)(options, ".");
const account = (0, auth_1.getProjectDefaultAccount)(projectRoot);
if (isDemoProject) {
options.site = project;
}
if (!options.site) {
try {
await (0, requireHostingSite_1.requireHostingSite)(options);
}
catch (_m) {
options.site = project;
}
}
const configs = (0, config_1.hostingConfig)(options);
let firebaseDefaults = undefined;
if (configs.length === 0) {
return;
}
const allowedRegionsValues = constants_2.ALLOWED_SSR_REGIONS.map((r) => r.value);
for (const config of configs) {
const { source, site, public: publicDir, frameworksBackend } = config;
if (!source) {
continue;
}
config.rewrites || (config.rewrites = []);
config.redirects || (config.redirects = []);
config.headers || (config.headers = []);
(_a = config.cleanUrls) !== null && _a !== void 0 ? _a : (config.cleanUrls = true);
const dist = (0, path_1.join)(projectRoot, ".firebase", site);
const hostingDist = (0, path_1.join)(dist, "hosting");
const functionsDist = (0, path_1.join)(dist, "functions");
if (publicDir) {
throw new Error(`hosting.public and hosting.source cannot both be set in firebase.json`);
}
const ssrRegion = (_b = frameworksBackend === null || frameworksBackend === void 0 ? void 0 : frameworksBackend.region) !== null && _b !== void 0 ? _b : constants_2.DEFAULT_REGION;
const omitCloudFunction = (_c = frameworksBackend === null || frameworksBackend === void 0 ? void 0 : frameworksBackend.omit) !== null && _c !== void 0 ? _c : false;
if (!allowedRegionsValues.includes(ssrRegion)) {
const validRegions = (0, utils_1.conjoinOptions)(allowedRegionsValues);
throw new error_1.FirebaseError(`Hosting config for site ${site} places server-side content in region ${ssrRegion} which is not known. Valid regions are ${validRegions}`);
}
const getProjectPath = (...args) => (0, path_1.join)(projectRoot, source, ...args);
const functionId = `ssr${site.toLowerCase().replace(/-/g, "").substring(0, 20)}`;
const usesFirebaseAdminSdk = !!(0, utils_1.findDependency)("firebase-admin", { cwd: getProjectPath() });
const usesFirebaseJsSdk = !!(0, utils_1.findDependency)("@firebase/app", { cwd: getProjectPath() });
if (usesFirebaseAdminSdk) {
process.env.GOOGLE_CLOUD_PROJECT = project;
if (account && !process.env.GOOGLE_APPLICATION_CREDENTIALS) {
const defaultCredPath = await (0, defaultCredentials_1.getCredentialPathAsync)(account);
if (defaultCredPath)
process.env.GOOGLE_APPLICATION_CREDENTIALS = defaultCredPath;
}
}
emulators.forEach((info) => {
if (usesFirebaseAdminSdk) {
if (info.name === types_1.Emulators.FIRESTORE)
process.env[constants_1.Constants.FIRESTORE_EMULATOR_HOST] = (0, functionsEmulatorShared_1.formatHost)(info);
if (info.name === types_1.Emulators.AUTH)
process.env[constants_1.Constants.FIREBASE_AUTH_EMULATOR_HOST] = (0, functionsEmulatorShared_1.formatHost)(info);
if (info.name === types_1.Emulators.DATABASE)
process.env[constants_1.Constants.FIREBASE_DATABASE_EMULATOR_HOST] = (0, functionsEmulatorShared_1.formatHost)(info);
if (info.name === types_1.Emulators.STORAGE)
process.env[constants_1.Constants.FIREBASE_STORAGE_EMULATOR_HOST] = (0, functionsEmulatorShared_1.formatHost)(info);
}
if (usesFirebaseJsSdk && types_1.EMULATORS_SUPPORTED_BY_USE_EMULATOR.includes(info.name)) {
firebaseDefaults || (firebaseDefaults = {});
firebaseDefaults.emulatorHosts || (firebaseDefaults.emulatorHosts = {});
firebaseDefaults.emulatorHosts[info.name] = (0, functionsEmulatorShared_1.formatHost)(info);
}
});
let firebaseConfig = null;
if (usesFirebaseJsSdk) {
const sites = isDemoProject ? (0, api_1.listDemoSites)(project) : await (0, api_1.listSites)(project);
const selectedSite = sites.find((it) => it.name && it.name.split("/").pop() === site);
if (selectedSite) {
const { appId } = selectedSite;
if (appId) {
firebaseConfig = isDemoProject
? (0, fetchWebSetup_1.constructDefaultWebSetup)(project)
: await (0, apps_1.getAppConfig)(appId, apps_1.AppPlatform.WEB);
firebaseDefaults || (firebaseDefaults = {});
firebaseDefaults.config = firebaseConfig;
}
else {
const defaultConfig = await (0, implicitInit_1.implicitInit)(options);
if (defaultConfig.json) {
console.warn(`No Firebase app associated with site ${site}, injecting project default config.
You can link a Web app to a Hosting site here https://console.firebase.google.com/project/${project}/settings/general/web`);
firebaseDefaults || (firebaseDefaults = {});
firebaseDefaults.config = JSON.parse(defaultConfig.json);
}
else {
console.warn(`No Firebase app associated with site ${site}, unable to provide authenticated server context.
You can link a Web app to a Hosting site here https://console.firebase.google.com/project/${project}/settings/general/web`);
if (!options.nonInteractive) {
const continueDeploy = await (0, prompt_1.confirm)({
default: true,
message: "Would you like to continue with the deploy?",
});
if (!continueDeploy)
(0, process_1.exit)(1);
}
}
}
}
}
if (firebaseDefaults) {
process.env.__FIREBASE_DEFAULTS__ = JSON.stringify(firebaseDefaults);
}
const results = await discover(getProjectPath());
if (!results) {
throw new error_1.FirebaseError((0, utils_1.frameworksCallToAction)("Unable to detect the web framework in use, check firebase-debug.log for more info."));
}
const { framework, mayWantBackend } = results;
const { build, ɵcodegenPublicDirectory, ɵcodegenFunctionsDirectory: codegenProdModeFunctionsDirectory, getDevModeHandle, name, support, docsUrl, supportedRange, getValidBuildTargets = constants_2.GET_DEFAULT_BUILD_TARGETS, shouldUseDevModeHandle = constants_2.DEFAULT_SHOULD_USE_DEV_MODE_HANDLE, } = frameworks_1.WebFrameworks[framework];
logger_1.logger.info(`\n${(0, utils_1.frameworksCallToAction)(constants_2.SupportLevelWarnings[support](name), docsUrl, " ", name, results.version, supportedRange, results.vite)}\n`);
const hostingEmulatorInfo = emulators.find((e) => e.name === types_1.Emulators.HOSTING);
const validBuildTargets = await getValidBuildTargets(purpose, getProjectPath());
const frameworksBuildTarget = (0, utils_1.getFrameworksBuildTarget)(purpose, validBuildTargets);
const useDevModeHandle = purpose !== "deploy" &&
(await shouldUseDevModeHandle(frameworksBuildTarget, getProjectPath()));
const frameworkContext = {
projectId: project,
site: options.site,
hostingChannel: context === null || context === void 0 ? void 0 : context.hostingChannel,
};
let codegenFunctionsDirectory;
let baseUrl = "";
const rewrites = [];
const redirects = [];
const headers = [];
const devModeHandle = useDevModeHandle &&
getDevModeHandle &&
(await getDevModeHandle(getProjectPath(), frameworksBuildTarget, hostingEmulatorInfo));
if (devModeHandle) {
options.frameworksDevModeHandle = devModeHandle;
if (mayWantBackend && firebaseDefaults) {
codegenFunctionsDirectory = codegenDevModeFunctionsDirectory;
}
}
else {
const buildResult = await memoizeBuild(getProjectPath(), build, [firebaseDefaults, frameworksBuildTarget], frameworksBuildTarget, frameworkContext);
const { wantsBackend = false, trailingSlash, i18n = false } = buildResult || {};
if (buildResult) {
baseUrl = (_d = buildResult.baseUrl) !== null && _d !== void 0 ? _d : baseUrl;
if (buildResult.headers)
headers.push(...buildResult.headers);
if (buildResult.rewrites)
rewrites.push(...buildResult.rewrites);
if (buildResult.redirects)
redirects.push(...buildResult.redirects);
}
(_e = config.trailingSlash) !== null && _e !== void 0 ? _e : (config.trailingSlash = trailingSlash);
if (i18n)
(_f = config.i18n) !== null && _f !== void 0 ? _f : (config.i18n = { root: constants_2.I18N_ROOT });
if (await (0, fs_extra_1.pathExists)(hostingDist))
await (0, promises_1.rm)(hostingDist, { recursive: true });
await (0, fs_extra_1.mkdirp)(hostingDist);
await ɵcodegenPublicDirectory(getProjectPath(), hostingDist, frameworksBuildTarget, {
project,
site,
});
if (wantsBackend && !omitCloudFunction)
codegenFunctionsDirectory = codegenProdModeFunctionsDirectory;
}
config.public = (0, path_1.relative)(projectRoot, hostingDist);
config.webFramework = `${framework}${codegenFunctionsDirectory ? "_ssr" : ""}`;
if (codegenFunctionsDirectory) {
if (firebaseDefaults) {
firebaseDefaults._authTokenSyncURL = "/__session";
process.env.__FIREBASE_DEFAULTS__ = JSON.stringify(firebaseDefaults);
}
if (context === null || context === void 0 ? void 0 : context.hostingChannel) {
experiments.assertEnabled("pintags", "deploy an app that requires a backend to a preview channel");
}
const codebase = generateSSRCodebaseId(site);
const existingFunctionsConfig = options.config.get("functions")
? [].concat(options.config.get("functions"))
: [];
options.config.set("functions", [
...existingFunctionsConfig,
{
source: (0, path_1.relative)(projectRoot, functionsDist),
codebase,
},
]);
if (!experiments.isEnabled("pintags") || purpose !== "deploy") {
if (!targetNames.includes("functions")) {
targetNames.unshift("functions");
}
if (options.only) {
options.only = (0, ensureTargeted_1.ensureTargeted)(options.only, codebase);
}
}
if (await (0, fs_extra_1.pathExists)(functionsDist)) {
const functionsDistStat = await (0, fs_extra_1.stat)(functionsDist);
if (functionsDistStat === null || functionsDistStat === void 0 ? void 0 : functionsDistStat.isDirectory()) {
const files = await (0, promises_1.readdir)(functionsDist);
for (const file of files) {
if (file !== "node_modules" && file !== "package-lock.json")
await (0, promises_1.rm)((0, path_1.join)(functionsDist, file), { recursive: true });
}
}
else {
await (0, promises_1.rm)(functionsDist);
}
}
else {
await (0, fs_extra_1.mkdirp)(functionsDist);
}
const { packageJson, bootstrapScript, frameworksEntry = framework, dotEnv = {}, rewriteSource, } = await codegenFunctionsDirectory(getProjectPath(), functionsDist, frameworksBuildTarget, frameworkContext);
const rewrite = {
source: rewriteSource || path_1.posix.join(baseUrl, "**"),
function: {
functionId,
region: ssrRegion,
pinTag: experiments.isEnabled("pintags"),
},
};
if (rewriteSource) {
config.rewrites.unshift(rewrite);
}
else {
rewrites.push(rewrite);
}
process.env.__FIREBASE_FRAMEWORKS_ENTRY__ = frameworksEntry;
packageJson.main = "server.js";
packageJson.dependencies || (packageJson.dependencies = {});
(_g = packageJson.dependencies)["firebase-frameworks"] || (_g["firebase-frameworks"] = constants_2.FIREBASE_FRAMEWORKS_VERSION);
(_h = packageJson.dependencies)["firebase-functions"] || (_h["firebase-functions"] = constants_2.FIREBASE_FUNCTIONS_VERSION);
(_j = packageJson.dependencies)["firebase-admin"] || (_j["firebase-admin"] = constants_2.FIREBASE_ADMIN_VERSION);
packageJson.engines || (packageJson.engines = {});
const validEngines = constants_2.VALID_ENGINES.node.filter((it) => it <= constants_2.NODE_VERSION);
const engine = validEngines[validEngines.length - 1] || constants_2.VALID_ENGINES.node[0];
if (engine !== constants_2.NODE_VERSION) {
(0, utils_2.logWarning)(`This integration expects Node version ${(0, utils_1.conjoinOptions)(constants_2.VALID_ENGINES.node, "or")}. You're running version ${constants_2.NODE_VERSION}, problems may be encountered.`);
}
(_k = packageJson.engines).node || (_k.node = engine.toString());
delete packageJson.scripts;
delete packageJson.devDependencies;
const bundledDependencies = packageJson.bundledDependencies || {};
if (Object.keys(bundledDependencies).length) {
(0, utils_2.logWarning)("Bundled dependencies aren't supported in Cloud Functions, converting to dependencies.");
for (const [dep, version] of Object.entries(bundledDependencies)) {
(_l = packageJson.dependencies)[dep] || (_l[dep] = version);
}
delete packageJson.bundledDependencies;
}
for (const [name, version] of Object.entries(packageJson.dependencies)) {
if (version.startsWith("file:")) {
const path = version.replace(/^file:/, "");
if (!(await (0, fs_extra_1.pathExists)(path)))
continue;
const stats = await (0, fs_extra_1.stat)(path);
if (stats.isDirectory()) {
const result = (0, cross_spawn_1.sync)("npm", ["pack", (0, path_1.relative)(functionsDist, path), "--json=true"], {
cwd: functionsDist,
});
if (result.status !== 0)
throw new error_1.FirebaseError(`Error running \`npm pack\` at ${path}`);
const { filename } = JSON.parse(result.stdout.toString())[0];
packageJson.dependencies[name] = `file:${filename}`;
}
else {
const filename = (0, path_1.basename)(path);
await (0, promises_1.copyFile)(path, (0, path_1.join)(functionsDist, filename));
packageJson.dependencies[name] = `file:${filename}`;
}
}
}
await (0, promises_1.writeFile)((0, path_1.join)(functionsDist, "package.json"), JSON.stringify(packageJson, null, 2));
await (0, promises_1.copyFile)(getProjectPath("package-lock.json"), (0, path_1.join)(functionsDist, "package-lock.json")).catch(() => {
});
if (await (0, fs_extra_1.pathExists)(getProjectPath(".npmrc"))) {
await (0, promises_1.copyFile)(getProjectPath(".npmrc"), (0, path_1.join)(functionsDist, ".npmrc"));
}
let dotEnvContents = "";
if (await (0, fs_extra_1.pathExists)(getProjectPath(".env"))) {
dotEnvContents = (await (0, promises_1.readFile)(getProjectPath(".env"))).toString();
}
for (const [key, value] of Object.entries(dotEnv)) {
dotEnvContents += `\n${key}=${value}`;
}
await (0, promises_1.writeFile)((0, path_1.join)(functionsDist, ".env"), `${dotEnvContents}
__FIREBASE_FRAMEWORKS_ENTRY__=${frameworksEntry}
${firebaseDefaults ? `__FIREBASE_DEFAULTS__=${JSON.stringify(firebaseDefaults)}\n` : ""}`.trimStart());
const envs = await (0, glob_1.glob)(getProjectPath(".env.*"), { windowsPathsNoEscape: utils_2.IS_WINDOWS });
await Promise.all(envs.map((path) => (0, promises_1.copyFile)(path, (0, path_1.join)(functionsDist, (0, path_1.basename)(path)))));
(0, child_process_1.execSync)(`npm i --omit dev --no-audit`, {
cwd: functionsDist,
stdio: "inherit",
});
if (bootstrapScript)
await (0, promises_1.writeFile)((0, path_1.join)(functionsDist, "bootstrap.js"), bootstrapScript);
if (packageJson.type === "module") {
await (0, promises_1.writeFile)((0, path_1.join)(functionsDist, "server.js"), `import { onRequest } from 'firebase-functions/v2/https';
const server = import('firebase-frameworks');
export const ${functionId} = onRequest(${JSON.stringify(frameworksBackend || {})}, (req, res) => server.then(it => it.handle(req, res)));
`);
}
else {
await (0, promises_1.writeFile)((0, path_1.join)(functionsDist, "server.js"), `const { onRequest } = require('firebase-functions/v2/https');
const server = import('firebase-frameworks');
exports.${functionId} = onRequest(${JSON.stringify(frameworksBackend || {})}, (req, res) => server.then(it => it.handle(req, res)));
`);
}
}
else {
if (await (0, fs_extra_1.pathExists)(functionsDist)) {
await (0, promises_1.rm)(functionsDist, { recursive: true });
}
}
const ourConfigShouldComeFirst = !["", "/"].includes(baseUrl);
const operation = ourConfigShouldComeFirst ? "unshift" : "push";
config.rewrites[operation](...rewrites);
config.redirects[operation](...redirects);
config.headers[operation](...headers);
if (firebaseDefaults) {
const encodedDefaults = Buffer.from(JSON.stringify(firebaseDefaults)).toString("base64url");
const expires = new Date(new Date().getTime() + 60000000000);
const sameSite = "Strict";
const path = `/`;
config.headers.push({
source: path_1.posix.join(baseUrl, "**", "*.[jt]s"),
headers: [
{
key: "Set-Cookie",
value: `__FIREBASE_DEFAULTS__=${encodedDefaults}; SameSite=${sameSite}; Expires=${expires.toISOString()}; Path=${path};`,
},
],
});
}
}
logger_1.logger.debug("[web frameworks] effective firebase.json: ", JSON.stringify({ hosting: configs, functions: options.config.get("functions") }, undefined, 2));
BUILD_MEMO.clear();
delete process.env.__FIREBASE_DEFAULTS__;
delete process.env.__FIREBASE_FRAMEWORKS_ENTRY__;
}
exports.prepareFrameworks = prepareFrameworks;
function codegenDevModeFunctionsDirectory() {
const packageJson = {};
return Promise.resolve({ packageJson, frameworksEntry: "_devMode" });
}
;