mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-04 00:56:01 +00:00
feat: init dev command
This commit is contained in:
+11
-1
@@ -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"],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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)),
|
||||
};
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user