Skip to content

Commit 5baa1d5

Browse files
authored
Extensions Registry Alpha (#2881)
1 parent 1e30c6b commit 5baa1d5

27 files changed

+3134
-666
lines changed

src/commands/ext-dev-list.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import * as clc from "cli-color";
2+
import Table = require("cli-table");
3+
import * as _ from "lodash";
4+
5+
import { Command } from "../command";
6+
import { logPrefix } from "../extensions/extensionsHelper";
7+
import { FirebaseError } from "../error";
8+
import * as utils from "../utils";
9+
import * as extensionsUtils from "../extensions/utils";
10+
import { listExtensions } from "../extensions/extensionsApi";
11+
import * as logger from "../logger";
12+
import { requireAuth } from "../requireAuth";
13+
14+
/**
15+
* List all published extensions associated with this publisher ID.
16+
*/
17+
export default new Command("ext:dev:list <publisherId>")
18+
.description("list all published extensions associated with this publisher ID")
19+
.before(requireAuth)
20+
.action(async (publisherId: string) => {
21+
let extensions;
22+
try {
23+
extensions = await listExtensions(publisherId);
24+
} catch (err) {
25+
throw new FirebaseError(err);
26+
}
27+
28+
if (extensions.length < 1) {
29+
throw new FirebaseError(
30+
`There are no published extensions associated with publisher ID ${clc.bold(
31+
publisherId
32+
)}. This could happen for two reasons:\n` +
33+
" - The publisher ID doesn't exist or could be misspelled\n" +
34+
" - This publisher has not published any extensions\n\n" +
35+
"If you are expecting some extensions to appear, please make sure you have the correct publisher ID and try again."
36+
);
37+
}
38+
39+
const table = new Table({
40+
head: ["Extension ID", "Version", "Published"],
41+
style: { head: ["yellow"] },
42+
});
43+
// Order extensions newest to oldest.
44+
const sorted = _.sortBy(extensions, "createTime", "asc").reverse();
45+
sorted.forEach((extension) => {
46+
table.push([
47+
_.last(extension.ref.split("/")),
48+
extension.latestVersion,
49+
extension.createTime ? extensionsUtils.formatTimestamp(extension.createTime) : "",
50+
]);
51+
});
52+
53+
utils.logLabeledBullet(
54+
logPrefix,
55+
`list of published extensions for publisher ${clc.bold(publisherId)}:`
56+
);
57+
logger.info(table.toString());
58+
return { extensions: sorted };
59+
});

src/commands/ext-dev-publish.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Command } from "../command";
2+
import { publishExtensionVersionFromLocalSource } from "../extensions/extensionsHelper";
3+
import { parseRef } from "../extensions/extensionsApi";
4+
5+
import { findExtensionYaml } from "../extensions/localHelper";
6+
import { requireAuth } from "../requireAuth";
7+
import * as clc from "cli-color";
8+
import { FirebaseError } from "../error";
9+
10+
/**
11+
* Command for publishing an extension version.
12+
*/
13+
export default new Command("ext:dev:publish <extensionRef>")
14+
.description(`publish a new version of an extension`)
15+
.help(
16+
"if you have not previously published a version of this extension, this will " +
17+
"create the extension. If you have previously published a version of this extension, this version must " +
18+
"be greater than previous versions."
19+
)
20+
.before(requireAuth)
21+
.action(async (extensionRef: string) => {
22+
const { publisherId, extensionId, version } = parseRef(extensionRef);
23+
if (version) {
24+
throw new FirebaseError(
25+
`The input extension reference must be of the format ${clc.bold(
26+
"<publisherId>/<extensionId>"
27+
)}. Version should not be supplied and will be inferred directly from extension.yaml. Please increment the version in extension.yaml if you would like to bump/specify a version.`
28+
);
29+
}
30+
if (!publisherId || !extensionId) {
31+
throw new FirebaseError(
32+
`Error parsing publisher ID and extension ID from extension reference '${clc.bold(
33+
extensionRef
34+
)}'. Please use the format '${clc.bold("<publisherId>/<extensionId>")}'.`
35+
);
36+
}
37+
const extensionYamlDirectory = findExtensionYaml(process.cwd());
38+
const res = await publishExtensionVersionFromLocalSource(
39+
publisherId,
40+
extensionId,
41+
extensionYamlDirectory
42+
);
43+
return res;
44+
});

