UNPKG

firebase-tools

Version:
911 lines (910 loc) 41.5 kB
"use strict"; 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;