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
package/src/build/ssg.ts CHANGED
@@ -2,9 +2,9 @@
2
2
  // Routes with `export const prerender = true` are rendered to static HTML
3
3
 
4
4
  import { createRouter } from "../router/scanner.ts"
5
- import { renderToString, ssrJsx } from "../runtime/server.ts"
6
5
  import { createContext } from "../server/middleware.ts"
7
- import { resetServerHead, getServerHead } from "../runtime/head.ts"
6
+ import { renderPageDocument, resolvePageRoute } from "../server/page-render.ts"
7
+ import { wrapHTML, type HTMLWrapOptions } from "../server/html-shell.ts"
8
8
  import { join } from "node:path"
9
9
  import { mkdir, writeFile } from "node:fs/promises"
10
10
 
@@ -16,7 +16,7 @@ export interface SSGResult {
16
16
  interface SSGOptions {
17
17
  routesDir: string
18
18
  outDir: string
19
- wrapHTML: (body: string, opts?: Record<string, unknown>) => string
19
+ wrapHTML?: (body: string, options?: HTMLWrapOptions) => string
20
20
  }
21
21
 
22
22
  export async function generateStaticPages(options: SSGOptions): Promise<SSGResult> {
@@ -34,54 +34,20 @@ export async function generateStaticPages(options: SSGOptions): Promise<SSGResul
34
34
  // Skip routes that don't opt-in to prerendering
35
35
  if (!mod.prerender) continue
36
36
 
37
- const component = mod.default as Function | undefined
38
- if (typeof component !== "function") continue
39
-
40
- // Parallel loading: import layout modules + run page loader
41
37
  const fakeRequest = new Request(`http://localhost${route.path}`)
42
38
  const ctx = createContext(fakeRequest, {})
43
- const layoutPaths = route.layoutPaths ?? []
44
- const layoutImportPromises = layoutPaths.map((lp) => import(lp))
45
- const pageLoaderPromise = typeof mod.loader === "function" ? mod.loader(ctx) : undefined
46
-
47
- const [layoutMods, loaderData] = await Promise.all([
48
- Promise.all(layoutImportPromises),
49
- pageLoaderPromise,
50
- ])
51
-
52
- // Run layout loaders in parallel
53
- const layoutLoaderPromises = layoutMods.map((lm) =>
54
- typeof lm.loader === "function" ? lm.loader(ctx) : undefined,
55
- )
56
- const layoutLoaderResults = await Promise.all(layoutLoaderPromises)
57
-
58
- // Nested layout wrapping: outermost first, innermost wraps page
59
- let pageComponent: Function = component
60
- for (let i = layoutMods.length - 1; i >= 0; i--) {
61
- const Layout = layoutMods[i]!.default
62
- if (typeof Layout === "function") {
63
- const inner = pageComponent
64
- const layoutData = layoutLoaderResults[i]
65
- pageComponent = (props: Record<string, unknown>) =>
66
- Layout({ ...props, data: layoutData, children: inner(props) })
67
- }
68
- }
69
-
70
- // Render
71
- resetServerHead()
72
- const pageProps = { params: {}, ctx, data: loaderData }
73
- const vnode = ssrJsx(pageComponent as any, pageProps)
74
- const body = renderToString(vnode)
75
- const headElements = getServerHead()
76
-
77
- // Extract title
78
- let title: string | undefined
79
- for (const el of headElements) {
80
- const m = el.match(/<title>(.+?)<\/title>/)
81
- if (m) { title = m[1]; break }
82
- }
83
-
84
- const html = wrapHTML(body, { title, loaderData, headElements })
39
+ const match = { route, params: {} }
40
+ const resolved = await resolvePageRoute(mod, match, ctx)
41
+ if (!resolved) continue
42
+
43
+ const rendered = renderPageDocument(resolved.pageComponent, ctx, {}, resolved.loaderData)
44
+ const renderHTML = wrapHTML ?? defaultWrapHTML
45
+ const html = renderHTML(rendered.html, {
46
+ title: rendered.title,
47
+ loaderData: resolved.loaderData,
48
+ headElements: rendered.headElements,
49
+ cssFiles: resolved.cssFiles,
50
+ })
85
51
 
86
52
  // Write file
87
53
  const outPath = route.path === "/"
@@ -98,3 +64,7 @@ export async function generateStaticPages(options: SSGOptions): Promise<SSGResul
98
64
 
99
65
  return { pages, errors }
100
66
  }
67
+
68
+ function defaultWrapHTML(body: string, options: HTMLWrapOptions = {}): string {
69
+ return wrapHTML(body, undefined, options)
70
+ }
@@ -9,8 +9,10 @@ const FRAMEWORK_ROOT = resolve(import.meta.dir, "..")
9
9
 
10
10
  const EXPORT_MAP: Record<string, string> = {
11
11
  "gorsee": resolve(FRAMEWORK_ROOT, "index.ts"),
12
+ "gorsee/compat": resolve(FRAMEWORK_ROOT, "index.ts"),
13
+ "gorsee/client": resolve(FRAMEWORK_ROOT, "client.ts"),
12
14
  "gorsee/reactive": resolve(FRAMEWORK_ROOT, "reactive/index.ts"),
13
- "gorsee/server": resolve(FRAMEWORK_ROOT, "server/index.ts"),
15
+ "gorsee/server": resolve(FRAMEWORK_ROOT, "server-entry.ts"),
14
16
  "gorsee/types": resolve(FRAMEWORK_ROOT, "types/index.ts"),
15
17
  "gorsee/db": resolve(FRAMEWORK_ROOT, "db/index.ts"),
16
18
  "gorsee/router": resolve(FRAMEWORK_ROOT, "router/index.ts"),
@@ -20,6 +22,25 @@ const EXPORT_MAP: Record<string, string> = {
20
22
  "gorsee/security": resolve(FRAMEWORK_ROOT, "security/index.ts"),
21
23
  "gorsee/jsx-runtime": resolve(FRAMEWORK_ROOT, "jsx-runtime.ts"),
22
24
  "gorsee/jsx-dev-runtime": resolve(FRAMEWORK_ROOT, "jsx-runtime.ts"),
25
+ "gorsee/testing": resolve(FRAMEWORK_ROOT, "testing/index.ts"),
26
+ "gorsee/i18n": resolve(FRAMEWORK_ROOT, "i18n/index.ts"),
27
+ "gorsee/env": resolve(FRAMEWORK_ROOT, "env/index.ts"),
28
+ "gorsee/auth": resolve(FRAMEWORK_ROOT, "auth/index.ts"),
29
+ "gorsee/routes": resolve(FRAMEWORK_ROOT, "runtime/typed-routes.ts"),
30
+ "gorsee/cli/cmd-create": resolve(FRAMEWORK_ROOT, "cli/cmd-create.ts"),
31
+ "gorsee/plugins": resolve(FRAMEWORK_ROOT, "plugins/index.ts"),
32
+ "gorsee/plugins/drizzle": resolve(FRAMEWORK_ROOT, "plugins/drizzle.ts"),
33
+ "gorsee/plugins/prisma": resolve(FRAMEWORK_ROOT, "plugins/prisma.ts"),
34
+ "gorsee/plugins/tailwind": resolve(FRAMEWORK_ROOT, "plugins/tailwind.ts"),
35
+ "gorsee/plugins/lucia": resolve(FRAMEWORK_ROOT, "plugins/lucia.ts"),
36
+ "gorsee/plugins/s3": resolve(FRAMEWORK_ROOT, "plugins/s3.ts"),
37
+ "gorsee/plugins/resend": resolve(FRAMEWORK_ROOT, "plugins/resend.ts"),
38
+ "gorsee/plugins/stripe": resolve(FRAMEWORK_ROOT, "plugins/stripe.ts"),
39
+ "gorsee/deploy": resolve(FRAMEWORK_ROOT, "deploy/index.ts"),
40
+ }
41
+
42
+ export function resolveFrameworkImport(specifier: string): string | undefined {
43
+ return EXPORT_MAP[specifier]
23
44
  }
24
45
 
25
46
  plugin({
@@ -27,7 +48,7 @@ plugin({
27
48
  setup(build) {
28
49
  // Resolve gorsee/* imports
29
50
  build.onResolve({ filter: /^gorsee(\/.*)?$/ }, (args) => {
30
- const mapped = EXPORT_MAP[args.path]
51
+ const mapped = resolveFrameworkImport(args.path)
31
52
  if (mapped) {
32
53
  return { path: mapped }
33
54
  }
@@ -6,14 +6,10 @@ import { mkdir, rm, writeFile, readdir, stat, watch } from "node:fs/promises"
6
6
  import { createRouter } from "../router/scanner.ts"
7
7
  import { buildClientBundles } from "../build/client.ts"
8
8
  import { generateStaticPages } from "../build/ssg.ts"
9
+ import { createBuildManifest } from "../build/manifest.ts"
9
10
  import { createHash } from "node:crypto"
10
-
11
- interface BuildManifest {
12
- routes: Record<string, { js?: string; hasLoader: boolean; prerendered?: boolean }>
13
- chunks: string[]
14
- prerendered: string[]
15
- buildTime: string
16
- }
11
+ import { wrapHTML } from "../server/html-shell.ts"
12
+ import { createProjectContext, type RuntimeOptions } from "../runtime/project.ts"
17
13
 
18
14
  async function hashFile(path: string): Promise<string> {
19
15
  const content = await Bun.file(path).arrayBuffer()
@@ -36,30 +32,33 @@ async function getAllFiles(dir: string): Promise<string[]> {
36
32
  return files
37
33
  }
38
34
 
39
- export async function runBuild(_args: string[]) {
40
- if (_args.includes("--watch")) {
41
- return runBuildWatch()
42
- }
43
- const cwd = process.cwd()
35
+ export interface BuildCommandOptions extends RuntimeOptions {
36
+ minify?: boolean
37
+ sourcemap?: boolean
38
+ }
39
+
40
+ export async function buildProject(options: BuildCommandOptions = {}) {
41
+ const { cwd, paths } = createProjectContext(options)
44
42
  const startTime = performance.now()
45
43
  console.log("\n Gorsee Build\n")
46
44
 
47
- const distDir = join(cwd, "dist")
48
- await rm(distDir, { recursive: true, force: true })
49
- await mkdir(join(distDir, "client"), { recursive: true })
50
- await mkdir(join(distDir, "server"), { recursive: true })
45
+ await rm(paths.distDir, { recursive: true, force: true })
46
+ await mkdir(paths.clientDir, { recursive: true })
47
+ await mkdir(paths.serverDir, { recursive: true })
51
48
 
52
49
  // 1. Scan routes
53
- const routesDir = join(cwd, "routes")
54
- const routes = await createRouter(routesDir)
50
+ const routes = await createRouter(paths.routesDir)
55
51
  console.log(` [1/5] Found ${routes.length} route(s)`)
56
52
 
57
53
  // 2. Build client bundles (minified)
58
- const build = await buildClientBundles(routes, cwd, { minify: true, sourcemap: true })
54
+ const build = await buildClientBundles(routes, cwd, {
55
+ minify: options.minify ?? true,
56
+ sourcemap: options.sourcemap ?? true,
57
+ })
59
58
  console.log(` [2/5] Client bundles built (${build.entryMap.size} entries)`)
60
59
 
61
60
  // 3. Hash and copy client files to dist
62
- const clientSrc = join(cwd, ".gorsee", "client")
61
+ const clientSrc = join(paths.gorseeDir, "client")
63
62
  const clientFiles = await getAllFiles(clientSrc)
64
63
  const hashMap = new Map<string, string>()
65
64
 
@@ -68,7 +67,7 @@ export async function runBuild(_args: string[]) {
68
67
  const hash = await hashFile(file)
69
68
  const ext = rel.lastIndexOf(".")
70
69
  const hashed = ext > 0 ? `${rel.slice(0, ext)}.${hash}${rel.slice(ext)}` : `${rel}.${hash}`
71
- const dest = join(distDir, "client", hashed)
70
+ const dest = join(paths.clientDir, hashed)
72
71
  await mkdir(join(dest, ".."), { recursive: true })
73
72
  await Bun.write(dest, Bun.file(file))
74
73
  hashMap.set(rel, hashed)
@@ -76,56 +75,22 @@ export async function runBuild(_args: string[]) {
76
75
  console.log(` [3/5] Assets hashed (${hashMap.size} files)`)
77
76
 
78
77
  // 4. Generate manifest
79
- const manifest: BuildManifest = {
80
- routes: {},
81
- chunks: [],
82
- prerendered: [],
83
- buildTime: new Date().toISOString(),
84
- }
85
-
86
- for (const route of routes) {
87
- const jsRel = build.entryMap.get(route.path)
88
- manifest.routes[route.path] = {
89
- js: jsRel ? hashMap.get(jsRel) : undefined,
90
- hasLoader: false, // will be detected at runtime
91
- }
92
- }
93
-
94
- for (const [, hashed] of hashMap) {
95
- if (hashed.includes("chunk-")) manifest.chunks.push(hashed)
96
- }
78
+ const manifest = await createBuildManifest(routes, build.entryMap, hashMap)
97
79
 
98
- await writeFile(join(distDir, "manifest.json"), JSON.stringify(manifest, null, 2))
80
+ await writeFile(join(paths.distDir, "manifest.json"), JSON.stringify(manifest, null, 2))
99
81
  console.log(` [4/5] Manifest generated`)
100
82
 
101
83
  // 5. Static Site Generation (prerender pages with `export const prerender = true`)
102
84
  const ssgResult = await generateStaticPages({
103
- routesDir,
104
- outDir: join(distDir, "static"),
105
- wrapHTML: (body, opts: Record<string, unknown> = {}) => {
106
- const title = (opts.title as string) ?? "Gorsee App"
107
- const headElements = (opts.headElements as string[]) ?? []
108
- return `<!DOCTYPE html>
109
- <html lang="en">
110
- <head>
111
- <meta charset="UTF-8" />
112
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
113
- <title>${title}</title>
114
- <link rel="stylesheet" href="/styles.css" />
115
- ${headElements.join("\n")}
116
- </head>
117
- <body>
118
- <div id="app">${body}</div>
119
- </body>
120
- </html>`
121
- },
85
+ routesDir: paths.routesDir,
86
+ outDir: join(paths.distDir, "static"),
87
+ wrapHTML: (body, options = {}) => wrapHTML(body, undefined, options),
122
88
  })
123
- for (const path of ssgResult.pages.keys()) {
124
- manifest.prerendered.push(path)
125
- if (manifest.routes[path]) manifest.routes[path]!.prerendered = true
126
- }
89
+ const finalManifest = ssgResult.pages.size > 0
90
+ ? await createBuildManifest(routes, build.entryMap, hashMap, ssgResult.pages.keys())
91
+ : manifest
127
92
  if (ssgResult.pages.size > 0) {
128
- await writeFile(join(distDir, "manifest.json"), JSON.stringify(manifest, null, 2))
93
+ await writeFile(join(paths.distDir, "manifest.json"), JSON.stringify(finalManifest, null, 2))
129
94
  }
130
95
  if (ssgResult.errors.length > 0) {
131
96
  for (const err of ssgResult.errors) console.error(` SSG error: ${err}`)
@@ -134,7 +99,7 @@ ${headElements.join("\n")}
134
99
 
135
100
  // Stats
136
101
  let totalSize = 0
137
- for (const file of await getAllFiles(join(distDir, "client"))) {
102
+ for (const file of await getAllFiles(paths.clientDir)) {
138
103
  const s = await stat(file)
139
104
  totalSize += s.size
140
105
  }
@@ -147,13 +112,12 @@ ${headElements.join("\n")}
147
112
  console.log()
148
113
  }
149
114
 
150
- async function runBuildWatch() {
151
- const cwd = process.cwd()
152
- const routesDir = join(cwd, "routes")
115
+ export async function watchBuildProject(options: BuildCommandOptions = {}) {
116
+ const { paths } = createProjectContext(options)
153
117
 
154
118
  console.log("\n Gorsee Build --watch\n")
155
119
  console.log(" Performing initial build...")
156
- await runBuild([])
120
+ await buildProject(options)
157
121
 
158
122
  let building = false
159
123
  let queued = false
@@ -163,7 +127,7 @@ async function runBuildWatch() {
163
127
  building = true
164
128
  try {
165
129
  const start = performance.now()
166
- await runBuild([])
130
+ await buildProject(options)
167
131
  const ms = (performance.now() - start).toFixed(0)
168
132
  console.log(` Rebuilt in ${ms}ms`)
169
133
  } catch (err) {
@@ -175,8 +139,15 @@ async function runBuildWatch() {
175
139
  }
176
140
 
177
141
  console.log(" Watching for changes...")
178
- const watcher = watch(routesDir, { recursive: true })
142
+ const watcher = watch(paths.routesDir, { recursive: true })
179
143
  for await (const _event of watcher) {
180
144
  await rebuild()
181
145
  }
182
146
  }
147
+
148
+ export async function runBuild(args: string[], options: BuildCommandOptions = {}) {
149
+ if (args.includes("--watch")) {
150
+ return watchBuildProject(options)
151
+ }
152
+ return buildProject(options)
153
+ }
@@ -3,6 +3,7 @@
3
3
  import { join, relative } from "node:path"
4
4
  import { readdir, stat, readFile } from "node:fs/promises"
5
5
  import { createRouter } from "../router/scanner.ts"
6
+ import { createProjectContext, type RuntimeOptions } from "../runtime/project.ts"
6
7
 
7
8
  interface CheckResult {
8
9
  errors: CheckIssue[]
@@ -20,6 +21,8 @@ interface CheckIssue {
20
21
 
21
22
  const MAX_FILE_LINES = 500
22
23
 
24
+ export type { CheckResult, CheckIssue }
25
+
23
26
  async function getAllTsFiles(dir: string): Promise<string[]> {
24
27
  const files: string[] = []
25
28
  let entries: string[]
@@ -144,31 +147,31 @@ async function checkProjectStructure(cwd: string): Promise<CheckIssue[]> {
144
147
  return issues
145
148
  }
146
149
 
147
- export async function runCheck(_args: string[]) {
148
- const cwd = process.cwd()
149
- console.log("\n Gorsee Check\n")
150
+ export interface CheckCommandOptions extends RuntimeOptions {
151
+ runTypeScript?: boolean
152
+ }
150
153
 
154
+ export async function checkProject(options: CheckCommandOptions = {}): Promise<CheckResult> {
155
+ const { cwd, paths } = createProjectContext(options)
156
+ const runTypeScript = options.runTypeScript ?? true
151
157
  const result: CheckResult = { errors: [], warnings: [], info: [] }
152
158
 
153
- // 1. Project structure
154
159
  const structIssues = await checkProjectStructure(cwd)
155
160
  for (const issue of structIssues) {
156
161
  if (issue.code.startsWith("E")) result.errors.push(issue)
157
162
  else result.warnings.push(issue)
158
163
  }
159
164
 
160
- // 2. Routes
161
165
  try {
162
- const routes = await createRouter(join(cwd, "routes"))
166
+ const routes = await createRouter(paths.routesDir)
163
167
  result.info.push(`Found ${routes.length} route(s)`)
164
168
  } catch {
165
169
  result.info.push("Could not scan routes")
166
170
  }
167
171
 
168
- // 3. File checks
169
- const files = await getAllTsFiles(join(cwd, "routes"))
170
- files.push(...(await getAllTsFiles(join(cwd, "shared"))))
171
- files.push(...(await getAllTsFiles(join(cwd, "middleware"))))
172
+ const files = await getAllTsFiles(paths.routesDir)
173
+ files.push(...(await getAllTsFiles(paths.sharedDir)))
174
+ files.push(...(await getAllTsFiles(paths.middlewareDir)))
172
175
 
173
176
  for (const file of files) {
174
177
  const sizeIssues = await checkFileSize(file, cwd)
@@ -179,24 +182,35 @@ export async function runCheck(_args: string[]) {
179
182
  }
180
183
  }
181
184
 
182
- // 4. TypeScript check
183
- console.log(" Running TypeScript check...")
184
- const tsc = Bun.spawn(["bun", "x", "tsc", "--noEmit"], {
185
- cwd,
186
- stdout: "pipe",
187
- stderr: "pipe",
188
- })
189
- const tscExit = await tsc.exited
190
- if (tscExit !== 0) {
191
- const stderr = await new Response(tsc.stderr).text()
192
- result.errors.push({
193
- code: "TSC",
194
- file: ".",
195
- message: `TypeScript errors:\n${stderr.trim()}`,
185
+ if (runTypeScript) {
186
+ const tsc = Bun.spawn(["bun", "x", "tsc", "--noEmit"], {
187
+ cwd,
188
+ stdout: "pipe",
189
+ stderr: "pipe",
196
190
  })
197
- } else {
198
- result.info.push("TypeScript: no errors")
191
+ const tscExit = await tsc.exited
192
+ if (tscExit !== 0) {
193
+ const stderr = await new Response(tsc.stderr).text()
194
+ result.errors.push({
195
+ code: "TSC",
196
+ file: ".",
197
+ message: `TypeScript errors:\n${stderr.trim()}`,
198
+ })
199
+ } else {
200
+ result.info.push("TypeScript: no errors")
201
+ }
202
+ }
203
+
204
+ return result
205
+ }
206
+
207
+ /** @deprecated Use checkProject() for programmatic access. */
208
+ export async function runCheck(_args: string[], options: CheckCommandOptions = {}) {
209
+ console.log("\n Gorsee Check\n")
210
+ if (options.runTypeScript ?? true) {
211
+ console.log(" Running TypeScript check...")
199
212
  }
213
+ const result = await checkProject(options)
200
214
 
201
215
  // Report
202
216
  for (const info of result.info) {
@@ -3,6 +3,7 @@
3
3
  import { mkdir, writeFile } from "node:fs/promises"
4
4
  import { join } from "node:path"
5
5
  import { generateFrameworkMD } from "./framework-md.ts"
6
+ import { createProjectContext, type RuntimeOptions } from "../runtime/project.ts"
6
7
 
7
8
  const DIRS = [
8
9
  "routes",
@@ -13,7 +14,7 @@ const DIRS = [
13
14
  "public",
14
15
  ]
15
16
 
16
- const INDEX_ROUTE = `import { createSignal, Head, Link } from "gorsee"
17
+ const INDEX_ROUTE = `import { createSignal, Head, Link } from "gorsee/client"
17
18
 
18
19
  export default function HomePage() {
19
20
  const [count, setCount] = createSignal(0)
@@ -37,7 +38,7 @@ export default function HomePage() {
37
38
  }
38
39
  `
39
40
 
40
- const ABOUT_ROUTE = `import { Head, Link } from "gorsee"
41
+ const ABOUT_ROUTE = `import { Head, Link } from "gorsee/client"
41
42
 
42
43
  export default function AboutPage() {
43
44
  return (
@@ -81,7 +82,7 @@ const ERROR_PAGE = `export default function ErrorPage(props: { error: Error }) {
81
82
  }
82
83
  `
83
84
 
84
- const NOT_FOUND_PAGE = `import { Head, Link } from "gorsee"
85
+ const NOT_FOUND_PAGE = `import { Head, Link } from "gorsee/client"
85
86
 
86
87
  export default function NotFoundPage() {
87
88
  return (
@@ -211,6 +212,12 @@ Open [http://localhost:3000](http://localhost:3000).
211
212
  | \`bunx gorsee typegen\` | Generate typed routes |
212
213
  | \`bunx gorsee migrate\` | Run DB migrations |
213
214
 
215
+ ## Import Boundaries
216
+
217
+ - Use \`gorsee/client\` for route components, islands, links, forms, and reactive primitives.
218
+ - Use \`gorsee/server\` for loaders, middleware, auth, db, cache, RPC, security, env, and logging.
219
+ - Do not use root \`gorsee\` in new code. It exists only as a compatibility entrypoint.
220
+
214
221
  ## Project Structure
215
222
 
216
223
  \`\`\`
@@ -233,14 +240,17 @@ See \`FRAMEWORK.md\` for the full API reference (AI-friendly).
233
240
  `
234
241
  }
235
242
 
236
- export async function runCreate(args: string[]) {
243
+ export interface CreateCommandOptions extends RuntimeOptions {}
244
+
245
+ export async function createProject(args: string[], options: CreateCommandOptions = {}) {
237
246
  const name = args[0]
238
247
  if (!name) {
239
248
  console.error("Usage: gorsee create <project-name>")
240
249
  process.exit(1)
241
250
  }
242
251
 
243
- const dir = join(process.cwd(), name)
252
+ const { cwd } = createProjectContext(options)
253
+ const dir = join(cwd, name)
244
254
  console.log(`\n Creating ${name}...\n`)
245
255
 
246
256
  await mkdir(dir, { recursive: true })
@@ -311,3 +321,8 @@ export async function runCreate(args: string[]) {
311
321
  console.log(" Then open http://localhost:3000")
312
322
  console.log()
313
323
  }
324
+
325
+ /** @deprecated Use createProject() for programmatic access. */
326
+ export async function runCreate(args: string[], options: CreateCommandOptions = {}) {
327
+ return createProject(args, options)
328
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { writeFile, access, mkdir } from "node:fs/promises"
4
4
  import { join } from "node:path"
5
+ import { createProjectContext, type RuntimeOptions } from "../runtime/project.ts"
5
6
 
6
7
  type Target = "vercel" | "fly" | "cloudflare" | "netlify" | "docker"
7
8
 
@@ -90,8 +91,10 @@ async function deployDocker(cwd: string): Promise<void> {
90
91
  console.log(" 2. Run: docker run -p 3000:3000 gorsee-app")
91
92
  }
92
93
 
93
- export async function runDeploy(args: string[]): Promise<void> {
94
- const cwd = process.cwd()
94
+ export interface DeployCommandOptions extends RuntimeOptions {}
95
+
96
+ export async function generateDeployConfig(args: string[], options: DeployCommandOptions = {}): Promise<void> {
97
+ const { cwd } = createProjectContext(options)
95
98
  const initOnly = args.includes("--init")
96
99
  const targetArg = args.find((a) => !a.startsWith("-")) as Target | undefined
97
100
 
@@ -139,3 +142,8 @@ export async function runDeploy(args: string[]): Promise<void> {
139
142
 
140
143
  console.log()
141
144
  }
145
+
146
+ /** @deprecated Use generateDeployConfig() for programmatic access. */
147
+ export async function runDeploy(args: string[], options: DeployCommandOptions = {}): Promise<void> {
148
+ return generateDeployConfig(args, options)
149
+ }
@@ -1,13 +1,13 @@
1
1
  // gorsee dev -- start dev server
2
2
 
3
- import { join } from "node:path"
3
+ import { createProjectContext, type RuntimeOptions } from "../runtime/project.ts"
4
4
 
5
- export async function runDev(_args: string[]) {
6
- const devPath = join(import.meta.dir, "../dev.ts")
7
- const proc = Bun.spawn(["bun", "run", devPath], {
8
- cwd: process.cwd(),
9
- stdio: ["inherit", "inherit", "inherit"],
10
- env: { ...process.env },
11
- })
12
- await proc.exited
5
+ export interface DevCommandOptions extends RuntimeOptions {
6
+ port?: number
7
+ }
8
+
9
+ export async function runDev(_args: string[], options: DevCommandOptions = {}) {
10
+ const { cwd } = createProjectContext(options)
11
+ const { startDevServer } = await import("../dev.ts")
12
+ await startDevServer({ cwd, port: options.port })
13
13
  }