Skip to content

Commit 2e8e909

Browse files
apascal07joehan
andauthored
Refactored ext:install to use the latest extension metadata. (#5997)
* 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>
1 parent a21ac90 commit 2e8e909

13 files changed

+327
-330
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Refactored `ext:install` to use the latest extension metadata. (#5997)

src/commands/ext-configure.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,6 @@ export const command = new Command("ext:configure <extensionInstanceId>")
8888
projectId,
8989
paramSpecs: tbdParams,
9090
nonInteractive: false,
91-
// TODO(b/230598656): Clean up paramsEnvPath after v11 launch.
92-
paramsEnvPath: "",
9391
instanceId,
9492
reconfiguring: true,
9593
});

src/commands/ext-install.ts

Lines changed: 65 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import * as clc from "colorette";
22
import { marked } from "marked";
3+
import * as semver from "semver";
34
import * as TerminalRenderer from "marked-terminal";
45

5-
import { displayExtInfo } from "../extensions/displayExtensionInfo";
6+
import { displayExtensionVersionInfo } from "../extensions/displayExtensionInfo";
67
import * as askUserForEventsConfig from "../extensions/askUserForEventsConfig";
78
import { checkMinRequiredVersion } from "../checkMinRequiredVersion";
89
import { Command } from "../command";
910
import { FirebaseError } from "../error";
11+
import { logger } from "../logger";
1012
import { getProjectId, needProjectId } from "../projectUtils";
1113
import * as extensionsApi from "../extensions/extensionsApi";
1214
import { ExtensionVersion, ExtensionSource } from "../extensions/types";
@@ -17,13 +19,11 @@ import {
1719
createSourceFromLocation,
1820
ensureExtensionsApiEnabled,
1921
logPrefix,
20-
promptForOfficialExtension,
2122
promptForValidInstanceId,
2223
diagnoseAndFixProject,
23-
isUrlPath,
2424
isLocalPath,
25-
canonicalizeRefInput,
2625
} from "../extensions/extensionsHelper";
26+
import { resolveVersion } from "../deploy/extensions/planner";
2727
import { getRandomString } from "../extensions/utils";
2828
import { requirePermissions } from "../requirePermissions";
2929
import * as utils from "../utils";
@@ -40,7 +40,7 @@ marked.setOptions({
4040
/**
4141
* Command for installing an extension
4242
*/
43-
export const command = new Command("ext:install [extensionName]")
43+
export const command = new Command("ext:install [extensionRef]")
4444
.description(
4545
"add an uploaded extension to firebase.json if [publisherId/extensionId] is provided;" +
4646
"or, add a local extension if [localPath] is provided"
@@ -51,67 +51,80 @@ export const command = new Command("ext:install [extensionName]")
5151
.before(ensureExtensionsApiEnabled)
5252
.before(checkMinRequiredVersion, "extMinVersion")
5353
.before(diagnoseAndFixProject)
54-
.action(async (extensionName: string, options: Options) => {
55-
const projectId = getProjectId(options);
56-
// TODO(b/230598656): Clean up paramsEnvPath after v11 launch.
57-
const paramsEnvPath = "";
58-
let learnMore = false;
59-
if (!extensionName) {
60-
if (options.interactive) {
61-
learnMore = true;
62-
extensionName = await promptForOfficialExtension(
63-
"Which official extension do you wish to install?\n" +
64-
" Select an extension, then press Enter to learn more."
65-
);
66-
} else {
67-
throw new FirebaseError(
68-
`Unable to find published extension '${clc.bold(extensionName)}'. ` +
69-
`Run ${clc.bold(
70-
"firebase ext:install -i"
71-
)} to select from the list of all available published extensions.`
72-
);
73-
}
74-
}
75-
let source;
76-
let extensionVersion;
77-
78-
// TODO(b/220900194): Remove when deprecating old install flow.
79-
// --local doesn't support urlPath so this will become dead codepath.
80-
if (isUrlPath(extensionName)) {
81-
throw new FirebaseError(
82-
`Installing with a source url is no longer supported in the CLI. Please use Firebase Console instead.`
83-
);
84-
}
54+
.action(async (extensionRef: string, options: Options) => {
8555
if (options.local) {
8656
utils.logLabeledWarning(
8757
logPrefix,
8858
"As of firebase-tools@11.0.0, the `--local` flag is no longer required, as it is the default behavior."
8959
);
9060
}
91-
61+
if (!extensionRef) {
62+
throw new FirebaseError(
63+
"Extension ref is required to install. To see a full list of available extensions, go to Extensions Hub (https://extensions.dev/extensions)."
64+
);
65+
}
66+
let source: ExtensionSource | undefined;
67+
let extensionVersion: ExtensionVersion | undefined;
68+
const projectId = getProjectId(options);
9269
// If the user types in a local path (prefixed with ~/, ../, or ./), install from local source.
9370
// Otherwise, treat the input as an extension reference and proceed with reference-based installation.
94-
if (isLocalPath(extensionName)) {
71+
if (isLocalPath(extensionRef)) {
9572
// TODO(b/228444119): Create source should happen at deploy time.
9673
// Should parse spec locally so we don't need project ID.
97-
source = await createSourceFromLocation(needProjectId({ projectId }), extensionName);
98-
await displayExtInfo(extensionName, "", source.spec);
74+
source = await createSourceFromLocation(needProjectId({ projectId }), extensionRef);
75+
await displayExtensionVersionInfo({ spec: source.spec });
9976
void trackGA4("extension_added_to_manifest", {
10077
published: "local",
10178
interactive: options.nonInteractive ? "false" : "true",
10279
});
10380
} else {
104-
extensionName = await canonicalizeRefInput(extensionName);
105-
extensionVersion = await extensionsApi.getExtensionVersion(extensionName);
106-
81+
const extension = await extensionsApi.getExtension(extensionRef);
82+
const ref = refs.parse(extensionRef);
83+
ref.version = await resolveVersion(ref, extension);
84+
const extensionVersionRef = refs.toExtensionVersionRef(ref);
85+
extensionVersion = await extensionsApi.getExtensionVersion(extensionVersionRef);
10786
void trackGA4("extension_added_to_manifest", {
10887
published: extensionVersion.listing?.state === "APPROVED" ? "published" : "uploaded",
10988
interactive: options.nonInteractive ? "false" : "true",
11089
});
111-
await infoExtensionVersion({
112-
extensionName,
90+
await displayExtensionVersionInfo({
91+
spec: extensionVersion.spec,
11392
extensionVersion,
93+
latestApprovedVersion: extension.latestApprovedVersion,
94+
latestVersion: extension.latestVersion,
11495
});
96+
if (extensionVersion.state === "DEPRECATED") {
97+
throw new FirebaseError(
98+
`Extension version ${clc.bold(
99+
extensionVersionRef
100+
)} is deprecated and cannot be installed. To install the latest non-deprecated version, omit the version in the extension ref.`
101+
);
102+
}
103+
logger.info();
104+
// Check if selected version is older than the latest approved version, or the latest version only if there is no approved version.
105+
if (
106+
(extension.latestApprovedVersion &&
107+
semver.gt(extension.latestApprovedVersion, extensionVersion.spec.version)) ||
108+
(!extension.latestApprovedVersion &&
109+
extension.latestVersion &&
110+
semver.gt(extension.latestVersion, extensionVersion.spec.version))
111+
) {
112+
const version = extension.latestApprovedVersion || extension.latestVersion;
113+
logger.info(
114+
`You are about to install extension version ${clc.bold(
115+
extensionVersion.spec.version
116+
)} which is older than the latest ${
117+
extension.latestApprovedVersion ? "accepted version" : "version"
118+
} ${clc.bold(version!)}.`
119+
);
120+
}
121+
}
122+
if (!source && !extensionVersion) {
123+
throw new FirebaseError(
124+
`Failed to parse ${clc.bold(
125+
extensionRef
126+
)} as an extension version or a path to a local extension. Please specify a valid reference.`
127+
);
115128
}
116129
if (
117130
!(await confirm({
@@ -122,33 +135,18 @@ export const command = new Command("ext:install [extensionName]")
122135
) {
123136
return;
124137
}
125-
if (!source && !extensionVersion) {
126-
throw new FirebaseError(
127-
"Could not find a source. Please specify a valid source to continue."
128-
);
129-
}
130138
const spec = source?.spec ?? extensionVersion?.spec;
131139
if (!spec) {
132140
throw new FirebaseError(
133141
`Could not find the extension.yaml for extension '${clc.bold(
134-
extensionName
142+
extensionRef
135143
)}'. Please make sure this is a valid extension and try again.`
136144
);
137145
}
138-
if (learnMore) {
139-
utils.logLabeledBullet(
140-
logPrefix,
141-
`You selected: ${clc.bold(spec.displayName || "")}.\n` +
142-
`${spec.description}\n` +
143-
`View details: https://firebase.google.com/products/extensions/${spec.name}\n`
144-
);
145-
}
146-
147146
try {
148147
return installToManifest({
149-
paramsEnvPath,
150148
projectId,
151-
extensionName,
149+
extensionRef,
152150
source,
153151
extVersion: extensionVersion,
154152
nonInteractive: options.nonInteractive,
@@ -164,18 +162,9 @@ export const command = new Command("ext:install [extensionName]")
164162
}
165163
});
166164

167-
async function infoExtensionVersion(args: {
168-
extensionName: string;
169-
extensionVersion: ExtensionVersion;
170-
}): Promise<void> {
171-
const ref = refs.parse(args.extensionName);
172-
await displayExtInfo(args.extensionName, ref.publisherId, args.extensionVersion.spec, true);
173-
}
174-
175165
interface InstallExtensionOptions {
176-
paramsEnvPath?: string;
177166
projectId?: string;
178-
extensionName: string;
167+
extensionRef: string;
179168
source?: ExtensionSource;
180169
extVersion?: ExtensionVersion;
181170
nonInteractive: boolean;
@@ -189,14 +178,13 @@ interface InstallExtensionOptions {
189178
* @param options
190179
*/
191180
async function installToManifest(options: InstallExtensionOptions): Promise<void> {
192-
const { projectId, extensionName, extVersion, source, paramsEnvPath, nonInteractive, force } =
193-
options;
194-
const isLocalSource = isLocalPath(extensionName);
181+
const { projectId, extensionRef, extVersion, source, nonInteractive, force } = options;
182+
const isLocalSource = isLocalPath(extensionRef);
195183

196184
const spec = extVersion?.spec ?? source?.spec;
197185
if (!spec) {
198186
throw new FirebaseError(
199-
`Could not find the extension.yaml for ${extensionName}. Please make sure this is a valid extension and try again.`
187+
`Could not find the extension.yaml for ${extensionRef}. Please make sure this is a valid extension and try again.`
200188
);
201189
}
202190

@@ -215,7 +203,6 @@ async function installToManifest(options: InstallExtensionOptions): Promise<void
215203
projectId,
216204
paramSpecs: (spec.params ?? []).concat(spec.systemParams ?? []),
217205
nonInteractive,
218-
paramsEnvPath,
219206
instanceId,
220207
});
221208
const eventsConfig = spec.events
@@ -237,7 +224,7 @@ async function installToManifest(options: InstallExtensionOptions): Promise<void
237224
{
238225
instanceId,
239226
ref: !isLocalSource ? ref : undefined,
240-
localPath: isLocalSource ? extensionName : undefined,
227+
localPath: isLocalSource ? extensionRef : undefined,
241228
params: paramBindingOptions,
242229
extensionSpec: spec,
243230
},

src/commands/ext-update.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,6 @@ export const command = new Command("ext:update <extensionInstanceId> [updateSour
117117
newSpec: newExtensionVersion.spec,
118118
currentParams: oldParamValues,
119119
projectId,
120-
// TODO(b/230598656): Clean up paramsEnvPath after v11 launch.
121-
paramsEnvPath: "",
122120
nonInteractive: options.nonInteractive,
123121
instanceId,
124122
});

src/deploy/extensions/planner.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -207,32 +207,33 @@ export async function want(args: {
207207
}
208208

209209
/**
210-
* resolveVersion resolves a semver string to the max matching version.
211-
* Exported for testing.
212-
* @param publisherId
213-
* @param extensionId
214-
* @param version a semver or semver range
210+
* Resolves a semver string to the max matching version. If no version is specified,
211+
* it will default to the extension's latest approved version if set, otherwise to the latest version.
212+
*
213+
* @param ref the extension version ref
214+
* @param extension the extension (optional)
215215
*/
216-
export async function resolveVersion(ref: refs.Ref): Promise<string> {
216+
export async function resolveVersion(ref: refs.Ref, extension?: Extension): Promise<string> {
217217
const extensionRef = refs.toExtensionRef(ref);
218-
const extension = await extensionsApi.getExtension(extensionRef);
219-
if (!ref.version || ref.version === "latest-approved") {
220-
if (!extension.latestApprovedVersion) {
218+
if (!ref.version && extension?.latestApprovedVersion) {
219+
return extension.latestApprovedVersion;
220+
}
221+
if (ref.version === "latest-approved") {
222+
if (!extension?.latestApprovedVersion) {
221223
throw new FirebaseError(
222224
`${extensionRef} has not been published to Extensions Hub (https://extensions.dev). To install it, you must specify the version you want to install.`
223225
);
224226
}
225227
return extension.latestApprovedVersion;
226228
}
227-
if (ref.version === "latest") {
228-
if (!extension.latestVersion) {
229+
if (!ref.version || ref.version === "latest") {
230+
if (!extension?.latestVersion) {
229231
throw new FirebaseError(
230232
`${extensionRef} has no stable non-deprecated versions. If you wish to install a prerelease version, you must specify the version you want to install.`
231233
);
232234
}
233235
return extension.latestVersion;
234236
}
235-
236237
const versions = await extensionsApi.listExtensionVersions(extensionRef, undefined, true);
237238
if (versions.length === 0) {
238239
throw new FirebaseError(`No versions found for ${extensionRef}`);

0 commit comments

Comments
 (0)