Skip to content

Commit 5dd9701

Browse files
authored
Merge b9fa60c into fcafc22
2 parents fcafc22 + b9fa60c commit 5dd9701

27 files changed

Lines changed: 1911 additions & 37 deletions

File tree

bun.lock

Lines changed: 372 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

deno-runtime/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Deno Runtime
2+
3+
This folder contains code that runs in **Deno**, not Node.js.
4+
5+
## Why separate?
6+
7+
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.
8+
9+
## TypeScript Configuration
10+
11+
This folder has its own `tsconfig.json` with Deno types (`@types/deno`) instead of Node types. This prevents type conflicts between the two runtimes.
12+
13+
## Usage
14+
15+
This server is started automatically by `base44 dev` to handle local function deployments.

deno-runtime/main.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Deno Function Wrapper
3+
*
4+
* This script is executed by Deno to run user functions.
5+
* It patches Deno.serve to inject a dynamic port before importing the user's function.
6+
*
7+
* Environment variables:
8+
* - FUNCTION_PATH: Absolute path to the user's function entry file
9+
* - FUNCTION_PORT: Port number for the function to listen on
10+
* - FUNCTION_NAME: Name of the function (for logging)
11+
*/
12+
13+
// Make this file a module for top-level await support
14+
export {};
15+
16+
const functionPath = Deno.env.get("FUNCTION_PATH");
17+
const port = parseInt(Deno.env.get("FUNCTION_PORT") || "8000", 10);
18+
const functionName = Deno.env.get("FUNCTION_NAME") || "unknown";
19+
20+
if (!functionPath) {
21+
console.error("[wrapper] FUNCTION_PATH environment variable is required");
22+
Deno.exit(1);
23+
}
24+
25+
// Store the original Deno.serve
26+
const originalServe = Deno.serve.bind(Deno);
27+
28+
// Patch Deno.serve to inject our port and add onListen callback
29+
// @ts-expect-error - We're intentionally overriding Deno.serve
30+
Deno.serve = (
31+
optionsOrHandler:
32+
| Deno.ServeOptions
33+
| Deno.ServeHandler
34+
| (Deno.ServeOptions & { handler: Deno.ServeHandler }),
35+
maybeHandler?: Deno.ServeHandler
36+
): Deno.HttpServer<Deno.NetAddr> => {
37+
const onListen = () => {
38+
// This message is used by FunctionManager to detect when the function is ready
39+
console.log(`[${functionName}] Listening on http://localhost:${port}`);
40+
};
41+
42+
// Handle the different Deno.serve signatures:
43+
// 1. Deno.serve(handler)
44+
// 2. Deno.serve(options, handler)
45+
// 3. Deno.serve({ ...options, handler })
46+
if (typeof optionsOrHandler === "function") {
47+
// Signature: Deno.serve(handler)
48+
return originalServe({ port, onListen }, optionsOrHandler);
49+
}
50+
51+
if (maybeHandler) {
52+
// Signature: Deno.serve(options, handler)
53+
return originalServe(
54+
{ ...optionsOrHandler, port, onListen },
55+
maybeHandler
56+
);
57+
}
58+
59+
// Signature: Deno.serve({ ...options, handler })
60+
const options = optionsOrHandler as Deno.ServeOptions & {
61+
handler: Deno.ServeHandler;
62+
};
63+
return originalServe({ ...options, port, onListen });
64+
};
65+
66+
console.log(`[${functionName}] Starting function from ${functionPath}`);
67+
68+
// Dynamically import the user's function
69+
// The function will call Deno.serve which is now patched to use our port
70+
try {
71+
await import(functionPath);
72+
} catch (error) {
73+
console.error(`[${functionName}] Failed to load function:`, error);
74+
Deno.exit(1);
75+
}

deno-runtime/tsconfig.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "ES2022",
5+
"lib": ["ES2022", "DOM"],
6+
"moduleResolution": "bundler",
7+
"strict": true,
8+
"esModuleInterop": true,
9+
"skipLibCheck": true,
10+
"forceConsistentCasingInFileNames": true,
11+
"resolveJsonModule": true,
12+
"noEmit": true,
13+
"allowImportingTsExtensions": true,
14+
"typeRoots": ["../node_modules/@types"],
15+
"types": ["deno"]
16+
},
17+
"include": ["./**/*"]
18+
}

infra/build.ts

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,36 @@
11
import { watch } from "node:fs";
2+
import { copyFile } from "node:fs/promises";
3+
import { join } from "node:path";
24
import chalk from "chalk";
5+
import { BuildConfig } from "bun";
36

