You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
417 lines
20 KiB
417 lines
20 KiB
"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;
|