@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.
Files changed (57) hide show
  1. package/dist/_chunks/{build-output-helper-DF5uScqS.js → build-output-helper-BC-Zg0_w.js} +38 -8
  2. package/dist/_chunks/build-output-helper-BC-Zg0_w.js.map +1 -0
  3. package/dist/_chunks/{cloudflare-AHoWYTYr.js → cloudflare-nlD9KhDj.js} +21 -3
  4. package/dist/_chunks/{cloudflare-AHoWYTYr.js.map → cloudflare-nlD9KhDj.js.map} +1 -1
  5. package/dist/_chunks/plugin-context---kTF5v8.js.map +1 -1
  6. package/dist/adapters/build-output-helper.d.ts +6 -0
  7. package/dist/adapters/build-output-helper.d.ts.map +1 -1
  8. package/dist/adapters/cloudflare-dev.js +1 -1
  9. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  10. package/dist/adapters/cloudflare.d.ts +10 -0
  11. package/dist/adapters/cloudflare.d.ts.map +1 -1
  12. package/dist/adapters/cloudflare.js +2 -2
  13. package/dist/adapters/nitro.d.ts +14 -0
  14. package/dist/adapters/nitro.d.ts.map +1 -1
  15. package/dist/adapters/nitro.js +14 -1
  16. package/dist/adapters/nitro.js.map +1 -1
  17. package/dist/adapters/shared.d.ts +1 -1
  18. package/dist/adapters/shared.d.ts.map +1 -1
  19. package/dist/adapters/types.d.ts +14 -0
  20. package/dist/adapters/types.d.ts.map +1 -1
  21. package/dist/client/internal.js +73 -11
  22. package/dist/client/internal.js.map +1 -1
  23. package/dist/client/rsc-fetch.d.ts +2 -0
  24. package/dist/client/rsc-fetch.d.ts.map +1 -1
  25. package/dist/index.js +324 -196
  26. package/dist/index.js.map +1 -1
  27. package/dist/plugin-context.d.ts +6 -0
  28. package/dist/plugin-context.d.ts.map +1 -1
  29. package/dist/plugins/adapter-build.d.ts.map +1 -1
  30. package/dist/plugins/build-report.d.ts +4 -3
  31. package/dist/plugins/build-report.d.ts.map +1 -1
  32. package/dist/plugins/static-build.d.ts +26 -2
  33. package/dist/plugins/static-build.d.ts.map +1 -1
  34. package/dist/server/rsc-entry/index.d.ts +12 -0
  35. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  36. package/dist/server/sitemap-generator.d.ts +1 -1
  37. package/dist/server/sitemap-generator.d.ts.map +1 -1
  38. package/dist/server/static-generator.d.ts +106 -0
  39. package/dist/server/static-generator.d.ts.map +1 -0
  40. package/docs/learn/13-configuration.mdx +5 -1
  41. package/package.json +1 -1
  42. package/src/adapters/build-output-helper.ts +34 -0
  43. package/src/adapters/cloudflare.ts +43 -1
  44. package/src/adapters/nitro.ts +58 -2
  45. package/src/adapters/shared.ts +38 -10
  46. package/src/adapters/types.ts +14 -0
  47. package/src/client/browser-entry/index.ts +7 -1
  48. package/src/client/rsc-fetch.ts +132 -19
  49. package/src/index.ts +1 -1
  50. package/src/plugin-context.ts +6 -0
  51. package/src/plugins/adapter-build.ts +5 -0
  52. package/src/plugins/build-report.ts +6 -12
  53. package/src/plugins/static-build.ts +264 -2
  54. package/src/server/rsc-entry/index.ts +24 -0
  55. package/src/server/sitemap-generator.ts +1 -1
  56. package/src/server/static-generator.ts +692 -0
  57. 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
- * In static mode, only pages with dynamic/catch-all segments are dynamic.
9320
- * API routes (route.ts) are always classified as function.
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
- if (outputMode === "server") return "dynamic";
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),