feat: init dev command

This commit is contained in:
dswbx
2025-12-03 17:46:02 +01:00
parent 959dee013e
commit 6bbc922710
22 changed files with 427 additions and 9 deletions
+11 -1
View File
@@ -3,7 +3,17 @@ import c from "picocolors";
import { formatNumber } from "bknd/utils";
const deps = Object.keys(pkg.dependencies);
const external = ["jsonv-ts/*", "wrangler", "bknd", "bknd/*", ...deps];
const external = [
"jsonv-ts/*",
"wrangler",
"bknd",
"bknd/*",
"@vitejs/plugin-react",
"vite",
"@tailwindcss/vite",
"@cloudflare/vite-plugin",
...deps,
];
const result = await Bun.build({
entrypoints: ["./src/cli/index.ts"],
+1
View File
@@ -80,6 +80,7 @@
"@aws-sdk/client-s3": "^3.922.0",
"@bluwy/giget-core": "^0.1.6",
"@clack/prompts": "^0.11.0",
"@cloudflare/vite-plugin": "^1.15.3",
"@cloudflare/vitest-pool-workers": "^0.10.4",
"@cloudflare/workers-types": "^4.20251014.0",
"@dagrejs/dagre": "^1.1.4",
+5
View File
@@ -69,6 +69,11 @@ export async function createAdapterApp<Config extends BkndConfig = BkndConfig, A
if (Connection.isConnection(config.connection)) {
connection = config.connection;
} else {
if (connection) {
$console.warn(
"Connection is not a valid connection object, using default SQLite connection",
);
}
const sqlite = (await import("bknd/adapter/sqlite")).sqlite;
const conf = appConfig.connection ?? { url: "file:data.db" };
connection = sqlite(conf) as any;
+17
View File
@@ -0,0 +1,17 @@
import type { CliCommand } from "cli/types";
import { withConfigOptions, type WithConfigOptions } from "cli/utils/options";
import * as utils from "./utils";
import { $console } from "core/utils";
import { Project } from "./lib/Project";
export const dev: CliCommand = (program) =>
withConfigOptions(program.command("dev")).description("dev server").action(action);
async function action(options: WithConfigOptions<{}>) {
const project = new Project({
templatePath: utils.TEMPLATE_PATH,
});
await project.init();
await project.listen();
}
+96
View File
@@ -0,0 +1,96 @@
import { RelativeFS } from "./RelativeFS";
//import { $console } from "bknd/utils";
import { type ViteDevServer, createServer } from "vite";
import { readdir, copyFile, stat } from "node:fs/promises";
import path from "node:path";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import { cloudflare } from "@cloudflare/vite-plugin";
import { getRelativeDistPath } from "cli/utils/sys";
import { injectMain } from "./vite/inject-main";
import { devFsVitePlugin } from "adapter/cloudflare";
export type ProjectOptions = {
userPath?: string;
templatePath?: string;
};
// @todo: add multiple public dirs
// @todo: add npm install (first time)
// @todo: add package.json
export class Project {
public userFs: RelativeFS;
public templateFs: RelativeFS;
private _server?: ViteDevServer;
constructor(public options: ProjectOptions) {
this.userFs = new RelativeFS(options.userPath ?? process.cwd());
this.templateFs = new RelativeFS(options.templatePath ?? "src/cli/commands/dev/template");
}
get server() {
if (!this._server) {
throw new Error("Server not initialized");
}
return this._server!;
}
async init() {
const source = this.templateFs.root;
const destination = this.userFs.root;
// recursively copy all files and directories from source to dest
const copyRecursive = async (src: string, dst: string) => {
const entries = await readdir(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const dstPath = path.join(dst, entry.name);
if (entry.isDirectory()) {
await this.userFs.makeDir(path.relative(destination, dstPath));
await copyRecursive(srcPath, dstPath);
} else if (entry.isFile()) {
const exists = await stat(dstPath)
.then((s) => s.isFile())
.catch(() => null);
// only copy everything from ".bknd" with override
if (!exists || (dstPath.includes(".bknd") && !dstPath.includes("bknd-types.d.ts"))) {
await copyFile(srcPath, dstPath);
}
}
}
};
await copyRecursive(source, destination);
}
async listen() {
this._server = await createServer({
clearScreen: false,
publicDir: getRelativeDistPath() + "/static",
plugins: [
react(),
tailwindcss(),
devFsVitePlugin({ configFile: ".bknd/bknd.config.ts" }) as any,
cloudflare({
configPath: this.userFs.path(".bknd/wrangler.json"),
persistState: {
path: this.userFs.path(".bknd/state"),
},
}),
injectMain({
mainPath: "/.bknd/main.tsx",
}),
],
build: {},
resolve: {
dedupe: ["react", "react-dom"],
},
});
await this._server.listen();
this._server.printUrls();
this._server.bindCLIShortcuts({ print: true });
}
}
@@ -0,0 +1,50 @@
import {
mkdir,
stat,
readFile as nodeReadFile,
writeFile as nodeWriteFile,
} from "node:fs/promises";
import { getRootPath } from "cli/utils/sys";
import path from "node:path";
export class RelativeFS {
public root: string;
constructor(p: string) {
this.root = path.resolve(getRootPath(), p);
}
path(p: string) {
return path.join(this.root, p);
}
async hasFile(path: string) {
try {
const s = await stat(this.path(path));
return s.isFile();
} catch (_) {
return false;
}
}
async hasDir(path: string) {
try {
const s = await stat(this.path(path));
return s.isDirectory();
} catch (_) {
return false;
}
}
async readFile(p: string) {
return await nodeReadFile(this.path(p), "utf-8");
}
async writeFile(p: string, content: string) {
return await nodeWriteFile(this.path(p), content);
}
async makeDir(p: string) {
return await mkdir(this.path(p), { recursive: true });
}
}
@@ -0,0 +1,22 @@
import type { PluginOption } from "vite";
export function injectMain({ mainPath }: { mainPath: string }): PluginOption {
return {
name: "inject-main-script",
transformIndexHtml: {
order: "pre", // run before other transforms
handler(html) {
return {
html,
tags: [
{
tag: "script",
attrs: { type: "module", src: mainPath },
injectTo: "body",
},
],
};
},
},
};
}
@@ -0,0 +1,8 @@
import type { DB } from "bknd";
import type { Insertable, Selectable, Updateable } from "kysely";
declare global {
type BkndEntity<T extends keyof DB> = Selectable<DB[T]>;
type BkndEntityCreate<T extends keyof DB> = Insertable<DB[T]>;
type BkndEntityUpdate<T extends keyof DB> = Updateable<DB[T]>;
}
@@ -0,0 +1,14 @@
/// <reference types="vite/client" />
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { ClientProvider } from "bknd/client";
import App from "../src/App.tsx";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ClientProvider>
<App />
</ClientProvider>
</StrictMode>,
);
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"types": ["vite/client"]
},
"include": ["bknd-types.d.ts"]
}
@@ -0,0 +1,4 @@
import { serve } from "bknd/adapter/cloudflare";
import config from "./bknd.config";
export default serve(config);
@@ -0,0 +1,28 @@
{
"name": "bknd-dev",
"main": "./worker.ts",
"compatibility_date": "2025-10-08",
"compatibility_flags": ["nodejs_compat"],
"observability": {
"enabled": true
},
"assets": {
"binding": "ASSETS",
"directory": "./dist/client",
"not_found_handling": "single-page-application",
"run_worker_first": ["!/", "/admin*", "/api*", "!/assets/*"]
},
"vars": {
"ENVIRONMENT": "development"
},
"d1_databases": [
{
"binding": "DB"
}
],
"r2_buckets": [
{
"binding": "BUCKET"
}
]
}
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>bknd + Vite + Cloudflare + React + TS</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
@@ -0,0 +1,5 @@
import "./styles.css";
export default function App() {
return <div>Hello World</div>;
}
@@ -0,0 +1,11 @@
import { Hono } from "hono";
import type { ServerEnv } from "bknd";
/**
* Add custom routes to the API here. Base path is `/api`.
*/
export default new Hono<ServerEnv>().get("/", (c) => {
// const app = c.var.app;
// const api = app.getApi();
return c.json({ message: "Hello, world!" });
});
@@ -0,0 +1,7 @@
import { AppEvents, DatabaseEvents, type EventManager } from "bknd";
export default function (emgr: EventManager) {
// emgr.onEvent(AppEvents.AppRequest, async (event) => {
// console.log("Request received", event.params.request.url);
// });
}
@@ -0,0 +1,8 @@
import { em, entity, text, boolean, number, datetime, json, jsonSchema, enumm } from "bknd";
export default em({
// todos: entity("todos", {
// title: text(),
// done: boolean(),
// }),
});
@@ -0,0 +1 @@
@import "tailwindcss";
@@ -0,0 +1,4 @@
{
"extends": "./.bknd/tsconfig.json",
"include": ["src"]
}
+40
View File
@@ -0,0 +1,40 @@
import path from "node:path";
import {
mkdir,
stat,
readFile as nodeReadFile,
writeFile as nodeWriteFile,
} from "node:fs/promises";
import { getRootPath } from "cli/utils/sys";
export const TEMPLATE_PATH = "src/cli/commands/dev/template";
export const currentDir = process.cwd();
export const fs = (dir: string) => {
const PATH = path.resolve(getRootPath(), dir);
return {
PATH,
path: (_path: string) => path.join(PATH, _path),
hasFile: async (file: string) => {
try {
const s = await stat(path.join(PATH, file));
return s.isFile();
} catch (_) {
return false;
}
},
hasDir: async (_dir: string) => {
try {
const s = await stat(path.join(PATH, _dir));
return s.isDirectory();
} catch (_) {
return false;
}
},
readFile: (file: string) => nodeReadFile(path.join(PATH, file), "utf-8"),
readJsonFile: async (file: string) =>
JSON.parse(await nodeReadFile(path.join(PATH, file), "utf-8")),
writeFile: (file: string, content: string) => nodeWriteFile(path.join(PATH, file), content),
makeDir: (_newDir: string) => mkdir(path.join(PATH, _newDir)),
};
};
+1
View File
@@ -9,3 +9,4 @@ export { types } from "./types";
export { mcp } from "./mcp/mcp";
export { sync } from "./sync";
export { secrets } from "./secrets";
export { dev } from "./dev/dev";