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/dev.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Orchestrator: routing + security + HMR + static serving
3
3
 
4
4
  import { createRouter, matchRoute, buildStaticMap } from "./router/index.ts"
5
- import { handleRPCRequest, __resetRPCState } from "./server/rpc.ts"
5
+ import { __resetRPCState } from "./server/rpc.ts"
6
6
  import { securityHeaders } from "./security/headers.ts"
7
7
  import { createRateLimiter } from "./security/rate-limit.ts"
8
8
  import { EVENT_REPLAY_SCRIPT } from "./runtime/event-replay.ts"
@@ -14,182 +14,122 @@ import { renderErrorOverlay } from "./dev/error-overlay.ts"
14
14
  import { handlePageRequest } from "./dev/request-handler.ts"
15
15
  import { handlePartialNavigation } from "./dev/partial-handler.ts"
16
16
  import { loadEnv } from "./env/index.ts"
17
- import { getMimeType } from "./server/mime.ts"
18
- import { join, resolve } from "node:path"
17
+ import { generateNonce, wrapHTML } from "./server/html-shell.ts"
18
+ import { renderNotFoundPage } from "./server/not-found.ts"
19
+ import { createRateLimitResponse, handleRPCWithHeaders } from "./server/request-preflight.ts"
20
+ import { servePrefixedStaticFile, serveStaticFile } from "./server/static-file.ts"
21
+ import { join } from "node:path"
19
22
  import type { Route } from "./router/index.ts"
23
+ import { createProjectContext, resolveRuntimeEnv, type RuntimeOptions } from "./runtime/project.ts"
20
24
 
