@timber-js/app 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/dist/index.js +5 -2
  2. package/dist/index.js.map +1 -1
  3. package/dist/plugins/entries.d.ts.map +1 -1
  4. package/package.json +2 -1
  5. package/src/adapters/cloudflare.ts +325 -0
  6. package/src/adapters/nitro.ts +366 -0
  7. package/src/adapters/types.ts +63 -0
  8. package/src/cache/index.ts +91 -0
  9. package/src/cache/redis-handler.ts +91 -0
  10. package/src/cache/register-cached-function.ts +99 -0
  11. package/src/cache/singleflight.ts +26 -0
  12. package/src/cache/stable-stringify.ts +21 -0
  13. package/src/cache/timber-cache.ts +116 -0
  14. package/src/cli.ts +201 -0
  15. package/src/client/browser-entry.ts +663 -0
  16. package/src/client/error-boundary.tsx +209 -0
  17. package/src/client/form.tsx +200 -0
  18. package/src/client/head.ts +61 -0
  19. package/src/client/history.ts +46 -0
  20. package/src/client/index.ts +60 -0
  21. package/src/client/link-navigate-interceptor.tsx +62 -0
  22. package/src/client/link-status-provider.tsx +40 -0
  23. package/src/client/link.tsx +310 -0
  24. package/src/client/nuqs-adapter.tsx +117 -0
  25. package/src/client/router-ref.ts +25 -0
  26. package/src/client/router.ts +563 -0
  27. package/src/client/segment-cache.ts +194 -0
  28. package/src/client/segment-context.ts +57 -0
  29. package/src/client/ssr-data.ts +95 -0
  30. package/src/client/types.ts +4 -0
  31. package/src/client/unload-guard.ts +34 -0
  32. package/src/client/use-cookie.ts +122 -0
  33. package/src/client/use-link-status.ts +46 -0
  34. package/src/client/use-navigation-pending.ts +47 -0
  35. package/src/client/use-params.ts +71 -0
  36. package/src/client/use-pathname.ts +43 -0
  37. package/src/client/use-query-states.ts +133 -0
  38. package/src/client/use-router.ts +77 -0
  39. package/src/client/use-search-params.ts +74 -0
  40. package/src/client/use-selected-layout-segment.ts +110 -0
  41. package/src/content/index.ts +13 -0
  42. package/src/cookies/define-cookie.ts +137 -0
  43. package/src/cookies/index.ts +9 -0
  44. package/src/fonts/ast.ts +359 -0
  45. package/src/fonts/css.ts +68 -0
  46. package/src/fonts/fallbacks.ts +248 -0
  47. package/src/fonts/google.ts +332 -0
  48. package/src/fonts/local.ts +177 -0
  49. package/src/fonts/types.ts +88 -0
  50. package/src/index.ts +413 -0
  51. package/src/plugins/adapter-build.ts +118 -0
  52. package/src/plugins/build-manifest.ts +323 -0
  53. package/src/plugins/build-report.ts +353 -0
  54. package/src/plugins/cache-transform.ts +199 -0
  55. package/src/plugins/chunks.ts +90 -0
  56. package/src/plugins/content.ts +136 -0
  57. package/src/plugins/dev-error-overlay.ts +230 -0
  58. package/src/plugins/dev-logs.ts +280 -0
  59. package/src/plugins/dev-server.ts +389 -0
  60. package/src/plugins/dynamic-transform.ts +161 -0
  61. package/src/plugins/entries.ts +207 -0
  62. package/src/plugins/fonts.ts +581 -0
  63. package/src/plugins/mdx.ts +179 -0
  64. package/src/plugins/react-prod.ts +56 -0
  65. package/src/plugins/routing.ts +419 -0
  66. package/src/plugins/server-action-exports.ts +220 -0
  67. package/src/plugins/server-bundle.ts +113 -0
  68. package/src/plugins/shims.ts +168 -0
  69. package/src/plugins/static-build.ts +207 -0
  70. package/src/routing/codegen.ts +396 -0
  71. package/src/routing/index.ts +14 -0
  72. package/src/routing/interception.ts +173 -0
  73. package/src/routing/scanner.ts +487 -0
  74. package/src/routing/status-file-lint.ts +114 -0
  75. package/src/routing/types.ts +100 -0
  76. package/src/search-params/analyze.ts +192 -0
  77. package/src/search-params/codecs.ts +153 -0
  78. package/src/search-params/create.ts +314 -0
  79. package/src/search-params/index.ts +23 -0
  80. package/src/search-params/registry.ts +31 -0
  81. package/src/server/access-gate.tsx +142 -0
  82. package/src/server/action-client.ts +473 -0
  83. package/src/server/action-handler.ts +325 -0
  84. package/src/server/actions.ts +236 -0
  85. package/src/server/asset-headers.ts +81 -0
  86. package/src/server/body-limits.ts +102 -0
  87. package/src/server/build-manifest.ts +234 -0
  88. package/src/server/canonicalize.ts +90 -0
  89. package/src/server/client-module-map.ts +58 -0
  90. package/src/server/csrf.ts +79 -0
  91. package/src/server/deny-renderer.ts +302 -0
  92. package/src/server/dev-logger.ts +419 -0
  93. package/src/server/dev-span-processor.ts +78 -0
  94. package/src/server/dev-warnings.ts +282 -0
  95. package/src/server/early-hints-sender.ts +55 -0
  96. package/src/server/early-hints.ts +142 -0
  97. package/src/server/error-boundary-wrapper.ts +69 -0
  98. package/src/server/error-formatter.ts +184 -0
  99. package/src/server/flush.ts +182 -0
  100. package/src/server/form-data.ts +176 -0
  101. package/src/server/form-flash.ts +93 -0
  102. package/src/server/html-injectors.ts +445 -0
  103. package/src/server/index.ts +222 -0
  104. package/src/server/instrumentation.ts +136 -0
  105. package/src/server/logger.ts +145 -0
  106. package/src/server/manifest-status-resolver.ts +215 -0
  107. package/src/server/metadata-render.ts +527 -0
  108. package/src/server/metadata-routes.ts +189 -0
  109. package/src/server/metadata.ts +263 -0
  110. package/src/server/middleware-runner.ts +32 -0
  111. package/src/server/nuqs-ssr-provider.tsx +63 -0
  112. package/src/server/pipeline.ts +555 -0
  113. package/src/server/prerender.ts +139 -0
  114. package/src/server/primitives.ts +264 -0
  115. package/src/server/proxy.ts +43 -0
  116. package/src/server/request-context.ts +554 -0
  117. package/src/server/route-element-builder.ts +395 -0
  118. package/src/server/route-handler.ts +153 -0
  119. package/src/server/route-matcher.ts +316 -0
  120. package/src/server/rsc-entry/api-handler.ts +112 -0
  121. package/src/server/rsc-entry/error-renderer.ts +177 -0
  122. package/src/server/rsc-entry/helpers.ts +147 -0
  123. package/src/server/rsc-entry/index.ts +688 -0
  124. package/src/server/rsc-entry/ssr-bridge.ts +18 -0
  125. package/src/server/slot-resolver.ts +359 -0
  126. package/src/server/ssr-entry.ts +161 -0
  127. package/src/server/ssr-render.ts +200 -0
  128. package/src/server/status-code-resolver.ts +282 -0
  129. package/src/server/tracing.ts +281 -0
  130. package/src/server/tree-builder.ts +354 -0
  131. package/src/server/types.ts +150 -0
  132. package/src/shims/font-google.ts +67 -0
  133. package/src/shims/headers.ts +11 -0
  134. package/src/shims/image.ts +48 -0
  135. package/src/shims/link.ts +9 -0
  136. package/src/shims/navigation-client.ts +52 -0
  137. package/src/shims/navigation.ts +31 -0
  138. package/src/shims/server-only-noop.js +5 -0
  139. package/src/utils/directive-parser.ts +529 -0
  140. package/src/utils/format.ts +10 -0
  141. package/src/utils/startup-timer.ts +102 -0
