Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions firebase-vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## NEXT

- [Added] Refine / Generate Operation Code Lens.
- [Added] Support run "firebase init" without login and project.

## 1.8.0

Expand Down
1 change: 1 addition & 0 deletions firebase-vscode/src/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 5 additions & 5 deletions firebase-vscode/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 });
});

Expand Down
15 changes: 7 additions & 8 deletions firebase-vscode/webviews/SidebarApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -277,14 +277,13 @@ export function SidebarApp() {
<ConfigPicker />
</PanelSection>

{user.value &&
(isInitialized.value ? (
<Content />
) : (
<PanelSection isLast={true}>
<Welcome />
</PanelSection>
))}
{isInitialized.value ? (
<Content />
) : (
<PanelSection isLast={true}>
<Welcome />
</PanelSection>
)}
</App>
);
}
2 changes: 0 additions & 2 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);

/**
Expand Down
44 changes: 3 additions & 41 deletions src/init/features/project.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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");
Expand Down Expand Up @@ -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;
});
Copy link
Contributor Author

@fredzqm fredzqm Oct 3, 2025

Choose a reason for hiding this comment

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

Still work. The validation is at prompt layer.

Image

});

describe('with "Add Firebase resources to GCP project" option', () => {
Expand All @@ -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;
});
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 still works. I moved the validation into promptAvailableProjectId.

});

describe(`with "Don't set up a default project" option`, () => {
Expand All @@ -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 () => {
Expand Down
194 changes: 83 additions & 111 deletions src/init/features/project.ts
Original file line number Diff line number Diff line change
@@ -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<FirebaseProjectMetadata> {
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<FirebaseProjectMetadata> {
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<FirebaseProjectMetadata | undefined> {
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.
Expand All @@ -106,52 +36,94 @@ export async function doSetup(setup: any, config: any, options: any): Promise<vo
logger.info(
`You can create multiple project aliases by running ${clc.bold("firebase use --add")}, `,
);
logger.info(`but for now we'll just set up a default project.`);
logger.info();

if (options.project) {
// If the user presented a project with `--project`, try to fetch that project.
if (Constants.isDemoProject(options.project)) {
logger.info(`Skipping Firebase project setup because a demo project is provided`);
return;
}
await requireAuth(options);
await usingProject(setup, config, options.project);
return;
}
const projectFromRcFile = setup.rcfile?.projects?.default;
if (projectFromRcFile && !options.project) {
utils.logBullet(`.firebaserc already has a default project, using ${projectFromRcFile}.`);
// we still need to get project info in case user wants to init firestore or storage, which
// require a resource location:
const rcProject: FirebaseProjectMetadata = await getFirebaseProject(projectFromRcFile);
setup.projectId = rcProject.projectId;
setup.projectLocation = rcProject?.resources?.locationId;
if (projectFromRcFile) {
await requireAuth(options);
await usingProject(setup, config, projectFromRcFile as string, ".firebaserc");
return;
}

let projectMetaData;
if (options.project) {
// If the user presented a project with `--project`, try to fetch that project.
logger.debug(`Using project from CLI flag: ${options.project}`);
projectMetaData = await getFirebaseProject(options.project);
} else {
const projectEnvVar = utils.envOverride("FIREBASE_PROJECT", "");
const projectEnvVar = utils.envOverride("FIREBASE_PROJECT", "");
if (projectEnvVar) {
// If env var $FIREBASE_PROJECT is set, try to fetch that project.
// This is used in some shell scripts e.g. under https://firebase.tools/.
if (projectEnvVar) {
logger.debug(`Using project from $FIREBASE_PROJECT: ${projectEnvVar}`);
projectMetaData = await getFirebaseProject(projectEnvVar);
} else {
if (options.nonInteractive) {
logger.info(
"No default project found. Continuing without a project in non interactive mode.",
);
return;
}
projectMetaData = await projectChoicePrompt(options);
if (!projectMetaData) {
return;
}
await requireAuth(options);
await usingProject(setup, config, projectEnvVar, "$FIREBASE_PROJECT");
return;
}
if (options.nonInteractive) {
logger.info("No default project found. Continuing without a project in non interactive mode.");
return;
}

// Prompt users about how to setup a project.
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: {
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<void> {
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<void> {
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);
}
Loading
Loading