@timber-js/app 0.2.0-alpha.182 → 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/internal.js +73 -11
- package/dist/client/internal.js.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 +324 -196
- 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/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 +1 -1
- 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 +7 -1
- package/src/client/rsc-fetch.ts +132 -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/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
|
@@ -6386,6 +6386,198 @@ function timberFonts(ctx) {
|
|
|
6386
6386
|
};
|
|
6387
6387
|
}
|
|
6388
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
|
|
6389
6581
|
//#region src/plugins/static-build.ts
|
|
6390
6582
|
/**
|
|
6391
6583
|
* Patterns that indicate dynamic per-request API usage.
|
|
@@ -6436,12 +6628,122 @@ function detectDynamicApis(code, fileId) {
|
|
|
6436
6628
|
return errors;
|
|
6437
6629
|
}
|
|
6438
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
|
+
/**
|
|
6439
6740
|
* Create the timber-static-build Vite plugin.
|
|
6440
6741
|
*
|
|
6441
6742
|
* Only active when output: 'static' is configured.
|
|
6442
6743
|
*
|
|
6443
6744
|
* Hooks:
|
|
6444
6745
|
* - transform: Validates source files for static mode violations
|
|
6746
|
+
* - buildApp (post): Renders all routes to static HTML after the build
|
|
6445
6747
|
*/
|
|
6446
6748
|
function timberStaticBuild(ctx) {
|
|
6447
6749
|
return {
|
|
@@ -6463,6 +6765,21 @@ function timberStaticBuild(ctx) {
|
|
|
6463
6765
|
this.error(messages.join("\n\n"));
|
|
6464
6766
|
}
|
|
6465
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
|
+
}
|
|
6466
6783
|
}
|
|
6467
6784
|
};
|
|
6468
6785
|
}
|
|
@@ -9102,196 +9419,6 @@ function timberCloudflareWasm(ctx) {
|
|
|
9102
9419
|
}];
|
|
9103
9420
|
}
|
|
9104
9421
|
//#endregion
|
|
9105
|
-
//#region src/plugins/adapter-build.ts
|
|
9106
|
-
function timberAdapterBuild(ctx) {
|
|
9107
|
-
return {
|
|
9108
|
-
name: "timber-adapter-build",
|
|
9109
|
-
buildApp: {
|
|
9110
|
-
order: "post",
|
|
9111
|
-
async handler() {
|
|
9112
|
-
if (ctx.dev) return;
|
|
9113
|
-
const adapter = ctx.config.adapter;
|
|
9114
|
-
if (!adapter || typeof adapter.buildOutput !== "function") return;
|
|
9115
|
-
const buildDir = ctx.buildDir;
|
|
9116
|
-
if (ctx.buildManifest) await mergeRscAssetsManifest(buildDir, ctx.root, ctx.buildManifest, ctx.routeClientRefKeys);
|
|
9117
|
-
let manifestInit;
|
|
9118
|
-
if (ctx.buildManifest) {
|
|
9119
|
-
const manifest = ctx.clientJavascript.disabled ? {
|
|
9120
|
-
...ctx.buildManifest,
|
|
9121
|
-
js: {},
|
|
9122
|
-
modulepreload: {}
|
|
9123
|
-
} : ctx.buildManifest;
|
|
9124
|
-
manifestInit = `globalThis.__TIMBER_BUILD_MANIFEST__ = ${JSON.stringify(manifest)};\n`;
|
|
9125
|
-
if (ctx.deploymentId) manifestInit += `globalThis.__TIMBER_DEPLOYMENT_ID__ = ${JSON.stringify(ctx.deploymentId)};\n`;
|
|
9126
|
-
}
|
|
9127
|
-
if (ctx.clientJavascript.disabled) await stripJsFromRscAssetsManifests(buildDir);
|
|
9128
|
-
const adapterConfig = {
|
|
9129
|
-
root: ctx.root,
|
|
9130
|
-
output: ctx.config.output ?? "server",
|
|
9131
|
-
clientJavascriptDisabled: ctx.clientJavascript.disabled,
|
|
9132
|
-
manifestInit
|
|
9133
|
-
};
|
|
9134
|
-
await adapter.buildOutput(adapterConfig, buildDir);
|
|
9135
|
-
}
|
|
9136
|
-
}
|
|
9137
|
-
};
|
|
9138
|
-
}
|
|
9139
|
-
/**
|
|
9140
|
-
* Parse the RSC assets manifest at `manifestPath`. Tries JSON.parse first
|
|
9141
|
-
* (fast path for plain-string manifests); falls back to a dynamic import()
|
|
9142
|
-
* for manifests that contain runtime expressions from renderBuiltUrl.
|
|
9143
|
-
* Returns null if both paths fail.
|
|
9144
|
-
*/
|
|
9145
|
-
async function parseRscManifest(manifestPath, content) {
|
|
9146
|
-
const jsonStr = content.replace(/^export default\s*/, "").replace(/;?\s*$/, "");
|
|
9147
|
-
try {
|
|
9148
|
-
return JSON.parse(jsonStr);
|
|
9149
|
-
} catch {}
|
|
9150
|
-
try {
|
|
9151
|
-
return (await import(`${pathToFileURL(manifestPath).href}?t=${Date.now()}`)).default ?? null;
|
|
9152
|
-
} catch (err) {
|
|
9153
|
-
swallow(err, `failed to evaluate RSC assets manifest: ${manifestPath}`, { level: "warn" });
|
|
9154
|
-
return null;
|
|
9155
|
-
}
|
|
9156
|
-
}
|
|
9157
|
-
/**
|
|
9158
|
-
* Replace all `"js": [...]` array contents with `"js": []` using balanced
|
|
9159
|
-
* bracket matching. Handles nested brackets from runtime expressions like
|
|
9160
|
-
* `__rbu("path/to/[slug]/file.js")`.
|
|
9161
|
-
*/
|
|
9162
|
-
function clearJsArrays(src) {
|
|
9163
|
-
const pattern = /"js"\s*:\s*\[/g;
|
|
9164
|
-
let result = "";
|
|
9165
|
-
let lastEnd = 0;
|
|
9166
|
-
let match;
|
|
9167
|
-
while ((match = pattern.exec(src)) !== null) {
|
|
9168
|
-
const arrayStart = match.index + match[0].length;
|
|
9169
|
-
let depth = 1;
|
|
9170
|
-
let i = arrayStart;
|
|
9171
|
-
while (i < src.length && depth > 0) {
|
|
9172
|
-
if (src[i] === "[") depth++;
|
|
9173
|
-
else if (src[i] === "]") depth--;
|
|
9174
|
-
i++;
|
|
9175
|
-
}
|
|
9176
|
-
result += src.slice(lastEnd, match.index) + "\"js\": []";
|
|
9177
|
-
lastEnd = i;
|
|
9178
|
-
}
|
|
9179
|
-
result += src.slice(lastEnd);
|
|
9180
|
-
return result;
|
|
9181
|
-
}
|
|
9182
|
-
/**
|
|
9183
|
-
* Replace `"bootstrapScriptContent": <value>` with an empty string. The
|
|
9184
|
-
* value may be a JS expression (e.g., concatenation with runtime helpers),
|
|
9185
|
-
* not a simple string literal. Scans forward from the colon, tracking
|
|
9186
|
-
* balanced parens and brackets, and stops at the next unbalanced `,` or `}`.
|
|
9187
|
-
*/
|
|
9188
|
-
function clearBootstrapScriptContent(src) {
|
|
9189
|
-
const idx = src.indexOf("\"bootstrapScriptContent\"");
|
|
9190
|
-
if (idx === -1) return src;
|
|
9191
|
-
const colonIdx = src.indexOf(":", idx + 24);
|
|
9192
|
-
if (colonIdx === -1) return src;
|
|
9193
|
-
let valStart = colonIdx + 1;
|
|
9194
|
-
while (valStart < src.length && /\s/.test(src[valStart])) valStart++;
|
|
9195
|
-
let i = valStart;
|
|
9196
|
-
let depth = 0;
|
|
9197
|
-
let inString = false;
|
|
9198
|
-
let stringChar = "";
|
|
9199
|
-
while (i < src.length) {
|
|
9200
|
-
const ch = src[i];
|
|
9201
|
-
if (inString) {
|
|
9202
|
-
if (ch === "\\") {
|
|
9203
|
-
i += 2;
|
|
9204
|
-
continue;
|
|
9205
|
-
}
|
|
9206
|
-
if (ch === stringChar) inString = false;
|
|
9207
|
-
} else if (ch === "\"" || ch === "'") {
|
|
9208
|
-
inString = true;
|
|
9209
|
-
stringChar = ch;
|
|
9210
|
-
} else if (ch === "(" || ch === "[") depth++;
|
|
9211
|
-
else if (ch === ")" || ch === "]") depth--;
|
|
9212
|
-
else if (depth === 0 && (ch === "," || ch === "}" || ch === "\n")) break;
|
|
9213
|
-
i++;
|
|
9214
|
-
}
|
|
9215
|
-
return src.slice(0, idx) + "\"bootstrapScriptContent\": \"\"" + src.slice(i);
|
|
9216
|
-
}
|
|
9217
|
-
/**
|
|
9218
|
-
* Strip JS references from the RSC plugin's assets manifest files.
|
|
9219
|
-
*
|
|
9220
|
-
* The RSC plugin writes `__vite_rsc_assets_manifest.js` to rsc/ and ssr/
|
|
9221
|
-
* as standalone files (not Rollup chunks), so generateBundle can't
|
|
9222
|
-
* intercept them. This rewrites the files on disk after all builds
|
|
9223
|
-
* complete but before the adapter copies them to the output directory.
|
|
9224
|
-
*/
|
|
9225
|
-
/**
|
|
9226
|
-
* Read the RSC plugin's assets manifest and merge per-route CSS and
|
|
9227
|
-
* per-route JS modulepreloads into the build manifest.
|
|
9228
|
-
*
|
|
9229
|
-
* CSS: `serverResources` maps server component paths to CSS URLs.
|
|
9230
|
-
* JS: `clientReferenceDeps` maps reference hashes to JS dep URLs.
|
|
9231
|
-
* Combined with `routeClientRefKeys` (from the RSC generateBundle
|
|
9232
|
-
* module graph walk) to produce per-route modulepreload entries.
|
|
9233
|
-
*/
|
|
9234
|
-
async function mergeRscAssetsManifest(buildDir, root, buildManifest, routeClientRefKeys) {
|
|
9235
|
-
const manifestPath = join(buildDir, "rsc", "__vite_rsc_assets_manifest.js");
|
|
9236
|
-
let content;
|
|
9237
|
-
try {
|
|
9238
|
-
content = await readFile(manifestPath, "utf-8");
|
|
9239
|
-
} catch {
|
|
9240
|
-
return;
|
|
9241
|
-
}
|
|
9242
|
-
const manifest = await parseRscManifest(manifestPath, content);
|
|
9243
|
-
if (!manifest) return;
|
|
9244
|
-
const serverResources = manifest.serverResources;
|
|
9245
|
-
if (serverResources) {
|
|
9246
|
-
for (const [relativePath, deps] of Object.entries(serverResources)) if (deps.css && deps.css.length > 0) {
|
|
9247
|
-
const absolutePath = join(root, relativePath);
|
|
9248
|
-
buildManifest.css[absolutePath] = deps.css;
|
|
9249
|
-
}
|
|
9250
|
-
}
|
|
9251
|
-
if (routeClientRefKeys && routeClientRefKeys.size > 0) {
|
|
9252
|
-
const clientReferenceDeps = manifest.clientReferenceDeps;
|
|
9253
|
-
if (clientReferenceDeps) for (const [filePath, refKeys] of routeClientRefKeys) {
|
|
9254
|
-
const jsUrls = /* @__PURE__ */ new Set();
|
|
9255
|
-
for (const key of refKeys) {
|
|
9256
|
-
const deps = clientReferenceDeps[key];
|
|
9257
|
-
if (deps?.js) for (const url of deps.js) jsUrls.add(url);
|
|
9258
|
-
}
|
|
9259
|
-
if (jsUrls.size > 0) buildManifest.modulepreload[filePath] = Array.from(jsUrls);
|
|
9260
|
-
}
|
|
9261
|
-
}
|
|
9262
|
-
}
|
|
9263
|
-
async function stripJsFromRscAssetsManifests(buildDir) {
|
|
9264
|
-
const manifestName = "__vite_rsc_assets_manifest.js";
|
|
9265
|
-
const paths = [join(buildDir, "rsc", manifestName), join(buildDir, "ssr", manifestName)];
|
|
9266
|
-
for (const path of paths) {
|
|
9267
|
-
let content;
|
|
9268
|
-
try {
|
|
9269
|
-
content = await readFile(path, "utf-8");
|
|
9270
|
-
} catch {
|
|
9271
|
-
continue;
|
|
9272
|
-
}
|
|
9273
|
-
const jsonStr = content.replace(/^export default\s*/, "").replace(/;?\s*$/, "");
|
|
9274
|
-
let isStrictJson = true;
|
|
9275
|
-
let manifest = null;
|
|
9276
|
-
try {
|
|
9277
|
-
manifest = JSON.parse(jsonStr);
|
|
9278
|
-
} catch {
|
|
9279
|
-
isStrictJson = false;
|
|
9280
|
-
}
|
|
9281
|
-
if (!isStrictJson) {
|
|
9282
|
-
let rewritten = content;
|
|
9283
|
-
rewritten = clearJsArrays(rewritten);
|
|
9284
|
-
rewritten = clearBootstrapScriptContent(rewritten);
|
|
9285
|
-
await writeFile(path, rewritten);
|
|
9286
|
-
continue;
|
|
9287
|
-
}
|
|
9288
|
-
const deps = manifest?.clientReferenceDeps;
|
|
9289
|
-
if (deps) for (const entry of Object.values(deps)) entry.js = [];
|
|
9290
|
-
if (manifest) manifest.bootstrapScriptContent = "";
|
|
9291
|
-
await writeFile(path, `export default ${JSON.stringify(manifest, null, 2)};\n`);
|
|
9292
|
-
}
|
|
9293
|
-
}
|
|
9294
|
-
//#endregion
|
|
9295
9422
|
//#region src/plugins/build-report.ts
|
|
9296
9423
|
/**
|
|
9297
9424
|
* timber-build-report — Post-build route summary table.
|
|
@@ -9315,14 +9442,15 @@ var ROUTE_TYPE_ICONS = {
|
|
|
9315
9442
|
/**
|
|
9316
9443
|
* Classify a route by its segment chain and output mode.
|
|
9317
9444
|
*
|
|
9318
|
-
* In server mode (default), all pages are dynamic (rendered per-request)
|
|
9319
|
-
*
|
|
9320
|
-
*
|
|
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.
|
|
9321
9449
|
*/
|
|
9322
9450
|
function classifyRoute(segments, outputMode = "server") {
|
|
9451
|
+
if (outputMode === "static") return "static";
|
|
9323
9452
|
if (segments[segments.length - 1]?.route) return "function";
|
|
9324
|
-
|
|
9325
|
-
return segments.some((s) => s.segmentType === "dynamic" || s.segmentType === "catch-all" || s.segmentType === "optional-catch-all") ? "dynamic" : "static";
|
|
9453
|
+
return "dynamic";
|
|
9326
9454
|
}
|
|
9327
9455
|
function green(text) {
|
|
9328
9456
|
return `\x1b[92m${text}\x1b[39m`;
|
|
@@ -10227,13 +10355,13 @@ function timber(config) {
|
|
|
10227
10355
|
timberRouting(ctx),
|
|
10228
10356
|
timberEntries(ctx),
|
|
10229
10357
|
timberBuildManifest(ctx),
|
|
10230
|
-
timberStaticBuild(ctx),
|
|
10231
10358
|
timberFonts(ctx),
|
|
10232
10359
|
timberMdx(ctx),
|
|
10233
10360
|
timberRequestDep(ctx),
|
|
10234
10361
|
timberCacheTransform(ctx),
|
|
10235
10362
|
timberPrerenderSugar(ctx),
|
|
10236
10363
|
timberPrebuilt(ctx),
|
|
10364
|
+
timberStaticBuild(ctx),
|
|
10237
10365
|
timberContent(ctx),
|
|
10238
10366
|
timberServerBundle(),
|
|
10239
10367
|
timberCloudflareWasm(ctx),
|