@rshono/core 1.0.0-rc.2 → 1.0.0-rc.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +348 -158
  3. package/bin/rshono.mjs +3 -4
  4. package/dist/builder/env-shadow-loader.cjs +123 -8
  5. package/dist/builder/page-entry-loader.cjs +28 -1
  6. package/dist/builder/page-files.d.ts +0 -1
  7. package/dist/builder/page-files.js +7 -3
  8. package/dist/builder/page-files.js.map +1 -1
  9. package/dist/builder/public-env.d.ts +7 -1
  10. package/dist/builder/public-env.js +7 -0
  11. package/dist/builder/public-env.js.map +1 -1
  12. package/dist/builder/react-versions.d.ts +12 -0
  13. package/dist/builder/react-versions.js +74 -0
  14. package/dist/builder/react-versions.js.map +1 -0
  15. package/dist/builder/rspack-config.d.ts +13 -4
  16. package/dist/builder/rspack-config.js +136 -53
  17. package/dist/builder/rspack-config.js.map +1 -1
  18. package/dist/builder/server-only-imports.d.ts +32 -0
  19. package/dist/builder/server-only-imports.js +51 -0
  20. package/dist/builder/server-only-imports.js.map +1 -0
  21. package/dist/cli/build.d.ts +2 -3
  22. package/dist/cli/build.js +51 -13
  23. package/dist/cli/build.js.map +1 -1
  24. package/dist/cli/dev.d.ts +2 -3
  25. package/dist/cli/dev.js +149 -45
  26. package/dist/cli/dev.js.map +1 -1
  27. package/dist/cli/exit.d.ts +12 -0
  28. package/dist/cli/exit.js +16 -0
  29. package/dist/cli/exit.js.map +1 -0
  30. package/dist/cli/index.d.ts +0 -1
  31. package/dist/cli/index.js +91 -30
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/cli/start.d.ts +0 -1
  34. package/dist/cli/start.js +15 -18
  35. package/dist/cli/start.js.map +1 -1
  36. package/dist/config.d.ts +72 -78
  37. package/dist/config.js +17 -1
  38. package/dist/config.js.map +1 -1
  39. package/dist/deploy/aws-lambda/runtime.d.ts +4 -7
  40. package/dist/deploy/aws-lambda/runtime.js +5 -8
  41. package/dist/deploy/aws-lambda/runtime.js.map +1 -1
  42. package/dist/deploy/build-marker.d.ts +9 -6
  43. package/dist/deploy/build-marker.js +8 -4
  44. package/dist/deploy/build-marker.js.map +1 -1
  45. package/dist/deploy/cloudflare/build.d.ts +0 -1
  46. package/dist/deploy/cloudflare/build.js +17 -13
  47. package/dist/deploy/cloudflare/build.js.map +1 -1
  48. package/dist/deploy/cloudflare/runtime.d.ts +2 -6
  49. package/dist/deploy/cloudflare/runtime.js +73 -68
  50. package/dist/deploy/cloudflare/runtime.js.map +1 -1
  51. package/dist/deploy/contract.d.ts +57 -41
  52. package/dist/deploy/contract.js.map +1 -1
  53. package/dist/deploy/filesystem.d.ts +3 -6
  54. package/dist/deploy/filesystem.js +23 -20
  55. package/dist/deploy/filesystem.js.map +1 -1
  56. package/dist/deploy/node/runtime.d.ts +5 -3
  57. package/dist/deploy/node/runtime.js +23 -9
  58. package/dist/deploy/node/runtime.js.map +1 -1
  59. package/dist/deploy/presets.d.ts +34 -30
  60. package/dist/deploy/presets.js +56 -44
  61. package/dist/deploy/presets.js.map +1 -1
  62. package/dist/deploy/public-paths.d.ts +42 -0
  63. package/dist/deploy/public-paths.js +66 -0
  64. package/dist/deploy/public-paths.js.map +1 -0
  65. package/dist/deploy/vercel/build.d.ts +0 -1
  66. package/dist/deploy/vercel/build.js +25 -16
  67. package/dist/deploy/vercel/build.js.map +1 -1
  68. package/dist/deploy/vercel/runtime.d.ts +10 -7
  69. package/dist/deploy/vercel/runtime.js +48 -11
  70. package/dist/deploy/vercel/runtime.js.map +1 -1
  71. package/dist/index.d.ts +19 -17
  72. package/dist/index.js +20 -10
  73. package/dist/index.js.map +1 -1
  74. package/dist/router.d.ts +212 -120
  75. package/dist/router.js +3 -8
  76. package/dist/router.js.map +1 -1
  77. package/dist/runtime/boundaries.d.ts +62 -48
  78. package/dist/runtime/boundaries.js +32 -34
  79. package/dist/runtime/boundaries.js.map +1 -1
  80. package/dist/runtime/client.d.ts +20 -11
  81. package/dist/runtime/client.js +20 -10
  82. package/dist/runtime/client.js.map +1 -1
  83. package/dist/runtime/context.d.ts +304 -123
  84. package/dist/runtime/context.js +377 -149
  85. package/dist/runtime/context.js.map +1 -1
  86. package/dist/runtime/control.d.ts +15 -1
  87. package/dist/runtime/control.js +24 -0
  88. package/dist/runtime/control.js.map +1 -1
  89. package/dist/runtime/dev-protocol.d.ts +4 -9
  90. package/dist/runtime/dev-protocol.js.map +1 -1
  91. package/dist/runtime/empty-server-app.d.ts +0 -1
  92. package/dist/runtime/entry.client.d.ts +4 -1
  93. package/dist/runtime/entry.client.js +455 -313
  94. package/dist/runtime/entry.client.js.map +1 -1
  95. package/dist/runtime/entry.rsc.d.ts +15 -7
  96. package/dist/runtime/entry.rsc.js +811 -252
  97. package/dist/runtime/entry.rsc.js.map +1 -1
  98. package/dist/runtime/entry.ssr.d.ts +27 -10
  99. package/dist/runtime/entry.ssr.js +30 -48
  100. package/dist/runtime/entry.ssr.js.map +1 -1
  101. package/dist/runtime/failure-document.d.ts +11 -0
  102. package/dist/runtime/failure-document.js +35 -0
  103. package/dist/runtime/failure-document.js.map +1 -0
  104. package/dist/runtime/flight-inject.d.ts +23 -0
  105. package/dist/runtime/flight-inject.js +403 -0
  106. package/dist/runtime/flight-inject.js.map +1 -0
  107. package/dist/runtime/hot-update.d.ts +44 -0
  108. package/dist/runtime/hot-update.js +44 -0
  109. package/dist/runtime/hot-update.js.map +1 -0
  110. package/dist/runtime/navigation.d.ts +46 -54
  111. package/dist/runtime/navigation.js +22 -63
  112. package/dist/runtime/navigation.js.map +1 -1
  113. package/dist/runtime/request.d.ts +42 -8
  114. package/dist/runtime/request.js +84 -7
  115. package/dist/runtime/request.js.map +1 -1
  116. package/dist/runtime/server.d.ts +20 -18
  117. package/dist/runtime/server.js +22 -17
  118. package/dist/runtime/server.js.map +1 -1
  119. package/dist/runtime/validate-entries.d.ts +77 -0
  120. package/dist/runtime/validate-entries.js +405 -0
  121. package/dist/runtime/validate-entries.js.map +1 -0
  122. package/dist/server/headers.d.ts +41 -15
  123. package/dist/server/headers.js +53 -24
  124. package/dist/server/headers.js.map +1 -1
  125. package/dist/server/load-config.d.ts +4 -5
  126. package/dist/server/load-config.js +22 -14
  127. package/dist/server/load-config.js.map +1 -1
  128. package/dist/server/load-env.d.ts +0 -1
  129. package/dist/server/prerendered.d.ts +88 -34
  130. package/dist/server/prerendered.js +184 -24
  131. package/dist/server/prerendered.js.map +1 -1
  132. package/dist/server/server-config.d.ts +35 -50
  133. package/dist/server/server-config.js +27 -74
  134. package/dist/server/server-config.js.map +1 -1
  135. package/dist/server/shutdown.d.ts +5 -4
  136. package/dist/server/shutdown.js +5 -3
  137. package/dist/server/shutdown.js.map +1 -1
  138. package/dist/server/ssg.d.ts +9 -7
  139. package/dist/server/ssg.js +209 -65
  140. package/dist/server/ssg.js.map +1 -1
  141. package/dist/server/static.d.ts +0 -1
  142. package/dist/server/static.js +7 -1
  143. package/dist/server/static.js.map +1 -1
  144. package/package.json +21 -23
  145. package/dist/builder/page-files.d.ts.map +0 -1
  146. package/dist/builder/public-env.d.ts.map +0 -1
  147. package/dist/builder/rspack-config.d.ts.map +0 -1
  148. package/dist/cli/build.d.ts.map +0 -1
  149. package/dist/cli/dev.d.ts.map +0 -1
  150. package/dist/cli/index.d.ts.map +0 -1
  151. package/dist/cli/start.d.ts.map +0 -1
  152. package/dist/config.d.ts.map +0 -1
  153. package/dist/deploy/aws-lambda/runtime.d.ts.map +0 -1
  154. package/dist/deploy/build-marker.d.ts.map +0 -1
  155. package/dist/deploy/bun/runtime.d.ts +0 -11
  156. package/dist/deploy/bun/runtime.d.ts.map +0 -1
  157. package/dist/deploy/bun/runtime.js +0 -22
  158. package/dist/deploy/bun/runtime.js.map +0 -1
  159. package/dist/deploy/cloudflare/build.d.ts.map +0 -1
  160. package/dist/deploy/cloudflare/runtime.d.ts.map +0 -1
  161. package/dist/deploy/contract.d.ts.map +0 -1
  162. package/dist/deploy/deno/runtime.d.ts +0 -11
  163. package/dist/deploy/deno/runtime.d.ts.map +0 -1
  164. package/dist/deploy/deno/runtime.js +0 -16
  165. package/dist/deploy/deno/runtime.js.map +0 -1
  166. package/dist/deploy/filesystem.d.ts.map +0 -1
  167. package/dist/deploy/listen.d.ts +0 -20
  168. package/dist/deploy/listen.d.ts.map +0 -1
  169. package/dist/deploy/listen.js +0 -24
  170. package/dist/deploy/listen.js.map +0 -1
  171. package/dist/deploy/netlify/build.d.ts +0 -8
  172. package/dist/deploy/netlify/build.d.ts.map +0 -1
  173. package/dist/deploy/netlify/build.js +0 -52
  174. package/dist/deploy/netlify/build.js.map +0 -1
  175. package/dist/deploy/netlify/runtime.d.ts +0 -13
  176. package/dist/deploy/netlify/runtime.d.ts.map +0 -1
  177. package/dist/deploy/netlify/runtime.js +0 -24
  178. package/dist/deploy/netlify/runtime.js.map +0 -1
  179. package/dist/deploy/node/runtime.d.ts.map +0 -1
  180. package/dist/deploy/presets.d.ts.map +0 -1
  181. package/dist/deploy/vercel/build.d.ts.map +0 -1
  182. package/dist/deploy/vercel/runtime.d.ts.map +0 -1
  183. package/dist/index.d.ts.map +0 -1
  184. package/dist/router.d.ts.map +0 -1
  185. package/dist/runtime/boundaries.d.ts.map +0 -1
  186. package/dist/runtime/client.d.ts.map +0 -1
  187. package/dist/runtime/context.d.ts.map +0 -1
  188. package/dist/runtime/control.d.ts.map +0 -1
  189. package/dist/runtime/dev-protocol.d.ts.map +0 -1
  190. package/dist/runtime/empty-server-app.d.ts.map +0 -1
  191. package/dist/runtime/entry.client.d.ts.map +0 -1
  192. package/dist/runtime/entry.rsc.d.ts.map +0 -1
  193. package/dist/runtime/entry.ssr.d.ts.map +0 -1
  194. package/dist/runtime/navigation.d.ts.map +0 -1
  195. package/dist/runtime/request.d.ts.map +0 -1
  196. package/dist/runtime/server.d.ts.map +0 -1
  197. package/dist/server/compress.d.ts +0 -15
  198. package/dist/server/compress.d.ts.map +0 -1
  199. package/dist/server/compress.js +0 -76
  200. package/dist/server/compress.js.map +0 -1
  201. package/dist/server/headers.d.ts.map +0 -1
  202. package/dist/server/load-config.d.ts.map +0 -1
  203. package/dist/server/load-env.d.ts.map +0 -1
  204. package/dist/server/prerendered.d.ts.map +0 -1
  205. package/dist/server/server-config.d.ts.map +0 -1
  206. package/dist/server/shutdown.d.ts.map +0 -1
  207. package/dist/server/ssg.d.ts.map +0 -1
  208. package/dist/server/static.d.ts.map +0 -1
