From fda88a1b69fa9645c85bf8d3ce03635d6a9b15c0 Mon Sep 17 00:00:00 2001 From: David Susskind Date: Thu, 30 Jul 2026 15:29:40 +0300 Subject: [PATCH] refactor(dev): decouple the serveCommand lifecycle from the dev server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit createDevServer now builds the backend only and returns its shutdown; dev.ts orchestrates the two visible steps — start the backend, then run the configured site.serveCommand pointed at it via a ServeCommandRunner. The isServingFrontend flag dies: the CLI never knew a frontend existed, only that a serveCommand was configured. Co-Authored-By: Claude Fable 5 --- packages/cli/src/cli/commands/dev.ts | 76 +++++++++++++++---- packages/cli/src/cli/dev/dev-server/main.ts | 33 +------- .../cli/src/cli/dev/serve-command-runner.ts | 27 +++++++ 3 files changed, 92 insertions(+), 44 deletions(-) create mode 100644 packages/cli/src/cli/dev/serve-command-runner.ts diff --git a/packages/cli/src/cli/commands/dev.ts b/packages/cli/src/cli/commands/dev.ts index cfcb7d8e3..3018839f6 100644 --- a/packages/cli/src/cli/commands/dev.ts +++ b/packages/cli/src/cli/commands/dev.ts @@ -1,5 +1,8 @@ import type { Command } from "commander"; +import { createDevLogger } from "@/cli/dev/createDevLogger.js"; import { createDevServer } from "@/cli/dev/dev-server/main.js"; +import type { ServeRunner } from "@/cli/dev/dev-server/serve-runner.js"; +import { createServeCommandRunner } from "@/cli/dev/serve-command-runner.js"; import type { CLIContext, RunCommandResult } from "@/cli/types.js"; import { type AppIdOptions, Base44Command, theme } from "@/cli/utils/index.js"; import { getDenoWrapperPath } from "@/core/assets.js"; @@ -12,6 +15,11 @@ interface DevOptions { port?: string; } +interface LinkedApp { + id: string; + projectRoot: string; +} + function localServerUrl(port: number): string { return `http://localhost:${port}`; } @@ -25,25 +33,61 @@ function validateDevOptions(command: Command): void { } } -async function devAction( - ctx: CLIContext, - options: DevOptions, -): Promise { - const { log, app } = ctx; +function requireLinkedProject({ app }: CLIContext): LinkedApp { if (!app?.projectRoot) { throw new ConfigInvalidError( "base44 dev requires a linked local project. Run it from a project with base44/.app.jsonc.", ); } + return { id: app.id, projectRoot: app.projectRoot }; +} + +async function createConfiguredServeRunner( + app: LinkedApp, + backendUrl: string, +): Promise { + const { project } = await readProjectConfig(app.projectRoot); + const serveCommand = project.site?.serveCommand; + if (!serveCommand) { + return undefined; + } + return createServeCommandRunner({ + serveCommand, + projectRoot: project.root, + appId: app.id, + appBaseUrl: backendUrl, + }); +} - const port = options.port ? Number(options.port) : undefined; - const appId = app.id; +function startServeCommand( + runner: ServeRunner, + backend: { url: string; shutdown: () => Promise }, +): void { + const stop = () => void runner.stop(); + process.on("SIGINT", stop); + process.on("SIGTERM", stop); + + // If the frontend dies, tear the whole dev environment down. + runner.onExit(() => { + void backend.shutdown().finally(() => process.exit(1)); + }); + + createDevLogger("backend", theme.styles.info).log( + `Backend running on ${backend.url}`, + ); + runner.start(); +} + +async function devAction( + ctx: CLIContext, + options: DevOptions, +): Promise { + const app = requireLinkedProject(ctx); const siteUrlPromise = getSiteUrl().catch(() => undefined); - const { port: resolvedPort, isServingFrontend } = await createDevServer({ - log, - port, - appId, + const backend = await createDevServer({ + log: ctx.log, + port: options.port ? Number(options.port) : undefined, denoWrapperPath: getDenoWrapperPath(), loadResources: async () => { const { functions, entities, project } = await readProjectConfig(); @@ -51,10 +95,16 @@ async function devAction( return { functions, entities, project, siteUrl }; }, }); + const backendUrl = localServerUrl(backend.port); + + const runner = await createConfiguredServeRunner(app, backendUrl); + if (runner) { + startServeCommand(runner, { url: backendUrl, shutdown: backend.shutdown }); + } - const outroMessage = isServingFrontend + const outroMessage = runner ? "Open your app using the frontend dev server URL" - : `Dev server is available at ${theme.colors.links(localServerUrl(resolvedPort))}`; + : `Dev server is available at ${theme.colors.links(backendUrl)}`; return { outroMessage }; } diff --git a/packages/cli/src/cli/dev/dev-server/main.ts b/packages/cli/src/cli/dev/dev-server/main.ts index b130b212b..f1ffc438b 100644 --- a/packages/cli/src/cli/dev/dev-server/main.ts +++ b/packages/cli/src/cli/dev/dev-server/main.ts @@ -24,7 +24,6 @@ import { createFileToken, createIntegrationRoutes, } from "./routes/integrations.js"; -import { ServeRunner } from "./serve-runner.js"; import { WatchBase44 } from "./watcher.js"; const DEFAULT_PORT = 4400; @@ -34,7 +33,6 @@ interface DevServerOptions { log: Logger; port?: number; denoWrapperPath: string; - appId?: string; loadResources: () => Promise<{ functions: ProjectData["functions"]; entities: ProjectData["entities"]; @@ -46,7 +44,7 @@ interface DevServerOptions { interface DevServerResult { port: number; server: Server; - isServingFrontend: boolean; + shutdown: () => Promise; } export async function createDevServer( @@ -245,22 +243,6 @@ export async function createDevServer( }); await base44ConfigWatcher.start(); - // Run the frontend dev server when the project configures a `site.serveCommand` - // and we have an app id to inject. It runs from the project root. - const serveCommand = project.site?.serveCommand; - let serveRunner: ServeRunner | undefined; - if (options.appId && serveCommand) { - serveRunner = new ServeRunner({ - command: serveCommand, - cwd: project.root, - env: { - VITE_BASE44_APP_ID: options.appId, - VITE_BASE44_APP_BASE_URL: baseUrl, - }, - logger: createDevLogger("frontend", theme.colors.base44Orange), - }); - } - const handleShutdownError = (error: unknown) => { const errorMessage = error instanceof Error ? error.message : String(error); devLogger.error(`Failed to shut down dev server: ${errorMessage}`); @@ -287,7 +269,6 @@ export async function createDevServer( base44ConfigWatcher.close(); await io.close(); await functionManager.stopAll(); - await serveRunner?.stop(); await closeServerIfRunning(); }; @@ -299,15 +280,5 @@ export async function createDevServer( process.on("SIGINT", shutdown); process.on("SIGTERM", shutdown); - // If the frontend dies, tear the whole dev environment down. - serveRunner?.onExit(() => { - void shutdown().finally(() => process.exit(1)); - }); - - if (serveRunner) { - devLogger.log(`Backend running on ${baseUrl}`); - serveRunner.start(); - } - - return { port, server, isServingFrontend: serveRunner !== undefined }; + return { port, server, shutdown }; } diff --git a/packages/cli/src/cli/dev/serve-command-runner.ts b/packages/cli/src/cli/dev/serve-command-runner.ts new file mode 100644 index 000000000..6e0d414fb --- /dev/null +++ b/packages/cli/src/cli/dev/serve-command-runner.ts @@ -0,0 +1,27 @@ +import { createDevLogger } from "@/cli/dev/createDevLogger.js"; +import { ServeRunner } from "@/cli/dev/dev-server/serve-runner.js"; +import { theme } from "@/cli/utils/index.js"; + +interface ServeCommandRunnerOptions { + serveCommand: string; + projectRoot: string; + appId: string; + appBaseUrl: string; +} + +export function createServeCommandRunner({ + serveCommand, + projectRoot, + appId, + appBaseUrl, +}: ServeCommandRunnerOptions): ServeRunner { + return new ServeRunner({ + command: serveCommand, + cwd: projectRoot, + env: { + VITE_BASE44_APP_ID: appId, + VITE_BASE44_APP_BASE_URL: appBaseUrl, + }, + logger: createDevLogger("frontend", theme.colors.base44Orange), + }); +}