@@ -0,0 +1,445 @@
1
+ /**
2
+ * HTML stream injectors — TransformStreams that modify streamed HTML.
3
+ *
4
+ * These are extracted into a separate module so they can be tested
5
+ * independently of rsc-entry.ts (which imports virtual modules).
6
+ *
7
+ * Design docs: 02-rendering-pipeline.md, 18-build-system.md §"Entry Files"
8
+ */
9
+
10
+ /**
11
+ * Inject HTML content before a closing tag in the stream.
12
+ *
13
+ * Streams chunks through immediately, keeping only a small trailing
14
+ * buffer (the length of the target tag minus one) to handle the case
15
+ * where the target tag spans two chunks. This preserves React's
16
+ * streaming behavior for Suspense boundaries — chunks are not held
17
+ * back waiting for the closing tag.
18
+ */
19
+ function createInjector(
20
+ stream: ReadableStream<Uint8Array>,
21
+ content: string,
22
+ targetTag: string,
23
+ position: 'before' | 'after' = 'before'
24
+ ): ReadableStream<Uint8Array> {
25
+ if (!content) return stream;
26
+
27
+ const decoder = new TextDecoder();
28
+ const encoder = new TextEncoder();
29
+ let injected = false;
30
+ // Keep a trailing buffer just large enough that the target tag
31
+ // can't be split across the boundary without us seeing it.
32
+ let tail = '';
33
+ const tailLen = targetTag.length - 1;
34
+
35
+ return stream.pipeThrough(
36
+ new TransformStream<Uint8Array, Uint8Array>({
37
+ transform(chunk, controller) {
38
+ if (injected) {
39
+ controller.enqueue(chunk);
40
+ return;
41
+ }
42
+
43
+ // Combine the trailing buffer with the new chunk
44
+ const text = tail + decoder.decode(chunk, { stream: true });
45
+ const tagIndex = text.indexOf(targetTag);
46
+
47
+ if (tagIndex !== -1) {
48
+ const splitPoint = position === 'before' ? tagIndex : tagIndex + targetTag.length;
49
+ const before = text.slice(0, splitPoint);
50
+ const after = text.slice(splitPoint);
51
+ controller.enqueue(encoder.encode(before + content + after));
52
+ injected = true;
53
+ tail = '';
54
+ } else {
55
+ // Flush everything except the last tailLen chars (which might
56
+ // be the start of the target tag split across chunks).
57
+ const safeEnd = Math.max(0, text.length - tailLen);
58
+ if (safeEnd > 0) {
59
+ controller.enqueue(encoder.encode(text.slice(0, safeEnd)));
60
+ }
61
+ tail = text.slice(safeEnd);
62
+ }
63
+ },
64
+ flush(controller) {
65
+ if (!injected && tail) {
66
+ controller.enqueue(encoder.encode(tail));
67
+ }
68
+ },
69
+ })
70
+ );
71
+ }
72
+
73
+ /**
74
+ * Inject metadata elements before </head> in the HTML stream.
75
+ *
76
+ * If no </head> is found, the buffer is emitted as-is.
77
+ */
78
+ export function injectHead(
79
+ stream: ReadableStream<Uint8Array>,
80
+ headHtml: string
81
+ ): ReadableStream<Uint8Array> {
82
+ return createInjector(stream, headHtml, '</head>');
83
+ }
84
+
85
+ /**
86
+ * Inject client bootstrap scripts before </body> in the HTML stream.
87
+ *
88
+ * Returns the stream unchanged if scriptsHtml is empty (client JS disabled mode).
89
+ * If no </body> is found, the buffer is emitted as-is.
90
+ */
91
+ export function injectScripts(
92
+ stream: ReadableStream<Uint8Array>,
93
+ scriptsHtml: string
94
+ ): ReadableStream<Uint8Array> {
95
+ return createInjector(stream, scriptsHtml, '</body>');
96
+ }
97
+
98
+ /**
99
+ * Escape a string for safe embedding inside a `<script>` tag within
100
+ * a JSON-encoded value.
101
+ *
102
+ * Only needs to prevent `</script>` from closing the tag early and
103
+ * handle U+2028/U+2029 (line/paragraph separators valid in JSON but
104
+ * historically problematic in JS). Since we use JSON.stringify for the
105
+ * outer encoding, we only escape `<` and the line separators.
106
+ */
107
+ function htmlEscapeJsonString(str: string): string {
108
+ return str
109
+ .replace(/</g, '\\u003c')
110
+ .replace(/\u2028/g, '\\u2028')
111
+ .replace(/\u2029/g, '\\u2029');
112
+ }
113
+
114
+ /**
115
+ * Transform an RSC Flight stream into a stream of inline `<script>` tags.
116
+ *
117
+ * Uses a **pull-based** ReadableStream — the consumer (the injection
118
+ * transform) drives reads from the RSC stream on demand. No background
119
+ * reader, no shared mutable arrays, no race conditions.
120
+ *
121
+ * Each RSC chunk becomes:
122
+ * <script>(self.__timber_f=self.__timber_f||[]).push([1,"escaped_chunk"])</script>
123
+ *
124
+ * The first chunk emitted is the bootstrap signal [0] which the client
125
+ * uses to initialize its buffer.
126
+ *
127
+ * Uses JSON-encoded typed tuples matching the pattern from Next.js:
128
+ * [0] — bootstrap signal
129
+ * [1, data] — RSC Flight data chunk (UTF-8 string)
130
+ */
131
+ export function createInlinedRscStream(
132
+ rscStream: ReadableStream<Uint8Array>
133
+ ): ReadableStream<Uint8Array> {
134
+ const encoder = new TextEncoder();
135
+ const rscReader = rscStream.getReader();
136
+ const decoder = new TextDecoder('utf-8', { fatal: true });
137
+
138
+ return new ReadableStream<Uint8Array>({
139
+ start(controller) {
140
+ // Emit bootstrap signal — tells the client that __timber_f is active
141
+ const bootstrap = `<script>(self.__timber_f=self.__timber_f||[]).push(${htmlEscapeJsonString(JSON.stringify([0]))})</script>`;
142
+ controller.enqueue(encoder.encode(bootstrap));
143
+ },
144
+ async pull(controller) {
145
+ try {
146
+ const { done, value } = await rscReader.read();
147
+ if (done) {
148
+ controller.close();
149
+ return;
150
+ }
151
+ if (value) {
152
+ const decoded = decoder.decode(value, { stream: true });
153
+ const escaped = htmlEscapeJsonString(JSON.stringify([1, decoded]));
154
+ controller.enqueue(encoder.encode(`<script>self.__timber_f.push(${escaped})</script>`));
155
+ }
156
+ } catch (error) {
157
+ controller.error(error);
158
+ }
159
+ },
160
+ });
161
+ }
162
+
163
+ /**
164
+ * Merge RSC script stream into the HTML stream, injecting scripts
165
+ * only as direct children of `<body>`.
166
+ *
167
+ * This single transform replaces the previous two-stage pipeline
168
+ * (createFlightInjectionTransform + createMoveSuffixStream). It:
169
+ *
170
+ * 1. Strips `</body></html>` from the shell chunk so all subsequent
171
+ * content is at the `<body>` level.
172
+ * 2. Buffers RSC `<script>` tags and drains them after the suffix
173
+ * has been stripped — guaranteeing body-level injection.
174
+ * 3. Re-emits `</body></html>` at the very end after all RSC scripts.
175
+ *
176
+ * Because the suffix is stripped before any scripts are injected,
177
+ * scripts are always direct children of `<body>` regardless of how
178
+ * React's renderToReadableStream chunks the HTML. No HTML structure
179
+ * scanning or depth tracking needed — the suffix removal is the
180
+ * structural guarantee.
181
+ *
182
+ * Inspired by Next.js createFlightDataInjectionTransformStream.
183
+ */
184
+ function createFlightInjectionTransform(
185
+ rscScriptStream: ReadableStream<Uint8Array>
186
+ ): TransformStream<Uint8Array, Uint8Array> {
187
+ const encoder = new TextEncoder();
188
+ const decoder = new TextDecoder();
189
+ const suffix = '</body></html>';
190
+ const suffixBytes = encoder.encode(suffix);
191
+
192
+ const rscReader = rscScriptStream.getReader();
193
+ let pullPromise: Promise<void> | null = null;
194
+ let donePulling = false;
195
+ let pullError: unknown = null;
196
+ // Once the suffix is stripped, all content is body-level and
197
+ // scripts can safely be drained after any HTML chunk.
198
+ let foundSuffix = false;
199
+
200
+ // RSC script chunks waiting to be injected at the body level.
201
+ const pending: Uint8Array[] = [];
202
+
203
+ async function pullLoop(): Promise<void> {
204
+ // Wait one macrotask so the HTML shell chunk flows through
205
+ // transform() first. The browser needs the shell HTML before
206
+ // RSC data script tags arrive.
207
+ await new Promise<void>((r) => setTimeout(r, 0));
208
+
209
+ try {
210
+ for (;;) {
211
+ const { done, value } = await rscReader.read();
212
+ if (done) {
213
+ donePulling = true;
214
+ return;
215
+ }
216
+ pending.push(value);
217
+ // Yield between reads so HTML chunks get a chance to flow
218
+ // through transform() first. RSC and HTML are driven by the
219
+ // same source — each RSC chunk typically produces a
220
+ // corresponding HTML chunk from SSR.
221
+ await new Promise<void>((r) => setTimeout(r, 0));
222
+ }
223
+ } catch (err) {
224
+ pullError = err;
225
+ donePulling = true;
226
+ }
227
+ }
228
+
229
+ /** Drain all buffered RSC script chunks to the output. */
230
+ function drainPending(controller: TransformStreamDefaultController<Uint8Array>): void {
231
+ while (pending.length > 0) {
232
+ controller.enqueue(pending.shift()!);
233
+ }
234
+ if (pullError) {
235
+ const err = pullError;
236
+ pullError = null;
237
+ controller.error(err);
238
+ }
239
+ }
240
+
241
+ return new TransformStream<Uint8Array, Uint8Array>({
242
+ transform(chunk, controller) {
243
+ // Start pulling RSC scripts into the buffer (if not started)
244
+ if (!pullPromise) {
245
+ pullPromise = pullLoop();
246
+ }
247
+
248
+ if (foundSuffix) {
249
+ // Post-suffix: everything is body-level (Suspense chunks).
250
+ // Emit HTML, then drain any buffered scripts.
251
+ controller.enqueue(chunk);
252
+ if (pending.length > 0) drainPending(controller);
253
+ return;
254
+ }
255
+
256
+ // Look for </body></html> in the shell chunk.
257
+ const text = decoder.decode(chunk, { stream: true });
258
+ const idx = text.indexOf(suffix);
259
+ if (idx !== -1) {
260
+ foundSuffix = true;
261
+ // Emit everything before the suffix (still inside <body>'s
262
+ // child elements — don't inject scripts here).
263
+ const before = text.slice(0, idx);
264
+ const after = text.slice(idx + suffix.length);
265
+ if (before) controller.enqueue(encoder.encode(before));
266
+ // Now we're at body level — drain buffered scripts
267
+ if (pending.length > 0) drainPending(controller);
268
+ // Emit any content after the suffix (shouldn't normally exist)
269
+ if (after) controller.enqueue(encoder.encode(after));
270
+ } else {
271
+ // Pre-suffix: inside nested elements. Pass through, don't
272
+ // inject scripts (they'd become children of nested elements).
273
+ controller.enqueue(chunk);
274
+ }
275
+ },
276
+ flush(controller) {
277
+ // HTML stream is done — drain remaining RSC chunks at body level
278
+ const finish = () => {
279
+ drainPending(controller);
280
+ // Re-emit the suffix at the very end so HTML is well-formed
281
+ if (foundSuffix) {
282
+ controller.enqueue(suffixBytes);
283
+ }
284
+ };
285
+
286
+ if (donePulling) {
287
+ finish();
288
+ return;
289
+ }
290
+ if (!pullPromise) {
291
+ pullPromise = pullLoop();
292
+ }
293
+ return pullPromise.then(finish);
294
+ },
295
+ });
296
+ }
297
+
298
+ /**
299
+ * Progressively inline RSC Flight payload chunks into the HTML stream.
300
+ *
301
+ * Architecture (3 TransformStream pipeline):
302
+ * 1. HTML stream → moveSuffix (captures </body></html>, re-emits at end)
303
+ * 2. → flightInjection (merges RSC <script> tags between HTML chunks)
304
+ * 3. → output (well-formed HTML with interleaved RSC data)
305
+ *
306
+ * The RSC stream is transformed into <script> tags via createInlinedRscStream
307
+ * (pull-based, no shared mutable state) and merged into the HTML pipeline
308
+ * via createFlightInjectionTransform.
309
+ *
310
+ * The client reads these script tags via `self.__timber_f` and feeds
311
+ * them to `createFromReadableStream` for progressive hydration.
312
+ * Stream completion is signaled by the DOMContentLoaded event on the
313
+ * client side — no custom done flag needed.
314
+ */
315
+ export function injectRscPayload(
316
+ htmlStream: ReadableStream<Uint8Array>,
317
+ rscStream: ReadableStream<Uint8Array> | undefined
318
+ ): ReadableStream<Uint8Array> {
319
+ if (!rscStream) return htmlStream;
320
+
321
+ // Transform RSC binary stream → stream of <script> tags
322
+ const rscScriptStream = createInlinedRscStream(rscStream);
323
+
324
+ // Single transform: strip </body></html>, inject RSC scripts at
325
+ // body level, re-emit suffix at the very end.
326
+ return htmlStream
327
+ .pipeThrough(createFlightInjectionTransform(rscScriptStream));
328
+ }
329
+
330
+ /**
331
+ * Client bootstrap configuration returned by buildClientScripts.
332
+ *
333
+ * - `bootstrapScriptContent`: Inline JS passed to React's renderToReadableStream
334
+ * as `bootstrapScriptContent`. React injects this as a non-deferred `<script>`
335
+ * in the shell HTML, so it executes immediately during parsing — even while
336
+ * Suspense boundaries are still streaming. Uses dynamic `import()` to kick off
337
+ * module loading, enabling hydration to start before the stream closes.
338
+ *
339
+ * - `preloadLinks`: `<link rel="modulepreload">` tags for production dependency
340
+ * preloading. Injected into `<head>` via injectHead so the browser starts
341
+ * downloading JS chunks early.
342
+ */
343
+ export interface ClientBootstrapConfig {
344
+ bootstrapScriptContent: string;
345
+ preloadLinks: string;
346
+ }
347
+
348
+ /** Find a manifest entry by matching the key suffix (e.g. 'client/browser-entry.ts'). */
349
+ function findManifestEntry(map: Record<string, string>, suffix: string): string | undefined {
350
+ for (const [key, value] of Object.entries(map)) {
351
+ if (key.endsWith(suffix)) return value;
352
+ }
353
+ return undefined;
354
+ }
355
+
356
+ /** Find a manifest array entry by matching the key suffix. */
357
+ function findManifestEntryArray(map: Record<string, string[]>, suffix: string): string[] | undefined {
358
+ for (const [key, value] of Object.entries(map)) {
359
+ if (key.endsWith(suffix)) return value;
360
+ }
361
+ return undefined;
362
+ }
363
+
364
+ /**
365
+ * Build client bootstrap configuration based on runtime config.
366
+ *
367
+ * Returns empty strings when client JavaScript is disabled,
368
+ * which produces zero-JS output. When `enableHMRInDev` is true and
369
+ * running in dev mode, injects only the Vite HMR client (no app
370
+ * bootstrap) so hot reloading works during development.
371
+ *
372
+ * In production, uses hashed chunk URLs from the build manifest.
373
+ *
374
+ * The bootstrap uses dynamic `import()` inside a regular (non-module)
375
+ * inline script so it executes immediately during HTML parsing. This
376
+ * is critical for streaming: `<script type="module">` is deferred
377
+ * until the document finishes parsing, which blocks hydration behind
378
+ * Suspense boundaries. Dynamic `import()` starts module loading and
379
+ * execution as soon as the shell HTML is parsed.
380
+ */
381
+ export function buildClientScripts(runtimeConfig: {
382
+ output: string;
383
+ clientJavascript: { disabled: boolean; enableHMRInDev: boolean };
384
+ dev: boolean;
385
+ buildManifest?: import('./build-manifest.js').BuildManifest;
386
+ }): ClientBootstrapConfig {
387
+ if (runtimeConfig.clientJavascript.disabled) {
388
+ // When client JS is disabled but enableHMRInDev is true in dev mode,
389
+ // inject only the Vite HMR client for hot reloading CSS, etc.
390
+ if (runtimeConfig.dev && runtimeConfig.clientJavascript.enableHMRInDev) {
391
+ return {
392
+ bootstrapScriptContent: 'import("/@vite/client")',
393
+ preloadLinks: '',
394
+ };
395
+ }
396
+ return { bootstrapScriptContent: '', preloadLinks: '' };
397
+ }
398
+
399
+ if (runtimeConfig.dev) {
400
+ // Dev mode: Vite HMR client + RSC virtual browser entry.
401
+ //
402
+ // We import virtual:vite-rsc/entry-browser (the RSC plugin's browser
403
+ // entry) instead of directly importing virtual:timber-browser-entry.
404
+ // The RSC entry sets up React Fast Refresh globals ($RefreshReg$,
405
+ // $RefreshSig$) BEFORE dynamically importing our browser entry
406
+ // (resolved via the `entries.client` option we pass to the RSC plugin).
407
+ // This ordering is critical — @vitejs/plugin-react's Babel transform
408
+ // injects preamble checks into client components that expect these
409
+ // globals to exist at module evaluation time.
410
+ //
411
+ // Dynamic import() ensures both scripts start loading immediately,
412
+ // not deferred until document parsing completes.
413
+ return {
414
+ bootstrapScriptContent:
415
+ 'import("/@vite/client");import("/@id/__x00__virtual:vite-rsc/entry-browser")',
416
+ preloadLinks: '',
417
+ };
418
+ }
419
+
420
+ // Production: resolve browser entry to hashed chunk URL from manifest.
421
+ // The manifest keys are facadeModuleIds — either root-relative paths or
422
+ // absolute paths (when the entry lives outside the project root, e.g. in
423
+ // a monorepo). Match by suffix to handle both cases.
424
+ const manifest = runtimeConfig.buildManifest;
425
+ const browserEntryUrl = manifest
426
+ ? findManifestEntry(manifest.js, 'client/browser-entry.ts')
427
+ : undefined;
428
+
429
+ let preloadLinks = '';
430
+ let bootstrapScriptContent: string;
431
+
432
+ if (browserEntryUrl) {
433
+ // Modulepreload hints for browser entry dependencies
434
+ const preloads = (manifest ? findManifestEntryArray(manifest.modulepreload, 'client/browser-entry.ts') : undefined) ?? [];
435
+ for (const url of preloads) {
436
+ preloadLinks += `<link rel="modulepreload" href="${url}">`;
437
+ }
438
+ bootstrapScriptContent = `import("${browserEntryUrl}")`;
439
+ } else {
440
+ // Fallback: no manifest entry (e.g. manifest not yet populated)
441
+ bootstrapScriptContent = 'import("/virtual:timber-browser-entry")';
442
+ }
443
+
444
+ return { bootstrapScriptContent, preloadLinks };
445
+ }
@@ -0,0 +1,222 @@
1
+ // @timber/app/server — Server-side primitives
2
+ // These are the primary imports for server components, middleware, and access files.
3
+
4
+ export type { AccessContext } from './types';
5
+ export type { MiddlewareContext } from './types';
6
+ export type { RouteContext } from './types';
7
+ export type { Metadata, MetadataRoute } from './types';
8
+
9
+ // Request Context — ALS-backed headers(), cookies(), and searchParams()
10
+ // Design doc: design/04-authorization.md §"AccessContext does not include cookies or headers"
11
+ // Design doc: design/23-search-params.md §"Server Integration"
12
+ export {
13
+ headers,
14
+ cookies,
15
+ searchParams,
16
+ setParsedSearchParams,
17
+ runWithRequestContext,
18
+ setMutableCookieContext,
19
+ markResponseFlushed,
20
+ getSetCookieHeaders,
21
+ setCookieSecrets,
22
+ } from './request-context';
23
+ export type { ReadonlyHeaders, RequestCookies, CookieOptions } from './request-context';
24
+
25
+ // Runtime primitives
26
+ export {
27
+ deny,
28
+ notFound,
29
+ redirect,
30
+ permanentRedirect,
31
+ redirectExternal,
32
+ RedirectType,
33
+ RenderError,
34
+ waitUntil,
35
+ DenySignal,
36
+ RedirectSignal,
37
+ } from './primitives';
38
+ export type { RenderErrorDigest, WaitUntilAdapter } from './primitives';
39
+
40
+ // Pipeline
41
+ export { createPipeline } from './pipeline';
42
+ export type {
43
+ PipelineConfig,
44
+ RouteMatch,
45
+ RouteMatcher,
46
+ RouteRenderer,
47
+ EarlyHintsEmitter,
48
+ } from './pipeline';
49
+
50
+ // Early Hints
51
+ export { collectEarlyHintHeaders, formatLinkHeader } from './early-hints';
52
+ export type { EarlyHint } from './early-hints';
53
+
54
+ // Early Hints 103 Sender — ALS bridge for platform adapters
55
+ export { runWithEarlyHintsSender, sendEarlyHints103 } from './early-hints-sender';
56
+ export type { EarlyHintsSenderFn } from './early-hints-sender';
57
+
58
+ // Canonicalization
59
+ export { canonicalize } from './canonicalize';
60
+ export type { CanonicalizeResult } from './canonicalize';
61
+
62
+ // Proxy
63
+ export { runProxy } from './proxy';
64
+ export type { ProxyFn, ProxyExport } from './proxy';
65
+
66
+ // Middleware
67
+ export { runMiddleware } from './middleware-runner';
68
+ export type { MiddlewareFn } from './middleware-runner';
69
+
70
+ // Tree Builder
71
+ export { buildElementTree } from './tree-builder';
72
+ export type {
73
+ TreeBuilderConfig,
74
+ TreeBuildResult,
75
+ LoadedModule,
76
+ ModuleLoader,
77
+ AccessGateProps,
78
+ SlotAccessGateProps,
79
+ ErrorBoundaryProps,
80
+ } from './tree-builder';
81
+
82
+ // Access Gates
83
+ export { AccessGate, SlotAccessGate } from './access-gate';
84
+
85
+ // Status-Code Resolver
86
+ export { resolveStatusFile, resolveSlotDenied } from './status-code-resolver';
87
+ export type {
88
+ StatusFileResolution,
89
+ StatusFileKind,
90
+ StatusFileFormat,
91
+ SlotDeniedResolution,
92
+ SlotDeniedKind,
93
+ } from './status-code-resolver';
94
+
95
+ // Flush Controller
96
+ export { flushResponse } from './flush';
97
+ export type { FlushOptions, FlushResult, RenderFn, RenderResult } from './flush';
98
+
99
+ // CSRF Protection
100
+ export { validateCsrf } from './csrf';
101
+ export type { CsrfConfig, CsrfResult } from './csrf';
102
+
103
+ // Body Limits
104
+ export { parseBodySize, enforceBodyLimits, DEFAULT_LIMITS } from './body-limits';
105
+ export type { BodyLimitsConfig, BodyLimitResult, BodyKind } from './body-limits';
106
+
107
+ // Metadata
108
+ export {
109
+ resolveMetadata,
110
+ resolveTitle,
111
+ resolveMetadataUrls,
112
+ renderMetadataToElements,
113
+ } from './metadata';
114
+ export type { SegmentMetadataEntry, ResolveMetadataOptions, HeadElement } from './metadata';
115
+
116
+ // Metadata Routes
117
+ export {
118
+ classifyMetadataRoute,
119
+ getMetadataRouteServePath,
120
+ getMetadataRouteAutoLink,
121
+ METADATA_ROUTE_CONVENTIONS,
122
+ } from './metadata-routes';
123
+ export type { MetadataRouteInfo, MetadataRouteType } from './metadata-routes';
124
+
125
+ // Server Actions
126
+ export { createActionClient, ActionError, validated } from './action-client';
127
+ export type {
128
+ ActionResult,
129
+ ActionFn,
130
+ ActionBuilder,
131
+ ActionBuilderWithSchema,
132
+ ActionContext,
133
+ ActionMiddleware,
134
+ ActionSchema,
135
+ ValidationErrors,
136
+ } from './action-client';
137
+
138
+ // FormData Preprocessing
139
+ export { parseFormData, coerce } from './form-data';
140
+
141
+ // Form Flash (no-JS error round-trip)
142
+ export { getFormFlash } from './form-flash';
143
+ export type { FormFlashData } from './form-flash';
144
+
145
+ // Revalidation
146
+ export {
147
+ revalidatePath,
148
+ revalidateTag,
149
+ executeAction,
150
+ buildNoJsResponse,
151
+ isRscActionRequest,
152
+ } from './actions';
153
+ export type {
154
+ RevalidateRenderer,
155
+ RevalidationState,
156
+ ActionHandlerConfig,
157
+ ActionHandlerResult,
158
+ } from './actions';
159
+
160
+ // Tracing — per-request trace ID via ALS
161
+ // Design doc: design/17-logging.md §"trace_id is Always Set"
162
+ export {
163
+ traceId,
164
+ spanId,
165
+ generateTraceId,
166
+ runWithTraceId,
167
+ replaceTraceId,
168
+ withSpan,
169
+ addSpanEvent,
170
+ } from './tracing';
171
+ export type { TraceStore } from './tracing';
172
+
173
+ // Logger — structured logging
174
+ // Design doc: design/17-logging.md §"Production Logging"
175
+ export { setLogger, getLogger } from './logger';
176
+ export {
177
+ logRequestCompleted,
178
+ logRequestReceived,
179
+ logSlowRequest,
180
+ logMiddlewareShortCircuit,
181
+ logMiddlewareError,
182
+ logRenderError,
183
+ logProxyError,
184
+ logWaitUntilUnsupported,
185
+ logWaitUntilRejected,
186
+ logSwrRefetchFailed,
187
+ logCacheMiss,
188
+ } from './logger';
189
+ export type { TimberLogger } from './logger';
190
+
191
+ // Instrumentation — instrumentation.ts file convention
192
+ // Design doc: design/17-logging.md §"instrumentation.ts"
193
+ export { loadInstrumentation, callOnRequestError, hasOnRequestError } from './instrumentation';
194
+ export type {
195
+ InstrumentationOnRequestError,
196
+ InstrumentationRequestInfo,
197
+ InstrumentationErrorContext,
198
+ } from './instrumentation';
199
+
200
+ // Dev Warnings — dev-mode misuse detection
201
+ // Design doc: design/21-dev-server.md §"Dev-Mode Warnings", design/11-platform.md §"Dev Mode"
202
+ export {
203
+ warnSuspenseWrappingChildren,
204
+ warnDenyInSuspense,
205
+ warnRedirectInSuspense,
206
+ warnRedirectInAccess,
207
+ warnStaticRequestApi,
208
+ warnCacheRequestProps,
209
+ warnSlowSlotWithoutSuspense,
210
+ setViteServer,
211
+ WarningId,
212
+ // Legacy aliases
213
+ warnDynamicApiInStaticBuild,
214
+ warnRedirectInSlotAccess,
215
+ warnDenyAfterFlush,
216
+ } from './dev-warnings';
217
+ export type { DevWarningConfig } from './dev-warnings';
218
+
219
+ // Route Handler — route.ts API endpoints
220
+ // Design doc: design/07-routing.md §"route.ts — API Endpoints"
221
+ export { handleRouteRequest, resolveAllowedMethods } from './route-handler';
222
+ export type { RouteModule, RouteHandler, HttpMethod } from './route-handler';