21
- const CWD = process.cwd()
22
- const ROUTES_DIR = join(CWD, "routes")
23
- const PUBLIC_DIR = join(CWD, "public")
24
- const CLIENT_DIR = join(CWD, ".gorsee", "client")
25
- const PORT = Number(process.env.PORT) || 3000
26
-
27
- let routes: Route[] = []
28
- let staticMap = new Map<string, Route>()
29
- let clientBuild: BuildResult = { entryMap: new Map() }
30
-
31
- const rateLimiter = createRateLimiter(200, "1m")
32
-
33
- // --- Static file serving ---
34
-
35
- async function tryServeStatic(pathname: string): Promise<Response | null> {
36
- if (pathname === "/") return null
37
- try {
38
- const filePath = resolve(PUBLIC_DIR, pathname.slice(1))
39
- if (!filePath.startsWith(PUBLIC_DIR)) return null
40
- const file = Bun.file(filePath)
41
- if (await file.exists()) {
42
- return new Response(file, {
43
- headers: { "Content-Type": getMimeType(filePath) },
44
- })
45
- }
46
- } catch {}
47
- return null
25
+ interface StartDevServerOptions extends RuntimeOptions {
26
+ port?: number
48
27
  }
49
28
 
50
- async function tryServeClientBundle(pathname: string): Promise<Response | null> {
51
- if (!pathname.startsWith("/_gorsee/")) return null
52
- const relPath = pathname.slice("/_gorsee/".length)
53
- try {
54
- const filePath = resolve(CLIENT_DIR, relPath)
55
- if (!filePath.startsWith(CLIENT_DIR)) return null
56
- const file = Bun.file(filePath)
57
- if (await file.exists()) {
58
- return new Response(file, {
59
- headers: { "Content-Type": "application/javascript" },
60
- })
61
- }
62
- } catch {}
63
- return null
29
+ interface DevRuntimeState {
30
+ cwd: string
31
+ routesDir: string
32
+ publicDir: string
33
+ clientDir: string
34
+ rateLimiter: ReturnType<typeof createRateLimiter>
35
+ routes: Route[]
36
+ staticMap: Map<string, Route>
37
+ clientBuild: BuildResult
64
38
  }
65
39
 
66
- // --- HTML shell ---
67
-
68
- function generateNonce(): string {
69
- const bytes = new Uint8Array(16)
70
- crypto.getRandomValues(bytes)
71
- return btoa(String.fromCharCode(...bytes))
72
- }
40
+ // --- Static file serving ---
73
41
 
74
- interface HTMLWrapOptions {
75
- title?: string
76
- clientScript?: string
77
- loaderData?: unknown
78
- params?: Record<string, string>
79
- cssFiles?: string[]
42
+ async function tryServeStatic(publicDir: string, pathname: string): Promise<Response | null> {
43
+ if (pathname === "/") return null
44
+ return serveStaticFile(publicDir, pathname.slice(1))
80
45
  }
81
46
 
82
- function wrapHTML(
83
- body: string,
84
- nonce: string,
85
- options: HTMLWrapOptions = {},
86
- ): string {
87
- const { title = "Gorsee App", clientScript, loaderData, params, cssFiles = [] } = options
88
-
89
- let dataScript = ""
90
- if (loaderData !== undefined) {
91
- const json = JSON.stringify(loaderData).replace(/</g, "\\u003c")
92
- dataScript = `\n <script id="__GORSEE_DATA__" type="application/json" nonce="${nonce}">${json}</script>`
93
- }
94
-
95
- let paramsScript = ""
96
- if (params && Object.keys(params).length > 0) {
97
- paramsScript = `\n <script nonce="${nonce}">window.__GORSEE_PARAMS__=${JSON.stringify(params)}</script>`
98
- }
99
-
100
- const clientTag = clientScript
101
- ? `\n <script type="module" src="${clientScript}" nonce="${nonce}"></script>`
102
- : ""
103
-
104
- return `<!DOCTYPE html>
105
- <html lang="en">
106
- <head>
107
- <meta charset="UTF-8" />
108
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
109
- <title>${title}</title>
110
- <link rel="stylesheet" href="/styles.css" />
111
- ${cssFiles.map((f: string) => ` <link rel="stylesheet" href="${f}" />`).join("\n")}
112
- </head>
113
- <body>
114
- ${EVENT_REPLAY_SCRIPT.replace("<script", `<script nonce="${nonce}"`)}
115
- <div id="app">${body}</div>${dataScript}${paramsScript}${clientTag}
116
- ${HMR_CLIENT_SCRIPT.replace("<script", `<script nonce="${nonce}"`)}
117
- </body>
118
- </html>`
47
+ async function tryServeClientBundle(clientDir: string, pathname: string): Promise<Response | null> {
48
+ return servePrefixedStaticFile(pathname, "/_gorsee/", clientDir, {
49
+ contentType: "application/javascript",
50
+ })
119
51
  }
120
52
 
121
53
  // --- 404 page ---
122
54
 
123
- async function render404Page(nonce: string): Promise<string> {
124
- try {
125
- const notFoundPath = join(ROUTES_DIR, "404.tsx")
126
- const file = Bun.file(notFoundPath)
127
- if (await file.exists()) {
128
- const mod = await import(notFoundPath)
129
- if (typeof mod.default === "function") {
130
- const { ssrJsx, renderToString } = await import("./runtime/server.ts")
131
- const vnode = ssrJsx(mod.default as any, {})
132
- const body = renderToString(vnode)
133
- return wrapHTML(body, nonce, { title: "404 - Not Found" })
134
- }
135
- }
136
- } catch {}
137
- return wrapHTML("<h1>404</h1><p>Page not found</p>", nonce)
55
+ async function render404Page(routesDir: string, nonce: string): Promise<string> {
56
+ return renderNotFoundPage(routesDir, nonce, {
57
+ bodyPrefix: [EVENT_REPLAY_SCRIPT.replace("<script", `<script nonce="${nonce}"`)],
58
+ bodySuffix: [HMR_CLIENT_SCRIPT.replace("<script", `<script nonce="${nonce}"`)],
59
+ })
138
60
  }
139
61
 
140
62
  // --- Build & watch ---
141
63
 
142
- let buildInProgress = false
143
- let buildQueued = false
64
+ async function createDevRuntimeState(cwd: string): Promise<DevRuntimeState> {
65
+ const { paths } = createProjectContext({ cwd })
66
+ return {
67
+ cwd,
68
+ routesDir: paths.routesDir,
69
+ publicDir: paths.publicDir,
70
+ clientDir: join(paths.gorseeDir, "client"),
71
+ rateLimiter: createRateLimiter(200, "1m"),
72
+ routes: [],
73
+ staticMap: new Map(),
74
+ clientBuild: { entryMap: new Map() },
75
+ }
76
+ }
144
77
 
145
- async function rebuildClient() {
146
- if (buildInProgress) {
147
- buildQueued = true
78
+ async function rebuildClient(state: DevRuntimeState, buildState: { inProgress: boolean, queued: boolean }) {
79
+ if (buildState.inProgress) {
80
+ buildState.queued = true
148
81
  return
149
82
  }
150
- buildInProgress = true
83
+ buildState.inProgress = true
151
84
  try {
152
85
  __resetRPCState()
153
- routes = await createRouter(ROUTES_DIR)
154
- staticMap = buildStaticMap(routes)
155
- clientBuild = await buildClientBundles(routes, CWD)
156
- log.info("client build complete", { routes: clientBuild.entryMap.size })
86
+ state.routes = await createRouter(state.routesDir)
87
+ state.staticMap = buildStaticMap(state.routes)
88
+ state.clientBuild = await buildClientBundles(state.routes, state.cwd)
89
+ log.info("client build complete", { routes: state.clientBuild.entryMap.size })
157
90
  } catch (err) {
158
91
  log.error("client build failed", { error: String(err) })
159
92
  } finally {
160
- buildInProgress = false
161
- if (buildQueued) {
162
- buildQueued = false
163
- await rebuildClient()
93
+ buildState.inProgress = false
94
+ if (buildState.queued) {
95
+ buildState.queued = false
96
+ await rebuildClient(state, buildState)
164
97
  }
165
98
  }
166
99
  }
167
100
 
168
101
  // --- Server ---
169
102
 
170
- async function main() {
171
- await loadEnv(CWD)
172
- setLogLevel("info")
173
- log.info("scanning routes", { dir: ROUTES_DIR })
174
-
175
- routes = await createRouter(ROUTES_DIR)
176
- staticMap = buildStaticMap(routes)
177
- for (const route of routes) {
103
+ export async function startDevServer(options: StartDevServerOptions = {}) {
104
+ const runtime = createProjectContext(options)
105
+ const cwd = runtime.cwd
106
+ const state = await createDevRuntimeState(cwd)
107
+ const buildState = { inProgress: false, queued: false }
108
+
109
+ await loadEnv(cwd)
110
+ const envConfig = resolveRuntimeEnv(process.env)
111
+ const port = options.port ?? envConfig.port
112
+ setLogLevel(envConfig.logLevel)
113
+ log.info("scanning routes", { dir: state.routesDir })
114
+
115
+ state.routes = await createRouter(state.routesDir)
116
+ state.staticMap = buildStaticMap(state.routes)
117
+ for (const route of state.routes) {
178
118
  log.info("route registered", { path: route.path, file: route.filePath })
179
119
  }
180
120
 
181
- await rebuildClient()
121
+ await rebuildClient(state, buildState)
182
122
 
183
123
  startWatcher({
184
- dirs: [ROUTES_DIR, join(CWD, "shared"), join(CWD, "middleware")],
124
+ dirs: [state.routesDir, runtime.paths.sharedDir, runtime.paths.middlewareDir],
185
125
  onChange: async () => {
186
- await rebuildClient()
126
+ await rebuildClient(state, buildState)
187
127
  notifyReload()
188
128
  },
189
129
  })
190
130
 
191
131
  const server = Bun.serve({
192
- port: PORT,
132
+ port,
193
133
  async fetch(request, server) {
194
134
  const url = new URL(request.url)
195
135
  const pathname = url.pathname
@@ -202,37 +142,29 @@ async function main() {
202
142
 
203
143
  // Rate limiting by IP
204
144
  const ip = server.requestIP(request)?.address ?? "unknown"
205
- const rl = rateLimiter.check(ip)
206
- if (!rl.allowed) {
207
- return new Response("Too Many Requests", {
208
- status: 429,
209
- headers: { "Retry-After": String(Math.ceil((rl.resetAt - Date.now()) / 1000)) },
210
- })
211
- }
145
+ const rateLimitResponse = createRateLimitResponse(state.rateLimiter, ip)
146
+ if (rateLimitResponse) return rateLimitResponse
212
147
 
213
148
  const start = performance.now()
214
149
  const nonce = generateNonce()
215
150
  const secHeaders = securityHeaders({}, nonce)
216
151
 
217
152
  // 1. RPC
218
- const rpcResponse = await handleRPCRequest(request)
219
- if (rpcResponse) {
220
- for (const k in secHeaders) rpcResponse.headers.set(k, secHeaders[k]!)
221
- return rpcResponse
222
- }
153
+ const rpcResponse = await handleRPCWithHeaders(request, secHeaders)
154
+ if (rpcResponse) return rpcResponse
223
155
 
224
156
  // 2. Client bundles
225
- const bundleResponse = await tryServeClientBundle(pathname)
157
+ const bundleResponse = await tryServeClientBundle(state.clientDir, pathname)
226
158
  if (bundleResponse) return bundleResponse
227
159
 
228
160
  // 3. Static files
229
- const staticResponse = await tryServeStatic(pathname)
161
+ const staticResponse = await tryServeStatic(state.publicDir, pathname)
230
162
  if (staticResponse) return staticResponse
231
163
 
232
164
  // 4. Route matching
233
- const match = matchRoute(routes, pathname, staticMap)
165
+ const match = matchRoute(state.routes, pathname, state.staticMap)
234
166
  if (!match) {
235
- const notFoundHtml = await render404Page(nonce)
167
+ const notFoundHtml = await render404Page(state.routesDir, nonce)
236
168
  return new Response(notFoundHtml, {
237
169
  status: 404,
238
170
  headers: { "Content-Type": "text/html", ...secHeaders },
@@ -242,11 +174,11 @@ async function main() {
242
174
  try {
243
175
  // Partial navigation (SPA client-side routing)
244
176
  if (request.headers.get("X-Gorsee-Navigate") === "partial") {
245
- return await handlePartialNavigation({ match, request, clientBuild })
177
+ return await handlePartialNavigation({ match, request, clientBuild: state.clientBuild })
246
178
  }
247
179
 
248
180
  return await handlePageRequest({
249
- match, request, nonce, start, clientBuild, secHeaders, wrapHTML,
181
+ match, request, nonce, start, clientBuild: state.clientBuild, secHeaders, wrapHTML,
250
182
  })
251
183
  } catch (err) {
252
184
  const errObj = err instanceof Error ? err : new Error(String(err))
@@ -265,9 +197,12 @@ async function main() {
265
197
  })
266
198
 
267
199
  log.info("dev server started", { url: `http://localhost:${server.port}` })
200
+ return server
268
201
  }
269
202
 
270
- main().catch((err) => {
271
- console.error("Failed to start dev server:", err)
272
- process.exit(1)
273
- })
203
+ if (import.meta.main) {
204
+ startDevServer().catch((err) => {
205
+ console.error("Failed to start dev server:", err)
206
+ process.exit(1)
207
+ })
208
+ }
@@ -0,0 +1,4 @@
1
+ // Browser-safe root entry used by client bundling.
2
+ // Mirrors the explicit public `gorsee/client` entrypoint.
3
+
4
+ export * from "./client.ts"
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
- // Gorsee.js -- main entry point
2
- // Re-exports core APIs for convenience
1
+ // Gorsee.js -- compatibility entry point.
2
+ // Prefer "gorsee/client" for browser-safe APIs, "gorsee/server" for server-only APIs,
3
+ // or "gorsee/compat" when you need an explicit legacy migration path.
3
4
 
5
+ /** @deprecated Import from "gorsee/client" instead. */
4
6
  export {
5
7
  createSignal,
6
8
  createComputed,
@@ -13,16 +15,29 @@ export {
13
15
  createMutation,
14
16
  } from "./reactive/index.ts"
15
17
 
18
+ /** @deprecated Import from "gorsee/client" instead. */
16
19
  export { Suspense } from "./runtime/suspense.ts"
20
+ /** @deprecated Import from "gorsee/client" instead. */
17
21
  export { Link } from "./runtime/link.ts"
22
+ /** @deprecated Import from "gorsee/client" instead. */
18
23
  export { Head } from "./runtime/head.ts"
24
+ /** @deprecated Import from "gorsee/client" instead. */
19
25
  export { navigate, onNavigate, beforeNavigate, getCurrentPath } from "./runtime/router.ts"
26
+ /** @deprecated Import from "gorsee/client" instead. */
20
27
  export { useFormAction } from "./runtime/form.ts"
28
+ /** @deprecated Import from "gorsee/client" instead. */
21
29
  export { Image } from "./runtime/image.ts"
30
+ /** @deprecated Import from "gorsee/client" instead. */
22
31
  export { ErrorBoundary } from "./runtime/error-boundary.ts"
32
+ /** @deprecated Import from "gorsee/client" instead. */
23
33
  export { island } from "./runtime/island.ts"
34
+ /** @deprecated Import from "gorsee/client" instead. */
24
35
  export { createEventSource } from "./server/sse.ts"
36
+ /** @deprecated Import from "gorsee/server" instead. */
25
37
  export { createAuth } from "./auth/index.ts"
38
+ /** @deprecated Import from "gorsee/client" instead. */
26
39
  export { typedLink, typedNavigate } from "./runtime/typed-routes.ts"
40
+ /** @deprecated Import from "gorsee/client" instead. */
27
41
  export { defineForm, validateForm, fieldAttrs } from "./runtime/validated-form.ts"
42
+ /** @deprecated Import from "gorsee/server" instead. */
28
43
  export { definePlugin, createPluginRunner, type GorseePlugin, type PluginContext } from "./plugins/index.ts"