gorsee 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/gorsee.js CHANGED
@@ -1,8 +1,14 @@
1
1
  #!/usr/bin/env node
2
- const { execSync } = require("child_process");
2
+ import { execSync } from "child_process";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const cli = join(__dirname, "..", "src", "cli", "index.ts");
3
8
  const args = process.argv.slice(2).join(" ");
9
+
4
10
  try {
5
- execSync(`bun ${require("path").join(__dirname, "..", "src", "cli", "index.ts")} ${args}`, { stdio: "inherit" });
11
+ execSync(`bun ${cli} ${args}`, { stdio: "inherit" });
6
12
  } catch (e) {
7
13
  process.exit(e.status || 1);
8
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gorsee",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Full-stack TypeScript framework — islands, reactive WebSocket, optimistic mutations, built-in auth, type-safe routes",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -42,7 +42,16 @@
42
42
  "./env": "./src/env/index.ts",
43
43
  "./auth": "./src/auth/index.ts",
44
44
  "./routes": "./src/runtime/typed-routes.ts",
45
- "./cli/cmd-create": "./src/cli/cmd-create.ts"
45
+ "./cli/cmd-create": "./src/cli/cmd-create.ts",
46
+ "./plugins": "./src/plugins/index.ts",
47
+ "./plugins/drizzle": "./src/plugins/drizzle.ts",
48
+ "./plugins/prisma": "./src/plugins/prisma.ts",
49
+ "./plugins/tailwind": "./src/plugins/tailwind.ts",
50
+ "./plugins/lucia": "./src/plugins/lucia.ts",
51
+ "./plugins/s3": "./src/plugins/s3.ts",
52
+ "./plugins/resend": "./src/plugins/resend.ts",
53
+ "./plugins/stripe": "./src/plugins/stripe.ts",
54
+ "./deploy": "./src/deploy/index.ts"
46
55
  },
