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
387 changes: 372 additions & 15 deletions 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": ["./**/*"]
}
86 changes: 71 additions & 15 deletions infra/build.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,36 @@
import { watch } from "node:fs";
import { copyFile } from "node:fs/promises";
import { join } from "node:path";
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: "external",
external: [
// Optional deps of Ink. Needed for Dev mode only, which we don't support.
"react-devtools-core"
],
plugins: [{
name: 'exclude-devtools',
setup(build) {
build.onResolve({ filter: /^react-devtools-core$/ }, () => ({
path: 'react-devtools-core',
namespace: 'empty-module',
}));
build.onLoad({ filter: /.*/, namespace: 'empty-module' }, () => ({
contents: 'module.exports = {};',
loader: 'js',
}));
},
}],
};

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

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

const runAllBuilds = async () => {
const outdir = "./dist/cli";
const cli = await runBuild({
entrypoints: ["./src/cli/index.ts"],
outdir,
});
/**
* This is a dep of Ink. This package imports the wasm file via (fs.readFile).
* We need to copy it to the build folder, so it will be available at runtime
* after the build. 'esbuild' doesn't handle this automatically.
*/
await copyFile(
Bun.resolveSync("yoga-wasm-web/dist/yoga.wasm", process.cwd()),
join(outdir, "yoga.wasm"),
);
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`);
}
21 changes: 17 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"bin"
],
"scripts": {
"build": "bun run clean && cp -r templates dist/ && bun run infra/build.ts",
"build:watch": "bun run clean && cp -r templates dist/ && bun run infra/build.ts --watch",
"build": "bun run clean && cp -r templates dist/ && DEV=false bun run infra/build.ts",
"build:watch": "bun run clean && cp -r templates dist/ && DEV=false bun run infra/build.ts --watch",
"typecheck": "tsc --noEmit",
"dev": "./bin/dev.ts",
"start": "./bin/run.js",
Expand All @@ -36,30 +36,43 @@
"devDependencies": {
"@biomejs/biome": "^2.0.0",
"@clack/prompts": "^0.11.0",
"@seald-io/nedb": "^4.1.2",
"@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/json-schema": "^7.0.15",
"@types/lodash.kebabcase": "^4.1.9",
"@types/express": "^5.0.6",
"@types/lodash": "^4.1.9",
"@types/multer": "^2.0.0",
"@types/node": "^22.10.5",
"@types/react": "^18.3.3",
"@types/tar": "^6.1.13",
"@vercel/detect-agent": "^1.1.0",
"chalk": "^5.6.2",
"commander": "^12.1.0",
"common-tags": "^1.8.2",
"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",
"json-schema-to-typescript": "^15.0.4",
"http-proxy-middleware": "^3.0.5",
"ink": "4.2.0",
"json5": "^2.2.3",
"ky": "^1.14.2",
"lodash.kebabcase": "^4.1.1",
"lodash": "^4.1.1",
"msw": "^2.12.7",
"multer": "^2.0.2",
"nanoid": "^5.1.6",
"open": "^11.0.0",
"p-wait-for": "^6.0.0",
"posthog-node": "5.21.2",
"react": "^18.3.1",
"strip-ansi": "^7.1.2",
"tar": "^7.5.4",
"tmp-promise": "^3.0.3",
Expand Down
46 changes: 46 additions & 0 deletions src/cli/commands/dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Command } from "commander";
import React from "react";
import { theme } from "@/cli/utils/theme.js";
import { isCLIError } from "@/core/errors.js";
import { DevCommand } from "@/dev/DevCommand";
import { render } from "../ink-render/renderer";
import type { CLIContext } from "../types";

async function devAction(context: CLIContext): Promise<void> {
try {
await render(React.createElement(DevCommand));
// 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);
});
}
2 changes: 1 addition & 1 deletion src/cli/commands/project/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Option } from "@clack/prompts";
import { confirm, group, isCancel, log, select, text } from "@clack/prompts";
import { Argument, Command } from "commander";
import { execa } from "execa";
import kebabCase from "lodash.kebabcase";
import kebabCase from "lodash/kebabCase";
import type { CLIContext } from "@/cli/types.js";
import {
getDashboardUrl,
Expand Down
24 changes: 24 additions & 0 deletions src/cli/ink-render/components/Key.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Text as InkText } from "ink";
import type { FC } from "react";

export interface KeyProps {
value: string;
skin?: "main" | "secondary";
}

export const Key: FC<KeyProps> = ({ value, skin }) => {
if (skin === "secondary") {
return (
<InkText>
<InkText inverse> {value} </InkText>
<InkText>░</InkText>
</InkText>
);
}
return (
<InkText>
<InkText backgroundColor="blueBright"> {value} </InkText>
<InkText color="blueBright">░</InkText>
</InkText>
);
};
Loading
Loading