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/cli/framework-md.ts
CHANGED
|
@@ -35,29 +35,56 @@ app.config.ts Configuration
|
|
|
35
35
|
## Imports
|
|
36
36
|
|
|
37
37
|
\`\`\`typescript
|
|
38
|
-
//
|
|
39
|
-
import { createSignal, createComputed, createEffect, createResource, createStore } from "gorsee/
|
|
38
|
+
// Browser-safe route code
|
|
39
|
+
import { createSignal, createComputed, createEffect, createResource, createStore } from "gorsee/client"
|
|
40
40
|
|
|
41
|
-
//
|
|
42
|
-
import {
|
|
43
|
-
|
|
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
|
-
|
|
48
|
-
import { createDB } from "gorsee/db"
|
|
45
|
+
## Import Boundaries
|
|
49
46
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
import {
|
|
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
|
-
|
|
58
|
-
|
|
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/cli/index.ts
CHANGED
|
@@ -15,6 +15,9 @@ const COMMANDS: Record<string, string> = {
|
|
|
15
15
|
generate: "Generate CRUD scaffold for entity",
|
|
16
16
|
typegen: "Generate typed route definitions",
|
|
17
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",
|
|
18
21
|
help: "Show this help message",
|
|
19
22
|
}
|
|
20
23
|
|
|
@@ -60,6 +63,21 @@ async function main() {
|
|
|
60
63
|
const { runDeploy } = await import("./cmd-deploy.ts")
|
|
61
64
|
await runDeploy(args.slice(1))
|
|
62
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
|
+
}
|
|
63
81
|
case "help":
|
|
64
82
|
case undefined:
|
|
65
83
|
case "--help":
|
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 {
|
|
6
|
-
|
|
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
|
|
36
|
-
if (
|
|
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
|
-
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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" },
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// Page route request handler — SSR + streaming + layouts + loaders
|
|
2
2
|
|
|
3
|
-
import { createContext
|
|
3
|
+
import { createContext } from "../server/middleware.ts"
|
|
4
4
|
import { renderToString, ssrJsx } from "../runtime/server.ts"
|
|
5
5
|
import { renderToStream, streamJsx } from "../runtime/stream.ts"
|
|
6
|
+
import { resetServerHead, getServerHead } from "../runtime/head.ts"
|
|
6
7
|
import { handleAction } from "../server/action.ts"
|
|
7
8
|
import { log } from "../log/index.ts"
|
|
9
|
+
import { createClientScriptPath, renderPageDocument, resolvePageRoute } from "../server/page-render.ts"
|
|
10
|
+
import { handleRouteRequest } from "../server/route-request.ts"
|
|
8
11
|
import type { MatchResult } from "../router/matcher.ts"
|
|
9
12
|
import type { BuildResult } from "../build/client.ts"
|
|
10
13
|
|
|
@@ -15,7 +18,7 @@ interface RenderOptions {
|
|
|
15
18
|
start: number
|
|
16
19
|
clientBuild: BuildResult
|
|
17
20
|
secHeaders: Record<string, string>
|
|
18
|
-
wrapHTML: (body: string, nonce: string, opts?: { title?: string; clientScript?: string; loaderData?: unknown; params?: Record<string, string>; cssFiles?: string[] }) => string
|
|
21
|
+
wrapHTML: (body: string, nonce: string, opts?: { title?: string; clientScript?: string; loaderData?: unknown; params?: Record<string, string>; cssFiles?: string[]; headElements?: string[] }) => string
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
async function renderPageRoute(mod: Record<string, unknown>, opts: RenderOptions): Promise<Response> {
|
|
@@ -47,49 +50,24 @@ async function renderPageRoute(mod: Record<string, unknown>, opts: RenderOptions
|
|
|
47
50
|
return new Response("Route has no default export", { status: 500 })
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const pageLoaderPromise = typeof mod.loader === "function" ? mod.loader(ctx) : undefined
|
|
54
|
-
|
|
55
|
-
const [layoutMods, loaderData] = await Promise.all([
|
|
56
|
-
Promise.all(layoutImportPromises),
|
|
57
|
-
pageLoaderPromise,
|
|
58
|
-
])
|
|
59
|
-
|
|
60
|
-
// Run layout loaders in parallel
|
|
61
|
-
const layoutLoaderPromises = layoutMods.map((lm) =>
|
|
62
|
-
typeof lm.loader === "function" ? lm.loader(ctx) : undefined,
|
|
63
|
-
)
|
|
64
|
-
const layoutLoaderResults = await Promise.all(layoutLoaderPromises)
|
|
65
|
-
|
|
66
|
-
// CSS
|
|
67
|
-
const cssFiles: string[] = []
|
|
68
|
-
if (typeof mod.css === "string") cssFiles.push(mod.css)
|
|
69
|
-
if (Array.isArray(mod.css)) cssFiles.push(...(mod.css as string[]))
|
|
70
|
-
|
|
71
|
-
// Nested layout wrapping: outermost first, innermost wraps page
|
|
72
|
-
let pageComponent: Function = component
|
|
73
|
-
for (let i = layoutMods.length - 1; i >= 0; i--) {
|
|
74
|
-
const Layout = layoutMods[i]!.default
|
|
75
|
-
if (typeof Layout === "function") {
|
|
76
|
-
const inner = pageComponent
|
|
77
|
-
const layoutData = layoutLoaderResults[i]
|
|
78
|
-
pageComponent = (props: Record<string, unknown>) =>
|
|
79
|
-
Layout({ ...props, data: layoutData, children: inner(props) })
|
|
80
|
-
}
|
|
53
|
+
const resolved = await resolvePageRoute(mod, match, ctx)
|
|
54
|
+
if (!resolved) {
|
|
55
|
+
return new Response("Route has no default export", { status: 500 })
|
|
81
56
|
}
|
|
82
57
|
|
|
58
|
+
const { pageComponent, loaderData, cssFiles, renderMode } = resolved
|
|
83
59
|
const pageProps = { params: match.params, ctx, data: loaderData }
|
|
84
|
-
const
|
|
85
|
-
const clientJsFile = clientBuild.entryMap.get(match.route.path)
|
|
86
|
-
const clientScript = clientJsFile ? `/_gorsee/${clientJsFile}` : undefined
|
|
60
|
+
const clientScript = createClientScriptPath(clientBuild.entryMap.get(match.route.path))
|
|
87
61
|
const htmlOpts = { clientScript, loaderData, params: match.params, cssFiles }
|
|
88
62
|
|
|
89
63
|
if (renderMode === "stream") {
|
|
64
|
+
resetServerHead()
|
|
90
65
|
const vnode = streamJsx(pageComponent as any, pageProps)
|
|
91
66
|
const stream = renderToStream(vnode, {
|
|
92
|
-
shell: (body: string) => wrapHTML(body, nonce,
|
|
67
|
+
shell: (body: string) => wrapHTML(body, nonce, {
|
|
68
|
+
...htmlOpts,
|
|
69
|
+
headElements: getServerHead(),
|
|
70
|
+
}),
|
|
93
71
|
})
|
|
94
72
|
const elapsed = (performance.now() - start).toFixed(1)
|
|
95
73
|
log.info("request", { method: request.method, path: match.route.path, status: 200, ms: elapsed, mode: "stream" })
|
|
@@ -99,9 +77,11 @@ async function renderPageRoute(mod: Record<string, unknown>, opts: RenderOptions
|
|
|
99
77
|
}
|
|
100
78
|
|
|
101
79
|
// Default: async SSR
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
80
|
+
const rendered = renderPageDocument(pageComponent, ctx, match.params, loaderData)
|
|
81
|
+
const html = wrapHTML(rendered.html, nonce, {
|
|
82
|
+
...htmlOpts,
|
|
83
|
+
headElements: rendered.headElements,
|
|
84
|
+
})
|
|
105
85
|
const elapsed = (performance.now() - start).toFixed(1)
|
|
106
86
|
log.info("request", { method: request.method, path: match.route.path, status: 200, ms: elapsed })
|
|
107
87
|
|
|
@@ -112,33 +92,22 @@ async function renderPageRoute(mod: Record<string, unknown>, opts: RenderOptions
|
|
|
112
92
|
|
|
113
93
|
export async function handlePageRequest(opts: RenderOptions): Promise<Response> {
|
|
114
94
|
const { match, request } = opts
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
status: err.status,
|
|
132
|
-
headers: { Location: err.url },
|
|
133
|
-
})
|
|
134
|
-
}
|
|
135
|
-
// Error boundary: render _error.tsx if available
|
|
136
|
-
if (match.route.errorPath) {
|
|
137
|
-
const errObj = err instanceof Error ? err : new Error(String(err))
|
|
138
|
-
return renderErrorBoundary(match.route.errorPath, errObj, opts)
|
|
139
|
-
}
|
|
140
|
-
throw err
|
|
141
|
-
}
|
|
95
|
+
return handleRouteRequest({
|
|
96
|
+
match,
|
|
97
|
+
request,
|
|
98
|
+
onPartialRequest: async () => {
|
|
99
|
+
const { handlePartialNavigation } = await import("./partial-handler.ts")
|
|
100
|
+
return handlePartialNavigation({ match, request, clientBuild: opts.clientBuild })
|
|
101
|
+
},
|
|
102
|
+
onPageRequest: async ({ mod }) => renderPageRoute(mod, opts),
|
|
103
|
+
onRouteError: async (err) => {
|
|
104
|
+
if (match.route.errorPath) {
|
|
105
|
+
const errObj = err instanceof Error ? err : new Error(String(err))
|
|
106
|
+
return renderErrorBoundary(match.route.errorPath, errObj, opts)
|
|
107
|
+
}
|
|
108
|
+
throw err
|
|
109
|
+
},
|
|
110
|
+
})
|
|
142
111
|
}
|
|
143
112
|
|
|
144
113
|
async function renderErrorBoundary(errorPath: string, error: Error, opts: RenderOptions): Promise<Response> {
|