fcf-deployer
Version:
A GUI to help you deploy your Firebase Cloud Functions when working locally
405 lines (388 loc) • 14.9 kB
JavaScript
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
);
});
});