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) 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..a4b883975ab 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"; @@ -56,25 +57,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 npxMissingWarning = commandExistsSync("npx") + ? "" + : clc.yellow(" (you need to install Node.js first)"); + const flutterMissingWarning = 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${npxMissingWarning}`, value: "react" }, + { name: `Next.JS${npxMissingWarning}`, value: "next" }, + { name: `Flutter${flutterMissingWarning}`, 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; + } +}