4-
const runBuild = async () => {
5-
const result = await Bun.build({
6-
entrypoints: ["./src/cli/index.ts"],
7-
outdir: "./dist/cli",
7+
const runBuild = async (config: BuildConfig) => {
8+
const defaultBuildOptions: Partial<BuildConfig> = {
89
target: "node",
910
format: "esm",
1011
sourcemap: "external",
12+
external: [
13+
// Optional deps of Ink. Needed for Dev mode only, which we don't support.
14+
"react-devtools-core"
15+
],
16+
plugins: [{
17+
name: 'exclude-devtools',
18+
setup(build) {
19+
build.onResolve({ filter: /^react-devtools-core$/ }, () => ({
20+
path: 'react-devtools-core',
21+
namespace: 'empty-module',
22+
}));
23+
build.onLoad({ filter: /.*/, namespace: 'empty-module' }, () => ({
24+
contents: 'module.exports = {};',
25+
loader: 'js',
26+
}));
27+
},
28+
}],
29+
};
30+
31+
const result = await Bun.build({
32+
...defaultBuildOptions,
33+
...config,
1134
});
1235

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

47+
const runAllBuilds = async () => {
48+
const outdir = "./dist/cli";
49+
const cli = await runBuild({
50+
entrypoints: ["./src/cli/index.ts"],
51+
outdir,
52+
});
53+
/**
54+
* This is a dep of Ink. This package imports the wasm file via (fs.readFile).
55+
* We need to copy it to the build folder, so it will be available at runtime
56+
* after the build. 'esbuild' doesn't handle this automatically.
57+
*/
58+
await copyFile(
59+
Bun.resolveSync("yoga-wasm-web/dist/yoga.wasm", process.cwd()),
60+
join(outdir, "yoga.wasm"),
61+
);
62+
const denoRuntime = await runBuild({
63+
entrypoints: ["./deno-runtime/main.ts"],
64+
outdir: "./dist/deno-runtime",
65+
});
66+
return {
67+
cli,
68+
denoRuntime,
69+
};
70+
};
71+
2472
const formatOutput = (outputs: { path: string }[]) => {
2573
return outputs.map((o) => chalk.cyan(o.path)).join("\n ");
2674
};
2775

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

31-
const changeHandler = async (event: "rename" | "change", filename: string | null) => {
79+
const changeHandler = async (
80+
event: "rename" | "change",
81+
filename: string | null
82+
) => {
3283
const time = new Date().toLocaleTimeString();
3384
console.log(chalk.dim(`[${time}]`), chalk.gray(`${filename} ${event}d`));
3485

35-
const result = await runBuild();
36-
console.log(
37-
chalk.green(` ✓ Rebuilt`),
38-
chalk.dim(`→`),
39-
formatOutput(result.outputs)
40-
);
86+
const { cli, denoRuntime } = await runAllBuilds();
87+
for (const result of [cli, denoRuntime]) {
88+
if (result.success && result.outputs.length > 0) {
89+
console.log(
90+
chalk.green(` ✓ Rebuilt`),
91+
chalk.dim(`→`),
92+
formatOutput(result.outputs)
93+
);
94+
}
95+
}
4196
};
4297

43-
await runBuild();
98+
await runAllBuilds();
4499

45-
for (const dir of ["./src"]) {
100+
for (const dir of ["./src", "./deno-runtime"]) {
46101
watch(dir, { recursive: true }, changeHandler);
47102
}
48103

49104
// Keep process alive
50105
await new Promise(() => {});
51106
} else {
52-
const result = await runBuild();
107+
const { cli, denoRuntime } = await runAllBuilds();
53108
console.log(chalk.green.bold(`\n✓ Build complete\n`));
54109
console.log(chalk.dim(" Output:"));
55-
console.log(` ${formatOutput(result.outputs)}\n`);
110+
console.log(` ${formatOutput(cli.outputs)}`);
111+
console.log(` ${formatOutput(denoRuntime.outputs)}\n`);
56112
}

package.json

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
"bin"
1212
],
1313
"scripts": {
14-
"build": "bun run clean && cp -r templates dist/ && bun run infra/build.ts",
15-
"build:watch": "bun run clean && cp -r templates dist/ && bun run infra/build.ts --watch",
14+
"build": "bun run clean && cp -r templates dist/ && DEV=false bun run infra/build.ts",
15+
"build:watch": "bun run clean && cp -r templates dist/ && DEV=false bun run infra/build.ts --watch",
1616
"typecheck": "tsc --noEmit",
1717
"dev": "./bin/dev.ts",
1818
"start": "./bin/run.js",
@@ -36,30 +36,43 @@
3636
"devDependencies": {
3737
"@biomejs/biome": "^2.0.0",
3838
"@clack/prompts": "^0.11.0",
39+
"@seald-io/nedb": "^4.1.2",
3940
"@types/bun": "^1.2.15",
4041
"@types/common-tags": "^1.8.4",
42+
"@types/cors": "^2.8.19",
43+
"@types/deno": "^2.5.0",
4144
"@types/ejs": "^3.1.5",
4245
"@types/json-schema": "^7.0.15",
43-
"@types/lodash.kebabcase": "^4.1.9",
46+
"@types/express": "^5.0.6",
47+
"@types/lodash": "^4.1.9",
48+
"@types/multer": "^2.0.0",
4449
"@types/node": "^22.10.5",
50+
"@types/react": "^18.3.3",
4551
"@types/tar": "^6.1.13",
4652
"@vercel/detect-agent": "^1.1.0",
4753
"chalk": "^5.6.2",
4854
"commander": "^12.1.0",
4955
"common-tags": "^1.8.2",
56+
"cors": "^2.8.6",
5057
"ejs": "^3.1.10",
5158
"execa": "^9.6.1",
59+
"express": "^5.2.1",
5260
"front-matter": "^4.0.2",
61+
"get-port": "^7.1.0",
5362
"globby": "^16.1.0",
5463
"json-schema-to-typescript": "^15.0.4",
64+
"http-proxy-middleware": "^3.0.5",
65+
"ink": "4.2.0",
5566
"json5": "^2.2.3",
5667
"ky": "^1.14.2",
57-
"lodash.kebabcase": "^4.1.1",
68+
"lodash": "^4.1.1",
5869
"msw": "^2.12.7",
70+
"multer": "^2.0.2",
5971
"nanoid": "^5.1.6",
6072
"open": "^11.0.0",
6173
"p-wait-for": "^6.0.0",
6274
"posthog-node": "5.21.2",
75+
"react": "^18.3.1",
6376
"strip-ansi": "^7.1.2",
6477
"tar": "^7.5.4",
6578
"tmp-promise": "^3.0.3",

src/cli/commands/dev.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Command } from "commander";
2+
import React from "react";
3+
import { theme } from "@/cli/utils/theme.js";
4+
import { isCLIError } from "@/core/errors.js";
5+
import { DevCommand } from "@/dev/DevCommand";
6+
import { render } from "../ink-render/renderer";
7+
import type { CLIContext } from "../types";
8+
9+
async function devAction(context: CLIContext): Promise<void> {
10+
try {
11+
await render(React.createElement(DevCommand));
12+
// await createDevServer();
13+
} catch (error) {
14+
// Display error message
15+
const errorMessage = error instanceof Error ? error.message : String(error);
16+
console.error(errorMessage);
17+
18+
// Show stack trace if DEBUG mode
19+
if (process.env.DEBUG === "1" && error instanceof Error && error.stack) {
20+
console.error(theme.styles.dim(error.stack));
21+
}
22+
23+
// Display hints if this is a CLIError with hints
24+
if (isCLIError(error)) {
25+
const hints = theme.format.agentHints(error.hints);
26+
if (hints) {
27+
console.error(hints);
28+
}
29+
}
30+
31+
// Get error context and display in outro
32+
const errorContext = context.errorReporter.getErrorContext();
33+
console.log(theme.format.errorContext(errorContext));
34+
35+
// Re-throw for runCLI to handle (error reporting, exit code)
36+
throw error;
37+
}
38+
}
39+
40+
export function getDevCommand(context: CLIContext): Command {
41+
return new Command("dev")
42+
.description("Start the development server")
43+
.action(async () => {
44+
await devAction(context);
45+
});
46+
}

src/cli/commands/project/create.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Option } from "@clack/prompts";
33
import { confirm, group, isCancel, log, select, text } from "@clack/prompts";
44
import { Argument, Command } from "commander";
55
import { execa } from "execa";
6-
import kebabCase from "lodash.kebabcase";
6+
import kebabCase from "lodash/kebabCase";
77
import type { CLIContext } from "@/cli/types.js";
88
import {
99
getDashboardUrl,
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Text as InkText } from "ink";
2+
import type { FC } from "react";
3+
4+
export interface KeyProps {
5+
value: string;
6+
skin?: "main" | "secondary";
7+
}
8+
9+
export const Key: FC<KeyProps> = ({ value, skin }) => {
10+
if (skin === "secondary") {
11+
return (
12+
<InkText>
13+
<InkText inverse> {value} </InkText>
14+
<InkText></InkText>
15+
</InkText>
16+
);
17+
}
18+
return (
19+
<InkText>
20+
<InkText backgroundColor="blueBright"> {value} </InkText>
21+
<InkText color="blueBright"></InkText>
22+
</InkText>
23+
);
24+
};

0 commit comments

Comments
 (0)