gorsee 0.2.0 → 0.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gorsee",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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",
@@ -0,0 +1,145 @@
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
+
7
+ const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"] as const
8
+
9
+ interface DocFlags {
10
+ output: string
11
+ format: "md" | "json" | "html"
12
+ routesOnly: boolean
13
+ }
14
+
15
+ interface RouteDoc {
16
+ path: string
17
+ methods: string[]
18
+ hasLoader: boolean
19
+ isApi: boolean
20
+ hasMiddleware: boolean
21
+ title: string
22
+ meta: Record<string, unknown> | null
23
+ }
24
+
25
+ export function parseDocsFlags(args: string[]): DocFlags {
26
+ const flags: DocFlags = { output: "docs/api.md", format: "md", routesOnly: false }
27
+
28
+ for (let i = 0; i < args.length; i++) {
29
+ const arg = args[i]!
30
+ if (arg === "--output" && args[i + 1]) flags.output = args[++i]!
31
+ else if (arg === "--format" && args[i + 1]) {
32
+ const fmt = args[++i]!
33
+ if (fmt === "md" || fmt === "json" || fmt === "html") flags.format = fmt
34
+ } else if (arg === "--routes-only") flags.routesOnly = true
35
+ }
36
+
37
+ return flags
38
+ }
39
+
40
+ async function extractRouteInfo(route: Route): Promise<RouteDoc> {
41
+ const content = await readFile(route.filePath, "utf-8")
42
+ const methods: string[] = []
43
+
44
+ for (const method of HTTP_METHODS) {
45
+ if (new RegExp(`export\\s+(async\\s+)?function\\s+${method}\\b`, "i").test(content)) {
46
+ methods.push(method)
47
+ }
48
+ }
49
+
50
+ const hasDefault = /export\s+default\s+/.test(content)
51
+ const hasLoader = /export\s+(async\s+)?function\s+loader\b/.test(content)
52
+ const isApi = !hasDefault || methods.length > 0
53
+
54
+ if (methods.length === 0 && hasDefault) methods.push("GET")
55
+ if (methods.length === 0 && hasLoader) methods.push("GET")
56
+
57
+ // Extract title from JSDoc or component name
58
+ let title = ""
59
+ const jsdocMatch = content.match(/\/\*\*\s*\n?\s*\*\s*(.+?)(?:\n|\*\/)/s)
60
+ if (jsdocMatch) title = jsdocMatch[1]!.trim()
61
+
62
+ // Extract meta export
63
+ let meta: Record<string, unknown> | null = null
64
+ const metaMatch = content.match(/export\s+const\s+meta\s*=\s*(\{[^}]+\})/)
65
+ if (metaMatch) {
66
+ try { meta = JSON.parse(metaMatch[1]!.replace(/'/g, '"')) } catch { /* skip */ }
67
+ }
68
+
69
+ return {
70
+ path: route.path,
71
+ methods,
72
+ hasLoader,
73
+ isApi,
74
+ hasMiddleware: route.middlewarePaths.length > 0,
75
+ title,
76
+ meta,
77
+ }
78
+ }
79
+
80
+ function generateMarkdown(docs: RouteDoc[]): string {
81
+ const lines = ["# API Documentation", "", "| Path | Methods | Type | Loader | Middleware |", "| --- | --- | --- | --- | --- |"]
82
+ for (const doc of docs) {
83
+ const type = doc.isApi ? "API" : "Page"
84
+ const mw = doc.hasMiddleware ? "Yes" : "-"
85
+ lines.push(`| ${doc.path} | ${doc.methods.join(", ")} | ${type} | ${doc.hasLoader ? "Yes" : "-"} | ${mw} |`)
86
+ }
87
+ return lines.join("\n") + "\n"
88
+ }
89
+
90
+ function generateJson(docs: RouteDoc[]): string {
91
+ return JSON.stringify(docs, null, 2) + "\n"
92
+ }
93
+
94
+ function generateHtml(docs: RouteDoc[]): string {
95
+ const rows = docs.map((d) => {
96
+ const type = d.isApi ? "API" : "Page"
97
+ const mw = d.hasMiddleware ? "Yes" : "-"
98
+ return `<tr><td>${d.path}</td><td>${d.methods.join(", ")}</td><td>${type}</td><td>${d.hasLoader ? "Yes" : "-"}</td><td>${mw}</td></tr>`
99
+ }).join("\n ")
100
+
101
+ return `<!DOCTYPE html>
102
+ <html><head><meta charset="utf-8"><title>API Docs</title>
103
+ <style>body{font-family:sans-serif;margin:2rem}table{border-collapse:collapse;width:100%}
104
+ th,td{border:1px solid #ddd;padding:8px;text-align:left}th{background:#f5f5f5}</style>
105
+ </head><body><h1>API Documentation</h1>
106
+ <table><thead><tr><th>Path</th><th>Methods</th><th>Type</th><th>Loader</th><th>Middleware</th></tr></thead>
107
+ <tbody>${rows}</tbody></table></body></html>\n`
108
+ }
109
+
110
+ const GENERATORS: Record<string, (docs: RouteDoc[]) => string> = {
111
+ md: generateMarkdown,
112
+ json: generateJson,
113
+ html: generateHtml,
114
+ }
115
+
116
+ export async function runDocs(args: string[]) {
117
+ const cwd = process.cwd()
118
+ const flags = parseDocsFlags(args)
119
+ const routesDir = join(cwd, "routes")
120
+
121
+ const routes = await createRouter(routesDir)
122
+ if (routes.length === 0) {
123
+ console.log("\n No routes found in routes/\n")
124
+ return
125
+ }
126
+
127
+ const docs: RouteDoc[] = []
128
+ for (const route of routes) {
129
+ const info = await extractRouteInfo(route)
130
+ if (flags.routesOnly && info.isApi) continue
131
+ docs.push(info)
132
+ }
133
+
134
+ const generate = GENERATORS[flags.format]!
135
+ const content = generate(docs)
136
+
137
+ const outputPath = join(cwd, flags.output)
138
+ await mkdir(join(outputPath, ".."), { recursive: true })
139
+ await writeFile(outputPath, content, "utf-8")
140
+
141
+ console.log(`\n Generated docs for ${docs.length} routes -> ${flags.output}\n`)
142
+ }
143
+
144
+ export { extractRouteInfo, generateMarkdown, generateJson, generateHtml }
145
+ export type { RouteDoc, DocFlags }
@@ -0,0 +1,121 @@
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
+
6
+ interface TestFlags {
7
+ watch: boolean
8
+ coverage: boolean
9
+ filter: string | null
10
+ e2e: boolean
11
+ unit: boolean
12
+ integration: boolean
13
+ }
14
+
15
+ function parseFlags(args: string[]): TestFlags {
16
+ const flags: TestFlags = {
17
+ watch: false,
18
+ coverage: false,
19
+ filter: null,
20
+ e2e: false,
21
+ unit: false,
22
+ integration: false,
23
+ }
24
+
25
+ for (let i = 0; i < args.length; i++) {
26
+ const arg = args[i]!
27
+ if (arg === "--watch") flags.watch = true
28
+ else if (arg === "--coverage") flags.coverage = true
29
+ else if (arg === "--filter" && args[i + 1]) flags.filter = args[++i]!
30
+ else if (arg === "--e2e") flags.e2e = true
31
+ else if (arg === "--unit") flags.unit = true
32
+ else if (arg === "--integration") flags.integration = true
33
+ }
34
+
35
+ return flags
36
+ }
37
+
38
+ async function findTestFiles(dir: string, pattern: RegExp): Promise<string[]> {
39
+ const results: string[] = []
40
+ let entries: string[]
41
+ try {
42
+ entries = await readdir(dir)
43
+ } catch {
44
+ return results
45
+ }
46
+
47
+ for (const entry of entries) {
48
+ if (entry === "node_modules" || entry === "dist") continue
49
+ const fullPath = join(dir, entry)
50
+ const s = await stat(fullPath)
51
+ if (s.isDirectory()) {
52
+ results.push(...(await findTestFiles(fullPath, pattern)))
53
+ } else if (pattern.test(fullPath)) {
54
+ results.push(fullPath)
55
+ }
56
+ }
57
+ return results
58
+ }
59
+
60
+ function getTestPattern(flags: TestFlags): RegExp {
61
+ if (flags.e2e) return /(?:\.e2e\.test\.ts$|e2e\/.*\.test\.ts$)/
62
+ if (flags.integration) return /(?:\.integration\.test\.ts$|tests\/integration\/.*\.test\.ts$)/
63
+ if (flags.unit) return /(?:\.unit\.test\.ts$|tests\/unit\/.*\.test\.ts$)/
64
+ return /\.test\.ts$/
65
+ }
66
+
67
+ /** Build bun test args from parsed flags */
68
+ export function buildTestArgs(flags: TestFlags, files: string[]): string[] {
69
+ const bunArgs = ["test"]
70
+
71
+ if (flags.watch) bunArgs.push("--watch")
72
+ if (flags.coverage) bunArgs.push("--coverage")
73
+ if (flags.filter) {
74
+ bunArgs.push("--bail", "--filter", flags.filter)
75
+ }
76
+
77
+ bunArgs.push(...files)
78
+ return bunArgs
79
+ }
80
+
81
+ export async function runTest(args: string[]) {
82
+ const cwd = process.cwd()
83
+ const flags = parseFlags(args)
84
+ const pattern = getTestPattern(flags)
85
+
86
+ const files = await findTestFiles(cwd, pattern)
87
+
88
+ if (files.length === 0) {
89
+ const kind = flags.e2e ? "e2e" : flags.integration ? "integration" : flags.unit ? "unit" : "any"
90
+ console.log(`\n No ${kind} test files found.\n`)
91
+ return
92
+ }
93
+
94
+ console.log(`\n Running ${files.length} test file(s)...\n`)
95
+
96
+ const bunArgs = buildTestArgs(flags, files)
97
+
98
+ const proc = Bun.spawn(["bun", ...bunArgs], {
99
+ cwd,
100
+ stdout: "inherit",
101
+ stderr: "inherit",
102
+ env: {
103
+ ...process.env,
104
+ NODE_ENV: "test",
105
+ GORSEE_TEST: "1",
106
+ },
107
+ })
108
+
109
+ const exitCode = await proc.exited
110
+
111
+ console.log()
112
+ if (exitCode === 0) {
113
+ console.log(` Tests passed (${files.length} file(s))`)
114
+ } else {
115
+ console.log(` Tests failed (exit code ${exitCode})`)
116
+ process.exit(exitCode)
117
+ }
118
+ }
119
+
120
+ // Re-export for testing
121
+ export { parseFlags, findTestFiles, getTestPattern }
@@ -0,0 +1,135 @@
1
+ // gorsee upgrade -- upgrade framework version with migration guidance
2
+
3
+ import { readFile } from "node:fs/promises"
4
+ import { join } from "node:path"
5
+
6
+ interface UpgradeFlags {
7
+ check: boolean
8
+ force: boolean
9
+ }
10
+
11
+ export function parseUpgradeFlags(args: string[]): UpgradeFlags {
12
+ const flags: UpgradeFlags = { check: false, force: false }
13
+ for (const arg of args) {
14
+ if (arg === "--check") flags.check = true
15
+ else if (arg === "--force") flags.force = true
16
+ }
17
+ return flags
18
+ }
19
+
20
+ /** Compare semver strings: -1 (a < b), 0 (equal), 1 (a > b) */
21
+ export function compareVersions(a: string, b: string): number {
22
+ const pa = a.replace(/^v/, "").split(".").map(Number)
23
+ const pb = b.replace(/^v/, "").split(".").map(Number)
24
+ for (let i = 0; i < 3; i++) {
25
+ const va = pa[i] ?? 0
26
+ const vb = pb[i] ?? 0
27
+ if (va < vb) return -1
28
+ if (va > vb) return 1
29
+ }
30
+ return 0
31
+ }
32
+
33
+ async function getCurrentVersion(cwd: string): Promise<string | null> {
34
+ try {
35
+ const pkg = await readFile(join(cwd, "node_modules/gorsee/package.json"), "utf-8")
36
+ return JSON.parse(pkg).version ?? null
37
+ } catch {
38
+ return null
39
+ }
40
+ }
41
+
42
+ export const NPM_REGISTRY_URL = "https://registry.npmjs.org/gorsee/latest"
43
+
44
+ async function fetchLatestVersion(): Promise<string | null> {
45
+ try {
46
+ const res = await fetch(NPM_REGISTRY_URL)
47
+ if (!res.ok) return null
48
+ const data = (await res.json()) as { version?: string }
49
+ return data.version ?? null
50
+ } catch {
51
+ return null
52
+ }
53
+ }
54
+
55
+ async function checkMigrationHints(cwd: string): Promise<string[]> {
56
+ const hints: string[] = []
57
+
58
+ // Check tsconfig.json jsx setting
59
+ try {
60
+ const tsconfig = await readFile(join(cwd, "tsconfig.json"), "utf-8")
61
+ const parsed = JSON.parse(tsconfig)
62
+ if (parsed.compilerOptions?.jsx !== "react-jsx") {
63
+ hints.push("tsconfig.json: set compilerOptions.jsx to \"react-jsx\"")
64
+ }
65
+ } catch { /* no tsconfig */ }
66
+
67
+ // Check for deprecated app.config.ts keys
68
+ try {
69
+ const config = await readFile(join(cwd, "app.config.ts"), "utf-8")
70
+ if (config.includes("ssr:")) {
71
+ hints.push("app.config.ts: 'ssr' key is deprecated, use 'rendering' instead")
72
+ }
73
+ } catch { /* no config */ }
74
+
75
+ return hints
76
+ }
77
+
78
+ export async function runUpgrade(args: string[]) {
79
+ const cwd = process.cwd()
80
+ const flags = parseUpgradeFlags(args)
81
+
82
+ const current = await getCurrentVersion(cwd)
83
+ if (!current) {
84
+ console.log("\n Gorsee.js not found in node_modules. Run: bun add gorsee\n")
85
+ return
86
+ }
87
+
88
+ console.log(`\n Current version: v${current}`)
89
+
90
+ const latest = await fetchLatestVersion()
91
+ if (!latest) {
92
+ console.log(" Could not fetch latest version from npm registry.\n")
93
+ return
94
+ }
95
+
96
+ if (compareVersions(current, latest) >= 0) {
97
+ console.log(` Already up to date (v${current})\n`)
98
+ return
99
+ }
100
+
101
+ console.log(` Latest version: v${latest}`)
102
+ console.log(` Upgrade: v${current} -> v${latest}`)
103
+
104
+ if (flags.check) {
105
+ console.log()
106
+ return
107
+ }
108
+
109
+ if (!flags.force) {
110
+ console.log("\n Run with --force to install, or confirm interactively.")
111
+ }
112
+
113
+ console.log("\n Installing gorsee@latest...")
114
+ const proc = Bun.spawn(["bun", "add", "gorsee@latest"], {
115
+ cwd,
116
+ stdout: "inherit",
117
+ stderr: "inherit",
118
+ })
119
+ const exitCode = await proc.exited
120
+
121
+ if (exitCode !== 0) {
122
+ console.log(`\n Install failed (exit code ${exitCode})\n`)
123
+ process.exit(exitCode)
124
+ }
125
+
126
+ const hints = await checkMigrationHints(cwd)
127
+ if (hints.length > 0) {
128
+ console.log("\n Migration hints:")
129
+ for (const hint of hints) {
130
+ console.log(` - ${hint}`)
131
+ }
132
+ }
133
+
134
+ console.log(`\n Upgraded successfully to v${latest}\n`)
135
+ }
package/src/cli/index.ts CHANGED
@@ -15,6 +15,9 @@ const COMMANDS: Record<string, string> = {
15
15
  generate: "Generate CRUD scaffold for entity",
16
16
  typegen: "Generate typed route definitions",
17
17
  deploy: "Generate deploy config (vercel/fly/cloudflare/netlify/docker)",
18
+ test: "Run tests (unit/integration/e2e)",
19
+ docs: "Generate API documentation from routes",
20
+ upgrade: "Upgrade Gorsee.js to latest version",
18
21
  help: "Show this help message",
19
22
  }
20
23
 
@@ -60,6 +63,21 @@ async function main() {
60
63
  const { runDeploy } = await import("./cmd-deploy.ts")
61
64
  await runDeploy(args.slice(1))
62
65
  break
66
+ case "test": {
67
+ const { runTest } = await import("./cmd-test.ts")
68
+ await runTest(args.slice(1))
69
+ break
70
+ }
71
+ case "docs": {
72
+ const { runDocs } = await import("./cmd-docs.ts")
73
+ await runDocs(args.slice(1))
74
+ break
75
+ }
76
+ case "upgrade": {
77
+ const { runUpgrade } = await import("./cmd-upgrade.ts")
78
+ await runUpgrade(args.slice(1))
79
+ break
80
+ }
63
81
  case "help":
64
82
  case undefined:
65
83
  case "--help":