-
Notifications
You must be signed in to change notification settings - Fork 1.1k
firebase init
no longer requires login if no project is selected
#9251
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
Changes from 13 commits
fadefbe
b4d470c
61953c4
7b5c744
86ff3a7
8129202
589be1e
c180607
0ef83e2
61c1e65
da9065c
fe8e0f0
4743e37
2dd41c8
b6f7436
edbb6df
63658b4
9e1bff5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
- `firebase init` support a way to skip project setup and login via `--project demo-no-project` (#9251) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
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 @@ | |
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 @@ | |
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 @@ | |
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; | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This still works. I moved the validation into |
||
}); | ||
|
||
describe(`with "Don't set up a default project" option`, () => { | ||
|
@@ -174,13 +135,14 @@ | |
}); | ||
|
||
describe("with defined .firebaserc file", () => { | ||
let options: any; | ||
let setup: any; | ||
|
||
beforeEach(() => { | ||
options = {}; | ||
setup = { config: {}, rcfile: { projects: { default: "my-project-123" } } }; | ||
getProjectStub.onFirstCall().resolves(TEST_FIREBASE_PROJECT); | ||
configstoreSetStub.onFirstCall().resolves(); | ||
}); | ||
|
||
it("should not prompt", async () => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,157 +1,129 @@ | ||
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. | ||
* @param config Configuration for the project. | ||
* @param options Command line options. | ||
*/ | ||
export async function doSetup(setup: any, config: any, options: any): Promise<void> { | ||
Check warning on line 31 in src/init/features/project.ts
|
||
setup.project = {}; | ||
|
||
logger.info(); | ||
logger.info(`First, let's associate this project directory with a Firebase project.`); | ||
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)) { | ||
Check warning on line 43 in src/init/features/project.ts
|
||
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); | ||
} |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.