@timber-js/app 0.2.0-alpha.183 → 0.2.0-alpha.184
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/_chunks/{build-output-helper-BC-Zg0_w.js → build-output-helper-DOGYFb_X.js} +11 -3
- package/dist/_chunks/build-output-helper-DOGYFb_X.js.map +1 -0
- package/dist/_chunks/{cloudflare-nlD9KhDj.js → cloudflare-CnT5Lr7U.js} +2 -2
- package/dist/_chunks/{cloudflare-nlD9KhDj.js.map → cloudflare-CnT5Lr7U.js.map} +1 -1
- package/dist/_chunks/plugin-context---kTF5v8.js.map +1 -1
- package/dist/adapters/build-output-helper.d.ts.map +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/nitro.js +1 -1
- package/dist/adapters/shared.d.ts.map +1 -1
- package/dist/client/internal.js +44 -4
- package/dist/client/internal.js.map +1 -1
- package/dist/client/rsc-fetch.d.ts +7 -0
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/index.js +113 -4
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +6 -0
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +29 -0
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapters/build-output-helper.ts +6 -1
- package/src/adapters/shared.ts +5 -2
- package/src/client/rsc-fetch.ts +61 -6
- package/src/plugin-context.ts +6 -0
- package/src/plugins/static-build.ts +128 -1
- package/dist/_chunks/build-output-helper-BC-Zg0_w.js.map +0 -1
package/dist/plugin-context.d.ts
CHANGED
|
@@ -137,6 +137,12 @@ export interface PluginContext {
|
|
|
137
137
|
* write per-path Content-Type rules in _headers. See TIM-1241.
|
|
138
138
|
*/
|
|
139
139
|
staticContentTypes?: Record<string, string>;
|
|
140
|
+
/**
|
|
141
|
+
* RSC flight payload manifest mapping unhashed → hashed URLs.
|
|
142
|
+
* Populated during static generation, consumed by adapters to set
|
|
143
|
+
* immutable cache headers for `_rsc/` files. See TIM-1254.
|
|
144
|
+
*/
|
|
145
|
+
rscManifest?: Record<string, string>;
|
|
140
146
|
}
|
|
141
147
|
/**
|
|
142
148
|
* Content-keyed LRU memo for `parseAst`. Transforms for the same module run
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-context.d.ts","sourceRoot":"","sources":["../src/plugin-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAE,gBAAgB,EAA0B,MAAM,mBAAmB,CAAC;AAClF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAG7D,YAAY,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAIlF,qEAAqE;AACrE,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,gBAAgB,GAAG,wBAAwB,CAc1F;AAID;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,gBAAgB,CAAC;IACzB,+CAA+C;IAC/C,gBAAgB,EAAE,wBAAwB,CAAC;IAC3C,uFAAuF;IACvF,SAAS,EAAE,SAAS,GAAG,IAAI,CAAC;IAC5B,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,oFAAoF;IACpF,GAAG,EAAE,OAAO,CAAC;IACb,gFAAgF;IAChF,aAAa,EAAE,aAAa,GAAG,IAAI,CAAC;IACpC,uEAAuE;IACvE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,mFAAmF;IACnF,KAAK,EAAE,YAAY,CAAC;IACpB,0FAA0F;IAC1F,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC;;;;;;;;OAQG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9C;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC5C;;;;;OAKG;IACH,wBAAwB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACjD;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACxD;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IAC/D;;;;;OAKG;IACH,uBAAuB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAChD;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAClC;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,oGAAoG;IACpG,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin-context.d.ts","sourceRoot":"","sources":["../src/plugin-context.ts"],"names":[],"mappings":"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"}
|
|
@@ -38,6 +38,35 @@ export declare function generateRedirectsFile(redirects: StaticRedirectEntry[]):
|
|
|
38
38
|
* API: /feed.xml → feed.xml, /api/data → api/data
|
|
39
39
|
*/
|
|
40
40
|
export declare function staticOutputPath(entry: StaticPageEntry): string;
|
|
41
|
+
/**
|
|
42
|
+
* Compute a short content hash for immutable caching.
|
|
43
|
+
* Uses 8 chars of base64url-encoded SHA-256, matching Vite's asset hash style.
|
|
44
|
+
*/
|
|
45
|
+
export declare function contentHash(data: Buffer | Uint8Array): string;
|
|
46
|
+
/**
|
|
47
|
+
* Hash all RSC flight payloads and params sidecars in `_rsc/`, rename
|
|
48
|
+
* them with content hashes, and inject a manifest into each HTML file.
|
|
49
|
+
*
|
|
50
|
+
* Before: `_rsc/index.rsc`, `_rsc/about.rsc`
|
|
51
|
+
* After: `_rsc/index-B7YxEKdN.rsc`, `_rsc/about-C8ZzFLfO.rsc`
|
|
52
|
+
*
|
|
53
|
+
* The manifest maps the original `_rsc/` URL to the hashed filename:
|
|
54
|
+
* `{"/_rsc/index.rsc":"/_rsc/index-B7YxEKdN.rsc", ...}`
|
|
55
|
+
*
|
|
56
|
+
* Injected into HTML as:
|
|
57
|
+
* `<script>window.__TIMBER_RSC_MANIFEST__={...}</script>`
|
|
58
|
+
*
|
|
59
|
+
* @internal Exported for testing.
|
|
60
|
+
*/
|
|
61
|
+
export declare function hashRscPayloads(staticDir: string): Promise<Record<string, string>>;
|
|
62
|
+
/**
|
|
63
|
+
* Inject the RSC manifest into all HTML files as a `<script>` tag
|
|
64
|
+
* before `</head>`. This lets the client resolve hashed RSC URLs
|
|
65
|
+
* without an extra network request.
|
|
66
|
+
*
|
|
67
|
+
* @internal Exported for testing.
|
|
68
|
+
*/
|
|
69
|
+
export declare function injectRscManifestIntoHtml(staticDir: string, manifest: Record<string, string>): Promise<void>;
|
|
41
70
|
/**
|
|
42
71
|
* Read a user-authored `_redirects` file from the project's `public/`
|
|
43
72
|
* directory. Returns `null` if no file exists.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"static-build.d.ts","sourceRoot":"","sources":["../../src/plugins/static-build.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"static-build.d.ts","sourceRoot":"","sources":["../../src/plugins/static-build.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAMnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAGV,eAAe,EACf,mBAAmB,EACpB,MAAM,+BAA+B,CAAC;AAQvC,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAmBD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,qBAAqB,EAAE,CA2BvF;AAMD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,mBAAmB,EAAE,GAAG,MAAM,CAG9E;AAaD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAsB/D;AAkLD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAE7D;AAwBD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAwBxF;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC7C,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,OAAO,CAAC,IAAI,CAAC,CAkBf;AAMD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAU5E;AAMD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CA8D5D"}
|
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.184",
|
|
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",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// IMPORTANT: This module must remain a leaf — no imports from ../server/,
|
|
12
12
|
// ../client/, or Vite-dependent code. See adapters/shared.ts header.
|
|
13
13
|
|
|
14
|
-
import { writeFile, mkdir, cp, readdir, readFile } from 'node:fs/promises';
|
|
14
|
+
import { writeFile, mkdir, cp, rm, readdir, readFile } from 'node:fs/promises';
|
|
15
15
|
import { join, basename, relative } from 'node:path';
|
|
16
16
|
import { generateHeadersFile } from './shared.js';
|
|
17
17
|
import type { TimberConfig } from './types';
|
|
@@ -141,6 +141,7 @@ export async function findUnhashedStaticFiles(publicDir: string): Promise<string
|
|
|
141
141
|
for (const file of files) {
|
|
142
142
|
const rel = relative(publicDir, file).replace(/\\/g, '/');
|
|
143
143
|
if (rel.startsWith('assets/') || rel.startsWith('_timber/')) continue;
|
|
144
|
+
if (rel.startsWith('rsc/') || rel.startsWith('ssr/') || rel.startsWith('_rsc/')) continue;
|
|
144
145
|
if (basename(rel).startsWith('_')) continue;
|
|
145
146
|
const urlPath = '/' + rel;
|
|
146
147
|
if (PRIORITY_FILES.has(urlPath)) {
|
|
@@ -195,6 +196,10 @@ export async function buildStaticDirectoryOutput(
|
|
|
195
196
|
outDir: string,
|
|
196
197
|
publicDirName: string
|
|
197
198
|
): Promise<void> {
|
|
199
|
+
// Clean stale artifacts from prior builds (e.g. server-mode builds that
|
|
200
|
+
// left rsc/, ssr/, _worker.js in the outDir). Without this, a static
|
|
201
|
+
// build after a server build inherits 140+ unused server chunks.
|
|
202
|
+
await rm(outDir, { recursive: true, force: true }).catch(() => {});
|
|
198
203
|
await mkdir(outDir, { recursive: true });
|
|
199
204
|
|
|
200
205
|
const publicDir = join(outDir, publicDirName);
|
package/src/adapters/shared.ts
CHANGED
|
@@ -35,8 +35,8 @@ export const STATIC_CACHE = 'public, max-age=3600, must-revalidate';
|
|
|
35
35
|
* (invalid per RFC 9111).
|
|
36
36
|
*/
|
|
37
37
|
// Cloudflare Workers Static Assets caps _headers at 100 rules.
|
|
38
|
-
// Reserve
|
|
39
|
-
const MAX_PER_FILE_RULES =
|
|
38
|
+
// Reserve 3 for the wildcard hashed-asset rules (/assets/*, /_timber/fonts/*, /_rsc/*).
|
|
39
|
+
const MAX_PER_FILE_RULES = 97;
|
|
40
40
|
|
|
41
41
|
export function generateHeadersFile(
|
|
42
42
|
unhashedPaths?: string[],
|
|
@@ -50,6 +50,9 @@ export function generateHeadersFile(
|
|
|
50
50
|
|
|
51
51
|
/_timber/fonts/*
|
|
52
52
|
Cache-Control: ${IMMUTABLE_CACHE}
|
|
53
|
+
|
|
54
|
+
/_rsc/*
|
|
55
|
+
Cache-Control: ${IMMUTABLE_CACHE}
|
|
53
56
|
`;
|
|
54
57
|
|
|
55
58
|
// Content-Type rules are higher priority than cache headers for
|
package/src/client/rsc-fetch.ts
CHANGED
|
@@ -106,15 +106,47 @@ export function isStaticMode(): boolean {
|
|
|
106
106
|
return staticMode;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* RSC manifest mapping unhashed → hashed URLs. Populated from
|
|
111
|
+
* `window.__TIMBER_RSC_MANIFEST__` (injected into HTML during
|
|
112
|
+
* static generation). See TIM-1254.
|
|
113
|
+
*/
|
|
114
|
+
let rscManifest: Record<string, string> | null = null;
|
|
115
|
+
|
|
116
|
+
function getRscManifest(): Record<string, string> | null {
|
|
117
|
+
if (rscManifest) return rscManifest;
|
|
118
|
+
if (typeof window !== 'undefined' && (window as any).__TIMBER_RSC_MANIFEST__) {
|
|
119
|
+
rscManifest = (window as any).__TIMBER_RSC_MANIFEST__;
|
|
120
|
+
}
|
|
121
|
+
return rscManifest;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** @internal Exported for testing. */
|
|
125
|
+
export function setRscManifest(manifest: Record<string, string> | null): void {
|
|
126
|
+
rscManifest = manifest;
|
|
127
|
+
}
|
|
128
|
+
|
|
109
129
|
/**
|
|
110
130
|
* Convert a route URL to the corresponding _rsc/*.rsc file path.
|
|
111
131
|
* Mirrors the naming in plugins/static-build.ts staticOutputPath.
|
|
112
132
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
133
|
+
* When an RSC manifest is available (hashed filenames from TIM-1254),
|
|
134
|
+
* the manifest is consulted to resolve to the hashed path.
|
|
135
|
+
*
|
|
136
|
+
* / → /_rsc/index.rsc (or /_rsc/index-B7YxEKdN.rsc with manifest)
|
|
137
|
+
* /about → /_rsc/about.rsc (or /_rsc/about-C8ZzFLfO.rsc with manifest)
|
|
115
138
|
* /blog/hello → /_rsc/blog/hello.rsc
|
|
116
139
|
*/
|
|
117
140
|
function toStaticRscUrl(url: string): string {
|
|
141
|
+
const unhashed = toUnhashedRscUrl(url);
|
|
142
|
+
return manifestLookup(unhashed) ?? unhashed;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Compute the unhashed _rsc/*.rsc URL for a route path.
|
|
147
|
+
* @internal Exported for testing.
|
|
148
|
+
*/
|
|
149
|
+
export function toUnhashedRscUrl(url: string): string {
|
|
118
150
|
const hashIndex = url.indexOf('#');
|
|
119
151
|
const queryIndex = url.indexOf('?');
|
|
120
152
|
const hashEnd = hashIndex === -1 ? url.length : hashIndex;
|
|
@@ -133,9 +165,29 @@ function toStaticRscUrl(url: string): string {
|
|
|
133
165
|
* Convert a route URL to the corresponding _rsc/*.params.json sidecar path.
|
|
134
166
|
* Used in static mode to fetch route params that are normally carried
|
|
135
167
|
* in the X-Timber-Params response header. See TIM-1246.
|
|
168
|
+
*
|
|
169
|
+
* Consults the RSC manifest for hashed filenames (TIM-1254).
|
|
170
|
+
*/
|
|
171
|
+
/**
|
|
172
|
+
* Look up a key in the RSC manifest, falling back to percent-decoded
|
|
173
|
+
* lookup for encoded browser URLs. Returns null if not found.
|
|
136
174
|
*/
|
|
175
|
+
function manifestLookup(key: string): string | null {
|
|
176
|
+
const manifest = getRscManifest();
|
|
177
|
+
if (!manifest) return null;
|
|
178
|
+
if (manifest[key]) return manifest[key];
|
|
179
|
+
try {
|
|
180
|
+
const decoded = decodeURIComponent(key);
|
|
181
|
+
if (decoded !== key && manifest[decoded]) return manifest[decoded];
|
|
182
|
+
} catch {
|
|
183
|
+
// Malformed percent sequence (e.g. /100%.rsc) — skip decoded lookup
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
137
188
|
function toStaticParamsUrl(url: string): string {
|
|
138
|
-
|
|
189
|
+
const unhashed = toUnhashedRscUrl(url).replace(/\.rsc$/, '.params.json');
|
|
190
|
+
return manifestLookup(unhashed) ?? unhashed;
|
|
139
191
|
}
|
|
140
192
|
|
|
141
193
|
// ─── Reload Signal ───────────────────────────────────────────────
|
|
@@ -378,10 +430,13 @@ export async function fetchRscPayload(
|
|
|
378
430
|
): Promise<FetchResult> {
|
|
379
431
|
// In static mode, fetch the pre-generated _rsc/*.rsc file directly
|
|
380
432
|
// instead of the route URL with Accept headers. Static hosts ignore
|
|
381
|
-
// Accept headers, so the route URL would return HTML.
|
|
382
|
-
// is still appended for cache busting.
|
|
433
|
+
// Accept headers, so the route URL would return HTML.
|
|
383
434
|
const fetchTarget = staticMode ? toStaticRscUrl(url) : url;
|
|
384
|
-
|
|
435
|
+
// Skip the cache-bust param when the manifest supplies a content-hashed
|
|
436
|
+
// URL — the hash itself guarantees freshness, and the param would defeat
|
|
437
|
+
// the immutable cache headers on /_rsc/* (TIM-1254).
|
|
438
|
+
const isHashedFromManifest = staticMode && fetchTarget !== toUnhashedRscUrl(url);
|
|
439
|
+
const rscUrl = isHashedFromManifest ? fetchTarget : appendRscParam(fetchTarget);
|
|
385
440
|
const headers = buildRscHeaders(staticMode ? undefined : stateTree, currentUrl);
|
|
386
441
|
if (deps.decodeRsc) {
|
|
387
442
|
// Production path: use createFromFetch for streaming RSC decoding.
|
package/src/plugin-context.ts
CHANGED
|
@@ -166,6 +166,12 @@ export interface PluginContext {
|
|
|
166
166
|
* write per-path Content-Type rules in _headers. See TIM-1241.
|
|
167
167
|
*/
|
|
168
168
|
staticContentTypes?: Record<string, string>;
|
|
169
|
+
/**
|
|
170
|
+
* RSC flight payload manifest mapping unhashed → hashed URLs.
|
|
171
|
+
* Populated during static generation, consumed by adapters to set
|
|
172
|
+
* immutable cache headers for `_rsc/` files. See TIM-1254.
|
|
173
|
+
*/
|
|
174
|
+
rscManifest?: Record<string, string>;
|
|
169
175
|
}
|
|
170
176
|
|
|
171
177
|
// ── AST parse memo ───────────────────────────────────────────────────────
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Plugin } from 'vite';
|
|
12
12
|
import { existsSync } from 'node:fs';
|
|
13
|
-
import { mkdir, readFile, writeFile, rename } from 'node:fs/promises';
|
|
13
|
+
import { mkdir, readdir, readFile, writeFile, rename, rm } from 'node:fs/promises';
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
14
15
|
import { dirname, extname, join, resolve, relative } from 'node:path';
|
|
15
16
|
import { pathToFileURL } from 'node:url';
|
|
16
17
|
import type { PluginContext } from '../plugin-context.js';
|
|
@@ -21,6 +22,7 @@ import type {
|
|
|
21
22
|
StaticRedirectEntry,
|
|
22
23
|
} from '../server/static-generator.js';
|
|
23
24
|
import { mergeRscAssetsManifest } from './adapter-build.js';
|
|
25
|
+
import { htmlEscapeJsonString } from '../server/flight-scripts.js';
|
|
24
26
|
|
|
25
27
|
// ---------------------------------------------------------------------------
|
|
26
28
|
// Types
|
|
@@ -207,6 +209,10 @@ async function runStaticGeneration(ctx: PluginContext): Promise<StaticGeneration
|
|
|
207
209
|
}
|
|
208
210
|
|
|
209
211
|
const staticDir = join(ctx.buildDir, 'static-pages');
|
|
212
|
+
// Clean stale RSC artifacts from previous builds before generating
|
|
213
|
+
// new ones. Without this, already-hashed files from a prior build
|
|
214
|
+
// would get re-hashed and accumulate in the output.
|
|
215
|
+
await rm(join(staticDir, '_rsc'), { recursive: true, force: true }).catch(() => {});
|
|
210
216
|
await mkdir(staticDir, { recursive: true });
|
|
211
217
|
|
|
212
218
|
const sitemapConfig = ctx.config.sitemap;
|
|
@@ -269,6 +275,22 @@ async function runStaticGeneration(ctx: PluginContext): Promise<StaticGeneration
|
|
|
269
275
|
ctx.staticContentTypes = contentTypes;
|
|
270
276
|
}
|
|
271
277
|
|
|
278
|
+
// Hash RSC flight payloads for immutable caching (TIM-1254).
|
|
279
|
+
// Renames _rsc/*.rsc → _rsc/*-<hash>.rsc and injects a manifest
|
|
280
|
+
// into each HTML file so the client can resolve hashed URLs.
|
|
281
|
+
// Skip when client JS is disabled — no SPA navigation, no RSC fetches,
|
|
282
|
+
// and injecting a <script> tag would violate the no-JS contract.
|
|
283
|
+
if (!ctx.clientJavascript.disabled) {
|
|
284
|
+
const rscManifest = await hashRscPayloads(staticDir);
|
|
285
|
+
if (Object.keys(rscManifest).length > 0) {
|
|
286
|
+
await injectRscManifestIntoHtml(staticDir, rscManifest);
|
|
287
|
+
ctx.rscManifest = rscManifest;
|
|
288
|
+
console.log(
|
|
289
|
+
`[timber] Static generation: hashed ${Object.keys(rscManifest).length} RSC flight payloads`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
272
294
|
// Write _redirects file for platform redirect rules (Cloudflare Pages,
|
|
273
295
|
// Netlify, etc.). Format: /from /to status. See TIM-1245.
|
|
274
296
|
if (summary.redirects.length > 0) {
|
|
@@ -294,6 +316,111 @@ async function runStaticGeneration(ctx: PluginContext): Promise<StaticGeneration
|
|
|
294
316
|
return summary;
|
|
295
317
|
}
|
|
296
318
|
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// RSC flight payload hashing (TIM-1254)
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Compute a short content hash for immutable caching.
|
|
325
|
+
* Uses 8 chars of base64url-encoded SHA-256, matching Vite's asset hash style.
|
|
326
|
+
*/
|
|
327
|
+
export function contentHash(data: Buffer | Uint8Array): string {
|
|
328
|
+
return createHash('sha256').update(data).digest('base64url').slice(0, 8);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Walk a directory recursively and return all file paths.
|
|
333
|
+
*/
|
|
334
|
+
async function walkDir(dir: string): Promise<string[]> {
|
|
335
|
+
const results: string[] = [];
|
|
336
|
+
let entries;
|
|
337
|
+
try {
|
|
338
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
339
|
+
} catch {
|
|
340
|
+
return results;
|
|
341
|
+
}
|
|
342
|
+
for (const entry of entries) {
|
|
343
|
+
const full = join(dir, entry.name);
|
|
344
|
+
if (entry.isDirectory()) {
|
|
345
|
+
results.push(...(await walkDir(full)));
|
|
346
|
+
} else {
|
|
347
|
+
results.push(full);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return results;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Hash all RSC flight payloads and params sidecars in `_rsc/`, rename
|
|
355
|
+
* them with content hashes, and inject a manifest into each HTML file.
|
|
356
|
+
*
|
|
357
|
+
* Before: `_rsc/index.rsc`, `_rsc/about.rsc`
|
|
358
|
+
* After: `_rsc/index-B7YxEKdN.rsc`, `_rsc/about-C8ZzFLfO.rsc`
|
|
359
|
+
*
|
|
360
|
+
* The manifest maps the original `_rsc/` URL to the hashed filename:
|
|
361
|
+
* `{"/_rsc/index.rsc":"/_rsc/index-B7YxEKdN.rsc", ...}`
|
|
362
|
+
*
|
|
363
|
+
* Injected into HTML as:
|
|
364
|
+
* `<script>window.__TIMBER_RSC_MANIFEST__={...}</script>`
|
|
365
|
+
*
|
|
366
|
+
* @internal Exported for testing.
|
|
367
|
+
*/
|
|
368
|
+
export async function hashRscPayloads(staticDir: string): Promise<Record<string, string>> {
|
|
369
|
+
const rscDir = join(staticDir, '_rsc');
|
|
370
|
+
const files = await walkDir(rscDir);
|
|
371
|
+
const manifest: Record<string, string> = {};
|
|
372
|
+
|
|
373
|
+
for (const filePath of files) {
|
|
374
|
+
if (!filePath.endsWith('.rsc') && !filePath.endsWith('.params.json')) continue;
|
|
375
|
+
|
|
376
|
+
const content = await readFile(filePath);
|
|
377
|
+
const hash = contentHash(content);
|
|
378
|
+
const rel = relative(staticDir, filePath).replace(/\\/g, '/');
|
|
379
|
+
|
|
380
|
+
// Compute hashed filename: index.rsc → index-B7YxEKdN.rsc
|
|
381
|
+
const ext = filePath.endsWith('.params.json') ? '.params.json' : '.rsc';
|
|
382
|
+
const base = filePath.slice(0, -ext.length);
|
|
383
|
+
const hashedPath = `${base}-${hash}${ext}`;
|
|
384
|
+
|
|
385
|
+
await rename(filePath, hashedPath);
|
|
386
|
+
|
|
387
|
+
const hashedRel = relative(staticDir, hashedPath).replace(/\\/g, '/');
|
|
388
|
+
manifest[`/${rel}`] = `/${hashedRel}`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return manifest;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Inject the RSC manifest into all HTML files as a `<script>` tag
|
|
396
|
+
* before `</head>`. This lets the client resolve hashed RSC URLs
|
|
397
|
+
* without an extra network request.
|
|
398
|
+
*
|
|
399
|
+
* @internal Exported for testing.
|
|
400
|
+
*/
|
|
401
|
+
export async function injectRscManifestIntoHtml(
|
|
402
|
+
staticDir: string,
|
|
403
|
+
manifest: Record<string, string>
|
|
404
|
+
): Promise<void> {
|
|
405
|
+
const allFiles = await walkDir(staticDir);
|
|
406
|
+
const htmlFiles = allFiles.filter((f) => f.endsWith('.html'));
|
|
407
|
+
const escapedJson = htmlEscapeJsonString(JSON.stringify(manifest));
|
|
408
|
+
const scriptTag = `<script>window.__TIMBER_RSC_MANIFEST__=${escapedJson}</script>`;
|
|
409
|
+
|
|
410
|
+
for (const htmlFile of htmlFiles) {
|
|
411
|
+
let html = await readFile(htmlFile, 'utf-8');
|
|
412
|
+
// Insert before </head> so it's available before any client JS runs
|
|
413
|
+
const headClose = html.indexOf('</head>');
|
|
414
|
+
if (headClose !== -1) {
|
|
415
|
+
html = html.slice(0, headClose) + scriptTag + html.slice(headClose);
|
|
416
|
+
} else {
|
|
417
|
+
// Fallback: prepend to body
|
|
418
|
+
html = scriptTag + html;
|
|
419
|
+
}
|
|
420
|
+
await writeFile(htmlFile, html);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
297
424
|
// ---------------------------------------------------------------------------
|
|
298
425
|
// User-authored _redirects merge (TIM-1249)
|
|
299
426
|
// ---------------------------------------------------------------------------
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"build-output-helper-BC-Zg0_w.js","names":[],"sources":["../../src/adapters/shared.ts","../../src/adapters/build-output-helper.ts"],"sourcesContent":["// Shared adapter utilities — leaf module with zero deps on server/, client/,\n// or any Vite-dependent code.\n//\n// Adapters are loaded by Node at Vite startup time, before Vite's module\n// resolver is available. This module uses only Node built-ins and can be\n// imported by both adapters via a relative path.\n//\n// IMPORTANT: Do NOT add imports from ../server/, ../client/, ../plugins/,\n// or any module that transitively depends on Vite. A test in\n// tests/adapters/shared-no-forbidden-imports.test.ts enforces this.\n\n// ─── Static asset cache headers ──────────────────────────────────────────\n\n/** Cache-Control value for hashed (immutable) assets. */\nexport const IMMUTABLE_CACHE = 'public, max-age=31536000, immutable';\n\n/** Cache-Control value for unhashed static assets. */\nexport const STATIC_CACHE = 'public, max-age=3600, must-revalidate';\n\n/**\n * Generate a `_headers` file for static asset cache control.\n *\n * The `_headers` file is a platform convention supported by Cloudflare Workers\n * Static Assets, Cloudflare Pages, and Netlify. It maps URL patterns to\n * HTTP response headers.\n *\n * Only hashed (immutable) asset paths are listed here by default. When\n * `unhashedPaths` is provided, per-file rules are appended for unhashed\n * static assets (favicon.ico, robots.txt, etc.) so they get Cache-Control\n * on platforms like Cloudflare Workers Static Assets where the server\n * handler never sees static file requests.\n *\n * A `/*` catch-all is NOT used — it would be additive on Cloudflare,\n * producing duplicate Cache-Control directives on hashed assets\n * (invalid per RFC 9111).\n */\n// Cloudflare Workers Static Assets caps _headers at 100 rules.\n// Reserve 2 for the wildcard hashed-asset rules.\nconst MAX_PER_FILE_RULES = 98;\n\nexport function generateHeadersFile(\n unhashedPaths?: string[],\n contentTypes?: Record<string, string>\n): string {\n let content = `# Auto-generated by @timber-js/app — static asset cache headers.\n# See design/25-production-deployments.md §\"CDN / Edge Cache\"\n\n/assets/*\n Cache-Control: ${IMMUTABLE_CACHE}\n\n/_timber/fonts/*\n Cache-Control: ${IMMUTABLE_CACHE}\n`;\n\n // Content-Type rules are higher priority than cache headers for\n // extensionless files — without them the file is unreadable.\n const allCtEntries = contentTypes\n ? Object.entries(contentTypes).sort(([a], [b]) => a.localeCompare(b))\n : [];\n const ctEntries = allCtEntries.slice(0, MAX_PER_FILE_RULES);\n if (allCtEntries.length > MAX_PER_FILE_RULES) {\n console.warn(\n `[timber] _headers: ${allCtEntries.length} content-type rules exceed ` +\n `Cloudflare's 100-rule limit. Only the first ${MAX_PER_FILE_RULES} ` +\n `extensionless API routes will get Content-Type headers.`\n );\n }\n const remainingBudget = MAX_PER_FILE_RULES - ctEntries.length;\n\n if (unhashedPaths && unhashedPaths.length > 0) {\n const capped = unhashedPaths.slice(0, Math.max(0, remainingBudget));\n if (unhashedPaths.length > remainingBudget) {\n console.warn(\n `[timber] _headers: ${unhashedPaths.length} unhashed static files + ` +\n `${ctEntries.length} content-type rules found, but Cloudflare limits ` +\n `_headers to 100 rules. Only the first ${capped.length} unhashed ` +\n `files will get Cache-Control headers.`\n );\n }\n if (capped.length > 0) {\n content += `\\n# Unhashed static assets — short-lived cache with revalidation\\n`;\n for (const p of capped) {\n content += `\\n${p}\\n Cache-Control: ${STATIC_CACHE}\\n`;\n }\n }\n }\n\n if (ctEntries.length > 0) {\n content += `\\n# Content-Type for extensionless API routes\\n`;\n for (const [urlPath, ct] of ctEntries) {\n content += `\\n${urlPath}\\n Content-Type: ${ct}\\n`;\n }\n }\n\n return content;\n}\n","// Shared build-output helper for adapter buildOutput() methods.\n//\n// Steps 1–5 of every adapter's buildOutput are platform-agnostic:\n// 1. Copy client assets to the public/static directory\n// 2. Write _headers file for static asset cache control\n// 3. Copy RSC + SSR server bundles\n// 4. Write manifest-init module (if present)\n//\n// Only the final steps (entry codegen, platform config) vary per adapter.\n//\n// IMPORTANT: This module must remain a leaf — no imports from ../server/,\n// ../client/, or Vite-dependent code. See adapters/shared.ts header.\n\nimport { writeFile, mkdir, cp, readdir, readFile } from 'node:fs/promises';\nimport { join, basename, relative } from 'node:path';\nimport { generateHeadersFile } from './shared.js';\nimport type { TimberConfig } from './types';\n\n/** Options for the shared build output steps. */\nexport interface BuildOutputBaseOptions {\n /** Resolved timber config. */\n config: TimberConfig;\n /** Root build directory (e.g. `.timber/build`). */\n buildDir: string;\n /** Adapter-specific output directory (e.g. `.timber/build/nitro`). */\n outDir: string;\n /**\n * Name of the public/static directory within outDir.\n * Nitro uses 'public', Cloudflare uses 'static'.\n */\n publicDirName: string;\n}\n\n/**\n * Run the platform-agnostic build output steps shared by all adapters:\n *\n * 1. Create the output directory\n * 2. Copy client assets to the public/static directory\n * 3. Write the `_headers` file for static asset cache control\n * 4. Copy RSC and SSR server bundles into the output directory\n * 5. Write the manifest-init module if present\n *\n * Returns the resolved public directory path for further adapter-specific use.\n */\nexport async function runSharedBuildSteps(opts: BuildOutputBaseOptions): Promise<string> {\n const { config, buildDir, outDir, publicDirName } = opts;\n\n // 1. Create the output directory.\n await mkdir(outDir, { recursive: true });\n\n // 2. Copy client assets to public/static directory.\n // When client JavaScript is disabled, skip .js files — only CSS,\n // fonts, images, and other static assets are needed.\n const clientDir = join(buildDir, 'client');\n const publicDir = join(outDir, publicDirName);\n await mkdir(publicDir, { recursive: true });\n // Find WASM files that are referenced by client JS (legitimate client WASM)\n // vs orphaned server-only WASM (e.g. @takumi-rs for OG image generation).\n // Only strip orphaned WASM to avoid breaking client-side WASM usage.\n const referencedWasm = await findReferencedWasm(clientDir);\n\n await cp(clientDir, publicDir, {\n recursive: true,\n filter: (src: string) => {\n if (src.endsWith('.wasm') && !referencedWasm.has(basename(src))) {\n return false;\n }\n if (config.clientJavascriptDisabled && src.endsWith('.js')) return false;\n return true;\n },\n }).catch((e: NodeJS.ErrnoException) => {\n // Client dir may not exist when client JavaScript is disabled\n if (e.code !== 'ENOENT') throw e;\n });\n\n // 3. Write _headers file for static asset cache control.\n // Scan for unhashed files so they get per-file Cache-Control rules in the\n // _headers file. On platforms like Cloudflare Workers Static Assets, the\n // worker never sees static file requests — _headers is the only mechanism.\n const unhashedPaths = await findUnhashedStaticFiles(publicDir);\n await writeFile(join(publicDir, '_headers'), generateHeadersFile(unhashedPaths));\n\n // 4. Copy RSC + SSR server bundles into the output directory.\n await cp(join(buildDir, 'rsc'), join(outDir, 'rsc'), { recursive: true });\n await cp(join(buildDir, 'ssr'), join(outDir, 'ssr'), { recursive: true }).catch(\n (e: NodeJS.ErrnoException) => {\n // SSR dir may not exist for static-only builds\n if (e.code !== 'ENOENT') throw e;\n }\n );\n\n // 4b. Copy prebuilt/ flight payloads if present (cache.component artifacts).\n // These are produced by the post-build capture pass (design/44) and read\n // at request time via the adapter's PrebuiltPayloadSource.\n await cp(join(buildDir, 'prebuilt'), join(outDir, 'prebuilt'), { recursive: true }).catch(\n (e: NodeJS.ErrnoException) => {\n // No prebuilt dir when the app has no cache.component calls\n if (e.code !== 'ENOENT') throw e;\n }\n );\n\n // 5. Write manifest-init module if present.\n if (config.manifestInit) {\n await writeFile(join(outDir, '_timber-manifest-init.js'), config.manifestInit);\n }\n\n return publicDir;\n}\n\n/**\n * Find unhashed static files in the public directory that need per-file\n * Cache-Control rules in the `_headers` file.\n *\n * Excludes:\n * - Files under `assets/` and `_timber/` (covered by wildcard rules)\n * - Underscore-prefixed files (`_headers`, `_worker.js`, etc.)\n *\n * @internal Exported for testing.\n */\n// Well-known metadata files that should be prioritized when capping\n// _headers rules, so they're never dropped in favor of less important files.\nconst PRIORITY_FILES = new Set([\n '/favicon.ico',\n '/robots.txt',\n '/sitemap.xml',\n '/manifest.json',\n '/manifest.webmanifest',\n '/apple-touch-icon.png',\n]);\n\nexport async function findUnhashedStaticFiles(publicDir: string): Promise<string[]> {\n let files: string[];\n try {\n files = await collectFiles(publicDir);\n } catch {\n return [];\n }\n\n const priority: string[] = [];\n const rest: string[] = [];\n for (const file of files) {\n const rel = relative(publicDir, file).replace(/\\\\/g, '/');\n if (rel.startsWith('assets/') || rel.startsWith('_timber/')) continue;\n if (basename(rel).startsWith('_')) continue;\n const urlPath = '/' + rel;\n if (PRIORITY_FILES.has(urlPath)) {\n priority.push(urlPath);\n } else {\n rest.push(urlPath);\n }\n }\n\n return [...priority.sort(), ...rest.sort()];\n}\n\n/**\n * Scan client JS files for references to .wasm filenames.\n * Returns the set of WASM basenames that are actually imported/referenced\n * by client code and must be preserved in the static output.\n */\nasync function findReferencedWasm(clientDir: string): Promise<Set<string>> {\n const referenced = new Set<string>();\n\n let files: string[];\n try {\n files = await collectFiles(clientDir);\n } catch {\n return referenced;\n }\n\n const jsFiles = files.filter((f) => f.endsWith('.js'));\n const wasmFiles = files.filter((f) => f.endsWith('.wasm')).map((f) => basename(f));\n if (wasmFiles.length === 0) return referenced;\n\n for (const jsFile of jsFiles) {\n const content = await readFile(jsFile, 'utf-8');\n for (const wasmName of wasmFiles) {\n if (content.includes(wasmName)) {\n referenced.add(wasmName);\n }\n }\n }\n\n return referenced;\n}\n\n/**\n * Build a static-only output directory (no server entry). Copies client\n * assets and pre-rendered HTML/RSC/API files into `outDir/publicDirName`.\n * Used by both Cloudflare and Nitro adapters in `output: 'static'` mode.\n */\nexport async function buildStaticDirectoryOutput(\n config: TimberConfig,\n buildDir: string,\n outDir: string,\n publicDirName: string\n): Promise<void> {\n await mkdir(outDir, { recursive: true });\n\n const publicDir = join(outDir, publicDirName);\n await mkdir(publicDir, { recursive: true });\n\n // Copy client assets (JS, CSS, fonts, images).\n const clientDir = join(buildDir, 'client');\n await cp(clientDir, publicDir, {\n recursive: true,\n filter: (src: string) => {\n if (config.clientJavascriptDisabled && src.endsWith('.js')) return false;\n return true;\n },\n }).catch((e: NodeJS.ErrnoException) => {\n if (e.code !== 'ENOENT') throw e;\n });\n\n // Copy pre-rendered static pages (HTML + RSC flight + API routes).\n if (config.staticPagesDir) {\n await cp(config.staticPagesDir, publicDir, { recursive: true });\n }\n}\n\nasync function collectFiles(dir: string): Promise<string[]> {\n const result: string[] = [];\n const entries = await readdir(dir, { withFileTypes: true });\n for (const entry of entries) {\n const fullPath = join(dir, entry.name);\n if (entry.isDirectory()) {\n result.push(...(await collectFiles(fullPath)));\n } else {\n result.push(fullPath);\n }\n }\n return result;\n}\n"],"mappings":";;;;AAcA,IAAa,kBAAkB;;AAG/B,IAAa,eAAe;;;;;;;;;;;;;;;;;;AAqB5B,IAAM,qBAAqB;AAE3B,SAAgB,oBACd,eACA,cACQ;CACR,IAAI,UAAU;;;;mBAIG,gBAAgB;;;mBAGhB,gBAAgB;;CAKjC,MAAM,eAAe,eACjB,OAAO,QAAQ,YAAY,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,IAClE,CAAC;CACL,MAAM,YAAY,aAAa,MAAM,GAAG,kBAAkB;CAC1D,IAAI,aAAa,SAAS,oBACxB,QAAQ,KACN,sBAAsB,aAAa,OAAO,yEACO,mBAAmB,yDAEtE;CAEF,MAAM,kBAAkB,qBAAqB,UAAU;CAEvD,IAAI,iBAAiB,cAAc,SAAS,GAAG;EAC7C,MAAM,SAAS,cAAc,MAAM,GAAG,KAAK,IAAI,GAAG,eAAe,CAAC;EAClE,IAAI,cAAc,SAAS,iBACzB,QAAQ,KACN,sBAAsB,cAAc,OAAO,2BACtC,UAAU,OAAO,yFACqB,OAAO,OAAO,gDAE3D;EAEF,IAAI,OAAO,SAAS,GAAG;GACrB,WAAW;GACX,KAAK,MAAM,KAAK,QACd,WAAW,KAAK,EAAE,qBAAqB,aAAa;EAExD;CACF;CAEA,IAAI,UAAU,SAAS,GAAG;EACxB,WAAW;EACX,KAAK,MAAM,CAAC,SAAS,OAAO,WAC1B,WAAW,KAAK,QAAQ,oBAAoB,GAAG;CAEnD;CAEA,OAAO;AACT;;;;;;;;;;;;;;ACnDA,eAAsB,oBAAoB,MAA+C;CACvF,MAAM,EAAE,QAAQ,UAAU,QAAQ,kBAAkB;CAGpD,MAAM,MAAM,QAAQ,EAAE,WAAW,KAAK,CAAC;CAKvC,MAAM,YAAY,KAAK,UAAU,QAAQ;CACzC,MAAM,YAAY,KAAK,QAAQ,aAAa;CAC5C,MAAM,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;CAI1C,MAAM,iBAAiB,MAAM,mBAAmB,SAAS;CAEzD,MAAM,GAAG,WAAW,WAAW;EAC7B,WAAW;EACX,SAAS,QAAgB;GACvB,IAAI,IAAI,SAAS,OAAO,KAAK,CAAC,eAAe,IAAI,SAAS,GAAG,CAAC,GAC5D,OAAO;GAET,IAAI,OAAO,4BAA4B,IAAI,SAAS,KAAK,GAAG,OAAO;GACnE,OAAO;EACT;CACF,CAAC,CAAC,CAAC,OAAO,MAA6B;EAErC,IAAI,EAAE,SAAS,UAAU,MAAM;CACjC,CAAC;CAMD,MAAM,gBAAgB,MAAM,wBAAwB,SAAS;CAC7D,MAAM,UAAU,KAAK,WAAW,UAAU,GAAG,oBAAoB,aAAa,CAAC;CAG/E,MAAM,GAAG,KAAK,UAAU,KAAK,GAAG,KAAK,QAAQ,KAAK,GAAG,EAAE,WAAW,KAAK,CAAC;CACxE,MAAM,GAAG,KAAK,UAAU,KAAK,GAAG,KAAK,QAAQ,KAAK,GAAG,EAAE,WAAW,KAAK,CAAC,CAAC,CAAC,OACvE,MAA6B;EAE5B,IAAI,EAAE,SAAS,UAAU,MAAM;CACjC,CACF;CAKA,MAAM,GAAG,KAAK,UAAU,UAAU,GAAG,KAAK,QAAQ,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC,CAAC,CAAC,OACjF,MAA6B;EAE5B,IAAI,EAAE,SAAS,UAAU,MAAM;CACjC,CACF;CAGA,IAAI,OAAO,cACT,MAAM,UAAU,KAAK,QAAQ,0BAA0B,GAAG,OAAO,YAAY;CAG/E,OAAO;AACT;;;;;;;;;;;AAcA,IAAM,iCAAiB,IAAI,IAAI;CAC7B;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,eAAsB,wBAAwB,WAAsC;CAClF,IAAI;CACJ,IAAI;EACF,QAAQ,MAAM,aAAa,SAAS;CACtC,QAAQ;EACN,OAAO,CAAC;CACV;CAEA,MAAM,WAAqB,CAAC;CAC5B,MAAM,OAAiB,CAAC;CACxB,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,MAAM,SAAS,WAAW,IAAI,CAAC,CAAC,QAAQ,OAAO,GAAG;EACxD,IAAI,IAAI,WAAW,SAAS,KAAK,IAAI,WAAW,UAAU,GAAG;EAC7D,IAAI,SAAS,GAAG,CAAC,CAAC,WAAW,GAAG,GAAG;EACnC,MAAM,UAAU,MAAM;EACtB,IAAI,eAAe,IAAI,OAAO,GAC5B,SAAS,KAAK,OAAO;OAErB,KAAK,KAAK,OAAO;CAErB;CAEA,OAAO,CAAC,GAAG,SAAS,KAAK,GAAG,GAAG,KAAK,KAAK,CAAC;AAC5C;;;;;;AAOA,eAAe,mBAAmB,WAAyC;CACzE,MAAM,6BAAa,IAAI,IAAY;CAEnC,IAAI;CACJ,IAAI;EACF,QAAQ,MAAM,aAAa,SAAS;CACtC,QAAQ;EACN,OAAO;CACT;CAEA,MAAM,UAAU,MAAM,QAAQ,MAAM,EAAE,SAAS,KAAK,CAAC;CACrD,MAAM,YAAY,MAAM,QAAQ,MAAM,EAAE,SAAS,OAAO,CAAC,CAAC,CAAC,KAAK,MAAM,SAAS,CAAC,CAAC;CACjF,IAAI,UAAU,WAAW,GAAG,OAAO;CAEnC,KAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,UAAU,MAAM,SAAS,QAAQ,OAAO;EAC9C,KAAK,MAAM,YAAY,WACrB,IAAI,QAAQ,SAAS,QAAQ,GAC3B,WAAW,IAAI,QAAQ;CAG7B;CAEA,OAAO;AACT;;;;;;AAOA,eAAsB,2BACpB,QACA,UACA,QACA,eACe;CACf,MAAM,MAAM,QAAQ,EAAE,WAAW,KAAK,CAAC;CAEvC,MAAM,YAAY,KAAK,QAAQ,aAAa;CAC5C,MAAM,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;CAI1C,MAAM,GADY,KAAK,UAAU,QACxB,GAAW,WAAW;EAC7B,WAAW;EACX,SAAS,QAAgB;GACvB,IAAI,OAAO,4BAA4B,IAAI,SAAS,KAAK,GAAG,OAAO;GACnE,OAAO;EACT;CACF,CAAC,CAAC,CAAC,OAAO,MAA6B;EACrC,IAAI,EAAE,SAAS,UAAU,MAAM;CACjC,CAAC;CAGD,IAAI,OAAO,gBACT,MAAM,GAAG,OAAO,gBAAgB,WAAW,EAAE,WAAW,KAAK,CAAC;AAElE;AAEA,eAAe,aAAa,KAAgC;CAC1D,MAAM,SAAmB,CAAC;CAC1B,MAAM,UAAU,MAAM,QAAQ,KAAK,EAAE,eAAe,KAAK,CAAC;CAC1D,KAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,WAAW,KAAK,KAAK,MAAM,IAAI;EACrC,IAAI,MAAM,YAAY,GACpB,OAAO,KAAK,GAAI,MAAM,aAAa,QAAQ,CAAE;OAE7C,OAAO,KAAK,QAAQ;CAExB;CACA,OAAO;AACT"}
|