gorsee 0.2.0 → 0.2.2

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.
Files changed (52) hide show
  1. package/README.md +132 -4
  2. package/package.json +4 -2
  3. package/src/auth/index.ts +48 -17
  4. package/src/auth/redis-session-store.ts +46 -0
  5. package/src/auth/sqlite-session-store.ts +98 -0
  6. package/src/auth/store-utils.ts +21 -0
  7. package/src/build/client.ts +25 -7
  8. package/src/build/manifest.ts +34 -0
  9. package/src/build/route-metadata.ts +12 -0
  10. package/src/build/ssg.ts +19 -49
  11. package/src/cli/bun-plugin.ts +23 -2
  12. package/src/cli/cmd-build.ts +42 -71
  13. package/src/cli/cmd-check.ts +40 -26
  14. package/src/cli/cmd-create.ts +20 -5
  15. package/src/cli/cmd-deploy.ts +10 -2
  16. package/src/cli/cmd-dev.ts +9 -9
  17. package/src/cli/cmd-docs.ts +152 -0
  18. package/src/cli/cmd-generate.ts +15 -7
  19. package/src/cli/cmd-migrate.ts +15 -7
  20. package/src/cli/cmd-routes.ts +12 -5
  21. package/src/cli/cmd-start.ts +14 -5
  22. package/src/cli/cmd-test.ts +129 -0
  23. package/src/cli/cmd-typegen.ts +13 -3
  24. package/src/cli/cmd-upgrade.ts +143 -0
  25. package/src/cli/context.ts +12 -0
  26. package/src/cli/framework-md.ts +43 -16
  27. package/src/cli/index.ts +18 -0
  28. package/src/client.ts +26 -0
  29. package/src/dev/partial-handler.ts +17 -74
  30. package/src/dev/request-handler.ts +36 -67
  31. package/src/dev.ts +92 -157
  32. package/src/index-client.ts +4 -0
  33. package/src/index.ts +17 -2
  34. package/src/prod.ts +195 -253
  35. package/src/runtime/project.ts +73 -0
  36. package/src/server/cache-utils.ts +23 -0
  37. package/src/server/cache.ts +37 -14
  38. package/src/server/html-shell.ts +69 -0
  39. package/src/server/index.ts +40 -2
  40. package/src/server/manifest.ts +36 -0
  41. package/src/server/middleware.ts +18 -2
  42. package/src/server/not-found.ts +35 -0
  43. package/src/server/page-render.ts +123 -0
  44. package/src/server/redis-cache-store.ts +87 -0
  45. package/src/server/redis-client.ts +71 -0
  46. package/src/server/request-preflight.ts +45 -0
  47. package/src/server/route-request.ts +72 -0
  48. package/src/server/rpc-utils.ts +27 -0
  49. package/src/server/rpc.ts +70 -18
  50. package/src/server/sqlite-cache-store.ts +109 -0
  51. package/src/server/static-file.ts +63 -0
  52. package/src/server-entry.ts +36 -0
