@timber-js/app 0.2.0-alpha.35 → 0.2.0-alpha.36
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/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/flight-scripts.d.ts +39 -0
- package/dist/server/flight-scripts.d.ts.map +1 -0
- package/dist/server/html-injectors.d.ts +3 -9
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/browser-entry.ts +3 -12
- package/src/server/deny-renderer.ts +2 -1
- package/src/server/flight-scripts.ts +59 -0
- package/src/server/html-injectors.ts +8 -32
- package/src/server/node-stream-transforms.ts +8 -24
- package/src/server/rsc-entry/error-renderer.ts +2 -1
- package/src/server/rsc-entry/ssr-renderer.ts +9 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deny-renderer.d.ts","sourceRoot":"","sources":["../../src/server/deny-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAK7C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"deny-renderer.d.ts","sourceRoot":"","sources":["../../src/server/deny-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAK7C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAMjE,qDAAqD;AACrD,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IAC3C,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAED,iEAAiE;AACjE,MAAM,MAAM,mBAAmB,GAAG,MAAM;IACtC,QAAQ,EAAE,cAAc,CAAC;IACzB,QAAQ,EAAE,cAAc,CAAC;CAC1B,CAAC;AAEF,6DAA6D;AAC7D,MAAM,MAAM,SAAS,GAAG,CACtB,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,KACnB,OAAO,CAAC,QAAQ,CAAC,CAAC;AAUvB;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,WAAW,EAAE,EAC/B,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,eAAe,EAAE,qBAAqB,EACtC,sBAAsB,EAAE,mBAAmB,EAC3C,OAAO,EAAE,SAAS,GACjB,OAAO,CAAC,QAAQ,CAAC,CA6GnB;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,WAAW,EAAE,EAC/B,eAAe,EAAE,OAAO,EACxB,sBAAsB,EAAE,mBAAmB,GAC1C,OAAO,CAAC,QAAQ,CAAC,CAgDnB"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Flight data script generation.
|
|
3
|
+
*
|
|
4
|
+
* Both html-injectors.ts (Web Streams / dev) and node-stream-transforms.ts
|
|
5
|
+
* (Node streams / production) use this module to generate inline `<script>`
|
|
6
|
+
* tags for RSC Flight data injection.
|
|
7
|
+
*
|
|
8
|
+
* The init script goes in `<head>` as part of the HTML shell (guaranteed
|
|
9
|
+
* to execute before any streaming chunk scripts). Push scripts are emitted
|
|
10
|
+
* as RSC chunks arrive during streaming.
|
|
11
|
+
*
|
|
12
|
+
* See design/02-rendering-pipeline.md, LOCAL-415
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Escape a JSON string for safe embedding inside an HTML `<script>` tag.
|
|
16
|
+
*
|
|
17
|
+
* Prevents XSS via `</script>` injection and handles Unicode line/paragraph
|
|
18
|
+
* separators that are valid JSON but invalid in JS string literals (pre-ES2019).
|
|
19
|
+
*/
|
|
20
|
+
export declare function htmlEscapeJsonString(str: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Generate the init script that creates the Flight data array.
|
|
23
|
+
*
|
|
24
|
+
* This MUST be included in `<head>` (via headHtml) so it executes before
|
|
25
|
+
* any streaming chunk scripts arrive in `<body>`. The array is created
|
|
26
|
+
* unconditionally — no `||[]` guard needed in push scripts.
|
|
27
|
+
*
|
|
28
|
+
* Also emits the bootstrap signal [0] which tells the client that
|
|
29
|
+
* Flight data is active for this page.
|
|
30
|
+
*/
|
|
31
|
+
export declare function flightInitScript(): string;
|
|
32
|
+
/**
|
|
33
|
+
* Generate a push script for a Flight data chunk.
|
|
34
|
+
*
|
|
35
|
+
* The init script is guaranteed to have run (it's in `<head>`), so
|
|
36
|
+
* `self.__timber_f` always exists — no `||[]` guard needed.
|
|
37
|
+
*/
|
|
38
|
+
export declare function flightChunkScript(data: string): string;
|
|
39
|
+
//# sourceMappingURL=flight-scripts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flight-scripts.d.ts","sourceRoot":"","sources":["../../src/server/flight-scripts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMxD;AAOD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGtD"}
|
|
@@ -26,15 +26,9 @@ export declare function injectScripts(stream: ReadableStream<Uint8Array>, script
|
|
|
26
26
|
* transform) drives reads from the RSC stream on demand. No background
|
|
27
27
|
* reader, no shared mutable arrays, no race conditions.
|
|
28
28
|
*
|
|
29
|
-
* Each RSC chunk becomes
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* The first chunk emitted is the bootstrap signal [0] which the client
|
|
33
|
-
* uses to initialize its buffer.
|
|
34
|
-
*
|
|
35
|
-
* Uses JSON-encoded typed tuples matching the pattern from Next.js:
|
|
36
|
-
* [0] — bootstrap signal
|
|
37
|
-
* [1, data] — RSC Flight data chunk (UTF-8 string)
|
|
29
|
+
* Each RSC chunk becomes a `<script>self.__timber_f.push([1,"data"])</script>`.
|
|
30
|
+
* The init script (which creates __timber_f) is in `<head>` via
|
|
31
|
+
* flightInitScript() — see flight-scripts.ts.
|
|
38
32
|
*/
|
|
39
33
|
export declare function createInlinedRscStream(rscStream: ReadableStream<Uint8Array>): ReadableStream<Uint8Array>;
|
|
40
34
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html-injectors.d.ts","sourceRoot":"","sources":["../../src/server/html-injectors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"html-injectors.d.ts","sourceRoot":"","sources":["../../src/server/html-injectors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA4EH;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,QAAQ,EAAE,MAAM,GACf,cAAc,CAAC,UAAU,CAAC,CAE5B;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,WAAW,EAAE,MAAM,GAClB,cAAc,CAAC,UAAU,CAAC,CAE5B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,GACpC,cAAc,CAAC,UAAU,CAAC,CAyB5B;AAmKD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,cAAc,CAAC,UAAU,CAAC,EACtC,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,SAAS,GAChD,cAAc,CAAC,UAAU,CAAC,CAS5B;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,qBAAqB;IACpC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC;CACtB;AAqBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,OAAO,CAAA;KAAE,CAAC;IACjE,GAAG,EAAE,OAAO,CAAC;IACb,aAAa,CAAC,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC;CAC7D,GAAG,qBAAqB,CA8DxB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-stream-transforms.d.ts","sourceRoot":"","sources":["../../src/server/node-stream-transforms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"node-stream-transforms.d.ts","sourceRoot":"","sources":["../../src/server/node-stream-transforms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAgBxC;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CA+ClE;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,SAAS,GAChD,SAAS,CAkIX;AAOD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAwBtE;AAoBD;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CACtC,cAAc,EAAE,OAAO,EACvB,eAAe,EAAE,OAAO,GACvB,SAAS,GAAG,IAAI,CA8BlB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-renderer.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/error-renderer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"error-renderer.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/error-renderer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAGxE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAM7D;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,WAAW,EAAE,EAC/B,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,eAAe,EAAE,qBAAqB,GACrC,OAAO,CAAC,QAAQ,CAAC,CA6FnB;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,WAAW,EAAE,mBAAmB,EAChC,eAAe,EAAE,OAAO,EACxB,eAAe,EAAE,qBAAqB,GACrC,OAAO,CAAC,QAAQ,CAAC,CA6BnB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssr-renderer.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/ssr-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"ssr-renderer.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/ssr-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAIxE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAYrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGrD,UAAU,gBAAgB;IACxB,GAAG,EAAE,OAAO,CAAC;IACb,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACtC,OAAO,EAAE,aAAa,CAAC;IACvB,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,gBAAgB,EAAE,oBAAoB,EAAE,CAAC;IACzC,KAAK,EAAE,UAAU,CAAC;IAClB,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,EAAE,qBAAqB,CAAC;IACvC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC,CA4LjF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timber-js/app",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.36",
|
|
4
4
|
"description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare-workers",
|
|
@@ -232,18 +232,9 @@ function bootstrap(runtimeConfig: typeof config): void {
|
|
|
232
232
|
// For subsequent navigations, it's fetched from the server.
|
|
233
233
|
type FlightSegment = [isBootstrap: 0] | [isData: 1, data: string];
|
|
234
234
|
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
// is injected by the flight injector AFTER Suspense resolution scripts.
|
|
239
|
-
// If the module import resolves before those scripts execute, __timber_f
|
|
240
|
-
// will be undefined.
|
|
241
|
-
//
|
|
242
|
-
// Fix: if __timber_f isn't available yet, pre-initialize it so the RSC
|
|
243
|
-
// bootstrap script's `(self.__timber_f=self.__timber_f||[]).push([0])` finds
|
|
244
|
-
// our array and pushes into it. This avoids a race condition that causes
|
|
245
|
-
// the browser entry to fall through to createRoot() (no hydration) on
|
|
246
|
-
// streaming pages.
|
|
235
|
+
// __timber_f is initialized in <head> via flightInitScript() (see
|
|
236
|
+
// flight-scripts.ts). It should always exist by the time this module
|
|
237
|
+
// runs. Defensive fallback kept for safety.
|
|
247
238
|
if (!(self as unknown as Record<string, unknown>).__timber_f) {
|
|
248
239
|
(self as unknown as Record<string, FlightSegment[]>).__timber_f = [];
|
|
249
240
|
}
|
|
@@ -26,6 +26,7 @@ import { resolveManifestStatusFile } from './manifest-status-resolver.js';
|
|
|
26
26
|
import type { ManifestSegmentNode } from './route-matcher.js';
|
|
27
27
|
import type { RouteMatch } from './pipeline.js';
|
|
28
28
|
import type { NavContext } from './ssr-entry.js';
|
|
29
|
+
import { flightInitScript } from './flight-scripts.js';
|
|
29
30
|
import type { ClientBootstrapConfig } from './html-injectors.js';
|
|
30
31
|
import type { Metadata } from './types.js';
|
|
31
32
|
|
|
@@ -178,7 +179,7 @@ export async function renderDenyPage(
|
|
|
178
179
|
searchParams: Object.fromEntries(new URL(req.url).searchParams),
|
|
179
180
|
statusCode: deny.status,
|
|
180
181
|
responseHeaders,
|
|
181
|
-
headHtml,
|
|
182
|
+
headHtml: headHtml + flightInitScript(),
|
|
182
183
|
bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
|
|
183
184
|
rscStream: inlineStream,
|
|
184
185
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Flight data script generation.
|
|
3
|
+
*
|
|
4
|
+
* Both html-injectors.ts (Web Streams / dev) and node-stream-transforms.ts
|
|
5
|
+
* (Node streams / production) use this module to generate inline `<script>`
|
|
6
|
+
* tags for RSC Flight data injection.
|
|
7
|
+
*
|
|
8
|
+
* The init script goes in `<head>` as part of the HTML shell (guaranteed
|
|
9
|
+
* to execute before any streaming chunk scripts). Push scripts are emitted
|
|
10
|
+
* as RSC chunks arrive during streaming.
|
|
11
|
+
*
|
|
12
|
+
* See design/02-rendering-pipeline.md, LOCAL-415
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ─── JSON Escaping ────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Escape a JSON string for safe embedding inside an HTML `<script>` tag.
|
|
19
|
+
*
|
|
20
|
+
* Prevents XSS via `</script>` injection and handles Unicode line/paragraph
|
|
21
|
+
* separators that are valid JSON but invalid in JS string literals (pre-ES2019).
|
|
22
|
+
*/
|
|
23
|
+
export function htmlEscapeJsonString(str: string): string {
|
|
24
|
+
return str
|
|
25
|
+
.replace(/</g, '\\u003c')
|
|
26
|
+
.replace(/>/g, '\\u003e')
|
|
27
|
+
.replace(/\u2028/g, '\\u2028')
|
|
28
|
+
.replace(/\u2029/g, '\\u2029');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Script Generation ────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/** The global variable name used for Flight data on the client. */
|
|
34
|
+
const FLIGHT_VAR = 'self.__timber_f';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generate the init script that creates the Flight data array.
|
|
38
|
+
*
|
|
39
|
+
* This MUST be included in `<head>` (via headHtml) so it executes before
|
|
40
|
+
* any streaming chunk scripts arrive in `<body>`. The array is created
|
|
41
|
+
* unconditionally — no `||[]` guard needed in push scripts.
|
|
42
|
+
*
|
|
43
|
+
* Also emits the bootstrap signal [0] which tells the client that
|
|
44
|
+
* Flight data is active for this page.
|
|
45
|
+
*/
|
|
46
|
+
export function flightInitScript(): string {
|
|
47
|
+
return `<script>${FLIGHT_VAR}=[${htmlEscapeJsonString(JSON.stringify([0]))}]</script>`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generate a push script for a Flight data chunk.
|
|
52
|
+
*
|
|
53
|
+
* The init script is guaranteed to have run (it's in `<head>`), so
|
|
54
|
+
* `self.__timber_f` always exists — no `||[]` guard needed.
|
|
55
|
+
*/
|
|
56
|
+
export function flightChunkScript(data: string): string {
|
|
57
|
+
const escaped = htmlEscapeJsonString(JSON.stringify([1, data]));
|
|
58
|
+
return `<script>${FLIGHT_VAR}.push(${escaped})</script>`;
|
|
59
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { createMachine } from '../utils/state-machine.js';
|
|
11
|
+
import { flightChunkScript } from './flight-scripts.js';
|
|
11
12
|
import {
|
|
12
13
|
flightInjectionTransitions,
|
|
13
14
|
isSuffixStripped,
|
|
@@ -105,22 +106,6 @@ export function injectScripts(
|
|
|
105
106
|
return createInjector(stream, scriptsHtml, '</body>');
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
/**
|
|
109
|
-
* Escape a string for safe embedding inside a `<script>` tag within
|
|
110
|
-
* a JSON-encoded value.
|
|
111
|
-
*
|
|
112
|
-
* Only needs to prevent `</script>` from closing the tag early and
|
|
113
|
-
* handle U+2028/U+2029 (line/paragraph separators valid in JSON but
|
|
114
|
-
* historically problematic in JS). Since we use JSON.stringify for the
|
|
115
|
-
* outer encoding, we only escape `<` and the line separators.
|
|
116
|
-
*/
|
|
117
|
-
function htmlEscapeJsonString(str: string): string {
|
|
118
|
-
return str
|
|
119
|
-
.replace(/</g, '\\u003c')
|
|
120
|
-
.replace(/\u2028/g, '\\u2028')
|
|
121
|
-
.replace(/\u2029/g, '\\u2029');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
109
|
/**
|
|
125
110
|
* Transform an RSC Flight stream into a stream of inline `<script>` tags.
|
|
126
111
|
*
|
|
@@ -128,15 +113,9 @@ function htmlEscapeJsonString(str: string): string {
|
|
|
128
113
|
* transform) drives reads from the RSC stream on demand. No background
|
|
129
114
|
* reader, no shared mutable arrays, no race conditions.
|
|
130
115
|
*
|
|
131
|
-
* Each RSC chunk becomes
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* The first chunk emitted is the bootstrap signal [0] which the client
|
|
135
|
-
* uses to initialize its buffer.
|
|
136
|
-
*
|
|
137
|
-
* Uses JSON-encoded typed tuples matching the pattern from Next.js:
|
|
138
|
-
* [0] — bootstrap signal
|
|
139
|
-
* [1, data] — RSC Flight data chunk (UTF-8 string)
|
|
116
|
+
* Each RSC chunk becomes a `<script>self.__timber_f.push([1,"data"])</script>`.
|
|
117
|
+
* The init script (which creates __timber_f) is in `<head>` via
|
|
118
|
+
* flightInitScript() — see flight-scripts.ts.
|
|
140
119
|
*/
|
|
141
120
|
export function createInlinedRscStream(
|
|
142
121
|
rscStream: ReadableStream<Uint8Array>
|
|
@@ -146,11 +125,9 @@ export function createInlinedRscStream(
|
|
|
146
125
|
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
147
126
|
|
|
148
127
|
return new ReadableStream<Uint8Array>({
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
controller.enqueue(encoder.encode(bootstrap));
|
|
153
|
-
},
|
|
128
|
+
// No bootstrap signal here — the init script is in <head> via
|
|
129
|
+
// flightInitScript() (see flight-scripts.ts). This ensures the
|
|
130
|
+
// __timber_f array exists before any chunk scripts execute.
|
|
154
131
|
async pull(controller) {
|
|
155
132
|
try {
|
|
156
133
|
const { done, value } = await rscReader.read();
|
|
@@ -160,8 +137,7 @@ export function createInlinedRscStream(
|
|
|
160
137
|
}
|
|
161
138
|
if (value) {
|
|
162
139
|
const decoded = decoder.decode(value, { stream: true });
|
|
163
|
-
|
|
164
|
-
controller.enqueue(encoder.encode(`<script>self.__timber_f.push(${escaped})</script>`));
|
|
140
|
+
controller.enqueue(encoder.encode(flightChunkScript(decoded)));
|
|
165
141
|
}
|
|
166
142
|
} catch (error) {
|
|
167
143
|
controller.error(error);
|
|
@@ -21,6 +21,7 @@ import { Transform } from 'node:stream';
|
|
|
21
21
|
import { createGzip, constants } from 'node:zlib';
|
|
22
22
|
|
|
23
23
|
import { createMachine } from '../utils/state-machine.js';
|
|
24
|
+
import { flightChunkScript } from './flight-scripts.js';
|
|
24
25
|
import {
|
|
25
26
|
flightInjectionTransitions,
|
|
26
27
|
isSuffixStripped,
|
|
@@ -90,17 +91,6 @@ export function createNodeHeadInjector(headHtml: string): Transform {
|
|
|
90
91
|
|
|
91
92
|
// ─── RSC Flight Injection ────────────────────────────────────────────────────
|
|
92
93
|
|
|
93
|
-
/**
|
|
94
|
-
* Escape a string for safe embedding inside a `<script>` tag within
|
|
95
|
-
* a JSON-encoded value. Same as htmlEscapeJsonString in html-injectors.ts.
|
|
96
|
-
*/
|
|
97
|
-
function htmlEscapeJsonString(str: string): string {
|
|
98
|
-
return str
|
|
99
|
-
.replace(/</g, '\\u003c')
|
|
100
|
-
.replace(/\u2028/g, '\\u2028')
|
|
101
|
-
.replace(/\u2029/g, '\\u2029');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
94
|
/**
|
|
105
95
|
* Node.js Transform that merges RSC script tags into the HTML stream.
|
|
106
96
|
*
|
|
@@ -157,8 +147,7 @@ export function createNodeFlightInjector(
|
|
|
157
147
|
return;
|
|
158
148
|
}
|
|
159
149
|
const decoded = decoder.decode(value, { stream: true });
|
|
160
|
-
const
|
|
161
|
-
const scriptBuf = Buffer.from(`<script>self.__timber_f.push(${escaped})</script>`, 'utf-8');
|
|
150
|
+
const scriptBuf = Buffer.from(flightChunkScript(decoded), 'utf-8');
|
|
162
151
|
// Push directly to the transform output — don't wait for an
|
|
163
152
|
// HTML chunk to trigger drainPending.
|
|
164
153
|
stream.push(scriptBuf);
|
|
@@ -175,13 +164,9 @@ export function createNodeFlightInjector(
|
|
|
175
164
|
}
|
|
176
165
|
}
|
|
177
166
|
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
const bootstrapBuf = Buffer.from(
|
|
182
|
-
`<script>(self.__timber_f=self.__timber_f||[]).push(${htmlEscapeJsonString(JSON.stringify([0]))})</script>`,
|
|
183
|
-
'utf-8'
|
|
184
|
-
);
|
|
167
|
+
// No bootstrap script here — the init script is in <head> via
|
|
168
|
+
// flightInitScript() (see flight-scripts.ts). This ensures __timber_f
|
|
169
|
+
// exists before any chunk scripts execute.
|
|
185
170
|
|
|
186
171
|
const transform = new Transform({
|
|
187
172
|
transform(chunk: Buffer, _encoding, callback) {
|
|
@@ -208,11 +193,10 @@ export function createNodeFlightInjector(
|
|
|
208
193
|
transform.push(chunk);
|
|
209
194
|
}
|
|
210
195
|
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
196
|
+
// Start the pull loop on the first HTML chunk to stream RSC
|
|
197
|
+
// data chunks alongside the HTML. The __timber_f init script is
|
|
198
|
+
// already in <head> (via flightInitScript), so no bootstrap needed.
|
|
214
199
|
if (isFirst) {
|
|
215
|
-
transform.push(bootstrapBuf);
|
|
216
200
|
pullLoop(transform);
|
|
217
201
|
}
|
|
218
202
|
callback();
|
|
@@ -12,6 +12,7 @@ import { logRenderError } from '#/server/logger.js';
|
|
|
12
12
|
import type { ManifestSegmentNode } from '#/server/route-matcher.js';
|
|
13
13
|
import { DenySignal, RenderError } from '#/server/primitives.js';
|
|
14
14
|
import type { ClientBootstrapConfig } from '#/server/html-injectors.js';
|
|
15
|
+
import { flightInitScript } from '#/server/flight-scripts.js';
|
|
15
16
|
import { renderDenyPage } from '#/server/deny-renderer.js';
|
|
16
17
|
import type { LayoutEntry } from '#/server/deny-renderer.js';
|
|
17
18
|
import type { NavContext } from '#/server/ssr-entry.js';
|
|
@@ -125,7 +126,7 @@ export async function renderErrorPage(
|
|
|
125
126
|
searchParams: Object.fromEntries(new URL(req.url).searchParams),
|
|
126
127
|
statusCode: status,
|
|
127
128
|
responseHeaders,
|
|
128
|
-
headHtml:
|
|
129
|
+
headHtml: flightInitScript(),
|
|
129
130
|
bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
|
|
130
131
|
rscStream: inlineStream,
|
|
131
132
|
cookies: getCookiesForSsr(),
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { ClientBootstrapConfig } from '#/server/html-injectors.js';
|
|
16
|
+
import { flightInitScript } from '#/server/flight-scripts.js';
|
|
16
17
|
import type { LayoutEntry } from '#/server/deny-renderer.js';
|
|
17
18
|
import { renderDenyPage } from '#/server/deny-renderer.js';
|
|
18
19
|
import type { RouteMatch } from '#/server/pipeline.js';
|
|
@@ -105,7 +106,14 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
|
|
|
105
106
|
searchParams: Object.fromEntries(new URL(req.url).searchParams),
|
|
106
107
|
statusCode: 200,
|
|
107
108
|
responseHeaders,
|
|
108
|
-
headHtml:
|
|
109
|
+
headHtml:
|
|
110
|
+
headHtml +
|
|
111
|
+
clientBootstrap.preloadLinks +
|
|
112
|
+
segmentScript +
|
|
113
|
+
paramsScript +
|
|
114
|
+
// Initialize __timber_f in <head> so it exists before any streaming
|
|
115
|
+
// chunk scripts arrive in <body>. See flight-scripts.ts, LOCAL-415.
|
|
116
|
+
(clientJsDisabled ? '' : flightInitScript()),
|
|
109
117
|
bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
|
|
110
118
|
// Skip RSC inline stream when client JS is disabled — no client to hydrate.
|
|
111
119
|
rscStream: clientJsDisabled ? undefined : inlineStream,
|