gorsee 0.2.1 → 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 (51) 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 +11 -4
  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 +11 -3
  23. package/src/cli/cmd-typegen.ts +13 -3
  24. package/src/cli/cmd-upgrade.ts +10 -2
  25. package/src/cli/context.ts +12 -0
  26. package/src/cli/framework-md.ts +43 -16
  27. package/src/client.ts +26 -0
  28. package/src/dev/partial-handler.ts +17 -74
  29. package/src/dev/request-handler.ts +36 -67
  30. package/src/dev.ts +92 -157
  31. package/src/index-client.ts +4 -0
  32. package/src/index.ts +17 -2
  33. package/src/prod.ts +195 -253
  34. package/src/runtime/project.ts +73 -0
  35. package/src/server/cache-utils.ts +23 -0
  36. package/src/server/cache.ts +37 -14
  37. package/src/server/html-shell.ts +69 -0
  38. package/src/server/index.ts +40 -2
  39. package/src/server/manifest.ts +36 -0
  40. package/src/server/middleware.ts +18 -2
  41. package/src/server/not-found.ts +35 -0
  42. package/src/server/page-render.ts +123 -0
  43. package/src/server/redis-cache-store.ts +87 -0
  44. package/src/server/redis-client.ts +71 -0
  45. package/src/server/request-preflight.ts +45 -0
  46. package/src/server/route-request.ts +72 -0
  47. package/src/server/rpc-utils.ts +27 -0
  48. package/src/server/rpc.ts +70 -18
  49. package/src/server/sqlite-cache-store.ts +109 -0
  50. package/src/server/static-file.ts +63 -0
  51. package/src/server-entry.ts +36 -0
@@ -4,6 +4,7 @@
4
4
  import { join } from "node:path"
5
5
  import { mkdir, writeFile } from "node:fs/promises"
6
6
  import { createMigration } from "../db/migrate.ts"
7
+ import { createProjectContext, type RuntimeOptions } from "../runtime/project.ts"
7
8
 
