@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.
Files changed (64) 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/browser-entry/post-hydration.d.ts.map +1 -1
  22. package/dist/client/internal.js +75 -12
  23. package/dist/client/internal.js.map +1 -1
  24. package/dist/client/router.d.ts.map +1 -1
  25. package/dist/client/rsc-fetch.d.ts +2 -0
  26. package/dist/client/rsc-fetch.d.ts.map +1 -1
  27. package/dist/index.js +352 -200
  28. package/dist/index.js.map +1 -1
  29. package/dist/plugin-context.d.ts +6 -0
  30. package/dist/plugin-context.d.ts.map +1 -1
  31. package/dist/plugins/adapter-build.d.ts.map +1 -1
  32. package/dist/plugins/build-report.d.ts +4 -3
  33. package/dist/plugins/build-report.d.ts.map +1 -1
  34. package/dist/plugins/request-dep.d.ts +11 -0
  35. package/dist/plugins/request-dep.d.ts.map +1 -1
  36. package/dist/plugins/static-build.d.ts +26 -2
  37. package/dist/plugins/static-build.d.ts.map +1 -1
  38. package/dist/server/rsc-entry/index.d.ts +12 -0
  39. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  40. package/dist/server/sitemap-generator.d.ts +1 -1
  41. package/dist/server/sitemap-generator.d.ts.map +1 -1
  42. package/dist/server/static-generator.d.ts +106 -0
  43. package/dist/server/static-generator.d.ts.map +1 -0
  44. package/docs/learn/13-configuration.mdx +5 -1
  45. package/package.json +3 -3
  46. package/src/adapters/build-output-helper.ts +34 -0
  47. package/src/adapters/cloudflare.ts +43 -1
  48. package/src/adapters/nitro.ts +58 -2
  49. package/src/adapters/shared.ts +38 -10
  50. package/src/adapters/types.ts +14 -0
  51. package/src/client/browser-entry/index.ts +6 -7
  52. package/src/client/browser-entry/post-hydration.ts +16 -5
  53. package/src/client/router.ts +3 -1
  54. package/src/client/rsc-fetch.ts +136 -19
  55. package/src/index.ts +1 -1
  56. package/src/plugin-context.ts +6 -0
  57. package/src/plugins/adapter-build.ts +5 -0
  58. package/src/plugins/build-report.ts +6 -12
  59. package/src/plugins/request-dep.ts +47 -6
  60. package/src/plugins/static-build.ts +264 -2
  61. package/src/server/rsc-entry/index.ts +24 -0
  62. package/src/server/sitemap-generator.ts +1 -1
  63. package/src/server/static-generator.ts +692 -0
  64. 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.11.22/node_modules/srvx/dist/adapters/node.mjs
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
- /** Check if `localName.get(...)` appears anywhere in the module. */
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.object.type === "Identifier" && member.object.name === localName && member.property.type === "Identifier" && member.property.name === "get") return true;
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
- * In static mode, only pages with dynamic/catch-all segments are dynamic.
9296
- * 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.
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
- if (outputMode === "server") return "dynamic";
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),