@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.
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/adapters/cloudflare.ts +325 -0
- package/src/adapters/nitro.ts +366 -0
- package/src/adapters/types.ts +63 -0
- package/src/cache/index.ts +91 -0
- package/src/cache/redis-handler.ts +91 -0
- package/src/cache/register-cached-function.ts +99 -0
- package/src/cache/singleflight.ts +26 -0
- package/src/cache/stable-stringify.ts +21 -0
- package/src/cache/timber-cache.ts +116 -0
- package/src/cli.ts +201 -0
- package/src/client/browser-entry.ts +663 -0
- package/src/client/error-boundary.tsx +209 -0
- package/src/client/form.tsx +200 -0
- package/src/client/head.ts +61 -0
- package/src/client/history.ts +46 -0
- package/src/client/index.ts +60 -0
- package/src/client/link-navigate-interceptor.tsx +62 -0
- package/src/client/link-status-provider.tsx +40 -0
- package/src/client/link.tsx +310 -0
- package/src/client/nuqs-adapter.tsx +117 -0
- package/src/client/router-ref.ts +25 -0
- package/src/client/router.ts +563 -0
- package/src/client/segment-cache.ts +194 -0
- package/src/client/segment-context.ts +57 -0
- package/src/client/ssr-data.ts +95 -0
- package/src/client/types.ts +4 -0
- package/src/client/unload-guard.ts +34 -0
- package/src/client/use-cookie.ts +122 -0
- package/src/client/use-link-status.ts +46 -0
- package/src/client/use-navigation-pending.ts +47 -0
- package/src/client/use-params.ts +71 -0
- package/src/client/use-pathname.ts +43 -0
- package/src/client/use-query-states.ts +133 -0
- package/src/client/use-router.ts +77 -0
- package/src/client/use-search-params.ts +74 -0
- package/src/client/use-selected-layout-segment.ts +110 -0
- package/src/content/index.ts +13 -0
- package/src/cookies/define-cookie.ts +137 -0
- package/src/cookies/index.ts +9 -0
- package/src/fonts/ast.ts +359 -0
- package/src/fonts/css.ts +68 -0
- package/src/fonts/fallbacks.ts +248 -0
- package/src/fonts/google.ts +332 -0
- package/src/fonts/local.ts +177 -0
- package/src/fonts/types.ts +88 -0
- package/src/index.ts +413 -0
- package/src/plugins/adapter-build.ts +118 -0
- package/src/plugins/build-manifest.ts +323 -0
- package/src/plugins/build-report.ts +353 -0
- package/src/plugins/cache-transform.ts +199 -0
- package/src/plugins/chunks.ts +90 -0
- package/src/plugins/content.ts +136 -0
- package/src/plugins/dev-error-overlay.ts +230 -0
- package/src/plugins/dev-logs.ts +280 -0
- package/src/plugins/dev-server.ts +389 -0
- package/src/plugins/dynamic-transform.ts +161 -0
- package/src/plugins/entries.ts +207 -0
- package/src/plugins/fonts.ts +581 -0
- package/src/plugins/mdx.ts +179 -0
- package/src/plugins/react-prod.ts +56 -0
- package/src/plugins/routing.ts +419 -0
- package/src/plugins/server-action-exports.ts +220 -0
- package/src/plugins/server-bundle.ts +113 -0
- package/src/plugins/shims.ts +168 -0
- package/src/plugins/static-build.ts +207 -0
- package/src/routing/codegen.ts +396 -0
- package/src/routing/index.ts +14 -0
- package/src/routing/interception.ts +173 -0
- package/src/routing/scanner.ts +487 -0
- package/src/routing/status-file-lint.ts +114 -0
- package/src/routing/types.ts +100 -0
- package/src/search-params/analyze.ts +192 -0
- package/src/search-params/codecs.ts +153 -0
- package/src/search-params/create.ts +314 -0
- package/src/search-params/index.ts +23 -0
- package/src/search-params/registry.ts +31 -0
- package/src/server/access-gate.tsx +142 -0
- package/src/server/action-client.ts +473 -0
- package/src/server/action-handler.ts +325 -0
- package/src/server/actions.ts +236 -0
- package/src/server/asset-headers.ts +81 -0
- package/src/server/body-limits.ts +102 -0
- package/src/server/build-manifest.ts +234 -0
- package/src/server/canonicalize.ts +90 -0
- package/src/server/client-module-map.ts +58 -0
- package/src/server/csrf.ts +79 -0
- package/src/server/deny-renderer.ts +302 -0
- package/src/server/dev-logger.ts +419 -0
- package/src/server/dev-span-processor.ts +78 -0
- package/src/server/dev-warnings.ts +282 -0
- package/src/server/early-hints-sender.ts +55 -0
- package/src/server/early-hints.ts +142 -0
- package/src/server/error-boundary-wrapper.ts +69 -0
- package/src/server/error-formatter.ts +184 -0
- package/src/server/flush.ts +182 -0
- package/src/server/form-data.ts +176 -0
- package/src/server/form-flash.ts +93 -0
- package/src/server/html-injectors.ts +445 -0
- package/src/server/index.ts +222 -0
- package/src/server/instrumentation.ts +136 -0
- package/src/server/logger.ts +145 -0
- package/src/server/manifest-status-resolver.ts +215 -0
- package/src/server/metadata-render.ts +527 -0
- package/src/server/metadata-routes.ts +189 -0
- package/src/server/metadata.ts +263 -0
- package/src/server/middleware-runner.ts +32 -0
- package/src/server/nuqs-ssr-provider.tsx +63 -0
- package/src/server/pipeline.ts +555 -0
- package/src/server/prerender.ts +139 -0
- package/src/server/primitives.ts +264 -0
- package/src/server/proxy.ts +43 -0
- package/src/server/request-context.ts +554 -0
- package/src/server/route-element-builder.ts +395 -0
- package/src/server/route-handler.ts +153 -0
- package/src/server/route-matcher.ts +316 -0
- package/src/server/rsc-entry/api-handler.ts +112 -0
- package/src/server/rsc-entry/error-renderer.ts +177 -0
- package/src/server/rsc-entry/helpers.ts +147 -0
- package/src/server/rsc-entry/index.ts +688 -0
- package/src/server/rsc-entry/ssr-bridge.ts +18 -0
- package/src/server/slot-resolver.ts +359 -0
- package/src/server/ssr-entry.ts +161 -0
- package/src/server/ssr-render.ts +200 -0
- package/src/server/status-code-resolver.ts +282 -0
- package/src/server/tracing.ts +281 -0
- package/src/server/tree-builder.ts +354 -0
- package/src/server/types.ts +150 -0
- package/src/shims/font-google.ts +67 -0
- package/src/shims/headers.ts +11 -0
- package/src/shims/image.ts +48 -0
- package/src/shims/link.ts +9 -0
- package/src/shims/navigation-client.ts +52 -0
- package/src/shims/navigation.ts +31 -0
- package/src/shims/server-only-noop.js +5 -0
- package/src/utils/directive-parser.ts +529 -0
- package/src/utils/format.ts +10 -0
- 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';
|