gorsee 0.1.3 → 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 +11 -2
- package/src/cli/cmd-deploy.ts +141 -0
- package/src/cli/cmd-docs.ts +145 -0
- package/src/cli/cmd-test.ts +121 -0
- package/src/cli/cmd-upgrade.ts +135 -0
- package/src/cli/index.ts +23 -0
- package/src/deploy/cloudflare.ts +109 -0
- package/src/deploy/fly.ts +79 -0
- package/src/deploy/index.ts +31 -0
- package/src/deploy/netlify.ts +77 -0
- package/src/deploy/vercel.ts +94 -0
- package/src/index.ts +1 -0
- package/src/plugins/drizzle.ts +84 -0
- package/src/plugins/index.ts +86 -0
- package/src/plugins/lucia.ts +111 -0
- package/src/plugins/prisma.ts +85 -0
- package/src/plugins/resend.ts +78 -0
- package/src/plugins/s3.ts +102 -0
- package/src/plugins/stripe.ts +133 -0
- package/src/plugins/tailwind.ts +92 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gorsee",
|
|
3
|
-
"version": "0.1
|
|
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",
|
|
@@ -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
|
+
}
|
|
@@ -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
|
@@ -14,6 +14,10 @@ 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)",
|
|
18
|
+
test: "Run tests (unit/integration/e2e)",
|
|
19
|
+
docs: "Generate API documentation from routes",
|
|
20
|
+
upgrade: "Upgrade Gorsee.js to latest version",
|
|
17
21
|
help: "Show this help message",
|
|
18
22
|
}
|
|
19
23
|
|
|
@@ -55,6 +59,25 @@ async function main() {
|
|
|
55
59
|
const { runTypegen } = await import("./cmd-typegen.ts")
|
|
56
60
|
await runTypegen(args.slice(1))
|
|
57
61
|
break
|
|
62
|
+
case "deploy":
|
|
63
|
+
const { runDeploy } = await import("./cmd-deploy.ts")
|
|
64
|
+
await runDeploy(args.slice(1))
|
|
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
|
+
}
|
|
58
81
|
case "help":
|
|
59
82
|
case undefined:
|
|
60
83
|
case "--help":
|