src/commands/ext-dev-register.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import * as clc from "cli-color";
2+
import * as marked from "marked";
3+
4+
import { Command } from "../command";
5+
import { registerPublisherProfile } from "../extensions/extensionsApi";
6+
import * as getProjectId from "../getProjectId";
7+
import { promptOnce } from "../prompt";
8+
import { ensureExtensionsApiEnabled, logPrefix } from "../extensions/extensionsHelper";
9+
import { promptForPublisherTOS } from "../extensions/askUserForConsent";
10+
import { requirePermissions } from "../requirePermissions";
11+
import { FirebaseError } from "../error";
12+
import * as utils from "../utils";
13+
14+
/**
15+
* Register a publisher ID; run this before publishing any extensions.
16+
*/
17+
export default new Command("ext:dev:register")
18+
.description("register a publisher ID; run this before publishing your first extension.")
19+
// temporary until registry-specific permissions are available
20+
.before(requirePermissions, ["firebaseextensions.sources.create"])
21+
.before(ensureExtensionsApiEnabled)
22+
.action(async (options: any) => {
23+
await promptForPublisherTOS();
24+
const projectId = getProjectId(options, false);
25+
const msg =
26+
"What would you like to register as your publisher ID? " +
27+
"This value identifies you in Firebase's registry of extensions as the author of your extensions. " +
28+
"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" +
29+
"You can only do this once for each project.";
30+
const publisherId = await promptOnce({
31+
name: "publisherId",
32+
type: "input",
33+
message: msg,
34+
default: projectId,
35+
});
36+
try {
37+
await registerPublisherProfile(projectId, publisherId);
38+
} catch (err) {
39+
if (err.status === 409) {
40+
const error =
41+
`Couldn't register the publisher ID '${clc.bold(publisherId)}' to the project '${clc.bold(
42+
projectId
43+
)}'.` +
44+
" This can happen for either of two reasons:\n\n" +
45+
` - Publisher ID '${clc.bold(publisherId)}' is registered to another project\n` +
46+
` - Project '${clc.bold(projectId)}' already has a publisher ID\n\n` +
47+
` Try again with a unique publisher ID or a new project. If your business’s name has been registered to another project, contact Firebase support ${marked(
48+
"(https://firebase.google.com/support/troubleshooter/contact)."
49+
)}`;
50+
throw new FirebaseError(error, { exit: 1 });
51+
}
52+
throw new FirebaseError(
53+
`Failed to register publisher ID ${clc.bold(publisherId)} for project ${clc.bold(
54+
projectId
55+
)}: ${err.message}`,
56+
{ exit: 1 }
57+
);
58+
}
59+
return utils.logLabeledSuccess(
60+
logPrefix,
61+
`Publisher ID '${clc.bold(publisherId)}' has been registered to project ${clc.bold(
62+
projectId
63+
)}`
64+
);
65+
});

src/commands/ext-dev-unpublish.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Command } from "../command";
2+
import { logPrefix } from "../extensions/extensionsHelper";
3+
import { unpublishExtension, parseRef, getExtension } from "../extensions/extensionsApi";
4+
import * as utils from "../utils";
5+
import { promptOnce } from "../prompt";
6+
import * as clc from "cli-color";
7+
import { requireAuth } from "../requireAuth";
8+
import { FirebaseError } from "../error";
9+
10+
module.exports = new Command("ext:dev:unpublish <extensionRef>")
11+
.description("unpublish an extension")
12+
.help(
13+
"use this command to unpublish an extension, and make it unavailable for developers to install or reconfigure. " +
14+
"Specify the extension you want to unpublish using the format '<publisherId>/<extensionId>."
15+
)
16+
.before(requireAuth)
17+
.action(async (extensionRef: string) => {
18+
const { publisherId, extensionId, version } = parseRef(extensionRef);
19+
const message =
20+
"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.";
21+
utils.logLabeledWarning(logPrefix, message);
22+
if (version) {
23+
throw new FirebaseError(
24+
`Unpublishing a single version is not currently supported. You can only unpublish ${clc.bold(
25+
"ALL versions"
26+
)} of an extension. To unpublish all versions, please remove the version from the reference.`
27+
);
28+
}
29+
await getExtension(extensionRef);
30+
const consent = await comfirmUnpublish(publisherId, extensionId);
31+
if (!consent) {
32+
throw new FirebaseError("unpublishing cancelled.");
33+
}
34+
await unpublishExtension(extensionRef);
35+
utils.logLabeledSuccess(logPrefix, "successfully unpublished all versions of this extension.");
36+
});
37+
38+
async function comfirmUnpublish(publisherId: string, extensionId: string): Promise<string> {
39+
const message = `You are about to unpublish ALL versions of ${clc.green(
40+
`${publisherId}/${extensionId}`
41+
)}.\nDo you wish to continue? `;
42+
return await promptOnce({
43+
type: "confirm",
44+
message,
45+
default: false, // Force users to explicitly type 'yes'
46+
});
47+
}

0 commit comments

Comments
 (0)