@rshono/core 1.0.0-rc.18 → 1.0.0-rc.19
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 +13 -3
- package/dist/builder/rspack-config.d.ts.map +1 -1
- package/dist/builder/rspack-config.js +5 -1
- package/dist/builder/rspack-config.js.map +1 -1
- package/dist/builder/server-only-imports.d.ts +33 -0
- package/dist/builder/server-only-imports.d.ts.map +1 -0
- package/dist/builder/server-only-imports.js +51 -0
- package/dist/builder/server-only-imports.js.map +1 -0
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/build.js +18 -9
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/dev.d.ts.map +1 -1
- package/dist/cli/dev.js +45 -0
- package/dist/cli/dev.js.map +1 -1
- package/dist/deploy/presets.d.ts +8 -0
- package/dist/deploy/presets.d.ts.map +1 -1
- package/dist/deploy/presets.js +1 -0
- package/dist/deploy/presets.js.map +1 -1
- package/dist/router.d.ts +33 -18
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js.map +1 -1
- package/dist/runtime/boundaries.d.ts +11 -4
- package/dist/runtime/boundaries.d.ts.map +1 -1
- package/dist/runtime/boundaries.js.map +1 -1
- package/dist/runtime/context.d.ts +9 -2
- package/dist/runtime/context.d.ts.map +1 -1
- package/dist/runtime/context.js +12 -3
- package/dist/runtime/context.js.map +1 -1
- package/dist/runtime/entry.client.js +124 -19
- package/dist/runtime/entry.client.js.map +1 -1
- package/dist/runtime/entry.rsc.d.ts +7 -0
- package/dist/runtime/entry.rsc.d.ts.map +1 -1
- package/dist/runtime/entry.rsc.js +17 -20
- package/dist/runtime/entry.rsc.js.map +1 -1
- package/dist/runtime/validate-entries.d.ts +49 -4
- package/dist/runtime/validate-entries.d.ts.map +1 -1
- package/dist/runtime/validate-entries.js +118 -2
- package/dist/runtime/validate-entries.js.map +1 -1
- package/dist/server/prerendered.d.ts +0 -5
- package/dist/server/prerendered.d.ts.map +1 -1
- package/dist/server/prerendered.js +1 -1
- package/dist/server/prerendered.js.map +1 -1
- package/dist/server/server-config.d.ts +8 -1
- package/dist/server/server-config.d.ts.map +1 -1
- package/dist/server/server-config.js +2 -1
- package/dist/server/server-config.js.map +1 -1
- package/dist/server/ssg.d.ts.map +1 -1
- package/dist/server/ssg.js +24 -7
- package/dist/server/ssg.js.map +1 -1
- package/dist/server/static.d.ts.map +1 -1
- package/dist/server/static.js +5 -1
- package/dist/server/static.js.map +1 -1
- package/package.json +2 -2
|
@@ -9,11 +9,14 @@
|
|
|
9
9
|
* The mistakes that produced no error at all were worse: a duplicated `path` whose second entry silently
|
|
10
10
|
* never ran, a `staticPaths` on a route that renders per request.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
* prerender pass; at `rshono dev` startup; and when a deployed server boots.
|
|
12
|
+
* The route *table* is checked at module load — during `rshono build`, which imports the server bundle for
|
|
13
|
+
* the prerender pass; at `rshono dev` startup; and when a deployed server boots. The modules the table points
|
|
14
|
+
* *at* cannot be, because they load lazily: {@link assertPageModule} and {@link assertEndpointModule} run on
|
|
15
|
+
* first request, and {@link assertRouteModules} runs both against every route during `rshono build`.
|
|
14
16
|
*/
|
|
15
|
-
import type { Hono } from 'hono';
|
|
16
|
-
import {
|
|
17
|
+
import type { Handler, Hono } from 'hono';
|
|
18
|
+
import type { ServerEntry } from 'react-server-dom-rspack/server';
|
|
19
|
+
import { type PageComponent, type RouteConfig } from '../router.js';
|
|
17
20
|
/**
|
|
18
21
|
* Normalises and checks what `src/routes.ts` exported as `routes`.
|
|
19
22
|
*
|
|
@@ -30,4 +33,46 @@ export declare function validateRoutesModule(exported: unknown): RouteConfig;
|
|
|
30
33
|
* which reads `routes` off it and mounts the handlers, so those are what have to be there.
|
|
31
34
|
*/
|
|
32
35
|
export declare function validateServerApp(exported: unknown): Hono | null;
|
|
36
|
+
/**
|
|
37
|
+
* The server component a page module must default-export, checked rather than cast.
|
|
38
|
+
*
|
|
39
|
+
* Two structural mistakes land here, and neither can serve *any* request, which is why they are also checked
|
|
40
|
+
* at build time by {@link assertRouteModules}:
|
|
41
|
+
*
|
|
42
|
+
* - no default export at all — a named `export function Page`, or a file that exports its layout instead;
|
|
43
|
+
* - a `'use client'` page. That module's default export is a client reference, so it carries no
|
|
44
|
+
* `entryJsFiles` — the list of scripts the document has to boot, recorded only for a *server* entry.
|
|
45
|
+
* Rendering it anyway would produce a page with no client runtime, which is worse than a message.
|
|
46
|
+
*/
|
|
47
|
+
export declare function assertPageModule(mod: {
|
|
48
|
+
default?: unknown;
|
|
49
|
+
}, label: string): ServerEntry<PageComponent>;
|
|
50
|
+
/**
|
|
51
|
+
* The Hono handler an endpoint module must export as `handler`.
|
|
52
|
+
*
|
|
53
|
+
* The page half of this fork has been checked since it existed; this half used to destructure and call, which
|
|
54
|
+
* turns the one mistake people actually make — a method-named export, the shape other frameworks ask for —
|
|
55
|
+
* into `TypeError: r is not a function` from a minified frame. That is verbatim the failure mode this file's
|
|
56
|
+
* header names as the reason it exists.
|
|
57
|
+
*/
|
|
58
|
+
export declare function assertEndpointModule(mod: {
|
|
59
|
+
handler?: unknown;
|
|
60
|
+
}, label: string): Handler;
|
|
61
|
+
/**
|
|
62
|
+
* Resolves every route's own module once and runs the two checks above against it — what `rshono build` does
|
|
63
|
+
* once the bundle exists, so a route that cannot serve *any* request fails the build instead of exiting 0 and
|
|
64
|
+
* answering 500 in production for the life of the deployment.
|
|
65
|
+
*
|
|
66
|
+
* Every route is checked, not just the first that fails: these are all fixed by editing a file, and a build
|
|
67
|
+
* that names one of four broken routes costs three more builds to get through.
|
|
68
|
+
*
|
|
69
|
+
* A module that throws while *evaluating* is warned about rather than raised. Checking a module's shape means
|
|
70
|
+
* importing it, and whether an import succeeds is a question about the environment, not about the module: a
|
|
71
|
+
* page whose module scope reads a secret, opens a connection or touches a file that only exists in production
|
|
72
|
+
* fails here and works per request. Failing the build on that would make "every route module must be
|
|
73
|
+
* importable during the build" a new requirement on apps, to catch a mistake this cannot tell from a
|
|
74
|
+
* deliberate one. A `render: 'static'` route is the exception, and needs nothing here: it promised to render
|
|
75
|
+
* at build time, so the prerender pass demands the import and fails the build when it throws.
|
|
76
|
+
*/
|
|
77
|
+
export declare function assertRouteModules(config: RouteConfig): Promise<void>;
|
|
33
78
|
//# sourceMappingURL=validate-entries.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-entries.d.ts","sourceRoot":"","sources":["../../src/runtime/validate-entries.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"validate-entries.d.ts","sourceRoot":"","sources":["../../src/runtime/validate-entries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAe,KAAK,aAAa,EAAc,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAmJ7F;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,OAAO,GAAG,WAAW,CAcnE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI,CAOhE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC,aAAa,CAAC,CActG;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CASvF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD3E"}
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
* The mistakes that produced no error at all were worse: a duplicated `path` whose second entry silently
|
|
10
10
|
* never ran, a `staticPaths` on a route that renders per request.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
* prerender pass; at `rshono dev` startup; and when a deployed server boots.
|
|
12
|
+
* The route *table* is checked at module load — during `rshono build`, which imports the server bundle for
|
|
13
|
+
* the prerender pass; at `rshono dev` startup; and when a deployed server boots. The modules the table points
|
|
14
|
+
* *at* cannot be, because they load lazily: {@link assertPageModule} and {@link assertEndpointModule} run on
|
|
15
|
+
* first request, and {@link assertRouteModules} runs both against every route during `rshono build`.
|
|
14
16
|
*/
|
|
15
17
|
import { isPageRoute } from '../router.js';
|
|
16
18
|
/** Every method an endpoint route can name, `'all'` aside — which is also what `'all'` is expanded to below. */
|
|
@@ -182,4 +184,118 @@ export function validateServerApp(exported) {
|
|
|
182
184
|
}
|
|
183
185
|
return app;
|
|
184
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* The server component a page module must default-export, checked rather than cast.
|
|
189
|
+
*
|
|
190
|
+
* Two structural mistakes land here, and neither can serve *any* request, which is why they are also checked
|
|
191
|
+
* at build time by {@link assertRouteModules}:
|
|
192
|
+
*
|
|
193
|
+
* - no default export at all — a named `export function Page`, or a file that exports its layout instead;
|
|
194
|
+
* - a `'use client'` page. That module's default export is a client reference, so it carries no
|
|
195
|
+
* `entryJsFiles` — the list of scripts the document has to boot, recorded only for a *server* entry.
|
|
196
|
+
* Rendering it anyway would produce a page with no client runtime, which is worse than a message.
|
|
197
|
+
*/
|
|
198
|
+
export function assertPageModule(mod, label) {
|
|
199
|
+
const Page = mod.default;
|
|
200
|
+
if (typeof Page !== 'function') {
|
|
201
|
+
fail(`The page module for ${label} must default-export a server component.`);
|
|
202
|
+
}
|
|
203
|
+
if (!Page.entryJsFiles) {
|
|
204
|
+
fail(`The page component for ${label} is missing its client-asset info ('use server-entry'). ` +
|
|
205
|
+
"The directive is added automatically for inline `component: () => import('…')` thunks in routes.ts. " +
|
|
206
|
+
"If this page is wired up another way, put 'use server-entry' on the first line of the page module yourself — " +
|
|
207
|
+
"and make sure the page is a server component (a 'use client' page must be wrapped by a server component instead).");
|
|
208
|
+
}
|
|
209
|
+
return Page;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The Hono handler an endpoint module must export as `handler`.
|
|
213
|
+
*
|
|
214
|
+
* The page half of this fork has been checked since it existed; this half used to destructure and call, which
|
|
215
|
+
* turns the one mistake people actually make — a method-named export, the shape other frameworks ask for —
|
|
216
|
+
* into `TypeError: r is not a function` from a minified frame. That is verbatim the failure mode this file's
|
|
217
|
+
* header names as the reason it exists.
|
|
218
|
+
*/
|
|
219
|
+
export function assertEndpointModule(mod, label) {
|
|
220
|
+
const handler = mod.handler;
|
|
221
|
+
if (typeof handler !== 'function') {
|
|
222
|
+
fail(`The endpoint module for ${label} must export \`handler\` — \`export const handler: Handler = (c) => c.json({ ok: true });\`. ` +
|
|
223
|
+
'One `handler` answers every method the route declares, so a method-named export (`GET`, `POST`) is never read.');
|
|
224
|
+
}
|
|
225
|
+
return handler;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Resolves every route's own module once and runs the two checks above against it — what `rshono build` does
|
|
229
|
+
* once the bundle exists, so a route that cannot serve *any* request fails the build instead of exiting 0 and
|
|
230
|
+
* answering 500 in production for the life of the deployment.
|
|
231
|
+
*
|
|
232
|
+
* Every route is checked, not just the first that fails: these are all fixed by editing a file, and a build
|
|
233
|
+
* that names one of four broken routes costs three more builds to get through.
|
|
234
|
+
*
|
|
235
|
+
* A module that throws while *evaluating* is warned about rather than raised. Checking a module's shape means
|
|
236
|
+
* importing it, and whether an import succeeds is a question about the environment, not about the module: a
|
|
237
|
+
* page whose module scope reads a secret, opens a connection or touches a file that only exists in production
|
|
238
|
+
* fails here and works per request. Failing the build on that would make "every route module must be
|
|
239
|
+
* importable during the build" a new requirement on apps, to catch a mistake this cannot tell from a
|
|
240
|
+
* deliberate one. A `render: 'static'` route is the exception, and needs nothing here: it promised to render
|
|
241
|
+
* at build time, so the prerender pass demands the import and fails the build when it throws.
|
|
242
|
+
*/
|
|
243
|
+
export async function assertRouteModules(config) {
|
|
244
|
+
const failures = [];
|
|
245
|
+
/**
|
|
246
|
+
* Loads one route's module, or reports why it could not be checked. `null` — never a silent skip: the
|
|
247
|
+
* whole point of running at build time is saying what was and was not looked at.
|
|
248
|
+
*/
|
|
249
|
+
const load = async (thunk, label) => {
|
|
250
|
+
try {
|
|
251
|
+
return await thunk();
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
255
|
+
console.warn(` ⚠ ${label} could not be loaded at build time, so its module was not checked — ${reason}`);
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
/** Keeps a check's message instead of raising it, so every broken route is named rather than the first. */
|
|
260
|
+
const check = (assert) => {
|
|
261
|
+
try {
|
|
262
|
+
assert();
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
failures.push(error instanceof Error ? error.message : String(error));
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
for (const route of config.routes) {
|
|
269
|
+
const label = `"${route.path}"`;
|
|
270
|
+
if (isPageRoute(route)) {
|
|
271
|
+
const mod = await load(route.component, label);
|
|
272
|
+
if (mod)
|
|
273
|
+
check(() => assertPageModule(mod, label));
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
const mod = await load(route.server, label);
|
|
277
|
+
if (mod)
|
|
278
|
+
check(() => assertEndpointModule(mod, label));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
for (const [page, label] of [
|
|
282
|
+
[config.notFound, 'the notFound page'],
|
|
283
|
+
[config.error, 'the error page'],
|
|
284
|
+
]) {
|
|
285
|
+
if (!page)
|
|
286
|
+
continue;
|
|
287
|
+
const mod = await load(page.component, label);
|
|
288
|
+
if (mod)
|
|
289
|
+
check(() => assertPageModule(mod, label));
|
|
290
|
+
}
|
|
291
|
+
// One failure — overwhelmingly the common case — is raised as itself, so the message reads the way it
|
|
292
|
+
// does on the request path. Several are gathered under a count, each stripped of the prefix the whole
|
|
293
|
+
// list now carries once.
|
|
294
|
+
if (failures.length === 1)
|
|
295
|
+
throw new Error(failures[0]);
|
|
296
|
+
if (failures.length > 1) {
|
|
297
|
+
fail(`${failures.length} route modules cannot serve a request:\n` +
|
|
298
|
+
failures.map((message) => ` • ${message.replace(/^\[rshono\] /, '')}`).join('\n'));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
185
301
|
//# sourceMappingURL=validate-entries.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-entries.js","sourceRoot":"","sources":["../../src/runtime/validate-entries.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The two modules an app hands the framework — `src/routes.ts` and `src/server.ts` — checked before\n * anything is built on them.\n *\n * Both arrive through a build-time alias that carries no type the compiler can hold on to, so they used to\n * be cast and the mistake found later, somewhere else: a `routes` array that skipped `defineRoutes` surfaced\n * as `TypeError: nN is not iterable` from a minified bundle, and a `src/server.ts` exporting the wrong thing\n * as `Cannot read properties of undefined (reading 'map')` from inside Hono. Neither names the file to open.\n * The mistakes that produced no error at all were worse: a duplicated `path` whose second entry silently\n * never ran, a `staticPaths` on a route that renders per request.\n *\n * Everything here runs once, at module load — during `rshono build`, which imports the server bundle for the\n * prerender pass; at `rshono dev` startup; and when a deployed server boots.\n */\n\nimport type { Hono } from 'hono';\nimport { isPageRoute, type Route, type RouteConfig } from '../router.js';\n\n/** Every method an endpoint route can name, `'all'` aside — which is also what `'all'` is expanded to below. */\nconst CONCRETE_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options'];\nconst METHODS = new Set([...CONCRETE_METHODS, 'all']);\n\n/** What a page route's `render` accepts. */\nconst RENDER_MODES = ['static', 'dynamic'];\n\nfunction fail(message: string): never {\n throw new Error(`[rshono] ${message}`);\n}\n\n/**\n * How a route is named in a message. Always by position, because that is what a duplicate `path` needs to be\n * told apart, and by path as well wherever there is one.\n */\nfunction label(route: unknown, index: number): string {\n const path = (route as { path?: unknown } | null)?.path;\n return typeof path === 'string' ? `routes[${index}] (\"${path}\")` : `routes[${index}]`;\n}\n\n/** Refuses a key that belongs to the *other* kind of route — see {@link validateRoute}. */\nfunction refuseForeignKeys(route: Record<string, unknown>, name: string, keys: readonly string[], advice: string): void {\n for (const key of keys) {\n if (route[key] !== undefined) fail(`src/routes.ts: ${name} has \\`${key}\\`, which ${advice}`);\n }\n}\n\n/**\n * Checks one entry of the route table.\n *\n * The cross-kind key checks are the ones TypeScript cannot make: excess-property checking against a union\n * accepts any key present in *some* member, so `staticPaths` on an `{ type: 'endpoint' }` route type-checks\n * and is then ignored at runtime — which is indistinguishable from prerendering that quietly does nothing.\n */\nfunction validateRoute(route: unknown, index: number): void {\n if (route === null || typeof route !== 'object') {\n fail(`src/routes.ts: routes[${index}] is ${route === null ? 'null' : typeof route}, not a route object.`);\n }\n const entry = route as Record<string, unknown>;\n const name = label(route, index);\n\n if (typeof entry.path !== 'string' || !entry.path.startsWith('/')) {\n fail(`src/routes.ts: ${name} needs a \\`path\\` starting with \"/\".`);\n }\n if (entry.type !== undefined && entry.type !== 'endpoint') {\n fail(`src/routes.ts: ${name} has type ${JSON.stringify(entry.type)} — the only \\`type\\` is 'endpoint', and a page route omits it.`);\n }\n\n if (entry.type === 'endpoint') {\n if (typeof entry.server !== 'function') {\n fail(`src/routes.ts: ${name} is an endpoint, so it needs \\`server\\` — a function importing the module that exports \\`handler\\`.`);\n }\n if (entry.method !== undefined) validateMethod(entry.method, name);\n refuseForeignKeys(\n entry,\n name,\n ['component', 'render', 'staticPaths'],\n \"only a page route has — an endpoint's `server` handler decides its own response.\",\n );\n } else {\n if (typeof entry.component !== 'function') {\n fail(`src/routes.ts: ${name} needs \\`component\\` — a function importing the page module, e.g. \\`() => import('./pages/home')\\`.`);\n }\n if (entry.render !== undefined && (typeof entry.render !== 'string' || !RENDER_MODES.includes(entry.render))) {\n fail(`src/routes.ts: ${name} has render ${JSON.stringify(entry.render)} — it is 'static' or 'dynamic'.`);\n }\n if (entry.staticPaths !== undefined) {\n if (typeof entry.staticPaths !== 'function') fail(`src/routes.ts: ${name} has a \\`staticPaths\\` that is not a function.`);\n if (entry.render !== 'static') {\n fail(`src/routes.ts: ${name} has \\`staticPaths\\` but is not \\`render: 'static'\\`, so it renders per request and the paths are never built.`);\n }\n }\n refuseForeignKeys(entry, name, ['server', 'method'], \"only an endpoint route has — add `type: 'endpoint'` if that is what this is.\");\n }\n}\n\n/**\n * Checks an endpoint's `method`, which is one method or a list of them.\n *\n * A list is checked member by member so the message names the bad one rather than printing the array, and\n * `'all'` is refused inside one: a list meaning every method is a list its author did not mean to write.\n */\nfunction validateMethod(method: unknown, name: string): void {\n if (Array.isArray(method)) {\n if (method.length === 0) fail(`src/routes.ts: ${name} has an empty \\`method\\` list — omit it to answer every method.`);\n for (const entry of method as unknown[]) {\n if (entry === 'all') {\n fail(`src/routes.ts: ${name} has 'all' inside a \\`method\\` list — use \\`method: 'all'\\`, or list the methods it answers.`);\n }\n validateMethod(entry, name);\n }\n return;\n }\n if (typeof method !== 'string' || !METHODS.has(method)) {\n const meantHead = typeof method === 'string' && method.toLowerCase() === 'head';\n fail(\n `src/routes.ts: ${name} has method ${JSON.stringify(method)}, which is not one of ${[...METHODS].join(', ')}.` +\n (meantHead ? \" A HEAD is dispatched as a GET, so use 'get' — a route registered for HEAD alone answers neither.\" : ''),\n );\n }\n}\n\n/** The methods a route answers, with `'all'` expanded and a list flattened, so two can be compared. */\nfunction methodsOf(route: Route): readonly string[] {\n // A page is registered for GET and POST: the POST is how a `<form action={serverAction}>` reaches it.\n if (isPageRoute(route)) return ['get', 'post'];\n const methods = [route.method ?? 'all'].flat();\n return methods.includes('all') ? CONCRETE_METHODS : methods;\n}\n\n/**\n * Refuses a route the table already answers in full. Hono matches in registration order, so a later entry\n * whose every method and path is spoken for is dead code — and the build that produced it exits 0 with\n * nothing to say, which is how a duplicated `path` survives.\n *\n * Method by method, and only when *all* of them are taken: one path split between a `'get'` endpoint and a\n * `'post'` one shadows nothing, and neither does a catch-all registered behind a route that claims one\n * method of it — that one still answers the rest.\n */\nfunction assertNothingIsShadowed(routes: readonly Route[]): void {\n /** `\"<method> <path>\"` → the first route answering it. */\n const claimed = new Map<string, string>();\n routes.forEach((route, index) => {\n const name = label(route, index);\n const methods = methodsOf(route);\n const claimants = methods.map((method) => claimed.get(`${method} ${route.path}`));\n if (claimants.every((claimant) => claimant !== undefined)) {\n const by = [...new Set(claimants)].join(' and ');\n const verbs = methods.map((method) => method.toUpperCase()).join(', ');\n fail(`src/routes.ts: ${name} would never run — ${by} already answers ${verbs} ${route.path}, and Hono matches in registration order.`);\n }\n for (const method of methods) {\n const key = `${method} ${route.path}`;\n if (!claimed.has(key)) claimed.set(key, name);\n }\n });\n}\n\nfunction validateFallbackPage(page: unknown, field: 'notFound' | 'error'): void {\n if (page === undefined || page === null) return;\n if (typeof page !== 'object' || typeof (page as { component?: unknown }).component !== 'function') {\n fail(`src/routes.ts: \\`${field}\\` must be a page — \\`{ component: () => import('./pages/${field}') }\\`.`);\n }\n}\n\n/**\n * Normalises and checks what `src/routes.ts` exported as `routes`.\n *\n * A bare array is accepted alongside a {@link RouteConfig}, because the docs present both and\n * `defineRoutes` is an identity function over the array form — which makes leaving it off look equivalent\n * right up to the point the build fails somewhere unrelated.\n */\nexport function validateRoutesModule(exported: unknown): RouteConfig {\n const config = (Array.isArray(exported) ? { routes: exported as Route[] } : exported) as RouteConfig | null | undefined;\n if (config === null || config === undefined || typeof config !== 'object' || !Array.isArray(config.routes)) {\n fail(\n 'src/routes.ts must export `routes` as a route array, or as a { routes, notFound?, error? } object — ' +\n 'wrap it in `defineRoutes(…)` to have TypeScript check it for you.',\n );\n }\n\n config.routes.forEach(validateRoute);\n assertNothingIsShadowed(config.routes);\n validateFallbackPage(config.notFound, 'notFound');\n validateFallbackPage(config.error, 'error');\n return config;\n}\n\n/**\n * The Hono app `src/server.ts` default-exports, or `null` for an app that has no `src/server.ts` — the\n * fallback module the alias resolves to instead default-exports exactly that.\n *\n * Duck-typed rather than `instanceof Hono`: what the framework does with the value is `app.route('/', …)`,\n * which reads `routes` off it and mounts the handlers, so those are what have to be there.\n */\nexport function validateServerApp(exported: unknown): Hono | null {\n const app = (exported as { default?: unknown } | null)?.default;\n if (app === null || app === undefined) return null;\n if (typeof app !== 'object' || typeof (app as Hono).fetch !== 'function' || !Array.isArray((app as Hono).routes)) {\n fail('src/server.ts must `export default` a Hono app — `const server = new Hono(); … export default server;`.');\n }\n return app as Hono;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"validate-entries.js","sourceRoot":"","sources":["../../src/runtime/validate-entries.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The two modules an app hands the framework — `src/routes.ts` and `src/server.ts` — checked before\n * anything is built on them.\n *\n * Both arrive through a build-time alias that carries no type the compiler can hold on to, so they used to\n * be cast and the mistake found later, somewhere else: a `routes` array that skipped `defineRoutes` surfaced\n * as `TypeError: nN is not iterable` from a minified bundle, and a `src/server.ts` exporting the wrong thing\n * as `Cannot read properties of undefined (reading 'map')` from inside Hono. Neither names the file to open.\n * The mistakes that produced no error at all were worse: a duplicated `path` whose second entry silently\n * never ran, a `staticPaths` on a route that renders per request.\n *\n * The route *table* is checked at module load — during `rshono build`, which imports the server bundle for\n * the prerender pass; at `rshono dev` startup; and when a deployed server boots. The modules the table points\n * *at* cannot be, because they load lazily: {@link assertPageModule} and {@link assertEndpointModule} run on\n * first request, and {@link assertRouteModules} runs both against every route during `rshono build`.\n */\n\nimport type { Handler, Hono } from 'hono';\nimport type { ServerEntry } from 'react-server-dom-rspack/server';\nimport { isPageRoute, type PageComponent, type Route, type RouteConfig } from '../router.js';\n\n/** Every method an endpoint route can name, `'all'` aside — which is also what `'all'` is expanded to below. */\nconst CONCRETE_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options'];\nconst METHODS = new Set([...CONCRETE_METHODS, 'all']);\n\n/** What a page route's `render` accepts. */\nconst RENDER_MODES = ['static', 'dynamic'];\n\nfunction fail(message: string): never {\n throw new Error(`[rshono] ${message}`);\n}\n\n/**\n * How a route is named in a message. Always by position, because that is what a duplicate `path` needs to be\n * told apart, and by path as well wherever there is one.\n */\nfunction label(route: unknown, index: number): string {\n const path = (route as { path?: unknown } | null)?.path;\n return typeof path === 'string' ? `routes[${index}] (\"${path}\")` : `routes[${index}]`;\n}\n\n/** Refuses a key that belongs to the *other* kind of route — see {@link validateRoute}. */\nfunction refuseForeignKeys(route: Record<string, unknown>, name: string, keys: readonly string[], advice: string): void {\n for (const key of keys) {\n if (route[key] !== undefined) fail(`src/routes.ts: ${name} has \\`${key}\\`, which ${advice}`);\n }\n}\n\n/**\n * Checks one entry of the route table.\n *\n * The cross-kind key checks are the ones TypeScript cannot make: excess-property checking against a union\n * accepts any key present in *some* member, so `staticPaths` on an `{ type: 'endpoint' }` route type-checks\n * and is then ignored at runtime — which is indistinguishable from prerendering that quietly does nothing.\n */\nfunction validateRoute(route: unknown, index: number): void {\n if (route === null || typeof route !== 'object') {\n fail(`src/routes.ts: routes[${index}] is ${route === null ? 'null' : typeof route}, not a route object.`);\n }\n const entry = route as Record<string, unknown>;\n const name = label(route, index);\n\n if (typeof entry.path !== 'string' || !entry.path.startsWith('/')) {\n fail(`src/routes.ts: ${name} needs a \\`path\\` starting with \"/\".`);\n }\n if (entry.type !== undefined && entry.type !== 'endpoint') {\n fail(`src/routes.ts: ${name} has type ${JSON.stringify(entry.type)} — the only \\`type\\` is 'endpoint', and a page route omits it.`);\n }\n\n if (entry.type === 'endpoint') {\n if (typeof entry.server !== 'function') {\n fail(`src/routes.ts: ${name} is an endpoint, so it needs \\`server\\` — a function importing the module that exports \\`handler\\`.`);\n }\n if (entry.method !== undefined) validateMethod(entry.method, name);\n refuseForeignKeys(\n entry,\n name,\n ['component', 'render', 'staticPaths'],\n \"only a page route has — an endpoint's `server` handler decides its own response.\",\n );\n } else {\n if (typeof entry.component !== 'function') {\n fail(`src/routes.ts: ${name} needs \\`component\\` — a function importing the page module, e.g. \\`() => import('./pages/home')\\`.`);\n }\n if (entry.render !== undefined && (typeof entry.render !== 'string' || !RENDER_MODES.includes(entry.render))) {\n fail(`src/routes.ts: ${name} has render ${JSON.stringify(entry.render)} — it is 'static' or 'dynamic'.`);\n }\n if (entry.staticPaths !== undefined) {\n if (typeof entry.staticPaths !== 'function') fail(`src/routes.ts: ${name} has a \\`staticPaths\\` that is not a function.`);\n if (entry.render !== 'static') {\n fail(`src/routes.ts: ${name} has \\`staticPaths\\` but is not \\`render: 'static'\\`, so it renders per request and the paths are never built.`);\n }\n }\n refuseForeignKeys(entry, name, ['server', 'method'], \"only an endpoint route has — add `type: 'endpoint'` if that is what this is.\");\n }\n}\n\n/**\n * Checks an endpoint's `method`, which is one method or a list of them.\n *\n * A list is checked member by member so the message names the bad one rather than printing the array, and\n * `'all'` is refused inside one: a list meaning every method is a list its author did not mean to write.\n */\nfunction validateMethod(method: unknown, name: string): void {\n if (Array.isArray(method)) {\n if (method.length === 0) fail(`src/routes.ts: ${name} has an empty \\`method\\` list — omit it to answer every method.`);\n for (const entry of method as unknown[]) {\n if (entry === 'all') {\n fail(`src/routes.ts: ${name} has 'all' inside a \\`method\\` list — use \\`method: 'all'\\`, or list the methods it answers.`);\n }\n validateMethod(entry, name);\n }\n return;\n }\n if (typeof method !== 'string' || !METHODS.has(method)) {\n const meantHead = typeof method === 'string' && method.toLowerCase() === 'head';\n fail(\n `src/routes.ts: ${name} has method ${JSON.stringify(method)}, which is not one of ${[...METHODS].join(', ')}.` +\n (meantHead ? \" A HEAD is dispatched as a GET, so use 'get' — a route registered for HEAD alone answers neither.\" : ''),\n );\n }\n}\n\n/** The methods a route answers, with `'all'` expanded and a list flattened, so two can be compared. */\nfunction methodsOf(route: Route): readonly string[] {\n // A page is registered for GET and POST: the POST is how a `<form action={serverAction}>` reaches it.\n if (isPageRoute(route)) return ['get', 'post'];\n const methods = [route.method ?? 'all'].flat();\n return methods.includes('all') ? CONCRETE_METHODS : methods;\n}\n\n/**\n * Refuses a route the table already answers in full. Hono matches in registration order, so a later entry\n * whose every method and path is spoken for is dead code — and the build that produced it exits 0 with\n * nothing to say, which is how a duplicated `path` survives.\n *\n * Method by method, and only when *all* of them are taken: one path split between a `'get'` endpoint and a\n * `'post'` one shadows nothing, and neither does a catch-all registered behind a route that claims one\n * method of it — that one still answers the rest.\n */\nfunction assertNothingIsShadowed(routes: readonly Route[]): void {\n /** `\"<method> <path>\"` → the first route answering it. */\n const claimed = new Map<string, string>();\n routes.forEach((route, index) => {\n const name = label(route, index);\n const methods = methodsOf(route);\n const claimants = methods.map((method) => claimed.get(`${method} ${route.path}`));\n if (claimants.every((claimant) => claimant !== undefined)) {\n const by = [...new Set(claimants)].join(' and ');\n const verbs = methods.map((method) => method.toUpperCase()).join(', ');\n fail(`src/routes.ts: ${name} would never run — ${by} already answers ${verbs} ${route.path}, and Hono matches in registration order.`);\n }\n for (const method of methods) {\n const key = `${method} ${route.path}`;\n if (!claimed.has(key)) claimed.set(key, name);\n }\n });\n}\n\nfunction validateFallbackPage(page: unknown, field: 'notFound' | 'error'): void {\n if (page === undefined || page === null) return;\n if (typeof page !== 'object' || typeof (page as { component?: unknown }).component !== 'function') {\n fail(`src/routes.ts: \\`${field}\\` must be a page — \\`{ component: () => import('./pages/${field}') }\\`.`);\n }\n}\n\n/**\n * Normalises and checks what `src/routes.ts` exported as `routes`.\n *\n * A bare array is accepted alongside a {@link RouteConfig}, because the docs present both and\n * `defineRoutes` is an identity function over the array form — which makes leaving it off look equivalent\n * right up to the point the build fails somewhere unrelated.\n */\nexport function validateRoutesModule(exported: unknown): RouteConfig {\n const config = (Array.isArray(exported) ? { routes: exported as Route[] } : exported) as RouteConfig | null | undefined;\n if (config === null || config === undefined || typeof config !== 'object' || !Array.isArray(config.routes)) {\n fail(\n 'src/routes.ts must export `routes` as a route array, or as a { routes, notFound?, error? } object — ' +\n 'wrap it in `defineRoutes(…)` to have TypeScript check it for you.',\n );\n }\n\n config.routes.forEach(validateRoute);\n assertNothingIsShadowed(config.routes);\n validateFallbackPage(config.notFound, 'notFound');\n validateFallbackPage(config.error, 'error');\n return config;\n}\n\n/**\n * The Hono app `src/server.ts` default-exports, or `null` for an app that has no `src/server.ts` — the\n * fallback module the alias resolves to instead default-exports exactly that.\n *\n * Duck-typed rather than `instanceof Hono`: what the framework does with the value is `app.route('/', …)`,\n * which reads `routes` off it and mounts the handlers, so those are what have to be there.\n */\nexport function validateServerApp(exported: unknown): Hono | null {\n const app = (exported as { default?: unknown } | null)?.default;\n if (app === null || app === undefined) return null;\n if (typeof app !== 'object' || typeof (app as Hono).fetch !== 'function' || !Array.isArray((app as Hono).routes)) {\n fail('src/server.ts must `export default` a Hono app — `const server = new Hono(); … export default server;`.');\n }\n return app as Hono;\n}\n\n/**\n * The server component a page module must default-export, checked rather than cast.\n *\n * Two structural mistakes land here, and neither can serve *any* request, which is why they are also checked\n * at build time by {@link assertRouteModules}:\n *\n * - no default export at all — a named `export function Page`, or a file that exports its layout instead;\n * - a `'use client'` page. That module's default export is a client reference, so it carries no\n * `entryJsFiles` — the list of scripts the document has to boot, recorded only for a *server* entry.\n * Rendering it anyway would produce a page with no client runtime, which is worse than a message.\n */\nexport function assertPageModule(mod: { default?: unknown }, label: string): ServerEntry<PageComponent> {\n const Page = mod.default as ServerEntry<PageComponent> | undefined;\n if (typeof Page !== 'function') {\n fail(`The page module for ${label} must default-export a server component.`);\n }\n if (!Page.entryJsFiles) {\n fail(\n `The page component for ${label} is missing its client-asset info ('use server-entry'). ` +\n \"The directive is added automatically for inline `component: () => import('…')` thunks in routes.ts. \" +\n \"If this page is wired up another way, put 'use server-entry' on the first line of the page module yourself — \" +\n \"and make sure the page is a server component (a 'use client' page must be wrapped by a server component instead).\",\n );\n }\n return Page;\n}\n\n/**\n * The Hono handler an endpoint module must export as `handler`.\n *\n * The page half of this fork has been checked since it existed; this half used to destructure and call, which\n * turns the one mistake people actually make — a method-named export, the shape other frameworks ask for —\n * into `TypeError: r is not a function` from a minified frame. That is verbatim the failure mode this file's\n * header names as the reason it exists.\n */\nexport function assertEndpointModule(mod: { handler?: unknown }, label: string): Handler {\n const handler = mod.handler;\n if (typeof handler !== 'function') {\n fail(\n `The endpoint module for ${label} must export \\`handler\\` — \\`export const handler: Handler = (c) => c.json({ ok: true });\\`. ` +\n 'One `handler` answers every method the route declares, so a method-named export (`GET`, `POST`) is never read.',\n );\n }\n return handler as Handler;\n}\n\n/**\n * Resolves every route's own module once and runs the two checks above against it — what `rshono build` does\n * once the bundle exists, so a route that cannot serve *any* request fails the build instead of exiting 0 and\n * answering 500 in production for the life of the deployment.\n *\n * Every route is checked, not just the first that fails: these are all fixed by editing a file, and a build\n * that names one of four broken routes costs three more builds to get through.\n *\n * A module that throws while *evaluating* is warned about rather than raised. Checking a module's shape means\n * importing it, and whether an import succeeds is a question about the environment, not about the module: a\n * page whose module scope reads a secret, opens a connection or touches a file that only exists in production\n * fails here and works per request. Failing the build on that would make \"every route module must be\n * importable during the build\" a new requirement on apps, to catch a mistake this cannot tell from a\n * deliberate one. A `render: 'static'` route is the exception, and needs nothing here: it promised to render\n * at build time, so the prerender pass demands the import and fails the build when it throws.\n */\nexport async function assertRouteModules(config: RouteConfig): Promise<void> {\n const failures: string[] = [];\n\n /**\n * Loads one route's module, or reports why it could not be checked. `null` — never a silent skip: the\n * whole point of running at build time is saying what was and was not looked at.\n */\n const load = async <T>(thunk: () => Promise<T>, label: string): Promise<T | null> => {\n try {\n return await thunk();\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error);\n console.warn(` ⚠ ${label} could not be loaded at build time, so its module was not checked — ${reason}`);\n return null;\n }\n };\n\n /** Keeps a check's message instead of raising it, so every broken route is named rather than the first. */\n const check = (assert: () => void): void => {\n try {\n assert();\n } catch (error) {\n failures.push(error instanceof Error ? error.message : String(error));\n }\n };\n\n for (const route of config.routes) {\n const label = `\"${route.path}\"`;\n if (isPageRoute(route)) {\n const mod = await load(route.component, label);\n if (mod) check(() => assertPageModule(mod, label));\n } else {\n const mod = await load(route.server, label);\n if (mod) check(() => assertEndpointModule(mod, label));\n }\n }\n for (const [page, label] of [\n [config.notFound, 'the notFound page'],\n [config.error, 'the error page'],\n ] as const) {\n if (!page) continue;\n const mod = await load(page.component, label);\n if (mod) check(() => assertPageModule(mod, label));\n }\n\n // One failure — overwhelmingly the common case — is raised as itself, so the message reads the way it\n // does on the request path. Several are gathered under a count, each stripped of the prefix the whole\n // list now carries once.\n if (failures.length === 1) throw new Error(failures[0]);\n if (failures.length > 1) {\n fail(\n `${failures.length} route modules cannot serve a request:\\n` +\n failures.map((message) => ` • ${message.replace(/^\\[rshono\\] /, '')}`).join('\\n'),\n );\n }\n}\n"]}
|
|
@@ -78,11 +78,6 @@ export declare function createPageCache(maxBytes?: number): {
|
|
|
78
78
|
get(key: string): PrerenderedPage | undefined;
|
|
79
79
|
set(key: string, page: PrerenderedPage): void;
|
|
80
80
|
};
|
|
81
|
-
/**
|
|
82
|
-
* A weak `ETag` for a page body — see {@link PrerenderedPage.etag} for why weak. Web Crypto rather than
|
|
83
|
-
* `node:crypto`, so one implementation serves both a Node server and `workerd`.
|
|
84
|
-
*/
|
|
85
|
-
export declare function weakEtag(body: Uint8Array<ArrayBuffer>): Promise<string>;
|
|
86
81
|
/**
|
|
87
82
|
* Assembles a {@link PrerenderedPage} from a body just read out of the build. `storeEtag` is the validator the
|
|
88
83
|
* store supplied, where it has one: it already describes these exact bytes, so it is weakened rather than
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prerendered.d.ts","sourceRoot":"","sources":["../../src/server/prerendered.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEjD,eAAO,MAAM,QAAQ;;iBAGX,IAAI,EAAE,YAAY;iBAAE,OAAO;iBAAM,WAAW,EAAE,WAAW;;;iBACvD,IAAI,EAAE,WAAW;iBAAE,OAAO;qBAAI,GAAG,EAAE,GAAG;;iBAAI,WAAW,EAAE,kBAAkB;;CACgC,CAAC;AAEtH;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAgCjD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAyB,GAAG,MAAM,GAAG,IAAI,CAY3F;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEpD;AAKD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,QAAQ,SAAsB,GAAG;IAC/D,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAAC;IAC9C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,IAAI,CAAC;CAC/C,CA0BA;
|
|
1
|
+
{"version":3,"file":"prerendered.d.ts","sourceRoot":"","sources":["../../src/server/prerendered.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEjD,eAAO,MAAM,QAAQ;;iBAGX,IAAI,EAAE,YAAY;iBAAE,OAAO;iBAAM,WAAW,EAAE,WAAW;;;iBACvD,IAAI,EAAE,WAAW;iBAAE,OAAO;qBAAI,GAAG,EAAE,GAAG;;iBAAI,WAAW,EAAE,kBAAkB;;CACgC,CAAC;AAEtH;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAgCjD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAyB,GAAG,MAAM,GAAG,IAAI,CAY3F;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEpD;AAKD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,QAAQ,SAAsB,GAAG;IAC/D,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAAC;IAC9C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,IAAI,CAAC;CAC/C,CA0BA;AAeD;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,UAAU,CAAC,WAAW,CAAC,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,eAAe,CAAC,CAM1H;AAED,mGAAmG;AACnG,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,IAAI,EAAE,UAAU,CAAC,WAAW,CAAC,CAAC;IAC9B;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;;OAMG;IACH,IAAI,EAAE,MAAM,CAAC;CACd"}
|
|
@@ -138,7 +138,7 @@ export function createPageCache(maxBytes = DEFAULT_CACHE_BYTES) {
|
|
|
138
138
|
* A weak `ETag` for a page body — see {@link PrerenderedPage.etag} for why weak. Web Crypto rather than
|
|
139
139
|
* `node:crypto`, so one implementation serves both a Node server and `workerd`.
|
|
140
140
|
*/
|
|
141
|
-
|
|
141
|
+
async function weakEtag(body) {
|
|
142
142
|
const digest = await crypto.subtle.digest('SHA-256', body);
|
|
143
143
|
const base64url = btoa(String.fromCharCode(...new Uint8Array(digest)))
|
|
144
144
|
.replace(/\+/g, '-')
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prerendered.js","sourceRoot":"","sources":["../../src/server/prerendered.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Everything about a prerendered page that doesn't need a filesystem: where the build puts it, and what it\n * looks like once read back.\n *\n * Apart from `ssg.ts`, which does the reading and writing through `node:fs`, because a deploy target without a\n * filesystem reads the same layout out of an asset store — and importing it from there would drag `node:fs`\n * into a bundle that has neither.\n */\n\n/**\n * The two representations of a page, prerendered side by side: a hard load wants the HTML document, a soft\n * navigation asks the same URL for a flight payload. Without both, every in-app click would re-render a page\n * that was already built.\n */\nexport type PrerenderVariant = 'html' | 'flight';\n\nexport const VARIANTS = {\n // `headers` is what the build asks each representation for, so it stays the same request the browser makes:\n // a document is the default and a flight payload is the `RSC` header. See `runtime/request.ts`.\n html: { file: 'index.html', headers: {}, contentType: 'text/html' },\n flight: { file: 'index.rsc', headers: { RSC: '1' }, contentType: 'text/x-component' },\n} as const satisfies Record<PrerenderVariant, { file: string; headers: Record<string, string>; contentType: string }>;\n\n/**\n * The index the build leaves beside the pages, naming every file it wrote — one `files` array of\n * {@link ssgFilePath} names.\n *\n * It exists so a reader can tell \"this store has no page for that path\" without asking the store, which is\n * the difference between a `Map` lookup and a failed `readFile` (or, without a filesystem, a failed store\n * fetch) on **every** request to a static route the build could not prerender — misses are deliberately not\n * cached, so that one never warms up. Absent is not an error: a build from an older rshono has none, and a\n * reader that finds none looks the way it always did.\n */\nexport const SSG_MANIFEST_FILE = 'manifest.json';\n\n/**\n * What a decoded path segment may not hold if it is to be one portable file name. `\\ / : * ? \" < > |` are\n * refused by Windows and `/` by every host, so a `staticPaths` value containing one fails the build rather\n * than writing a page on one machine and not on another — and a route pattern, whose params and wildcards are\n * spelled with `:` and `*`, falls out of the same rule.\n */\nconst UNPORTABLE_SEGMENT = /[\\\\/:*?\"<>|]/;\n\n/**\n * Decodes one path segment the way a URL does, treating a malformed escape as the literal text it is:\n * `decodeURIComponent('%')` throws, and a request path is whatever the client chose to send.\n */\nfunction decodeSegment(segment: string): string {\n try {\n return decodeURIComponent(segment);\n } catch {\n return segment;\n }\n}\n\n/**\n * Whether one decoded segment can be a directory in the prerender tree: not empty, not a relative-path\n * marker, and made only of characters a file name can hold — {@link UNPORTABLE_SEGMENT}, plus the control\n * characters, which a percent-escape is free to carry into a path and no filesystem wants in a name.\n */\nfunction isStorableSegment(segment: string): boolean {\n if (segment === '' || segment === '.' || segment === '..') return false;\n return !UNPORTABLE_SEGMENT.test(segment) && ![...segment].some((char) => char < ' ');\n}\n\n/**\n * Where a path's prerendered output lives, relative to the output root — or `null` for a path no single file\n * can answer.\n *\n * **The one canonical form**, shared by the build that writes the file and by every deploy target that reads\n * it back, because the two drifting apart is invisible: the build reports the page as prerendered and every\n * request afterwards falls through to SSR, forever. Each segment is stored *decoded*, so `/docs/café` is\n * `docs/café` whether the caller holds the path percent-encoded — the build, which interpolates `staticPaths`\n * values into a URL — or already decoded, as Hono's `c.req.path` is: it runs `decodeURI` over any path\n * containing a `%`. Decoded is also the form every other static file server addresses, so the tree stays\n * servable by something that is not this framework.\n *\n * Always `/`-separated: the same string addresses a file on a filesystem, which accepts forward slashes on\n * Windows too, and a key in an asset store, where a backslash would be the wrong character.\n *\n * `null` is everything that is not one file: a `.` or `..` segment — so traversal is a miss rather than a\n * lookup, which a store addressed by key, with no `resolve()` to fall back on, depends on — an empty segment,\n * and any segment holding a character a file name cannot.\n */\nexport function ssgFilePath(path: string, variant: PrerenderVariant = 'html'): string | null {\n const file = VARIANTS[variant].file;\n const trimmed = path.replace(/^\\/+|\\/+$/g, '');\n if (trimmed === '') return file;\n\n const segments: string[] = [];\n for (const encoded of trimmed.split('/')) {\n const segment = decodeSegment(encoded);\n if (!isStorableSegment(segment)) return null;\n segments.push(segment);\n }\n return `${segments.join('/')}/${file}`;\n}\n\n/**\n * {@link ssgFilePath} as a URL path, for a store addressed by one rather than by key. Every segment is escaped\n * on the way out and the store decodes it back, so a page reached through a URL lands on the same file a\n * filesystem target opens by name.\n */\nexport function ssgAssetPath(relPath: string): string {\n return relPath.split('/').map(encodeURIComponent).join('/');\n}\n\n/** The default cache budget: enough for a large documentation site's working set, small enough to be ignorable. */\nconst DEFAULT_CACHE_BYTES = 32 * 1024 * 1024;\n\n/**\n * A bounded, insertion-ordered cache of prerendered pages, so a site with thousands of them keeps a working set\n * rather than the whole build in memory. Only *hits* are stored — caching misses would let anyone mint entries\n * by requesting paths that don't exist — and the files never change while the server is up.\n *\n * Bounded by **bytes**, not by entry count: a count says nothing about how much memory it stands for, and a\n * page can be any size. At 128 entries — the previous limit — a documentation site with half-megabyte pages\n * retained ~64 MB across the two variants, with nothing about the number to suggest it.\n *\n * A page larger than the whole budget is served and not stored, rather than evicting everything to hold one.\n */\nexport function createPageCache(maxBytes = DEFAULT_CACHE_BYTES): {\n get(key: string): PrerenderedPage | undefined;\n set(key: string, page: PrerenderedPage): void;\n} {\n const pages = new Map<string, PrerenderedPage>();\n let bytes = 0;\n return {\n get: (key) => pages.get(key),\n set(key, page) {\n const size = page.body.byteLength;\n if (size > maxBytes) return;\n // Deleted before it is set, so re-setting an existing key moves it to the *back*: `Map.set` on a key it\n // already holds keeps that key's original position, which would leave the entry just stored sitting at\n // the front as the next thing evicted — a store whose eviction could drop the write that caused it.\n const replaced = pages.get(key);\n if (replaced) {\n bytes -= replaced.body.byteLength;\n pages.delete(key);\n }\n pages.set(key, page);\n bytes += size;\n // Insertion-ordered, so the first key is the oldest.\n while (bytes > maxBytes) {\n const oldest = pages.keys().next().value!;\n bytes -= pages.get(oldest)!.body.byteLength;\n pages.delete(oldest);\n }\n },\n };\n}\n\n/**\n * A weak `ETag` for a page body — see {@link PrerenderedPage.etag} for why weak. Web Crypto rather than\n * `node:crypto`, so one implementation serves both a Node server and `workerd`.\n */\nexport async function weakEtag(body: Uint8Array<ArrayBuffer>): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', body);\n const base64url = btoa(String.fromCharCode(...new Uint8Array(digest)))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n return `W/\"${base64url.slice(0, 22)}\"`;\n}\n\n/**\n * Assembles a {@link PrerenderedPage} from a body just read out of the build. `storeEtag` is the validator the\n * store supplied, where it has one: it already describes these exact bytes, so it is weakened rather than\n * replaced by a fresh hash.\n */\nexport async function toPrerenderedPage(body: Uint8Array<ArrayBuffer>, storeEtag?: string | null): Promise<PrerenderedPage> {\n return {\n body,\n contentLength: String(body.byteLength),\n etag: storeEtag ? storeEtag.replace(/^(?!W\\/)/, 'W/') : await weakEtag(body),\n };\n}\n\n/** A prerendered page, ready to serve: its body and a validator derived from those exact bytes. */\nexport interface PrerenderedPage {\n /**\n * The document or the flight payload, depending on which {@link PrerenderVariant} was read. Bytes rather\n * than a string, because this cache entry is served verbatim to every request that hits it — as a string,\n * each would pay a fresh UTF-8 encode of the whole page.\n */\n body: Uint8Array<ArrayBuffer>;\n /**\n * `Content-Length` for {@link body}, in bytes. Served explicitly because Hono sets no length for an\n * in-memory body, and a proxy in front is entitled to make decisions with it.\n */\n contentLength: string;\n /**\n * `ETag` for the page, so a revalidating client can be answered with a 304 instead of the body.\n *\n * Deliberately **weak**: the bytes on the wire depend on whether something in front re-encoded them, so a\n * strong validator would differ per coding and a cache would treat the 200 and the 304 that revalidates it\n * as different pages. A weak tag says \"the same representation\", which stays true across codings.\n */\n etag: string;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"prerendered.js","sourceRoot":"","sources":["../../src/server/prerendered.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Everything about a prerendered page that doesn't need a filesystem: where the build puts it, and what it\n * looks like once read back.\n *\n * Apart from `ssg.ts`, which does the reading and writing through `node:fs`, because a deploy target without a\n * filesystem reads the same layout out of an asset store — and importing it from there would drag `node:fs`\n * into a bundle that has neither.\n */\n\n/**\n * The two representations of a page, prerendered side by side: a hard load wants the HTML document, a soft\n * navigation asks the same URL for a flight payload. Without both, every in-app click would re-render a page\n * that was already built.\n */\nexport type PrerenderVariant = 'html' | 'flight';\n\nexport const VARIANTS = {\n // `headers` is what the build asks each representation for, so it stays the same request the browser makes:\n // a document is the default and a flight payload is the `RSC` header. See `runtime/request.ts`.\n html: { file: 'index.html', headers: {}, contentType: 'text/html' },\n flight: { file: 'index.rsc', headers: { RSC: '1' }, contentType: 'text/x-component' },\n} as const satisfies Record<PrerenderVariant, { file: string; headers: Record<string, string>; contentType: string }>;\n\n/**\n * The index the build leaves beside the pages, naming every file it wrote — one `files` array of\n * {@link ssgFilePath} names.\n *\n * It exists so a reader can tell \"this store has no page for that path\" without asking the store, which is\n * the difference between a `Map` lookup and a failed `readFile` (or, without a filesystem, a failed store\n * fetch) on **every** request to a static route the build could not prerender — misses are deliberately not\n * cached, so that one never warms up. Absent is not an error: a build from an older rshono has none, and a\n * reader that finds none looks the way it always did.\n */\nexport const SSG_MANIFEST_FILE = 'manifest.json';\n\n/**\n * What a decoded path segment may not hold if it is to be one portable file name. `\\ / : * ? \" < > |` are\n * refused by Windows and `/` by every host, so a `staticPaths` value containing one fails the build rather\n * than writing a page on one machine and not on another — and a route pattern, whose params and wildcards are\n * spelled with `:` and `*`, falls out of the same rule.\n */\nconst UNPORTABLE_SEGMENT = /[\\\\/:*?\"<>|]/;\n\n/**\n * Decodes one path segment the way a URL does, treating a malformed escape as the literal text it is:\n * `decodeURIComponent('%')` throws, and a request path is whatever the client chose to send.\n */\nfunction decodeSegment(segment: string): string {\n try {\n return decodeURIComponent(segment);\n } catch {\n return segment;\n }\n}\n\n/**\n * Whether one decoded segment can be a directory in the prerender tree: not empty, not a relative-path\n * marker, and made only of characters a file name can hold — {@link UNPORTABLE_SEGMENT}, plus the control\n * characters, which a percent-escape is free to carry into a path and no filesystem wants in a name.\n */\nfunction isStorableSegment(segment: string): boolean {\n if (segment === '' || segment === '.' || segment === '..') return false;\n return !UNPORTABLE_SEGMENT.test(segment) && ![...segment].some((char) => char < ' ');\n}\n\n/**\n * Where a path's prerendered output lives, relative to the output root — or `null` for a path no single file\n * can answer.\n *\n * **The one canonical form**, shared by the build that writes the file and by every deploy target that reads\n * it back, because the two drifting apart is invisible: the build reports the page as prerendered and every\n * request afterwards falls through to SSR, forever. Each segment is stored *decoded*, so `/docs/café` is\n * `docs/café` whether the caller holds the path percent-encoded — the build, which interpolates `staticPaths`\n * values into a URL — or already decoded, as Hono's `c.req.path` is: it runs `decodeURI` over any path\n * containing a `%`. Decoded is also the form every other static file server addresses, so the tree stays\n * servable by something that is not this framework.\n *\n * Always `/`-separated: the same string addresses a file on a filesystem, which accepts forward slashes on\n * Windows too, and a key in an asset store, where a backslash would be the wrong character.\n *\n * `null` is everything that is not one file: a `.` or `..` segment — so traversal is a miss rather than a\n * lookup, which a store addressed by key, with no `resolve()` to fall back on, depends on — an empty segment,\n * and any segment holding a character a file name cannot.\n */\nexport function ssgFilePath(path: string, variant: PrerenderVariant = 'html'): string | null {\n const file = VARIANTS[variant].file;\n const trimmed = path.replace(/^\\/+|\\/+$/g, '');\n if (trimmed === '') return file;\n\n const segments: string[] = [];\n for (const encoded of trimmed.split('/')) {\n const segment = decodeSegment(encoded);\n if (!isStorableSegment(segment)) return null;\n segments.push(segment);\n }\n return `${segments.join('/')}/${file}`;\n}\n\n/**\n * {@link ssgFilePath} as a URL path, for a store addressed by one rather than by key. Every segment is escaped\n * on the way out and the store decodes it back, so a page reached through a URL lands on the same file a\n * filesystem target opens by name.\n */\nexport function ssgAssetPath(relPath: string): string {\n return relPath.split('/').map(encodeURIComponent).join('/');\n}\n\n/** The default cache budget: enough for a large documentation site's working set, small enough to be ignorable. */\nconst DEFAULT_CACHE_BYTES = 32 * 1024 * 1024;\n\n/**\n * A bounded, insertion-ordered cache of prerendered pages, so a site with thousands of them keeps a working set\n * rather than the whole build in memory. Only *hits* are stored — caching misses would let anyone mint entries\n * by requesting paths that don't exist — and the files never change while the server is up.\n *\n * Bounded by **bytes**, not by entry count: a count says nothing about how much memory it stands for, and a\n * page can be any size. At 128 entries — the previous limit — a documentation site with half-megabyte pages\n * retained ~64 MB across the two variants, with nothing about the number to suggest it.\n *\n * A page larger than the whole budget is served and not stored, rather than evicting everything to hold one.\n */\nexport function createPageCache(maxBytes = DEFAULT_CACHE_BYTES): {\n get(key: string): PrerenderedPage | undefined;\n set(key: string, page: PrerenderedPage): void;\n} {\n const pages = new Map<string, PrerenderedPage>();\n let bytes = 0;\n return {\n get: (key) => pages.get(key),\n set(key, page) {\n const size = page.body.byteLength;\n if (size > maxBytes) return;\n // Deleted before it is set, so re-setting an existing key moves it to the *back*: `Map.set` on a key it\n // already holds keeps that key's original position, which would leave the entry just stored sitting at\n // the front as the next thing evicted — a store whose eviction could drop the write that caused it.\n const replaced = pages.get(key);\n if (replaced) {\n bytes -= replaced.body.byteLength;\n pages.delete(key);\n }\n pages.set(key, page);\n bytes += size;\n // Insertion-ordered, so the first key is the oldest.\n while (bytes > maxBytes) {\n const oldest = pages.keys().next().value!;\n bytes -= pages.get(oldest)!.body.byteLength;\n pages.delete(oldest);\n }\n },\n };\n}\n\n/**\n * A weak `ETag` for a page body — see {@link PrerenderedPage.etag} for why weak. Web Crypto rather than\n * `node:crypto`, so one implementation serves both a Node server and `workerd`.\n */\nasync function weakEtag(body: Uint8Array<ArrayBuffer>): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', body);\n const base64url = btoa(String.fromCharCode(...new Uint8Array(digest)))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n return `W/\"${base64url.slice(0, 22)}\"`;\n}\n\n/**\n * Assembles a {@link PrerenderedPage} from a body just read out of the build. `storeEtag` is the validator the\n * store supplied, where it has one: it already describes these exact bytes, so it is weakened rather than\n * replaced by a fresh hash.\n */\nexport async function toPrerenderedPage(body: Uint8Array<ArrayBuffer>, storeEtag?: string | null): Promise<PrerenderedPage> {\n return {\n body,\n contentLength: String(body.byteLength),\n etag: storeEtag ? storeEtag.replace(/^(?!W\\/)/, 'W/') : await weakEtag(body),\n };\n}\n\n/** A prerendered page, ready to serve: its body and a validator derived from those exact bytes. */\nexport interface PrerenderedPage {\n /**\n * The document or the flight payload, depending on which {@link PrerenderVariant} was read. Bytes rather\n * than a string, because this cache entry is served verbatim to every request that hits it — as a string,\n * each would pay a fresh UTF-8 encode of the whole page.\n */\n body: Uint8Array<ArrayBuffer>;\n /**\n * `Content-Length` for {@link body}, in bytes. Served explicitly because Hono sets no length for an\n * in-memory body, and a proxy in front is entitled to make decisions with it.\n */\n contentLength: string;\n /**\n * `ETag` for the page, so a revalidating client can be answered with a 304 instead of the body.\n *\n * Deliberately **weak**: the bytes on the wire depend on whether something in front re-encoded them, so a\n * strong validator would differ per coding and a cache would treat the 200 and the 304 that revalidates it\n * as different pages. A weak tag says \"the same representation\", which stays true across codings.\n */\n etag: string;\n}\n"]}
|
|
@@ -18,6 +18,12 @@ export interface ServerConfig {
|
|
|
18
18
|
* compiler knows which directory it used.
|
|
19
19
|
*/
|
|
20
20
|
outDir: string;
|
|
21
|
+
/**
|
|
22
|
+
* Whether this target's platform supplies per-request bindings as the second argument to
|
|
23
|
+
* `app.fetch(request, env)`. Baked in because only the build knows which preset was selected, and
|
|
24
|
+
* `getRequestContext().env` has to know: see `DeployPreset.envBindings`, which decides it.
|
|
25
|
+
*/
|
|
26
|
+
envBindings: boolean;
|
|
21
27
|
}
|
|
22
28
|
/**
|
|
23
29
|
* The framework's built-in defaults. `port` and `host` are not config fields — they come from `PORT` / `HOST`
|
|
@@ -44,8 +50,9 @@ export declare function parsePort(value: string | undefined, source: string): nu
|
|
|
44
50
|
* rather than a config field because it decides one thing the user should not have to: `trustProxy` is forced on
|
|
45
51
|
* under `rshono dev`, where the framework's own localhost proxy is the only way in.
|
|
46
52
|
*/
|
|
47
|
-
export declare function resolveServerConfig(config: RshonoConfig, { isDev, outDir }: {
|
|
53
|
+
export declare function resolveServerConfig(config: RshonoConfig, { isDev, outDir, envBindings }: {
|
|
48
54
|
isDev: boolean;
|
|
49
55
|
outDir: string;
|
|
56
|
+
envBindings?: boolean;
|
|
50
57
|
}): ServerConfig;
|
|
51
58
|
//# sourceMappingURL=server-config.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/server/server-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,KAAK,EAAE,OAAO,CAAC;IACf,oGAAoG;IACpG,UAAU,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/server/server-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,KAAK,EAAE,OAAO,CAAC;IACf,oGAAoG;IACpG,UAAU,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,WAAW,EAAE,OAAO,CAAC;CACtB;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe;aAC1B,IAAI,EAAE,IAAI;aACV,IAAI,EAAE,SAAS;CACP,CAAC;AAKX;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUvF;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,YAAY,EACpB,EAAE,KAAK,EAAE,MAAM,EAAE,WAAmB,EAAE,EAAE;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAChG,YAAY,CAOd"}
|
|
@@ -36,11 +36,12 @@ export function parsePort(value, source) {
|
|
|
36
36
|
* rather than a config field because it decides one thing the user should not have to: `trustProxy` is forced on
|
|
37
37
|
* under `rshono dev`, where the framework's own localhost proxy is the only way in.
|
|
38
38
|
*/
|
|
39
|
-
export function resolveServerConfig(config, { isDev, outDir }) {
|
|
39
|
+
export function resolveServerConfig(config, { isDev, outDir, envBindings = false }) {
|
|
40
40
|
return {
|
|
41
41
|
isDev,
|
|
42
42
|
trustProxy: isDev || (config.trustProxy ?? false),
|
|
43
43
|
outDir,
|
|
44
|
+
envBindings,
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
47
|
//# sourceMappingURL=server-config.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/server/server-config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/server/server-config.ts"],"names":[],"mappings":"AA6BA;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,SAAS;CACP,CAAC;AAEX,8FAA8F;AAC9F,MAAM,QAAQ,GAAG,KAAK,CAAC;AAEvB;;;;;;;;;;GAUG;AACH,MAAM,UAAU,SAAS,CAAC,KAAyB,EAAE,MAAc;IACjE,MAAM,IAAI,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC;IAC3B,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,qGAAqG;IACrG,yCAAyC;IACzC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;IAC5D,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,QAAQ,EAAE,CAAC;QAC1C,MAAM,IAAI,UAAU,CAAC,WAAW,MAAM,IAAI,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,wCAAwC,QAAQ,GAAG,CAAC,CAAC;IACtH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CACjC,MAAoB,EACpB,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,GAAG,KAAK,EAA6D;IAEjG,OAAO;QACL,KAAK;QACL,UAAU,EAAE,KAAK,IAAI,CAAC,MAAM,CAAC,UAAU,IAAI,KAAK,CAAC;QACjD,MAAM;QACN,WAAW;KACZ,CAAC;AACJ,CAAC","sourcesContent":["import type { RshonoConfig } from '../config.js';\n\n/**\n * The framework settings the server bundle needs at request time, fully resolved — produced once by\n * {@link resolveServerConfig} and compiled in as the `__RSHONO_CONFIG__` literal. There is no runtime env-var\n * interface for these, and nothing here is a per-request concern: those are Hono middleware in `src/server.ts`.\n */\nexport interface ServerConfig {\n /**\n * `true` when the bundle came from `rshono dev`. Baked in rather than read from `process.env.NODE_ENV`,\n * because a deploy target need not have a `process`.\n */\n isDev: boolean;\n /** Honour `X-Forwarded-Host` / `-Proto` when resolving the browser-facing URL. Forced on in dev. */\n trustProxy: boolean;\n /**\n * The output directory this bundle was written to, relative to the project root. Carried into the bundle\n * because `deploy/filesystem.ts` locates the assets and prerendered pages beside it at runtime, and only the\n * compiler knows which directory it used.\n */\n outDir: string;\n /**\n * Whether this target's platform supplies per-request bindings as the second argument to\n * `app.fetch(request, env)`. Baked in because only the build knows which preset was selected, and\n * `getRequestContext().env` has to know: see `DeployPreset.envBindings`, which decides it.\n */\n envBindings: boolean;\n}\n\n/**\n * The framework's built-in defaults. `port` and `host` are not config fields — they come from `PORT` / `HOST`\n * or `--port` wherever the address is resolved, and this is what those fall back to.\n */\nexport const SERVER_DEFAULTS = {\n port: 3000,\n host: '0.0.0.0',\n} as const;\n\n/** The highest port a TCP listener can bind. `0` is the lowest, and means \"any free port\". */\nconst MAX_PORT = 65535;\n\n/**\n * Reads a port out of `--port` or `PORT`, so both are read the same way wherever the address is resolved.\n *\n * A blank value is `undefined` — \"unset\", falling through to {@link SERVER_DEFAULTS}. An empty `PORT` is\n * common in CI images and container templates, and the other reading of it is a silent failure: `Number('')`\n * is `0`, which binds a random free port and then reports success.\n *\n * @param source how to name the value in the error — the flag or the variable it came from.\n * @throws RangeError for anything present that is not a port, rather than letting `NaN` reach Node as a raw\n * `ERR_SOCKET_BAD_PORT` with a bundler frame in the stack.\n */\nexport function parsePort(value: string | undefined, source: string): number | undefined {\n const text = value?.trim();\n if (!text) return undefined;\n // Digits only: `Number` alone would also accept `0x50`, `1e3`, `+80` and `3.0`, none of which anyone\n // typed meaning the port they would get.\n const port = /^\\d+$/.test(text) ? Number(text) : Number.NaN;\n if (Number.isNaN(port) || port > MAX_PORT) {\n throw new RangeError(`invalid ${source} ${JSON.stringify(value)} — expected an integer between 0 and ${MAX_PORT}.`);\n }\n return port;\n}\n\n/**\n * Resolves the user's config into the {@link ServerConfig} baked into the bundle. `isDev` is a build-time input\n * rather than a config field because it decides one thing the user should not have to: `trustProxy` is forced on\n * under `rshono dev`, where the framework's own localhost proxy is the only way in.\n */\nexport function resolveServerConfig(\n config: RshonoConfig,\n { isDev, outDir, envBindings = false }: { isDev: boolean; outDir: string; envBindings?: boolean },\n): ServerConfig {\n return {\n isDev,\n trustProxy: isDev || (config.trustProxy ?? false),\n outDir,\n envBindings,\n };\n}\n"]}
|
package/dist/server/ssg.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssg.d.ts","sourceRoot":"","sources":["../../src/server/ssg.ts"],"names":[],"mappings":"AAGA,OAAO,EAA+B,KAAK,KAAK,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAML,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACtB,MAAM,kBAAkB,CAAC;AAQ1B;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAUrE;AAsDD,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAyB,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAgC9I;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IACzB,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,oGAAoG;IACpG,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;
|
|
1
|
+
{"version":3,"file":"ssg.d.ts","sourceRoot":"","sources":["../../src/server/ssg.ts"],"names":[],"mappings":"AAGA,OAAO,EAA+B,KAAK,KAAK,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAML,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACtB,MAAM,kBAAkB,CAAC;AAQ1B;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAUrE;AAsDD,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAyB,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAgC9I;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IACzB,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,oGAAoG;IACpG,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAiED,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CA4I/F"}
|
package/dist/server/ssg.js
CHANGED
|
@@ -143,9 +143,9 @@ async function mapBounded(items, limit, run) {
|
|
|
143
143
|
async function renderVariant(fetch, url, variant) {
|
|
144
144
|
const response = await fetch(new Request(url, { headers: VARIANTS[variant].headers }));
|
|
145
145
|
if (response.status !== 200)
|
|
146
|
-
return { ok: false, reason: `${response.status}
|
|
146
|
+
return { ok: false, reason: `${response.status}`, failed: response.status >= 500 };
|
|
147
147
|
if (!(response.headers.get('Content-Type') ?? '').includes(VARIANTS[variant].contentType)) {
|
|
148
|
-
return { ok: false, reason: `a non-${VARIANTS[variant].contentType} response
|
|
148
|
+
return { ok: false, reason: `a non-${VARIANTS[variant].contentType} response`, failed: false };
|
|
149
149
|
}
|
|
150
150
|
return { ok: true, body: await response.text() };
|
|
151
151
|
}
|
|
@@ -204,7 +204,7 @@ export async function prerenderStaticRoutes(options) {
|
|
|
204
204
|
// second reason too: which of several bad paths fails the build should not depend on render order.
|
|
205
205
|
const relPath = ssgFilePath(path, 'html');
|
|
206
206
|
if (relPath === null) {
|
|
207
|
-
throw new Error(`Cannot prerender "${path}" for route "${route.path}": a path segment is "." or "..", or holds a ` +
|
|
207
|
+
throw new Error(`[rshono] Cannot prerender "${path}" for route "${route.path}": a path segment is "." or "..", or holds a ` +
|
|
208
208
|
`character a portable file name cannot — one of \\ / : * ? " < > | or a control character.`);
|
|
209
209
|
}
|
|
210
210
|
targets.push({ path, relPath });
|
|
@@ -218,8 +218,10 @@ export async function prerenderStaticRoutes(options) {
|
|
|
218
218
|
const warnings = [];
|
|
219
219
|
const document = await renderVariant(fetch, origin + path, 'html');
|
|
220
220
|
if (!document.ok) {
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
// A 5xx fails the build instead of being warned about — see {@link RenderedVariant}.
|
|
222
|
+
if (!document.failed)
|
|
223
|
+
warnings.push(` ⚠ "${path}" rendered ${document.reason} at build time — skipping, will SSR per request.`);
|
|
224
|
+
return { path, ok: false, files: [], warnings, failure: document.failed ? document.reason : undefined };
|
|
223
225
|
}
|
|
224
226
|
// Both representations of a page share its directory and differ only in the file name.
|
|
225
227
|
const pageDir = join(ssgDir, dirname(relPath));
|
|
@@ -239,15 +241,21 @@ export async function prerenderStaticRoutes(options) {
|
|
|
239
241
|
write('flight', flight.body);
|
|
240
242
|
}
|
|
241
243
|
else {
|
|
242
|
-
|
|
244
|
+
// Not fatal the way a failed *document* is, even for a 5xx: the page is on disk and serves, and only
|
|
245
|
+
// soft navigation to it degrades to rendering per request. The reason is named because the two cases
|
|
246
|
+
// want different things done about them.
|
|
247
|
+
warnings.push(` ⚠ "${path}" produced no flight payload (${flight.reason}) — soft navigations to it will render per request.`);
|
|
243
248
|
}
|
|
244
|
-
return { path, ok: true, files: wrote, warnings };
|
|
249
|
+
return { path, ok: true, files: wrote, warnings, failure: undefined };
|
|
245
250
|
});
|
|
246
251
|
// Folded back in target order rather than completion order, so a concurrent pass writes the same manifest
|
|
247
252
|
// and prints the same log a serial one did.
|
|
253
|
+
const failures = [];
|
|
248
254
|
for (const outcome of outcomes) {
|
|
249
255
|
for (const warning of outcome.warnings)
|
|
250
256
|
console.warn(warning);
|
|
257
|
+
if (outcome.failure !== undefined)
|
|
258
|
+
failures.push(`"${outcome.path}" rendered ${outcome.failure}`);
|
|
251
259
|
if (!outcome.ok) {
|
|
252
260
|
skipped.push(outcome.path);
|
|
253
261
|
continue;
|
|
@@ -255,6 +263,15 @@ export async function prerenderStaticRoutes(options) {
|
|
|
255
263
|
files.push(...outcome.files);
|
|
256
264
|
written.push(outcome.path);
|
|
257
265
|
}
|
|
266
|
+
// Raised together, and in target order, so which of several failing pages is reported does not depend on
|
|
267
|
+
// which render finished first. The render's own error is already in the build log above: the framework's
|
|
268
|
+
// `onError` reported it when it turned the throw into the 500 counted here.
|
|
269
|
+
if (failures.length > 0) {
|
|
270
|
+
throw new Error(`[rshono] ${failures.length} page${failures.length === 1 ? '' : 's'} failed to render while prerendering:\n` +
|
|
271
|
+
failures.map((failure) => ` • ${failure}`).join('\n') +
|
|
272
|
+
'\n Prerendering renders a page the way a request does, so this is not a build-only failure — the route' +
|
|
273
|
+
'\n would answer the same status per request. The error it threw is logged above.');
|
|
274
|
+
}
|
|
258
275
|
// Only where the app has static routes at all: an empty manifest would be a store, and a build with
|
|
259
276
|
// nothing to prerender should leave no `ssg/` directory for a preset to carry around.
|
|
260
277
|
if (staticRoutes.length > 0)
|