@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.
@@ -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;AACjD,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"}
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
- * <script>(self.__timber_f=self.__timber_f||[]).push([1,"escaped_chunk"])</script>
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;AA2EH;;;;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;AAkBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,GACpC,cAAc,CAAC,UAAU,CAAC,CA4B5B;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
+ {"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;AAexC;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CA+ClE;AAeD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,SAAS,GAChD,SAAS,CAwIX;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
+ {"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;AAExE,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
+ {"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;AAGxE,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,CAqLjF"}
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.35",
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
- // On streaming pages, the browser entry module may load before the RSC
236
- // payload scripts arrive. The <script id="_R_"> tag is in the shell (flushed
237
- // on onShellReady), but the RSC bootstrap script `(self.__timber_f=...).push([0])`
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
- * <script>(self.__timber_f=self.__timber_f||[]).push([1,"escaped_chunk"])</script>
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
- start(controller) {
150
- // Emit bootstrap signal tells the client that __timber_f is active
151
- const bootstrap = `<script>(self.__timber_f=self.__timber_f||[]).push(${htmlEscapeJsonString(JSON.stringify([0]))})</script>`;
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
- const escaped = htmlEscapeJsonString(JSON.stringify([1, decoded]));
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 escaped = htmlEscapeJsonString(JSON.stringify([1, decoded]));
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
- // Bootstrap script to emit after the first HTML chunk (but before
179
- // any RSC data chunks). Must come AFTER the doctype + <html> so
180
- // browsers don't enter Quirks Mode.
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
- // Emit bootstrap AFTER the first HTML chunk so the doctype and
212
- // <html> tag are the first bytes the browser sees. Then start
213
- // the pull loop to stream RSC data chunks.
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: headHtml + clientBootstrap.preloadLinks + segmentScript + paramsScript,
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,