No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

extensionsHelper.js 25KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.canonicalizeRefInput = exports.diagnoseAndFixProject = exports.confirm = exports.getSourceOrigin = exports.isLocalOrURLPath = exports.isLocalPath = exports.isUrlPath = exports.instanceIdExists = exports.promptForRepeatInstance = exports.promptForOfficialExtension = exports.displayReleaseNotes = exports.getPublisherProjectFromName = exports.createSourceFromLocation = exports.publishExtensionVersionFromLocalSource = exports.incrementPrereleaseVersion = exports.ensureExtensionsApiEnabled = exports.promptForValidInstanceId = exports.validateSpec = exports.validateCommandLineParams = exports.populateDefaultParams = exports.substituteParams = exports.getFirebaseProjectParams = exports.getDBInstanceFromURL = exports.AUTOPOULATED_PARAM_PLACEHOLDERS = exports.EXTENSIONS_BUCKET_NAME = exports.URL_REGEX = exports.logPrefix = exports.SourceOrigin = exports.SpecParamType = void 0;
  4. const clc = require("colorette");
  5. const ora = require("ora");
  6. const semver = require("semver");
  7. const { marked } = require("marked");
  8. const TerminalRenderer = require("marked-terminal");
  9. marked.setOptions({
  10. renderer: new TerminalRenderer(),
  11. });
  12. const api_1 = require("../api");
  13. const archiveDirectory_1 = require("../archiveDirectory");
  14. const utils_1 = require("./utils");
  15. const functionsConfig_1 = require("../functionsConfig");
  16. const adminSdkConfig_1 = require("../emulator/adminSdkConfig");
  17. const resolveSource_1 = require("./resolveSource");
  18. const error_1 = require("../error");
  19. const diagnose_1 = require("./diagnose");
  20. const askUserForParam_1 = require("./askUserForParam");
  21. const ensureApiEnabled_1 = require("../ensureApiEnabled");
  22. const storage_1 = require("../gcp/storage");
  23. const projectUtils_1 = require("../projectUtils");
  24. const extensionsApi_1 = require("./extensionsApi");
  25. const refs = require("./refs");
  26. const localHelper_1 = require("./localHelper");
  27. const prompt_1 = require("../prompt");
  28. const logger_1 = require("../logger");
  29. const utils_2 = require("../utils");
  30. const change_log_1 = require("./change-log");
  31. const getProjectNumber_1 = require("../getProjectNumber");
  32. const constants_1 = require("../emulator/constants");
  33. const planner_1 = require("../deploy/extensions/planner");
  34. var SpecParamType;
  35. (function (SpecParamType) {
  36. SpecParamType["SELECT"] = "select";
  37. SpecParamType["MULTISELECT"] = "multiSelect";
  38. SpecParamType["STRING"] = "string";
  39. SpecParamType["SELECTRESOURCE"] = "selectResource";
  40. SpecParamType["SECRET"] = "secret";
  41. })(SpecParamType = exports.SpecParamType || (exports.SpecParamType = {}));
  42. var SourceOrigin;
  43. (function (SourceOrigin) {
  44. SourceOrigin["OFFICIAL_EXTENSION"] = "official extension";
  45. SourceOrigin["LOCAL"] = "unpublished extension (local source)";
  46. SourceOrigin["PUBLISHED_EXTENSION"] = "published extension";
  47. SourceOrigin["PUBLISHED_EXTENSION_VERSION"] = "specific version of a published extension";
  48. SourceOrigin["URL"] = "unpublished extension (URL source)";
  49. SourceOrigin["OFFICIAL_EXTENSION_VERSION"] = "specific version of an official extension";
  50. })(SourceOrigin = exports.SourceOrigin || (exports.SourceOrigin = {}));
  51. exports.logPrefix = "extensions";
  52. const VALID_LICENSES = ["apache-2.0"];
  53. exports.URL_REGEX = /^https:/;
  54. exports.EXTENSIONS_BUCKET_NAME = (0, utils_2.envOverride)("FIREBASE_EXTENSIONS_UPLOAD_BUCKET", "firebase-ext-eap-uploads");
  55. const AUTOPOPULATED_PARAM_NAMES = [
  56. "PROJECT_ID",
  57. "STORAGE_BUCKET",
  58. "EXT_INSTANCE_ID",
  59. "DATABASE_INSTANCE",
  60. "DATABASE_URL",
  61. ];
  62. exports.AUTOPOULATED_PARAM_PLACEHOLDERS = {
  63. PROJECT_ID: "project-id",
  64. STORAGE_BUCKET: "project-id.appspot.com",
  65. EXT_INSTANCE_ID: "extension-id",
  66. DATABASE_INSTANCE: "project-id-default-rtdb",
  67. DATABASE_URL: "https://project-id-default-rtdb.firebaseio.com",
  68. };
  69. function getDBInstanceFromURL(databaseUrl = "") {
  70. const instanceRegex = new RegExp("(?:https://)(.*)(?:.firebaseio.com)");
  71. const matches = instanceRegex.exec(databaseUrl);
  72. if (matches && matches.length > 1) {
  73. return matches[1];
  74. }
  75. return "";
  76. }
  77. exports.getDBInstanceFromURL = getDBInstanceFromURL;
  78. async function getFirebaseProjectParams(projectId, emulatorMode = false) {
  79. var _a, _b;
  80. if (!projectId) {
  81. return {};
  82. }
  83. const body = emulatorMode
  84. ? await (0, adminSdkConfig_1.getProjectAdminSdkConfigOrCached)(projectId)
  85. : await (0, functionsConfig_1.getFirebaseConfig)({ project: projectId });
  86. const projectNumber = emulatorMode && constants_1.Constants.isDemoProject(projectId)
  87. ? constants_1.Constants.FAKE_PROJECT_NUMBER
  88. : await (0, getProjectNumber_1.getProjectNumber)({ projectId });
  89. const databaseURL = (_a = body === null || body === void 0 ? void 0 : body.databaseURL) !== null && _a !== void 0 ? _a : `https://${projectId}.firebaseio.com`;
  90. const storageBucket = (_b = body === null || body === void 0 ? void 0 : body.storageBucket) !== null && _b !== void 0 ? _b : `${projectId}.appspot.com`;
  91. const FIREBASE_CONFIG = JSON.stringify({
  92. projectId,
  93. databaseURL,
  94. storageBucket,
  95. });
  96. return {
  97. PROJECT_ID: projectId,
  98. PROJECT_NUMBER: projectNumber,
  99. DATABASE_URL: databaseURL,
  100. STORAGE_BUCKET: storageBucket,
  101. FIREBASE_CONFIG,
  102. DATABASE_INSTANCE: getDBInstanceFromURL(databaseURL),
  103. };
  104. }
  105. exports.getFirebaseProjectParams = getFirebaseProjectParams;
  106. function substituteParams(original, params) {
  107. const startingString = JSON.stringify(original);
  108. const applySubstitution = (str, paramVal, paramKey) => {
  109. const exp1 = new RegExp("\\$\\{" + paramKey + "\\}", "g");
  110. const exp2 = new RegExp("\\$\\{param:" + paramKey + "\\}", "g");
  111. const regexes = [exp1, exp2];
  112. const substituteRegexMatches = (unsubstituted, regex) => {
  113. return unsubstituted.replace(regex, paramVal);
  114. };
  115. return regexes.reduce(substituteRegexMatches, str);
  116. };
  117. const s = Object.entries(params).reduce((str, [key, val]) => applySubstitution(str, val, key), startingString);
  118. return JSON.parse(s);
  119. }
  120. exports.substituteParams = substituteParams;
  121. function populateDefaultParams(paramVars, paramSpecs) {
  122. const newParams = paramVars;
  123. for (const param of paramSpecs) {
  124. if (!paramVars[param.param]) {
  125. if (param.default !== undefined && param.required) {
  126. newParams[param.param] = param.default;
  127. }
  128. else if (param.required) {
  129. throw new error_1.FirebaseError(`${param.param} has not been set in the given params file` +
  130. " and there is no default available. Please set this variable before installing again.");
  131. }
  132. }
  133. }
  134. return newParams;
  135. }
  136. exports.populateDefaultParams = populateDefaultParams;
  137. function validateCommandLineParams(envVars, paramSpec) {
  138. const paramNames = paramSpec.map((p) => p.param);
  139. const misnamedParams = Object.keys(envVars).filter((key) => {
  140. return !paramNames.includes(key) && !AUTOPOPULATED_PARAM_NAMES.includes(key);
  141. });
  142. if (misnamedParams.length) {
  143. logger_1.logger.warn("Warning: The following params were specified in your env file but do not exist in the extension spec: " +
  144. `${misnamedParams.join(", ")}.`);
  145. }
  146. let allParamsValid = true;
  147. for (const param of paramSpec) {
  148. if (!(0, askUserForParam_1.checkResponse)(envVars[param.param], param)) {
  149. allParamsValid = false;
  150. }
  151. }
  152. if (!allParamsValid) {
  153. throw new error_1.FirebaseError(`Some param values are not valid. Please check your params file.`);
  154. }
  155. }
  156. exports.validateCommandLineParams = validateCommandLineParams;
  157. function validateSpec(spec) {
  158. const errors = [];
  159. if (!spec.name) {
  160. errors.push("extension.yaml is missing required field: name");
  161. }
  162. if (!spec.specVersion) {
  163. errors.push("extension.yaml is missing required field: specVersion");
  164. }
  165. if (!spec.version) {
  166. errors.push("extension.yaml is missing required field: version");
  167. }
  168. if (!spec.license) {
  169. errors.push("extension.yaml is missing required field: license");
  170. }
  171. else {
  172. const formattedLicense = String(spec.license).toLocaleLowerCase();
  173. if (!VALID_LICENSES.includes(formattedLicense)) {
  174. errors.push(`license field in extension.yaml is invalid. Valid value(s): ${VALID_LICENSES.join(", ")}`);
  175. }
  176. }
  177. if (!spec.resources) {
  178. errors.push("Resources field must contain at least one resource");
  179. }
  180. else {
  181. for (const resource of spec.resources) {
  182. if (!resource.name) {
  183. errors.push("Resource is missing required field: name");
  184. }
  185. if (!resource.type) {
  186. errors.push(`Resource${resource.name ? ` ${resource.name}` : ""} is missing required field: type`);
  187. }
  188. }
  189. }
  190. for (const api of spec.apis || []) {
  191. if (!api.apiName) {
  192. errors.push("API is missing required field: apiName");
  193. }
  194. }
  195. for (const role of spec.roles || []) {
  196. if (!role.role) {
  197. errors.push("Role is missing required field: role");
  198. }
  199. }
  200. for (const param of spec.params || []) {
  201. if (!param.param) {
  202. errors.push("Param is missing required field: param");
  203. }
  204. if (!param.label) {
  205. errors.push(`Param${param.param ? ` ${param.param}` : ""} is missing required field: label`);
  206. }
  207. if (param.type && !Object.values(SpecParamType).includes(param.type)) {
  208. errors.push(`Invalid type ${param.type} for param${param.param ? ` ${param.param}` : ""}. Valid types are ${Object.values(SpecParamType).join(", ")}`);
  209. }
  210. if (!param.type || param.type === SpecParamType.STRING) {
  211. if (param.options) {
  212. errors.push(`Param${param.param ? ` ${param.param}` : ""} cannot have options because it is type STRING`);
  213. }
  214. }
  215. if (param.type &&
  216. (param.type === SpecParamType.SELECT || param.type === SpecParamType.MULTISELECT)) {
  217. if (param.validationRegex) {
  218. errors.push(`Param${param.param ? ` ${param.param}` : ""} cannot have validationRegex because it is type ${param.type}`);
  219. }
  220. if (!param.options) {
  221. errors.push(`Param${param.param ? ` ${param.param}` : ""} requires options because it is type ${param.type}`);
  222. }
  223. for (const opt of param.options || []) {
  224. if (opt.value === undefined) {
  225. errors.push(`Option for param${param.param ? ` ${param.param}` : ""} is missing required field: value`);
  226. }
  227. }
  228. }
  229. if (param.type && param.type === SpecParamType.SELECTRESOURCE) {
  230. if (!param.resourceType) {
  231. errors.push(`Param${param.param ? ` ${param.param}` : ""} must have resourceType because it is type ${param.type}`);
  232. }
  233. }
  234. }
  235. if (errors.length) {
  236. const formatted = errors.map((error) => ` - ${error}`);
  237. const message = `The extension.yaml has the following errors: \n${formatted.join("\n")}`;
  238. throw new error_1.FirebaseError(message);
  239. }
  240. }
  241. exports.validateSpec = validateSpec;
  242. async function promptForValidInstanceId(instanceId) {
  243. let instanceIdIsValid = false;
  244. let newInstanceId = "";
  245. const instanceIdRegex = /^[a-z][a-z\d\-]*[a-z\d]$/;
  246. while (!instanceIdIsValid) {
  247. newInstanceId = await (0, prompt_1.promptOnce)({
  248. type: "input",
  249. default: instanceId,
  250. message: `Please enter a new name for this instance:`,
  251. });
  252. if (newInstanceId.length <= 6 || 45 <= newInstanceId.length) {
  253. logger_1.logger.info("Invalid instance ID. Instance ID must be between 6 and 45 characters.");
  254. }
  255. else if (!instanceIdRegex.test(newInstanceId)) {
  256. logger_1.logger.info("Invalid instance ID. Instance ID must start with a lowercase letter, " +
  257. "end with a lowercase letter or number, and only contain lowercase letters, numbers, or -");
  258. }
  259. else {
  260. instanceIdIsValid = true;
  261. }
  262. }
  263. return newInstanceId;
  264. }
  265. exports.promptForValidInstanceId = promptForValidInstanceId;
  266. async function ensureExtensionsApiEnabled(options) {
  267. const projectId = (0, projectUtils_1.getProjectId)(options);
  268. if (!projectId) {
  269. return;
  270. }
  271. return await (0, ensureApiEnabled_1.ensure)(projectId, "firebaseextensions.googleapis.com", "extensions", options.markdown);
  272. }
  273. exports.ensureExtensionsApiEnabled = ensureExtensionsApiEnabled;
  274. async function archiveAndUploadSource(extPath, bucketName) {
  275. const zippedSource = await (0, archiveDirectory_1.archiveDirectory)(extPath, {
  276. type: "zip",
  277. ignore: ["node_modules", ".git"],
  278. });
  279. const res = await (0, storage_1.uploadObject)(zippedSource, bucketName);
  280. return `/${res.bucket}/${res.object}`;
  281. }
  282. async function incrementPrereleaseVersion(ref, extensionVersion, stage) {
  283. var _a;
  284. const stageOptions = ["stable", "alpha", "beta", "rc"];
  285. if (!stageOptions.includes(stage)) {
  286. throw new error_1.FirebaseError(`--stage flag only supports the following values: ${stageOptions}`);
  287. }
  288. if (stage !== "stable") {
  289. const version = semver.parse(extensionVersion);
  290. if (version.prerelease.length > 0 || version.build.length > 0) {
  291. throw new error_1.FirebaseError(`Cannot combine the --stage flag with a version with a prerelease annotation in extension.yaml.`);
  292. }
  293. let extensionVersions = [];
  294. try {
  295. extensionVersions = await (0, extensionsApi_1.listExtensionVersions)(ref, `id="${version.version}"`, true);
  296. }
  297. catch (e) {
  298. }
  299. const latestVersion = (_a = extensionVersions
  300. .map((version) => semver.parse(version.spec.version))
  301. .filter((version) => version.prerelease.length > 0 && version.prerelease[0] === stage)
  302. .sort((v1, v2) => semver.compare(v1, v2))
  303. .pop()) !== null && _a !== void 0 ? _a : `${version}-${stage}`;
  304. return semver.inc(latestVersion, "prerelease", undefined, stage);
  305. }
  306. return extensionVersion;
  307. }
  308. exports.incrementPrereleaseVersion = incrementPrereleaseVersion;
  309. async function publishExtensionVersionFromLocalSource(args) {
  310. const extensionSpec = await (0, localHelper_1.getLocalExtensionSpec)(args.rootDirectory);
  311. if (extensionSpec.name !== args.extensionId) {
  312. throw new error_1.FirebaseError(`Extension ID '${clc.bold(args.extensionId)}' does not match the name in extension.yaml '${clc.bold(extensionSpec.name)}'.`);
  313. }
  314. const subbedSpec = JSON.parse(JSON.stringify(extensionSpec));
  315. subbedSpec.params = substituteParams(extensionSpec.params || [], exports.AUTOPOULATED_PARAM_PLACEHOLDERS);
  316. validateSpec(subbedSpec);
  317. extensionSpec.version = await incrementPrereleaseVersion(`${args.publisherId}/${args.extensionId}`, extensionSpec.version, args.stage);
  318. let extension;
  319. try {
  320. extension = await (0, extensionsApi_1.getExtension)(`${args.publisherId}/${args.extensionId}`);
  321. }
  322. catch (err) {
  323. }
  324. let notes;
  325. try {
  326. const changes = (0, change_log_1.getLocalChangelog)(args.rootDirectory);
  327. notes = changes[extensionSpec.version];
  328. }
  329. catch (err) {
  330. throw new error_1.FirebaseError("No CHANGELOG.md file found. " +
  331. "Please create one and add an entry for this version. " +
  332. marked("See https://firebase.google.com/docs/extensions/alpha/create-user-docs#writing-changelog for more details."));
  333. }
  334. if (!notes && !semver.prerelease(extensionSpec.version) && extension) {
  335. throw new error_1.FirebaseError(`No entry for version ${extensionSpec.version} found in CHANGELOG.md. ` +
  336. "Please add one so users know what has changed in this version. " +
  337. marked("See https://firebase.google.com/docs/extensions/alpha/create-user-docs#writing-changelog for more details."));
  338. }
  339. displayReleaseNotes(args.publisherId, args.extensionId, extensionSpec.version, notes);
  340. if (!(await confirm({
  341. nonInteractive: args.nonInteractive,
  342. force: args.force,
  343. default: false,
  344. }))) {
  345. return;
  346. }
  347. if (extension &&
  348. extension.latestVersion &&
  349. semver.lt(extensionSpec.version, extension.latestVersion)) {
  350. throw new error_1.FirebaseError(`The version you are trying to publish (${clc.bold(extensionSpec.version)}) is lower than the current version (${clc.bold(extension.latestVersion)}) for the extension '${clc.bold(`${args.publisherId}/${args.extensionId}`)}'. Please make sure this version is greater than the current version (${clc.bold(extension.latestVersion)}) inside of extension.yaml.\n`, { exit: 104 });
  351. }
  352. else if (extension &&
  353. extension.latestVersion &&
  354. semver.eq(extensionSpec.version, extension.latestVersion)) {
  355. throw new error_1.FirebaseError(`The version you are trying to publish (${clc.bold(extensionSpec.version)}) already exists for the extension '${clc.bold(`${args.publisherId}/${args.extensionId}`)}'. Please increment the version inside of extension.yaml.\n`, { exit: 103 });
  356. }
  357. const ref = `${args.publisherId}/${args.extensionId}@${extensionSpec.version}`;
  358. let packageUri;
  359. let objectPath = "";
  360. const uploadSpinner = ora(" Archiving and uploading extension source code");
  361. try {
  362. uploadSpinner.start();
  363. objectPath = await archiveAndUploadSource(args.rootDirectory, exports.EXTENSIONS_BUCKET_NAME);
  364. uploadSpinner.succeed(" Uploaded extension source code");
  365. packageUri = api_1.storageOrigin + objectPath + "?alt=media";
  366. }
  367. catch (err) {
  368. uploadSpinner.fail();
  369. throw new error_1.FirebaseError(`Failed to archive and upload extension source, ${err}`, {
  370. original: err,
  371. });
  372. }
  373. const publishSpinner = ora(`Publishing ${clc.bold(ref)}`);
  374. let res;
  375. try {
  376. publishSpinner.start();
  377. res = await (0, extensionsApi_1.publishExtensionVersion)(ref, packageUri);
  378. publishSpinner.succeed(` Successfully published ${clc.bold(ref)}`);
  379. }
  380. catch (err) {
  381. publishSpinner.fail();
  382. if (err.status === 404) {
  383. throw new error_1.FirebaseError(marked(`Couldn't find publisher ID '${clc.bold(args.publisherId)}'. Please ensure that you have registered this ID. To register as a publisher, you can check out the [Firebase documentation](https://firebase.google.com/docs/extensions/alpha/share#register_as_an_extensions_publisher) for step-by-step instructions.`));
  384. }
  385. throw err;
  386. }
  387. await deleteUploadedSource(objectPath);
  388. return res;
  389. }
  390. exports.publishExtensionVersionFromLocalSource = publishExtensionVersionFromLocalSource;
  391. async function createSourceFromLocation(projectId, sourceUri) {
  392. const extensionRoot = "/";
  393. let packageUri;
  394. let objectPath = "";
  395. const spinner = ora(" Archiving and uploading extension source code");
  396. try {
  397. spinner.start();
  398. objectPath = await archiveAndUploadSource(sourceUri, exports.EXTENSIONS_BUCKET_NAME);
  399. spinner.succeed(" Uploaded extension source code");
  400. packageUri = api_1.storageOrigin + objectPath + "?alt=media";
  401. const res = await (0, extensionsApi_1.createSource)(projectId, packageUri, extensionRoot);
  402. logger_1.logger.debug("Created new Extension Source %s", res.name);
  403. await deleteUploadedSource(objectPath);
  404. return res;
  405. }
  406. catch (err) {
  407. spinner.fail();
  408. throw new error_1.FirebaseError(`Failed to archive and upload extension source from ${sourceUri}, ${err}`, {
  409. original: err,
  410. });
  411. }
  412. }
  413. exports.createSourceFromLocation = createSourceFromLocation;
  414. async function deleteUploadedSource(objectPath) {
  415. if (objectPath.length) {
  416. try {
  417. await (0, storage_1.deleteObject)(objectPath);
  418. logger_1.logger.debug("Cleaned up uploaded source archive");
  419. }
  420. catch (err) {
  421. logger_1.logger.debug("Unable to clean up uploaded source archive");
  422. }
  423. }
  424. }
  425. function getPublisherProjectFromName(publisherName) {
  426. const publisherNameRegex = /projects\/.+\/publisherProfile/;
  427. if (publisherNameRegex.test(publisherName)) {
  428. const [_, projectNumber, __] = publisherName.split("/");
  429. return Number.parseInt(projectNumber);
  430. }
  431. throw new error_1.FirebaseError(`Could not find publisher with name '${publisherName}'.`);
  432. }
  433. exports.getPublisherProjectFromName = getPublisherProjectFromName;
  434. function displayReleaseNotes(publisherId, extensionId, versionId, releaseNotes) {
  435. const releaseNotesMessage = releaseNotes
  436. ? ` Release notes for this version:\n${marked(releaseNotes)}\n`
  437. : "\n";
  438. const message = `You are about to publish version ${clc.green(versionId)} of ${clc.green(`${publisherId}/${extensionId}`)} to Firebase's registry of extensions.${releaseNotesMessage}` +
  439. "Once an extension version is published, it cannot be changed. If you wish to make changes after publishing, you will need to publish a new version.\n\n";
  440. logger_1.logger.info(message);
  441. }
  442. exports.displayReleaseNotes = displayReleaseNotes;
  443. async function promptForOfficialExtension(message) {
  444. const officialExts = await (0, resolveSource_1.getExtensionRegistry)(true);
  445. return await (0, prompt_1.promptOnce)({
  446. name: "input",
  447. type: "list",
  448. message,
  449. choices: (0, utils_1.convertOfficialExtensionsToList)(officialExts),
  450. pageSize: Object.keys(officialExts).length,
  451. });
  452. }
  453. exports.promptForOfficialExtension = promptForOfficialExtension;
  454. async function promptForRepeatInstance(projectName, extensionName) {
  455. const message = `An extension with the ID '${clc.bold(extensionName)}' already exists in the project '${clc.bold(projectName)}'. What would you like to do?`;
  456. const choices = [
  457. { name: "Update or reconfigure the existing instance", value: "updateExisting" },
  458. { name: "Install a new instance with a different ID", value: "installNew" },
  459. { name: "Cancel extension installation", value: "cancel" },
  460. ];
  461. return await (0, prompt_1.promptOnce)({
  462. type: "list",
  463. message,
  464. choices,
  465. });
  466. }
  467. exports.promptForRepeatInstance = promptForRepeatInstance;
  468. async function instanceIdExists(projectId, instanceId) {
  469. try {
  470. await (0, extensionsApi_1.getInstance)(projectId, instanceId);
  471. }
  472. catch (err) {
  473. if (err instanceof error_1.FirebaseError) {
  474. if (err.status === 404) {
  475. return false;
  476. }
  477. const msg = `Unexpected error when checking if instance ID exists: ${err}`;
  478. throw new error_1.FirebaseError(msg, {
  479. original: err,
  480. });
  481. }
  482. else {
  483. throw err;
  484. }
  485. }
  486. return true;
  487. }
  488. exports.instanceIdExists = instanceIdExists;
  489. function isUrlPath(extInstallPath) {
  490. return extInstallPath.startsWith("https:");
  491. }
  492. exports.isUrlPath = isUrlPath;
  493. function isLocalPath(extInstallPath) {
  494. const trimmedPath = extInstallPath.trim();
  495. return (trimmedPath.startsWith("~/") ||
  496. trimmedPath.startsWith("./") ||
  497. trimmedPath.startsWith("../") ||
  498. trimmedPath.startsWith("/") ||
  499. [".", ".."].includes(trimmedPath));
  500. }
  501. exports.isLocalPath = isLocalPath;
  502. function isLocalOrURLPath(extInstallPath) {
  503. return isLocalPath(extInstallPath) || isUrlPath(extInstallPath);
  504. }
  505. exports.isLocalOrURLPath = isLocalOrURLPath;
  506. function getSourceOrigin(sourceOrVersion) {
  507. if (isLocalPath(sourceOrVersion)) {
  508. return SourceOrigin.LOCAL;
  509. }
  510. if (isUrlPath(sourceOrVersion)) {
  511. return SourceOrigin.URL;
  512. }
  513. if (sourceOrVersion.includes("/")) {
  514. let ref;
  515. try {
  516. ref = refs.parse(sourceOrVersion);
  517. }
  518. catch (err) {
  519. }
  520. if (ref && ref.publisherId && ref.extensionId && !ref.version) {
  521. return SourceOrigin.PUBLISHED_EXTENSION;
  522. }
  523. else if (ref && ref.publisherId && ref.extensionId && ref.version) {
  524. return SourceOrigin.PUBLISHED_EXTENSION_VERSION;
  525. }
  526. }
  527. 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.`);
  528. }
  529. exports.getSourceOrigin = getSourceOrigin;
  530. async function confirm(args) {
  531. if (!args.nonInteractive && !args.force) {
  532. const message = `Do you wish to continue?`;
  533. return await (0, prompt_1.promptOnce)({
  534. type: "confirm",
  535. message,
  536. default: args.default,
  537. });
  538. }
  539. else if (args.nonInteractive && !args.force) {
  540. throw new error_1.FirebaseError("Pass the --force flag to use this command in non-interactive mode");
  541. }
  542. else {
  543. return true;
  544. }
  545. }
  546. exports.confirm = confirm;
  547. async function diagnoseAndFixProject(options) {
  548. const projectId = (0, projectUtils_1.getProjectId)(options);
  549. if (!projectId) {
  550. return;
  551. }
  552. const ok = await (0, diagnose_1.diagnose)(projectId);
  553. if (!ok) {
  554. throw new error_1.FirebaseError("Unable to proceed until all issues are resolved.");
  555. }
  556. }
  557. exports.diagnoseAndFixProject = diagnoseAndFixProject;
  558. async function canonicalizeRefInput(refInput) {
  559. let inferredRef = refInput;
  560. if (refInput.split("/").length < 2) {
  561. inferredRef = `firebase/${inferredRef}`;
  562. }
  563. if (refInput.split("@").length < 2) {
  564. inferredRef = `${inferredRef}@latest`;
  565. }
  566. const ref = refs.parse(inferredRef);
  567. ref.version = await (0, planner_1.resolveVersion)(ref);
  568. return refs.toExtensionVersionRef(ref);
  569. }
  570. exports.canonicalizeRefInput = canonicalizeRefInput;