diff --git a/CHANGELOG.md b/CHANGELOG.md index 45060095ac8..44dcbdd780e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,2 @@ - Fix Functions MCP log tool to normalize sort order and surface Cloud Logging error details (#9247) +- Fixed an issue where `firebase init` would require log in even when no project is selected. (#9251) diff --git a/firebase-vscode/CHANGELOG.md b/firebase-vscode/CHANGELOG.md index a688cf5bb98..23afe5f27f8 100644 --- a/firebase-vscode/CHANGELOG.md +++ b/firebase-vscode/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT - [Added] Refine / Generate Operation Code Lens. +- [Added] Support run "firebase init" without login and project. ## 1.8.0 diff --git a/firebase-vscode/src/analytics.ts b/firebase-vscode/src/analytics.ts index 20c5aba4fb7..b65d8a0114f 100644 --- a/firebase-vscode/src/analytics.ts +++ b/firebase-vscode/src/analytics.ts @@ -27,6 +27,7 @@ export enum DATA_CONNECT_EVENT_NAME { MOVE_TO_CONNECTOR = "move_to_connector", START_EMULATOR_FROM_EXECUTION = "start_emulator_from_execution", REFUSE_START_EMULATOR_FROM_EXECUTION = "refuse_start_emulator_from_execution", + INIT = "init", INIT_SDK = "init_sdk", INIT_SDK_CLI = "init_sdk_cli", INIT_SDK_CODELENSE = "init_sdk_codelense", diff --git a/firebase-vscode/src/core/index.ts b/firebase-vscode/src/core/index.ts index 1b70508edd1..4032ecf5286 100644 --- a/firebase-vscode/src/core/index.ts +++ b/firebase-vscode/src/core/index.ts @@ -14,7 +14,8 @@ import { upsertFile } from "../data-connect/file-utils"; import { registerWebhooks } from "./webhook"; import { createE2eMockable } from "../utils/test_hooks"; import { runTerminalTask } from "../data-connect/terminal"; -import { AnalyticsLogger } from "../analytics"; +import { AnalyticsLogger, DATA_CONNECT_EVENT_NAME } from "../analytics"; +import { EmulatorHub } from "../../../src/emulator/hub"; export async function registerCore( broker: ExtensionBrokerImpl, @@ -66,10 +67,9 @@ export async function registerCore( ); return; } - const initCommand = currentProjectId.value - ? `${settings.firebasePath} init dataconnect --project ${currentProjectId.value}` - : `${settings.firebasePath} init dataconnect`; - + analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.INIT); + const projectId = currentProjectId.value || EmulatorHub.MISSING_PROJECT_PLACEHOLDER; + const initCommand = `${settings.firebasePath} init dataconnect --project ${projectId}`; initSpy.call("firebase init", initCommand, { focus: true }); }); diff --git a/firebase-vscode/webviews/SidebarApp.tsx b/firebase-vscode/webviews/SidebarApp.tsx index 16389d4e17a..318a34f7f95 100644 --- a/firebase-vscode/webviews/SidebarApp.tsx +++ b/firebase-vscode/webviews/SidebarApp.tsx @@ -277,14 +277,13 @@ export function SidebarApp() { - {user.value && - (isInitialized.value ? ( - - ) : ( - - - - ))} + {isInitialized.value ? ( + + ) : ( + + + + )} ); } diff --git a/src/commands/init.ts b/src/commands/init.ts index 8186241d498..39409efcac6 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -8,7 +8,6 @@ import { getAllAccounts } from "../auth"; import { init, Setup } from "../init"; import { logger } from "../logger"; import { checkbox, confirm } from "../prompt"; -import { requireAuth } from "../requireAuth"; import * as fsutils from "../fsutils"; import * as utils from "../utils"; import { Options } from "../options"; @@ -147,7 +146,6 @@ ${[...featureNames] export const command = new Command("init [feature]") .description("interactively configure the current directory as a Firebase project directory") .help(HELP) - .before(requireAuth) .action(initAction); /** diff --git a/src/init/features/project.spec.ts b/src/init/features/project.spec.ts index 15a305d3c8e..868a5e6afc9 100644 --- a/src/init/features/project.spec.ts +++ b/src/init/features/project.spec.ts @@ -8,6 +8,7 @@ import * as projectManager from "../../management/projects"; import { Config } from "../../config"; import { FirebaseProjectMetadata } from "../../types/project"; import * as promptImport from "../../prompt"; +import * as requireAuthImport from "../../requireAuth"; const TEST_FIREBASE_PROJECT: FirebaseProjectMetadata = { projectId: "my-project-123", @@ -34,6 +35,7 @@ describe("project", () => { let emptyConfig: Config; beforeEach(() => { + sandbox.stub(requireAuthImport, "requireAuth").resolves(); getProjectStub = sandbox.stub(projectManager, "getFirebaseProject"); createFirebaseProjectStub = sandbox.stub(projectManager, "createFirebaseProjectAndLog"); getOrPromptProjectStub = sandbox.stub(projectManager, "getOrPromptProject"); @@ -94,26 +96,6 @@ describe("project", () => { displayName: "my-project", }); }); - - it("should throw if project ID is empty after prompt", async () => { - const options = {}; - const setup = { config: {}, rcfile: {} }; - prompt.select.onFirstCall().resolves("Create a new project"); - prompt.input.resolves(""); - configstoreSetStub.onFirstCall().resolves(); - - let err; - try { - await doSetup(setup, emptyConfig, options); - } catch (e: any) { - err = e; - } - - expect(err.message).to.equal("Project ID cannot be empty"); - expect(prompt.select).to.be.calledOnce; - expect(prompt.input).to.be.calledTwice; - expect(createFirebaseProjectStub).to.be.not.called; - }); }); describe('with "Add Firebase resources to GCP project" option', () => { @@ -137,27 +119,6 @@ describe("project", () => { expect(promptAvailableProjectIdStub).to.be.calledOnce; expect(addFirebaseProjectStub).to.be.calledOnceWith("my-project-123"); }); - - it("should throw if project ID is empty after prompt", async () => { - const options = {}; - const setup = { config: {}, rcfile: {} }; - prompt.select - .onFirstCall() - .resolves("Add Firebase to an existing Google Cloud Platform project"); - promptAvailableProjectIdStub.onFirstCall().resolves(""); - - let err; - try { - await doSetup(setup, emptyConfig, options); - } catch (e: any) { - err = e; - } - - expect(err.message).to.equal("Project ID cannot be empty"); - expect(prompt.select).to.be.calledOnce; - expect(promptAvailableProjectIdStub).to.be.calledOnce; - expect(addFirebaseProjectStub).to.be.not.called; - }); }); describe(`with "Don't set up a default project" option`, () => { @@ -181,6 +142,7 @@ describe("project", () => { options = {}; setup = { config: {}, rcfile: { projects: { default: "my-project-123" } } }; getProjectStub.onFirstCall().resolves(TEST_FIREBASE_PROJECT); + configstoreSetStub.onFirstCall().resolves(); }); it("should not prompt", async () => { diff --git a/src/init/features/project.ts b/src/init/features/project.ts index 762921108e9..1e7b6527f57 100644 --- a/src/init/features/project.ts +++ b/src/init/features/project.ts @@ -1,97 +1,27 @@ import * as clc from "colorette"; import * as _ from "lodash"; -import { FirebaseError } from "../../error"; import { addFirebaseToCloudProjectAndLog, createFirebaseProjectAndLog, getFirebaseProject, - getOrPromptProject, promptAvailableProjectId, promptProjectCreation, + selectProjectInteractively, } from "../../management/projects"; import { FirebaseProjectMetadata } from "../../types/project"; import { logger } from "../../logger"; import * as utils from "../../utils"; import * as prompt from "../../prompt"; -import { Options } from "../../options"; +import { requireAuth } from "../../requireAuth"; +import { Constants } from "../../emulator/constants"; +import { FirebaseError } from "../../error"; const OPTION_NO_PROJECT = "Don't set up a default project"; const OPTION_USE_PROJECT = "Use an existing project"; const OPTION_NEW_PROJECT = "Create a new project"; const OPTION_ADD_FIREBASE = "Add Firebase to an existing Google Cloud Platform project"; -/** - * Used in init flows to keep information about the project - basically - * a shorter version of {@link FirebaseProjectMetadata} with some additional fields. - */ -export interface InitProjectInfo { - id: string; // maps to FirebaseProjectMetadata.projectId - label?: string; - instance?: string; // maps to FirebaseProjectMetadata.resources.realtimeDatabaseInstance - location?: string; // maps to FirebaseProjectMetadata.resources.locationId -} - -function toInitProjectInfo(projectMetaData: FirebaseProjectMetadata): InitProjectInfo { - const { projectId, displayName, resources } = projectMetaData; - return { - id: projectId, - label: `${projectId}` + (displayName ? ` (${displayName})` : ""), - instance: resources?.realtimeDatabaseInstance, - location: resources?.locationId, - }; -} - -async function promptAndCreateNewProject(options: Options): Promise { - utils.logBullet( - "If you want to create a project in a Google Cloud organization or folder, please use " + - `"firebase projects:create" instead, and return to this command when you've created the project.`, - ); - const { projectId, displayName } = await promptProjectCreation(options); - // N.B. This shouldn't be possible because of the validator on the input field, but it - // is being left around in case there's something I don't know. - if (!projectId) { - throw new FirebaseError("Project ID cannot be empty"); - } - - return await createFirebaseProjectAndLog(projectId, { displayName }); -} - -async function promptAndAddFirebaseToCloudProject(): Promise { - const projectId = await promptAvailableProjectId(); - if (!projectId) { - // N.B. This shouldn't be possible because of the validator on the input field, but it - // is being left around in case there's something I don't know. - throw new FirebaseError("Project ID cannot be empty"); - } - return await addFirebaseToCloudProjectAndLog(projectId); -} - -/** - * Prompt the user about how they would like to select a project. - * @param options the Firebase CLI options object. - * @return the project metadata, or undefined if no project was selected. - */ -async function projectChoicePrompt(options: any): Promise { - const choices = [OPTION_USE_PROJECT, OPTION_NEW_PROJECT, OPTION_ADD_FIREBASE, OPTION_NO_PROJECT]; - const projectSetupOption: string = await prompt.select<(typeof choices)[number]>({ - message: "Please select an option:", - choices, - }); - - switch (projectSetupOption) { - case OPTION_USE_PROJECT: - return getOrPromptProject(options); - case OPTION_NEW_PROJECT: - return promptAndCreateNewProject(options); - case OPTION_ADD_FIREBASE: - return promptAndAddFirebaseToCloudProject(); - default: - // Do nothing if user chooses NO_PROJECT - return; - } -} - /** * Sets up the default project if provided and writes .firebaserc file. * @param setup A helper object to use for the rest of the init features. @@ -106,52 +36,94 @@ export async function doSetup(setup: any, config: any, options: any): Promise({ + message: "Please select an option:", + choices, + }); + switch (projectSetupOption) { + case OPTION_USE_PROJECT: { + await requireAuth(options); + const pm = await selectProjectInteractively(); + return await usingProjectMetadata(setup, config, pm); + } + case OPTION_NEW_PROJECT: { + utils.logBullet( + "If you want to create a project in a Google Cloud organization or folder, please use " + + `"firebase projects:create" instead, and return to this command when you've created the project.`, + ); + await requireAuth(options); + const { projectId, displayName } = await promptProjectCreation(options); + const pm = await createFirebaseProjectAndLog(projectId, { displayName }); + return await usingProjectMetadata(setup, config, pm); + } + case OPTION_ADD_FIREBASE: { + await requireAuth(options); + const pm = await addFirebaseToCloudProjectAndLog(await promptAvailableProjectId()); + return await usingProjectMetadata(setup, config, pm); } + default: + // Do nothing if user chooses NO_PROJECT + return; } +} - const projectInfo = toInitProjectInfo(projectMetaData); - utils.logBullet(`Using project ${projectInfo.label}`); +async function usingProject( + setup: any, + config: any, + projectId: string, + from: string = "", +): Promise { + const pm = await getFirebaseProject(projectId); + const label = `${pm.projectId}` + (pm.displayName ? ` (${pm.displayName})` : ""); + utils.logBullet(`Using project ${label} ${from ? "from ${from}" : ""}.`); + await usingProjectMetadata(setup, config, pm); +} + +async function usingProjectMetadata( + setup: any, + config: any, + pm: FirebaseProjectMetadata, +): Promise { + if (!pm) { + throw new FirebaseError("null FirebaseProjectMetadata"); + } // write "default" alias and activate it immediately - _.set(setup.rcfile, "projects.default", projectInfo.id); - setup.projectId = projectInfo.id; - setup.instance = projectInfo.instance; - setup.projectLocation = projectInfo.location; - utils.makeActiveProject(config.projectDir, projectInfo.id); + _.set(setup.rcfile, "projects.default", pm.projectId); + setup.projectId = pm.projectId; + setup.instance = pm.resources?.realtimeDatabaseInstance; + setup.projectLocation = pm.resources?.locationId; + utils.makeActiveProject(config.projectDir, pm.projectId); } diff --git a/src/management/projects.ts b/src/management/projects.ts index a45ea60fd09..65b465aa150 100644 --- a/src/management/projects.ts +++ b/src/management/projects.ts @@ -11,6 +11,7 @@ import * as utils from "../utils"; import { FirebaseProjectMetadata, CloudProjectInfo, ProjectPage } from "../types/project"; import { bestEffortEnsure } from "../ensureApiEnabled"; import { Options } from "../options"; +import { Constants } from "../emulator/constants"; const TIMEOUT_MILLIS = 30000; const MAXIMUM_PROMPT_LIST = 100; @@ -46,6 +47,9 @@ export async function promptProjectCreation( } else if (projectId.length > 30) { return "Project ID cannot be longer than 30 characters"; } + if (Constants.isDemoProject(projectId)) { + return "Project ID cannot starts with demo-"; + } try { // Best effort. We should still allow project creation even if this fails. @@ -172,7 +176,7 @@ export async function getOrPromptProject( return selectProjectInteractively(); } -async function selectProjectInteractively( +export async function selectProjectInteractively( pageSize: number = MAXIMUM_PROMPT_LIST, ): Promise { const { projects, nextPageToken } = await getFirebaseProjectPage(pageSize); @@ -182,15 +186,20 @@ async function selectProjectInteractively( if (nextPageToken) { // Prompt user for project ID if we can't list all projects in 1 page logger.debug(`Found more than ${projects.length} projects, selecting via prompt`); - return selectProjectByPrompting(); + return await getFirebaseProject(await selectProjectByPrompting()); } return selectProjectFromList(projects); } -async function selectProjectByPrompting(): Promise { +async function selectProjectByPrompting(): Promise { const projectId = await prompt.input("Please input the project ID you would like to use:"); - - return await getFirebaseProject(projectId); + if (!projectId) { + throw new FirebaseError("Project ID cannot be empty"); + } + if (Constants.isDemoProject(projectId)) { + throw new FirebaseError("Project ID cannot starts with demo-"); + } + return projectId; } /** @@ -251,10 +260,9 @@ export async function promptAvailableProjectId(): Promise { } if (nextPageToken) { - // Prompt for project ID if we can't list all projects in 1 page - return await prompt.input( - "Please input the ID of the Google Cloud Project you would like to add Firebase:", - ); + // Prompt user for project ID if we can't list all projects in 1 page + logger.debug(`Found more than ${projects.length} projects, selecting via prompt`); + return await selectProjectByPrompting(); } else { const choices = projects .filter((p: CloudProjectInfo) => !!p)