@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/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
20
20
|
import { styleText } from "node:util";
|
|
21
21
|
import { constants, createGzip, gzipSync } from "node:zlib";
|
|
22
22
|
import { createHash, randomUUID } from "node:crypto";
|
|
23
|
-
import { copyFile, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
23
|
+
import { copyFile, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
24
24
|
//#region src/plugins/content.ts
|
|
25
25
|
var CONFIG_FILE_NAMES$1 = [
|
|
26
26
|
"content-collections.ts",
|
|
@@ -3491,7 +3491,7 @@ function filterBySubsets(faces, subsets) {
|
|
|
3491
3491
|
* Generate a content hash for font data.
|
|
3492
3492
|
* Returns the first 8 hex chars of the SHA-256 hash.
|
|
3493
3493
|
*/
|
|
3494
|
-
function contentHash(data) {
|
|
3494
|
+
function contentHash$1(data) {
|
|
3495
3495
|
return createHash("sha256").update(data).digest("hex").slice(0, 8);
|
|
3496
3496
|
}
|
|
3497
3497
|
/**
|
|
@@ -3502,7 +3502,7 @@ function contentHash(data) {
|
|
|
3502
3502
|
*/
|
|
3503
3503
|
function hashedFontFilename(face, data) {
|
|
3504
3504
|
const slug = face.family.toLowerCase().replace(/\s+/g, "-");
|
|
3505
|
-
const hash = contentHash(data);
|
|
3505
|
+
const hash = contentHash$1(data);
|
|
3506
3506
|
return `${slug}-${face.subset}-${face.weight}-${face.style}-${hash}.woff2`;
|
|
3507
3507
|
}
|
|
3508
3508
|
/**
|
|
@@ -4733,7 +4733,7 @@ var FontPipeline = class {
|
|
|
4733
4733
|
const absolutePath = normalize(resolve(src.path));
|
|
4734
4734
|
if (!existsSync(absolutePath)) continue;
|
|
4735
4735
|
const data = readFileSync(absolutePath);
|
|
4736
|
-
src.hashedBasename = hashedFontBasename(src.path, contentHash(data));
|
|
4736
|
+
src.hashedBasename = hashedFontBasename(src.path, contentHash$1(data));
|
|
4737
4737
|
}
|
|
4738
4738
|
this._entries.set(entry.id, entry);
|
|
4739
4739
|
}
|
|
@@ -6578,6 +6578,30 @@ async function stripJsFromRscAssetsManifests(buildDir) {
|
|
|
6578
6578
|
}
|
|
6579
6579
|
}
|
|
6580
6580
|
//#endregion
|
|
6581
|
+
//#region src/server/flight-scripts.ts
|
|
6582
|
+
/**
|
|
6583
|
+
* Shared Flight data script generation.
|
|
6584
|
+
*
|
|
6585
|
+
* Both html-injectors.ts (Web Streams / dev) and node-stream-transforms.ts
|
|
6586
|
+
* (Node streams / production) use this module to generate inline `<script>`
|
|
6587
|
+
* tags for RSC Flight data injection.
|
|
6588
|
+
*
|
|
6589
|
+
* The init script goes in `<head>` as part of the HTML shell (guaranteed
|
|
6590
|
+
* to execute before any streaming chunk scripts). Push scripts are emitted
|
|
6591
|
+
* as RSC chunks arrive during streaming.
|
|
6592
|
+
*
|
|
6593
|
+
* See design/02-rendering-pipeline.md, LOCAL-415
|
|
6594
|
+
*/
|
|
6595
|
+
/**
|
|
6596
|
+
* Escape a JSON string for safe embedding inside an HTML `<script>` tag.
|
|
6597
|
+
*
|
|
6598
|
+
* Prevents XSS via `<\/script>` injection and handles Unicode line/paragraph
|
|
6599
|
+
* separators that are valid JSON but invalid in JS string literals (pre-ES2019).
|
|
6600
|
+
*/
|
|
6601
|
+
function htmlEscapeJsonString(str) {
|
|
6602
|
+
return str.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
6603
|
+
}
|
|
6604
|
+
//#endregion
|
|
6581
6605
|
//#region src/plugins/static-build.ts
|
|
6582
6606
|
/**
|
|
6583
6607
|
* Patterns that indicate dynamic per-request API usage.
|
|
@@ -6684,6 +6708,10 @@ async function runStaticGeneration(ctx) {
|
|
|
6684
6708
|
const generate = mod.__timber_generateStaticSite;
|
|
6685
6709
|
if (typeof generate !== "function") throw new Error("[timber] Static generation: built RSC entry does not export __timber_generateStaticSite — rebuild from a clean build directory");
|
|
6686
6710
|
const staticDir = join(ctx.buildDir, "static-pages");
|
|
6711
|
+
await rm(join(staticDir, "_rsc"), {
|
|
6712
|
+
recursive: true,
|
|
6713
|
+
force: true
|
|
6714
|
+
}).catch(() => {});
|
|
6687
6715
|
await mkdir(staticDir, { recursive: true });
|
|
6688
6716
|
const sitemapConfig = ctx.config.sitemap;
|
|
6689
6717
|
const autoSitemapEnabled = Boolean(sitemapConfig?.enabled && sitemapConfig?.baseUrl);
|
|
@@ -6709,6 +6737,14 @@ async function runStaticGeneration(ctx) {
|
|
|
6709
6737
|
throw new Error(`[timber] Static generation failed:\n${details}`);
|
|
6710
6738
|
}
|
|
6711
6739
|
if (Object.keys(contentTypes).length > 0) ctx.staticContentTypes = contentTypes;
|
|
6740
|
+
if (!ctx.clientJavascript.disabled) {
|
|
6741
|
+
const rscManifest = await hashRscPayloads(staticDir);
|
|
6742
|
+
if (Object.keys(rscManifest).length > 0) {
|
|
6743
|
+
await injectRscManifestIntoHtml(staticDir, rscManifest);
|
|
6744
|
+
ctx.rscManifest = rscManifest;
|
|
6745
|
+
console.log(`[timber] Static generation: hashed ${Object.keys(rscManifest).length} RSC flight payloads`);
|
|
6746
|
+
}
|
|
6747
|
+
}
|
|
6712
6748
|
if (summary.redirects.length > 0) {
|
|
6713
6749
|
const generated = generateRedirectsFile(summary.redirects);
|
|
6714
6750
|
const redirectsPath = join(staticDir, "_redirects");
|
|
@@ -6724,6 +6760,79 @@ async function runStaticGeneration(ctx) {
|
|
|
6724
6760
|
return summary;
|
|
6725
6761
|
}
|
|
6726
6762
|
/**
|
|
6763
|
+
* Compute a short content hash for immutable caching.
|
|
6764
|
+
* Uses 8 chars of base64url-encoded SHA-256, matching Vite's asset hash style.
|
|
6765
|
+
*/
|
|
6766
|
+
function contentHash(data) {
|
|
6767
|
+
return createHash("sha256").update(data).digest("base64url").slice(0, 8);
|
|
6768
|
+
}
|
|
6769
|
+
/**
|
|
6770
|
+
* Walk a directory recursively and return all file paths.
|
|
6771
|
+
*/
|
|
6772
|
+
async function walkDir(dir) {
|
|
6773
|
+
const results = [];
|
|
6774
|
+
let entries;
|
|
6775
|
+
try {
|
|
6776
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
6777
|
+
} catch {
|
|
6778
|
+
return results;
|
|
6779
|
+
}
|
|
6780
|
+
for (const entry of entries) {
|
|
6781
|
+
const full = join(dir, entry.name);
|
|
6782
|
+
if (entry.isDirectory()) results.push(...await walkDir(full));
|
|
6783
|
+
else results.push(full);
|
|
6784
|
+
}
|
|
6785
|
+
return results;
|
|
6786
|
+
}
|
|
6787
|
+
/**
|
|
6788
|
+
* Hash all RSC flight payloads and params sidecars in `_rsc/`, rename
|
|
6789
|
+
* them with content hashes, and inject a manifest into each HTML file.
|
|
6790
|
+
*
|
|
6791
|
+
* Before: `_rsc/index.rsc`, `_rsc/about.rsc`
|
|
6792
|
+
* After: `_rsc/index-B7YxEKdN.rsc`, `_rsc/about-C8ZzFLfO.rsc`
|
|
6793
|
+
*
|
|
6794
|
+
* The manifest maps the original `_rsc/` URL to the hashed filename:
|
|
6795
|
+
* `{"/_rsc/index.rsc":"/_rsc/index-B7YxEKdN.rsc", ...}`
|
|
6796
|
+
*
|
|
6797
|
+
* Injected into HTML as:
|
|
6798
|
+
* `<script>window.__TIMBER_RSC_MANIFEST__={...}<\/script>`
|
|
6799
|
+
*
|
|
6800
|
+
* @internal Exported for testing.
|
|
6801
|
+
*/
|
|
6802
|
+
async function hashRscPayloads(staticDir) {
|
|
6803
|
+
const files = await walkDir(join(staticDir, "_rsc"));
|
|
6804
|
+
const manifest = {};
|
|
6805
|
+
for (const filePath of files) {
|
|
6806
|
+
if (!filePath.endsWith(".rsc") && !filePath.endsWith(".params.json")) continue;
|
|
6807
|
+
const hash = contentHash(await readFile(filePath));
|
|
6808
|
+
const rel = relative(staticDir, filePath).replace(/\\/g, "/");
|
|
6809
|
+
const ext = filePath.endsWith(".params.json") ? ".params.json" : ".rsc";
|
|
6810
|
+
const hashedPath = `${filePath.slice(0, -ext.length)}-${hash}${ext}`;
|
|
6811
|
+
await rename(filePath, hashedPath);
|
|
6812
|
+
const hashedRel = relative(staticDir, hashedPath).replace(/\\/g, "/");
|
|
6813
|
+
manifest[`/${rel}`] = `/${hashedRel}`;
|
|
6814
|
+
}
|
|
6815
|
+
return manifest;
|
|
6816
|
+
}
|
|
6817
|
+
/**
|
|
6818
|
+
* Inject the RSC manifest into all HTML files as a `<script>` tag
|
|
6819
|
+
* before `</head>`. This lets the client resolve hashed RSC URLs
|
|
6820
|
+
* without an extra network request.
|
|
6821
|
+
*
|
|
6822
|
+
* @internal Exported for testing.
|
|
6823
|
+
*/
|
|
6824
|
+
async function injectRscManifestIntoHtml(staticDir, manifest) {
|
|
6825
|
+
const htmlFiles = (await walkDir(staticDir)).filter((f) => f.endsWith(".html"));
|
|
6826
|
+
const scriptTag = `<script>window.__TIMBER_RSC_MANIFEST__=${htmlEscapeJsonString(JSON.stringify(manifest))}<\/script>`;
|
|
6827
|
+
for (const htmlFile of htmlFiles) {
|
|
6828
|
+
let html = await readFile(htmlFile, "utf-8");
|
|
6829
|
+
const headClose = html.indexOf("</head>");
|
|
6830
|
+
if (headClose !== -1) html = html.slice(0, headClose) + scriptTag + html.slice(headClose);
|
|
6831
|
+
else html = scriptTag + html;
|
|
6832
|
+
await writeFile(htmlFile, html);
|
|
6833
|
+
}
|
|
6834
|
+
}
|
|
6835
|
+
/**
|
|
6727
6836
|
* Read a user-authored `_redirects` file from the project's `public/`
|
|
6728
6837
|
* directory. Returns `null` if no file exists.
|
|
6729
6838
|
*
|