"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createServerResponseProxy = exports.prepareFrameworks = exports.findDependency = exports.discover = exports.relativeRequire = exports.WebFrameworks = exports.NODE_VERSION = exports.DEFAULT_REGION = exports.FIREBASE_ADMIN_VERSION = exports.FIREBASE_FUNCTIONS_VERSION = exports.FIREBASE_FRAMEWORKS_VERSION = void 0; const path_1 = require("path"); const process_1 = require("process"); const child_process_1 = require("child_process"); const fs_1 = require("fs"); const url_1 = require("url"); const http_1 = require("http"); const promises_1 = require("fs/promises"); const fs_extra_1 = require("fs-extra"); const clc = require("colorette"); const process = require("node:process"); const semver = require("semver"); 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 ensureTargeted_1 = require("../functions/ensureTargeted"); const implicitInit_1 = require("../hosting/implicitInit"); const { dynamicImport } = require(true && "../dynamicImport"); const SupportLevelWarnings = { ["experimental"]: clc.yellow(`This is an experimental integration, proceed with caution.`), ["community-supported"]: clc.yellow(`This is a community-supported integration, support is best effort.`), }; exports.FIREBASE_FRAMEWORKS_VERSION = "^0.6.0"; exports.FIREBASE_FUNCTIONS_VERSION = "^3.23.0"; exports.FIREBASE_ADMIN_VERSION = "^11.0.1"; exports.DEFAULT_REGION = "us-central1"; exports.NODE_VERSION = parseInt(process.versions.node, 10).toString(); const DEFAULT_FIND_DEP_OPTIONS = { cwd: process.cwd(), omitDev: true, }; const NPM_COMMAND = process.platform === "win32" ? "npm.cmd" : "npm"; exports.WebFrameworks = Object.fromEntries((0, fs_1.readdirSync)(__dirname) .filter((path) => (0, fs_1.statSync)((0, path_1.join)(__dirname, path)).isDirectory()) .map((path) => [path, require((0, path_1.join)(__dirname, path))]) .filter(([, obj]) => obj.name && obj.discover && obj.build && obj.type !== undefined && obj.support)); function relativeRequire(dir, mod) { try { const path = require.resolve(mod, { paths: [dir] }); if ((0, path_1.extname)(path) === ".mjs") { return dynamicImport((0, url_1.pathToFileURL)(path).toString()); } else { return require(path); } } catch (e) { const path = (0, path_1.relative)(process.cwd(), dir); console.error(`Could not load dependency ${mod} in ${path.startsWith("..") ? path : `./${path}`}, have you run \`npm install\`?`); throw e; } } exports.relativeRequire = relativeRequire; async function discover(dir, warn = true) { const allFrameworkTypes = [ ...new Set(Object.values(exports.WebFrameworks).map(({ type }) => type)), ].sort(); for (const discoveryType of allFrameworkTypes) { const frameworksDiscovered = []; for (const framework in exports.WebFrameworks) { if (exports.WebFrameworks[framework]) { const { discover, type } = exports.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; function scanDependencyTree(searchingFor, dependencies = {}) { for (const [name, dependency] of Object.entries(dependencies)) { if (name === searchingFor) return dependency; const result = scanDependencyTree(searchingFor, dependency.dependencies); if (result) return result; } return; } function findDependency(name, options = {}) { const { cwd, depth, omitDev } = Object.assign(Object.assign({}, DEFAULT_FIND_DEP_OPTIONS), options); const env = Object.assign({}, process.env); delete env.NODE_ENV; const result = (0, child_process_1.spawnSync)(NPM_COMMAND, [ "list", name, "--json", ...(omitDev ? ["--omit", "dev"] : []), ...(depth === undefined ? [] : ["--depth", depth.toString(10)]), ], { cwd, env }); if (!result.stdout) return; const json = JSON.parse(result.stdout.toString()); return scanDependencyTree(name, json.dependencies); } exports.findDependency = findDependency; async function prepareFrameworks(targetNames, context, options, emulators = []) { var _a; var _b, _c, _d, _e; const nodeVersion = process.version; if (!semver.satisfies(nodeVersion, ">=16.0.0")) { throw new error_1.FirebaseError(`The frameworks awareness feature requires Node.JS >= 16 and npm >= 8 in order to work correctly, due to some of the downstream dependencies. Please upgrade your version of Node.JS, reinstall firebase-tools, and give it another go.`); } const project = (0, projectUtils_1.needProjectId)(context); const { projectRoot } = options; const account = (0, auth_1.getProjectDefaultAccount)(projectRoot); if (!options.site) { try { await (0, requireHostingSite_1.requireHostingSite)(options); } catch (_f) { options.site = project; } } const configs = (0, config_1.hostingConfig)(options); let firebaseDefaults = undefined; if (configs.length === 0) { return; } for (const config of configs) { const { source, site, public: publicDir } = 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 getProjectPath = (...args) => (0, path_1.join)(projectRoot, source, ...args); const functionName = `ssr${site.toLowerCase().replace(/-/g, "")}`; const usesFirebaseAdminSdk = !!findDependency("firebase-admin", { cwd: getProjectPath() }); const usesFirebaseJsSdk = !!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 = 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 = 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.promptOnce)({ type: "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("Epic fail."); const { framework, mayWantBackend, publicDirectory } = results; const { build, ɵcodegenPublicDirectory, ɵcodegenFunctionsDirectory: codegenProdModeFunctionsDirectory, getDevModeHandle, name, support, } = exports.WebFrameworks[framework]; console.log(`Detected a ${name} codebase. ${SupportLevelWarnings[support] || ""}\n`); const isDevMode = context._name === "serve" || context._name === "emulators:start"; const hostingEmulatorInfo = emulators.find((e) => e.name === types_1.Emulators.HOSTING); const devModeHandle = isDevMode && getDevModeHandle && (await getDevModeHandle(getProjectPath(), hostingEmulatorInfo)); let codegenFunctionsDirectory; if (devModeHandle) { config.public = (0, path_1.relative)(projectRoot, publicDirectory); options.frameworksDevModeHandle = devModeHandle; if (mayWantBackend && firebaseDefaults) { codegenFunctionsDirectory = codegenDevModeFunctionsDirectory; } } else { const { wantsBackend = false, rewrites = [], redirects = [], headers = [], } = (await build(getProjectPath())) || {}; config.rewrites.push(...rewrites); config.redirects.push(...redirects); config.headers.push(...headers); 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); config.public = (0, path_1.relative)(projectRoot, hostingDist); if (wantsBackend) codegenFunctionsDirectory = codegenProdModeFunctionsDirectory; } config.webFramework = `${framework}${codegenFunctionsDirectory ? "_ssr" : ""}`; if (codegenFunctionsDirectory) { if (firebaseDefaults) firebaseDefaults._authTokenSyncURL = "/__session"; const rewrite = { source: "**", function: { functionId: functionName, }, }; if (experiments.isEnabled("pintags")) { rewrite.function.pinTag = true; } config.rewrites.push(rewrite); const codebase = `firebase-frameworks-${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 (!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, } = await codegenFunctionsDirectory(getProjectPath(), functionsDist); await (0, promises_1.writeFile)((0, path_1.join)(functionsDist, "functions.yaml"), JSON.stringify({ endpoints: { [functionName]: { platform: "gcfv2", region: [exports.DEFAULT_REGION], labels: {}, httpsTrigger: {}, entryPoint: "ssr", }, }, specVersion: "v1alpha1", requiredAPIs: [], }, null, 2)); packageJson.main = "server.js"; delete packageJson.devDependencies; packageJson.dependencies || (packageJson.dependencies = {}); (_b = packageJson.dependencies)["firebase-frameworks"] || (_b["firebase-frameworks"] = exports.FIREBASE_FRAMEWORKS_VERSION); (_c = packageJson.dependencies)["firebase-functions"] || (_c["firebase-functions"] = exports.FIREBASE_FUNCTIONS_VERSION); (_d = packageJson.dependencies)["firebase-admin"] || (_d["firebase-admin"] = exports.FIREBASE_ADMIN_VERSION); packageJson.engines || (packageJson.engines = {}); (_e = packageJson.engines).node || (_e.node = exports.NODE_VERSION); await (0, promises_1.writeFile)((0, path_1.join)(functionsDist, "package.json"), JSON.stringify(packageJson, null, 2)); await (0, promises_1.writeFile)((0, path_1.join)(functionsDist, ".env"), `__FIREBASE_FRAMEWORKS_ENTRY__=${frameworksEntry} ${firebaseDefaults ? `__FIREBASE_DEFAULTS__=${JSON.stringify(firebaseDefaults)}\n` : ""}`); 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")); } (0, child_process_1.execSync)(`${NPM_COMMAND} i --omit dev --no-audit`, { cwd: functionsDist, stdio: "inherit", }); if (bootstrapScript) await (0, promises_1.writeFile)((0, path_1.join)(functionsDist, "bootstrap.js"), bootstrapScript); 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.ssr = onRequest((req, res) => server.then(it => it.handle(req, res))); `); } else { config.rewrites.push({ source: "**", destination: "/index.html", }); } 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: "**/*.js", headers: [ { key: "Set-Cookie", value: `__FIREBASE_DEFAULTS__=${encodedDefaults}; SameSite=${sameSite}; Expires=${expires.toISOString()}; Path=${path};`, }, ], }); } } } exports.prepareFrameworks = prepareFrameworks; function codegenDevModeFunctionsDirectory() { const packageJson = {}; return Promise.resolve({ packageJson, frameworksEntry: "_devMode" }); } function createServerResponseProxy(req, res, next) { const proxiedRes = new http_1.ServerResponse(req); const buffer = []; proxiedRes.write = new Proxy(proxiedRes.write.bind(proxiedRes), { apply: (target, thisArg, args) => { target.call(thisArg, ...args); buffer.push(["write", args]); }, }); proxiedRes.setHeader = new Proxy(proxiedRes.setHeader.bind(proxiedRes), { apply: (target, thisArg, args) => { target.call(thisArg, ...args); buffer.push(["setHeader", args]); }, }); proxiedRes.removeHeader = new Proxy(proxiedRes.removeHeader.bind(proxiedRes), { apply: (target, thisArg, args) => { target.call(thisArg, ...args); buffer.push(["removeHeader", args]); }, }); proxiedRes.writeHead = new Proxy(proxiedRes.writeHead.bind(proxiedRes), { apply: (target, thisArg, args) => { target.call(thisArg, ...args); buffer.push(["writeHead", args]); }, }); proxiedRes.end = new Proxy(proxiedRes.end.bind(proxiedRes), { apply: (target, thisArg, args) => { target.call(thisArg, ...args); if (proxiedRes.statusCode === 404) { next(); } else { for (const [fn, args] of buffer) { res[fn](...args); } res.end(...args); } }, }); return proxiedRes; } exports.createServerResponseProxy = createServerResponseProxy;