8
9
  function capitalize(s: string): string {
9
10
  return s.charAt(0).toUpperCase() + s.slice(1)
@@ -18,7 +19,7 @@ function singularize(s: string): string {
18
19
 
19
20
  function listRoute(entity: string, singular: string): string {
20
21
  const cap = capitalize(singular)
21
- return `import { Head, Link } from "gorsee"
22
+ return `import { Head, Link } from "gorsee/client"
22
23
  import { SafeSQL } from "gorsee/types"
23
24
  import type { Context } from "gorsee/server"
24
25
 
@@ -49,7 +50,7 @@ export default function ${capitalize(entity)}ListPage(props: { data: { ${entity}
49
50
 
50
51
  function detailRoute(entity: string, singular: string): string {
51
52
  const cap = capitalize(singular)
52
- return `import { Head, Link } from "gorsee"
53
+ return `import { Head, Link } from "gorsee/client"
53
54
  import type { Context } from "gorsee/server"
54
55
 
55
56
  export async function loader(ctx: Context) {
@@ -73,7 +74,7 @@ export default function ${cap}DetailPage(props: { data: { ${singular}: { id: num
73
74
 
74
75
  function newRoute(entity: string, singular: string): string {
75
76
  const cap = capitalize(singular)
76
- return `import { Head, Link } from "gorsee"
77
+ return `import { Head, Link } from "gorsee/client"
77
78
  import { defineAction, parseFormData } from "gorsee/server"
78
79
 
79
80
  export const action = defineAction(async (ctx) => {
@@ -109,7 +110,9 @@ CREATE TABLE IF NOT EXISTS ${entity} (
109
110
  `
110
111
  }
111
112
 
112
- export async function runGenerate(args: string[]) {
113
+ export interface GenerateCommandOptions extends RuntimeOptions {}
114
+
115
+ export async function generateCrudScaffold(args: string[], options: GenerateCommandOptions = {}) {
113
116
  const entity = args[0]
114
117
  if (!entity) {
115
118
  console.error("Usage: gorsee generate <entity-name>")
@@ -117,9 +120,9 @@ export async function runGenerate(args: string[]) {
117
120
  process.exit(1)
118
121
  }
119
122
 
120
- const cwd = process.cwd()
123
+ const { cwd, paths } = createProjectContext(options)
121
124
  const singular = singularize(entity)
122
- const routeDir = join(cwd, "routes", entity)
125
+ const routeDir = join(paths.routesDir, entity)
123
126
 
124
127
  console.log(`\n Generating CRUD for: ${entity}\n`)
125
128
 
@@ -130,7 +133,7 @@ export async function runGenerate(args: string[]) {
130
133
  await writeFile(join(routeDir, "new.tsx"), newRoute(entity, singular))
131
134
 
132
135
  // Create migration
133
- const migrationDir = join(cwd, "migrations")
136
+ const migrationDir = paths.migrationsDir
134
137
  await mkdir(migrationDir, { recursive: true })
135
138
  const migrationFile = await createMigration(migrationDir, `create_${entity}`)
136
139
  const migrationPath = join(migrationDir, migrationFile)
@@ -145,3 +148,8 @@ export async function runGenerate(args: string[]) {
145
148
  console.log(" Next: run `gorsee migrate` to apply the migration")
146
149
  console.log()
147
150
  }
151
+
152
+ /** @deprecated Use generateCrudScaffold() for programmatic access. */
153
+ export async function runGenerate(args: string[], options: GenerateCommandOptions = {}) {
154
+ return generateCrudScaffold(args, options)
155
+ }
@@ -1,12 +1,15 @@
1
1
  // gorsee migrate -- run database migrations
2
2
  // gorsee migrate create <name> -- create new migration file
3
3
 
4
- import { join } from "node:path"
5
4
  import { runMigrations, createMigration } from "../db/migrate.ts"
5
+ import { createProjectContext, type RuntimeOptions } from "../runtime/project.ts"
6
6
 
7
- export async function runMigrate(args: string[]) {
8
- const cwd = process.cwd()
9
- const migrationsDir = join(cwd, "migrations")
7
+ export interface MigrateCommandOptions extends RuntimeOptions {
8
+ dbPath?: string
9
+ }
10
+
11
+ export async function runProjectMigrations(args: string[], options: MigrateCommandOptions = {}) {
12
+ const { env, paths } = createProjectContext(options)
10
13
  const subcommand = args[0]
11
14
 
12
15
  if (subcommand === "create") {
@@ -15,16 +18,16 @@ export async function runMigrate(args: string[]) {
15
18
  console.error("Usage: gorsee migrate create <migration-name>")
16
19
  process.exit(1)
17
20
  }
18
- const filename = await createMigration(migrationsDir, name)
21
+ const filename = await createMigration(paths.migrationsDir, name)
19
22
  console.log(`\n Created: migrations/${filename}\n`)
20
23
  return
21
24
  }
22
25
 
23
26
  // Default: run pending migrations
24
- const dbPath = process.env.DATABASE_URL ?? join(cwd, "data.sqlite")
27
+ const dbPath = options.dbPath ?? env.DATABASE_URL ?? paths.dataFile
25
28
  console.log("\n Running migrations...\n")
26
29
 
27
- const result = await runMigrations(dbPath, migrationsDir)
30
+ const result = await runMigrations(dbPath, paths.migrationsDir)
28
31
 
29
32
  if (result.applied.length > 0) {
30
33
  console.log(" Applied:")
@@ -43,3 +46,8 @@ export async function runMigrate(args: string[]) {
43
46
 
44
47
  console.log(`\n Done: ${result.applied.length} migration(s) applied\n`)
45
48
  }
49
+
50
+ /** @deprecated Use runProjectMigrations() for programmatic access. */
51
+ export async function runMigrate(args: string[], options: MigrateCommandOptions = {}) {
52
+ return runProjectMigrations(args, options)
53
+ }
@@ -1,11 +1,13 @@
1
1
  // gorsee routes -- list all routes
2
2
 
3
- import { join } from "node:path"
4
3
  import { createRouter } from "../router/scanner.ts"
4
+ import { createProjectContext, type RuntimeOptions } from "../runtime/project.ts"
5
5
 
6
- export async function runRoutes(_args: string[]) {
7
- const routesDir = join(process.cwd(), "routes")
8
- const routes = await createRouter(routesDir)
6
+ export interface RoutesCommandOptions extends RuntimeOptions {}
7
+
8
+ export async function listRoutes(options: RoutesCommandOptions = {}) {
9
+ const { cwd, paths } = createProjectContext(options)
10
+ const routes = await createRouter(paths.routesDir)
9
11
 
10
12
  if (routes.length === 0) {
11
13
  console.log("\n No routes found in routes/\n")
@@ -21,9 +23,14 @@ export async function runRoutes(_args: string[]) {
21
23
  console.log(
22
24
  " " +
23
25
  route.path.padEnd(25) +
24
- route.filePath.replace(process.cwd() + "/", "").padEnd(40) +
26
+ route.filePath.replace(cwd + "/", "").padEnd(40) +
25
27
  params
26
28
  )
27
29
  }
28
30
  console.log()
29
31
  }
32
+
33
+ /** @deprecated Use listRoutes() for programmatic access. */
34
+ export async function runRoutes(_args: string[], options: RoutesCommandOptions = {}) {
35
+ return listRoutes(options)
36
+ }
@@ -2,14 +2,18 @@
2
2
 
3
3
  import { join } from "node:path"
4
4
  import { stat } from "node:fs/promises"
5
+ import { createProjectContext, type RuntimeOptions } from "../runtime/project.ts"
5
6
 
6
- export async function runStart(_args: string[]) {
7
- const cwd = process.cwd()
8
- const distDir = join(cwd, "dist")
7
+ export interface StartCommandOptions extends RuntimeOptions {
8
+ port?: number
9
+ }
10
+
11
+ export async function startBuiltProject(options: StartCommandOptions = {}) {
12
+ const { paths } = createProjectContext(options)
9
13
 
10
14
  // Verify build exists
11
15
  try {
12
- await stat(join(distDir, "manifest.json"))
16
+ await stat(join(paths.distDir, "manifest.json"))
13
17
  } catch {
14
18
  console.error("\n Error: No production build found.")
15
19
  console.error(" Run `gorsee build` first.\n")
@@ -17,5 +21,10 @@ export async function runStart(_args: string[]) {
17
21
  }
18
22
 
19
23
  const { startProductionServer } = await import("../prod.ts")
20
- await startProductionServer()
24
+ await startProductionServer({ cwd: paths.cwd, port: options.port })
25
+ }
26
+
27
+ /** @deprecated Use startBuiltProject() for programmatic access. */
28
+ export async function runStart(_args: string[], options: StartCommandOptions = {}) {
29
+ return startBuiltProject(options)
21
30
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { readdir, stat } from "node:fs/promises"
4
4
  import { join } from "node:path"
5
+ import { createProjectContext, type RuntimeOptions } from "../runtime/project.ts"
5
6
 
6
7
  interface TestFlags {
7
8
  watch: boolean
@@ -78,8 +79,10 @@ export function buildTestArgs(flags: TestFlags, files: string[]): string[] {
78
79
  return bunArgs
79
80
  }
80
81
 
81
- export async function runTest(args: string[]) {
82
- const cwd = process.cwd()
82
+ export interface TestCommandOptions extends RuntimeOptions {}
83
+
84
+ export async function runTests(args: string[], options: TestCommandOptions = {}) {
85
+ const { cwd, env } = createProjectContext(options)
83
86
  const flags = parseFlags(args)
84
87
  const pattern = getTestPattern(flags)
85
88
 
@@ -100,7 +103,7 @@ export async function runTest(args: string[]) {
100
103
  stdout: "inherit",
101
104
  stderr: "inherit",
102
105
  env: {
103
- ...process.env,
106
+ ...env,
104
107
  NODE_ENV: "test",
105
108
  GORSEE_TEST: "1",
106
109
  },
@@ -117,5 +120,10 @@ export async function runTest(args: string[]) {
117
120
  }
118
121
  }
119
122
 
123
+ /** @deprecated Use runTests() for programmatic access. */
124
+ export async function runTest(args: string[], options: TestCommandOptions = {}) {
125
+ return runTests(args, options)
126
+ }
127
+
120
128
  // Re-export for testing
121
129
  export { parseFlags, findTestFiles, getTestPattern }
@@ -4,6 +4,7 @@
4
4
  import { join } from "node:path"
5
5
  import { mkdir, writeFile } from "node:fs/promises"
6
6
  import { createRouter, type Route } from "../router/index.ts"
7
+ import { createProjectContext, type RuntimeOptions } from "../runtime/project.ts"
7
8
 
8
9
  const OUTPUT_DIR = ".gorsee"
9
10
  const TYPES_FILE = "routes.d.ts"
@@ -54,9 +55,13 @@ function generateDeclaration(routes: Route[]): string {
54
55
  return lines.join("\n")
55
56
  }
56
57
 
57
- export async function runTypegen(args: string[]): Promise<void> {
58
- const cwd = process.cwd()
59
- const routesDir = args[0] ?? join(cwd, "routes")
58
+ export interface TypegenCommandOptions extends RuntimeOptions {
59
+ routesDir?: string
60
+ }
61
+
62
+ export async function generateRouteTypes(args: string[], options: TypegenCommandOptions = {}): Promise<void> {
63
+ const { cwd, paths } = createProjectContext(options)
64
+ const routesDir = options.routesDir ?? args[0] ?? paths.routesDir
60
65
 
61
66
  console.log(`Scanning routes in: ${routesDir}`)
62
67
 
@@ -81,3 +86,8 @@ export async function runTypegen(args: string[]): Promise<void> {
81
86
  console.log(` ${route.path}${params}`)
82
87
  }
83
88
  }
89
+
90
+ /** @deprecated Use generateRouteTypes() for programmatic access. */
91
+ export async function runTypegen(args: string[], options: TypegenCommandOptions = {}): Promise<void> {
92
+ return generateRouteTypes(args, options)
93
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { readFile } from "node:fs/promises"
4
4
  import { join } from "node:path"
5
+ import { createProjectContext, type RuntimeOptions } from "../runtime/project.ts"
5
6
 
6
7
  interface UpgradeFlags {
7
8
  check: boolean
@@ -75,8 +76,10 @@ async function checkMigrationHints(cwd: string): Promise<string[]> {
75
76
  return hints
76
77
  }
77
78
 
78
- export async function runUpgrade(args: string[]) {
79
- const cwd = process.cwd()
79
+ export interface UpgradeCommandOptions extends RuntimeOptions {}
80
+
81
+ export async function upgradeFramework(args: string[], options: UpgradeCommandOptions = {}) {
82
+ const { cwd } = createProjectContext(options)
80
83
  const flags = parseUpgradeFlags(args)
81
84
 
82
85
  const current = await getCurrentVersion(cwd)
@@ -133,3 +136,8 @@ export async function runUpgrade(args: string[]) {
133
136
 
134
137
  console.log(`\n Upgraded successfully to v${latest}\n`)
135
138
  }
139
+
140
+ /** @deprecated Use upgradeFramework() for programmatic access. */
141
+ export async function runUpgrade(args: string[], options: UpgradeCommandOptions = {}) {
142
+ return upgradeFramework(args, options)
143
+ }
@@ -0,0 +1,12 @@
1
+ /** @deprecated Import shared project context from "../runtime/project" in new code. */
2
+ export {
3
+ createProjectContext as createCommandContext,
4
+ resolveProjectPaths,
5
+ } from "../runtime/project.ts"
6
+
7
+ /** @deprecated Import shared runtime types from "../runtime/project" in new code. */
8
+ export type {
9
+ RuntimeOptions as CommandRuntimeOptions,
10
+ ProjectContext as CommandContext,
11
+ ProjectPaths,
12
+ } from "../runtime/project.ts"
@@ -35,29 +35,56 @@ app.config.ts Configuration
35
35
  ## Imports
36
36
 
37
37
  \`\`\`typescript
38
- // Reactivity
39
- import { createSignal, createComputed, createEffect, createResource, createStore } from "gorsee/reactive"
38
+ // Browser-safe route code
39
+ import { createSignal, createComputed, createEffect, createResource, createStore } from "gorsee/client"
40
40
 
41
- // Types (compile-time safety)
42
- import { SafeSQL } from "gorsee/types" // SQL injection = compile error
43
- import { SafeHTML, sanitize } from "gorsee/types" // XSS = compile error
44
- import { SafeURL, validateURL } from "gorsee/types"
45
- import { validate, type UserInput } from "gorsee/types"
41
+ // Server-only code
42
+ import { server, middleware, type Context, createDB, createAuth, cors, log } from "gorsee/server"
43
+ \`\`\`
46
44
 
47
- // Database
48
- import { createDB } from "gorsee/db"
45
+ ## Import Boundaries
49
46
 
50
- // Server functions
51
- import { server } from "gorsee/server"
52
- import { middleware, type Context } from "gorsee/server"
47
+ - \`gorsee/client\` for route components, islands, navigation, forms, and reactive primitives
48
+ - \`gorsee/server\` for middleware, loaders, RPC, auth, db, security, env, and logging
49
+ - Root \`gorsee\` is compatibility-only and should not be used in new code
50
+ - \`gorsee/compat\` is available as an explicit legacy migration entrypoint
51
+
52
+ ## Adapter Recipes
53
53
 
54
- // Logging
55
- import { log } from "gorsee/log"
54
+ \`\`\`typescript
55
+ import { createClient } from "redis"
56
+ import {
57
+ createAuth,
58
+ createRedisSessionStore,
59
+ createRedisCacheStore,
60
+ createNodeRedisLikeClient,
61
+ routeCache,
62
+ } from "gorsee/server"
63
+
64
+ const redis = createClient({ url: process.env.REDIS_URL })
65
+ await redis.connect()
66
+ const redisClient = createNodeRedisLikeClient(redis)
67
+
68
+ const auth = createAuth({
69
+ secret: process.env.SESSION_SECRET!,
70
+ store: createRedisSessionStore(redisClient, { prefix: "app:sessions" }),
71
+ })
56
72
 
57
- // Escape hatches (DANGER)
58
- import { unsafeSQL, unsafeHTML } from "gorsee/unsafe"
73
+ export const cache = routeCache({
74
+ maxAge: 60,
75
+ staleWhileRevalidate: 300,
76
+ store: createRedisCacheStore(redisClient, {
77
+ prefix: "app:cache",
78
+ maxEntryAgeMs: 360_000,
79
+ }),
80
+ })
59
81
  \`\`\`
60
82
 
83
+ - SQLite adapters are the default persistent single-node path
84
+ - Redis adapters are the default multi-instance path
85
+ - \`createNodeRedisLikeClient()\` and \`createIORedisLikeClient()\` normalize real Redis SDK clients to the framework adapter contract
86
+ - RPC handlers remain process-local by design; do not try to distribute closures through Redis
87
+
61
88
  ## Patterns
62
89
 
63
90
  ### Page Route
package/src/client.ts ADDED
@@ -0,0 +1,26 @@
1
+ // Explicit browser-safe public entrypoint.
2
+ // Use this in route components and client-facing modules.
3
+
4
+ export {
5
+ createSignal,
6
+ createComputed,
7
+ createEffect,
8
+ createResource,
9
+ createStore,
10
+ createLive,
11
+ invalidateResource,
12
+ invalidateAll,
13
+ createMutation,
14
+ } from "./reactive/index.ts"
15
+
16
+ export { Suspense } from "./runtime/suspense.ts"
17
+ export { Link } from "./runtime/link.ts"
18
+ export { Head } from "./runtime/head.ts"
19
+ export { navigate, onNavigate, beforeNavigate, getCurrentPath } from "./runtime/router.ts"
20
+ export { useFormAction } from "./runtime/form.ts"
21
+ export { Image } from "./runtime/image.ts"
22
+ export { ErrorBoundary } from "./runtime/error-boundary.ts"
23
+ export { island } from "./runtime/island.ts"
24
+ export { createEventSource } from "./server/sse.ts"
25
+ export { typedLink, typedNavigate } from "./runtime/typed-routes.ts"
26
+ export { defineForm, validateForm, fieldAttrs } from "./runtime/validated-form.ts"
@@ -2,8 +2,12 @@
2
2
  // Returns JSON with rendered HTML + metadata instead of full page
3
3
 
4
4
  import { createContext } from "../server/middleware.ts"
5
- import { renderToString, ssrJsx } from "../runtime/server.ts"
6
- import { resetServerHead, getServerHead } from "../runtime/head.ts"
5
+ import {
6
+ buildPartialResponsePayload,
7
+ createClientScriptPath,
8
+ renderPageDocument,
9
+ resolvePageRoute,
10
+ } from "../server/page-render.ts"
7
11
  import type { MatchResult } from "../router/matcher.ts"
8
12
  import type { BuildResult } from "../build/client.ts"
9
13
 
@@ -13,15 +17,6 @@ interface PartialRenderOptions {
13
17
  clientBuild: BuildResult
14
18
  }
15
19
 
16
- interface PartialResponse {
17
- html: string
18
- data?: unknown
19
- params?: Record<string, string>
20
- title?: string
21
- css?: string[]
22
- script?: string
23
- }
24
-
25
20
  export async function handlePartialNavigation(opts: PartialRenderOptions): Promise<Response> {
26
21
  const { match, request, clientBuild } = opts
27
22
  const mod = await import(match.route.filePath)
@@ -32,76 +27,24 @@ export async function handlePartialNavigation(opts: PartialRenderOptions): Promi
32
27
  return new Response("Not a page route", { status: 400 })
33
28
  }
34
29
 
35
- const component = mod.default as Function | undefined
36
- if (typeof component !== "function") {
30
+ const resolved = await resolvePageRoute(mod, match, ctx)
31
+ if (!resolved) {
37
32
  return new Response(JSON.stringify({ error: "Route has no default export" }), {
38
33
  status: 500,
39
34
  headers: { "Content-Type": "application/json" },
40
35
  })
41
36
  }
42
37
 
43
- // Parallel loading: import layout modules + run page loader simultaneously
44
- const layoutPaths = match.route.layoutPaths ?? []
45
- const layoutImportPromises = layoutPaths.map((lp) => import(lp))
46
- const pageLoaderPromise = typeof mod.loader === "function" ? mod.loader(ctx) : undefined
47
-
48
- const [layoutMods, loaderData] = await Promise.all([
49
- Promise.all(layoutImportPromises),
50
- pageLoaderPromise,
51
- ])
52
-
53
- // Run layout loaders in parallel
54
- const layoutLoaderPromises = layoutMods.map((lm) =>
55
- typeof lm.loader === "function" ? lm.loader(ctx) : undefined,
38
+ const { pageComponent, loaderData, cssFiles } = resolved
39
+ const rendered = renderPageDocument(pageComponent, ctx, match.params, loaderData)
40
+ const clientScript = createClientScriptPath(clientBuild.entryMap.get(match.route.path))
41
+ const result = buildPartialResponsePayload(
42
+ rendered,
43
+ loaderData,
44
+ match.params,
45
+ cssFiles,
46
+ clientScript,
56
47
  )
57
- const layoutLoaderResults = await Promise.all(layoutLoaderPromises)
58
-
59
- // CSS
60
- const cssFiles: string[] = []
61
- if (typeof mod.css === "string") cssFiles.push(mod.css)
62
- if (Array.isArray(mod.css)) cssFiles.push(...(mod.css as string[]))
63
-
64
- // Nested layout wrapping: outermost first, innermost wraps page
65
- let pageComponent: Function = component
66
- for (let i = layoutMods.length - 1; i >= 0; i--) {
67
- const Layout = layoutMods[i]!.default
68
- if (typeof Layout === "function") {
69
- const inner = pageComponent
70
- const layoutData = layoutLoaderResults[i]
71
- pageComponent = (props: Record<string, unknown>) =>
72
- Layout({ ...props, data: layoutData, children: inner(props) })
73
- }
74
- }
75
-
76
- // Render
77
- resetServerHead()
78
- const pageProps = { params: match.params, ctx, data: loaderData }
79
- const vnode = ssrJsx(pageComponent as any, pageProps)
80
- const html = renderToString(vnode)
81
-
82
- // Extract title from Head component
83
- const headElements = getServerHead()
84
- let title: string | undefined
85
- for (const el of headElements) {
86
- const titleMatch = el.match(/<title>(.+?)<\/title>/)
87
- if (titleMatch) {
88
- title = titleMatch[1]
89
- break
90
- }
91
- }
92
-
93
- // Client JS path
94
- const clientJsFile = clientBuild.entryMap.get(match.route.path)
95
- const clientScript = clientJsFile ? `/_gorsee/${clientJsFile}` : undefined
96
-
97
- const result: PartialResponse = {
98
- html,
99
- data: loaderData,
100
- params: Object.keys(match.params).length > 0 ? match.params : undefined,
101
- title,
102
- css: cssFiles.length > 0 ? cssFiles : undefined,
103
- script: clientScript,
104
- }
105
48
 
106
49
  return new Response(JSON.stringify(result), {
107
50
  headers: { "Content-Type": "application/json" },