Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.13/schema.json",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no related to PR, but now schema is aligned to our version

"$schema": "https://biomejs.dev/schemas/2.3.14/schema.json",
"assist": {
"actions": {
"source": {
Expand All @@ -13,7 +13,7 @@
}
},
"files": {
"includes": ["**", "!**/dist/**", "!**/node_modules/**", "!**/tests/fixtures/**", "!**/*.d.ts"]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no related to PR, but the correct syntaxis is not to mention ** at the end

"includes": ["**", "!**/dist", "!**/node_modules", "!**/tests/fixtures", "!**/*.d.ts"]
},
"formatter": {
"enabled": true,
Expand Down
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions deno-runtime/README.md
Original file line number Diff line number Diff line change
@@ -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.
72 changes: 72 additions & 0 deletions deno-runtime/main.ts
Original file line number Diff line number Diff line change
@@ -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<Deno.NetAddr> => {
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);
}
11 changes: 11 additions & 0 deletions deno-runtime/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM"],
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"typeRoots": ["../node_modules/@types"],
"types": ["deno"]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deno types are different from Node and in order to handle it without collision between types I need separate tsconfig

},
"include": ["./**/*"]
}
1 change: 1 addition & 0 deletions docs/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
22 changes: 13 additions & 9 deletions infra/build.ts
Original file line number Diff line number Diff line change
@@ -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<BuildConfig> = {
Expand Down Expand Up @@ -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,
};
};

Expand All @@ -46,34 +49,35 @@ 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),
);
}
}
};

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)}`);
}
3 changes: 2 additions & 1 deletion knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 15 additions & 2 deletions src/cli/commands/dev.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
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 type { CLIContext } from "../types.js";
import { readProjectConfig } from "@/core/project/config.js";
import { functionResource } from "@/core/resources/function/resource.js";

interface DevOptions {
port?: string;
}

async function devAction(options: DevOptions): Promise<RunCommandResult> {
const port = options.port ? Number(options.port) : undefined;
const { port: resolvedPort } = await createDevServer({ port });
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 {
outroMessage: `Dev server is available at ${theme.colors.links(`http://localhost:${resolvedPort}`)}`,
Expand Down
33 changes: 33 additions & 0 deletions src/cli/dev/createDevLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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 colorByType: Record<LogType, (text: string) => string> = {
error: theme.styles.error,
warn: theme.styles.warn,
log: (text: string) => text,
};

export function createDevLogger(): Logger {
const print = (type: LogType, msg: string) => {
const colorize = colorByType[type];
console[type](colorize(msg));
};

return {
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),
};
}
Loading
Loading