@@ -1,39 +1,199 @@
1
1
  /**
2
- * Everything about a prerendered page that doesn't need a filesystem: where the build puts it, and
3
- * what it looks like once read back.
2
+ * Everything about a prerendered page that doesn't need a filesystem: where the build puts it, and what it
3
+ * looks like once read back.
4
4
  *
5
- * Split out of `ssg.ts` which owns the writing and the reading, both through `node:fs` because a
6
- * deploy target without a filesystem reads the very same layout out of an asset store, and importing
7
- * it from the fs module would drag `node:fs` and `node:crypto` into a bundle that has neither.
5
+ * Apart from `ssg.ts`, which does the reading and writing through `node:fs`, because a deploy target without a
6
+ * filesystem reads the same layout out of an asset store and importing it from there would drag `node:fs`
7
+ * into a bundle that has neither.
8
8
  */
9
9
  export const VARIANTS = {
10
- html: { file: 'index.html', accept: 'text/html', contentType: 'text/html' },
11
- flight: { file: 'index.rsc', accept: 'text/x-component', contentType: 'text/x-component' },
10
+ // `headers` is what the build asks each representation for, so it stays the same request the browser makes:
11
+ // a document is the default and a flight payload is the `RSC` header. See `runtime/request.ts`.
12
+ html: { file: 'index.html', headers: {}, contentType: 'text/html' },
13
+ flight: { file: 'index.rsc', headers: { RSC: '1' }, contentType: 'text/x-component' },
12
14
  };
