Skip to content
Draft
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
209 changes: 208 additions & 1 deletion bun.lock

Large diffs are not rendered by default.

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.
75 changes: 75 additions & 0 deletions deno-runtime/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* 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);
}
18 changes: 18 additions & 0 deletions deno-runtime/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022", "DOM"],
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"typeRoots": ["../node_modules/@types"],
"types": ["deno"]
},
"include": ["./**/*"]
}
57 changes: 42 additions & 15 deletions infra/build.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { watch } from "node:fs";
import chalk from "chalk";
import { BuildConfig } from "bun";

const runBuild = async () => {
const result = await Bun.build({
entrypoints: ["./src/cli/index.ts"],
outdir: "./dist/cli",
const runBuild = async (config: BuildConfig) => {
const defaultBuildOptions: Partial<BuildConfig> = {
target: "node",
format: "esm",
sourcemap: "inline",
};

const result = await Bun.build({
...defaultBuildOptions,
...config,
});

if (!result.success) {
Expand All @@ -21,36 +25,59 @@ const runBuild = async () => {
return result;
};

const runAllBuilds = async () => {
const cli = await runBuild({
entrypoints: ["./src/cli/index.ts"],
outdir: "./dist/cli",
});
const denoRuntime = await runBuild({
entrypoints: ["./deno-runtime/main.ts"],
outdir: "./dist/deno-runtime",
});
return {
cli,
denoRuntime,
};
};

const formatOutput = (outputs: { path: string }[]) => {
return outputs.map((o) => chalk.cyan(o.path)).join("\n ");
};

if (process.argv.includes("--watch")) {
console.log(chalk.yellow("Watching for changes..."));

const changeHandler = async (event: "rename" | "change", filename: string | null) => {
const changeHandler = async (
event: "rename" | "change",
filename: string | null
) => {
const time = new Date().toLocaleTimeString();
console.log(chalk.dim(`[${time}]`), chalk.gray(`${filename} ${event}d`));

const result = await runBuild();
console.log(
chalk.green(` ✓ Rebuilt`),
chalk.dim(`→`),
formatOutput(result.outputs)
);
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)
);
}
}
};

await runBuild();
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 result = await runBuild();
const { cli, denoRuntime } = await runAllBuilds();
console.log(chalk.green.bold(`\n✓ Build complete\n`));
console.log(chalk.dim(" Output:"));
console.log(` ${formatOutput(result.outputs)}\n`);
console.log(` ${formatOutput(cli.outputs)}`);
console.log(` ${formatOutput(denoRuntime.outputs)}\n`);
}
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,24 @@
"devDependencies": {
"@biomejs/biome": "^2.0.0",
"@clack/prompts": "^0.11.0",
"@seald-io/nedb": "^4.1.2",
"@types/bun": "^1.2.15",
"@types/cors": "^2.8.19",
"@types/deno": "^2.5.0",
"@types/ejs": "^3.1.5",
"@types/express": "^5.0.6",
"@types/lodash.kebabcase": "^4.1.9",
"@types/node": "^22.10.5",
"@types/tar": "^6.1.13",
"@vercel/detect-agent": "^1.1.0",
"chalk": "^5.6.2",
"commander": "^12.1.0",
"cors": "^2.8.6",
"ejs": "^3.1.10",
"execa": "^9.6.1",
"express": "^5.2.1",
"front-matter": "^4.0.2",
"get-port": "^7.1.0",
"globby": "^16.1.0",
"json5": "^2.2.3",
"ky": "^1.14.2",
Expand Down
47 changes: 47 additions & 0 deletions src/cli/commands/dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Command } from "commander";
import { theme } from "@/cli/utils/theme.js";
import { isCLIError } from "@/core/errors.js";
import { createDevServer } from "@/dev-server/main.js";
import type { CLIContext } from "../types";

async function devAction(context: CLIContext): Promise<void> {
console.log();

console.log(theme.colors.base44OrangeBackground(" Base 44 "));

try {
await createDevServer();
} catch (error) {
// Display error message
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(errorMessage);

// Show stack trace if DEBUG mode
if (process.env.DEBUG === "1" && error instanceof Error && error.stack) {
console.error(theme.styles.dim(error.stack));
}

// Display hints if this is a CLIError with hints
if (isCLIError(error)) {
const hints = theme.format.agentHints(error.hints);
if (hints) {
console.error(hints);
}
}

// Get error context and display in outro
const errorContext = context.errorReporter.getErrorContext();
console.log(theme.format.errorContext(errorContext));

// Re-throw for runCLI to handle (error reporting, exit code)
throw error;
}
}

export function getDevCommand(context: CLIContext): Command {
return new Command("dev")
.description("Start the development server")
.action(async () => {
await devAction(context);
});
}
4 changes: 4 additions & 0 deletions src/cli/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getDeployCommand } from "@/cli/commands/project/deploy.js";
import { getLinkCommand } from "@/cli/commands/project/link.js";
import { getSiteCommand } from "@/cli/commands/site/index.js";
import packageJson from "../../package.json";
import { getDevCommand } from "./commands/dev.js";
import type { CLIContext } from "./types.js";

export function createProgram(context: CLIContext): Command {
Expand Down Expand Up @@ -50,5 +51,8 @@ export function createProgram(context: CLIContext): Command {
// Register site commands
program.addCommand(getSiteCommand(context));

// Register development commands
program.addCommand(getDevCommand(context));

return program;
}
25 changes: 25 additions & 0 deletions src/dev-server/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Datastore from "@seald-io/nedb";
import type { Entity } from "@/core/resources/entity/schema.js";

export class Database {
private collections: Map<string, Datastore>;

constructor(entities: Entity[]) {
this.collections = new Map();
for (const entity of entities) {
this.collections.set(entity.name, new Datastore());
}
}

getCollection(name: string): Datastore | undefined {
return this.collections.get(name);
}

hasCollection(name: string): boolean {
return this.collections.has(name);
}

getEntityNames(): string[] {
return [...this.collections.keys()];
}
}
Loading
Loading