47
56
  "files": [
48
57
  "src/",
@@ -0,0 +1,141 @@
1
+ // Gorsee.js — CLI deploy command
2
+
3
+ import { writeFile, access, mkdir } from "node:fs/promises"
4
+ import { join } from "node:path"
5
+
6
+ type Target = "vercel" | "fly" | "cloudflare" | "netlify" | "docker"
7
+
8
+ const TARGETS: Target[] = ["vercel", "fly", "cloudflare", "netlify", "docker"]
9
+
10
+ const DETECT_FILES: Record<string, Target> = {
11
+ "vercel.json": "vercel",
12
+ "fly.toml": "fly",
13
+ "wrangler.toml": "cloudflare",
14
+ "netlify.toml": "netlify",
15
+ "Dockerfile": "docker",
16
+ }
17
+
18
+ async function fileExists(path: string): Promise<boolean> {
19
+ try {
20
+ await access(path)
21
+ return true
22
+ } catch {
23
+ return false
24
+ }
25
+ }
26
+
27
+ async function detectTarget(cwd: string): Promise<Target | null> {
28
+ for (const [file, target] of Object.entries(DETECT_FILES)) {
29
+ if (await fileExists(join(cwd, file))) return target
30
+ }
31
+ return null
32
+ }
33
+
34
+ async function writeAndLog(filePath: string, content: string): Promise<void> {
35
+ await writeFile(filePath, content, "utf-8")
36
+ console.log(` created ${filePath}`)
37
+ }
38
+
39
+ async function deployVercel(cwd: string): Promise<void> {
40
+ const { generateVercelConfig, generateVercelServerlessEntry } = await import("../deploy/vercel.ts")
41
+ await writeAndLog(join(cwd, "vercel.json"), JSON.stringify(generateVercelConfig(), null, 2))
42
+ await mkdir(join(cwd, "api"), { recursive: true })
43
+ await writeAndLog(join(cwd, "api/index.ts"), generateVercelServerlessEntry())
44
+ console.log("\n Next steps:")
45
+ console.log(" 1. Install Vercel CLI: npm i -g vercel")
46
+ console.log(" 2. Run: vercel")
47
+ console.log(" 3. Follow prompts to link your project")
48
+ }
49
+
50
+ async function deployFly(cwd: string, appName: string): Promise<void> {
51
+ const { generateFlyConfig, generateFlyDockerfile } = await import("../deploy/fly.ts")
52
+ await writeAndLog(join(cwd, "fly.toml"), generateFlyConfig(appName))
53
+ await writeAndLog(join(cwd, "Dockerfile"), generateFlyDockerfile())
54
+ console.log("\n Next steps:")
55
+ console.log(" 1. Install Fly CLI: curl -L https://fly.io/install.sh | sh")
56
+ console.log(` 2. Run: fly launch --name ${appName}`)
57
+ console.log(" 3. Deploy: fly deploy")
58
+ }
59
+
60
+ async function deployCloudflare(cwd: string, name: string): Promise<void> {
61
+ const { generateWranglerConfig, generateCloudflareEntry, generateCloudflareStaticAssets } =
62
+ await import("../deploy/cloudflare.ts")
63
+ await writeAndLog(join(cwd, "wrangler.toml"), generateWranglerConfig(name))
64
+ await writeAndLog(join(cwd, "worker.ts"), generateCloudflareEntry())
65
+ await writeAndLog(join(cwd, "_routes.json"), JSON.stringify(generateCloudflareStaticAssets(), null, 2))
66
+ console.log("\n Next steps:")
67
+ console.log(" 1. Install Wrangler: npm i -g wrangler")
68
+ console.log(" 2. Authenticate: wrangler login")
69
+ console.log(" 3. Deploy: wrangler deploy")
70
+ }
71
+
72
+ async function deployNetlify(cwd: string): Promise<void> {
73
+ const { generateNetlifyConfig, generateNetlifyFunction } = await import("../deploy/netlify.ts")
74
+ await writeAndLog(join(cwd, "netlify.toml"), generateNetlifyConfig())
75
+ const edgeFnDir = join(cwd, "netlify/edge-functions")
76
+ await mkdir(edgeFnDir, { recursive: true })
77
+ await writeAndLog(join(edgeFnDir, "gorsee-handler.ts"), generateNetlifyFunction())
78
+ console.log("\n Next steps:")
79
+ console.log(" 1. Install Netlify CLI: npm i -g netlify-cli")
80
+ console.log(" 2. Run: netlify init")
81
+ console.log(" 3. Deploy: netlify deploy --prod")
82
+ }
83
+
84
+ async function deployDocker(cwd: string): Promise<void> {
85
+ const { generateDockerfile, generateDockerignore } = await import("../deploy/dockerfile.ts")
86
+ await writeAndLog(join(cwd, "Dockerfile"), generateDockerfile())
87
+ await writeAndLog(join(cwd, ".dockerignore"), generateDockerignore())
88
+ console.log("\n Next steps:")
89
+ console.log(" 1. Build image: docker build -t gorsee-app .")
90
+ console.log(" 2. Run: docker run -p 3000:3000 gorsee-app")
91
+ }
92
+
93
+ export async function runDeploy(args: string[]): Promise<void> {
94
+ const cwd = process.cwd()
95
+ const initOnly = args.includes("--init")
96
+ const targetArg = args.find((a) => !a.startsWith("-")) as Target | undefined
97
+
98
+ let target = targetArg ?? null
99
+ if (!target) {
100
+ target = await detectTarget(cwd)
101
+ if (!target) {
102
+ console.error(" No deploy target specified and none detected.")
103
+ console.error(` Usage: gorsee deploy <${TARGETS.join("|")}> [--init]`)
104
+ process.exit(1)
105
+ }
106
+ console.log(` Auto-detected target: ${target}`)
107
+ }
108
+
109
+ if (!TARGETS.includes(target)) {
110
+ console.error(` Unknown target: ${target}`)
111
+ console.error(` Available: ${TARGETS.join(", ")}`)
112
+ process.exit(1)
113
+ }
114
+
115
+ const projectName = cwd.split("/").pop() ?? "gorsee-app"
116
+ console.log(`\n Generating ${target} deploy config...\n`)
117
+
118
+ switch (target) {
119
+ case "vercel":
120
+ await deployVercel(cwd)
121
+ break
122
+ case "fly":
123
+ await deployFly(cwd, projectName)
124
+ break
125
+ case "cloudflare":
126
+ await deployCloudflare(cwd, projectName)
127
+ break
128
+ case "netlify":
129
+ await deployNetlify(cwd)
130
+ break
131
+ case "docker":
132
+ await deployDocker(cwd)
133
+ break
134
+ }
135
+
136
+ if (initOnly) {
137
+ console.log("\n Config generated (--init mode). Deploy manually when ready.")
138
+ }
139
+
140
+ console.log()
141
+ }
package/src/cli/index.ts CHANGED
@@ -14,6 +14,7 @@ const COMMANDS: Record<string, string> = {
14
14
  migrate: "Run database migrations",
15
15
  generate: "Generate CRUD scaffold for entity",
16
16
  typegen: "Generate typed route definitions",
17
+ deploy: "Generate deploy config (vercel/fly/cloudflare/netlify/docker)",
17
18
  help: "Show this help message",
18
19
  }
19
20
 
@@ -55,6 +56,10 @@ async function main() {
55
56
  const { runTypegen } = await import("./cmd-typegen.ts")
56
57
  await runTypegen(args.slice(1))
57
58
  break
59
+ case "deploy":
60
+ const { runDeploy } = await import("./cmd-deploy.ts")
61
+ await runDeploy(args.slice(1))
62
+ break
58
63
  case "help":
59
64
  case undefined:
60
65
  case "--help":
@@ -0,0 +1,109 @@
1
+ // Gorsee.js — Cloudflare Workers/Pages deploy adapter
2
+
3
+ export function generateWranglerConfig(name: string): string {
4
+ const today = new Date().toISOString().split("T")[0]
5
+
6
+ return `# Gorsee.js — Cloudflare Workers configuration
7
+ # Auto-generated by \`gorsee deploy cloudflare\`
8
+
9
+ name = "${name}"
10
+ main = "dist/worker.js"
11
+ compatibility_date = "${today}"
12
+ compatibility_flags = ["nodejs_compat"]
13
+
14
+ [site]
15
+ bucket = "./dist/client"
16
+
17
+ [build]
18
+ command = "bun run build"
19
+
20
+ # KV namespace for caching (optional)
21
+ # [[kv_namespaces]]
22
+ # binding = "CACHE"
23
+ # id = ""
24
+
25
+ # Environment variables
26
+ [vars]
27
+ NODE_ENV = "production"
28
+
29
+ # Production overrides
30
+ [env.production]
31
+ name = "${name}"
32
+ route = { pattern = "*", zone_name = "" }
33
+ `
34
+ }
35
+
36
+ export function generateCloudflareEntry(): string {
37
+ return `// Gorsee.js — Cloudflare Worker entry
38
+ // Auto-generated by \`gorsee deploy cloudflare\`
39
+
40
+ export default {
41
+ async fetch(
42
+ request: Request,
43
+ env: Record<string, unknown>,
44
+ ctx: ExecutionContext,
45
+ ): Promise<Response> {
46
+ const url = new URL(request.url)
47
+
48
+ // Serve static client assets from KV/site
49
+ if (url.pathname.startsWith("/_gorsee/")) {
50
+ const assetPath = url.pathname.slice("/_gorsee/".length)
51
+ // @ts-expect-error — __STATIC_CONTENT is injected by Cloudflare
52
+ const asset = await (env.__STATIC_CONTENT as any)?.get(assetPath)
53
+ if (asset) {
54
+ return new Response(asset, {
55
+ headers: {
56
+ "Content-Type": "application/javascript",
57
+ "Cache-Control": "public, max-age=31536000, immutable",
58
+ },
59
+ })
60
+ }
61
+ }
62
+
63
+ // Serve public static files
64
+ const staticExts = [".css", ".ico", ".svg", ".png", ".jpg", ".woff2", ".txt"]
65
+ if (staticExts.some((ext) => url.pathname.endsWith(ext))) {
66
+ // @ts-expect-error — __STATIC_CONTENT is injected by Cloudflare
67
+ const asset = await (env.__STATIC_CONTENT as any)?.get(url.pathname.slice(1))
68
+ if (asset) {
69
+ return new Response(asset, {
70
+ headers: { "Cache-Control": "public, max-age=3600" },
71
+ })
72
+ }
73
+ }
74
+
75
+ // Forward all other requests to Gorsee server handler
76
+ // In production, this imports the built server bundle
77
+ try {
78
+ const { handleRequest } = await import("./server-handler.js")
79
+ return await handleRequest(request, env)
80
+ } catch {
81
+ return new Response("Internal Server Error", { status: 500 })
82
+ }
83
+ },
84
+ }
85
+ `
86
+ }
87
+
88
+ export interface CloudflareRoutesConfig {
89
+ version: number
90
+ include: string[]
91
+ exclude: string[]
92
+ }
93
+
94
+ export function generateCloudflareStaticAssets(): CloudflareRoutesConfig {
95
+ return {
96
+ version: 1,
97
+ include: ["/*"],
98
+ exclude: [
99
+ "/_gorsee/*",
100
+ "/favicon.ico",
101
+ "/robots.txt",
102
+ "/styles.css",
103
+ "/*.png",
104
+ "/*.jpg",
105
+ "/*.svg",
106
+ "/*.woff2",
107
+ ],
108
+ }
109
+ }
@@ -0,0 +1,79 @@
1
+ // Gorsee.js — Fly.io deploy adapter
2
+
3
+ export function generateFlyConfig(appName: string): string {
4
+ return `# Gorsee.js — Fly.io configuration
5
+ # Auto-generated by \`gorsee deploy fly\`
6
+
7
+ app = "${appName}"
8
+ primary_region = "iad"
9
+
10
+ [build]
11
+ dockerfile = "Dockerfile"
12
+
13
+ [env]
14
+ NODE_ENV = "production"
15
+ PORT = "3000"
16
+
17
+ [http_service]
18
+ internal_port = 3000
19
+ force_https = true
20
+ auto_stop_machines = "stop"
21
+ auto_start_machines = true
22
+ min_machines_running = 1
23
+
24
+ [http_service.concurrency]
25
+ type = "requests"
26
+ hard_limit = 250
27
+ soft_limit = 200
28
+
29
+ [[http_service.checks]]
30
+ grace_period = "10s"
31
+ interval = "30s"
32
+ method = "GET"
33
+ path = "/api/health"
34
+ timeout = "5s"
35
+
36
+ [[vm]]
37
+ size = "shared-cpu-1x"
38
+ memory = "512mb"
39
+ processes = ["app"]
40
+ count_min = 1
41
+ count_max = 3
42
+ `
43
+ }
44
+
45
+ export function generateFlyDockerfile(): string {
46
+ return `# Gorsee.js — Fly.io optimized Dockerfile
47
+ # Auto-generated by \`gorsee deploy fly\`
48
+
49
+ FROM oven/bun:1 AS builder
50
+ WORKDIR /app
51
+ COPY package.json bun.lock* ./
52
+ RUN bun install --frozen-lockfile
53
+ COPY . .
54
+ RUN bun run build
55
+
56
+ FROM oven/bun:1-slim
57
+ WORKDIR /app
58
+
59
+ COPY --from=builder /app/package.json ./
60
+ COPY --from=builder /app/node_modules ./node_modules
61
+ COPY --from=builder /app/dist ./dist
62
+ COPY --from=builder /app/routes ./routes
63
+ COPY --from=builder /app/public ./public
64
+ COPY --from=builder /app/.env* ./
65
+
66
+ ENV NODE_ENV=production
67
+ ENV PORT=3000
68
+
69
+ # Fly.io runtime env vars are injected automatically:
70
+ # FLY_ALLOC_ID, FLY_APP_NAME, FLY_REGION, FLY_MACHINE_ID
71
+
72
+ EXPOSE 3000
73
+
74
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \\
75
+ CMD curl -f http://localhost:3000/api/health || exit 1
76
+
77
+ CMD ["bun", "run", "start"]
78
+ `
79
+ }
@@ -0,0 +1,31 @@
1
+ // Gorsee.js — Deploy adapters barrel export
2
+
3
+ export {
4
+ generateDockerfile,
5
+ generateDockerignore,
6
+ } from "./dockerfile.ts"
7
+
8
+ export {
9
+ generateVercelConfig,
10
+ generateVercelServerlessEntry,
11
+ generateVercelBuildOutput,
12
+ type VercelConfig,
13
+ type VercelOutputConfig,
14
+ } from "./vercel.ts"
15
+
16
+ export {
17
+ generateFlyConfig,
18
+ generateFlyDockerfile,
19
+ } from "./fly.ts"
20
+
21
+ export {
22
+ generateWranglerConfig,
23
+ generateCloudflareEntry,
24
+ generateCloudflareStaticAssets,
25
+ type CloudflareRoutesConfig,
26
+ } from "./cloudflare.ts"
27
+
28
+ export {
29
+ generateNetlifyConfig,
30
+ generateNetlifyFunction,
31
+ } from "./netlify.ts"
@@ -0,0 +1,77 @@
1
+ // Gorsee.js — Netlify deploy adapter
2
+
3
+ export function generateNetlifyConfig(): string {
4
+ return `# Gorsee.js — Netlify configuration
5
+ # Auto-generated by \`gorsee deploy netlify\`
6
+
7
+ [build]
8
+ command = "bun run build"
9
+ publish = "dist/client"
10
+
11
+ [build.environment]
12
+ NODE_VERSION = "20"
13
+
14
+ [[edge_functions]]
15
+ function = "gorsee-handler"
16
+ path = "/*"
17
+
18
+ # Static assets bypass edge functions
19
+ [[edge_functions]]
20
+ function = "gorsee-handler"
21
+ path = "/_gorsee/*"
22
+ excludedPath = true
23
+
24
+ [[redirects]]
25
+ from = "/_gorsee/*"
26
+ to = "/client/:splat"
27
+ status = 200
28
+
29
+ [[redirects]]
30
+ from = "/*"
31
+ to = "/.netlify/edge-functions/gorsee-handler"
32
+ status = 200
33
+ force = false
34
+
35
+ [[headers]]
36
+ for = "/_gorsee/*"
37
+ [headers.values]
38
+ Cache-Control = "public, max-age=31536000, immutable"
39
+ `
40
+ }
41
+
42
+ export function generateNetlifyFunction(): string {
43
+ return `// Gorsee.js — Netlify Edge Function
44
+ // Auto-generated by \`gorsee deploy netlify\`
45
+ // Place in: netlify/edge-functions/gorsee-handler.ts
46
+
47
+ import type { Context } from "https://edge.netlify.com"
48
+
49
+ export default async function handler(
50
+ request: Request,
51
+ context: Context,
52
+ ): Promise<Response> {
53
+ const url = new URL(request.url)
54
+
55
+ // Skip static assets — handled by Netlify CDN
56
+ if (url.pathname.startsWith("/_gorsee/")) {
57
+ return context.next()
58
+ }
59
+
60
+ const staticExts = [".css", ".ico", ".svg", ".png", ".jpg", ".woff2", ".txt"]
61
+ if (staticExts.some((ext) => url.pathname.endsWith(ext))) {
62
+ return context.next()
63
+ }
64
+
65
+ // Forward to Gorsee server handler
66
+ try {
67
+ const { handleRequest } = await import("../../dist/server-handler.js")
68
+ return await handleRequest(request, { netlifyContext: context })
69
+ } catch (err) {
70
+ console.error("Gorsee handler error:", err)
71
+ return new Response("Internal Server Error", { status: 500 })
72
+ }
73
+ }
74
+
75
+ export const config = { path: "/*" }
76
+ `
77
+ }
@@ -0,0 +1,94 @@
1
+ // Gorsee.js — Vercel deploy adapter
2
+
3
+ export interface VercelConfig {
4
+ version: number
5
+ framework: null
6
+ buildCommand: string
7
+ outputDirectory: string
8
+ routes: Array<{ src: string; dest?: string; headers?: Record<string, string> }>
9
+ }
10
+
11
+ export function generateVercelConfig(): VercelConfig {
12
+ return {
13
+ version: 2,
14
+ framework: null,
15
+ buildCommand: "bun run build",
16
+ outputDirectory: ".vercel/output",
17
+ routes: [
18
+ {
19
+ src: "/_gorsee/(.*)",
20
+ headers: { "Cache-Control": "public, max-age=31536000, immutable" },
21
+ },
22
+ { src: "/(.*)", dest: "/api/index" },
23
+ ],
24
+ }
25
+ }
26
+
27
+ export function generateVercelServerlessEntry(): string {
28
+ return `// Gorsee.js — Vercel serverless entry
29
+ // Auto-generated by \`gorsee deploy vercel\`
30
+
31
+ import { startProductionServer } from "./dist/prod.js"
32
+
33
+ let initialized = false
34
+
35
+ async function ensureInit() {
36
+ if (!initialized) {
37
+ initialized = true
38
+ // Production server setup without Bun.serve()
39
+ }
40
+ }
41
+
42
+ export default async function handler(request: Request): Promise<Response> {
43
+ await ensureInit()
44
+
45
+ const url = new URL(request.url)
46
+
47
+ // Serve static client assets
48
+ if (url.pathname.startsWith("/_gorsee/")) {
49
+ return new Response(null, { status: 404 })
50
+ }
51
+
52
+ // Forward to Gorsee production handler
53
+ const { createRouter, matchRoute, buildStaticMap } = await import("./dist/router/index.js")
54
+ const { renderToString, ssrJsx } = await import("./dist/runtime/server.js")
55
+ const { handleRPCRequest } = await import("./dist/server/rpc.js")
56
+
57
+ // RPC handling
58
+ const rpcResponse = await handleRPCRequest(request)
59
+ if (rpcResponse) return rpcResponse
60
+
61
+ // Return placeholder — full implementation wires into prod.ts handler
62
+ return new Response("Gorsee on Vercel", {
63
+ headers: { "Content-Type": "text/html" },
64
+ })
65
+ }
66
+ `
67
+ }
68
+
69
+ export interface VercelOutputConfig {
70
+ version: number
71
+ routes: Array<{ src: string; dest?: string; headers?: Record<string, string> }>
72
+ }
73
+
74
+ export function generateVercelBuildOutput(routes: string[]): VercelOutputConfig {
75
+ const outputRoutes: VercelOutputConfig["routes"] = [
76
+ {
77
+ src: "/_gorsee/(.*)",
78
+ dest: "/static/_gorsee/$1",
79
+ headers: { "Cache-Control": "public, max-age=31536000, immutable" },
80
+ },
81
+ {
82
+ src: "/favicon\\.ico|/robots\\.txt|/styles\\.css",
83
+ dest: "/static/$0",
84
+ },
85
+ ]
86
+
87
+ for (const route of routes) {
88
+ outputRoutes.push({ src: route, dest: "/functions/index" })
89
+ }
90
+
91
+ outputRoutes.push({ src: "/(.*)", dest: "/functions/index" })
92
+
93
+ return { version: 3, routes: outputRoutes }
94
+ }
package/src/index.ts CHANGED
@@ -25,3 +25,4 @@ export { createEventSource } from "./server/sse.ts"
25
25
  export { createAuth } from "./auth/index.ts"
26
26
  export { typedLink, typedNavigate } from "./runtime/typed-routes.ts"
27
27
  export { defineForm, validateForm, fieldAttrs } from "./runtime/validated-form.ts"
28
+ export { definePlugin, createPluginRunner, type GorseePlugin, type PluginContext } from "./plugins/index.ts"
@@ -0,0 +1,84 @@
1
+ // Drizzle ORM integration plugin -- zero external dependencies
2
+
3
+ import type { MiddlewareFn, Context } from "../server/middleware.ts"
4
+ import type { GorseePlugin } from "./index.ts"
5
+ import { definePlugin } from "./index.ts"
6
+
7
+ export interface DrizzlePluginConfig {
8
+ schema: string
9
+ out: string
10
+ dialect: "sqlite" | "postgres" | "mysql"
11
+ connectionUrl?: string
12
+ }
13
+
14
+ let drizzleInstance: unknown = null
15
+
16
+ /** Returns the current drizzle instance (available after setup) */
17
+ export function getDrizzle<T = unknown>(): T {
18
+ if (!drizzleInstance) {
19
+ throw new Error("Drizzle not initialized. Did you register drizzlePlugin?")
20
+ }
21
+ return drizzleInstance as T
22
+ }
23
+
24
+ /** Middleware that attaches drizzle instance to ctx.locals.db */
25
+ export function drizzleMiddleware(instance: unknown): MiddlewareFn {
26
+ return async (ctx: Context, next) => {
27
+ ctx.locals.db = instance
28
+ return next()
29
+ }
30
+ }
31
+
32
+ /** Generates drizzle.config.ts content string */
33
+ export function generateDrizzleConfig(config: DrizzlePluginConfig): string {
34
+ const dbCredentials =
35
+ config.dialect === "sqlite"
36
+ ? `{ url: "${config.connectionUrl ?? "./data.db"}" }`
37
+ : `{ connectionString: "${config.connectionUrl ?? ""}" }`
38
+
39
+ return `import { defineConfig } from "drizzle-kit"
40
+
41
+ export default defineConfig({
42
+ schema: "${config.schema}",
43
+ out: "${config.out}",
44
+ dialect: "${config.dialect}",
45
+ dbCredentials: ${dbCredentials},
46
+ })
47
+ `
48
+ }
49
+
50
+ /** Creates a Drizzle ORM integration plugin */
51
+ export function drizzlePlugin(config: DrizzlePluginConfig): GorseePlugin {
52
+ return definePlugin({
53
+ name: "gorsee-drizzle",
54
+
55
+ async setup(app) {
56
+ if (config.dialect === "sqlite") {
57
+ const { Database } = await import("bun:sqlite" as string)
58
+ const db = new Database(config.connectionUrl ?? "./data.db")
59
+ db.exec("PRAGMA journal_mode=WAL")
60
+
61
+ // Dynamic import for drizzle-orm/bun-sqlite
62
+ try {
63
+ const { drizzle } = await import("drizzle-orm/bun-sqlite" as string)
64
+ drizzleInstance = drizzle(db)
65
+ } catch {
66
+ // If drizzle-orm not installed, store raw db
67
+ drizzleInstance = db
68
+ }
69
+ } else {
70
+ // For postgres/mysql, store connection URL for user to configure
71
+ drizzleInstance = { dialect: config.dialect, url: config.connectionUrl }
72
+ }
73
+
74
+ app.addMiddleware(drizzleMiddleware(drizzleInstance))
75
+ },
76
+
77
+ async teardown() {
78
+ if (drizzleInstance && typeof (drizzleInstance as any).close === "function") {
79
+ ;(drizzleInstance as any).close()
80
+ }
81
+ drizzleInstance = null
82
+ },
83
+ })
84
+ }