13
15
  /**
14
- * Where a route's prerendered output lives, relative to the output root or `null` for a path that
15
- * cannot be prerendered at all (one with a param or a wildcard left in it).
16
+ * How a build-time document render tells the prerender pass that the app minted a CSP nonce for its path.
16
17
  *
17
- * Always `/`-separated, never the host's separator: the same string addresses a file on a filesystem
18
- * (`resolve()` and `join()` both accept forward slashes on Windows) and a key in an asset store,
19
- * where a backslash would simply be the wrong character.
18
+ * The other half of the same contract as {@link VARIANTS}, and it exists because the pass cannot see the
19
+ * request it made: it renders through the app's own middleware inside the server bundle, in a module graph of
20
+ * its own, and keeps the body alone. What it needs to know is whether a *request* for this path will mint a
21
+ * nonce, because a document that will is one the framework re-renders per request — so the file the pass is
22
+ * about to write is one no deployment will read, and saying "prerendered" about it is a lie by omission.
23
+ *
24
+ * Set only while `RSHONO_PRERENDER` is in the environment, which is `rshono build`'s own process, so it never
25
+ * reaches a deployed response; and never stored, since only the body is.
26
+ */
27
+ export const PRERENDER_NONCE_HEADER = 'x-rshono-prerender-nonce';
28
+ /**
29
+ * The index the build leaves beside the pages, naming every file it wrote — one `files` array of
30
+ * {@link ssgFilePath} names.
31
+ *
32
+ * It exists so a reader can tell "this store has no page for that path" without asking the store, which is
33
+ * the difference between a `Map` lookup and a failed `readFile` (or, without a filesystem, a failed store
34
+ * fetch) on **every** request to a static route the build could not prerender — misses are deliberately not
35
+ * cached, so that one never warms up. Absent is not an error: a build from an older rshono has none, and a
36
+ * reader that finds none looks the way it always did.
37
+ */
38
+ export const SSG_MANIFEST_FILE = 'manifest.json';
39
+ /**
40
+ * What a decoded path segment may not hold if it is to be one portable file name. `\ / : * ? " < > |` are
41
+ * refused by Windows and `/` by every host, so a `staticPaths` value containing one fails the build rather
42
+ * than writing a page on one machine and not on another — and a route pattern, whose params and wildcards are
43
+ * spelled with `:` and `*`, falls out of the same rule.
44
+ */
45
+ const UNPORTABLE_SEGMENT = /[\\/:*?"<>|]/;
46
+ /**
47
+ * The DOS device names Windows still reserves, with or without an extension and whatever the case — so
48
+ * `con`, `CON` and `Con.txt` are all the console. `mkdir` on one fails with `EINVAL`, naming a path the
49
+ * author never wrote, which is why they are refused here with the rest.
50
+ *
51
+ * The list is Microsoft's, superscripts included: `CON PRN AUX NUL`, `COM0`–`COM9`, `COM¹ COM² COM³`, and
52
+ * the same three suffixes for `LPT`. A documentation slug of `con` is a plausible thing to write and
53
+ * builds fine on Linux today; refusing it everywhere is the same trade `:` and `*` already make — portable
54
+ * or not at all, rather than a build that works on the author's machine and fails in CI on Windows.
55
+ */
56
+ const RESERVED_DEVICE_NAME = /^(?:con|prn|aux|nul|com[0-9¹²³]|lpt[0-9¹²³])(?:\..*)?$/i;
57
+ /**
58
+ * A trailing `.` or space, which Win32 **strips** rather than refusing: `docs/x.` is stored as `docs/x`, so
59
+ * the page is written where no reader will look for it — "a page the build reports as prerendered and
60
+ * nothing ever serves", which is the failure {@link ssgFilePath} exists to prevent. The silent half of this
61
+ * rule, and the more important one: the reserved names above at least fail loudly.
62
+ */
63
+ const TRAILING_STRIPPED = /[. ]$/;
64
+ /**
65
+ * Decodes one path segment the way a URL does, treating a malformed escape as the literal text it is:
66
+ * `decodeURIComponent('%')` throws, and a request path is whatever the client chose to send.
67
+ */
68
+ function decodeSegment(segment) {
69
+ try {
70
+ return decodeURIComponent(segment);
71
+ }
72
+ catch {
73
+ return segment;
74
+ }
75
+ }
76
+ /**
77
+ * Whether one decoded segment can be a directory in the prerender tree, on every host rather than on this
78
+ * one: not empty, not a relative-path marker, made only of characters a file name can hold
79
+ * ({@link UNPORTABLE_SEGMENT}, plus the control characters, which a percent-escape is free to carry into a
80
+ * path and no filesystem wants in a name), not a {@link RESERVED_DEVICE_NAME}, and not ending in a
81
+ * character Win32 would {@link TRAILING_STRIPPED | strip}.
82
+ *
83
+ * The last two are Windows rules that no macOS or Linux build can discover for itself — which is exactly
84
+ * why they belong in a check rather than in a caveat.
20
85
  */
21
- export function ssgFilePath(routePath, variant = 'html') {
22
- if (/[:*]/.test(routePath))
23
- return null;
24
- const trimmed = routePath.replace(/^\/+|\/+$/g, '');
86
+ function isStorableSegment(segment) {
87
+ if (segment === '' || segment === '.' || segment === '..')
88
+ return false;
89
+ if (RESERVED_DEVICE_NAME.test(segment) || TRAILING_STRIPPED.test(segment))
90
+ return false;
91
+ return !UNPORTABLE_SEGMENT.test(segment) && ![...segment].some((char) => char < ' ');
92
+ }
93
+ /**
94
+ * Where a path's prerendered output lives, relative to the output root — or `null` for a path no single file
95
+ * can answer.
96
+ *
97
+ * **The one canonical form**, shared by the build that writes the file and by every deploy target that reads
98
+ * it back, because the two drifting apart is invisible: the build reports the page as prerendered and every
99
+ * request afterwards falls through to SSR, forever. Each segment is stored *decoded*, so `/docs/café` is
100
+ * `docs/café` whether the caller holds the path percent-encoded — the build, which interpolates `staticPaths`
101
+ * values into a URL — or already decoded, as Hono's `c.req.path` is: it runs `decodeURI` over any path
102
+ * containing a `%`. Decoded is also the form every other static file server addresses, so the tree stays
103
+ * servable by something that is not this framework.
104
+ *
105
+ * Always `/`-separated: the same string addresses a file on a filesystem, which accepts forward slashes on
106
+ * Windows too, and a key in an asset store, where a backslash would be the wrong character.
107
+ *
108
+ * `null` is everything that is not one file: a `.` or `..` segment — so traversal is a miss rather than a
109
+ * lookup, which a store addressed by key, with no `resolve()` to fall back on, depends on — an empty segment,
110
+ * and any segment {@link isStorableSegment} refuses as unportable.
111
+ */
112
+ export function ssgFilePath(path, variant = 'html') {
25
113
  const file = VARIANTS[variant].file;
26
- return trimmed === '' ? file : `${trimmed}/${file}`;
114
+ const trimmed = path.replace(/^\/+|\/+$/g, '');
115
+ if (trimmed === '')
116
+ return file;
117
+ const segments = [];
118
+ for (const encoded of trimmed.split('/')) {
119
+ const segment = decodeSegment(encoded);
120
+ if (!isStorableSegment(segment))
121
+ return null;
122
+ segments.push(segment);
123
+ }
124
+ return `${segments.join('/')}/${file}`;
125
+ }
126
+ /**
127
+ * {@link ssgFilePath} as a URL path, for a store addressed by one rather than by key. Every segment is escaped
128
+ * on the way out and the store decodes it back, so a page reached through a URL lands on the same file a
129
+ * filesystem target opens by name.
130
+ */
131
+ export function ssgAssetPath(relPath) {
132
+ return relPath.split('/').map(encodeURIComponent).join('/');
27
133
  }
134
+ /** The default cache budget: enough for a large documentation site's working set, small enough to be ignorable. */
135
+ const DEFAULT_CACHE_BYTES = 32 * 1024 * 1024;
28
136
  /**
29
- * {@link ssgFilePath} for a path that came off a request, so traversal is a miss rather than a lookup.
137
+ * A bounded, insertion-ordered cache of prerendered pages, so a site with thousands of them keeps a working set
138
+ * rather than the whole build in memory. Only *hits* are stored — caching misses would let anyone mint entries
139
+ * by requesting paths that don't exist — and the files never change while the server is up.
140
+ *
141
+ * Bounded by **bytes**, not by entry count: a count says nothing about how much memory it stands for, and a
142
+ * page can be any size. At 128 entries — the previous limit — a documentation site with half-megabyte pages
143
+ * retained ~64 MB across the two variants, with nothing about the number to suggest it.
30
144
  *
31
- * The first line of defence for every deploy target: a store addressed by key has no `resolve()` to
32
- * fall back on, so `..` has to be refused here or not at all.
145
+ * A page larger than the whole budget is served and not stored, rather than evicting everything to hold one.
146
+ */
147
+ export function createPageCache(maxBytes = DEFAULT_CACHE_BYTES) {
148
+ const pages = new Map();
149
+ let bytes = 0;
150
+ return {
151
+ get: (key) => pages.get(key),
152
+ set(key, page) {
153
+ const size = page.body.byteLength;
154
+ if (size > maxBytes)
155
+ return;
156
+ // Deleted before it is set, so re-setting an existing key moves it to the *back*: `Map.set` on a key it
157
+ // already holds keeps that key's original position, which would leave the entry just stored sitting at
158
+ // the front as the next thing evicted — a store whose eviction could drop the write that caused it.
159
+ const replaced = pages.get(key);
160
+ if (replaced) {
161
+ bytes -= replaced.body.byteLength;
162
+ pages.delete(key);
163
+ }
164
+ pages.set(key, page);
165
+ bytes += size;
166
+ // Insertion-ordered, so the first key is the oldest.
167
+ while (bytes > maxBytes) {
168
+ const oldest = pages.keys().next().value;
169
+ bytes -= pages.get(oldest).body.byteLength;
170
+ pages.delete(oldest);
171
+ }
172
+ },
173
+ };
174
+ }
175
+ /**
176
+ * A weak `ETag` for a page body — see {@link PrerenderedPage.etag} for why weak. Web Crypto rather than
177
+ * `node:crypto`, so one implementation serves both a Node server and `workerd`.
178
+ */
179
+ async function weakEtag(body) {
180
+ const digest = await crypto.subtle.digest('SHA-256', body);
181
+ const base64url = btoa(String.fromCharCode(...new Uint8Array(digest)))
182
+ .replace(/\+/g, '-')
183
+ .replace(/\//g, '_')
184
+ .replace(/=+$/, '');
185
+ return `W/"${base64url.slice(0, 22)}"`;
186
+ }
187
+ /**
188
+ * Assembles a {@link PrerenderedPage} from a body just read out of the build. `storeEtag` is the validator the
189
+ * store supplied, where it has one: it already describes these exact bytes, so it is weakened rather than
190
+ * replaced by a fresh hash.
33
191
  */
34
- export function prerenderedRelPath(requestPath, variant) {
35
- if (/(^|\/)\.\.?(\/|$)/.test(requestPath))
36
- return null;
37
- return ssgFilePath(requestPath, variant);
192
+ export async function toPrerenderedPage(body, storeEtag) {
193
+ return {
194
+ body,
195
+ contentLength: String(body.byteLength),
196
+ etag: storeEtag ? storeEtag.replace(/^(?!W\/)/, 'W/') : await weakEtag(body),
197
+ };
38
198
  }
39
199
  //# sourceMappingURL=prerendered.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"prerendered.js","sourceRoot":"","sources":["../../src/server/prerendered.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAWH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,IAAI,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE;IAC3E,MAAM,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,kBAAkB,EAAE,WAAW,EAAE,kBAAkB,EAAE;CACQ,CAAC;AAErG;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,SAAiB,EAAE,OAAO,GAAqB,MAAM;IAC/E,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAC;IACxC,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;IACpD,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC;IACpC,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,IAAI,IAAI,EAAE,CAAC;AACtD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,WAAmB,EAAE,OAAyB;IAC/E,IAAI,mBAAmB,CAAC,IAAI,CAAC,WAAW,CAAC;QAAE,OAAO,IAAI,CAAC;IACvD,OAAO,WAAW,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;AAC3C,CAAC","sourcesContent":["/**\n * Everything about a prerendered page that doesn't need a filesystem: where the build puts it, and\n * what it looks like once read back.\n *\n * Split out of `ssg.ts` — which owns the writing and the reading, both through `node:fs` — because a\n * deploy target without a filesystem reads the very same layout out of an asset store, and importing\n * it from the fs module would drag `node:fs` and `node:crypto` into a bundle that has neither.\n */\n\n/**\n * The two representations of a page, prerendered side by side.\n *\n * A hard load wants the HTML document; a soft navigation asks the same URL for a flight payload.\n * Writing only the HTML meant every in-app click re-rendered a page that was already built, so the\n * prerender only ever paid off for cold loads and crawlers.\n */\nexport type PrerenderVariant = 'html' | 'flight';\n\nexport const VARIANTS = {\n html: { file: 'index.html', accept: 'text/html', contentType: 'text/html' },\n flight: { file: 'index.rsc', accept: 'text/x-component', contentType: 'text/x-component' },\n} as const satisfies Record<PrerenderVariant, { file: string; accept: string; contentType: string }>;\n\n/**\n * Where a route's prerendered output lives, relative to the output root — or `null` for a path that\n * cannot be prerendered at all (one with a param or a wildcard left in it).\n *\n * Always `/`-separated, never the host's separator: the same string addresses a file on a filesystem\n * (`resolve()` and `join()` both accept forward slashes on Windows) and a key in an asset store,\n * where a backslash would simply be the wrong character.\n */\nexport function ssgFilePath(routePath: string, variant: PrerenderVariant = 'html'): string | null {\n if (/[:*]/.test(routePath)) return null;\n const trimmed = routePath.replace(/^\\/+|\\/+$/g, '');\n const file = VARIANTS[variant].file;\n return trimmed === '' ? file : `${trimmed}/${file}`;\n}\n\n/**\n * {@link ssgFilePath} for a path that came off a request, so traversal is a miss rather than a lookup.\n *\n * The first line of defence for every deploy target: a store addressed by key has no `resolve()` to\n * fall back on, so `..` has to be refused here or not at all.\n */\nexport function prerenderedRelPath(requestPath: string, variant: PrerenderVariant): string | null {\n if (/(^|\\/)\\.\\.?(\\/|$)/.test(requestPath)) return null;\n return ssgFilePath(requestPath, variant);\n}\n\n/** A prerendered page, ready to serve: its body and a validator derived from those exact bytes. */\nexport interface PrerenderedPage {\n /** The document or the flight payload, depending on which {@link PrerenderVariant} was read. */\n body: string;\n /**\n * `Content-Length` for {@link body}, in bytes rather than characters.\n *\n * Served with the response because Hono sets no length for an in-memory body, and without one the\n * compressor cannot tell a 300-byte page from a 300 KB one — so it gzips both, including the ones\n * where the framing costs more than it saves.\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 the client took gzip, and a\n * strong validator would have to differ between those two — so the 200 and the 304 that\n * revalidates it would disagree, and a cache would treat them as different pages. A weak tag\n * says \"the same representation\", which is exactly what is true across content 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 * How a build-time document render tells the prerender pass that the app minted a CSP nonce for its path.\n *\n * The other half of the same contract as {@link VARIANTS}, and it exists because the pass cannot see the\n * request it made: it renders through the app's own middleware inside the server bundle, in a module graph of\n * its own, and keeps the body alone. What it needs to know is whether a *request* for this path will mint a\n * nonce, because a document that will is one the framework re-renders per request — so the file the pass is\n * about to write is one no deployment will read, and saying \"prerendered\" about it is a lie by omission.\n *\n * Set only while `RSHONO_PRERENDER` is in the environment, which is `rshono build`'s own process, so it never\n * reaches a deployed response; and never stored, since only the body is.\n */\nexport const PRERENDER_NONCE_HEADER = 'x-rshono-prerender-nonce';\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 * The DOS device names Windows still reserves, with or without an extension and whatever the case — so\n * `con`, `CON` and `Con.txt` are all the console. `mkdir` on one fails with `EINVAL`, naming a path the\n * author never wrote, which is why they are refused here with the rest.\n *\n * The list is Microsoft's, superscripts included: `CON PRN AUX NUL`, `COM0`–`COM9`, `COM¹ COM² COM³`, and\n * the same three suffixes for `LPT`. A documentation slug of `con` is a plausible thing to write and\n * builds fine on Linux today; refusing it everywhere is the same trade `:` and `*` already make — portable\n * or not at all, rather than a build that works on the author's machine and fails in CI on Windows.\n */\nconst RESERVED_DEVICE_NAME = /^(?:con|prn|aux|nul|com[0-9¹²³]|lpt[0-9¹²³])(?:\\..*)?$/i;\n\n/**\n * A trailing `.` or space, which Win32 **strips** rather than refusing: `docs/x.` is stored as `docs/x`, so\n * the page is written where no reader will look for it — \"a page the build reports as prerendered and\n * nothing ever serves\", which is the failure {@link ssgFilePath} exists to prevent. The silent half of this\n * rule, and the more important one: the reserved names above at least fail loudly.\n */\nconst TRAILING_STRIPPED = /[. ]$/;\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, on every host rather than on this\n * one: not empty, not a relative-path marker, made only of characters a file name can hold\n * ({@link UNPORTABLE_SEGMENT}, plus the control characters, which a percent-escape is free to carry into a\n * path and no filesystem wants in a name), not a {@link RESERVED_DEVICE_NAME}, and not ending in a\n * character Win32 would {@link TRAILING_STRIPPED | strip}.\n *\n * The last two are Windows rules that no macOS or Linux build can discover for itself — which is exactly\n * why they belong in a check rather than in a caveat.\n */\nfunction isStorableSegment(segment: string): boolean {\n if (segment === '' || segment === '.' || segment === '..') return false;\n if (RESERVED_DEVICE_NAME.test(segment) || TRAILING_STRIPPED.test(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 {@link isStorableSegment} refuses as unportable.\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"]}
@@ -1,72 +1,57 @@
1
- import type { RSHonoConfig } from '../config.js';
1
+ import type { RshonoConfig } from '../config.js';
2
2
  /**
3
- * The framework settings the server bundle needs at request time, fully resolved
4
- * (defaults applied, `bodySizeLimit` parsed to bytes, origins normalized to hosts).
5
- *
6
- * The value is produced once by {@link resolveServerConfig} from `rshono.config.ts`
7
- * and compiled into the server bundle as the `__RSHONO_CONFIG__` literal (see
8
- * `builder/rspack-config.ts`) — there is no runtime env-var interface for these.
3
+ * The framework settings the server bundle needs at request time, fully resolved — produced once by
4
+ * {@link resolveServerConfig} and compiled in as the `__RSHONO_CONFIG__` literal. There is no runtime env-var
5
+ * interface for these, and nothing here is a per-request concern: those are Hono middleware in `src/server.ts`.
9
6
  */
10
7
  export interface ServerConfig {
11
8
  /**
12
- * `true` when the bundle came from `rshono dev`.
13
- *
14
- * Baked in rather than read from `process.env.NODE_ENV` at runtime: it is decided by which command
15
- * produced the bundle, and a deploy target need not have a `process` to read it from.
9
+ * `true` when the bundle came from `rshono dev`. Baked in rather than read from `process.env.NODE_ENV`,
10
+ * because a deploy target need not have a `process`.
16
11
  */
17
12
  isDev: boolean;
18
- /** Deadline in ms for a single request (server action + flight + SSR). */
19
- renderTimeoutMs: number;
20
13
  /** Honour `X-Forwarded-Host` / `-Proto` when resolving the browser-facing URL. Forced on in dev. */
21
14
  trustProxy: boolean;
22
- /** Send a strict per-request-nonce Content-Security-Policy with every HTML document. */
23
- cspEnabled: boolean;
24
- /** The resolved CSP directives (built-in defaults with the user's merged over them), minus the nonce. */
25
- cspDirectives: Record<string, string>;
26
- /** CSRF origin check on server-action POSTs. */
27
- checkOrigin: boolean;
28
- /** Extra origins allowed to post server actions, normalized to lowercase `URL.host` values. */
29
- allowedOrigins: string[];
30
- /** Max request body in bytes before a 413; `0` disables the cap. */
31
- maxBodyBytes: number;
32
- /** Gzip compressible responses on the way out. */
33
- compress: boolean;
34
- /** Default listen port for `start` (overridden by `PORT`). */
35
- port?: number;
36
- /** Default bind address for `start` (overridden by `HOST`). */
37
- host?: string;
15
+ /**
16
+ * The output directory this bundle was written to, relative to the project root. Carried into the bundle
17
+ * because `deploy/filesystem.ts` locates the assets and prerendered pages beside it at runtime, and only the
18
+ * compiler knows which directory it used.
19
+ */
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;
38
27
  }
39
28
  /**
40
- * The single source of truth for the framework's built-in defaults.
41
- *
42
- * `renderTimeoutMs` and `maxBodyBytes` are resolved here, into the bundle. `port` and `host` are not:
43
- * they stay env-overridable at runtime, so the default is applied where the address is resolved
44
- * (`deploy/listen.ts` for a server bundle, `cli/dev.ts` for the dev server) and this is what both read.
29
+ * The framework's built-in defaults. `port` and `host` are not config fields — they come from `PORT` / `HOST`
30
+ * or `--port` wherever the address is resolved, and this is what those fall back to.
45
31
  */
46
32
  export declare const SERVER_DEFAULTS: {
47
- readonly renderTimeoutMs: 10000;
48
- readonly maxBodyBytes: number;
49
33
  readonly port: 3000;
50
34
  readonly host: '0.0.0.0';
51
35
  };
52
36
  /**
53
- * The built-in {@link RSHonoConfig.csp} policy, keyed by directive so
54
- * {@link RSHonoConfig.cspDirectives} can override entries individually.
37
+ * Reads a port out of `--port` or `PORT`, so both are read the same way wherever the address is resolved.
55
38
  *
56
- * `script-src` carries the per-request nonce, appended at request time (see `entry.rsc.tsx`).
57
- * `style-src` needs `'unsafe-inline'` because React writes inline styles.
39
+ * A blank value is `undefined` "unset", falling through to {@link SERVER_DEFAULTS}. An empty `PORT` is
40
+ * common in CI images and container templates, and the other reading of it is a silent failure: `Number('')`
41
+ * is `0`, which binds a random free port and then reports success.
42
+ *
43
+ * @param source how to name the value in the error — the flag or the variable it came from.
44
+ * @throws RangeError for anything present that is not a port, rather than letting `NaN` reach Node as a raw
45
+ * `ERR_SOCKET_BAD_PORT` with a bundler frame in the stack.
58
46
  */
59
- export declare const CSP_DEFAULTS: Record<string, string>;
60
- /** Parse a {@link RSHonoConfig.bodySizeLimit} value into a byte count (`false`/`0` → `0`, disabling the cap). */
61
- export declare function parseByteSize(value: string | number | false | undefined): number | undefined;
47
+ export declare function parsePort(value: string | undefined, source: string): number | undefined;
62
48
  /**
63
- * Resolve the user's {@link RSHonoConfig} into the {@link ServerConfig} baked into the bundle.
64
- *
65
- * `isDev` is a build-time input rather than a config field because it decides one thing the user
66
- * shouldn't have to: `trustProxy` is forced on under `rshono dev`, where the framework's own proxy
67
- * is the only way in (it sets the forwarded headers itself and binds to localhost).
49
+ * Resolves the user's config into the {@link ServerConfig} baked into the bundle. `isDev` is a build-time input
50
+ * rather than a config field because it decides one thing the user should not have to: `trustProxy` is forced on
51
+ * under `rshono dev`, where the framework's own localhost proxy is the only way in.
68
52
  */
69
- export declare function resolveServerConfig(config: RSHonoConfig, { isDev }: {
53
+ export declare function resolveServerConfig(config: RshonoConfig, { isDev, outDir, envBindings }: {
70
54
  isDev: boolean;
55
+ outDir: string;
56
+ envBindings?: boolean;
71
57
  }): ServerConfig;
72
- //# sourceMappingURL=server-config.d.ts.map
@@ -1,94 +1,47 @@
1
1
  /**
2
- * The single source of truth for the framework's built-in defaults.
3
- *
4
- * `renderTimeoutMs` and `maxBodyBytes` are resolved here, into the bundle. `port` and `host` are not:
5
- * they stay env-overridable at runtime, so the default is applied where the address is resolved
6
- * (`deploy/listen.ts` for a server bundle, `cli/dev.ts` for the dev server) and this is what both read.
2
+ * The framework's built-in defaults. `port` and `host` are not config fields — they come from `PORT` / `HOST`
3
+ * or `--port` wherever the address is resolved, and this is what those fall back to.
7
4
  */
8
5
  export const SERVER_DEFAULTS = {
9
- renderTimeoutMs: 10_000,
10
- maxBodyBytes: 1024 * 1024, // 1 MiB, matching Next.js's server-action body-size limit.
11
6
  port: 3000,
12
7
  host: '0.0.0.0',
13
8
  };
9
+ /** The highest port a TCP listener can bind. `0` is the lowest, and means "any free port". */
10
+ const MAX_PORT = 65535;
14
11
  /**
15
- * The built-in {@link RSHonoConfig.csp} policy, keyed by directive so
16
- * {@link RSHonoConfig.cspDirectives} can override entries individually.
12
+ * Reads a port out of `--port` or `PORT`, so both are read the same way wherever the address is resolved.
17
13
  *
18
- * `script-src` carries the per-request nonce, appended at request time (see `entry.rsc.tsx`).
19
- * `style-src` needs `'unsafe-inline'` because React writes inline styles.
20
- */
21
- export const CSP_DEFAULTS = {
22
- 'default-src': "'self'",
23
- 'script-src': "'self'",
24
- 'style-src': "'self' 'unsafe-inline'",
25
- 'img-src': "'self' data:",
26
- 'connect-src': "'self'",
27
- // Not covered by default-src, and each closes an injection route of its own: a stray <base>
28
- // retargeting every relative URL, plugin content, framing (clickjacking), off-site form posts.
29
- 'base-uri': "'self'",
30
- 'object-src': "'none'",
31
- 'frame-ancestors': "'none'",
32
- 'form-action': "'self'",
33
- };
34
- const UNITS = { b: 1, kb: 1024, mb: 1024 ** 2, gb: 1024 ** 3 };
35
- /** Parse a {@link RSHonoConfig.bodySizeLimit} value into a byte count (`false`/`0` → `0`, disabling the cap). */
36
- export function parseByteSize(value) {
37
- if (value === undefined)
38
- return undefined;
39
- if (value === false)
40
- return 0;
41
- if (typeof value === 'number')
42
- return value;
43
- const match = /^\s*(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?\s*$/i.exec(value);
44
- if (!match) {
45
- throw new Error(`[rshono] invalid bodySizeLimit ${JSON.stringify(value)} — use e.g. '1mb', 1048576, or false.`);
46
- }
47
- return Math.floor(Number(match[1]) * UNITS[(match[2] ?? 'b').toLowerCase()]);
48
- }
49
- /**
50
- * Normalize a config `allowedOrigins` entry (full origin or bare host) to a lowercase `URL.host`,
51
- * ready for a direct comparison against a parsed `Origin` header's host.
14
+ * A blank value is `undefined` "unset", falling through to {@link SERVER_DEFAULTS}. An empty `PORT` is
15
+ * common in CI images and container templates, and the other reading of it is a silent failure: `Number('')`
16
+ * is `0`, which binds a random free port and then reports success.
52
17
  *
53
- * A bare `host:port` has to be retried against a base, because on its own `'localhost:4000'`
54
- * parses as the *scheme* `localhost:` with path `4000` leaving an empty host that would
55
- * silently never match anything. Throws rather than passing a junk entry through, so a typo
56
- * fails the build instead of quietly disabling the allowlist entry it was meant to add.
18
+ * @param source how to name the value in the error the flag or the variable it came from.
19
+ * @throws RangeError for anything present that is not a port, rather than letting `NaN` reach Node as a raw
20
+ * `ERR_SOCKET_BAD_PORT` with a bundler frame in the stack.
57
21
  */
58
- function normalizeOrigin(entry) {
59
- const host = URL.parse(entry)?.host || URL.parse(`http://${entry}`)?.host;
60
- if (!host) {
61
- throw new Error(`[rshono] invalid allowedOrigins entry ${JSON.stringify(entry)} — use a full origin ('https://admin.example.com') or a bare host ('localhost:4000').`);
22
+ export function parsePort(value, source) {
23
+ const text = value?.trim();
24
+ if (!text)
25
+ return undefined;
26
+ // Digits only: `Number` alone would also accept `0x50`, `1e3`, `+80` and `3.0`, none of which anyone
27
+ // typed meaning the port they would get.
28
+ const port = /^\d+$/.test(text) ? Number(text) : Number.NaN;
29
+ if (Number.isNaN(port) || port > MAX_PORT) {
30
+ throw new RangeError(`invalid ${source} ${JSON.stringify(value)} — expected an integer between 0 and ${MAX_PORT}.`);
62
31
  }
63
- return host.toLowerCase();
64
- }
65
- /** Drop directives the user blanked out, so `cspDirectives: { 'frame-ancestors': '' }` removes one. */
66
- function resolveCspDirectives(overrides) {
67
- return Object.fromEntries(Object.entries({ ...CSP_DEFAULTS, ...overrides }).filter(([, value]) => value.trim() !== ''));
32
+ return port;
68
33
  }
69
34
  /**
70
- * Resolve the user's {@link RSHonoConfig} into the {@link ServerConfig} baked into the bundle.
71
- *
72
- * `isDev` is a build-time input rather than a config field because it decides one thing the user
73
- * shouldn't have to: `trustProxy` is forced on under `rshono dev`, where the framework's own proxy
74
- * is the only way in (it sets the forwarded headers itself and binds to localhost).
35
+ * Resolves the user's config into the {@link ServerConfig} baked into the bundle. `isDev` is a build-time input
36
+ * rather than a config field because it decides one thing the user should not have to: `trustProxy` is forced on
37
+ * under `rshono dev`, where the framework's own localhost proxy is the only way in.
75
38
  */
76
- export function resolveServerConfig(config, { isDev }) {
39
+ export function resolveServerConfig(config, { isDev, outDir, envBindings = false }) {
77
40
  return {
78
41
  isDev,
79
- renderTimeoutMs: config.renderTimeout ?? SERVER_DEFAULTS.renderTimeoutMs,
80
42
  trustProxy: isDev || (config.trustProxy ?? false),
81
- cspEnabled: config.csp ?? false,
82
- cspDirectives: resolveCspDirectives(config.cspDirectives),
83
- checkOrigin: config.checkOrigin ?? true,
84
- allowedOrigins: (config.allowedOrigins ?? [])
85
- .map((entry) => entry.trim())
86
- .filter(Boolean)
87
- .map(normalizeOrigin),
88
- maxBodyBytes: parseByteSize(config.bodySizeLimit) ?? SERVER_DEFAULTS.maxBodyBytes,
89
- compress: config.compress ?? true,
90
- port: config.port,
91
- host: config.host,
43
+ outDir,
44
+ envBindings,
92
45
  };
93
46
  }
94
47
  //# sourceMappingURL=server-config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/server/server-config.ts"],"names":[],"mappings":"AAwCA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,eAAe,EAAE,MAAM;IACvB,YAAY,EAAE,IAAI,GAAG,IAAI,EAAE,2DAA2D;IACtF,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,SAAS;CACP,CAAC;AAEX;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,YAAY,GAA2B;IAClD,aAAa,EAAE,QAAQ;IACvB,YAAY,EAAE,QAAQ;IACtB,WAAW,EAAE,wBAAwB;IACrC,SAAS,EAAE,cAAc;IACzB,aAAa,EAAE,QAAQ;IACvB,4FAA4F;IAC5F,+FAA+F;IAC/F,UAAU,EAAE,QAAQ;IACpB,YAAY,EAAE,QAAQ;IACtB,iBAAiB,EAAE,QAAQ;IAC3B,aAAa,EAAE,QAAQ;CACxB,CAAC;AAEF,MAAM,KAAK,GAA2B,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC;AAEvF,iHAAiH;AACjH,MAAM,UAAU,aAAa,CAAC,KAA0C;IACtE,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC1C,IAAI,KAAK,KAAK,KAAK;QAAE,OAAO,CAAC,CAAC;IAC9B,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,MAAM,KAAK,GAAG,0CAA0C,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrE,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,kCAAkC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAClH,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;AAC/E,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,eAAe,CAAC,KAAa;IACpC,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,UAAU,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC;IAC1E,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CACb,yCAAyC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,uFAAuF,CACtJ,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC;AAC5B,CAAC;AAED,uGAAuG;AACvG,SAAS,oBAAoB,CAAC,SAA6C;IACzE,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,GAAG,YAAY,EAAE,GAAG,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;AAC1H,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAoB,EAAE,EAAE,KAAK,EAAsB;IACrF,OAAO;QACL,KAAK;QACL,eAAe,EAAE,MAAM,CAAC,aAAa,IAAI,eAAe,CAAC,eAAe;QACxE,UAAU,EAAE,KAAK,IAAI,CAAC,MAAM,CAAC,UAAU,IAAI,KAAK,CAAC;QACjD,UAAU,EAAE,MAAM,CAAC,GAAG,IAAI,KAAK;QAC/B,aAAa,EAAE,oBAAoB,CAAC,MAAM,CAAC,aAAa,CAAC;QACzD,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,IAAI;QACvC,cAAc,EAAE,CAAC,MAAM,CAAC,cAAc,IAAI,EAAE,CAAC;aAC1C,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;aAC5B,MAAM,CAAC,OAAO,CAAC;aACf,GAAG,CAAC,eAAe,CAAC;QACvB,YAAY,EAAE,aAAa,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,eAAe,CAAC,YAAY;QACjF,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,IAAI;QACjC,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,IAAI,EAAE,MAAM,CAAC,IAAI;KAClB,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\n * (defaults applied, `bodySizeLimit` parsed to bytes, origins normalized to hosts).\n *\n * The value is produced once by {@link resolveServerConfig} from `rshono.config.ts`\n * and compiled into the server bundle as the `__RSHONO_CONFIG__` literal (see\n * `builder/rspack-config.ts`) there is no runtime env-var interface for these.\n */\nexport interface ServerConfig {\n /**\n * `true` when the bundle came from `rshono dev`.\n *\n * Baked in rather than read from `process.env.NODE_ENV` at runtime: it is decided by which command\n * produced the bundle, and a deploy target need not have a `process` to read it from.\n */\n isDev: boolean;\n /** Deadline in ms for a single request (server action + flight + SSR). */\n renderTimeoutMs: number;\n /** Honour `X-Forwarded-Host` / `-Proto` when resolving the browser-facing URL. Forced on in dev. */\n trustProxy: boolean;\n /** Send a strict per-request-nonce Content-Security-Policy with every HTML document. */\n cspEnabled: boolean;\n /** The resolved CSP directives (built-in defaults with the user's merged over them), minus the nonce. */\n cspDirectives: Record<string, string>;\n /** CSRF origin check on server-action POSTs. */\n checkOrigin: boolean;\n /** Extra origins allowed to post server actions, normalized to lowercase `URL.host` values. */\n allowedOrigins: string[];\n /** Max request body in bytes before a 413; `0` disables the cap. */\n maxBodyBytes: number;\n /** Gzip compressible responses on the way out. */\n compress: boolean;\n /** Default listen port for `start` (overridden by `PORT`). */\n port?: number;\n /** Default bind address for `start` (overridden by `HOST`). */\n host?: string;\n}\n\n/**\n * The single source of truth for the framework's built-in defaults.\n *\n * `renderTimeoutMs` and `maxBodyBytes` are resolved here, into the bundle. `port` and `host` are not:\n * they stay env-overridable at runtime, so the default is applied where the address is resolved\n * (`deploy/listen.ts` for a server bundle, `cli/dev.ts` for the dev server) and this is what both read.\n */\nexport const SERVER_DEFAULTS = {\n renderTimeoutMs: 10_000,\n maxBodyBytes: 1024 * 1024, // 1 MiB, matching Next.js's server-action body-size limit.\n port: 3000,\n host: '0.0.0.0',\n} as const;\n\n/**\n * The built-in {@link RSHonoConfig.csp} policy, keyed by directive so\n * {@link RSHonoConfig.cspDirectives} can override entries individually.\n *\n * `script-src` carries the per-request nonce, appended at request time (see `entry.rsc.tsx`).\n * `style-src` needs `'unsafe-inline'` because React writes inline styles.\n */\nexport const CSP_DEFAULTS: Record<string, string> = {\n 'default-src': \"'self'\",\n 'script-src': \"'self'\",\n 'style-src': \"'self' 'unsafe-inline'\",\n 'img-src': \"'self' data:\",\n 'connect-src': \"'self'\",\n // Not covered by default-src, and each closes an injection route of its own: a stray <base>\n // retargeting every relative URL, plugin content, framing (clickjacking), off-site form posts.\n 'base-uri': \"'self'\",\n 'object-src': \"'none'\",\n 'frame-ancestors': \"'none'\",\n 'form-action': \"'self'\",\n};\n\nconst UNITS: Record<string, number> = { b: 1, kb: 1024, mb: 1024 ** 2, gb: 1024 ** 3 };\n\n/** Parse a {@link RSHonoConfig.bodySizeLimit} value into a byte count (`false`/`0` → `0`, disabling the cap). */\nexport function parseByteSize(value: string | number | false | undefined): number | undefined {\n if (value === undefined) return undefined;\n if (value === false) return 0;\n if (typeof value === 'number') return value;\n const match = /^\\s*(\\d+(?:\\.\\d+)?)\\s*(b|kb|mb|gb)?\\s*$/i.exec(value);\n if (!match) {\n throw new Error(`[rshono] invalid bodySizeLimit ${JSON.stringify(value)} — use e.g. '1mb', 1048576, or false.`);\n }\n return Math.floor(Number(match[1]) * UNITS[(match[2] ?? 'b').toLowerCase()]);\n}\n\n/**\n * Normalize a config `allowedOrigins` entry (full origin or bare host) to a lowercase `URL.host`,\n * ready for a direct comparison against a parsed `Origin` header's host.\n *\n * A bare `host:port` has to be retried against a base, because on its own `'localhost:4000'`\n * parses as the *scheme* `localhost:` with path `4000` leaving an empty host that would\n * silently never match anything. Throws rather than passing a junk entry through, so a typo\n * fails the build instead of quietly disabling the allowlist entry it was meant to add.\n */\nfunction normalizeOrigin(entry: string): string {\n const host = URL.parse(entry)?.host || URL.parse(`http://${entry}`)?.host;\n if (!host) {\n throw new Error(\n `[rshono] invalid allowedOrigins entry ${JSON.stringify(entry)} use a full origin ('https://admin.example.com') or a bare host ('localhost:4000').`,\n );\n }\n return host.toLowerCase();\n}\n\n/** Drop directives the user blanked out, so `cspDirectives: { 'frame-ancestors': '' }` removes one. */\nfunction resolveCspDirectives(overrides: Record<string, string> | undefined): Record<string, string> {\n return Object.fromEntries(Object.entries({ ...CSP_DEFAULTS, ...overrides }).filter(([, value]) => value.trim() !== ''));\n}\n\n/**\n * Resolve the user's {@link RSHonoConfig} into the {@link ServerConfig} baked into the bundle.\n *\n * `isDev` is a build-time input rather than a config field because it decides one thing the user\n * shouldn't have to: `trustProxy` is forced on under `rshono dev`, where the framework's own proxy\n * is the only way in (it sets the forwarded headers itself and binds to localhost).\n */\nexport function resolveServerConfig(config: RSHonoConfig, { isDev }: { isDev: boolean }): ServerConfig {\n return {\n isDev,\n renderTimeoutMs: config.renderTimeout ?? SERVER_DEFAULTS.renderTimeoutMs,\n trustProxy: isDev || (config.trustProxy ?? false),\n cspEnabled: config.csp ?? false,\n cspDirectives: resolveCspDirectives(config.cspDirectives),\n checkOrigin: config.checkOrigin ?? true,\n allowedOrigins: (config.allowedOrigins ?? [])\n .map((entry) => entry.trim())\n .filter(Boolean)\n .map(normalizeOrigin),\n maxBodyBytes: parseByteSize(config.bodySizeLimit) ?? SERVER_DEFAULTS.maxBodyBytes,\n compress: config.compress ?? true,\n port: config.port,\n host: config.host,\n };\n}\n"]}
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"]}
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Registers `handler` for both `SIGINT` and `SIGTERM` — the two signals a process
3
- * manager or `Ctrl-C` uses to ask for a graceful stop. Consolidates the shutdown
4
- * wiring shared by the server bundle and the `start` launcher.
2
+ * Registers `handler` for both `SIGINT` and `SIGTERM` — the two signals a process manager or `Ctrl-C` uses to
3
+ * ask for a graceful stop.
4
+ *
5
+ * Registered by the server bundle itself, which is why `rshono start` needs no supervisor to forward signals
6
+ * to: it imports the bundle into its own process, so these handlers are already the process's.
5
7
  */
6
8
  export declare function onShutdown(handler: (signal: NodeJS.Signals) => void): void;
7
- //# sourceMappingURL=shutdown.d.ts.map
@@ -1,7 +1,9 @@
1
1
  /**
2
- * Registers `handler` for both `SIGINT` and `SIGTERM` — the two signals a process
3
- * manager or `Ctrl-C` uses to ask for a graceful stop. Consolidates the shutdown
4
- * wiring shared by the server bundle and the `start` launcher.
2
+ * Registers `handler` for both `SIGINT` and `SIGTERM` — the two signals a process manager or `Ctrl-C` uses to
3
+ * ask for a graceful stop.
4
+ *
5
+ * Registered by the server bundle itself, which is why `rshono start` needs no supervisor to forward signals
6
+ * to: it imports the bundle into its own process, so these handlers are already the process's.
5
7
  */
6
8
  export function onShutdown(handler) {
7
9
  for (const signal of ['SIGINT', 'SIGTERM']) {
@@ -1 +1 @@
1
- {"version":3,"file":"shutdown.js","sourceRoot":"","sources":["../../src/server/shutdown.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,OAAyC;IAClE,KAAK,MAAM,MAAM,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAU,EAAE,CAAC;QACpD,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC","sourcesContent":["/**\n * Registers `handler` for both `SIGINT` and `SIGTERM` — the two signals a process\n * manager or `Ctrl-C` uses to ask for a graceful stop. Consolidates the shutdown\n * wiring shared by the server bundle and the `start` launcher.\n */\nexport function onShutdown(handler: (signal: NodeJS.Signals) => void): void {\n for (const signal of ['SIGINT', 'SIGTERM'] as const) {\n process.on(signal, () => handler(signal));\n }\n}\n"]}
1
+ {"version":3,"file":"shutdown.js","sourceRoot":"","sources":["../../src/server/shutdown.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,OAAyC;IAClE,KAAK,MAAM,MAAM,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAU,EAAE,CAAC;QACpD,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC","sourcesContent":["/**\n * Registers `handler` for both `SIGINT` and `SIGTERM` — the two signals a process manager or `Ctrl-C` uses to\n * ask for a graceful stop.\n *\n * Registered by the server bundle itself, which is why `rshono start` needs no supervisor to forward signals\n * to: it imports the bundle into its own process, so these handlers are already the process's.\n */\nexport function onShutdown(handler: (signal: NodeJS.Signals) => void): void {\n for (const signal of ['SIGINT', 'SIGTERM'] as const) {\n process.on(signal, () => handler(signal));\n }\n}\n"]}