From eb59fa8f8b5e7db8d4a0b4227564d814bf45875d Mon Sep 17 00:00:00 2001 From: Fred Zhang Date: Fri, 5 Sep 2025 17:05:06 -0700 Subject: [PATCH 1/3] Provide flutter as an option as well --- src/init/features/dataconnect/create_app.ts | 8 ++++++- src/init/features/dataconnect/sdk.ts | 25 ++++++++++++++------- src/mcp/errors.ts | 3 ++- src/mcp/index.ts | 2 +- src/mcp/util.ts | 24 -------------------- src/utils.ts | 24 ++++++++++++++++++++ 6 files changed, 51 insertions(+), 35 deletions(-) diff --git a/src/init/features/dataconnect/create_app.ts b/src/init/features/dataconnect/create_app.ts index d1a2f53bb14..2403666426d 100644 --- a/src/init/features/dataconnect/create_app.ts +++ b/src/init/features/dataconnect/create_app.ts @@ -26,9 +26,15 @@ export async function createNextApp(webAppId: string): Promise { await executeCommand("npx", args); } +/** Create a Flutter app using flutter create. */ +export async function createFlutterApp(webAppId: string): Promise { + const args = ["create", webAppId]; + await executeCommand("flutter", args); +} + // Function to execute a command asynchronously and pipe I/O async function executeCommand(command: string, args: string[]): Promise { - logLabeledBullet("dataconnect", `Running ${clc.bold(`${command} ${args.join(" ")}`)}`); + logLabeledBullet("dataconnect", `> ${clc.bold(`${command} ${args.join(" ")}`)}`); return new Promise((resolve, reject) => { // spawn returns a ChildProcess object const childProcess = spawn(command, args, { diff --git a/src/init/features/dataconnect/sdk.ts b/src/init/features/dataconnect/sdk.ts index c5b6c7d9830..85366d1e648 100644 --- a/src/init/features/dataconnect/sdk.ts +++ b/src/init/features/dataconnect/sdk.ts @@ -28,10 +28,11 @@ import { logLabeledWarning, logLabeledBullet, newUniqueId, + commandExistsSync, } from "../../../utils"; import { DataConnectEmulator } from "../../../emulator/dataconnectEmulator"; import { getGlobalDefaultAccount } from "../../../auth"; -import { createNextApp, createReactApp } from "./create_app"; +import { createFlutterApp, createNextApp, createReactApp } from "./create_app"; import { trackGA4 } from "../../../track"; import { dirExistsSync, listFiles } from "../../../fsutils"; @@ -57,24 +58,32 @@ export async function askQuestions(setup: Setup): Promise { info.apps = await chooseApp(); if (!info.apps.length) { // By default, create an React web app. - const existingFilesAndDirs = listFiles(cwd); - const webAppId = newUniqueId("web-app", existingFilesAndDirs); + const npmWarning = commandExistsSync("npx") + ? "" + : clc.yellow(" (you need to install Node.js first)"); + const flutterWarning = commandExistsSync("flutter") + ? "" + : clc.yellow(" (you need to install Flutter first)"); + const choice = await select({ message: `Do you want to create an app template?`, choices: [ // TODO: Create template tailored to FDC. - { name: "React", value: "react" }, - { name: "Next.JS", value: "next" }, - // TODO: Add flutter here. + { name: `React${npmWarning}`, value: "react" }, + { name: `Next.JS${npmWarning}`, value: "next" }, + { name: `Flutter${flutterWarning}`, value: "flutter" }, { name: "no", value: "no" }, ], }); switch (choice) { case "react": - await createReactApp(webAppId); + await createReactApp(newUniqueId("web-app", listFiles(cwd))); break; case "next": - await createNextApp(webAppId); + await createNextApp(newUniqueId("web-app", listFiles(cwd))); + break; + case "flutter": + await createFlutterApp(newUniqueId("flutter_app", listFiles(cwd))); break; case "no": break; diff --git a/src/mcp/errors.ts b/src/mcp/errors.ts index 00a007b3bec..9bfba87f688 100644 --- a/src/mcp/errors.ts +++ b/src/mcp/errors.ts @@ -1,5 +1,6 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { commandExistsSync, mcpError } from "./util"; +import { mcpError } from "./util"; +import { commandExistsSync } from "../utils"; export const NO_PROJECT_ERROR = mcpError( 'No active project was found. Use the `firebase_update_environment` tool to set the project directory to an absolute folder location containing a firebase.json config file. Alternatively, change the MCP server config to add [...,"--dir","/absolute/path/to/project/directory"] in its command-line arguments.', diff --git a/src/mcp/index.ts b/src/mcp/index.ts index 0838c5a4317..2bbab682567 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -183,7 +183,7 @@ export class FirebaseMcpServer { } async getEmulatorHubClient(): Promise { - // Single initilization + // Single initialization if (this.emulatorHubClient) { return this.emulatorHubClient; } diff --git a/src/mcp/util.ts b/src/mcp/util.ts index d6cd01cc8b8..14725b43483 100644 --- a/src/mcp/util.ts +++ b/src/mcp/util.ts @@ -1,7 +1,5 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { execSync } from "child_process"; import { dump } from "js-yaml"; -import { platform } from "os"; import { ServerFeature } from "./types"; import { apphostingOrigin, @@ -64,28 +62,6 @@ export function mcpError(message: Error | string | unknown, code?: string): Call * Wraps a throwing function with a safe conversion to mcpError. */ -/** - * Checks if a command exists in the system. - */ -export function commandExistsSync(command: string): boolean { - try { - const isWindows = platform() === "win32"; - // For Windows, `where` is more appropriate. It also often outputs the path. - // For Unix-like systems, `which` is standard. - // The `2> nul` (Windows) or `2>/dev/null` (Unix) redirects stderr to suppress error messages. - // The `>` nul / `>/dev/null` redirects stdout as we only care about the exit code. - const commandToCheck = isWindows - ? `where "${command}" > nul 2> nul` - : `which "${command}" > /dev/null 2> /dev/null`; - - execSync(commandToCheck); - return true; // If execSync doesn't throw, the command was found (exit code 0) - } catch (error) { - // If the command is not found, execSync will throw an error (non-zero exit code) - return false; - } -} - const SERVER_FEATURE_APIS: Record = { core: "", firestore: firestoreOrigin(), diff --git a/src/utils.ts b/src/utils.ts index fd81616311c..71802bbfe52 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -25,6 +25,8 @@ import { readTemplateSync } from "./templates"; import { isVSCodeExtension } from "./vsCodeUtils"; import { Config } from "./config"; import { dirExistsSync, fileExistsSync } from "./fsutils"; +import { platform } from "node:os"; +import { execSync } from "node:child_process"; export const IS_WINDOWS = process.platform === "win32"; const SUCCESS_CHAR = IS_WINDOWS ? "+" : "✔"; const WARNING_CHAR = IS_WINDOWS ? "!" : "⚠"; @@ -1006,3 +1008,25 @@ export function newUniqueId(recommended: string, existingIDs: string[]): string } return id; } + +/** + * Checks if a command exists in the system. + */ +export function commandExistsSync(command: string): boolean { + try { + const isWindows = platform() === "win32"; + // For Windows, `where` is more appropriate. It also often outputs the path. + // For Unix-like systems, `which` is standard. + // The `2> nul` (Windows) or `2>/dev/null` (Unix) redirects stderr to suppress error messages. + // The `>` nul / `>/dev/null` redirects stdout as we only care about the exit code. + const commandToCheck = isWindows + ? `where "${command}" > nul 2> nul` + : `which "${command}" > /dev/null 2> /dev/null`; + + execSync(commandToCheck); + return true; // If execSync doesn't throw, the command was found (exit code 0) + } catch (error) { + // If the command is not found, execSync will throw an error (non-zero exit code) + return false; + } +} From a1d2b01125300f5d37f3d13f95e9452af506da0e Mon Sep 17 00:00:00 2001 From: Fred Zhang Date: Fri, 5 Sep 2025 17:11:31 -0700 Subject: [PATCH 2/3] changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41d99ba0619..36c3ad430de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,2 @@ -- `firebase emulator:start` use a default project if no project can be found. (#9072) +- `firebase emulator:start` use a default project `demo-no-project` if no project can be found. (#9072) +- `firebase init dataconnect` also supports bootstrapping flutter template. (#9084) From ca64da505b0a0863ff275737898dd9b0fb9f5566 Mon Sep 17 00:00:00 2001 From: Fred Zhang Date: Fri, 5 Sep 2025 17:13:26 -0700 Subject: [PATCH 3/3] m --- src/init/features/dataconnect/sdk.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/init/features/dataconnect/sdk.ts b/src/init/features/dataconnect/sdk.ts index 85366d1e648..a4b883975ab 100644 --- a/src/init/features/dataconnect/sdk.ts +++ b/src/init/features/dataconnect/sdk.ts @@ -57,11 +57,10 @@ export async function askQuestions(setup: Setup): Promise { info.apps = await chooseApp(); if (!info.apps.length) { - // By default, create an React web app. - const npmWarning = commandExistsSync("npx") + const npxMissingWarning = commandExistsSync("npx") ? "" : clc.yellow(" (you need to install Node.js first)"); - const flutterWarning = commandExistsSync("flutter") + const flutterMissingWarning = commandExistsSync("flutter") ? "" : clc.yellow(" (you need to install Flutter first)"); @@ -69,9 +68,9 @@ export async function askQuestions(setup: Setup): Promise { message: `Do you want to create an app template?`, choices: [ // TODO: Create template tailored to FDC. - { name: `React${npmWarning}`, value: "react" }, - { name: `Next.JS${npmWarning}`, value: "next" }, - { name: `Flutter${flutterWarning}`, value: "flutter" }, + { name: `React${npxMissingWarning}`, value: "react" }, + { name: `Next.JS${npxMissingWarning}`, value: "next" }, + { name: `Flutter${flutterMissingWarning}`, value: "flutter" }, { name: "no", value: "no" }, ], });