@@ -0,0 +1,152 @@
1
+ // gorsee docs -- generate API documentation from route files
2
+
3
+ import { join } from "node:path"
4
+ import { readFile, mkdir, writeFile } from "node:fs/promises"
5
+ import { createRouter, type Route } from "../router/scanner.ts"
6
+ import { createProjectContext, type RuntimeOptions } from "../runtime/project.ts"
7
+
8
+ const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"] as const
9
+
10
+ interface DocFlags {
11
+ output: string
12
+ format: "md" | "json" | "html"
13
+ routesOnly: boolean
14
+ }
15
+
16
+ interface RouteDoc {
17
+ path: string
18
+ methods: string[]
19
+ hasLoader: boolean
20
+ isApi: boolean
21
+ hasMiddleware: boolean
22
+ title: string
23
+ meta: Record<string, unknown> | null
24
+ }
25
+
26
+ export function parseDocsFlags(args: string[]): DocFlags {
27
+ const flags: DocFlags = { output: "docs/api.md", format: "md", routesOnly: false }
28
+
29
+ for (let i = 0; i < args.length; i++) {
30
+ const arg = args[i]!
31
+ if (arg === "--output" && args[i + 1]) flags.output = args[++i]!
32
+ else if (arg === "--format" && args[i + 1]) {
33
+ const fmt = args[++i]!
34
+ if (fmt === "md" || fmt === "json" || fmt === "html") flags.format = fmt
35
+ } else if (arg === "--routes-only") flags.routesOnly = true
36
+ }
37
+
38
+ return flags
39
+ }
40
+
41
+ async function extractRouteInfo(route: Route): Promise<RouteDoc> {
42
+ const content = await readFile(route.filePath, "utf-8")
43
+ const methods: string[] = []
44
+
45
+ for (const method of HTTP_METHODS) {
46
+ if (new RegExp(`export\\s+(async\\s+)?function\\s+${method}\\b`, "i").test(content)) {
47
+ methods.push(method)
48
+ }
49
+ }
50
+
51
+ const hasDefault = /export\s+default\s+/.test(content)
52
+ const hasLoader = /export\s+(async\s+)?function\s+loader\b/.test(content)
53
+ const isApi = !hasDefault || methods.length > 0
54
+
55
+ if (methods.length === 0 && hasDefault) methods.push("GET")
56
+ if (methods.length === 0 && hasLoader) methods.push("GET")
57
+
58
+ // Extract title from JSDoc or component name
59
+ let title = ""
60
+ const jsdocMatch = content.match(/\/\*\*\s*\n?\s*\*\s*(.+?)(?:\n|\*\/)/s)
61
+ if (jsdocMatch) title = jsdocMatch[1]!.trim()
62
+
63
+ // Extract meta export
64
+ let meta: Record<string, unknown> | null = null
65
+ const metaMatch = content.match(/export\s+const\s+meta\s*=\s*(\{[^}]+\})/)
66
+ if (metaMatch) {
67
+ try { meta = JSON.parse(metaMatch[1]!.replace(/'/g, '"')) } catch { /* skip */ }
68
+ }
69
+
70
+ return {
71
+ path: route.path,
72
+ methods,
73
+ hasLoader,
74
+ isApi,
75
+ hasMiddleware: route.middlewarePaths.length > 0,
76
+ title,
77
+ meta,
78
+ }
79
+ }
80
+
81
+ function generateMarkdown(docs: RouteDoc[]): string {
82
+ const lines = ["# API Documentation", "", "| Path | Methods | Type | Loader | Middleware |", "| --- | --- | --- | --- | --- |"]
83
+ for (const doc of docs) {
84
+ const type = doc.isApi ? "API" : "Page"
85
+ const mw = doc.hasMiddleware ? "Yes" : "-"
86
+ lines.push(`| ${doc.path} | ${doc.methods.join(", ")} | ${type} | ${doc.hasLoader ? "Yes" : "-"} | ${mw} |`)
87
+ }
88
+ return lines.join("\n") + "\n"
89
+ }
90
+
91
+ function generateJson(docs: RouteDoc[]): string {
92
+ return JSON.stringify(docs, null, 2) + "\n"
93
+ }
94
+
95
+ function generateHtml(docs: RouteDoc[]): string {
96
+ const rows = docs.map((d) => {
97
+ const type = d.isApi ? "API" : "Page"
98
+ const mw = d.hasMiddleware ? "Yes" : "-"
99
+ return `<tr><td>${d.path}</td><td>${d.methods.join(", ")}</td><td>${type}</td><td>${d.hasLoader ? "Yes" : "-"}</td><td>${mw}</td></tr>`
100
+ }).join("\n ")
101
+
102
+ return `<!DOCTYPE html>
103
+ <html><head><meta charset="utf-8"><title>API Docs</title>
104
+ <style>body{font-family:sans-serif;margin:2rem}table{border-collapse:collapse;width:100%}
105
+ th,td{border:1px solid #ddd;padding:8px;text-align:left}th{background:#f5f5f5}</style>
106
+ </head><body><h1>API Documentation</h1>
107
+ <table><thead><tr><th>Path</th><th>Methods</th><th>Type</th><th>Loader</th><th>Middleware</th></tr></thead>
108
+ <tbody>${rows}</tbody></table></body></html>\n`
109
+ }
110
+
111
+ const GENERATORS: Record<string, (docs: RouteDoc[]) => string> = {
112
+ md: generateMarkdown,
113
+ json: generateJson,
114
+ html: generateHtml,
115
+ }
116
+
117
+ export interface DocsCommandOptions extends RuntimeOptions {}
118
+
119
+ export async function generateDocs(args: string[], options: DocsCommandOptions = {}) {
120
+ const { cwd, paths } = createProjectContext(options)
121
+ const flags = parseDocsFlags(args)
122
+
123
+ const routes = await createRouter(paths.routesDir)
124
+ if (routes.length === 0) {
125
+ console.log("\n No routes found in routes/\n")
126
+ return
127
+ }
128
+
129
+ const docs: RouteDoc[] = []
130
+ for (const route of routes) {
131
+ const info = await extractRouteInfo(route)
132
+ if (flags.routesOnly && info.isApi) continue
133
+ docs.push(info)
134
+ }
135
+
136
+ const generate = GENERATORS[flags.format]!
137
+ const content = generate(docs)
138
+
139
+ const outputPath = join(cwd, flags.output)
140
+ await mkdir(join(outputPath, ".."), { recursive: true })
141
+ await writeFile(outputPath, content, "utf-8")
142
+
143
+ console.log(`\n Generated docs for ${docs.length} routes -> ${flags.output}\n`)
144
+ }
145
+
146
+ /** @deprecated Use generateDocs() for programmatic access. */
147
+ export async function runDocs(args: string[], options: DocsCommandOptions = {}) {
148
+ return generateDocs(args, options)
149
+ }
150
+
151
+ export { extractRouteInfo, generateMarkdown, generateJson, generateHtml }
152
+ export type { RouteDoc, DocFlags }
@@ -4,6 +4,7 @@
4
4
  import { join } from "node:path"
5
5
  import { mkdir, writeFile } from "node:fs/promises"
6
6
  import { createMigration } from "../db/migrate.ts"
7
+ import { createProjectContext, type RuntimeOptions } from "../runtime/project.ts"
7
8
 
8
9
  function capitalize(s: string): string {
9
10
  return s.charAt(0).toUpperCase() + s.slice(1)
@@ -18,7 +19,7 @@ function singularize(s: string): string {
18
19
 
19
20
  function listRoute(entity: string, singular: string): string {
20
21
  const cap = capitalize(singular)
21
- return `import { Head, Link } from "gorsee"
22
+ return `import { Head, Link } from "gorsee/client"
22
23
  import { SafeSQL } from "gorsee/types"
23
24
  import type { Context } from "gorsee/server"
24
25
 
@@ -49,7 +50,7 @@ export default function ${capitalize(entity)}ListPage(props: { data: { ${entity}
49
50
 
50
51
  function detailRoute(entity: string, singular: string): string {
51
52
  const cap = capitalize(singular)
52
- return `import { Head, Link } from "gorsee"
53
+ return `import { Head, Link } from "gorsee/client"
53
54
  import type { Context } from "gorsee/server"
54
55
 
55
56
  export async function loader(ctx: Context) {
@@ -73,7 +74,7 @@ export default function ${cap}DetailPage(props: { data: { ${singular}: { id: num
73
74
 
74
75
  function newRoute(entity: string, singular: string): string {
75
76
  const cap = capitalize(singular)
76
- return `import { Head, Link } from "gorsee"
77
+ return `import { Head, Link } from "gorsee/client"
77
78
  import { defineAction, parseFormData } from "gorsee/server"
78
79
 
79
80
  export const action = defineAction(async (ctx) => {
@@ -109,7 +110,9 @@ CREATE TABLE IF NOT EXISTS ${entity} (
109
110
  `
110
111
  }
111
112
 
112
- export async function runGenerate(args: string[]) {
113
+ export interface GenerateCommandOptions extends RuntimeOptions {}
114
+
115
+ export async function generateCrudScaffold(args: string[], options: GenerateCommandOptions = {}) {
113
116
  const entity = args[0]
114
117
  if (!entity) {
115
118
  console.error("Usage: gorsee generate <entity-name>")
@@ -117,9 +120,9 @@ export async function runGenerate(args: string[]) {
117
120
  process.exit(1)
118
121
  }
119
122
 
120
- const cwd = process.cwd()
123
+ const { cwd, paths } = createProjectContext(options)
121
124
  const singular = singularize(entity)
122
- const routeDir = join(cwd, "routes", entity)
125
+ const routeDir = join(paths.routesDir, entity)
123
126
 
124
127
  console.log(`\n Generating CRUD for: ${entity}\n`)
125
128
 
@@ -130,7 +133,7 @@ export async function runGenerate(args: string[]) {
130
133
  await writeFile(join(routeDir, "new.tsx"), newRoute(entity, singular))
131
134
 
132
135
  // Create migration
133
- const migrationDir = join(cwd, "migrations")
136
+ const migrationDir = paths.migrationsDir
134
137
  await mkdir(migrationDir, { recursive: true })
135
138
  const migrationFile = await createMigration(migrationDir, `create_${entity}`)
136
139
  const migrationPath = join(migrationDir, migrationFile)
@@ -145,3 +148,8 @@ export async function runGenerate(args: string[]) {
145
148
  console.log(" Next: run `gorsee migrate` to apply the migration")
146
149
  console.log()
147
150
  }
151
+
152
+ /** @deprecated Use generateCrudScaffold() for programmatic access. */
153
+ export async function runGenerate(args: string[], options: GenerateCommandOptions = {}) {
154
+ return generateCrudScaffold(args, options)
155
+ }
@@ -1,12 +1,15 @@
1
1
  // gorsee migrate -- run database migrations
2
2
  // gorsee migrate create <name> -- create new migration file
3
3
 
4
- import { join } from "node:path"
5
4
  import { runMigrations, createMigration } from "../db/migrate.ts"
5
+ import { createProjectContext, type RuntimeOptions } from "../runtime/project.ts"
6
6
 
7
- export async function runMigrate(args: string[]) {
8
- const cwd = process.cwd()
9
- const migrationsDir = join(cwd, "migrations")
7
+ export interface MigrateCommandOptions extends RuntimeOptions {
8
+ dbPath?: string
9
+ }
10
+
11
+ export async function runProjectMigrations(args: string[], options: MigrateCommandOptions = {}) {
12
+ const { env, paths } = createProjectContext(options)
10
13
  const subcommand = args[0]
11
14
 
12
15
  if (subcommand === "create") {
@@ -15,16 +18,16 @@ export async function runMigrate(args: string[]) {
15
18
  console.error("Usage: gorsee migrate create <migration-name>")
16
19
  process.exit(1)
17
20
  }
18
- const filename = await createMigration(migrationsDir, name)
21
+ const filename = await createMigration(paths.migrationsDir, name)
19
22
  console.log(`\n Created: migrations/${filename}\n`)
20
23
  return
21
24
  }
22
25
 
23
26
  // Default: run pending migrations
24
- const dbPath = process.env.DATABASE_URL ?? join(cwd, "data.sqlite")
27
+ const dbPath = options.dbPath ?? env.DATABASE_URL ?? paths.dataFile
25
28
  console.log("\n Running migrations...\n")
26
29
 
27
- const result = await runMigrations(dbPath, migrationsDir)
30
+ const result = await runMigrations(dbPath, paths.migrationsDir)
28
31
 
29
32
  if (result.applied.length > 0) {
30
33
  console.log(" Applied:")
@@ -43,3 +46,8 @@ export async function runMigrate(args: string[]) {
43
46
 
44
47
  console.log(`\n Done: ${result.applied.length} migration(s) applied\n`)
45
48
  }
49
+
50
+ /** @deprecated Use runProjectMigrations() for programmatic access. */
51
+ export async function runMigrate(args: string[], options: MigrateCommandOptions = {}) {
52
+ return runProjectMigrations(args, options)
53
+ }
@@ -1,11 +1,13 @@
1
1
  // gorsee routes -- list all routes
2
2
 
3
- import { join } from "node:path"
4
3
  import { createRouter } from "../router/scanner.ts"
4
+ import { createProjectContext, type RuntimeOptions } from "../runtime/project.ts"
5
5
 
6
- export async function runRoutes(_args: string[]) {
7
- const routesDir = join(process.cwd(), "routes")
8
- const routes = await createRouter(routesDir)
6
+ export interface RoutesCommandOptions extends RuntimeOptions {}
7
+
8
+ export async function listRoutes(options: RoutesCommandOptions = {}) {
9
+ const { cwd, paths } = createProjectContext(options)
10
+ const routes = await createRouter(paths.routesDir)
9
11
 
10
12
  if (routes.length === 0) {
11
13
  console.log("\n No routes found in routes/\n")
@@ -21,9 +23,14 @@ export async function runRoutes(_args: string[]) {
21
23
  console.log(
22
24
  " " +
23
25
  route.path.padEnd(25) +
24
- route.filePath.replace(process.cwd() + "/", "").padEnd(40) +
26
+ route.filePath.replace(cwd + "/", "").padEnd(40) +
25
27
  params
26
28
  )
27
29
  }
28
30
  console.log()
29
31
  }
32
+
33
+ /** @deprecated Use listRoutes() for programmatic access. */
34
+ export async function runRoutes(_args: string[], options: RoutesCommandOptions = {}) {
35
+ return listRoutes(options)
36
+ }
@@ -2,14 +2,18 @@
2
2
 
3
3
  import { join } from "node:path"
4
4
  import { stat } from "node:fs/promises"
5
+ import { createProjectContext, type RuntimeOptions } from "../runtime/project.ts"
5
6
 
6
- export async function runStart(_args: string[]) {
7
- const cwd = process.cwd()
8
- const distDir = join(cwd, "dist")
7
+ export interface StartCommandOptions extends RuntimeOptions {
8
+ port?: number
9
+ }
10
+
11
+ export async function startBuiltProject(options: StartCommandOptions = {}) {
12
+ const { paths } = createProjectContext(options)
9
13
 
10
14
  // Verify build exists
11
15
  try {
12
- await stat(join(distDir, "manifest.json"))
16
+ await stat(join(paths.distDir, "manifest.json"))
13
17
  } catch {
14
18
  console.error("\n Error: No production build found.")
15
19
  console.error(" Run `gorsee build` first.\n")
@@ -17,5 +21,10 @@ export async function runStart(_args: string[]) {
17
21
  }
18
22
 
19
23
  const { startProductionServer } = await import("../prod.ts")
20
- await startProductionServer()
24
+ await startProductionServer({ cwd: paths.cwd, port: options.port })
25
+ }
26
+
27
+ /** @deprecated Use startBuiltProject() for programmatic access. */
28
+ export async function runStart(_args: string[], options: StartCommandOptions = {}) {
29
+ return startBuiltProject(options)
21
30
  }
@@ -0,0 +1,129 @@
1
+ // gorsee test -- smart test runner wrapping bun test
2
+
3
+ import { readdir, stat } from "node:fs/promises"
4
+ import { join } from "node:path"
5
+ import { createProjectContext, type RuntimeOptions } from "../runtime/project.ts"
6
+
7
+ interface TestFlags {
8
+ watch: boolean
9
+ coverage: boolean
10
+ filter: string | null
11
+ e2e: boolean
12
+ unit: boolean
13
+ integration: boolean
14
+ }
15
+
16
+ function parseFlags(args: string[]): TestFlags {
17
+ const flags: TestFlags = {
18
+ watch: false,
19
+ coverage: false,
20
+ filter: null,
21
+ e2e: false,
22
+ unit: false,
23
+ integration: false,
24
+ }
25
+
26
+ for (let i = 0; i < args.length; i++) {
27
+ const arg = args[i]!
28
+ if (arg === "--watch") flags.watch = true
29
+ else if (arg === "--coverage") flags.coverage = true
30
+ else if (arg === "--filter" && args[i + 1]) flags.filter = args[++i]!
31
+ else if (arg === "--e2e") flags.e2e = true
32
+ else if (arg === "--unit") flags.unit = true
33
+ else if (arg === "--integration") flags.integration = true
34
+ }
35
+
36
+ return flags
37
+ }
38
+
39
+ async function findTestFiles(dir: string, pattern: RegExp): Promise<string[]> {
40
+ const results: string[] = []
41
+ let entries: string[]
42
+ try {
43
+ entries = await readdir(dir)
44
+ } catch {
45
+ return results
46
+ }
47
+
48
+ for (const entry of entries) {
49
+ if (entry === "node_modules" || entry === "dist") continue
50
+ const fullPath = join(dir, entry)
51
+ const s = await stat(fullPath)
52
+ if (s.isDirectory()) {
53
+ results.push(...(await findTestFiles(fullPath, pattern)))
54
+ } else if (pattern.test(fullPath)) {
55
+ results.push(fullPath)
56
+ }
57
+ }
58
+ return results
59
+ }
60
+
61
+ function getTestPattern(flags: TestFlags): RegExp {
62
+ if (flags.e2e) return /(?:\.e2e\.test\.ts$|e2e\/.*\.test\.ts$)/
63
+ if (flags.integration) return /(?:\.integration\.test\.ts$|tests\/integration\/.*\.test\.ts$)/
64
+ if (flags.unit) return /(?:\.unit\.test\.ts$|tests\/unit\/.*\.test\.ts$)/
65
+ return /\.test\.ts$/
66
+ }
67
+
68
+ /** Build bun test args from parsed flags */
69
+ export function buildTestArgs(flags: TestFlags, files: string[]): string[] {
70
+ const bunArgs = ["test"]
71
+
72
+ if (flags.watch) bunArgs.push("--watch")
73
+ if (flags.coverage) bunArgs.push("--coverage")
74
+ if (flags.filter) {
75
+ bunArgs.push("--bail", "--filter", flags.filter)
76
+ }
77
+
78
+ bunArgs.push(...files)
79
+ return bunArgs
80
+ }
81
+
82
+ export interface TestCommandOptions extends RuntimeOptions {}
83
+
84
+ export async function runTests(args: string[], options: TestCommandOptions = {}) {
85
+ const { cwd, env } = createProjectContext(options)
86
+ const flags = parseFlags(args)
87
+ const pattern = getTestPattern(flags)
88
+
89
+ const files = await findTestFiles(cwd, pattern)
90
+
91
+ if (files.length === 0) {
92
+ const kind = flags.e2e ? "e2e" : flags.integration ? "integration" : flags.unit ? "unit" : "any"
93
+ console.log(`\n No ${kind} test files found.\n`)
94
+ return
95
+ }
96
+
97
+ console.log(`\n Running ${files.length} test file(s)...\n`)
98
+
99
+ const bunArgs = buildTestArgs(flags, files)
100
+
101
+ const proc = Bun.spawn(["bun", ...bunArgs], {
102
+ cwd,
103
+ stdout: "inherit",
104
+ stderr: "inherit",
105
+ env: {
106
+ ...env,
107
+ NODE_ENV: "test",
108
+ GORSEE_TEST: "1",
109
+ },
110
+ })
111
+
112
+ const exitCode = await proc.exited
113
+
114
+ console.log()
115
+ if (exitCode === 0) {
116
+ console.log(` Tests passed (${files.length} file(s))`)
117
+ } else {
118
+ console.log(` Tests failed (exit code ${exitCode})`)
119
+ process.exit(exitCode)
120
+ }
121
+ }
122
+
123
+ /** @deprecated Use runTests() for programmatic access. */
124
+ export async function runTest(args: string[], options: TestCommandOptions = {}) {
125
+ return runTests(args, options)
126
+ }
127
+
128
+ // Re-export for testing
129
+ export { parseFlags, findTestFiles, getTestPattern }
@@ -4,6 +4,7 @@
4
4
  import { join } from "node:path"
5
5
  import { mkdir, writeFile } from "node:fs/promises"
6
6
  import { createRouter, type Route } from "../router/index.ts"
7
+ import { createProjectContext, type RuntimeOptions } from "../runtime/project.ts"
7
8
 
8
9
  const OUTPUT_DIR = ".gorsee"
9
10
  const TYPES_FILE = "routes.d.ts"
@@ -54,9 +55,13 @@ function generateDeclaration(routes: Route[]): string {
54
55
  return lines.join("\n")
55
56
  }
56
57
 
57
- export async function runTypegen(args: string[]): Promise<void> {
58
- const cwd = process.cwd()
59
- const routesDir = args[0] ?? join(cwd, "routes")
58
+ export interface TypegenCommandOptions extends RuntimeOptions {
59
+ routesDir?: string
60
+ }
61
+
62
+ export async function generateRouteTypes(args: string[], options: TypegenCommandOptions = {}): Promise<void> {
63
+ const { cwd, paths } = createProjectContext(options)
64
+ const routesDir = options.routesDir ?? args[0] ?? paths.routesDir
60
65
 
61
66
  console.log(`Scanning routes in: ${routesDir}`)
62
67
 
@@ -81,3 +86,8 @@ export async function runTypegen(args: string[]): Promise<void> {
81
86
  console.log(` ${route.path}${params}`)
82
87
  }
83
88
  }
89
+
90
+ /** @deprecated Use generateRouteTypes() for programmatic access. */
91
+ export async function runTypegen(args: string[], options: TypegenCommandOptions = {}): Promise<void> {
92
+ return generateRouteTypes(args, options)
93
+ }
@@ -0,0 +1,143 @@
1
+ // gorsee upgrade -- upgrade framework version with migration guidance
2
+
3
+ import { readFile } from "node:fs/promises"
4
+ import { join } from "node:path"
5
+ import { createProjectContext, type RuntimeOptions } from "../runtime/project.ts"
6
+
7
+ interface UpgradeFlags {
8
+ check: boolean
9
+ force: boolean
10
+ }
11
+
12
+ export function parseUpgradeFlags(args: string[]): UpgradeFlags {
13
+ const flags: UpgradeFlags = { check: false, force: false }
14
+ for (const arg of args) {
15
+ if (arg === "--check") flags.check = true
16
+ else if (arg === "--force") flags.force = true
17
+ }
18
+ return flags
19
+ }
20
+
21
+ /** Compare semver strings: -1 (a < b), 0 (equal), 1 (a > b) */
22
+ export function compareVersions(a: string, b: string): number {
23
+ const pa = a.replace(/^v/, "").split(".").map(Number)
24
+ const pb = b.replace(/^v/, "").split(".").map(Number)
25
+ for (let i = 0; i < 3; i++) {
26
+ const va = pa[i] ?? 0
27
+ const vb = pb[i] ?? 0
28
+ if (va < vb) return -1
29
+ if (va > vb) return 1
30
+ }
31
+ return 0
32
+ }
33
+
34
+ async function getCurrentVersion(cwd: string): Promise<string | null> {
35
+ try {
36
+ const pkg = await readFile(join(cwd, "node_modules/gorsee/package.json"), "utf-8")
37
+ return JSON.parse(pkg).version ?? null
38
+ } catch {
39
+ return null
40
+ }
41
+ }
42
+
43
+ export const NPM_REGISTRY_URL = "https://registry.npmjs.org/gorsee/latest"
44
+
45
+ async function fetchLatestVersion(): Promise<string | null> {
46
+ try {
47
+ const res = await fetch(NPM_REGISTRY_URL)
48
+ if (!res.ok) return null
49
+ const data = (await res.json()) as { version?: string }
50
+ return data.version ?? null
51
+ } catch {
52
+ return null
53
+ }
54
+ }
55
+
56
+ async function checkMigrationHints(cwd: string): Promise<string[]> {
57
+ const hints: string[] = []
58
+
59
+ // Check tsconfig.json jsx setting
60
+ try {
61
+ const tsconfig = await readFile(join(cwd, "tsconfig.json"), "utf-8")
62
+ const parsed = JSON.parse(tsconfig)
63
+ if (parsed.compilerOptions?.jsx !== "react-jsx") {
64
+ hints.push("tsconfig.json: set compilerOptions.jsx to \"react-jsx\"")
65
+ }
66
+ } catch { /* no tsconfig */ }
67
+
68
+ // Check for deprecated app.config.ts keys
69
+ try {
70
+ const config = await readFile(join(cwd, "app.config.ts"), "utf-8")
71
+ if (config.includes("ssr:")) {
72
+ hints.push("app.config.ts: 'ssr' key is deprecated, use 'rendering' instead")
73
+ }
74
+ } catch { /* no config */ }
75
+
76
+ return hints
77
+ }
78
+
79
+ export interface UpgradeCommandOptions extends RuntimeOptions {}
80
+
81
+ export async function upgradeFramework(args: string[], options: UpgradeCommandOptions = {}) {
82
+ const { cwd } = createProjectContext(options)
83
+ const flags = parseUpgradeFlags(args)
84
+
85
+ const current = await getCurrentVersion(cwd)
86
+ if (!current) {
87
+ console.log("\n Gorsee.js not found in node_modules. Run: bun add gorsee\n")
88
+ return
89
+ }
90
+
91
+ console.log(`\n Current version: v${current}`)
92
+
93
+ const latest = await fetchLatestVersion()
94
+ if (!latest) {
95
+ console.log(" Could not fetch latest version from npm registry.\n")
96
+ return
97
+ }
98
+
99
+ if (compareVersions(current, latest) >= 0) {
100
+ console.log(` Already up to date (v${current})\n`)
101
+ return
102
+ }
103
+
104
+ console.log(` Latest version: v${latest}`)
105
+ console.log(` Upgrade: v${current} -> v${latest}`)
106
+
107
+ if (flags.check) {
108
+ console.log()
109
+ return
110
+ }
111
+
112
+ if (!flags.force) {
113
+ console.log("\n Run with --force to install, or confirm interactively.")
114
+ }
115
+
116
+ console.log("\n Installing gorsee@latest...")
117
+ const proc = Bun.spawn(["bun", "add", "gorsee@latest"], {
118
+ cwd,
119
+ stdout: "inherit",
120
+ stderr: "inherit",
121
+ })
122
+ const exitCode = await proc.exited
123
+
124
+ if (exitCode !== 0) {
125
+ console.log(`\n Install failed (exit code ${exitCode})\n`)
126
+ process.exit(exitCode)
127
+ }
128
+
129
+ const hints = await checkMigrationHints(cwd)
130
+ if (hints.length > 0) {
131
+ console.log("\n Migration hints:")
132
+ for (const hint of hints) {
133
+ console.log(` - ${hint}`)
134
+ }
135
+ }
136
+
137
+ console.log(`\n Upgraded successfully to v${latest}\n`)
138
+ }
139
+
140
+ /** @deprecated Use upgradeFramework() for programmatic access. */
141
+ export async function runUpgrade(args: string[], options: UpgradeCommandOptions = {}) {
142
+ return upgradeFramework(args, options)
143
+ }
@@ -0,0 +1,12 @@
1
+ /** @deprecated Import shared project context from "../runtime/project" in new code. */
2
+ export {
3
+ createProjectContext as createCommandContext,
4
+ resolveProjectPaths,
5
+ } from "../runtime/project.ts"
6
+
7
+ /** @deprecated Import shared runtime types from "../runtime/project" in new code. */
8
+ export type {
9
+ RuntimeOptions as CommandRuntimeOptions,
10
+ ProjectContext as CommandContext,
11
+ ProjectPaths,
12
+ } from "../runtime/project.ts"