UNPKG

fcf-deployer

Version:

A GUI to help you deploy your Firebase Cloud Functions when working locally

405 lines (388 loc) 14.9 kB
#!/usr/bin/env node var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); // server/index.ts var import_express3 = __toESM(require("express")); var import_get_port_please = require("get-port-please"); var import_node_child_process2 = require("child_process"); var import_node_path4 = __toESM(require("path")); var import_cors = __toESM(require("cors")); // server/stores/config-store.ts var import_node_path2 = __toESM(require("path")); // server/utils/config-utils.ts var import_node_path = __toESM(require("path")); var import_node_fs = __toESM(require("fs")); var import_minimist = __toESM(require("minimist")); // server/constants/configs.default.ts var DEFAULT_CONFIGS = { port: { defaultValue: 10444 }, functionsEntrypoint: { defaultValue: "./functions.js" }, firebasercFile: { defaultValue: "../.firebaserc" }, prerunScript: { defaultValue: "" } }; var configs_default_default = DEFAULT_CONFIGS; // server/utils/config-utils.ts var getCommandLineArgs = () => { const argv = (0, import_minimist.default)(process.argv.slice(2)); return argv; }; var CONFIG_FILE_NAME = "./fcf-deployer.config.json"; var parseConfigFile = () => { const configFilePath = import_node_path.default.resolve(process.cwd(), CONFIG_FILE_NAME); if (import_node_fs.default.existsSync(configFilePath)) { try { const config = JSON.parse(import_node_fs.default.readFileSync(configFilePath, "utf-8")); return config; } catch (error) { console.warn( "There is a problem with your cloud functions deployer GUI config", error ); return {}; } } return {}; }; var getConfigFromBothCommandLineAndConfigFile = () => { const cliConfig = getCommandLineArgs(); const fileConfig = parseConfigFile(); const finalConfig = {}; for (let property in configs_default_default) { finalConfig[property] = cliConfig[property] || fileConfig[property] || configs_default_default[property].defaultValue; } return finalConfig; }; var getFirebaseFunctionsBuildCommand = () => { const packageJSONFile = import_node_path.default.resolve(process.cwd(), "./package.json"); if (!import_node_fs.default.existsSync(packageJSONFile)) return null; try { const packageJSON = JSON.parse(import_node_fs.default.readFileSync(packageJSONFile, "utf-8")); if (packageJSON && packageJSON.scripts && packageJSON.scripts.build) return packageJSON.scripts.build; return null; } catch (error) { console.warn("There is a problem with your package.json file", error); return null; } }; var getListOfFirebaseProjects = (firebaseRCFilePath) => { if (!import_node_fs.default.existsSync(firebaseRCFilePath)) return []; try { const firebaseRCJSON = JSON.parse( import_node_fs.default.readFileSync(firebaseRCFilePath, "utf-8") ); if (firebaseRCJSON && firebaseRCJSON.projects) return Object.keys(firebaseRCJSON.projects); return []; } catch (error) { console.warn("There is a problem with your firebase.json file", error); return []; } }; // server/stores/config-store.ts var ConfigStore = class { configs; // Firebase Cloud Functions supports Typescript, since we can't read/require typescript files directly from our source code which would be compiled. // We first run a build ourselves and read from dist/firebase.js bundle instead as told to us via the entryPoint argument. firebaseFunctionsBuildCommand = ""; // Some firebase cloud function directories have multiple projects linked for use cases like staging and production. // We need to provide all options to the end user. firebaseProjectsList = []; constructor() { this.configs = getConfigFromBothCommandLineAndConfigFile(); this.firebaseFunctionsBuildCommand = getFirebaseFunctionsBuildCommand(); this.firebaseProjectsList = getListOfFirebaseProjects( import_node_path2.default.resolve(process.cwd(), this.configs.firebasercFile) ); } }; var config_store_default = new ConfigStore(); // server/routers/git.ts var import_express = require("express"); // server/utils/git.ts var import_child_process = require("child_process"); var isGitRepo = () => new Promise( (resolve) => (0, import_child_process.exec)("git rev-parse --is-inside-work-tree", (error, stdout) => { if (error) return resolve(false); return resolve(stdout.includes("true")); }) ); var getGitBranches = () => new Promise( (resolve) => (0, import_child_process.exec)("git branch --list", (error, stdout) => { const branches = stdout.replace(/\* /g, "").split("\n").filter(Boolean).map((branchName) => branchName.trim()); if (error) return resolve([]); return resolve(branches); }) ); var getActiveBranch = () => new Promise( (resolve) => (0, import_child_process.exec)("git branch --show-current", (error, stdout) => { const branch = stdout.trim(); if (error) return resolve(null); return resolve(branch); }) ); var switchGitBranch = (branchName) => new Promise( (resolve) => (0, import_child_process.exec)("git checkout " + branchName, (error) => { if (error) return resolve({ error }); return resolve({ error: null }); }) ); // server/routers/git.ts var gitRouter = (0, import_express.Router)(); gitRouter.get("/list-branches", async (_, res) => { try { res.json({ branches: await getGitBranches() }); } catch (error) { res.status(500).json({ error: "Something went wrong.", detailed: error }); } }); gitRouter.get("/active-branch", async (_, res) => { try { res.json({ branch: await getActiveBranch() }); } catch (error) { res.status(500).json({ error: "Something went wrong.", detailed: error }); } }); gitRouter.post("/switch-branch", async (req, res) => { try { if (!req.body.branchName) return res.status(400).json({ error: "Branch name not specified" }); const { error } = await switchGitBranch(req.body.branchName); res.json({ successful: !error, error }); } catch (error) { res.status(500).json({ error: "Something went wrong.", detailed: error }); } }); gitRouter.get("/is-git-repo", async (_, res) => { try { res.json({ isGitRepo: await isGitRepo() }); } catch (error) { res.status(500).json({ error: "Something went wrong.", detailed: error }); } }); var git_default = gitRouter; // server/routers/functions.ts var import_express2 = require("express"); var import_uuid = require("uuid"); // server/utils/functions.ts var import_node_path3 = __toESM(require("path")); var listFunctions = () => { var _a; process.env.FIREBASE_CONFIG = JSON.stringify({ projectId: "dummy-project-id", databaseURL: "dummy-db-url", storageBucket: "dummy-storage-bucket" }); process.env.GCLOUD_PROJECT = "dummy-project-id"; const functionExportsEntryPointPath = import_node_path3.default.resolve( process.cwd(), config_store_default.configs.functionsEntrypoint ); const allFunctions = require(functionExportsEntryPointPath); const functions = []; for (let functionName in allFunctions) { const functionTriggerInfo = allFunctions[functionName].__trigger; functions.push({ name: functionName, triggerType: functionTriggerInfo.httpsTrigger ? "http" : functionTriggerInfo.schedule ? "schedule" : functionTriggerInfo.eventTrigger ? "event" : "other", minInstances: functionTriggerInfo.minInstances || "-", maxInstances: functionTriggerInfo.maxInstances || "-", timeout: functionTriggerInfo.timeout || "-", availableMemoryMb: functionTriggerInfo.availableMemoryMb || "-", regions: functionTriggerInfo.regions || "-", eventTrigger: functionTriggerInfo.eventTrigger || null, schedule: functionTriggerInfo.schedule ? functionTriggerInfo.schedule.schedule : null, httpsTrigger: functionTriggerInfo.httpsTrigger || null, isCallableFunction: !!((_a = functionTriggerInfo.labels) == null ? void 0 : _a["deployment-callable"]) }); } return functions; }; // server/stores/individual-deployment-state.ts var import_node_child_process = require("child_process"); var IndividualDeploymentState = class { id = ""; functionsList = []; logs = []; status; environment; changeListeners = /* @__PURE__ */ new Set(); constructor(id, functionsList, environment = null) { this.status = "ongoing"; this.id = id; this.functionsList = functionsList; this.environment = environment; this.startDeployment(); } onChange = (listener) => { this.changeListeners.add(listener); return () => this.changeListeners.delete(listener); }; notifyListeners = () => { this.changeListeners.forEach((listener) => listener(this)); }; setStatus = (status) => { this.status = status; this.notifyListeners(); }; addToLogs = (data) => { const logString = data.toString().trim(); if (logString.length) { console.log(logString); this.logs.push( `\x1B[36m${(/* @__PURE__ */ new Date()).toLocaleDateString()} ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}\x1B[0m: ${logString}` ); this.notifyListeners(); } }; startDeployment = () => { if (this.environment) (0, import_node_child_process.execSync)(`firebase use ${this.environment}`, { stdio: "inherit" }); const deploymentCommand = `firebase deploy --only ${this.functionsList.map((func) => `functions:${func}`).join(",")}`; const deploymentSpawnedProcess = (0, import_node_child_process.spawn)(deploymentCommand, { shell: true }); deploymentSpawnedProcess.stdout.on("data", this.addToLogs); deploymentSpawnedProcess.stderr.on("data", this.addToLogs); deploymentSpawnedProcess.on("close", (code, signal) => { console.log("Deployment job ", this.id, "finished with code: ", code); if (code || signal) this.setStatus("errorred"); else this.setStatus("completed"); }); }; }; var individual_deployment_state_default = IndividualDeploymentState; // server/routers/functions.ts var functionsRouter = (0, import_express2.Router)(); var deploymentJobs = /* @__PURE__ */ new Map(); functionsRouter.get("/list", async (_, res) => { try { res.json({ functions: listFunctions() }); } catch (error) { res.status(500).json({ error: "Something went wrong.", detailed: error }); } }); functionsRouter.post("/start-deployment", async (req, res) => { try { const { functionsList, environment = null } = req.body; if (!functionsList || !Array.isArray(functionsList) || !functionsList.length) return res.status(400).json({ error: "No functions received for deployment" }); const newDeploymentJobId = (0, import_uuid.v4)(); deploymentJobs.set( newDeploymentJobId, new individual_deployment_state_default( newDeploymentJobId, functionsList, environment ) ); res.status(201).json({ jobId: newDeploymentJobId }); } catch (error) { res.status(500).json({ error: "Something went wrong.", detailed: error }); } }); functionsRouter.get("/listen-to-deployment-state/:jobId", async (req, res) => { try { const { jobId } = req.params; const headers = { "Content-Type": "text/event-stream", Connection: "keep-alive", "Cache-Control": "no-cache" }; res.writeHead(200, headers); const individualDeploymentState = deploymentJobs.get(jobId); if (!individualDeploymentState) { const message = `data: ${JSON.stringify({ error: "Job not found" })} `; return res.write(message); } let logsSentTillNow = 0; const sendStoreStateToClient = (store) => { const message = `data: ${JSON.stringify({ status: store.status, functionsList: !logsSentTillNow ? store.functionsList : [], logs: store.logs.slice(logsSentTillNow) })} `; logsSentTillNow = Math.max(store.logs.length - 1, 0); res.write(message); }; sendStoreStateToClient(individualDeploymentState); const unsubscribeFromDeploymentLogs = individualDeploymentState.onChange( (storeState) => { sendStoreStateToClient(storeState); if (storeState.status === "completed" || storeState.status === "errorred") unsubscribeFromDeploymentLogs(); } ); req.on("close", () => { if (unsubscribeFromDeploymentLogs) unsubscribeFromDeploymentLogs(); }); } catch (error) { if (!res.headersSent) return res.end(); } }); functionsRouter.get("/environments", async (_, res) => { res.json({ environments: config_store_default.firebaseProjectsList }); }); var functions_default = functionsRouter; // server/index.ts if (config_store_default.configs.prerunScript) { console.log("Running pre-run script"); (0, import_node_child_process2.execSync)(config_store_default.configs.prerunScript, { stdio: "inherit" }); } if (config_store_default.firebaseFunctionsBuildCommand) { console.log( "Running firebase functions build command (You probably have typescript in your project which needs to be compiled first)" ); (0, import_node_child_process2.execSync)(config_store_default.firebaseFunctionsBuildCommand, { stdio: "inherit" }); } var enableCors = false; var server = (0, import_express3.default)(); if (enableCors) server.use((0, import_cors.default)()); server.use(import_express3.default.json()); server.use("/", import_express3.default.static(import_node_path4.default.resolve(__dirname, "./client"))); server.use("/git", git_default); server.use("/functions", functions_default); (0, import_get_port_please.getPort)({ port: config_store_default.configs.port }).then((availablePort) => { server.listen(availablePort, () => { const url = `http://localhost:${availablePort}`; console.log( "Firebase Cloud Functions deployer GUI listening on port: ", url ); }); });