diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d..41d99ba0619 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- `firebase emulator:start` use a default project if no project can be found. (#9072) diff --git a/firebase-vscode/CHANGELOG.md b/firebase-vscode/CHANGELOG.md index 27bc4c80acb..a1967843151 100644 --- a/firebase-vscode/CHANGELOG.md +++ b/firebase-vscode/CHANGELOG.md @@ -1,5 +1,7 @@ ## NEXT +- Fixed the projectless developer experience. There are "error linter", "run (local)" buttons. + ## 1.6.1 - Update internal `firebase-tools` dependency to 14.13.0 diff --git a/firebase-vscode/src/core/emulators.ts b/firebase-vscode/src/core/emulators.ts index 67acbf235dd..6a307c64166 100644 --- a/firebase-vscode/src/core/emulators.ts +++ b/firebase-vscode/src/core/emulators.ts @@ -180,8 +180,7 @@ export class EmulatorsController implements Disposable { private getHubClient(): EmulatorHubClient | undefined { const projectId = firebaseRC.value?.tryReadValue?.projects?.default; - // TODO: think about what to without projectID, in potentially a logged out mode - const hubClient = new EmulatorHubClient(projectId!); + const hubClient = new EmulatorHubClient(projectId); if (hubClient.foundHub()) { return hubClient; } else { diff --git a/firebase-vscode/src/data-connect/code-lens-provider.ts b/firebase-vscode/src/data-connect/code-lens-provider.ts index 5b9ca2722d2..14215b63ebe 100644 --- a/firebase-vscode/src/data-connect/code-lens-provider.ts +++ b/firebase-vscode/src/data-connect/code-lens-provider.ts @@ -71,8 +71,8 @@ export class OperationCodeLensProvider extends ComputedCodeLensProvider { ): vscode.CodeLens[] { // Wait for configs to be loaded and emulator to be running const fdcConfigs = this.watch(dataConnectConfigs)?.tryReadValue; - const rc = this.watch(firebaseRC)?.tryReadValue; - if (!fdcConfigs || !rc) { + const projectId = this.watch(firebaseRC)?.tryReadValue?.projects.default; + if (!fdcConfigs) { return []; } @@ -117,14 +117,16 @@ export class OperationCodeLensProvider extends ComputedCodeLensProvider { }), ); - codeLenses.push( - new vscode.CodeLens(range, { - title: `$(play) Run (Production – Project: ${rc.projects.default})`, - command: "firebase.dataConnect.executeOperation", - tooltip: "Execute the operation (⌘+enter or Ctrl+Enter)", - arguments: [x, operationLocation, InstanceType.PRODUCTION], - }), - ); + if (projectId) { + codeLenses.push( + new vscode.CodeLens(range, { + title: `$(play) Run (Production – Project: ${projectId})`, + command: "firebase.dataConnect.executeOperation", + tooltip: "Execute the operation (⌘+enter or Ctrl+Enter)", + arguments: [x, operationLocation, InstanceType.PRODUCTION], + }), + ); + } } } } @@ -201,8 +203,7 @@ export class ConfigureSdkCodeLensProvider extends ComputedCodeLensProvider { ): vscode.CodeLens[] { // Wait for configs to be loaded const fdcConfigs = this.watch(dataConnectConfigs)?.tryReadValue; - const rc = this.watch(firebaseRC)?.tryReadValue; - if (!fdcConfigs || !rc) { + if (!fdcConfigs) { return []; } diff --git a/firebase-vscode/src/data-connect/config.ts b/firebase-vscode/src/data-connect/config.ts index 73014408f12..548e2927aab 100644 --- a/firebase-vscode/src/data-connect/config.ts +++ b/firebase-vscode/src/data-connect/config.ts @@ -20,6 +20,7 @@ import { DataConnectMultiple } from "../firebaseConfig"; import path from "path"; import { ExtensionBrokerImpl } from "../extension-broker"; import * as fs from "fs"; +import { EmulatorHub } from "../emulator/hub"; export * from "../core/config"; @@ -305,26 +306,37 @@ export class ResolvedDataConnectConfig { export class ResolvedDataConnectConfigs { constructor(readonly values: DeepReadOnly) {} - get serviceIds() { + get serviceIds(): string[] { return this.values.map((config) => config.value.serviceId); } - get allConnectors() { + get allConnectors(): ResolvedConnectorYaml[] { return this.values.flatMap((dc) => dc.resolvedConnectors); } - findById(serviceId: string) { - return this.values.find((dc) => dc.value.serviceId === serviceId); + findById(serviceId: string): ResolvedDataConnectConfig { + const dc = this.values.find((dc) => dc.value.serviceId === serviceId); + if (!dc) { + throw new Error(`No dataconnect.yaml with serviceId ${serviceId}. Available: ${this.serviceIds.join(", ")}`); + } + return dc; } - findEnclosingServiceForPath(filePath: string) { - return this.values.find((dc) => dc.containsPath(filePath)); + findEnclosingServiceForPath(filePath: string): ResolvedDataConnectConfig { + const dc = this.values.find((dc) => dc.containsPath(filePath)); + if (!dc) { + throw new Error(`No enclosing dataconnect.yaml found for path ${filePath}. Available Paths: ${this.values.map((dc) => dc.path).join(", ")}`); + } + return dc; } - getApiServicePathByPath(projectId: string, path: string) { + getApiServicePathByPath(projectId: string | undefined, path: string): string { const dataConnectConfig = this.findEnclosingServiceForPath(path); const serviceId = dataConnectConfig?.value.serviceId; const locationId = dataConnectConfig?.dataConnectLocation; + // FDC emulator can service multiple services keyed by serviceId. + // ${projectId} and ${locationId} aren't used to resolve emulator service. + projectId = projectId || EmulatorHub.MISSING_PROJECT_PLACEHOLDER; return `projects/${projectId}/locations/${locationId}/services/${serviceId}`; } } diff --git a/firebase-vscode/src/data-connect/execution/execution.ts b/firebase-vscode/src/data-connect/execution/execution.ts index 2167e4026a8..dd3840b2b5d 100644 --- a/firebase-vscode/src/data-connect/execution/execution.ts +++ b/firebase-vscode/src/data-connect/execution/execution.ts @@ -136,7 +136,6 @@ export function registerExecution( const alwaysExecuteMutationsInProduction = "alwaysAllowMutationsInProduction"; - const alwaysStartEmulator = "alwaysStartEmulator"; // notify users that emulator is starting if ( diff --git a/firebase-vscode/src/data-connect/service.ts b/firebase-vscode/src/data-connect/service.ts index 2c517fc63a0..99eafe1d768 100644 --- a/firebase-vscode/src/data-connect/service.ts +++ b/firebase-vscode/src/data-connect/service.ts @@ -52,24 +52,15 @@ export class DataConnectService { private context: ExtensionContext, ) {} - async servicePath(path: string): Promise { + async servicePath(path: string): Promise { const dataConnectConfigsValue = await firstWhereDefined(dataConnectConfigs); // TODO: avoid calling this here and in getApiServicePathByPath - const serviceId = - dataConnectConfigsValue?.tryReadValue?.findEnclosingServiceForPath(path) - ?.value.serviceId; - const projectId = firebaseRC.value?.tryReadValue?.projects?.default; - - if (serviceId === undefined || projectId === undefined) { - return undefined; + const dcs = dataConnectConfigsValue?.tryReadValue; + if (!dcs) { + throw new Error("cannot find dataconnect.yaml in the project"); } - - return ( - dataConnectConfigsValue?.tryReadValue?.getApiServicePathByPath( - projectId, - path, - ) || `projects/p/locations/l/services/${serviceId}` - ); + const projectId = firebaseRC.value?.tryReadValue?.projects?.default; + return dcs?.getApiServicePathByPath(projectId, path); } private async decodeResponse( diff --git a/firebase-vscode/src/data-connect/terminal.ts b/firebase-vscode/src/data-connect/terminal.ts index eefda43b302..495d44e08a8 100644 --- a/firebase-vscode/src/data-connect/terminal.ts +++ b/firebase-vscode/src/data-connect/terminal.ts @@ -92,8 +92,10 @@ export function registerTerminalTasks( const startEmulatorsTask = () => { analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.START_EMULATORS); - let cmd = `${settings.firebasePath} emulators:start --project ${currentProjectId.value}`; - + let cmd = `${settings.firebasePath} emulators:start`; + if (currentProjectId.value) { + cmd += ` --project ${currentProjectId.value}`; + } if (settings.importPath) { cmd += ` --import ${settings.importPath}`; } diff --git a/firebase-vscode/src/data-connect/toolkit.ts b/firebase-vscode/src/data-connect/toolkit.ts index a307dbc5a48..d01b06f7932 100644 --- a/firebase-vscode/src/data-connect/toolkit.ts +++ b/firebase-vscode/src/data-connect/toolkit.ts @@ -1,7 +1,6 @@ import * as vscode from "vscode"; import { ExtensionBrokerImpl } from "../extension-broker"; import { effect } from "@preact/signals-core"; -import { firebaseRC } from "../core/config"; import { dataConnectConfigs, firebaseConfig } from "./config"; import { runDataConnectCompiler } from "./core-compiler"; import { DataConnectToolkitController } from "../../../src/emulator/dataconnectToolkitController"; @@ -19,10 +18,9 @@ export class DataConnectToolkit implements vscode.Disposable { this.subs.push( effect(() => { if (!this.isFDCToolkitRunning()) { - const rc = firebaseRC.value?.tryReadValue; const config = firebaseConfig.value?.tryReadValue; - if (rc && config) { - this.startFDCToolkit("./dataconnect", config, rc).then(() => { + if (config) { + this.startFDCToolkit("./dataconnect", config).then(() => { this.connectToToolkit(); }); } @@ -35,7 +33,7 @@ export class DataConnectToolkit implements vscode.Disposable { } // special function to start FDC emulator with special flags & port - async startFDCToolkit(configDir: string, config: Config, RC: RC) { + async startFDCToolkit(configDir: string, config: Config) { const port = await findOpenPort(DEFAULT_PORT); const settings = getSettings(); @@ -44,7 +42,6 @@ export class DataConnectToolkit implements vscode.Disposable { listen: [{ address: "localhost", port, family: "IPv4" }], config, configDir, - rc: RC, autoconnectToPostgres: false, enable_output_generated_sdk: true, enable_output_schema_extensions: true, diff --git a/src/emulator/controller.spec.ts b/src/emulator/controller.spec.ts index 1469afc8bb0..a8bb3529234 100644 --- a/src/emulator/controller.spec.ts +++ b/src/emulator/controller.spec.ts @@ -47,9 +47,9 @@ describe("EmulatorController", () => { expect(shouldStart(options, Emulators.HUB)).to.be.true; }); - it("should not start the hub if no project is specified", () => { + it("should start the hub even if no project is specified", () => { const options = {} as Options; - expect(shouldStart(options, Emulators.HUB)).to.be.false; + expect(shouldStart(options, Emulators.HUB)).to.be.true; }); it("should start the UI if options.ui is true", () => { @@ -63,10 +63,10 @@ describe("EmulatorController", () => { expect(shouldStart(options, Emulators.UI)).to.be.true; }); - it("should not start the UI if no project is specified", () => { + it("should start the UI even if no project is specified", () => { const options = createMockOptions("firestore", { firestore: {} }); delete options.project; - expect(shouldStart(options, Emulators.UI)).to.be.false; + expect(shouldStart(options, Emulators.UI)).to.be.true; }); it("should not start the UI if no UI-supported emulator is running", () => { diff --git a/src/emulator/controller.ts b/src/emulator/controller.ts index 5cf631d1aae..b50f2679210 100755 --- a/src/emulator/controller.ts +++ b/src/emulator/controller.ts @@ -20,7 +20,7 @@ import { import { Constants, FIND_AVAILBLE_PORT_BY_DEFAULT } from "./constants"; import { EmulatableBackend, FunctionsEmulator } from "./functionsEmulator"; import { FirebaseError } from "../error"; -import { getProjectId, needProjectId, getAliases, needProjectNumber } from "../projectUtils"; +import { getProjectId, getAliases, needProjectNumber } from "../projectUtils"; import * as commandUtils from "./commandUtils"; import { EmulatorHub } from "./hub"; import { ExportMetadata, HubExport } from "./hubExport"; @@ -148,8 +148,8 @@ export function filterEmulatorTargets(options: { only: string; config: any }): E */ export function shouldStart(options: Options, name: Emulators): boolean { if (name === Emulators.HUB) { - // The hub only starts if we know the project ID. - return !!options.project; + // The emulator hub always starts. + return true; } const targets = filterEmulatorTargets(options); const emulatorInTargets = targets.includes(name); @@ -166,9 +166,7 @@ export function shouldStart(options: Options, name: Emulators): boolean { } // Emulator UI only starts if we know the project ID AND at least one // emulator supported by Emulator UI is launching. - return ( - !!options.project && targets.some((target) => EMULATORS_SUPPORTED_BY_UI.includes(target)) - ); + return targets.some((target) => EMULATORS_SUPPORTED_BY_UI.includes(target)); } // Don't start the functions emulator if we can't validate the functions config @@ -310,7 +308,7 @@ export async function startAll( const hubLogger = EmulatorLogger.forEmulator(Emulators.HUB); hubLogger.logLabeled("BULLET", "emulators", `Starting emulators: ${targets.join(", ")}`); - const projectId: string = getProjectId(options) || ""; // TODO: Next breaking change, consider making this fall back to demo project. + const projectId = getProjectId(options) || EmulatorHub.MISSING_PROJECT_PLACEHOLDER; const isDemoProject = Constants.isDemoProject(projectId); if (isDemoProject) { hubLogger.logLabeled( @@ -579,7 +577,6 @@ export async function startAll( } const functionsLogger = EmulatorLogger.forEmulator(Emulators.FUNCTIONS); const functionsAddr = legacyGetFirstAddr(Emulators.FUNCTIONS); - const projectId = needProjectId(options); const inspectFunctions = commandUtils.parseInspectionPort(options); if (inspectFunctions) { @@ -723,16 +720,8 @@ export async function startAll( // undefined in the config defaults to setting single_project_mode. if (singleProjectModeEnabled) { - if (projectId) { - args.single_project_mode = true; - args.single_project_mode_error = false; - } else { - firestoreLogger.logLabeled( - "DEBUG", - "firestore", - "Could not enable single_project_mode: missing projectId.", - ); - } + args.single_project_mode = true; + args.single_project_mode_error = false; } const firestoreEmulator = new FirestoreEmulator(args); @@ -822,14 +811,6 @@ export async function startAll( } if (listenForEmulator.auth) { - if (!projectId) { - throw new FirebaseError( - `Cannot start the ${Constants.description( - Emulators.AUTH, - )} without a project: run 'firebase init' or provide the --project flag`, - ); - } - const authAddr = legacyGetFirstAddr(Emulators.AUTH); const authEmulator = new AuthEmulator({ host: authAddr.host, @@ -851,12 +832,6 @@ export async function startAll( } if (listenForEmulator.pubsub) { - if (!projectId) { - throw new FirebaseError( - "Cannot start the Pub/Sub emulator without a project: run 'firebase init' or provide the --project flag", - ); - } - const pubsubAddr = legacyGetFirstAddr(Emulators.PUBSUB); const pubsubEmulator = new PubsubEmulator({ host: pubsubAddr.host, @@ -882,7 +857,6 @@ export async function startAll( projectId, auto_download: true, configDir: config[0].source, - rc: options.rc, config: options.config, autoconnectToPostgres: true, postgresListen: listenForEmulator["dataconnect.postgres"], @@ -941,7 +915,7 @@ export async function startAll( const storageEmulator = new StorageEmulator({ host: storageAddr.host, port: storageAddr.port, - projectId: projectId, + projectId, rules: getStorageRulesConfig(projectId, options), }); await startEmulator(storageEmulator); @@ -1030,15 +1004,14 @@ export async function startAll( hubLogger.logLabeled( "WARN", "emulators", - "The Emulator UI is not starting, either because none of the running " + - "emulators have a UI component or the Emulator UI cannot " + - "determine the Project ID. Pass the --project flag to specify a project.", + "The Emulator UI is not starting because none of the running " + + "emulators have a UI component.", ); } if (listenForEmulator.ui) { const ui = new EmulatorUI({ - projectId: projectId, + projectId, listen: listenForEmulator[Emulators.UI], }); await startEmulator(ui); @@ -1104,13 +1077,6 @@ function getListenConfig( */ export async function exportEmulatorData(exportPath: string, options: any, initiatedBy: string) { const projectId = options.project; - if (!projectId) { - throw new FirebaseError( - "Could not determine project ID, make sure you're running in a Firebase project directory or add the --project flag.", - { exit: 1 }, - ); - } - const hubClient = new EmulatorHubClient(projectId); if (!hubClient.foundHub()) { throw new FirebaseError( diff --git a/src/emulator/dataconnectEmulator.ts b/src/emulator/dataconnectEmulator.ts index 97e0c47966a..554c8405ce5 100644 --- a/src/emulator/dataconnectEmulator.ts +++ b/src/emulator/dataconnectEmulator.ts @@ -17,7 +17,6 @@ import { import { EmulatorInfo, EmulatorInstance, Emulators, ListenSpec } from "./types"; import { FirebaseError } from "../error"; import { EmulatorLogger } from "./emulatorLogger"; -import { RC } from "../rc"; import { BuildResult, requiresVector } from "../dataconnect/types"; import { listenSpecsToString } from "./portUtils"; import { Client, ClientResponse } from "../apiv2"; @@ -36,7 +35,6 @@ export interface DataConnectEmulatorArgs { listen: ListenSpec[]; configDir: string; auto_download?: boolean; - rc: RC; config: Config; autoconnectToPostgres: boolean; postgresListen?: ListenSpec[]; diff --git a/src/emulator/hub.ts b/src/emulator/hub.ts index 9aa876560c1..4467b919873 100644 --- a/src/emulator/hub.ts +++ b/src/emulator/hub.ts @@ -31,6 +31,7 @@ export interface EmulatorHubArgs { export type GetEmulatorsResponse = Partial>; export class EmulatorHub extends ExpressBasedEmulator { + static MISSING_PROJECT_PLACEHOLDER = "demo-no-project"; static CLI_VERSION = pkg.version; static PATH_EXPORT = "/_admin/export"; static PATH_DISABLE_FUNCTIONS = "/functions/disableBackgroundTriggers"; @@ -43,7 +44,7 @@ export class EmulatorHub extends ExpressBasedEmulator { * This is useful so that multiple copies of the Firebase CLI can discover * each other. */ - static readLocatorFile(projectId: string): Locator | undefined { + static readLocatorFile(projectId: string | undefined): Locator | undefined { const locatorPath = this.getLocatorFilePath(projectId); if (!fs.existsSync(locatorPath)) { return undefined; @@ -61,10 +62,15 @@ export class EmulatorHub extends ExpressBasedEmulator { return locator; } - static getLocatorFilePath(projectId: string): string { + static getLocatorFilePath(projectId: string | undefined): string { const dir = os.tmpdir(); + if (!projectId) { + projectId = EmulatorHub.MISSING_PROJECT_PLACEHOLDER; + } const filename = `hub-${projectId}.json`; - return path.join(dir, filename); + const locatorPath = path.join(dir, filename); + logger.debug(`Emulator locator file path: ${locatorPath}`); + return locatorPath; } constructor(private args: EmulatorHubArgs) { diff --git a/src/emulator/hubClient.ts b/src/emulator/hubClient.ts index bd8ee7cd248..52f036f6ada 100644 --- a/src/emulator/hubClient.ts +++ b/src/emulator/hubClient.ts @@ -6,7 +6,7 @@ import { ExportOptions } from "./hubExport"; export class EmulatorHubClient { private locator: Locator | undefined; - constructor(private projectId: string) { + constructor(private projectId: string | undefined) { this.locator = EmulatorHub.readLocatorFile(projectId); } diff --git a/src/functionsShellCommandAction.ts b/src/functionsShellCommandAction.ts index b4768bc802d..fb6cefbba08 100644 --- a/src/functionsShellCommandAction.ts +++ b/src/functionsShellCommandAction.ts @@ -31,7 +31,7 @@ export const actionFunction = async (options: Options) => { } needProjectId(options); - const hubClient = new EmulatorHubClient(options.project!); + const hubClient = new EmulatorHubClient(options.project); let remoteEmulators: Record = {}; if (hubClient.foundHub()) { diff --git a/src/mcp/index.ts b/src/mcp/index.ts index f82b7242fd0..0838c5a4317 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -188,9 +188,6 @@ export class FirebaseMcpServer { return this.emulatorHubClient; } const projectId = await this.getProjectId(); - if (!projectId) { - return; - } this.emulatorHubClient = new EmulatorHubClient(projectId); return this.emulatorHubClient; }