Skip to content

Commit

Permalink
Migrate extensions commands to use registry APIs (#3273)
Browse files Browse the repository at this point in the history
* Add publisher to registry file fields (#686)

* Migrate ext:update ref-based flow to include update warnings + min extension version guard (#684)

* Migrate ext:install flow to install via extension reference and remove EAP gating (#679)

* Remove EAP-specific copy in ext:dev:register and ext:dev:publish command (#691)

* Warn user that unpublishing is final in ext:dev:unpublish command (#693)

* Add extMinVersion flag to ext:dev:unpublish command (#698)

* Migrate ext:info flow to retrieve spec from Registry API (#683)

* Migrate "author" terminology to use "publisher" in Extensions CLI commands (#694)

* Fix bug in confirmInstallByReference and refactor ext:install error messages (#702)

* Fix local path detection logic and refactor warnings logic in ext:update flow (#703)

* Migrate extensions warnings relating to audiences to use (backend) launch stage and visibility fields (#705)

* Only infer firebase if publisher not provided as part of user input in ext:info flow (#708)

* Adds new warning prompt for non-trusted publishers during ext:install (#707)

* add new warning prompt for non-trusted publishers during ext:install

* clean up param namne

* clean up comment

* switch from author ulr to sourceUrl

* no please

* Update copy to link user to documentation on ext:install flow if input not found (#711)

* Adds console install link to ext:dev:publish (#709)

* Adds console install link to ext:dev:publish

* formats

* Add firebase ext:dev:delete command to CLI (#712)

* adds changelog

* formats

Co-authored-by: huangjeff5 <[email protected]>
Co-authored-by: Jeff Huang <[email protected]>
Co-authored-by: Elvis Sun <[email protected]>
  • Loading branch information
4 people authored Apr 13, 2021
1 parent e8c817c commit 01610e0
Show file tree
Hide file tree
Showing 23 changed files with 694 additions and 358 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Migrates Firebase Extensions commands to use registry API.
58 changes: 58 additions & 0 deletions src/commands/ext-dev-extension-delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as utils from "../utils";
import * as clc from "cli-color";

import { Command } from "../command";
import { logPrefix } from "../extensions/extensionsHelper";
import { parseRef, getExtension, deleteExtension } from "../extensions/extensionsApi";
import { promptOnce } from "../prompt";
import { requireAuth } from "../requireAuth";
import { FirebaseError } from "../error";
import { checkMinRequiredVersion } from "../checkMinRequiredVersion";

module.exports = new Command("ext:dev:delete <extensionRef>")
.description("delete an extension")
.help(
"use this command to delete an extension, and make it unavailable for developers to install or reconfigure. " +
"Specify the extension you want to delete using the format '<publisherId>/<extensionId>."
)
.before(requireAuth)
.before(checkMinRequiredVersion, "extDevMinVersion")
.action(async (extensionRef: string) => {
const { publisherId, extensionId, version } = parseRef(extensionRef);
if (version) {
throw new FirebaseError(
`Deleting a single version is not currently supported. You can only delete ${clc.bold(
"ALL versions"
)} of an extension. To delete all versions, please remove the version from the reference.`
);
}
utils.logLabeledWarning(
logPrefix,
"If you delete this extension, developers won't be able to install it. " +
"For developers who currently have this extension installed, " +
"it will continue to run and will appear as unpublished when " +
"listed in the Firebase console or Firebase CLI."
);
utils.logLabeledWarning(
"This is a permanent action",
`Once deleted, you may never use the extension name '${clc.bold(extensionId)}' again.`
);
await getExtension(extensionRef);
const consent = await confirmDelete(publisherId, extensionId);
if (!consent) {
throw new FirebaseError("deletion cancelled.");
}
await deleteExtension(extensionRef);
utils.logLabeledSuccess(logPrefix, "successfully deleted all versions of this extension.");
});

async function confirmDelete(publisherId: string, extensionId: string): Promise<string> {
const message = `You are about to delete ALL versions of ${clc.green(
`${publisherId}/${extensionId}`
)}.\nDo you wish to continue? `;
return await promptOnce({
type: "confirm",
message,
default: false, // Force users to explicitly type 'yes'
});
}
17 changes: 14 additions & 3 deletions src/commands/ext-dev-publish.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import * as clc from "cli-color";
import * as marked from "marked";
import TerminalRenderer = require("marked-terminal");

import { Command } from "../command";
import { publishExtensionVersionFromLocalSource } from "../extensions/extensionsHelper";
import { publishExtensionVersionFromLocalSource, logPrefix } from "../extensions/extensionsHelper";
import { parseRef } from "../extensions/extensionsApi";

import { findExtensionYaml } from "../extensions/localHelper";
import { consoleInstallLink } from "../extensions/publishHelpers";
import { requireAuth } from "../requireAuth";
import * as clc from "cli-color";
import { FirebaseError } from "../error";
import * as utils from "../utils";

marked.setOptions({
renderer: new TerminalRenderer(),
});

/**
* Command for publishing an extension version.
Expand Down Expand Up @@ -40,5 +48,8 @@ export default new Command("ext:dev:publish <extensionRef>")
extensionId,
extensionYamlDirectory
);
if (res) {
utils.logLabeledBullet(logPrefix, marked(`[Install Link](${consoleInstallLink(res.ref)})`));
}
return res;
});
2 changes: 1 addition & 1 deletion src/commands/ext-dev-register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default new Command("ext:dev:register")
const msg =
"What would you like to register as your publisher ID? " +
"This value identifies you in Firebase's registry of extensions as the author of your extensions. " +
"Examples: my-company-name, MyGitHubUsername. If you are a member of the Extensions EAP group, your published extensions will only be accessible to other members of the EAP group. \n\n" +
"Examples: my-company-name, MyGitHubUsername.\n\n" +
"You can only do this once for each project.";
const publisherId = await promptOnce({
name: "publisherId",
Expand Down
13 changes: 10 additions & 3 deletions src/commands/ext-dev-unpublish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { promptOnce } from "../prompt";
import * as clc from "cli-color";
import { requireAuth } from "../requireAuth";
import { FirebaseError } from "../error";
import { checkMinRequiredVersion } from "../checkMinRequiredVersion";

module.exports = new Command("ext:dev:unpublish <extensionRef>")
.description("unpublish an extension")
Expand All @@ -14,11 +15,17 @@ module.exports = new Command("ext:dev:unpublish <extensionRef>")
"Specify the extension you want to unpublish using the format '<publisherId>/<extensionId>."
)
.before(requireAuth)
.before(checkMinRequiredVersion, "extDevMinVersion")
.action(async (extensionRef: string) => {
const { publisherId, extensionId, version } = parseRef(extensionRef);
const message =
"If you unpublish this extension, developers won't be able to install it. For developers who currently have this extension installed, it will continue to run and will appear as unpublished when listed in the Firebase console or Firebase CLI.";
utils.logLabeledWarning(logPrefix, message);
utils.logLabeledWarning(
logPrefix,
"If you unpublish this extension, developers won't be able to install it. For developers who currently have this extension installed, it will continue to run and will appear as unpublished when listed in the Firebase console or Firebase CLI."
);
utils.logLabeledWarning(
"This is a permanent action",
`Once unpublished, you may never use the extension name '${clc.bold(extensionId)}' again.`
);
if (version) {
throw new FirebaseError(
`Unpublishing a single version is not currently supported. You can only unpublish ${clc.bold(
Expand Down
20 changes: 13 additions & 7 deletions src/commands/ext-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as _ from "lodash";

import { checkMinRequiredVersion } from "../checkMinRequiredVersion";
import { Command } from "../command";
import { resolveRegistryEntry, resolveSourceUrl } from "../extensions/resolveSource";
import { resolveRegistryEntry } from "../extensions/resolveSource";
import * as extensionsApi from "../extensions/extensionsApi";
import { ensureExtensionsApiEnabled, logPrefix } from "../extensions/extensionsHelper";
import { isLocalExtension, getLocalExtensionSpec } from "../extensions/localHelper";
Expand Down Expand Up @@ -31,12 +31,18 @@ export default new Command("ext:info <extensionName>")
} else {
await requirePermissions(options, ["firebaseextensions.sources.get"]);
await ensureExtensionsApiEnabled(options);

const [name, version] = extensionName.split("@");
const registryEntry = await resolveRegistryEntry(name);
const sourceUrl = resolveSourceUrl(registryEntry, name, version);
const source = await extensionsApi.getSource(sourceUrl);
spec = source.spec;
const hasPublisherId = extensionName.split("/").length >= 2;
if (hasPublisherId) {
const nameAndVersion = extensionName.split("/")[1];
if (nameAndVersion.split("@").length < 2) {
extensionName = extensionName + "@latest";
}
} else {
const [name, version] = extensionName.split("@");
extensionName = `firebase/${name}@${version || "latest"}`;
}
const version = await extensionsApi.getExtensionVersion(extensionName);
spec = version.spec;
}

if (!options.markdown) {
Expand Down
177 changes: 87 additions & 90 deletions src/commands/ext-install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,23 @@ import { Command } from "../command";
import { FirebaseError } from "../error";
import * as getProjectId from "../getProjectId";
import * as extensionsApi from "../extensions/extensionsApi";
import {
promptForAudienceConsent,
resolveRegistryEntry,
resolveSourceUrl,
} from "../extensions/resolveSource";
import { promptForLaunchStageConsent } from "../extensions/resolveSource";
import * as paramHelper from "../extensions/paramHelper";
import {
confirmInstallInstance,
createSourceFromLocation,
ensureExtensionsApiEnabled,
getSourceOrigin,
instanceIdExists,
logPrefix,
promptForOfficialExtension,
promptForRepeatInstance,
promptForValidInstanceId,
SourceOrigin,
isLocalOrURLPath,
} from "../extensions/extensionsHelper";
import { getRandomString } from "../extensions/utils";
import { requirePermissions } from "../requirePermissions";
import * as utils from "../utils";
import { logger } from "../logger";
import { promptOnce } from "../prompt";
import { previews } from "../previews";

marked.setOptions({
Expand Down Expand Up @@ -144,6 +138,64 @@ async function installExtension(options: InstallExtensionOptions): Promise<void>
}
}

async function confirmInstallBySource(
projectId: string,
extensionName: string
): Promise<extensionsApi.ExtensionSource> {
// Create a one off source to use for the install flow.
let source;
try {
source = await createSourceFromLocation(projectId, extensionName);
} catch (err) {
throw new FirebaseError(
`Unable to find published extension '${clc.bold(extensionName)}', ` +
`and encountered the following error when trying to create an instance of extension '${clc.bold(
extensionName
)}':\n ${err.message}`
);
}
displayExtInfo(extensionName, "", source.spec);
const confirm = await confirmInstallInstance();
if (!confirm) {
throw new FirebaseError("Install cancelled.");
}
return source;
}

async function confirmInstallByReference(
extensionName: string
): Promise<extensionsApi.ExtensionVersion> {
// Infer firebase if publisher ID not provided.
if (extensionName.split("/").length < 2) {
const [extensionID, version] = extensionName.split("@");
extensionName = `firebase/${extensionID}@${version || "latest"}`;
}
// Get the correct version for a given extension reference from the Registry API.
const ref = extensionsApi.parseRef(extensionName);
const extension = await extensionsApi.getExtension(`${ref.publisherId}/${ref.extensionId}`);
if (!ref.version) {
extensionName = `${extensionName}@latest`;
}
const extVersion = await extensionsApi.getExtensionVersion(extensionName);
displayExtInfo(extensionName, ref.publisherId, extVersion.spec, true);
const confirm = await confirmInstallInstance();
if (!confirm) {
throw new FirebaseError("Install cancelled.");
}
const audienceConsent = await promptForLaunchStageConsent(extension.registryLaunchStage);
if (!audienceConsent) {
throw new FirebaseError("Install cancelled.");
}
const eapPublisherConsent = await askUserForConsent.checkAndPromptForEapPublisher(
ref.publisherId,
extVersion.spec?.sourceUrl
);
if (!eapPublisherConsent) {
throw new FirebaseError("Install cancelled.");
}
return extVersion;
}

/**
* Command for installing an extension
*/
Expand All @@ -170,80 +222,29 @@ export default new Command("ext:install [extensionName]")
"Which official extension do you wish to install?\n" +
" Select an extension, then press Enter to learn more."
);
} else {
throw new FirebaseError(
`Please provide an extension name, or run ${clc.bold(
"firebase ext:install -i"
)} to select from the list of all available official extensions.`
);
}
}

const [name, version] = extensionName.split("@");
let source;
let extVersion;
try {
const registryEntry = await resolveRegistryEntry(name);
const sourceUrl = resolveSourceUrl(registryEntry, name, version);
source = await extensionsApi.getSource(sourceUrl);
displayExtInfo(extensionName, source.spec, true);
await confirmInstallInstance();
const audienceConsent = await promptForAudienceConsent(registryEntry);
if (!audienceConsent) {
logger.info("Install cancelled.");
return;
}
} catch (err) {
if (previews.extdev) {
const sourceOrigin = await getSourceOrigin(extensionName);
switch (sourceOrigin) {
case SourceOrigin.LOCAL || SourceOrigin.URL: {
try {
source = await createSourceFromLocation(projectId, extensionName);
} catch (err) {
throw new FirebaseError(
`Unable to find published extension '${clc.bold(extensionName)}', ` +
`and encountered the following error when trying to create an instance of extension '${clc.bold(
extensionName
)}':\n ${err.message}`
);
}
displayExtInfo(extensionName, source.spec);
await confirmInstallInstance();
break;
}
case SourceOrigin.PUBLISHED_EXTENSION: {
await extensionsApi.getExtension(extensionName);
extVersion = await extensionsApi.getExtensionVersion(`${extensionName}@latest`);
displayExtInfo(extensionName, extVersion.spec, true);
await confirmInstallInstance();
break;
}
case SourceOrigin.PUBLISHED_EXTENSION_VERSION: {
extVersion = await extensionsApi.getExtensionVersion(`${extensionName}`);
displayExtInfo(extensionName, extVersion.spec, true);
await confirmInstallInstance();
break;
}
default: {
throw new FirebaseError(
`Could not determine source origin for extension '${extensionName}'. If this is a published extension, ` +
"please make sure the publisher and extension exist before trying again. If trying to create an extension, " +
"please ensure the path or URL given is valid."
);
}
}
} else {
throw new FirebaseError(
`Unable to find published extension '${clc.bold(extensionName)}'. ` +
`Run ${clc.bold(
"firebase ext:install -i"
)} to select from the list of all available published extensions.`,
{ original: err }
)} to select from the list of all available published extensions.`
);
}
}

let source;
let extVersion;
// If the user types in URL, or a local path (prefixed with ~/, ../, or ./), install from local/URL source.
// Otherwise, treat the input as an extension reference and proceed with reference-based installation.
if (isLocalOrURLPath(extensionName)) {
source = await confirmInstallBySource(projectId, extensionName);
} else {
extVersion = await confirmInstallByReference(extensionName);
}
if (!source && !extVersion) {
throw new FirebaseError(
"Could not find a source. Please specify a valid source to continue."
);
}
const spec = source?.spec || extVersion?.spec;
if (!spec) {
throw new FirebaseError(
Expand All @@ -252,23 +253,19 @@ export default new Command("ext:install [extensionName]")
)}'. Please make sure this is a valid extension and try again.`
);
}
try {
if (learnMore) {
utils.logLabeledBullet(
logPrefix,
`You selected: ${clc.bold(spec.displayName)}.\n` +
`${spec.description}\n` +
`View details: https://firebase.google.com/products/extensions/${name}\n`
);
const confirm = await promptOnce({
type: "confirm",
default: true,
message: "Do you wish to install this extension?",
});
if (!confirm) {
return;
}
if (learnMore) {
utils.logLabeledBullet(
logPrefix,
`You selected: ${clc.bold(spec.displayName)}.\n` +
`${spec.description}\n` +
`View details: https://firebase.google.com/products/extensions/${spec.name}\n`
);
const confirm = await confirmInstallInstance();
if (!confirm) {
return;
}
}
try {
return installExtension({
paramFilePath,
projectId,
Expand Down
Loading

0 comments on commit 01610e0

Please sign in to comment.