Skip to content

Commit

Permalink
Refactored ext:install to use the latest extension metadata. (#5997)
Browse files Browse the repository at this point in the history
* Added cascading of latest approved version to latest version when installing.

* Changed output of extension version info.

* Formatting, added more metadata, and cleaned up TODOs.

* Formatting and extra notices.

* Added even more metadata.

* Formatting.

* Fixing tests.

* Added display of extension resources.

* Added link to Extensions Hub.

* Added displaying of events.

* Formatting.

* Formatting.

* Version bug.

* Added displaying of secrets and task queues.

* Added displaying of external services.

* Fixed resolveVersion() + tests.

* Added tests for displayExtensionInfo().

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

Co-authored-by: joehan <joehanley@google.com>

* Better messaging and parameterizing.

* Update displayExtensionInfo.ts

* Update displayExtensionInfo.spec.ts

* Update CHANGELOG.md

---------

Co-authored-by: joehan <joehanley@google.com>
  • Loading branch information
apascal07 and joehan authored Jun 27, 2023
1 parent a21ac90 commit 2e8e909
Show file tree
Hide file tree
Showing 13 changed files with 327 additions and 330 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Refactored `ext:install` to use the latest extension metadata. (#5997)
2 changes: 0 additions & 2 deletions src/commands/ext-configure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,6 @@ export const command = new Command("ext:configure <extensionInstanceId>")
projectId,
paramSpecs: tbdParams,
nonInteractive: false,
// TODO(b/230598656): Clean up paramsEnvPath after v11 launch.
paramsEnvPath: "",
instanceId,
reconfiguring: true,
});
Expand Down
143 changes: 65 additions & 78 deletions src/commands/ext-install.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import * as clc from "colorette";
import { marked } from "marked";
import * as semver from "semver";
import * as TerminalRenderer from "marked-terminal";

