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

Adding extensions to firebase init #5701

Merged
merged 5 commits into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 7 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
- Adds new commands for provisioning and managing Firestore databases: (#5616)
- firestore:databases:list
- firestore:databases:create
- firestore:databases:get
- firestore:databases:update
- firestore:databases:delete
- firestore:locations
- firestore:databases:list
- firestore:databases:create
- firestore:databases:get
- firestore:databases:update
- firestore:databases:delete
- firestore:locations
- Adds `extensions` as an option in `firebase init`.
- Relaxed repo URI validation in ext:dev:publish (#5698).
2 changes: 1 addition & 1 deletion src/commands/ext-install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import * as refs from "../extensions/refs";
import { displayWarningPrompts } from "../extensions/warnings";
import * as paramHelper from "../extensions/paramHelper";
import {
confirm,
createSourceFromLocation,
ensureExtensionsApiEnabled,
logPrefix,
Expand All @@ -25,6 +24,7 @@ import {
isLocalPath,
canonicalizeRefInput,
} from "../extensions/extensionsHelper";
import { confirm } from "../prompt";
import { getRandomString } from "../extensions/utils";
import { requirePermissions } from "../requirePermissions";
import * as utils from "../utils";
Expand Down
2 changes: 1 addition & 1 deletion src/commands/ext-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import {
logPrefix,
getSourceOrigin,
SourceOrigin,
confirm,
diagnoseAndFixProject,
isLocalPath,
} from "../extensions/extensionsHelper";
import * as paramHelper from "../extensions/paramHelper";
import { inferUpdateSource } from "../extensions/updateHelper";
import * as refs from "../extensions/refs";
import { getProjectId } from "../projectUtils";
import { confirm } from "../prompt";
import { requirePermissions } from "../requirePermissions";
import * as utils from "../utils";
import * as experiments from "../experiments";
Expand Down
5 changes: 5 additions & 0 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ const choices = [
name: "Remote Config: Configure a template file for Remote Config",
checked: false,
},
{
value: "extensions",
name: "Extensions: Set up an empty Extensions manifest",
checked: false,
},
];
const featureNames = choices.map((choice) => choice.value);

Expand Down
24 changes: 1 addition & 23 deletions src/extensions/extensionsHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
import { Extension, ExtensionSource, ExtensionSpec, ExtensionVersion, Param } from "./types";
import * as refs from "./refs";
import { EXTENSIONS_SPEC_FILE, readFile, getLocalExtensionSpec } from "./localHelper";
import { promptOnce } from "../prompt";
import { confirm, promptOnce } from "../prompt";
import { logger } from "../logger";
import { envOverride } from "../utils";
import { getLocalChangelog } from "./change-log";
Expand Down Expand Up @@ -1037,28 +1037,6 @@ export function getSourceOrigin(sourceOrVersion: string): SourceOrigin {
);
}

/**
* Confirm if the user wants to continue
*/
export async function confirm(args: {
nonInteractive?: boolean;
force?: boolean;
default?: boolean;
}): Promise<boolean> {
if (!args.nonInteractive && !args.force) {
const message = `Do you wish to continue?`;
return await promptOnce({
type: "confirm",
message,
default: args.default,
});
} else if (args.nonInteractive && !args.force) {
throw new FirebaseError("Pass the --force flag to use this command in non-interactive mode");
} else {
return true;
}
}

export async function diagnoseAndFixProject(options: any): Promise<void> {
const projectId = getProjectId(options);
if (!projectId) {
Expand Down
31 changes: 29 additions & 2 deletions src/extensions/manifest.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import * as clc from "colorette";
import * as path from "path";
import * as fs from "fs-extra";

import * as refs from "./refs";
import { Config } from "../config";
import { getExtensionSpec, ManifestInstanceSpec } from "../deploy/extensions/planner";
import { logger } from "../logger";
import { promptOnce } from "../prompt";
import { confirm, promptOnce } from "../prompt";
import { readEnvFile } from "./paramHelper";
import { FirebaseError } from "../error";
import * as utils from "../utils";
Expand Down Expand Up @@ -60,6 +62,31 @@ export async function writeToManifest(
await writeLocalSecrets(specs, config, options.force);
}

export async function writeEmptyManifest(
config: Config,
options: { nonInteractive: boolean; force: boolean }
): Promise<void> {
if (!fs.existsSync(config.path("extensions"))) {
fs.mkdirSync(config.path("extensions"));
}
if (config.has("extensions") && Object.keys(config.get("extensions")).length) {
const currentExtensions = Object.entries(config.get("extensions"))
.map((i) => `${i[0]}: ${i[1]}`)
.join("\n\t");
if (
!(await confirm({
message: `firebase.json already contains extensions:\n${currentExtensions}\nWould you like to overwrite them?`,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested copy: "firebase.json already contains extensions:\n${currentExtensions}\nThese extensions will be removed from the manifest if you choose to re-initialize. Would you like to overwrite them?"

Just to be extra clear what overwrite means.

nonInteractive: options.nonInteractive,
force: options.force,
default: false,
}))
) {
return;
}
}
config.set("extensions", {});
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't delete the .env files for the extensions. Rather than trying to overwrite it, can we just throw if something already exists to keep things simple?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a little awkward as is, but intentional. I'd like init to be idempotent so that it can more safely be used into scripts - init is meant as an interactive command, but some folks still try to use it non interactively.

I considered cleaning up the .env files, with a prompt, only in interactive mode, but leaving them doesn't have any ill effects.

}

/**
* Write the secrets in a list of ManifestInstanceSpec into extensions/{instance-id}.secret.local.
*
Expand Down Expand Up @@ -173,7 +200,7 @@ export function getInstanceRef(instanceId: string, config: Config): refs.Ref {
return refs.parse(source);
}

function writeExtensionsToFirebaseJson(specs: ManifestInstanceSpec[], config: Config): void {
export function writeExtensionsToFirebaseJson(specs: ManifestInstanceSpec[], config: Config): void {
const extensions = config.get("extensions", {});
for (const s of specs) {
let target;
Expand Down
17 changes: 17 additions & 0 deletions src/init/features/extensions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { requirePermissions } from "../../../requirePermissions";
import { Options } from "../../../options";
import { ensure } from "../../../ensureApiEnabled";
import { Config } from "../../../config";
import * as manifest from "../../../extensions/manifest";

/**
* Set up a new firebase project for extensions.
*/
export async function doSetup(setup: any, config: Config, options: Options): Promise<any> {
const projectId = setup?.rcfile?.projects?.default;
if (projectId) {
await requirePermissions({ ...options, project: projectId });
await Promise.all([ensure(projectId, "firebaseextensions.googleapis.com", "unused", true)]);
}
return manifest.writeEmptyManifest(config, options);
}
1 change: 1 addition & 0 deletions src/init/features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { doSetup as functions } from "./functions";
export { doSetup as hosting } from "./hosting";
export { doSetup as storage } from "./storage";
export { doSetup as emulators } from "./emulators";
export { doSetup as extensions } from "./extensions";
// always runs, sets up .firebaserc
export { doSetup as project } from "./project";
export { doSetup as remoteconfig } from "./remoteconfig";
Expand Down
1 change: 1 addition & 0 deletions src/init/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const featureFns = new Map<string, (setup: any, config: any, options?: any) => P
["hosting", features.hosting],
["storage", features.storage],
["emulators", features.emulators],
["extensions", features.extensions],
["project", features.project], // always runs, sets up .firebaserc
["remoteconfig", features.remoteconfig],
["hosting:github", features.hostingGithub],
Expand Down
23 changes: 23 additions & 0 deletions src/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,26 @@ export async function promptOnce<A>(question: Question, options: Options = {}):
await prompt(options, [question]);
return options[question.name];
}

/**
* Confirm if the user wants to continue
*/
export async function confirm(args: {
nonInteractive?: boolean;
force?: boolean;
default?: boolean;
message?: string;
}): Promise<boolean> {
if (!args.nonInteractive && !args.force) {
const message = args.message ?? `Do you wish to continue?`;
return await promptOnce({
type: "confirm",
message,
default: args.default,
});
} else if (args.nonInteractive && !args.force) {
throw new FirebaseError("Pass the --force flag to use this command in non-interactive mode");
} else {
return true;
}
}