Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactored ext:install to use the latest extension metadata. #5997

Merged
merged 28 commits into from
Jun 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8d993df
Added cascading of latest approved version to latest version when ins…
apascal07 Jun 15, 2023
caf805d
Changed output of extension version info.
apascal07 Jun 15, 2023
4ce8fc6
Formatting, added more metadata, and cleaned up TODOs.
apascal07 Jun 15, 2023
3ce0c95
Formatting and extra notices.
apascal07 Jun 16, 2023
1ef2992
Added even more metadata.
apascal07 Jun 16, 2023
160eb51
Formatting.
apascal07 Jun 16, 2023
5558760
Fixing tests.
apascal07 Jun 16, 2023
f4a6231
Merge branch 'master' into ap-latest-version
apascal07 Jun 16, 2023
e3e7065
Added display of extension resources.
apascal07 Jun 16, 2023
d8fcfb4
Added link to Extensions Hub.
apascal07 Jun 16, 2023
83abfb0
Added displaying of events.
apascal07 Jun 16, 2023
89bd3d4
Formatting.
apascal07 Jun 16, 2023
8aa5f18
Formatting.
apascal07 Jun 16, 2023
2f26e26
Version bug.
apascal07 Jun 16, 2023
7190227
Added displaying of secrets and task queues.
apascal07 Jun 17, 2023
79d21d1
Added displaying of external services.
apascal07 Jun 17, 2023
f07fe5e
Fixed resolveVersion() + tests.
apascal07 Jun 20, 2023
6fb7722
Added tests for displayExtensionInfo().
apascal07 Jun 20, 2023
7e6fcb9
Merge branch 'master' into ap-latest-version
apascal07 Jun 20, 2023
66edd99
Update CHANGELOG.md
apascal07 Jun 21, 2023
1e3d2e2
Update CHANGELOG.md
apascal07 Jun 21, 2023
286685f
Update CHANGELOG.md
apascal07 Jun 27, 2023
6d0c1a9
Better messaging and parameterizing.
apascal07 Jun 27, 2023
6ce2d53
Merge branch 'master' into ap-latest-version
apascal07 Jun 27, 2023
01c68e3
Update displayExtensionInfo.ts
apascal07 Jun 27, 2023
dad2289
Update displayExtensionInfo.spec.ts
apascal07 Jun 27, 2023
9b9f959
Merge branch 'master' into ap-latest-version
apascal07 Jun 27, 2023
376e595
Update CHANGELOG.md
apascal07 Jun 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these | undefined needed? I think it is usually only necessary if you are explicitly setting source=undefined

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, if I don't then if (!source && !extensionVersion) complains about using them before initialization. Isn't MyType | undefined the expanded version of an optional/nullable in TS?

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