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.
- package/README.md +132 -4
- package/package.json +4 -2
- package/src/auth/index.ts +48 -17
- package/src/auth/redis-session-store.ts +46 -0
- package/src/auth/sqlite-session-store.ts +98 -0
- package/src/auth/store-utils.ts +21 -0
- package/src/build/client.ts +25 -7
- package/src/build/manifest.ts +34 -0
- package/src/build/route-metadata.ts +12 -0
- package/src/build/ssg.ts +19 -49
- package/src/cli/bun-plugin.ts +23 -2
- package/src/cli/cmd-build.ts +42 -71
- package/src/cli/cmd-check.ts +40 -26
- package/src/cli/cmd-create.ts +20 -5
- package/src/cli/cmd-deploy.ts +10 -2
- package/src/cli/cmd-dev.ts +9 -9
- package/src/cli/cmd-docs.ts +152 -0
- package/src/cli/cmd-generate.ts +15 -7
- package/src/cli/cmd-migrate.ts +15 -7
- package/src/cli/cmd-routes.ts +12 -5
- package/src/cli/cmd-start.ts +14 -5
- package/src/cli/cmd-test.ts +129 -0
- package/src/cli/cmd-typegen.ts +13 -3
- package/src/cli/cmd-upgrade.ts +143 -0
- package/src/cli/context.ts +12 -0
- package/src/cli/framework-md.ts +43 -16
- package/src/cli/index.ts +18 -0
- package/src/client.ts +26 -0
- package/src/dev/partial-handler.ts +17 -74
- package/src/dev/request-handler.ts +36 -67
- package/src/dev.ts +92 -157
- package/src/index-client.ts +4 -0
- package/src/index.ts +17 -2
- package/src/prod.ts +195 -253
- package/src/runtime/project.ts +73 -0
- package/src/server/cache-utils.ts +23 -0
- package/src/server/cache.ts +37 -14
- package/src/server/html-shell.ts +69 -0
- package/src/server/index.ts +40 -2
- package/src/server/manifest.ts +36 -0
- package/src/server/middleware.ts +18 -2
- package/src/server/not-found.ts +35 -0
- package/src/server/page-render.ts +123 -0
- package/src/server/redis-cache-store.ts +87 -0
- package/src/server/redis-client.ts +71 -0
- package/src/server/request-preflight.ts +45 -0
- package/src/server/route-request.ts +72 -0
- package/src/server/rpc-utils.ts +27 -0
- package/src/server/rpc.ts +70 -18
- package/src/server/sqlite-cache-store.ts +109 -0
- package/src/server/static-file.ts +63 -0
- 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 }
|
package/src/cli/cmd-generate.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
123
|
+
const { cwd, paths } = createProjectContext(options)
|
|
121
124
|
const singular = singularize(entity)
|
|
122
|
-
const routeDir = join(
|
|
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 =
|
|
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
|
+
}
|
package/src/cli/cmd-migrate.ts
CHANGED
|
@@ -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
|
|
8
|
-
|
|
9
|
-
|
|
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 =
|
|
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
|
+
}
|
package/src/cli/cmd-routes.ts
CHANGED
|
@@ -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
|
|
7
|
-
|
|
8
|
-
|
|
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(
|
|
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
|
+
}
|
package/src/cli/cmd-start.ts
CHANGED
|
@@ -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
|
|
7
|
-
|
|
8
|
-
|
|
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 }
|
package/src/cli/cmd-typegen.ts
CHANGED
|
@@ -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
|
|
58
|
-
|
|
59
|
-
|
|
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"
|