@timber-js/app 0.2.0-alpha.181 → 0.2.0-alpha.183
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-DF5uScqS.js → build-output-helper-BC-Zg0_w.js} +38 -8
- package/dist/_chunks/build-output-helper-BC-Zg0_w.js.map +1 -0
- package/dist/_chunks/{cloudflare-AHoWYTYr.js → cloudflare-nlD9KhDj.js} +21 -3
- package/dist/_chunks/{cloudflare-AHoWYTYr.js.map → cloudflare-nlD9KhDj.js.map} +1 -1
- package/dist/_chunks/plugin-context---kTF5v8.js.map +1 -1
- package/dist/adapters/build-output-helper.d.ts +6 -0
- 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.d.ts +10 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +2 -2
- package/dist/adapters/nitro.d.ts +14 -0
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +14 -1
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/adapters/shared.d.ts +1 -1
- package/dist/adapters/shared.d.ts.map +1 -1
- package/dist/adapters/types.d.ts +14 -0
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/internal.js +75 -12
- package/dist/client/internal.js.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +2 -0
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/index.js +352 -200
- 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/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-report.d.ts +4 -3
- package/dist/plugins/build-report.d.ts.map +1 -1
- package/dist/plugins/request-dep.d.ts +11 -0
- package/dist/plugins/request-dep.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +26 -2
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +12 -0
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/sitemap-generator.d.ts +1 -1
- package/dist/server/sitemap-generator.d.ts.map +1 -1
- package/dist/server/static-generator.d.ts +106 -0
- package/dist/server/static-generator.d.ts.map +1 -0
- package/docs/learn/13-configuration.mdx +5 -1
- package/package.json +3 -3
- package/src/adapters/build-output-helper.ts +34 -0
- package/src/adapters/cloudflare.ts +43 -1
- package/src/adapters/nitro.ts +58 -2
- package/src/adapters/shared.ts +38 -10
- package/src/adapters/types.ts +14 -0
- package/src/client/browser-entry/index.ts +6 -7
- package/src/client/browser-entry/post-hydration.ts +16 -5
- package/src/client/router.ts +3 -1
- package/src/client/rsc-fetch.ts +136 -19
- package/src/index.ts +1 -1
- package/src/plugin-context.ts +6 -0
- package/src/plugins/adapter-build.ts +5 -0
- package/src/plugins/build-report.ts +6 -12
- package/src/plugins/request-dep.ts +47 -6
- package/src/plugins/static-build.ts +264 -2
- package/src/server/rsc-entry/index.ts +24 -0
- package/src/server/sitemap-generator.ts +1 -1
- package/src/server/static-generator.ts +692 -0
- package/dist/_chunks/build-output-helper-DF5uScqS.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -106,7 +106,7 @@ function timberContent(ctx) {
|
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
108
|
//#endregion
|
|
109
|
-
//#region ../../node_modules/.pnpm/srvx@0.
|
|
109
|
+
//#region ../../node_modules/.pnpm/srvx@0.12.0/node_modules/srvx/dist/adapters/node.mjs
|
|
110
110
|
function sendNodeResponse(nodeRes, webRes) {
|
|
111
111
|
try {
|
|
112
112
|
return _sendNodeResponse(nodeRes, webRes, false) || Promise.resolve();
|
|
@@ -183,9 +183,13 @@ function pipeBody(stream, nodeRes, status, statusText, headers) {
|
|
|
183
183
|
}
|
|
184
184
|
function streamBody(stream, nodeRes) {
|
|
185
185
|
if (nodeRes.destroyed) {
|
|
186
|
-
stream.cancel();
|
|
186
|
+
stream.cancel().catch(() => {});
|
|
187
187
|
return;
|
|
188
188
|
}
|
|
189
|
+
if (nodeRes.req?.method === "HEAD") {
|
|
190
|
+
stream.cancel().catch(() => {});
|
|
191
|
+
return endNodeResponse(nodeRes);
|
|
192
|
+
}
|
|
189
193
|
const reader = stream.getReader();
|
|
190
194
|
function streamCancel(error) {
|
|
191
195
|
reader.cancel(error).catch(() => {});
|
|
@@ -6382,6 +6386,198 @@ function timberFonts(ctx) {
|
|
|
6382
6386
|
};
|
|
6383
6387
|
}
|
|
6384
6388
|
//#endregion
|
|
6389
|
+
//#region src/plugins/adapter-build.ts
|
|
6390
|
+
function timberAdapterBuild(ctx) {
|
|
6391
|
+
return {
|
|
6392
|
+
name: "timber-adapter-build",
|
|
6393
|
+
buildApp: {
|
|
6394
|
+
order: "post",
|
|
6395
|
+
async handler() {
|
|
6396
|
+
if (ctx.dev) return;
|
|
6397
|
+
const adapter = ctx.config.adapter;
|
|
6398
|
+
if (!adapter || typeof adapter.buildOutput !== "function") return;
|
|
6399
|
+
const buildDir = ctx.buildDir;
|
|
6400
|
+
if (ctx.buildManifest) await mergeRscAssetsManifest(buildDir, ctx.root, ctx.buildManifest, ctx.routeClientRefKeys);
|
|
6401
|
+
let manifestInit;
|
|
6402
|
+
if (ctx.buildManifest) {
|
|
6403
|
+
const manifest = ctx.clientJavascript.disabled ? {
|
|
6404
|
+
...ctx.buildManifest,
|
|
6405
|
+
js: {},
|
|
6406
|
+
modulepreload: {}
|
|
6407
|
+
} : ctx.buildManifest;
|
|
6408
|
+
manifestInit = `globalThis.__TIMBER_BUILD_MANIFEST__ = ${JSON.stringify(manifest)};\n`;
|
|
6409
|
+
if (ctx.deploymentId) manifestInit += `globalThis.__TIMBER_DEPLOYMENT_ID__ = ${JSON.stringify(ctx.deploymentId)};\n`;
|
|
6410
|
+
}
|
|
6411
|
+
if (ctx.clientJavascript.disabled) await stripJsFromRscAssetsManifests(buildDir);
|
|
6412
|
+
const adapterConfig = {
|
|
6413
|
+
root: ctx.root,
|
|
6414
|
+
output: ctx.config.output ?? "server",
|
|
6415
|
+
clientJavascriptDisabled: ctx.clientJavascript.disabled,
|
|
6416
|
+
manifestInit,
|
|
6417
|
+
staticPagesDir: (ctx.config.output ?? "server") === "static" ? join(buildDir, "static-pages") : void 0,
|
|
6418
|
+
staticContentTypes: ctx.staticContentTypes
|
|
6419
|
+
};
|
|
6420
|
+
await adapter.buildOutput(adapterConfig, buildDir);
|
|
6421
|
+
}
|
|
6422
|
+
}
|
|
6423
|
+
};
|
|
6424
|
+
}
|
|
6425
|
+
/**
|
|
6426
|
+
* Parse the RSC assets manifest at `manifestPath`. Tries JSON.parse first
|
|
6427
|
+
* (fast path for plain-string manifests); falls back to a dynamic import()
|
|
6428
|
+
* for manifests that contain runtime expressions from renderBuiltUrl.
|
|
6429
|
+
* Returns null if both paths fail.
|
|
6430
|
+
*/
|
|
6431
|
+
async function parseRscManifest(manifestPath, content) {
|
|
6432
|
+
const jsonStr = content.replace(/^export default\s*/, "").replace(/;?\s*$/, "");
|
|
6433
|
+
try {
|
|
6434
|
+
return JSON.parse(jsonStr);
|
|
6435
|
+
} catch {}
|
|
6436
|
+
try {
|
|
6437
|
+
return (await import(`${pathToFileURL(manifestPath).href}?t=${Date.now()}`)).default ?? null;
|
|
6438
|
+
} catch (err) {
|
|
6439
|
+
swallow(err, `failed to evaluate RSC assets manifest: ${manifestPath}`, { level: "warn" });
|
|
6440
|
+
return null;
|
|
6441
|
+
}
|
|
6442
|
+
}
|
|
6443
|
+
/**
|
|
6444
|
+
* Replace all `"js": [...]` array contents with `"js": []` using balanced
|
|
6445
|
+
* bracket matching. Handles nested brackets from runtime expressions like
|
|
6446
|
+
* `__rbu("path/to/[slug]/file.js")`.
|
|
6447
|
+
*/
|
|
6448
|
+
function clearJsArrays(src) {
|
|
6449
|
+
const pattern = /"js"\s*:\s*\[/g;
|
|
6450
|
+
let result = "";
|
|
6451
|
+
let lastEnd = 0;
|
|
6452
|
+
let match;
|
|
6453
|
+
while ((match = pattern.exec(src)) !== null) {
|
|
6454
|
+
const arrayStart = match.index + match[0].length;
|
|
6455
|
+
let depth = 1;
|
|
6456
|
+
let i = arrayStart;
|
|
6457
|
+
while (i < src.length && depth > 0) {
|
|
6458
|
+
if (src[i] === "[") depth++;
|
|
6459
|
+
else if (src[i] === "]") depth--;
|
|
6460
|
+
i++;
|
|
6461
|
+
}
|
|
6462
|
+
result += src.slice(lastEnd, match.index) + "\"js\": []";
|
|
6463
|
+
lastEnd = i;
|
|
6464
|
+
}
|
|
6465
|
+
result += src.slice(lastEnd);
|
|
6466
|
+
return result;
|
|
6467
|
+
}
|
|
6468
|
+
/**
|
|
6469
|
+
* Replace `"bootstrapScriptContent": <value>` with an empty string. The
|
|
6470
|
+
* value may be a JS expression (e.g., concatenation with runtime helpers),
|
|
6471
|
+
* not a simple string literal. Scans forward from the colon, tracking
|
|
6472
|
+
* balanced parens and brackets, and stops at the next unbalanced `,` or `}`.
|
|
6473
|
+
*/
|
|
6474
|
+
function clearBootstrapScriptContent(src) {
|
|
6475
|
+
const idx = src.indexOf("\"bootstrapScriptContent\"");
|
|
6476
|
+
if (idx === -1) return src;
|
|
6477
|
+
const colonIdx = src.indexOf(":", idx + 24);
|
|
6478
|
+
if (colonIdx === -1) return src;
|
|
6479
|
+
let valStart = colonIdx + 1;
|
|
6480
|
+
while (valStart < src.length && /\s/.test(src[valStart])) valStart++;
|
|
6481
|
+
let i = valStart;
|
|
6482
|
+
let depth = 0;
|
|
6483
|
+
let inString = false;
|
|
6484
|
+
let stringChar = "";
|
|
6485
|
+
while (i < src.length) {
|
|
6486
|
+
const ch = src[i];
|
|
6487
|
+
if (inString) {
|
|
6488
|
+
if (ch === "\\") {
|
|
6489
|
+
i += 2;
|
|
6490
|
+
continue;
|
|
6491
|
+
}
|
|
6492
|
+
if (ch === stringChar) inString = false;
|
|
6493
|
+
} else if (ch === "\"" || ch === "'") {
|
|
6494
|
+
inString = true;
|
|
6495
|
+
stringChar = ch;
|
|
6496
|
+
} else if (ch === "(" || ch === "[") depth++;
|
|
6497
|
+
else if (ch === ")" || ch === "]") depth--;
|
|
6498
|
+
else if (depth === 0 && (ch === "," || ch === "}" || ch === "\n")) break;
|
|
6499
|
+
i++;
|
|
6500
|
+
}
|
|
6501
|
+
return src.slice(0, idx) + "\"bootstrapScriptContent\": \"\"" + src.slice(i);
|
|
6502
|
+
}
|
|
6503
|
+
/**
|
|
6504
|
+
* Strip JS references from the RSC plugin's assets manifest files.
|
|
6505
|
+
*
|
|
6506
|
+
* The RSC plugin writes `__vite_rsc_assets_manifest.js` to rsc/ and ssr/
|
|
6507
|
+
* as standalone files (not Rollup chunks), so generateBundle can't
|
|
6508
|
+
* intercept them. This rewrites the files on disk after all builds
|
|
6509
|
+
* complete but before the adapter copies them to the output directory.
|
|
6510
|
+
*/
|
|
6511
|
+
/**
|
|
6512
|
+
* Read the RSC plugin's assets manifest and merge per-route CSS and
|
|
6513
|
+
* per-route JS modulepreloads into the build manifest.
|
|
6514
|
+
*
|
|
6515
|
+
* CSS: `serverResources` maps server component paths to CSS URLs.
|
|
6516
|
+
* JS: `clientReferenceDeps` maps reference hashes to JS dep URLs.
|
|
6517
|
+
* Combined with `routeClientRefKeys` (from the RSC generateBundle
|
|
6518
|
+
* module graph walk) to produce per-route modulepreload entries.
|
|
6519
|
+
*/
|
|
6520
|
+
async function mergeRscAssetsManifest(buildDir, root, buildManifest, routeClientRefKeys) {
|
|
6521
|
+
const manifestPath = join(buildDir, "rsc", "__vite_rsc_assets_manifest.js");
|
|
6522
|
+
let content;
|
|
6523
|
+
try {
|
|
6524
|
+
content = await readFile(manifestPath, "utf-8");
|
|
6525
|
+
} catch {
|
|
6526
|
+
return;
|
|
6527
|
+
}
|
|
6528
|
+
const manifest = await parseRscManifest(manifestPath, content);
|
|
6529
|
+
if (!manifest) return;
|
|
6530
|
+
const serverResources = manifest.serverResources;
|
|
6531
|
+
if (serverResources) {
|
|
6532
|
+
for (const [relativePath, deps] of Object.entries(serverResources)) if (deps.css && deps.css.length > 0) {
|
|
6533
|
+
const absolutePath = join(root, relativePath);
|
|
6534
|
+
buildManifest.css[absolutePath] = deps.css;
|
|
6535
|
+
}
|
|
6536
|
+
}
|
|
6537
|
+
if (routeClientRefKeys && routeClientRefKeys.size > 0) {
|
|
6538
|
+
const clientReferenceDeps = manifest.clientReferenceDeps;
|
|
6539
|
+
if (clientReferenceDeps) for (const [filePath, refKeys] of routeClientRefKeys) {
|
|
6540
|
+
const jsUrls = /* @__PURE__ */ new Set();
|
|
6541
|
+
for (const key of refKeys) {
|
|
6542
|
+
const deps = clientReferenceDeps[key];
|
|
6543
|
+
if (deps?.js) for (const url of deps.js) jsUrls.add(url);
|
|
6544
|
+
}
|
|
6545
|
+
if (jsUrls.size > 0) buildManifest.modulepreload[filePath] = Array.from(jsUrls);
|
|
6546
|
+
}
|
|
6547
|
+
}
|
|
6548
|
+
}
|
|
6549
|
+
async function stripJsFromRscAssetsManifests(buildDir) {
|
|
6550
|
+
const manifestName = "__vite_rsc_assets_manifest.js";
|
|
6551
|
+
const paths = [join(buildDir, "rsc", manifestName), join(buildDir, "ssr", manifestName)];
|
|
6552
|
+
for (const path of paths) {
|
|
6553
|
+
let content;
|
|
6554
|
+
try {
|
|
6555
|
+
content = await readFile(path, "utf-8");
|
|
6556
|
+
} catch {
|
|
6557
|
+
continue;
|
|
6558
|
+
}
|
|
6559
|
+
const jsonStr = content.replace(/^export default\s*/, "").replace(/;?\s*$/, "");
|
|
6560
|
+
let isStrictJson = true;
|
|
6561
|
+
let manifest = null;
|
|
6562
|
+
try {
|
|
6563
|
+
manifest = JSON.parse(jsonStr);
|
|
6564
|
+
} catch {
|
|
6565
|
+
isStrictJson = false;
|
|
6566
|
+
}
|
|
6567
|
+
if (!isStrictJson) {
|
|
6568
|
+
let rewritten = content;
|
|
6569
|
+
rewritten = clearJsArrays(rewritten);
|
|
6570
|
+
rewritten = clearBootstrapScriptContent(rewritten);
|
|
6571
|
+
await writeFile(path, rewritten);
|
|
6572
|
+
continue;
|
|
6573
|
+
}
|
|
6574
|
+
const deps = manifest?.clientReferenceDeps;
|
|
6575
|
+
if (deps) for (const entry of Object.values(deps)) entry.js = [];
|
|
6576
|
+
if (manifest) manifest.bootstrapScriptContent = "";
|
|
6577
|
+
await writeFile(path, `export default ${JSON.stringify(manifest, null, 2)};\n`);
|
|
6578
|
+
}
|
|
6579
|
+
}
|
|
6580
|
+
//#endregion
|
|
6385
6581
|
//#region src/plugins/static-build.ts
|
|
6386
6582
|
/**
|
|
6387
6583
|
* Patterns that indicate dynamic per-request API usage.
|
|
@@ -6432,12 +6628,122 @@ function detectDynamicApis(code, fileId) {
|
|
|
6432
6628
|
return errors;
|
|
6433
6629
|
}
|
|
6434
6630
|
/**
|
|
6631
|
+
* Generate a `_redirects` file from collected redirect entries.
|
|
6632
|
+
* Uses the Cloudflare Pages / Netlify format: `/from /to statusCode`
|
|
6633
|
+
*
|
|
6634
|
+
* @internal Exported for testing.
|
|
6635
|
+
*/
|
|
6636
|
+
function generateRedirectsFile(redirects) {
|
|
6637
|
+
return redirects.map((r) => `${r.from} ${r.to} ${r.status}`).join("\n") + "\n";
|
|
6638
|
+
}
|
|
6639
|
+
/** Write a file atomically: temp sibling + rename. */
|
|
6640
|
+
async function writeFileAtomic$1(path, data) {
|
|
6641
|
+
const tmp = `${path}.tmp`;
|
|
6642
|
+
await writeFile(tmp, data);
|
|
6643
|
+
await rename(tmp, path);
|
|
6644
|
+
}
|
|
6645
|
+
/**
|
|
6646
|
+
* Determine the output file path for a static page entry.
|
|
6647
|
+
*
|
|
6648
|
+
* Pages: /about → about/index.html, / → index.html
|
|
6649
|
+
* RSC: /about → _rsc/about.rsc, / → _rsc/index.rsc
|
|
6650
|
+
* API: /feed.xml → feed.xml, /api/data → api/data
|
|
6651
|
+
*/
|
|
6652
|
+
function staticOutputPath(entry) {
|
|
6653
|
+
const urlPath = entry.urlPath === "/" ? "" : entry.urlPath;
|
|
6654
|
+
if (entry.kind === "rsc") return `_rsc${urlPath || "/index"}.rsc`;
|
|
6655
|
+
if (entry.kind === "api") return urlPath.startsWith("/") ? urlPath.slice(1) : urlPath || "index";
|
|
6656
|
+
if (!urlPath) return "index.html";
|
|
6657
|
+
if (urlPath === "/_timber_not_found") return "404.html";
|
|
6658
|
+
return `${urlPath.startsWith("/") ? urlPath.slice(1) : urlPath}/index.html`;
|
|
6659
|
+
}
|
|
6660
|
+
/**
|
|
6661
|
+
* Load the built RSC entry and run the static generation pass.
|
|
6662
|
+
*
|
|
6663
|
+
* Unlike prebuilt capture, this needs the FULL pipeline handler, so
|
|
6664
|
+
* __TIMBER_CAPTURE_MODE__ must be false.
|
|
6665
|
+
*/
|
|
6666
|
+
async function runStaticGeneration(ctx) {
|
|
6667
|
+
const entryPath = join(ctx.buildDir, "rsc", "index.js");
|
|
6668
|
+
if (!existsSync(entryPath)) throw new Error(`[timber] Static generation: built RSC entry not found at ${entryPath} — the build output is missing`);
|
|
6669
|
+
const g = globalThis;
|
|
6670
|
+
if (ctx.buildManifest) g.__TIMBER_BUILD_MANIFEST__ ??= ctx.buildManifest;
|
|
6671
|
+
if (ctx.deploymentId) g.__TIMBER_DEPLOYMENT_ID__ ??= ctx.deploymentId;
|
|
6672
|
+
g.__TIMBER_CAPTURE_MODE__ = false;
|
|
6673
|
+
if (ctx.buildManifest) await mergeRscAssetsManifest(ctx.buildDir, ctx.root, ctx.buildManifest, ctx.routeClientRefKeys);
|
|
6674
|
+
console.log("[timber] Static generation: building site…");
|
|
6675
|
+
let mod;
|
|
6676
|
+
try {
|
|
6677
|
+
mod = await import(
|
|
6678
|
+
/* @vite-ignore */
|
|
6679
|
+
`${pathToFileURL(entryPath).href}?t=${Date.now()}`
|
|
6680
|
+
);
|
|
6681
|
+
} catch (error) {
|
|
6682
|
+
throw new Error(`[timber] Static generation: failed to load the built RSC entry (${entryPath}). The full request handler must initialize for static rendering.`, { cause: error });
|
|
6683
|
+
}
|
|
6684
|
+
const generate = mod.__timber_generateStaticSite;
|
|
6685
|
+
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
|
+
const staticDir = join(ctx.buildDir, "static-pages");
|
|
6687
|
+
await mkdir(staticDir, { recursive: true });
|
|
6688
|
+
const sitemapConfig = ctx.config.sitemap;
|
|
6689
|
+
const autoSitemapEnabled = Boolean(sitemapConfig?.enabled && sitemapConfig?.baseUrl);
|
|
6690
|
+
const contentTypes = {};
|
|
6691
|
+
const summary = await generate({
|
|
6692
|
+
autoSitemapEnabled,
|
|
6693
|
+
renderTimeoutMs: ctx.config.renderTimeoutMs,
|
|
6694
|
+
onEntry: async (entry) => {
|
|
6695
|
+
const relPath = staticOutputPath(entry);
|
|
6696
|
+
const fullPath = resolve(staticDir, relPath);
|
|
6697
|
+
const rel = relative(staticDir, fullPath);
|
|
6698
|
+
if (rel.startsWith("..") || resolve(staticDir, rel) !== fullPath) throw new Error(`[timber] Static generation: output path "${relPath}" escapes the static output directory — check generateStaticSegmentParams for path traversal characters in ${entry.urlPath}`);
|
|
6699
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
6700
|
+
await writeFileAtomic$1(fullPath, entry.body);
|
|
6701
|
+
if (entry.kind === "rsc" && entry.headers["x-timber-params"]) await writeFileAtomic$1(fullPath.replace(/\.rsc$/, ".params.json"), entry.headers["x-timber-params"]);
|
|
6702
|
+
if (entry.kind === "api" && !extname(relPath) && entry.contentType) contentTypes[entry.urlPath] = entry.contentType;
|
|
6703
|
+
}
|
|
6704
|
+
});
|
|
6705
|
+
const totalUrls = summary.rendered + summary.errors.length;
|
|
6706
|
+
console.log(`[timber] Static generation: rendered ${summary.rendered}/${totalUrls} entries.` + (summary.errors.length > 0 ? ` ${summary.errors.length} failed.` : "") + (summary.redirects.length > 0 ? ` ${summary.redirects.length} redirect(s).` : ""));
|
|
6707
|
+
if (summary.errors.length > 0) {
|
|
6708
|
+
const details = summary.errors.map((e) => ` - ${e.urlPath}: ${e.message}`).join("\n");
|
|
6709
|
+
throw new Error(`[timber] Static generation failed:\n${details}`);
|
|
6710
|
+
}
|
|
6711
|
+
if (Object.keys(contentTypes).length > 0) ctx.staticContentTypes = contentTypes;
|
|
6712
|
+
if (summary.redirects.length > 0) {
|
|
6713
|
+
const generated = generateRedirectsFile(summary.redirects);
|
|
6714
|
+
const redirectsPath = join(staticDir, "_redirects");
|
|
6715
|
+
const userRedirects = await readUserRedirects(ctx.root);
|
|
6716
|
+
if (userRedirects !== null) {
|
|
6717
|
+
await writeFileAtomic$1(redirectsPath, generated + "\n" + userRedirects);
|
|
6718
|
+
console.log(`[timber] Static generation: wrote _redirects with ${summary.redirects.length} generated rule(s), merged with user-authored _redirects`);
|
|
6719
|
+
} else {
|
|
6720
|
+
await writeFileAtomic$1(redirectsPath, generated);
|
|
6721
|
+
console.log(`[timber] Static generation: wrote _redirects with ${summary.redirects.length} rule(s)`);
|
|
6722
|
+
}
|
|
6723
|
+
}
|
|
6724
|
+
return summary;
|
|
6725
|
+
}
|
|
6726
|
+
/**
|
|
6727
|
+
* Read a user-authored `_redirects` file from the project's `public/`
|
|
6728
|
+
* directory. Returns `null` if no file exists.
|
|
6729
|
+
*
|
|
6730
|
+
* @internal Exported for testing.
|
|
6731
|
+
*/
|
|
6732
|
+
async function readUserRedirects(root) {
|
|
6733
|
+
const candidates = [join(root, "public", "_redirects")];
|
|
6734
|
+
for (const path of candidates) try {
|
|
6735
|
+
return await readFile(path, "utf-8");
|
|
6736
|
+
} catch {}
|
|
6737
|
+
return null;
|
|
6738
|
+
}
|
|
6739
|
+
/**
|
|
6435
6740
|
* Create the timber-static-build Vite plugin.
|
|
6436
6741
|
*
|
|
6437
6742
|
* Only active when output: 'static' is configured.
|
|
6438
6743
|
*
|
|
6439
6744
|
* Hooks:
|
|
6440
6745
|
* - transform: Validates source files for static mode violations
|
|
6746
|
+
* - buildApp (post): Renders all routes to static HTML after the build
|
|
6441
6747
|
*/
|
|
6442
6748
|
function timberStaticBuild(ctx) {
|
|
6443
6749
|
return {
|
|
@@ -6459,6 +6765,21 @@ function timberStaticBuild(ctx) {
|
|
|
6459
6765
|
this.error(messages.join("\n\n"));
|
|
6460
6766
|
}
|
|
6461
6767
|
return null;
|
|
6768
|
+
},
|
|
6769
|
+
/**
|
|
6770
|
+
* After all environments are built, render every route to static HTML.
|
|
6771
|
+
*
|
|
6772
|
+
* Runs with order: 'post' so the RSC plugin's buildApp completes first,
|
|
6773
|
+
* producing the built entry we import. Must run BEFORE the adapter
|
|
6774
|
+
* build copies output to its platform-specific directory.
|
|
6775
|
+
*/
|
|
6776
|
+
buildApp: {
|
|
6777
|
+
order: "post",
|
|
6778
|
+
async handler() {
|
|
6779
|
+
if (ctx.dev) return;
|
|
6780
|
+
if (!(ctx.config.output === "static")) return;
|
|
6781
|
+
await runStaticGeneration(ctx);
|
|
6782
|
+
}
|
|
6462
6783
|
}
|
|
6463
6784
|
};
|
|
6464
6785
|
}
|
|
@@ -7777,6 +8098,9 @@ var ACCESSOR_NAMES = /* @__PURE__ */ new Set([
|
|
|
7777
8098
|
]);
|
|
7778
8099
|
var DEFINITION_FACTORY_SOURCES = /* @__PURE__ */ new Set(["@timber-js/app/search-params", "@timber-js/app/cookies"]);
|
|
7779
8100
|
var DEFINITION_FACTORY_NAMES = /* @__PURE__ */ new Set(["defineSearchParams", "defineCookie"]);
|
|
8101
|
+
function isFrameworkModule(id) {
|
|
8102
|
+
return /[\\/]node_modules[\\/]@timber-js[\\/]app[\\/]/.test(id) || /[\\/]packages[\\/]timber-app[\\/]src[\\/]/.test(id);
|
|
8103
|
+
}
|
|
7780
8104
|
var ACCESSOR_TOKENS = [
|
|
7781
8105
|
"getHeaders",
|
|
7782
8106
|
"getHeader",
|
|
@@ -7864,13 +8188,29 @@ function walkForCall(node, named, namespaces, importedName) {
|
|
|
7864
8188
|
}
|
|
7865
8189
|
return false;
|
|
7866
8190
|
}
|
|
7867
|
-
/**
|
|
8191
|
+
/**
|
|
8192
|
+
* Check if the object chain of a `.get()` call contains `localName`.
|
|
8193
|
+
* Walks through MemberExpression objects and CallExpression callees
|
|
8194
|
+
* to handle chained patterns like `getFilterCookie(key).get()` or
|
|
8195
|
+
* `cookies.theme.get()`.
|
|
8196
|
+
*/
|
|
8197
|
+
function objectChainContains(node, name) {
|
|
8198
|
+
if (node.type === "Identifier" && node.name === name) return true;
|
|
8199
|
+
if (node.type === "MemberExpression") return objectChainContains(node.object, name);
|
|
8200
|
+
if (node.type === "CallExpression") return objectChainContains(node.callee, name);
|
|
8201
|
+
return false;
|
|
8202
|
+
}
|
|
8203
|
+
/**
|
|
8204
|
+
* Check if `.get(...)` is called on `localName` anywhere in the module.
|
|
8205
|
+
* Handles direct (`localName.get()`), chained call (`localName(args).get()`),
|
|
8206
|
+
* and nested member (`localName.prop.get()`) patterns.
|
|
8207
|
+
*/
|
|
7868
8208
|
function hasGetterCall(node, localName) {
|
|
7869
8209
|
if (node.type === "CallExpression") {
|
|
7870
8210
|
const call = node;
|
|
7871
8211
|
if (call.callee.type === "MemberExpression") {
|
|
7872
8212
|
const member = call.callee;
|
|
7873
|
-
if (!member.computed && member.
|
|
8213
|
+
if (!member.computed && member.property.type === "Identifier" && member.property.name === "get" && objectChainContains(member.object, localName)) return true;
|
|
7874
8214
|
}
|
|
7875
8215
|
}
|
|
7876
8216
|
for (const key of Object.keys(node)) {
|
|
@@ -7926,6 +8266,7 @@ function timberRequestDep(ctx) {
|
|
|
7926
8266
|
async transform(code, id) {
|
|
7927
8267
|
if (!/\.[mc]?[jt]sx?$/.test(id) && !/\.mdx?$/.test(id)) return;
|
|
7928
8268
|
if (id.startsWith("\0") || id.startsWith("virtual:")) return;
|
|
8269
|
+
if (isFrameworkModule(id)) return;
|
|
7929
8270
|
if (!hasRelevantToken(code)) return;
|
|
7930
8271
|
const facts = analyzeModule(code, this.parse(code));
|
|
7931
8272
|
if (facts.callsAccessor || facts.importsDefinitionFactory || facts.getterCallSources.length > 0) {
|
|
@@ -9078,196 +9419,6 @@ function timberCloudflareWasm(ctx) {
|
|
|
9078
9419
|
}];
|
|
9079
9420
|
}
|
|
9080
9421
|
//#endregion
|
|
9081
|
-
//#region src/plugins/adapter-build.ts
|
|
9082
|
-
function timberAdapterBuild(ctx) {
|
|
9083
|
-
return {
|
|
9084
|
-
name: "timber-adapter-build",
|
|
9085
|
-
buildApp: {
|
|
9086
|
-
order: "post",
|
|
9087
|
-
async handler() {
|
|
9088
|
-
if (ctx.dev) return;
|
|
9089
|
-
const adapter = ctx.config.adapter;
|
|
9090
|
-
if (!adapter || typeof adapter.buildOutput !== "function") return;
|
|
9091
|
-
const buildDir = ctx.buildDir;
|
|
9092
|
-
if (ctx.buildManifest) await mergeRscAssetsManifest(buildDir, ctx.root, ctx.buildManifest, ctx.routeClientRefKeys);
|
|
9093
|
-
let manifestInit;
|
|
9094
|
-
if (ctx.buildManifest) {
|
|
9095
|
-
const manifest = ctx.clientJavascript.disabled ? {
|
|
9096
|
-
...ctx.buildManifest,
|
|
9097
|
-
js: {},
|
|
9098
|
-
modulepreload: {}
|
|
9099
|
-
} : ctx.buildManifest;
|
|
9100
|
-
manifestInit = `globalThis.__TIMBER_BUILD_MANIFEST__ = ${JSON.stringify(manifest)};\n`;
|
|
9101
|
-
if (ctx.deploymentId) manifestInit += `globalThis.__TIMBER_DEPLOYMENT_ID__ = ${JSON.stringify(ctx.deploymentId)};\n`;
|
|
9102
|
-
}
|
|
9103
|
-
if (ctx.clientJavascript.disabled) await stripJsFromRscAssetsManifests(buildDir);
|
|
9104
|
-
const adapterConfig = {
|
|
9105
|
-
root: ctx.root,
|
|
9106
|
-
output: ctx.config.output ?? "server",
|
|
9107
|
-
clientJavascriptDisabled: ctx.clientJavascript.disabled,
|
|
9108
|
-
manifestInit
|
|
9109
|
-
};
|
|
9110
|
-
await adapter.buildOutput(adapterConfig, buildDir);
|
|
9111
|
-
}
|
|
9112
|
-
}
|
|
9113
|
-
};
|
|
9114
|
-
}
|
|
9115
|
-
/**
|
|
9116
|
-
* Parse the RSC assets manifest at `manifestPath`. Tries JSON.parse first
|
|
9117
|
-
* (fast path for plain-string manifests); falls back to a dynamic import()
|
|
9118
|
-
* for manifests that contain runtime expressions from renderBuiltUrl.
|
|
9119
|
-
* Returns null if both paths fail.
|
|
9120
|
-
*/
|
|
9121
|
-
async function parseRscManifest(manifestPath, content) {
|
|
9122
|
-
const jsonStr = content.replace(/^export default\s*/, "").replace(/;?\s*$/, "");
|
|
9123
|
-
try {
|
|
9124
|
-
return JSON.parse(jsonStr);
|
|
9125
|
-
} catch {}
|
|
9126
|
-
try {
|
|
9127
|
-
return (await import(`${pathToFileURL(manifestPath).href}?t=${Date.now()}`)).default ?? null;
|
|
9128
|
-
} catch (err) {
|
|
9129
|
-
swallow(err, `failed to evaluate RSC assets manifest: ${manifestPath}`, { level: "warn" });
|
|
9130
|
-
return null;
|
|
9131
|
-
}
|
|
9132
|
-
}
|
|
9133
|
-
/**
|
|
9134
|
-
* Replace all `"js": [...]` array contents with `"js": []` using balanced
|
|
9135
|
-
* bracket matching. Handles nested brackets from runtime expressions like
|
|
9136
|
-
* `__rbu("path/to/[slug]/file.js")`.
|
|
9137
|
-
*/
|
|
9138
|
-
function clearJsArrays(src) {
|
|
9139
|
-
const pattern = /"js"\s*:\s*\[/g;
|
|
9140
|
-
let result = "";
|
|
9141
|
-
let lastEnd = 0;
|
|
9142
|
-
let match;
|
|
9143
|
-
while ((match = pattern.exec(src)) !== null) {
|
|
9144
|
-
const arrayStart = match.index + match[0].length;
|
|
9145
|
-
let depth = 1;
|
|
9146
|
-
let i = arrayStart;
|
|
9147
|
-
while (i < src.length && depth > 0) {
|
|
9148
|
-
if (src[i] === "[") depth++;
|
|
9149
|
-
else if (src[i] === "]") depth--;
|
|
9150
|
-
i++;
|
|
9151
|
-
}
|
|
9152
|
-
result += src.slice(lastEnd, match.index) + "\"js\": []";
|
|
9153
|
-
lastEnd = i;
|
|
9154
|
-
}
|
|
9155
|
-
result += src.slice(lastEnd);
|
|
9156
|
-
return result;
|
|
9157
|
-
}
|
|
9158
|
-
/**
|
|
9159
|
-
* Replace `"bootstrapScriptContent": <value>` with an empty string. The
|
|
9160
|
-
* value may be a JS expression (e.g., concatenation with runtime helpers),
|
|
9161
|
-
* not a simple string literal. Scans forward from the colon, tracking
|
|
9162
|
-
* balanced parens and brackets, and stops at the next unbalanced `,` or `}`.
|
|
9163
|
-
*/
|
|
9164
|
-
function clearBootstrapScriptContent(src) {
|
|
9165
|
-
const idx = src.indexOf("\"bootstrapScriptContent\"");
|
|
9166
|
-
if (idx === -1) return src;
|
|
9167
|
-
const colonIdx = src.indexOf(":", idx + 24);
|
|
9168
|
-
if (colonIdx === -1) return src;
|
|
9169
|
-
let valStart = colonIdx + 1;
|
|
9170
|
-
while (valStart < src.length && /\s/.test(src[valStart])) valStart++;
|
|
9171
|
-
let i = valStart;
|
|
9172
|
-
let depth = 0;
|
|
9173
|
-
let inString = false;
|
|
9174
|
-
let stringChar = "";
|
|
9175
|
-
while (i < src.length) {
|
|
9176
|
-
const ch = src[i];
|
|
9177
|
-
if (inString) {
|
|
9178
|
-
if (ch === "\\") {
|
|
9179
|
-
i += 2;
|
|
9180
|
-
continue;
|
|
9181
|
-
}
|
|
9182
|
-
if (ch === stringChar) inString = false;
|
|
9183
|
-
} else if (ch === "\"" || ch === "'") {
|
|
9184
|
-
inString = true;
|
|
9185
|
-
stringChar = ch;
|
|
9186
|
-
} else if (ch === "(" || ch === "[") depth++;
|
|
9187
|
-
else if (ch === ")" || ch === "]") depth--;
|
|
9188
|
-
else if (depth === 0 && (ch === "," || ch === "}" || ch === "\n")) break;
|
|
9189
|
-
i++;
|
|
9190
|
-
}
|
|
9191
|
-
return src.slice(0, idx) + "\"bootstrapScriptContent\": \"\"" + src.slice(i);
|
|
9192
|
-
}
|
|
9193
|
-
/**
|
|
9194
|
-
* Strip JS references from the RSC plugin's assets manifest files.
|
|
9195
|
-
*
|
|
9196
|
-
* The RSC plugin writes `__vite_rsc_assets_manifest.js` to rsc/ and ssr/
|
|
9197
|
-
* as standalone files (not Rollup chunks), so generateBundle can't
|
|
9198
|
-
* intercept them. This rewrites the files on disk after all builds
|
|
9199
|
-
* complete but before the adapter copies them to the output directory.
|
|
9200
|
-
*/
|
|
9201
|
-
/**
|
|
9202
|
-
* Read the RSC plugin's assets manifest and merge per-route CSS and
|
|
9203
|
-
* per-route JS modulepreloads into the build manifest.
|
|
9204
|
-
*
|
|
9205
|
-
* CSS: `serverResources` maps server component paths to CSS URLs.
|
|
9206
|
-
* JS: `clientReferenceDeps` maps reference hashes to JS dep URLs.
|
|
9207
|
-
* Combined with `routeClientRefKeys` (from the RSC generateBundle
|
|
9208
|
-
* module graph walk) to produce per-route modulepreload entries.
|
|
9209
|
-
*/
|
|
9210
|
-
async function mergeRscAssetsManifest(buildDir, root, buildManifest, routeClientRefKeys) {
|
|
9211
|
-
const manifestPath = join(buildDir, "rsc", "__vite_rsc_assets_manifest.js");
|
|
9212
|
-
let content;
|
|
9213
|
-
try {
|
|
9214
|
-
content = await readFile(manifestPath, "utf-8");
|
|
9215
|
-
} catch {
|
|
9216
|
-
return;
|
|
9217
|
-
}
|
|
9218
|
-
const manifest = await parseRscManifest(manifestPath, content);
|
|
9219
|
-
if (!manifest) return;
|
|
9220
|
-
const serverResources = manifest.serverResources;
|
|
9221
|
-
if (serverResources) {
|
|
9222
|
-
for (const [relativePath, deps] of Object.entries(serverResources)) if (deps.css && deps.css.length > 0) {
|
|
9223
|
-
const absolutePath = join(root, relativePath);
|
|
9224
|
-
buildManifest.css[absolutePath] = deps.css;
|
|
9225
|
-
}
|
|
9226
|
-
}
|
|
9227
|
-
if (routeClientRefKeys && routeClientRefKeys.size > 0) {
|
|
9228
|
-
const clientReferenceDeps = manifest.clientReferenceDeps;
|
|
9229
|
-
if (clientReferenceDeps) for (const [filePath, refKeys] of routeClientRefKeys) {
|
|
9230
|
-
const jsUrls = /* @__PURE__ */ new Set();
|
|
9231
|
-
for (const key of refKeys) {
|
|
9232
|
-
const deps = clientReferenceDeps[key];
|
|
9233
|
-
if (deps?.js) for (const url of deps.js) jsUrls.add(url);
|
|
9234
|
-
}
|
|
9235
|
-
if (jsUrls.size > 0) buildManifest.modulepreload[filePath] = Array.from(jsUrls);
|
|
9236
|
-
}
|
|
9237
|
-
}
|
|
9238
|
-
}
|
|
9239
|
-
async function stripJsFromRscAssetsManifests(buildDir) {
|
|
9240
|
-
const manifestName = "__vite_rsc_assets_manifest.js";
|
|
9241
|
-
const paths = [join(buildDir, "rsc", manifestName), join(buildDir, "ssr", manifestName)];
|
|
9242
|
-
for (const path of paths) {
|
|
9243
|
-
let content;
|
|
9244
|
-
try {
|
|
9245
|
-
content = await readFile(path, "utf-8");
|
|
9246
|
-
} catch {
|
|
9247
|
-
continue;
|
|
9248
|
-
}
|
|
9249
|
-
const jsonStr = content.replace(/^export default\s*/, "").replace(/;?\s*$/, "");
|
|
9250
|
-
let isStrictJson = true;
|
|
9251
|
-
let manifest = null;
|
|
9252
|
-
try {
|
|
9253
|
-
manifest = JSON.parse(jsonStr);
|
|
9254
|
-
} catch {
|
|
9255
|
-
isStrictJson = false;
|
|
9256
|
-
}
|
|
9257
|
-
if (!isStrictJson) {
|
|
9258
|
-
let rewritten = content;
|
|
9259
|
-
rewritten = clearJsArrays(rewritten);
|
|
9260
|
-
rewritten = clearBootstrapScriptContent(rewritten);
|
|
9261
|
-
await writeFile(path, rewritten);
|
|
9262
|
-
continue;
|
|
9263
|
-
}
|
|
9264
|
-
const deps = manifest?.clientReferenceDeps;
|
|
9265
|
-
if (deps) for (const entry of Object.values(deps)) entry.js = [];
|
|
9266
|
-
if (manifest) manifest.bootstrapScriptContent = "";
|
|
9267
|
-
await writeFile(path, `export default ${JSON.stringify(manifest, null, 2)};\n`);
|
|
9268
|
-
}
|
|
9269
|
-
}
|
|
9270
|
-
//#endregion
|
|
9271
9422
|
//#region src/plugins/build-report.ts
|
|
9272
9423
|
/**
|
|
9273
9424
|
* timber-build-report — Post-build route summary table.
|
|
@@ -9291,14 +9442,15 @@ var ROUTE_TYPE_ICONS = {
|
|
|
9291
9442
|
/**
|
|
9292
9443
|
* Classify a route by its segment chain and output mode.
|
|
9293
9444
|
*
|
|
9294
|
-
* In server mode (default), all pages are dynamic (rendered per-request)
|
|
9295
|
-
*
|
|
9296
|
-
*
|
|
9445
|
+
* In server mode (default), all pages are dynamic (rendered per-request)
|
|
9446
|
+
* and API routes (route.ts) are classified as function.
|
|
9447
|
+
* In static mode, everything is pre-rendered at build time — all routes
|
|
9448
|
+
* are static regardless of dynamic segments or route.ts presence.
|
|
9297
9449
|
*/
|
|
9298
9450
|
function classifyRoute(segments, outputMode = "server") {
|
|
9451
|
+
if (outputMode === "static") return "static";
|
|
9299
9452
|
if (segments[segments.length - 1]?.route) return "function";
|
|
9300
|
-
|
|
9301
|
-
return segments.some((s) => s.segmentType === "dynamic" || s.segmentType === "catch-all" || s.segmentType === "optional-catch-all") ? "dynamic" : "static";
|
|
9453
|
+
return "dynamic";
|
|
9302
9454
|
}
|
|
9303
9455
|
function green(text) {
|
|
9304
9456
|
return `\x1b[92m${text}\x1b[39m`;
|
|
@@ -10203,13 +10355,13 @@ function timber(config) {
|
|
|
10203
10355
|
timberRouting(ctx),
|
|
10204
10356
|
timberEntries(ctx),
|
|
10205
10357
|
timberBuildManifest(ctx),
|
|
10206
|
-
timberStaticBuild(ctx),
|
|
10207
10358
|
timberFonts(ctx),
|
|
10208
10359
|
timberMdx(ctx),
|
|
10209
10360
|
timberRequestDep(ctx),
|
|
10210
10361
|
timberCacheTransform(ctx),
|
|
10211
10362
|
timberPrerenderSugar(ctx),
|
|
10212
10363
|
timberPrebuilt(ctx),
|
|
10364
|
+
timberStaticBuild(ctx),
|
|
10213
10365
|
timberContent(ctx),
|
|
10214
10366
|
timberServerBundle(),
|
|
10215
10367
|
timberCloudflareWasm(ctx),
|