firebase-tools
Version:
Command-Line Interface for Firebase
911 lines (910 loc) • 41.5 kB
JavaScript
var __asyncValues = (this && this.__asyncValues) || function (o) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var m = o[Symbol.asyncIterator], i;
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.diagnoseAndFixProject = exports.getSourceOrigin = exports.isLocalOrURLPath = exports.isLocalPath = exports.isUrlPath = exports.instanceIdExists = exports.promptForRepeatInstance = exports.promptForOfficialExtension = exports.displayReleaseNotes = exports.getPublisherProjectFromName = exports.createSourceFromLocation = exports.getMissingPublisherError = exports.uploadExtensionVersionFromLocalSource = exports.uploadExtensionVersionFromGitHubSource = exports.unpackExtensionState = exports.getNextVersionByStage = exports.ensureExtensionsPublisherApiEnabled = exports.ensureExtensionsApiEnabled = exports.checkExtensionsApiEnabled = exports.promptForExtensionRoot = exports.promptForValidRepoURI = exports.promptForValidInstanceId = exports.validateSpec = exports.validateCommandLineParams = exports.populateDefaultParams = exports.substituteSecretParams = exports.substituteParams = exports.getFirebaseProjectParams = exports.getDBInstanceFromURL = exports.resourceTypeToNiceName = exports.AUTOPOPULATED_PARAM_PLACEHOLDERS = exports.EXTENSIONS_BUCKET_NAME = exports.URL_REGEX = exports.logPrefix = exports.SourceOrigin = exports.SpecParamType = void 0;
const clc = require("colorette");
const ora = require("ora");
const semver = require("semver");
const tmp = require("tmp");
const fs = require("fs-extra");
const node_fetch_1 = require("node-fetch");
const path = require("path");
const marked_1 = require("marked");
const marked_terminal_1 = require("marked-terminal");
const unzip_1 = require("./../unzip");
marked_1.marked.use((0, marked_terminal_1.markedTerminal)());
const api_1 = require("../api");
const archiveDirectory_1 = require("../archiveDirectory");
const utils_1 = require("./utils");
const functionsConfig_1 = require("../functionsConfig");
const adminSdkConfig_1 = require("../emulator/adminSdkConfig");
const resolveSource_1 = require("./resolveSource");
const error_1 = require("../error");
const diagnose_1 = require("./diagnose");
const askUserForParam_1 = require("./askUserForParam");
const ensureApiEnabled_1 = require("../ensureApiEnabled");
const storage_1 = require("../gcp/storage");
const projectUtils_1 = require("../projectUtils");
const extensionsApi_1 = require("./extensionsApi");
const publisherApi_1 = require("./publisherApi");
const prompt_1 = require("../prompt");
const refs = require("./refs");
const localHelper_1 = require("./localHelper");
const logger_1 = require("../logger");
const utils_2 = require("../utils");
const change_log_1 = require("./change-log");
const getProjectNumber_1 = require("../getProjectNumber");
const constants_1 = require("../emulator/constants");
var SpecParamType;
(function (SpecParamType) {
SpecParamType["SELECT"] = "select";
SpecParamType["MULTISELECT"] = "multiSelect";
SpecParamType["STRING"] = "string";
SpecParamType["SELECTRESOURCE"] = "selectResource";
SpecParamType["SECRET"] = "secret";
})(SpecParamType = exports.SpecParamType || (exports.SpecParamType = {}));
var SourceOrigin;
(function (SourceOrigin) {
SourceOrigin["OFFICIAL_EXTENSION"] = "official extension";
SourceOrigin["LOCAL"] = "unpublished extension (local source)";
SourceOrigin["PUBLISHED_EXTENSION"] = "published extension";
SourceOrigin["PUBLISHED_EXTENSION_VERSION"] = "specific version of a published extension";
SourceOrigin["URL"] = "unpublished extension (URL source)";
SourceOrigin["OFFICIAL_EXTENSION_VERSION"] = "specific version of an official extension";
})(SourceOrigin = exports.SourceOrigin || (exports.SourceOrigin = {}));
exports.logPrefix = "extensions";
const VALID_LICENSES = ["apache-2.0"];
exports.URL_REGEX = /^https:/;
exports.EXTENSIONS_BUCKET_NAME = (0, utils_2.envOverride)("FIREBASE_EXTENSIONS_UPLOAD_BUCKET", "firebase-ext-eap-uploads");
const AUTOPOPULATED_PARAM_NAMES = [
"PROJECT_ID",
"STORAGE_BUCKET",
"EXT_INSTANCE_ID",
"DATABASE_INSTANCE",
"DATABASE_URL",
];
exports.AUTOPOPULATED_PARAM_PLACEHOLDERS = {
PROJECT_ID: "project-id",
STORAGE_BUCKET: "project-id.appspot.com",
EXT_INSTANCE_ID: "extension-id",
DATABASE_INSTANCE: "project-id-default-rtdb",
DATABASE_URL: "https://project-id-default-rtdb.firebaseio.com",
};
exports.resourceTypeToNiceName = {
"firebaseextensions.v1beta.function": "Cloud Function",
};
const repoRegex = new RegExp(`^https:\/\/github\.com\/[^\/]+\/[^\/]+$`);
const stageOptions = ["rc", "alpha", "beta", "stable"];
function getDBInstanceFromURL(databaseUrl = "") {
const instanceRegex = new RegExp("(?:https://)(.*)(?:.firebaseio.com)");
const matches = instanceRegex.exec(databaseUrl);
if (matches && matches.length > 1) {
return matches[1];
}
return "";
}
exports.getDBInstanceFromURL = getDBInstanceFromURL;
async function getFirebaseProjectParams(projectId, emulatorMode = false) {
var _a, _b;
if (!projectId) {
return {};
}
const body = emulatorMode
? await (0, adminSdkConfig_1.getProjectAdminSdkConfigOrCached)(projectId)
: await (0, functionsConfig_1.getFirebaseConfig)({ project: projectId });
let projectNumber = constants_1.Constants.FAKE_PROJECT_NUMBER;
if (!constants_1.Constants.isDemoProject(projectId)) {
try {
projectNumber = await (0, getProjectNumber_1.getProjectNumber)({ projectId });
}
catch (err) {
(0, utils_2.logLabeledError)("extensions", `Unable to look up project number for ${projectId}.\n` +
" If this is a real project, ensure that you are logged in and have access to it.\n" +
" If this is a fake project, please use a project ID starting with 'demo-' to skip production calls.\n" +
" Continuing with a fake project number - secrets and other features that require production access may behave unexpectedly.");
}
}
const databaseURL = (_a = body === null || body === void 0 ? void 0 : body.databaseURL) !== null && _a !== void 0 ? _a : `https://${projectId}.firebaseio.com`;
const storageBucket = (_b = body === null || body === void 0 ? void 0 : body.storageBucket) !== null && _b !== void 0 ? _b : `${projectId}.appspot.com`;
const FIREBASE_CONFIG = JSON.stringify({
projectId,
databaseURL,
storageBucket,
});
return {
PROJECT_ID: projectId,
PROJECT_NUMBER: projectNumber,
DATABASE_URL: databaseURL,
STORAGE_BUCKET: storageBucket,
FIREBASE_CONFIG,
DATABASE_INSTANCE: getDBInstanceFromURL(databaseURL),
};
}
exports.getFirebaseProjectParams = getFirebaseProjectParams;
function substituteParams(original, params) {
const startingString = JSON.stringify(original);
const applySubstitution = (str, paramVal, paramKey) => {
const exp1 = new RegExp("\\$\\{" + paramKey + "\\}", "g");
const exp2 = new RegExp("\\$\\{param:" + paramKey + "\\}", "g");
const regexes = [exp1, exp2];
const substituteRegexMatches = (unsubstituted, regex) => {
return unsubstituted.replace(regex, paramVal);
};
return regexes.reduce(substituteRegexMatches, str);
};
const s = Object.entries(params).reduce((str, [key, val]) => applySubstitution(str, val, key), startingString);
return JSON.parse(s);
}
exports.substituteParams = substituteParams;
async function substituteSecretParams(projectNumber, params) {
var _a, e_1, _b, _c;
const newParams = {};
try {
for (var _d = true, _e = __asyncValues(Object.entries(params)), _f; _f = await _e.next(), _a = _f.done, !_a;) {
_c = _f.value;
_d = false;
try {
const [key, value] = _c;
if (typeof value !== "string") {
newParams[key] =
`projects/${projectNumber}/secrets/${value.name}/versions/latest`;
}
else {
newParams[key] = value;
}
}
finally {
_d = true;
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (!_d && !_a && (_b = _e.return)) await _b.call(_e);
}
finally { if (e_1) throw e_1.error; }
}
return newParams;
}
exports.substituteSecretParams = substituteSecretParams;
function populateDefaultParams(paramVars, paramSpecs) {
const newParams = paramVars;
for (const param of paramSpecs) {
if (!paramVars[param.param]) {
if (param.default !== undefined && param.required) {
newParams[param.param] = param.default;
}
else if (param.required) {
throw new error_1.FirebaseError(`${param.param} has not been set in the given params file` +
" and there is no default available. Please set this variable before installing again.");
}
}
}
return newParams;
}
exports.populateDefaultParams = populateDefaultParams;
function validateCommandLineParams(envVars, paramSpec) {
const paramNames = paramSpec.map((p) => p.param);
const misnamedParams = Object.keys(envVars).filter((key) => {
return !paramNames.includes(key) && !AUTOPOPULATED_PARAM_NAMES.includes(key);
});
if (misnamedParams.length) {
logger_1.logger.warn("Warning: The following params were specified in your env file but do not exist in the extension spec: " +
`${misnamedParams.join(", ")}.`);
}
let allParamsValid = true;
for (const param of paramSpec) {
if (!(0, askUserForParam_1.checkResponse)(envVars[param.param], param)) {
allParamsValid = false;
}
}
if (!allParamsValid) {
throw new error_1.FirebaseError(`Some param values are not valid. Please check your params file.`);
}
}
exports.validateCommandLineParams = validateCommandLineParams;
function validateSpec(spec) {
const errors = [];
if (!spec.name) {
errors.push("extension.yaml is missing required field: name");
}
if (!spec.specVersion) {
errors.push("extension.yaml is missing required field: specVersion");
}
if (!spec.version) {
errors.push("extension.yaml is missing required field: version");
}
else if (!semver.valid(spec.version)) {
errors.push(`version ${spec.version} in extension.yaml is not a valid semver`);
}
else {
const version = semver.parse(spec.version);
if (version.prerelease.length > 0 || version.build.length > 0) {
errors.push("version field in extension.yaml does not support pre-release annotations; instead, set a pre-release stage using the --stage flag");
}
}
if (!spec.license) {
errors.push("extension.yaml is missing required field: license");
}
else {
const formattedLicense = String(spec.license).toLocaleLowerCase();
if (!VALID_LICENSES.includes(formattedLicense)) {
errors.push(`license field in extension.yaml is invalid. Valid value(s): ${VALID_LICENSES.join(", ")}`);
}
}
if (!spec.resources) {
errors.push("Resources field must contain at least one resource");
}
else {
for (const resource of spec.resources) {
if (!resource.name) {
errors.push("Resource is missing required field: name");
}
if (!resource.type) {
errors.push(`Resource${resource.name ? ` ${resource.name}` : ""} is missing required field: type`);
}
}
}
for (const api of spec.apis || []) {
if (!api.apiName) {
errors.push("API is missing required field: apiName");
}
}
for (const role of spec.roles || []) {
if (!role.role) {
errors.push("Role is missing required field: role");
}
}
for (const param of spec.params || []) {
if (!param.param) {
errors.push("Param is missing required field: param");
}
if (!param.label) {
errors.push(`Param${param.param ? ` ${param.param}` : ""} is missing required field: label`);
}
if (param.type && !Object.values(SpecParamType).includes(param.type)) {
errors.push(`Invalid type ${param.type} for param${param.param ? ` ${param.param}` : ""}. Valid types are ${Object.values(SpecParamType).join(", ")}`);
}
if (!param.type || param.type === SpecParamType.STRING) {
if (param.options) {
errors.push(`Param${param.param ? ` ${param.param}` : ""} cannot have options because it is type STRING`);
}
}
if (param.type &&
(param.type === SpecParamType.SELECT || param.type === SpecParamType.MULTISELECT)) {
if (param.validationRegex) {
errors.push(`Param${param.param ? ` ${param.param}` : ""} cannot have validationRegex because it is type ${param.type}`);
}
if (!param.options) {
errors.push(`Param${param.param ? ` ${param.param}` : ""} requires options because it is type ${param.type}`);
}
for (const opt of param.options || []) {
if (opt.value === undefined) {
errors.push(`Option for param${param.param ? ` ${param.param}` : ""} is missing required field: value`);
}
}
}
if (param.type && param.type === SpecParamType.SELECTRESOURCE) {
if (!param.resourceType) {
errors.push(`Param${param.param ? ` ${param.param}` : ""} must have resourceType because it is type ${param.type}`);
}
}
}
if (errors.length) {
const formatted = errors.map((error) => ` - ${error}`);
const message = `The extension.yaml has the following errors: \n${formatted.join("\n")}`;
throw new error_1.FirebaseError(message);
}
}
exports.validateSpec = validateSpec;
async function promptForValidInstanceId(instanceId) {
let instanceIdIsValid = false;
let newInstanceId = "";
const instanceIdRegex = /^[a-z][a-z\d\-]*[a-z\d]$/;
while (!instanceIdIsValid) {
newInstanceId = await (0, prompt_1.input)({
default: instanceId,
message: `Please enter a new name for this instance:`,
});
if (newInstanceId.length <= 6 || 45 <= newInstanceId.length) {
logger_1.logger.info("Invalid instance ID. Instance ID must be between 6 and 45 characters.");
}
else if (!instanceIdRegex.test(newInstanceId)) {
logger_1.logger.info("Invalid instance ID. Instance ID must start with a lowercase letter, " +
"end with a lowercase letter or number, and only contain lowercase letters, numbers, or -");
}
else {
instanceIdIsValid = true;
}
}
return newInstanceId;
}
exports.promptForValidInstanceId = promptForValidInstanceId;
async function promptForValidRepoURI() {
let repoIsValid = false;
let extensionRoot = "";
while (!repoIsValid) {
extensionRoot = await (0, prompt_1.input)("Enter the GitHub repo URI where this extension's source code is located:");
if (!repoRegex.test(extensionRoot)) {
logger_1.logger.info("Repo URI must follow this format: https://github.com/<user>/<repo>");
}
else {
repoIsValid = true;
}
}
return extensionRoot;
}
exports.promptForValidRepoURI = promptForValidRepoURI;
async function promptForExtensionRoot(defaultRoot) {
return await (0, prompt_1.input)({
message: "Enter this extension's root directory in the repo (defaults to previous root if set):",
default: defaultRoot,
});
}
exports.promptForExtensionRoot = promptForExtensionRoot;
async function promptForReleaseStage(args) {
let stage = "rc";
if (!args.nonInteractive) {
const choices = [
{ name: `Release candidate (${args.versionByStage.get("rc")})`, value: "rc" },
{ name: `Alpha (${args.versionByStage.get("alpha")})`, value: "alpha" },
{ name: `Beta (${args.versionByStage.get("beta")})`, value: "beta" },
];
if (args.allowStable) {
const stableChoice = {
name: `Stable (${args.versionByStage.get("stable")}${args.autoReview ? ", automatically sent for review" : ""})`,
value: "stable",
};
choices.push(stableChoice);
}
stage = await (0, prompt_1.select)({
message: "Choose the release stage:",
choices: choices,
default: stage,
});
if (stage === "stable" && !args.hasVersions) {
const confirmed = await (0, prompt_1.confirm)({
message: `${clc.bold(clc.yellow("Warning:"))} It's highly recommended to first upload a pre-release version before choosing stable.`,
nonInteractive: args.nonInteractive,
force: args.force,
default: false,
});
if (!confirmed) {
stage = await (0, prompt_1.select)({
message: "Choose the release stage:",
choices: choices,
default: stage,
});
}
}
}
return stage;
}
async function checkExtensionsApiEnabled(options) {
const projectId = (0, projectUtils_1.getProjectId)(options);
if (!projectId) {
return false;
}
return await (0, ensureApiEnabled_1.check)(projectId, (0, api_1.extensionsOrigin)(), "extensions", options.markdown);
}
exports.checkExtensionsApiEnabled = checkExtensionsApiEnabled;
async function ensureExtensionsApiEnabled(options) {
const projectId = (0, projectUtils_1.getProjectId)(options);
if (!projectId) {
return;
}
return await (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.extensionsOrigin)(), "extensions", options.markdown);
}
exports.ensureExtensionsApiEnabled = ensureExtensionsApiEnabled;
async function ensureExtensionsPublisherApiEnabled(options) {
const projectId = (0, projectUtils_1.getProjectId)(options);
if (!projectId) {
return;
}
return await (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.extensionsPublisherOrigin)(), "extensions", options.markdown);
}
exports.ensureExtensionsPublisherApiEnabled = ensureExtensionsPublisherApiEnabled;
async function archiveAndUploadSource(extPath, bucketName) {
const zippedSource = await (0, archiveDirectory_1.archiveDirectory)(extPath, {
type: "zip",
ignore: ["node_modules", ".git"],
});
const res = await (0, storage_1.uploadObject)(zippedSource, bucketName);
return `/${res.bucket}/${res.object}`;
}
async function getNextVersionByStage(extensionRef, newVersion) {
let extensionVersions = [];
try {
extensionVersions = await (0, publisherApi_1.listExtensionVersions)(extensionRef, `id="${newVersion}"`, true);
}
catch (err) {
}
const versionByStage = new Map(["rc", "alpha", "beta"].map((stage) => [
stage,
semver.inc(`${newVersion}-${stage}`, "prerelease", undefined, stage),
]));
for (const extensionVersion of extensionVersions) {
const version = semver.parse(extensionVersion.spec.version);
if (!version.prerelease.length) {
continue;
}
const prerelease = semver.prerelease(version)[0];
const stage = prerelease.split(".")[0];
if (versionByStage.has(stage) && semver.gte(version, versionByStage.get(stage))) {
versionByStage.set(stage, semver.inc(version, "prerelease", undefined, stage));
}
}
versionByStage.set("stable", newVersion);
return { versionByStage, hasVersions: extensionVersions.length > 0 };
}
exports.getNextVersionByStage = getNextVersionByStage;
async function validateExtensionSpec(rootDirectory, extensionId) {
const extensionSpec = await (0, localHelper_1.getLocalExtensionSpec)(rootDirectory);
if (extensionSpec.name !== extensionId) {
throw new error_1.FirebaseError(`Extension ID '${clc.bold(extensionId)}' does not match the name in extension.yaml '${clc.bold(extensionSpec.name)}'.`);
}
const subbedSpec = JSON.parse(JSON.stringify(extensionSpec));
subbedSpec.params = substituteParams(extensionSpec.params || [], exports.AUTOPOPULATED_PARAM_PLACEHOLDERS);
validateSpec(subbedSpec);
return extensionSpec;
}
function validateReleaseNotes(rootDirectory, newVersion, extension) {
let notes;
try {
const changes = (0, change_log_1.getLocalChangelog)(rootDirectory);
notes = changes[newVersion];
}
catch (err) {
throw new error_1.FirebaseError("No CHANGELOG.md file found. " +
"Please create one and add an entry for this version. " +
"See https://firebase.google.com/docs/extensions/publishers/user-documentation#writing-changelog for more details.");
}
if (!notes && !semver.prerelease(newVersion) && extension) {
throw new error_1.FirebaseError(`No entry for version ${newVersion} found in CHANGELOG.md. ` +
"Please add one so users know what has changed in this version. " +
"See https://firebase.google.com/docs/extensions/publishers/user-documentation#writing-changelog for more details.");
}
return notes;
}
function validateVersion(extensionRef, newVersion, latestVersion) {
if (latestVersion) {
if (semver.lt(newVersion, latestVersion)) {
throw new error_1.FirebaseError(`The version you are trying to publish (${clc.bold(newVersion)}) is lower than the current version (${clc.bold(latestVersion)}) for the extension '${clc.bold(extensionRef)}'. Make sure this version is greater than the current version (${clc.bold(latestVersion)}) inside of extension.yaml and try again.\n`, { exit: 104 });
}
else if (semver.eq(newVersion, latestVersion)) {
throw new error_1.FirebaseError(`The version you are trying to upload (${clc.bold(newVersion)}) already exists for extension '${clc.bold(extensionRef)}'. Increment the version inside of extension.yaml and try again.\n`, { exit: 103 });
}
}
}
function unpackExtensionState(extension) {
switch (extension.state) {
case "PUBLISHED":
if (extension.latestApprovedVersion) {
return clc.bold(clc.green("Published"));
}
else if (extension.latestVersion) {
return clc.green("Uploaded");
}
else {
return "Prerelease";
}
case "DEPRECATED":
return clc.red("Deprecated");
case "SUSPENDED":
return clc.bold(clc.red("Suspended"));
default:
return "-";
}
}
exports.unpackExtensionState = unpackExtensionState;
function displayExtensionHeader(extensionRef, extension, extensionRoot) {
var _a, _b;
if (extension) {
let source = "Local source";
if (extension.repoUri) {
const uri = new URL(extension.repoUri);
uri.pathname = path.join(uri.pathname, extensionRoot !== null && extensionRoot !== void 0 ? extensionRoot : "");
source = `${uri.toString()} (use --repo and --root to modify)`;
}
logger_1.logger.info(`\n${clc.bold("Extension:")} ${extension.ref}\n` +
`${clc.bold("State:")} ${unpackExtensionState(extension)}\n` +
`${clc.bold("Latest Version:")} ${(_a = extension.latestVersion) !== null && _a !== void 0 ? _a : "-"}\n` +
`${clc.bold("Version in Extensions Hub:")} ${(_b = extension.latestApprovedVersion) !== null && _b !== void 0 ? _b : "-"}\n` +
`${clc.bold("Source in GitHub:")} ${source}\n`);
}
else {
logger_1.logger.info(`\n${clc.bold("Extension:")} ${extensionRef}\n` +
`${clc.bold("State:")} ${clc.bold(clc.blueBright("New"))}\n`);
}
}
async function fetchExtensionSource(repoUri, sourceRef, extensionRoot) {
const sourceUri = repoUri + path.join("/tree", sourceRef, extensionRoot);
logger_1.logger.info(`Validating source code at ${clc.bold(sourceUri)}...`);
const archiveUri = `${repoUri}/archive/${sourceRef}.zip`;
const tempDirectory = tmp.dirSync({ unsafeCleanup: true });
const archiveErrorMessage = `Failed to extract archive from ${clc.bold(archiveUri)}. Please check that the repo is public and that the source ref is valid.`;
try {
const response = await (0, node_fetch_1.default)(archiveUri);
if (response.ok) {
await response.body.pipe((0, unzip_1.createUnzipTransform)(tempDirectory.name)).promise();
}
}
catch (err) {
throw new error_1.FirebaseError(archiveErrorMessage);
}
const archiveName = fs.readdirSync(tempDirectory.name)[0];
if (!archiveName) {
throw new error_1.FirebaseError(archiveErrorMessage);
}
const rootDirectory = path.join(tempDirectory.name, archiveName, extensionRoot);
try {
(0, localHelper_1.readFile)(path.resolve(rootDirectory, localHelper_1.EXTENSIONS_SPEC_FILE));
}
catch (err) {
throw new error_1.FirebaseError(`Failed to find ${clc.bold(localHelper_1.EXTENSIONS_SPEC_FILE)} in directory ${clc.bold(extensionRoot)}. Please verify the root and try again.`);
}
return rootDirectory;
}
async function uploadExtensionVersionFromGitHubSource(args) {
var _a, _b, _c;
const extensionRef = `${args.publisherId}/${args.extensionId}`;
let extension;
let latestVersion;
try {
extension = await (0, publisherApi_1.getExtension)(extensionRef);
latestVersion = await (0, publisherApi_1.getExtensionVersion)(`${extensionRef}@latest`);
}
catch (err) {
}
displayExtensionHeader(extensionRef, extension, latestVersion === null || latestVersion === void 0 ? void 0 : latestVersion.extensionRoot);
if (args.stage && !stageOptions.includes(args.stage)) {
throw new error_1.FirebaseError(`--stage only supports the following values: ${stageOptions.join(", ")}`);
}
if (args.repoUri && !repoRegex.test(args.repoUri)) {
throw new error_1.FirebaseError("Repo URI must follow this format: https://github.com/<user>/<repo>");
}
let repoUri = args.repoUri || (extension === null || extension === void 0 ? void 0 : extension.repoUri);
if (!repoUri) {
if (!args.nonInteractive) {
repoUri = await promptForValidRepoURI();
}
else {
throw new error_1.FirebaseError("Repo URI is required but not currently set.");
}
}
let extensionRoot = args.extensionRoot || (latestVersion === null || latestVersion === void 0 ? void 0 : latestVersion.extensionRoot);
if (!extensionRoot) {
const defaultRoot = "/";
if (!args.nonInteractive) {
extensionRoot = await promptForExtensionRoot(defaultRoot);
}
else {
extensionRoot = defaultRoot;
}
}
const normalizedRoot = path
.normalize(extensionRoot)
.replaceAll(/^\/|\/$/g, "")
.replaceAll(/^(\.\.\/)*/g, "");
extensionRoot = normalizedRoot || "/";
let sourceRef = args.sourceRef;
const defaultSourceRef = "HEAD";
if (!sourceRef) {
if (!args.nonInteractive) {
sourceRef = await (0, prompt_1.input)({
message: "Enter the commit hash, branch, or tag name to build from in the repo:",
default: defaultSourceRef,
});
}
else {
sourceRef = defaultSourceRef;
}
}
const rootDirectory = await fetchExtensionSource(repoUri, sourceRef, extensionRoot);
const extensionSpec = await validateExtensionSpec(rootDirectory, args.extensionId);
validateVersion(extensionRef, extensionSpec.version, extension === null || extension === void 0 ? void 0 : extension.latestVersion);
const { versionByStage, hasVersions } = await getNextVersionByStage(extensionRef, extensionSpec.version);
const autoReview = !!(extension === null || extension === void 0 ? void 0 : extension.latestApprovedVersion) ||
((_a = latestVersion === null || latestVersion === void 0 ? void 0 : latestVersion.listing) === null || _a === void 0 ? void 0 : _a.state) === "PENDING" ||
((_b = latestVersion === null || latestVersion === void 0 ? void 0 : latestVersion.listing) === null || _b === void 0 ? void 0 : _b.state) === "APPROVED" ||
((_c = latestVersion === null || latestVersion === void 0 ? void 0 : latestVersion.listing) === null || _c === void 0 ? void 0 : _c.state) === "REJECTED";
let stage = args.stage;
if (!stage) {
stage = await promptForReleaseStage({
versionByStage,
autoReview,
allowStable: true,
hasVersions,
nonInteractive: args.nonInteractive,
force: args.force,
});
}
const newVersion = versionByStage.get(stage);
const releaseNotes = validateReleaseNotes(rootDirectory, extensionSpec.version, extension);
const sourceUri = repoUri + path.join("/tree", sourceRef, extensionRoot);
displayReleaseNotes({
extensionRef,
newVersion,
releaseNotes,
sourceUri,
autoReview: stage === "stable" && autoReview,
});
const confirmed = await (0, prompt_1.confirm)({
message: "Continue?",
nonInteractive: args.nonInteractive,
force: args.force,
default: false,
});
if (!confirmed) {
return;
}
const extensionVersionRef = `${extensionRef}@${newVersion}`;
const uploadSpinner = ora(`Uploading ${clc.bold(extensionVersionRef)}...`);
let res;
try {
uploadSpinner.start();
res = await (0, publisherApi_1.createExtensionVersionFromGitHubSource)({
extensionVersionRef,
extensionRoot,
repoUri,
sourceRef: sourceRef,
});
uploadSpinner.succeed(`Successfully uploaded ${clc.bold(extensionRef)}`);
}
catch (err) {
uploadSpinner.fail();
if (err.status === 404) {
throw getMissingPublisherError(args.publisherId);
}
throw err;
}
return res;
}
exports.uploadExtensionVersionFromGitHubSource = uploadExtensionVersionFromGitHubSource;
async function uploadExtensionVersionFromLocalSource(args) {
const extensionRef = `${args.publisherId}/${args.extensionId}`;
let extension;
let latestVersion;
try {
extension = await (0, publisherApi_1.getExtension)(extensionRef);
latestVersion = await (0, publisherApi_1.getExtensionVersion)(`${extensionRef}@latest`);
}
catch (err) {
}
displayExtensionHeader(extensionRef, extension, latestVersion === null || latestVersion === void 0 ? void 0 : latestVersion.extensionRoot);
const localStageOptions = ["rc", "alpha", "beta"];
if (args.stage && !localStageOptions.includes(args.stage)) {
throw new error_1.FirebaseError(`--stage only supports the following values when used with --local: ${localStageOptions.join(", ")}`);
}
const extensionSpec = await validateExtensionSpec(args.rootDirectory, args.extensionId);
validateVersion(extensionRef, extensionSpec.version, extension === null || extension === void 0 ? void 0 : extension.latestVersion);
const { versionByStage } = await getNextVersionByStage(extensionRef, extensionSpec.version);
let stage = args.stage;
if (!stage) {
if (!args.nonInteractive) {
stage = await promptForReleaseStage({
versionByStage,
autoReview: false,
allowStable: false,
hasVersions: false,
nonInteractive: args.nonInteractive,
force: args.force,
});
}
else {
stage = "rc";
}
}
const newVersion = versionByStage.get(stage);
const releaseNotes = validateReleaseNotes(args.rootDirectory, extensionSpec.version, extension);
displayReleaseNotes({ extensionRef, newVersion, releaseNotes, autoReview: false });
const confirmed = await (0, prompt_1.confirm)({
message: "Continue?",
nonInteractive: args.nonInteractive,
force: args.force,
default: false,
});
if (!confirmed) {
return;
}
const extensionVersionRef = `${extensionRef}@${newVersion}`;
let packageUri;
let objectPath = "";
const uploadSpinner = ora("Archiving and uploading extension source code...");
try {
uploadSpinner.start();
objectPath = await archiveAndUploadSource(args.rootDirectory, exports.EXTENSIONS_BUCKET_NAME);
uploadSpinner.succeed("Uploaded extension source code");
packageUri = (0, api_1.storageOrigin)() + objectPath + "?alt=media";
}
catch (err) {
uploadSpinner.fail();
throw new error_1.FirebaseError(`Failed to archive and upload extension source code, ${err}`, {
original: err,
});
}
const publishSpinner = ora(`Uploading ${clc.bold(extensionVersionRef)}...`);
let res;
try {
publishSpinner.start();
res = await (0, publisherApi_1.createExtensionVersionFromLocalSource)({ extensionVersionRef, packageUri });
publishSpinner.succeed(`Successfully uploaded ${clc.bold(extensionVersionRef)}`);
}
catch (err) {
publishSpinner.fail();
if (err.status === 404) {
throw getMissingPublisherError(args.publisherId);
}
throw err;
}
await deleteUploadedSource(objectPath);
return res;
}
exports.uploadExtensionVersionFromLocalSource = uploadExtensionVersionFromLocalSource;
function getMissingPublisherError(publisherId) {
return new error_1.FirebaseError(`Couldn't find publisher ID '${clc.bold(publisherId)}'. Please ensure that you have registered this ID. For step-by-step instructions on getting started as a publisher, see https://firebase.google.com/docs/extensions/publishers/get-started.`);
}
exports.getMissingPublisherError = getMissingPublisherError;
async function createSourceFromLocation(projectId, sourceUri) {
const extensionRoot = "/";
let packageUri;
let objectPath = "";
const spinner = ora(" Archiving and uploading extension source code");
try {
spinner.start();
objectPath = await archiveAndUploadSource(sourceUri, exports.EXTENSIONS_BUCKET_NAME);
spinner.succeed(" Uploaded extension source code");
packageUri = (0, api_1.storageOrigin)() + objectPath + "?alt=media";
const res = await (0, extensionsApi_1.createSource)(projectId, packageUri, extensionRoot);
logger_1.logger.debug("Created new Extension Source %s", res.name);
await deleteUploadedSource(objectPath);
return res;
}
catch (err) {
spinner.fail();
throw new error_1.FirebaseError(`Failed to archive and upload extension source from ${sourceUri}, ${err}`, {
original: err,
});
}
}
exports.createSourceFromLocation = createSourceFromLocation;
async function deleteUploadedSource(objectPath) {
if (objectPath.length) {
try {
await (0, storage_1.deleteObject)(objectPath);
logger_1.logger.debug("Cleaned up uploaded source archive");
}
catch (err) {
logger_1.logger.debug("Unable to clean up uploaded source archive");
}
}
}
function getPublisherProjectFromName(publisherName) {
const publisherNameRegex = /projects\/.+\/publisherProfile/;
if (publisherNameRegex.test(publisherName)) {
const [_, projectNumber, __] = publisherName.split("/");
return Number.parseInt(projectNumber);
}
throw new error_1.FirebaseError(`Could not find publisher with name '${publisherName}'.`);
}
exports.getPublisherProjectFromName = getPublisherProjectFromName;
function displayReleaseNotes(args) {
const source = args.sourceUri || "Local source";
const releaseNotesMessage = args.releaseNotes
? `${clc.bold("Release notes:")}\n${(0, marked_1.marked)(args.releaseNotes)}`
: "\n";
const metadataMessage = `${clc.bold("Extension:")} ${args.extensionRef}\n` +
`${clc.bold("Version:")} ${clc.bold(clc.green(args.newVersion))} ${args.autoReview ? "(automatically sent for review)" : ""}\n` +
`${clc.bold("Source:")} ${source}\n`;
const message = `\nYou are about to upload a new version to Firebase's registry of extensions.\n\n` +
metadataMessage +
releaseNotesMessage +
`Once an extension version is uploaded, it becomes installable by other users and cannot be changed. If you wish to make changes after uploading, you will need to upload a new version.\n`;
logger_1.logger.info(message);
}
exports.displayReleaseNotes = displayReleaseNotes;
async function promptForOfficialExtension(message) {
const officialExts = await (0, resolveSource_1.getExtensionRegistry)(true);
return await (0, prompt_1.select)({
message,
choices: (0, utils_1.convertOfficialExtensionsToList)(officialExts),
pageSize: Object.keys(officialExts).length,
});
}
exports.promptForOfficialExtension = promptForOfficialExtension;
async function promptForRepeatInstance(projectName, extensionName) {
const message = `An extension with the ID '${clc.bold(extensionName)}' already exists in the project '${clc.bold(projectName)}'. What would you like to do?`;
return await (0, prompt_1.select)({
message,
choices: [
{ name: "Update or reconfigure the existing instance", value: "updateExisting" },
{ name: "Install a new instance with a different ID", value: "installNew" },
{ name: "Cancel extension installation", value: "cancel" },
],
});
}
exports.promptForRepeatInstance = promptForRepeatInstance;
async function instanceIdExists(projectId, instanceId) {
try {
await (0, extensionsApi_1.getInstance)(projectId, instanceId);
}
catch (err) {
if (err instanceof error_1.FirebaseError) {
if (err.status === 404) {
return false;
}
const msg = `Unexpected error when checking if instance ID exists: ${err.message}`;
throw new error_1.FirebaseError(msg, {
original: err,
});
}
else {
throw err;
}
}
return true;
}
exports.instanceIdExists = instanceIdExists;
function isUrlPath(extInstallPath) {
return extInstallPath.startsWith("https:");
}
exports.isUrlPath = isUrlPath;
function isLocalPath(extInstallPath) {
const trimmedPath = extInstallPath.trim();
return (trimmedPath.startsWith(`~${path.sep}`) ||
trimmedPath.startsWith(`.${path.sep}`) ||
trimmedPath.startsWith(`..${path.sep}`) ||
trimmedPath.startsWith(`${path.sep}`) ||
trimmedPath.startsWith(`~/`) ||
trimmedPath.startsWith(`./`) ||
trimmedPath.startsWith(`../`) ||
trimmedPath.startsWith(`/`) ||
[".", ".."].includes(trimmedPath));
}
exports.isLocalPath = isLocalPath;
function isLocalOrURLPath(extInstallPath) {
return isLocalPath(extInstallPath) || isUrlPath(extInstallPath);
}
exports.isLocalOrURLPath = isLocalOrURLPath;
function getSourceOrigin(sourceOrVersion) {
if (isLocalPath(sourceOrVersion)) {
return SourceOrigin.LOCAL;
}
if (isUrlPath(sourceOrVersion)) {
return SourceOrigin.URL;
}
if (sourceOrVersion.includes("/")) {
let ref;
try {
ref = refs.parse(sourceOrVersion);
}
catch (err) {
}
if (ref && ref.publisherId && ref.extensionId && !ref.version) {
return SourceOrigin.PUBLISHED_EXTENSION;
}
else if (ref && ref.publisherId && ref.extensionId && ref.version) {
return SourceOrigin.PUBLISHED_EXTENSION_VERSION;
}
}
throw new error_1.FirebaseError(`Could not find source '${clc.bold(sourceOrVersion)}'. Check to make sure the source is correct, and then please try again.`);
}
exports.getSourceOrigin = getSourceOrigin;
async function diagnoseAndFixProject(options) {
const projectId = (0, projectUtils_1.getProjectId)(options);
if (!projectId) {
return;
}
const ok = await (0, diagnose_1.diagnose)(projectId);
if (!ok) {
throw new error_1.FirebaseError("Unable to proceed until all issues are resolved.");
}
}
exports.diagnoseAndFixProject = diagnoseAndFixProject;
;