import { displayExtInfo } from "../extensions/displayExtensionInfo";
import { displayExtensionVersionInfo } from "../extensions/displayExtensionInfo";
import * as askUserForEventsConfig from "../extensions/askUserForEventsConfig";
import { checkMinRequiredVersion } from "../checkMinRequiredVersion";
import { Command } from "../command";
import { FirebaseError } from "../error";
import { logger } from "../logger";
import { getProjectId, needProjectId } from "../projectUtils";
import * as extensionsApi from "../extensions/extensionsApi";
import { ExtensionVersion, ExtensionSource } from "../extensions/types";
Expand All @@ -17,13 +19,11 @@ import {
createSourceFromLocation,
ensureExtensionsApiEnabled,
logPrefix,
promptForOfficialExtension,
promptForValidInstanceId,
diagnoseAndFixProject,
isUrlPath,
isLocalPath,
canonicalizeRefInput,
} from "../extensions/extensionsHelper";
import { resolveVersion } from "../deploy/extensions/planner";
import { getRandomString } from "../extensions/utils";
import { requirePermissions } from "../requirePermissions";
import * as utils from "../utils";
Expand All @@ -40,7 +40,7 @@ marked.setOptions({
/**
* Command for installing an extension
*/
export const command = new Command("ext:install [extensionName]")
export const command = new Command("ext:install [extensionRef]")
.description(
"add an uploaded extension to firebase.json if [publisherId/extensionId] is provided;" +
"or, add a local extension if [localPath] is provided"
Expand All @@ -51,67 +51,80 @@ export const command = new Command("ext:install [extensionName]")
.before(ensureExtensionsApiEnabled)
.before(checkMinRequiredVersion, "extMinVersion")
.before(diagnoseAndFixProject)
.action(async (extensionName: string, options: Options) => {
const projectId = getProjectId(options);
// TODO(b/230598656): Clean up paramsEnvPath after v11 launch.
const paramsEnvPath = "";
let learnMore = false;
if (!extensionName) {
if (options.interactive) {
learnMore = true;
extensionName = await promptForOfficialExtension(
"Which official extension do you wish to install?\n" +
" Select an extension, then press Enter to learn more."
);
} 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.`
);
}
}
let source;
let extensionVersion;

// TODO(b/220900194): Remove when deprecating old install flow.
// --local doesn't support urlPath so this will become dead codepath.
if (isUrlPath(extensionName)) {
throw new FirebaseError(
`Installing with a source url is no longer supported in the CLI. Please use Firebase Console instead.`
);
}
.action(async (extensionRef: string, options: Options) => {
if (options.local) {
utils.logLabeledWarning(
logPrefix,
"As of firebase-tools@11.0.0, the `--local` flag is no longer required, as it is the default behavior."
);
}

if (!extensionRef) {
throw new FirebaseError(
"Extension ref is required to install. To see a full list of available extensions, go to Extensions Hub (https://extensions.dev/extensions)."
);
}
let source: ExtensionSource | undefined;
let extensionVersion: ExtensionVersion | undefined;
const projectId = getProjectId(options);
// If the user types in a local path (prefixed with ~/, ../, or ./), install from local source.
// Otherwise, treat the input as an extension reference and proceed with reference-based installation.
if (isLocalPath(extensionName)) {
if (isLocalPath(extensionRef)) {
// TODO(b/228444119): Create source should happen at deploy time.
// Should parse spec locally so we don't need project ID.
source = await createSourceFromLocation(needProjectId({ projectId }), extensionName);
await displayExtInfo(extensionName, "", source.spec);
source = await createSourceFromLocation(needProjectId({ projectId }), extensionRef);
await displayExtensionVersionInfo({ spec: source.spec });
void trackGA4("extension_added_to_manifest", {
published: "local",
interactive: options.nonInteractive ? "false" : "true",
});
} else {
extensionName = await canonicalizeRefInput(extensionName);
extensionVersion = await extensionsApi.getExtensionVersion(extensionName);

const extension = await extensionsApi.getExtension(extensionRef);
const ref = refs.parse(extensionRef);
ref.version = await resolveVersion(ref, extension);
const extensionVersionRef = refs.toExtensionVersionRef(ref);
extensionVersion = await extensionsApi.getExtensionVersion(extensionVersionRef);
void trackGA4("extension_added_to_manifest", {
published: extensionVersion.listing?.state === "APPROVED" ? "published" : "uploaded",
interactive: options.nonInteractive ? "false" : "true",
});
await infoExtensionVersion({
extensionName,
await displayExtensionVersionInfo({
spec: extensionVersion.spec,
extensionVersion,
latestApprovedVersion: extension.latestApprovedVersion,
latestVersion: extension.latestVersion,
});
if (extensionVersion.state === "DEPRECATED") {
throw new FirebaseError(
`Extension version ${clc.bold(
extensionVersionRef
)} is deprecated and cannot be installed. To install the latest non-deprecated version, omit the version in the extension ref.`
);
}
logger.info();
// Check if selected version is older than the latest approved version, or the latest version only if there is no approved version.
if (
(extension.latestApprovedVersion &&
semver.gt(extension.latestApprovedVersion, extensionVersion.spec.version)) ||
(!extension.latestApprovedVersion &&
extension.latestVersion &&
semver.gt(extension.latestVersion, extensionVersion.spec.version))
) {
const version = extension.latestApprovedVersion || extension.latestVersion;
logger.info(
`You are about to install extension version ${clc.bold(
extensionVersion.spec.version
)} which is older than the latest ${
extension.latestApprovedVersion ? "accepted version" : "version"
} ${clc.bold(version!)}.`
);
}
}
if (!source && !extensionVersion) {
throw new FirebaseError(
`Failed to parse ${clc.bold(
extensionRef
)} as an extension version or a path to a local extension. Please specify a valid reference.`
);
}
if (
!(await confirm({
Expand All @@ -122,33 +135,18 @@ export const command = new Command("ext:install [extensionName]")
) {
return;
}
if (!source && !extensionVersion) {
throw new FirebaseError(
"Could not find a source. Please specify a valid source to continue."
);
}
const spec = source?.spec ?? extensionVersion?.spec;
if (!spec) {
throw new FirebaseError(
`Could not find the extension.yaml for extension '${clc.bold(
extensionName
extensionRef
)}'. Please make sure this is a valid extension and try again.`
);
}
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`
);
}

try {
return installToManifest({
paramsEnvPath,
projectId,
extensionName,
extensionRef,
source,
extVersion: extensionVersion,
nonInteractive: options.nonInteractive,
Expand All @@ -164,18 +162,9 @@ export const command = new Command("ext:install [extensionName]")
}
});

async function infoExtensionVersion(args: {
extensionName: string;
extensionVersion: ExtensionVersion;
}): Promise<void> {
const ref = refs.parse(args.extensionName);
await displayExtInfo(args.extensionName, ref.publisherId, args.extensionVersion.spec, true);
}

interface InstallExtensionOptions {
paramsEnvPath?: string;
projectId?: string;
extensionName: string;
extensionRef: string;
source?: ExtensionSource;
extVersion?: ExtensionVersion;
nonInteractive: boolean;
Expand All @@ -189,14 +178,13 @@ interface InstallExtensionOptions {
* @param options
*/
async function installToManifest(options: InstallExtensionOptions): Promise<void> {
const { projectId, extensionName, extVersion, source, paramsEnvPath, nonInteractive, force } =
options;
const isLocalSource = isLocalPath(extensionName);
const { projectId, extensionRef, extVersion, source, nonInteractive, force } = options;
const isLocalSource = isLocalPath(extensionRef);

const spec = extVersion?.spec ?? source?.spec;
if (!spec) {
throw new FirebaseError(
`Could not find the extension.yaml for ${extensionName}. Please make sure this is a valid extension and try again.`
`Could not find the extension.yaml for ${extensionRef}. Please make sure this is a valid extension and try again.`
);
}

Expand All @@ -215,7 +203,6 @@ async function installToManifest(options: InstallExtensionOptions): Promise<void
projectId,
paramSpecs: (spec.params ?? []).concat(spec.systemParams ?? []),
nonInteractive,
paramsEnvPath,
instanceId,
});
const eventsConfig = spec.events
Expand All @@ -237,7 +224,7 @@ async function installToManifest(options: InstallExtensionOptions): Promise<void
{
instanceId,
ref: !isLocalSource ? ref : undefined,
localPath: isLocalSource ? extensionName : undefined,
localPath: isLocalSource ? extensionRef : undefined,
params: paramBindingOptions,
extensionSpec: spec,
},
Expand Down
2 changes: 0 additions & 2 deletions src/commands/ext-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,6 @@ export const command = new Command("ext:update <extensionInstanceId> [updateSour
newSpec: newExtensionVersion.spec,
currentParams: oldParamValues,
projectId,
// TODO(b/230598656): Clean up paramsEnvPath after v11 launch.
paramsEnvPath: "",
nonInteractive: options.nonInteractive,
instanceId,
});
Expand Down
25 changes: 13 additions & 12 deletions src/deploy/extensions/planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,32 +207,33 @@ export async function want(args: {
}

/**
* resolveVersion resolves a semver string to the max matching version.
* Exported for testing.
* @param publisherId
* @param extensionId
* @param version a semver or semver range
* Resolves a semver string to the max matching version. If no version is specified,
* it will default to the extension's latest approved version if set, otherwise to the latest version.
*
* @param ref the extension version ref
* @param extension the extension (optional)
*/
export async function resolveVersion(ref: refs.Ref): Promise<string> {
export async function resolveVersion(ref: refs.Ref, extension?: Extension): Promise<string> {
const extensionRef = refs.toExtensionRef(ref);
const extension = await extensionsApi.getExtension(extensionRef);
if (!ref.version || ref.version === "latest-approved") {
if (!extension.latestApprovedVersion) {
if (!ref.version && extension?.latestApprovedVersion) {
return extension.latestApprovedVersion;
}
if (ref.version === "latest-approved") {
if (!extension?.latestApprovedVersion) {
throw new FirebaseError(
`${extensionRef} has not been published to Extensions Hub (https://extensions.dev). To install it, you must specify the version you want to install.`
);
}
return extension.latestApprovedVersion;
}
if (ref.version === "latest") {
if (!extension.latestVersion) {
if (!ref.version || ref.version === "latest") {
if (!extension?.latestVersion) {
throw new FirebaseError(
`${extensionRef} has no stable non-deprecated versions. If you wish to install a prerelease version, you must specify the version you want to install.`
);
}
return extension.latestVersion;
}

const versions = await extensionsApi.listExtensionVersions(extensionRef, undefined, true);
if (versions.length === 0) {
throw new FirebaseError(`No versions found for ${extensionRef}`);
Expand Down
Loading

0 comments on commit 2e8e909

Please sign in to comment.