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
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 {
|
|
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 {
|
|
18
|
-
import {
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
// ---
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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 (
|
|
147
|
-
|
|
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
|
-
|
|
83
|
+
buildState.inProgress = true
|
|
151
84
|
try {
|
|
152
85
|
__resetRPCState()
|
|
153
|
-
routes = await createRouter(
|
|
154
|
-
staticMap = buildStaticMap(routes)
|
|
155
|
-
clientBuild = await buildClientBundles(routes,
|
|
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
|
-
|
|
161
|
-
if (
|
|
162
|
-
|
|
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
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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: [
|
|
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
|
|
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
|
|
206
|
-
if (
|
|
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
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
// Gorsee.js --
|
|
2
|
-
//
|
|
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"
|