From c910d1ddf2836f201d34a625a08825901c8f20c6 Mon Sep 17 00:00:00 2001 From: Artem Demochko Date: Wed, 11 Feb 2026 16:20:14 +0200 Subject: [PATCH 01/11] Dev Functions --- biome.json | 4 +- bun.lock | 3 + deno-runtime/README.md | 15 ++ deno-runtime/main.ts | 72 +++++++ deno-runtime/tsconfig.json | 11 + docs/error-handling.md | 1 + infra/build.ts | 22 +- knip.json | 3 +- package.json | 1 + src/cli/commands/dev.ts | 6 +- src/cli/dev/createDevLogger.ts | 66 ++++++ src/cli/dev/dev-server/function-manager.ts | 222 +++++++++++++++++++++ src/cli/dev/dev-server/main.ts | 40 +++- src/cli/dev/dev-server/routes/functions.ts | 107 ++++++++++ src/cli/utils/theme.ts | 2 + src/core/errors.ts | 16 ++ tsconfig.base.json | 14 ++ tsconfig.json | 13 +- 18 files changed, 591 insertions(+), 27 deletions(-) create mode 100644 deno-runtime/README.md create mode 100644 deno-runtime/main.ts create mode 100644 deno-runtime/tsconfig.json create mode 100644 src/cli/dev/createDevLogger.ts create mode 100644 src/cli/dev/dev-server/function-manager.ts create mode 100644 src/cli/dev/dev-server/routes/functions.ts create mode 100644 tsconfig.base.json diff --git a/biome.json b/biome.json index db88f38c..b2d7a972 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.13/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.14/schema.json", "assist": { "actions": { "source": { @@ -13,7 +13,7 @@ } }, "files": { - "includes": ["**", "!**/dist/**", "!**/node_modules/**", "!**/tests/fixtures/**", "!**/*.d.ts"] + "includes": ["**", "!**/dist", "!**/node_modules", "!**/tests/fixtures", "!**/*.d.ts"] }, "formatter": { "enabled": true, diff --git a/bun.lock b/bun.lock index 88b8bc9f..17c528bd 100644 --- a/bun.lock +++ b/bun.lock @@ -10,6 +10,7 @@ "@types/bun": "^1.2.15", "@types/common-tags": "^1.8.4", "@types/cors": "^2.8.19", + "@types/deno": "^2.5.0", "@types/ejs": "^3.1.5", "@types/express": "^5.0.6", "@types/json-schema": "^7.0.15", @@ -275,6 +276,8 @@ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/deno": ["@types/deno@2.5.0", "", {}, "sha512-g8JS38vmc0S87jKsFzre+0ZyMOUDHPVokEJymSCRlL57h6f/FdKPWBXgdFh3Z8Ees9sz11qt9VWELU9Y9ZkiVw=="], + "@types/ejs": ["@types/ejs@3.1.5", "", {}, "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], diff --git a/deno-runtime/README.md b/deno-runtime/README.md new file mode 100644 index 00000000..a35b2299 --- /dev/null +++ b/deno-runtime/README.md @@ -0,0 +1,15 @@ +# Deno Runtime + +This folder contains code that runs in **Deno**, not Node.js. + +## Why separate? + +The CLI itself is a Node.js application, but backend functions are executed in Deno. This folder provides a local Deno server for development that mimics the production function runtime. + +## TypeScript Configuration + +This folder has its own `tsconfig.json` with Deno types (`@types/deno`) instead of Node types. This prevents type conflicts between the two runtimes. + +## Usage + +This server is started automatically by `base44 dev` to handle local function deployments. diff --git a/deno-runtime/main.ts b/deno-runtime/main.ts new file mode 100644 index 00000000..d6c6874a --- /dev/null +++ b/deno-runtime/main.ts @@ -0,0 +1,72 @@ +/** + * Deno Function Wrapper + * + * This script is executed by Deno to run user functions. + * It patches Deno.serve to inject a dynamic port before importing the user's function. + * + * Environment variables: + * - FUNCTION_PATH: Absolute path to the user's function entry file + * - FUNCTION_PORT: Port number for the function to listen on + * - FUNCTION_NAME: Name of the function (for logging) + */ + +// Make this file a module for top-level await support +export {}; + +const functionPath = Deno.env.get("FUNCTION_PATH"); +const port = parseInt(Deno.env.get("FUNCTION_PORT") || "8000", 10); +const functionName = Deno.env.get("FUNCTION_NAME") || "unknown"; + +if (!functionPath) { + console.error("[wrapper] FUNCTION_PATH environment variable is required"); + Deno.exit(1); +} + +// Store the original Deno.serve +const originalServe = Deno.serve.bind(Deno); + +// Patch Deno.serve to inject our port and add onListen callback +// @ts-expect-error - We're intentionally overriding Deno.serve +Deno.serve = ( + optionsOrHandler: + | Deno.ServeOptions + | Deno.ServeHandler + | (Deno.ServeOptions & { handler: Deno.ServeHandler }), + maybeHandler?: Deno.ServeHandler, +): Deno.HttpServer => { + const onListen = () => { + // This message is used by FunctionManager to detect when the function is ready + console.log(`[${functionName}] Listening on http://localhost:${port}`); + }; + + // Handle the different Deno.serve signatures: + // 1. Deno.serve(handler) + // 2. Deno.serve(options, handler) + // 3. Deno.serve({ ...options, handler }) + if (typeof optionsOrHandler === "function") { + // Signature: Deno.serve(handler) + return originalServe({ port, onListen }, optionsOrHandler); + } + + if (maybeHandler) { + // Signature: Deno.serve(options, handler) + return originalServe({ ...optionsOrHandler, port, onListen }, maybeHandler); + } + + // Signature: Deno.serve({ ...options, handler }) + const options = optionsOrHandler as Deno.ServeOptions & { + handler: Deno.ServeHandler; + }; + return originalServe({ ...options, port, onListen }); +}; + +console.log(`[${functionName}] Starting function from ${functionPath}`); + +// Dynamically import the user's function +// The function will call Deno.serve which is now patched to use our port +try { + await import(functionPath); +} catch (error) { + console.error(`[${functionName}] Failed to load function:`, error); + Deno.exit(1); +} diff --git a/deno-runtime/tsconfig.json b/deno-runtime/tsconfig.json new file mode 100644 index 00000000..2ef342c5 --- /dev/null +++ b/deno-runtime/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2022", "DOM"], + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "typeRoots": ["../node_modules/@types"], + "types": ["deno"] + }, + "include": ["./**/*"] +} diff --git a/docs/error-handling.md b/docs/error-handling.md index c1ff8a2d..dd566e00 100644 --- a/docs/error-handling.md +++ b/docs/error-handling.md @@ -114,6 +114,7 @@ See [api-patterns.md](api-patterns.md) for the full `ApiError.fromHttpError()` p | `FILE_READ_ERROR` | `FileReadError` | Can't read/write file | | `INTERNAL_ERROR` | `InternalError` | Unexpected error | | `TYPE_GENERATION_ERROR` | `TypeGenerationError` | Type generation failed for entity | +| `DEPENDENCY_NOT_FOUND` | `DependencyNotFoundError` | Required external tool not installed | ## CLIExitError (Special Case) diff --git a/infra/build.ts b/infra/build.ts index 44053706..611f6243 100644 --- a/infra/build.ts +++ b/infra/build.ts @@ -1,8 +1,6 @@ import { watch } from "node:fs"; -import { copyFile } from "node:fs/promises"; -import { join } from "node:path"; +import type { BuildConfig } from "bun"; import chalk from "chalk"; -import { BuildConfig } from "bun"; const runBuild = async (config: BuildConfig) => { const defaultBuildOptions: Partial = { @@ -32,8 +30,13 @@ const runAllBuilds = async () => { entrypoints: ["./src/cli/index.ts"], outdir: "./dist/cli", }); + const denoRuntime = await runBuild({ + entrypoints: ["./deno-runtime/main.ts"], + outdir: "./dist/deno-runtime", + }); return { cli, + denoRuntime, }; }; @@ -46,18 +49,18 @@ if (process.argv.includes("--watch")) { const changeHandler = async ( event: "rename" | "change", - filename: string | null + filename: string | null, ) => { const time = new Date().toLocaleTimeString(); console.log(chalk.dim(`[${time}]`), chalk.gray(`${filename} ${event}d`)); - const { cli } = await runAllBuilds(); - for (const result of [cli]) { + const { cli, denoRuntime } = await runAllBuilds(); + for (const result of [cli, denoRuntime]) { if (result.success && result.outputs.length > 0) { console.log( chalk.green(` āœ“ Rebuilt`), chalk.dim(`→`), - formatOutput(result.outputs) + formatOutput(result.outputs), ); } } @@ -65,15 +68,16 @@ if (process.argv.includes("--watch")) { await runAllBuilds(); - for (const dir of ["./src"]) { + for (const dir of ["./src", "./deno-runtime"]) { watch(dir, { recursive: true }, changeHandler); } // Keep process alive await new Promise(() => {}); } else { - const { cli } = await runAllBuilds(); + const { cli, denoRuntime } = await runAllBuilds(); console.log(chalk.green.bold(`\nāœ“ Build complete\n`)); console.log(chalk.dim(" Output:")); console.log(` ${formatOutput(cli.outputs)}`); + console.log(` ${formatOutput(denoRuntime.outputs)}`); } diff --git a/knip.json b/knip.json index e0e4fa5b..9d6729e8 100644 --- a/knip.json +++ b/knip.json @@ -2,5 +2,6 @@ "$schema": "https://unpkg.com/knip@5/schema.json", "entry": ["src/cli/index.ts", "tests/**/testkit/index.ts"], "project": ["src/**/*.ts", "tests/**/*.ts"], - "ignore": ["dist/**", "tests/fixtures/**"] + "ignore": ["dist/**", "tests/fixtures/**"], + "ignoreDependencies": ["@types/deno"] } diff --git a/package.json b/package.json index 6b7323df..5ce8cb12 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@types/bun": "^1.2.15", "@types/common-tags": "^1.8.4", "@types/cors": "^2.8.19", + "@types/deno": "^2.5.0", "@types/ejs": "^3.1.5", "@types/express": "^5.0.6", "@types/json-schema": "^7.0.15", diff --git a/src/cli/commands/dev.ts b/src/cli/commands/dev.ts index 6a28688c..9a1d8842 100644 --- a/src/cli/commands/dev.ts +++ b/src/cli/commands/dev.ts @@ -1,8 +1,8 @@ import { Command } from "commander"; import { createDevServer } from "@/cli/dev/dev-server/main"; +import type { CLIContext } from "@/cli/types.js"; import { runCommand, theme } from "@/cli/utils/index.js"; import type { RunCommandResult } from "@/cli/utils/runCommand.js"; -import type { CLIContext } from "../types.js"; interface DevOptions { port?: string; @@ -10,7 +10,9 @@ interface DevOptions { async function devAction(options: DevOptions): Promise { const port = options.port ? Number(options.port) : undefined; - const { port: resolvedPort } = await createDevServer({ port }); + const { port: resolvedPort } = await createDevServer({ + port, + }); return { outroMessage: `Dev server is available at ${theme.colors.links(`http://localhost:${resolvedPort}`)}`, diff --git a/src/cli/dev/createDevLogger.ts b/src/cli/dev/createDevLogger.ts new file mode 100644 index 00000000..61288127 --- /dev/null +++ b/src/cli/dev/createDevLogger.ts @@ -0,0 +1,66 @@ +import { theme } from "@/cli/utils/theme"; + +type LogType = "log" | "error" | "warn"; + +export interface Logger { + log: (msg: string) => void; + error: (msg: string, err?: unknown) => void; + warn: (msg: string) => void; +} + +const dateTimeFormat = new Intl.DateTimeFormat([], { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, +}); + +const colorByType: Record string> = { + error: theme.styles.error, + warn: theme.styles.warn, + log: (text: string) => text, +}; + +export function createDevLogger(isPrefixed = true): Logger { + const print = (type: LogType, msg: string) => { + const colorize = colorByType[type]; + switch (type) { + case "error": + console.error(colorize(msg)); + break; + case "warn": + console.warn(colorize(msg)); + break; + default: + console.log(msg); + } + }; + + const prefixedLog = (type: LogType, msg: string) => { + const timestamp = dateTimeFormat.format(new Date()); + const colorize = colorByType[type]; + console.log(`${theme.styles.dim(timestamp)} ${colorize(msg)}`); + }; + + return isPrefixed + ? { + log: (msg: string) => prefixedLog("log", msg), + error: (msg: string, err?: unknown) => { + prefixedLog("error", msg); + if (err) { + prefixedLog("error", String(err)); + } + }, + warn: (msg: string) => prefixedLog("warn", msg), + } + : { + log: (msg: string) => print("log", msg), + error: (msg: string, err?: unknown) => { + print("error", msg); + if (err) { + print("error", String(err)); + } + }, + warn: (msg: string) => print("warn", msg), + }; +} diff --git a/src/cli/dev/dev-server/function-manager.ts b/src/cli/dev/dev-server/function-manager.ts new file mode 100644 index 00000000..5cba85b6 --- /dev/null +++ b/src/cli/dev/dev-server/function-manager.ts @@ -0,0 +1,222 @@ +import type { ChildProcess } from "node:child_process"; +import { spawn, spawnSync } from "node:child_process"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import getPort from "get-port"; +import { + DependencyNotFoundError, + InternalError, + InvalidInputError, +} from "@/core/errors.js"; +import type { BackendFunction } from "@/core/resources/function/schema.js"; +import type { Logger } from "../createDevLogger"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const WRAPPER_PATH = join(__dirname, "../deno-runtime/main.js"); + +const READY_TIMEOUT = 30000; + +interface RunningFunction { + process: ChildProcess; + port: number; + ready: boolean; +} + +export class FunctionManager { + private functions: Map; + private running: Map = new Map(); + private starting: Map> = new Map(); + private logger: Logger; + + constructor(functions: BackendFunction[], logger: Logger) { + this.functions = new Map(functions.map((f) => [f.name, f])); + this.logger = logger; + } + + functionNames(): string[] { + return Array.from(this.functions.keys()); + } + + getFunction(name: string): BackendFunction | undefined { + return this.functions.get(name); + } + + verifyDenoIsInstalled(): void { + if (this.functions.size > 0) { + const result = spawnSync("deno", ["--version"]); + if (result.error) { + throw new DependencyNotFoundError("Deno is required to run functions", { + hints: [{ message: "Install Deno from https://deno.com/download" }], + }); + } + } + } + + async ensureRunning(name: string): Promise { + const existing = this.running.get(name); + if (existing?.ready) { + return existing.port; + } + + const pending = this.starting.get(name); + if (pending) { + return pending; + } + + const backendFunction = this.functions.get(name); + if (!backendFunction) { + throw new InvalidInputError(`Function "${name}" not found`, { + hints: [{ message: "Check available functions in your project" }], + }); + } + + const promise = this.startFunction(name, backendFunction); + this.starting.set(name, promise); + + try { + return await promise; + } finally { + this.starting.delete(name); + } + } + + private async startFunction( + name: string, + backendFunction: BackendFunction, + ): Promise { + const port = await this.allocatePort(); + const process = this.spawnFunction(backendFunction, port); + + const runningFunc: RunningFunction = { + process, + port, + ready: false, + }; + + this.running.set(name, runningFunc); + this.setupProcessHandlers(name, process); + + return this.waitForReady(name, runningFunc); + } + + getPort(name: string): number | undefined { + const running = this.running.get(name); + return running?.ready ? running.port : undefined; + } + + stopAll(): void { + for (const [name, { process }] of this.running) { + this.logger.log(`[dev-server] Stopping function: ${name}`); + process.kill(); + } + this.running.clear(); + this.starting.clear(); + } + + stop(name: string): void { + const running = this.running.get(name); + if (running) { + this.logger.log(`Stopping function: ${name}`); + running.process.kill(); + this.running.delete(name); + } + } + + private async allocatePort(): Promise { + const usedPorts = Array.from(this.running.values()).map((r) => r.port); + return getPort({ exclude: usedPorts }); + } + + private spawnFunction(func: BackendFunction, port: number): ChildProcess { + this.logger.log( + `[dev-server] Spawning function "${func.name}" on port ${port}`, + ); + + const process = spawn("deno", ["run", "--allow-all", WRAPPER_PATH], { + env: { + ...globalThis.process.env, + FUNCTION_PATH: func.entryPath, + FUNCTION_PORT: String(port), + FUNCTION_NAME: func.name, + }, + stdio: ["pipe", "pipe", "pipe"], + }); + + return process; + } + + private setupProcessHandlers(name: string, process: ChildProcess): void { + // Pipe stdout with function name prefix + process.stdout?.on("data", (data: Buffer) => { + const lines = data.toString().trim().split("\n"); + for (const line of lines) { + this.logger.log(line); + } + }); + + // Pipe stderr with function name prefix + process.stderr?.on("data", (data: Buffer) => { + const lines = data.toString().trim().split("\n"); + for (const line of lines) { + this.logger.error(line); + } + }); + + process.on("exit", (code) => { + this.logger.log( + `[dev-server] Function "${name}" exited with code ${code}`, + ); + this.running.delete(name); + }); + + process.on("error", (error) => { + this.logger.error(`[dev-server] Function "${name}" error:`, error); + this.running.delete(name); + }); + } + + private waitForReady( + name: string, + runningFunc: RunningFunction, + ): Promise { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + runningFunc.process.kill(); + reject( + new InternalError( + `Function "${name}" failed to start within ${READY_TIMEOUT / 1000}s timeout`, + { + hints: [ + { message: "Check the function code for startup errors" }, + ], + }, + ), + ); + }, READY_TIMEOUT); + + const onData = (data: Buffer) => { + const output = data.toString(); + if (output.includes("Listening on")) { + runningFunc.ready = true; + clearTimeout(timeout); + runningFunc.process.stdout?.off("data", onData); + resolve(runningFunc.port); + } + }; + + runningFunc.process.stdout?.on("data", onData); + + runningFunc.process.on("exit", (code) => { + if (!runningFunc.ready) { + clearTimeout(timeout); + reject( + new InternalError(`Function "${name}" exited with code ${code}`, { + hints: [{ message: "Check the function code for errors" }], + }), + ); + } + }); + }); + } +} diff --git a/src/cli/dev/dev-server/main.ts b/src/cli/dev/dev-server/main.ts index 6e66ce81..62445b49 100644 --- a/src/cli/dev/dev-server/main.ts +++ b/src/cli/dev/dev-server/main.ts @@ -1,8 +1,15 @@ import type { Server } from "node:http"; +import { dirname, join } from "node:path"; +import { log as clackLog } from "@clack/prompts"; import cors from "cors"; import express from "express"; import getPort from "get-port"; import { createProxyMiddleware } from "http-proxy-middleware"; +import { createDevLogger } from "@/cli/dev/createDevLogger.js"; +import { FunctionManager } from "@/cli/dev/dev-server/function-manager.js"; +import { createFunctionRoutes } from "@/cli/dev/dev-server/routes/functions.js"; +import { readProjectConfig } from "@/core/project/config.js"; +import { functionResource } from "@/core/resources/function/resource.js"; const DEFAULT_PORT = 4400; const BASE44_APP_URL = "https://base44.app"; @@ -17,9 +24,13 @@ interface DevServerResult { } export async function createDevServer( - options: DevServerOptions = {}, + options: DevServerOptions, ): Promise { - const port = options.port ?? (await getPort({ port: DEFAULT_PORT })); + const { port: userPort } = options; + const port = userPort ?? (await getPort({ port: DEFAULT_PORT })); + + const { project } = await readProjectConfig(); + const configDir = dirname(project.configPath); const app = express(); @@ -46,6 +57,24 @@ export async function createDevServer( next(); }); + const functions = await functionResource.readAll( + join(configDir, project.functionsDir), + ); + + const devLogger = createDevLogger(false); + + const functionManager = new FunctionManager(functions, devLogger); + functionManager.verifyDenoIsInstalled(); + + if (functionManager.functionNames().length > 0) { + clackLog.info( + `Loaded functions: ${functionManager.functionNames().join(", ")}`, + ); + } + + const functionRoutes = createFunctionRoutes(functionManager, devLogger); + app.use("/api/apps/:appId/functions", functionRoutes); + app.use((req, res, next) => { return remoteProxy(req, res, next); }); @@ -63,6 +92,13 @@ export async function createDevServer( reject(err); } } else { + const shutdown = () => { + functionManager.stopAll(); + server.close(); + }; + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + resolve({ port, server, diff --git a/src/cli/dev/dev-server/routes/functions.ts b/src/cli/dev/dev-server/routes/functions.ts new file mode 100644 index 00000000..626cad56 --- /dev/null +++ b/src/cli/dev/dev-server/routes/functions.ts @@ -0,0 +1,107 @@ +import type { IncomingMessage } from "node:http"; +import { request as httpRequest } from "node:http"; +import type { Request, Response } from "express"; +import { Router } from "express"; +import type { Logger } from "../../createDevLogger.js"; +import type { FunctionManager } from "../function-manager.js"; + +export function createFunctionRoutes( + manager: FunctionManager, + logger: Logger, +): Router { + const router = Router({ mergeParams: true }); + + router.all( + "/:functionName", + async (req: Request<{ functionName: string }>, res: Response) => { + const { functionName } = req.params; + + try { + const func = manager.getFunction(functionName); + if (!func) { + res.status(404).json({ + error: `Function "${functionName}" not found`, + }); + return; + } + + const port = await manager.ensureRunning(functionName); + + await proxyRequest(req, res, port, logger); + } catch (error) { + logger.error(`Function error:`, error); + const message = error instanceof Error ? error.message : String(error); + res.status(500).json({ error: message }); + } + }, + ); + + return router; +} + +/** + * Proxy an Express request to a Deno function running on the specified port. + * Forwards headers, body, and method. + */ +function proxyRequest( + req: Request, + res: Response, + port: number, + logger: Logger, +): Promise { + return new Promise((resolve, reject) => { + const headers: Record = { + ...req.headers, + }; + delete headers.host; + + if (headers["x-app-id"]) { + headers["Base44-App-Id"] = headers["x-app-id"]; + } + + headers["Base44-Api-Url"] = `${req.protocol}://${req.get("host")}`; + + const options = { + hostname: "localhost", + port, + path: req.url, + method: req.method, + headers, + }; + + const proxyReq = httpRequest(options, (proxyRes: IncomingMessage) => { + res.status(proxyRes.statusCode || 200); + + for (const [key, value] of Object.entries(proxyRes.headers)) { + if (value !== undefined) { + res.setHeader(key, value); + } + } + + proxyRes.pipe(res); + + proxyRes.on("end", () => { + resolve(); + }); + + proxyRes.on("error", (error) => { + reject(error); + }); + }); + + proxyReq.on("error", (error) => { + logger.error(`Function proxy error:`, error); + if (!res.headersSent) { + res.status(502).json({ + error: "Failed to proxy request to function", + details: error.message, + }); + res.once("finish", resolve); + } else { + resolve(); + } + }); + + req.pipe(proxyReq); + }); +} diff --git a/src/cli/utils/theme.ts b/src/cli/utils/theme.ts index 5c2c527b..de50dabc 100644 --- a/src/cli/utils/theme.ts +++ b/src/cli/utils/theme.ts @@ -17,6 +17,8 @@ export const theme = { header: chalk.dim, bold: chalk.bold, dim: chalk.dim, + error: chalk.red, + warn: chalk.yellow, }, format: { errorContext(ctx: ErrorContext): string { diff --git a/src/core/errors.ts b/src/core/errors.ts index 11b449c5..c20ffa20 100644 --- a/src/core/errors.ts +++ b/src/core/errors.ts @@ -244,6 +244,22 @@ export class InvalidInputError extends UserError { readonly code = "INVALID_INPUT"; } +/** + * Thrown when a required external dependency is not installed (e.g., Deno, Git). + */ +export class DependencyNotFoundError extends UserError { + readonly code = "DEPENDENCY_NOT_FOUND"; + + constructor(message: string, options?: CLIErrorOptions) { + super(message, { + hints: options?.hints ?? [ + { message: "Install the required dependency and try again" }, + ], + cause: options?.cause, + }); + } +} + // ============================================================================ // System Errors // ============================================================================ diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 00000000..24d1ab80 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022"], + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "noEmit": true + } +} diff --git a/tsconfig.json b/tsconfig.json index 4fef6fdc..65568386 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,15 +1,6 @@ { + "extends": "./tsconfig.base.json", "compilerOptions": { - "target": "ES2022", - "module": "ES2022", - "lib": ["ES2022"], - "moduleResolution": "node", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "noEmit": true, "types": ["node", "bun"], "baseUrl": ".", "paths": { @@ -17,5 +8,5 @@ } }, "include": ["src/**/*", "tests/**/*"], - "exclude": ["node_modules", "dist", "tests/fixtures"] + "exclude": ["node_modules", "dist", "tests/fixtures", "deno-runtime"] } From 4a3d1f292c3ab8abd97344ff5215075aa59d0b4c Mon Sep 17 00:00:00 2001 From: Artem Demochko Date: Tue, 17 Feb 2026 13:17:14 +0200 Subject: [PATCH 02/11] rearranged registration --- src/cli/dev/dev-server/function-manager.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/cli/dev/dev-server/function-manager.ts b/src/cli/dev/dev-server/function-manager.ts index 5cba85b6..b6be5e7d 100644 --- a/src/cli/dev/dev-server/function-manager.ts +++ b/src/cli/dev/dev-server/function-manager.ts @@ -181,6 +181,17 @@ export class FunctionManager { runningFunc: RunningFunction, ): Promise { return new Promise((resolve, reject) => { + runningFunc.process.on("exit", (code) => { + if (!runningFunc.ready) { + clearTimeout(timeout); + reject( + new InternalError(`Function "${name}" exited with code ${code}`, { + hints: [{ message: "Check the function code for errors" }], + }), + ); + } + }); + const timeout = setTimeout(() => { runningFunc.process.kill(); reject( @@ -206,17 +217,6 @@ export class FunctionManager { }; runningFunc.process.stdout?.on("data", onData); - - runningFunc.process.on("exit", (code) => { - if (!runningFunc.ready) { - clearTimeout(timeout); - reject( - new InternalError(`Function "${name}" exited with code ${code}`, { - hints: [{ message: "Check the function code for errors" }], - }), - ); - } - }); }); } } From 76eea43a3d6526029b5e439d0fad247c5f8458fd Mon Sep 17 00:00:00 2001 From: Artem Demochko Date: Wed, 18 Feb 2026 09:26:37 +0200 Subject: [PATCH 03/11] upd --- src/cli/dev/createDevLogger.ts | 55 +++++----------------- src/cli/dev/dev-server/function-manager.ts | 2 +- src/cli/dev/dev-server/main.ts | 10 ++-- src/cli/dev/dev-server/routes/functions.ts | 2 +- 4 files changed, 18 insertions(+), 51 deletions(-) diff --git a/src/cli/dev/createDevLogger.ts b/src/cli/dev/createDevLogger.ts index 61288127..aeec2d4b 100644 --- a/src/cli/dev/createDevLogger.ts +++ b/src/cli/dev/createDevLogger.ts @@ -8,59 +8,26 @@ export interface Logger { warn: (msg: string) => void; } -const dateTimeFormat = new Intl.DateTimeFormat([], { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: false, -}); - const colorByType: Record string> = { error: theme.styles.error, warn: theme.styles.warn, log: (text: string) => text, }; -export function createDevLogger(isPrefixed = true): Logger { +export function createDevLogger(): Logger { const print = (type: LogType, msg: string) => { const colorize = colorByType[type]; - switch (type) { - case "error": - console.error(colorize(msg)); - break; - case "warn": - console.warn(colorize(msg)); - break; - default: - console.log(msg); - } + console[type](colorize(msg)); }; - const prefixedLog = (type: LogType, msg: string) => { - const timestamp = dateTimeFormat.format(new Date()); - const colorize = colorByType[type]; - console.log(`${theme.styles.dim(timestamp)} ${colorize(msg)}`); - }; - - return isPrefixed - ? { - log: (msg: string) => prefixedLog("log", msg), - error: (msg: string, err?: unknown) => { - prefixedLog("error", msg); - if (err) { - prefixedLog("error", String(err)); - } - }, - warn: (msg: string) => prefixedLog("warn", msg), + return { + log: (msg: string) => print("log", msg), + error: (msg: string, err?: unknown) => { + print("error", msg); + if (err) { + print("error", String(err)); } - : { - log: (msg: string) => print("log", msg), - error: (msg: string, err?: unknown) => { - print("error", msg); - if (err) { - print("error", String(err)); - } - }, - warn: (msg: string) => print("warn", msg), - }; + }, + warn: (msg: string) => print("warn", msg), + }; } diff --git a/src/cli/dev/dev-server/function-manager.ts b/src/cli/dev/dev-server/function-manager.ts index b6be5e7d..84e01fb7 100644 --- a/src/cli/dev/dev-server/function-manager.ts +++ b/src/cli/dev/dev-server/function-manager.ts @@ -34,7 +34,7 @@ export class FunctionManager { this.logger = logger; } - functionNames(): string[] { + getFunctionNames(): string[] { return Array.from(this.functions.keys()); } diff --git a/src/cli/dev/dev-server/main.ts b/src/cli/dev/dev-server/main.ts index 62445b49..ad5f9a6f 100644 --- a/src/cli/dev/dev-server/main.ts +++ b/src/cli/dev/dev-server/main.ts @@ -7,7 +7,7 @@ import getPort from "get-port"; import { createProxyMiddleware } from "http-proxy-middleware"; import { createDevLogger } from "@/cli/dev/createDevLogger.js"; import { FunctionManager } from "@/cli/dev/dev-server/function-manager.js"; -import { createFunctionRoutes } from "@/cli/dev/dev-server/routes/functions.js"; +import { createFunctionRouter } from "@/cli/dev/dev-server/routes/functions.js"; import { readProjectConfig } from "@/core/project/config.js"; import { functionResource } from "@/core/resources/function/resource.js"; @@ -61,18 +61,18 @@ export async function createDevServer( join(configDir, project.functionsDir), ); - const devLogger = createDevLogger(false); + const devLogger = createDevLogger(); const functionManager = new FunctionManager(functions, devLogger); functionManager.verifyDenoIsInstalled(); - if (functionManager.functionNames().length > 0) { + if (functionManager.getFunctionNames().length > 0) { clackLog.info( - `Loaded functions: ${functionManager.functionNames().join(", ")}`, + `Loaded functions: ${functionManager.getFunctionNames().join(", ")}`, ); } - const functionRoutes = createFunctionRoutes(functionManager, devLogger); + const functionRoutes = createFunctionRouter(functionManager, devLogger); app.use("/api/apps/:appId/functions", functionRoutes); app.use((req, res, next) => { diff --git a/src/cli/dev/dev-server/routes/functions.ts b/src/cli/dev/dev-server/routes/functions.ts index 626cad56..892883e1 100644 --- a/src/cli/dev/dev-server/routes/functions.ts +++ b/src/cli/dev/dev-server/routes/functions.ts @@ -5,7 +5,7 @@ import { Router } from "express"; import type { Logger } from "../../createDevLogger.js"; import type { FunctionManager } from "../function-manager.js"; -export function createFunctionRoutes( +export function createFunctionRouter( manager: FunctionManager, logger: Logger, ): Router { From 44e36e6a459506dc82e4728ecc59ac2a4fd9d5c6 Mon Sep 17 00:00:00 2001 From: Artem Demochko Date: Wed, 18 Feb 2026 13:40:31 +0200 Subject: [PATCH 04/11] proxy for functions --- src/cli/dev/dev-server/routes/functions.ts | 125 ++++++++------------- 1 file changed, 45 insertions(+), 80 deletions(-) diff --git a/src/cli/dev/dev-server/routes/functions.ts b/src/cli/dev/dev-server/routes/functions.ts index 892883e1..bbd5ddbf 100644 --- a/src/cli/dev/dev-server/routes/functions.ts +++ b/src/cli/dev/dev-server/routes/functions.ts @@ -1,7 +1,8 @@ import type { IncomingMessage } from "node:http"; -import { request as httpRequest } from "node:http"; +import { ServerResponse } from "node:http"; import type { Request, Response } from "express"; import { Router } from "express"; +import { createProxyMiddleware } from "http-proxy-middleware"; import type { Logger } from "../../createDevLogger.js"; import type { FunctionManager } from "../function-manager.js"; @@ -10,98 +11,62 @@ export function createFunctionRouter( logger: Logger, ): Router { const router = Router({ mergeParams: true }); + const portsByRequest = new WeakMap(); + + const proxy = createProxyMiddleware({ + router: (req) => `http://localhost:${portsByRequest.get(req)}`, + changeOrigin: true, + on: { + proxyReq: (proxyReq, req) => { + const xAppId = req.headers["x-app-id"]; + if (xAppId) { + proxyReq.setHeader("Base44-App-Id", xAppId as string); + } + proxyReq.setHeader( + "Base44-Api-Url", + `${(req as unknown as Request).protocol}://${req.headers.host}`, + ); + }, + error: (err, _req, res) => { + logger.error("Function proxy error:", err); + if (res instanceof ServerResponse && !res.headersSent) { + res.writeHead(502, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + error: "Failed to proxy request to function", + details: err.message, + }), + ); + } + }, + }, + }); router.all( "/:functionName", - async (req: Request<{ functionName: string }>, res: Response) => { + async (req: Request<{ functionName: string }>, res: Response, next) => { const { functionName } = req.params; - try { - const func = manager.getFunction(functionName); - if (!func) { - res.status(404).json({ - error: `Function "${functionName}" not found`, - }); - return; - } + const func = manager.getFunction(functionName); + if (!func) { + res.status(404).json({ + error: `Function "${functionName}" not found`, + }); + return; + } + try { const port = await manager.ensureRunning(functionName); - - await proxyRequest(req, res, port, logger); + portsByRequest.set(req, port); + next(); } catch (error) { - logger.error(`Function error:`, error); + logger.error("Function error:", error); const message = error instanceof Error ? error.message : String(error); res.status(500).json({ error: message }); } }, + proxy, ); return router; } - -/** - * Proxy an Express request to a Deno function running on the specified port. - * Forwards headers, body, and method. - */ -function proxyRequest( - req: Request, - res: Response, - port: number, - logger: Logger, -): Promise { - return new Promise((resolve, reject) => { - const headers: Record = { - ...req.headers, - }; - delete headers.host; - - if (headers["x-app-id"]) { - headers["Base44-App-Id"] = headers["x-app-id"]; - } - - headers["Base44-Api-Url"] = `${req.protocol}://${req.get("host")}`; - - const options = { - hostname: "localhost", - port, - path: req.url, - method: req.method, - headers, - }; - - const proxyReq = httpRequest(options, (proxyRes: IncomingMessage) => { - res.status(proxyRes.statusCode || 200); - - for (const [key, value] of Object.entries(proxyRes.headers)) { - if (value !== undefined) { - res.setHeader(key, value); - } - } - - proxyRes.pipe(res); - - proxyRes.on("end", () => { - resolve(); - }); - - proxyRes.on("error", (error) => { - reject(error); - }); - }); - - proxyReq.on("error", (error) => { - logger.error(`Function proxy error:`, error); - if (!res.headersSent) { - res.status(502).json({ - error: "Failed to proxy request to function", - details: error.message, - }); - res.once("finish", resolve); - } else { - resolve(); - } - }); - - req.pipe(proxyReq); - }); -} From 4cabed54d05b03d5744d136df27ec1822e17a436 Mon Sep 17 00:00:00 2001 From: Artem Demochko Date: Wed, 18 Feb 2026 13:47:41 +0200 Subject: [PATCH 05/11] error handling --- src/cli/dev/dev-server/function-manager.ts | 18 +++++++----------- src/cli/dev/dev-server/routes/functions.ts | 8 -------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/cli/dev/dev-server/function-manager.ts b/src/cli/dev/dev-server/function-manager.ts index 84e01fb7..997cc292 100644 --- a/src/cli/dev/dev-server/function-manager.ts +++ b/src/cli/dev/dev-server/function-manager.ts @@ -38,10 +38,6 @@ export class FunctionManager { return Array.from(this.functions.keys()); } - getFunction(name: string): BackendFunction | undefined { - return this.functions.get(name); - } - verifyDenoIsInstalled(): void { if (this.functions.size > 0) { const result = spawnSync("deno", ["--version"]); @@ -54,6 +50,13 @@ export class FunctionManager { } async ensureRunning(name: string): Promise { + const backendFunction = this.functions.get(name); + if (!backendFunction) { + throw new InvalidInputError(`Function "${name}" not found`, { + hints: [{ message: "Check available functions in your project" }], + }); + } + const existing = this.running.get(name); if (existing?.ready) { return existing.port; @@ -64,13 +67,6 @@ export class FunctionManager { return pending; } - const backendFunction = this.functions.get(name); - if (!backendFunction) { - throw new InvalidInputError(`Function "${name}" not found`, { - hints: [{ message: "Check available functions in your project" }], - }); - } - const promise = this.startFunction(name, backendFunction); this.starting.set(name, promise); diff --git a/src/cli/dev/dev-server/routes/functions.ts b/src/cli/dev/dev-server/routes/functions.ts index bbd5ddbf..15742aa3 100644 --- a/src/cli/dev/dev-server/routes/functions.ts +++ b/src/cli/dev/dev-server/routes/functions.ts @@ -47,14 +47,6 @@ export function createFunctionRouter( async (req: Request<{ functionName: string }>, res: Response, next) => { const { functionName } = req.params; - const func = manager.getFunction(functionName); - if (!func) { - res.status(404).json({ - error: `Function "${functionName}" not found`, - }); - return; - } - try { const port = await manager.ensureRunning(functionName); portsByRequest.set(req, port); From b88c3d11d54e6cd6278046ed585cd212779a00f7 Mon Sep 17 00:00:00 2001 From: Artem Demochko Date: Wed, 18 Feb 2026 13:51:20 +0200 Subject: [PATCH 06/11] removed unused method --- src/cli/dev/dev-server/function-manager.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/cli/dev/dev-server/function-manager.ts b/src/cli/dev/dev-server/function-manager.ts index 997cc292..b29a0e21 100644 --- a/src/cli/dev/dev-server/function-manager.ts +++ b/src/cli/dev/dev-server/function-manager.ts @@ -96,11 +96,6 @@ export class FunctionManager { return this.waitForReady(name, runningFunc); } - getPort(name: string): number | undefined { - const running = this.running.get(name); - return running?.ready ? running.port : undefined; - } - stopAll(): void { for (const [name, { process }] of this.running) { this.logger.log(`[dev-server] Stopping function: ${name}`); From fd919378bba99255e3bbe9e888ccfa73b92f4e24 Mon Sep 17 00:00:00 2001 From: Artem Demochko Date: Wed, 18 Feb 2026 13:54:35 +0200 Subject: [PATCH 07/11] comment --- src/cli/dev/dev-server/function-manager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cli/dev/dev-server/function-manager.ts b/src/cli/dev/dev-server/function-manager.ts index b29a0e21..80553d9d 100644 --- a/src/cli/dev/dev-server/function-manager.ts +++ b/src/cli/dev/dev-server/function-manager.ts @@ -199,6 +199,7 @@ export class FunctionManager { const onData = (data: Buffer) => { const output = data.toString(); + // We relay on the fact that logic in `deno-runtime/main.ts` will print `Listening on` when function is up and ready. if (output.includes("Listening on")) { runningFunc.ready = true; clearTimeout(timeout); From e1ba899a0d678ebe1d97dcf08e5445d94c9b590b Mon Sep 17 00:00:00 2001 From: Artem Demochko Date: Wed, 18 Feb 2026 16:54:23 +0200 Subject: [PATCH 08/11] fix --- src/cli/dev/dev-server/function-manager.ts | 22 +++++++++++----------- src/cli/dev/dev-server/main.ts | 1 - 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/cli/dev/dev-server/function-manager.ts b/src/cli/dev/dev-server/function-manager.ts index 80553d9d..68e01fff 100644 --- a/src/cli/dev/dev-server/function-manager.ts +++ b/src/cli/dev/dev-server/function-manager.ts @@ -32,23 +32,23 @@ export class FunctionManager { constructor(functions: BackendFunction[], logger: Logger) { this.functions = new Map(functions.map((f) => [f.name, f])); this.logger = logger; - } - getFunctionNames(): string[] { - return Array.from(this.functions.keys()); + this.verifyDenoIsInstalled(); } - verifyDenoIsInstalled(): void { - if (this.functions.size > 0) { - const result = spawnSync("deno", ["--version"]); - if (result.error) { - throw new DependencyNotFoundError("Deno is required to run functions", { - hints: [{ message: "Install Deno from https://deno.com/download" }], - }); - } + private verifyDenoIsInstalled(): void { + const result = spawnSync("deno", ["--version"]); + if (result.error) { + throw new DependencyNotFoundError("Deno is required to run functions", { + hints: [{ message: "Install Deno from https://deno.com/download" }], + }); } } + getFunctionNames(): string[] { + return Array.from(this.functions.keys()); + } + async ensureRunning(name: string): Promise { const backendFunction = this.functions.get(name); if (!backendFunction) { diff --git a/src/cli/dev/dev-server/main.ts b/src/cli/dev/dev-server/main.ts index ad5f9a6f..3c980cee 100644 --- a/src/cli/dev/dev-server/main.ts +++ b/src/cli/dev/dev-server/main.ts @@ -64,7 +64,6 @@ export async function createDevServer( const devLogger = createDevLogger(); const functionManager = new FunctionManager(functions, devLogger); - functionManager.verifyDenoIsInstalled(); if (functionManager.getFunctionNames().length > 0) { clackLog.info( From ae42b917628c901607026c69731cac5011380720 Mon Sep 17 00:00:00 2001 From: Artem Demochko Date: Wed, 18 Feb 2026 17:32:41 +0200 Subject: [PATCH 09/11] moved --- src/cli/commands/dev.ts | 11 +++++++++++ src/cli/dev/dev-server/main.ts | 12 +++--------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/cli/commands/dev.ts b/src/cli/commands/dev.ts index 9a1d8842..fd046cf6 100644 --- a/src/cli/commands/dev.ts +++ b/src/cli/commands/dev.ts @@ -1,8 +1,11 @@ +import { dirname, join } from "node:path"; import { Command } from "commander"; import { createDevServer } from "@/cli/dev/dev-server/main"; import type { CLIContext } from "@/cli/types.js"; import { runCommand, theme } from "@/cli/utils/index.js"; import type { RunCommandResult } from "@/cli/utils/runCommand.js"; +import { readProjectConfig } from "@/core/project/config.js"; +import { functionResource } from "@/core/resources/function/resource.js"; interface DevOptions { port?: string; @@ -12,6 +15,14 @@ async function devAction(options: DevOptions): Promise { const port = options.port ? Number(options.port) : undefined; const { port: resolvedPort } = await createDevServer({ port, + loadResources: async () => { + const { project } = await readProjectConfig(); + const configDir = dirname(project.configPath); + const functions = await functionResource.readAll( + join(configDir, project.functionsDir), + ); + return { functions }; + }, }); return { diff --git a/src/cli/dev/dev-server/main.ts b/src/cli/dev/dev-server/main.ts index 3c980cee..2f2c5e72 100644 --- a/src/cli/dev/dev-server/main.ts +++ b/src/cli/dev/dev-server/main.ts @@ -1,5 +1,4 @@ import type { Server } from "node:http"; -import { dirname, join } from "node:path"; import { log as clackLog } from "@clack/prompts"; import cors from "cors"; import express from "express"; @@ -8,14 +7,14 @@ import { createProxyMiddleware } from "http-proxy-middleware"; import { createDevLogger } from "@/cli/dev/createDevLogger.js"; import { FunctionManager } from "@/cli/dev/dev-server/function-manager.js"; import { createFunctionRouter } from "@/cli/dev/dev-server/routes/functions.js"; -import { readProjectConfig } from "@/core/project/config.js"; -import { functionResource } from "@/core/resources/function/resource.js"; +import type { BackendFunction } from "@/core/resources/function/schema.js"; const DEFAULT_PORT = 4400; const BASE44_APP_URL = "https://base44.app"; interface DevServerOptions { port?: number; + loadResources: () => Promise<{ functions: BackendFunction[] }>; } interface DevServerResult { @@ -29,8 +28,7 @@ export async function createDevServer( const { port: userPort } = options; const port = userPort ?? (await getPort({ port: DEFAULT_PORT })); - const { project } = await readProjectConfig(); - const configDir = dirname(project.configPath); + const { functions } = await options.loadResources(); const app = express(); @@ -57,10 +55,6 @@ export async function createDevServer( next(); }); - const functions = await functionResource.readAll( - join(configDir, project.functionsDir), - ); - const devLogger = createDevLogger(); const functionManager = new FunctionManager(functions, devLogger); From 39bc74cd56a53a87575290926bad3df0bf761738 Mon Sep 17 00:00:00 2001 From: Artem Demochko Date: Wed, 18 Feb 2026 17:40:53 +0200 Subject: [PATCH 10/11] removed unused function --- src/cli/dev/dev-server/function-manager.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/cli/dev/dev-server/function-manager.ts b/src/cli/dev/dev-server/function-manager.ts index 68e01fff..8d8a6f4f 100644 --- a/src/cli/dev/dev-server/function-manager.ts +++ b/src/cli/dev/dev-server/function-manager.ts @@ -105,15 +105,6 @@ export class FunctionManager { this.starting.clear(); } - stop(name: string): void { - const running = this.running.get(name); - if (running) { - this.logger.log(`Stopping function: ${name}`); - running.process.kill(); - this.running.delete(name); - } - } - private async allocatePort(): Promise { const usedPorts = Array.from(this.running.values()).map((r) => r.port); return getPort({ exclude: usedPorts }); From 7dce917402680f55303efdba31f551635307798b Mon Sep 17 00:00:00 2001 From: Artem Demochko Date: Thu, 19 Feb 2026 10:04:02 +0200 Subject: [PATCH 11/11] validate and create only if there are functions --- src/cli/dev/dev-server/function-manager.ts | 4 +++- src/cli/dev/dev-server/main.ts | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/cli/dev/dev-server/function-manager.ts b/src/cli/dev/dev-server/function-manager.ts index 8d8a6f4f..ac25357c 100644 --- a/src/cli/dev/dev-server/function-manager.ts +++ b/src/cli/dev/dev-server/function-manager.ts @@ -33,7 +33,9 @@ export class FunctionManager { this.functions = new Map(functions.map((f) => [f.name, f])); this.logger = logger; - this.verifyDenoIsInstalled(); + if (functions.length > 0) { + this.verifyDenoIsInstalled(); + } } private verifyDenoIsInstalled(): void { diff --git a/src/cli/dev/dev-server/main.ts b/src/cli/dev/dev-server/main.ts index 2f2c5e72..4abb73aa 100644 --- a/src/cli/dev/dev-server/main.ts +++ b/src/cli/dev/dev-server/main.ts @@ -63,10 +63,10 @@ export async function createDevServer( clackLog.info( `Loaded functions: ${functionManager.getFunctionNames().join(", ")}`, ); - } - const functionRoutes = createFunctionRouter(functionManager, devLogger); - app.use("/api/apps/:appId/functions", functionRoutes); + const functionRoutes = createFunctionRouter(functionManager, devLogger); + app.use("/api/apps/:appId/functions", functionRoutes); + } app.use((req, res, next) => { return remoteProxy(req, res, next);