@timber-js/app 0.2.0-alpha.157 → 0.2.0-alpha.159
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/{actions-CqE7SW7E.js → actions-CBbP1hHO.js} +22 -6
- package/dist/_chunks/actions-CBbP1hHO.js.map +1 -0
- package/dist/_chunks/{build-output-helper-C8q4Zpf5.js → build-output-helper-DF5uScqS.js} +4 -1
- package/dist/_chunks/{build-output-helper-C8q4Zpf5.js.map → build-output-helper-DF5uScqS.js.map} +1 -1
- package/dist/_chunks/cache-api-DsWEYwVZ.js +1120 -0
- package/dist/_chunks/cache-api-DsWEYwVZ.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-DcmQrzJG.js → cli-schema-sync-Bivx4Rn7.js} +4 -3
- package/dist/_chunks/{cli-schema-sync-DcmQrzJG.js.map → cli-schema-sync-Bivx4Rn7.js.map} +1 -1
- package/dist/_chunks/dist-BA3u1z3W.js +423 -0
- package/dist/_chunks/dist-BA3u1z3W.js.map +1 -0
- package/dist/_chunks/{logger-C5v1IMuh.js → logger-CrRmZMlo.js} +81 -56
- package/dist/_chunks/logger-CrRmZMlo.js.map +1 -0
- package/dist/_chunks/{plugin-context-DUl3dnCF.js → plugin-context-DeAxFRMq.js} +3 -36
- package/dist/_chunks/plugin-context-DeAxFRMq.js.map +1 -0
- package/dist/_chunks/sizeof-lXffQ1Vr.js +35 -0
- package/dist/_chunks/sizeof-lXffQ1Vr.js.map +1 -0
- package/dist/_chunks/{walkers-D5jCGPum.js → walkers-DXbJkeCY.js} +217 -19
- package/dist/_chunks/walkers-DXbJkeCY.js.map +1 -0
- package/dist/adapters/build-output-helper.d.ts.map +1 -1
- package/dist/adapters/cloudflare.d.ts +28 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +143 -9
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/nitro.js +1 -1
- package/dist/cache/cache-api.d.ts +2 -0
- package/dist/cache/cache-api.d.ts.map +1 -1
- package/dist/cache/handler-store.d.ts +2 -0
- package/dist/cache/handler-store.d.ts.map +1 -1
- package/dist/cache/index.d.ts +2 -0
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +3 -186
- package/dist/cache/redis-handler.d.ts +17 -5
- package/dist/cache/redis-handler.d.ts.map +1 -1
- package/dist/cache/singleflight.d.ts +1 -1
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cache/store.d.ts +53 -0
- package/dist/cache/store.d.ts.map +1 -0
- package/dist/cache/stores/cloudflare-kv.d.ts +38 -0
- package/dist/cache/stores/cloudflare-kv.d.ts.map +1 -0
- package/dist/cache/stores/cloudflare-kv.js +29 -0
- package/dist/cache/stores/cloudflare-kv.js.map +1 -0
- package/dist/cache/stores/memory.d.ts +42 -0
- package/dist/cache/stores/memory.d.ts.map +1 -0
- package/dist/cache/stores/memory.js +90 -0
- package/dist/cache/stores/memory.js.map +1 -0
- package/dist/cache/stores/redis.d.ts +69 -0
- package/dist/cache/stores/redis.d.ts.map +1 -0
- package/dist/cache/stores/redis.js +35 -0
- package/dist/cache/stores/redis.js.map +1 -0
- package/dist/cache/stores/vercel.d.ts +36 -0
- package/dist/cache/stores/vercel.d.ts.map +1 -0
- package/dist/cache/stores/vercel.js +33 -0
- package/dist/cache/stores/vercel.js.map +1 -0
- package/dist/cache/tag-aware-handler.d.ts +51 -0
- package/dist/cache/tag-aware-handler.d.ts.map +1 -0
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/cdn/cloudflare-purge.d.ts +23 -0
- package/dist/cdn/cloudflare-purge.d.ts.map +1 -0
- package/dist/cdn/fastly-purge.d.ts +25 -0
- package/dist/cdn/fastly-purge.d.ts.map +1 -0
- package/dist/cdn/headers.d.ts +82 -0
- package/dist/cdn/headers.d.ts.map +1 -0
- package/dist/cdn/index.d.ts +5 -0
- package/dist/cdn/index.d.ts.map +1 -0
- package/dist/cdn/purge-store.d.ts +23 -0
- package/dist/cdn/purge-store.d.ts.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/history.d.ts +0 -3
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.js +0 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +11 -86
- package/dist/client/internal.js.map +1 -1
- package/dist/client/navigation-api.d.ts +1 -3
- package/dist/client/navigation-api.d.ts.map +1 -1
- package/dist/client/router.d.ts +1 -4
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +1 -11
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +1 -3
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/use-cookie.d.ts.map +1 -1
- package/dist/config-types.d.ts +3 -1
- package/dist/config-types.d.ts.map +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/cookies/index.js +15 -11
- package/dist/cookies/index.js.map +1 -1
- package/dist/dev-tools/browser-logs.d.ts +28 -72
- package/dist/dev-tools/browser-logs.d.ts.map +1 -1
- package/dist/dev-tools/index.d.ts +1 -1
- package/dist/dev-tools/index.d.ts.map +1 -1
- package/dist/dev-tools/logger.d.ts.map +1 -1
- package/dist/dev-tools/terminal.d.ts.map +1 -1
- package/dist/fonts/transform.d.ts +4 -0
- package/dist/fonts/transform.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1269 -359
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +36 -0
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/build-report.d.ts +21 -0
- package/dist/plugins/build-report.d.ts.map +1 -1
- package/dist/plugins/cache.d.ts +5 -14
- package/dist/plugins/cache.d.ts.map +1 -1
- package/dist/plugins/callsite-ast.d.ts +95 -0
- package/dist/plugins/callsite-ast.d.ts.map +1 -0
- package/dist/plugins/callsite-jsx.d.ts +35 -0
- package/dist/plugins/callsite-jsx.d.ts.map +1 -0
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/prebuilt-capture.d.ts +60 -0
- package/dist/plugins/prebuilt-capture.d.ts.map +1 -0
- package/dist/plugins/prebuilt.d.ts +82 -0
- package/dist/plugins/prebuilt.d.ts.map +1 -0
- package/dist/plugins/prerender-sugar.d.ts +42 -0
- package/dist/plugins/prerender-sugar.d.ts.map +1 -0
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/export-detect.d.ts +56 -0
- package/dist/routing/export-detect.d.ts.map +1 -0
- package/dist/routing/index.js +2 -2
- package/dist/routing/manifest-codegen.d.ts.map +1 -1
- package/dist/rsc-runtime/rsc.d.ts +1 -1
- package/dist/rsc-runtime/rsc.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/actions.d.ts +0 -3
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +7 -0
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/cookie-parsing.d.ts +15 -1
- package/dist/server/cookie-parsing.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/form-data.d.ts +12 -0
- package/dist/server/form-data.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts +1 -0
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.d.ts +6 -1
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +563 -74
- package/dist/server/internal.js.map +1 -1
- package/dist/server/prebuilt/cache-key.d.ts +58 -0
- package/dist/server/prebuilt/cache-key.d.ts.map +1 -0
- package/dist/server/prebuilt/capture-state.d.ts +57 -0
- package/dist/server/prebuilt/capture-state.d.ts.map +1 -0
- package/dist/server/prebuilt/manifest-tag-index.d.ts +27 -0
- package/dist/server/prebuilt/manifest-tag-index.d.ts.map +1 -0
- package/dist/server/prebuilt/overlay.d.ts +100 -0
- package/dist/server/prebuilt/overlay.d.ts.map +1 -0
- package/dist/server/prebuilt/payload-source.d.ts +75 -0
- package/dist/server/prebuilt/payload-source.d.ts.map +1 -0
- package/dist/server/prebuilt/synthetic-store.d.ts +64 -0
- package/dist/server/prebuilt/synthetic-store.d.ts.map +1 -0
- package/dist/server/prebuilt-builder.d.ts +110 -0
- package/dist/server/prebuilt-builder.d.ts.map +1 -0
- package/dist/server/prebuilt-runtime.d.ts +101 -0
- package/dist/server/prebuilt-runtime.d.ts.map +1 -0
- package/dist/server/route-element-builder.d.ts +10 -8
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +0 -5
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +10 -0
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +3 -4
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/sitemap-generator.d.ts +6 -6
- package/dist/server/sitemap-generator.d.ts.map +1 -1
- package/dist/server/tracing.d.ts +37 -3
- package/dist/server/tracing.d.ts.map +1 -1
- package/docs/api/32-api-cache.mdx +10 -4
- package/docs/learn/00-introduction.mdx +8 -13
- package/docs/learn/02-pages-and-layouts.mdx +9 -0
- package/docs/learn/06-forms-and-actions.mdx +0 -4
- package/docs/learn/07-typed-routes.mdx +127 -0
- package/docs/learn/09-caching.mdx +12 -1
- package/docs/learn/10-middleware.mdx +1 -0
- package/docs/learn/11-error-handling.mdx +9 -0
- package/docs/learn/13-configuration.mdx +28 -0
- package/docs/learn/14-deploying.mdx +52 -0
- package/docs/more/06-instrumentation.mdx +27 -1
- package/docs/more/07-security.mdx +8 -2
- package/docs/more/08-developer-experience.mdx +134 -0
- package/package.json +31 -2
- package/src/adapters/build-output-helper.ts +10 -0
- package/src/adapters/cloudflare.ts +178 -4
- package/src/cache/cache-api.ts +60 -0
- package/src/cache/handler-store.ts +8 -0
- package/src/cache/index.ts +4 -0
- package/src/cache/redis-handler.ts +25 -13
- package/src/cache/singleflight.ts +11 -11
- package/src/cache/store.ts +71 -0
- package/src/cache/stores/cloudflare-kv.ts +52 -0
- package/src/cache/stores/memory.ts +125 -0
- package/src/cache/stores/redis.ts +97 -0
- package/src/cache/stores/vercel.ts +59 -0
- package/src/cache/tag-aware-handler.ts +233 -0
- package/src/cache/timber-cache.ts +17 -7
- package/src/cdn/cloudflare-purge.ts +50 -0
- package/src/cdn/fastly-purge.ts +63 -0
- package/src/cdn/headers.ts +127 -0
- package/src/cdn/index.ts +16 -0
- package/src/cdn/purge-store.ts +34 -0
- package/src/client/browser-entry/action-dispatch.ts +1 -10
- package/src/client/browser-entry/post-hydration.ts +0 -1
- package/src/client/browser-entry/router-init.ts +0 -5
- package/src/client/history.ts +0 -4
- package/src/client/navigation-api.ts +5 -11
- package/src/client/router.ts +9 -43
- package/src/client/rsc-fetch.ts +5 -27
- package/src/client/segment-cache.ts +1 -4
- package/src/client/use-cookie.ts +18 -12
- package/src/config-types.ts +3 -1
- package/src/config-validation.ts +1 -0
- package/src/dev-tools/browser-logs.ts +37 -273
- package/src/dev-tools/index.ts +2 -2
- package/src/dev-tools/instrumentation.ts +1 -1
- package/src/dev-tools/logger.ts +33 -51
- package/src/dev-tools/terminal.ts +21 -22
- package/src/fonts/transform.ts +4 -43
- package/src/index.ts +16 -3
- package/src/plugin-context.ts +36 -0
- package/src/plugins/build-report.ts +133 -1
- package/src/plugins/cache.ts +32 -131
- package/src/plugins/callsite-ast.ts +167 -0
- package/src/plugins/callsite-jsx.ts +243 -0
- package/src/plugins/entries.ts +23 -4
- package/src/plugins/fonts.ts +16 -2
- package/src/plugins/prebuilt-capture.ts +208 -0
- package/src/plugins/prebuilt.ts +673 -0
- package/src/plugins/prerender-sugar.ts +329 -0
- package/src/plugins/server-bundle.ts +12 -0
- package/src/plugins/shims.ts +9 -0
- package/src/plugins/static-build.ts +0 -1
- package/src/routing/codegen.ts +1 -21
- package/src/routing/convention-lint.ts +85 -49
- package/src/routing/export-detect.ts +304 -0
- package/src/routing/manifest-codegen.ts +2 -11
- package/src/rsc-runtime/rsc.ts +7 -0
- package/src/server/action-handler.ts +11 -27
- package/src/server/actions.ts +3 -4
- package/src/server/als-registry.ts +7 -0
- package/src/server/cookie-parsing.ts +75 -59
- package/src/server/deny-renderer.ts +7 -15
- package/src/server/form-data.ts +23 -7
- package/src/server/html-injectors.ts +11 -3
- package/src/server/internal.ts +17 -1
- package/src/server/prebuilt/cache-key.ts +224 -0
- package/src/server/prebuilt/capture-state.ts +89 -0
- package/src/server/prebuilt/manifest-tag-index.ts +46 -0
- package/src/server/prebuilt/overlay.ts +256 -0
- package/src/server/prebuilt/payload-source.ts +192 -0
- package/src/server/prebuilt/synthetic-store.ts +108 -0
- package/src/server/prebuilt-builder.ts +640 -0
- package/src/server/prebuilt-runtime.ts +706 -0
- package/src/server/route-element-builder.ts +40 -12
- package/src/server/rsc-entry/helpers.ts +0 -17
- package/src/server/rsc-entry/index.ts +83 -0
- package/src/server/rsc-entry/render-route.ts +6 -33
- package/src/server/rsc-entry/revalidate-renderer.ts +16 -11
- package/src/server/rsc-entry/rsc-payload.ts +5 -11
- package/src/server/sitemap-generator.ts +16 -10
- package/src/server/tracing.ts +88 -3
- package/dist/_chunks/actions-CqE7SW7E.js.map +0 -1
- package/dist/_chunks/cache-api-Cbyh3RW4.js +0 -401
- package/dist/_chunks/cache-api-Cbyh3RW4.js.map +0 -1
- package/dist/_chunks/logger-C5v1IMuh.js.map +0 -1
- package/dist/_chunks/plugin-context-DUl3dnCF.js.map +0 -1
- package/dist/_chunks/validation-CnKhGJsp.js +0 -113
- package/dist/_chunks/validation-CnKhGJsp.js.map +0 -1
- package/dist/_chunks/walkers-D5jCGPum.js.map +0 -1
- package/dist/cache/index.js.map +0 -1
- package/dist/client/head.d.ts +0 -16
- package/dist/client/head.d.ts.map +0 -1
- package/dist/client/navigation-api-types.d.ts +0 -90
- package/dist/client/navigation-api-types.d.ts.map +0 -1
- package/dist/server/client-module-map.d.ts +0 -47
- package/dist/server/client-module-map.d.ts.map +0 -1
- package/dist/shared/head-header.d.ts +0 -54
- package/dist/shared/head-header.d.ts.map +0 -1
- package/src/client/head.ts +0 -61
- package/src/client/navigation-api-types.ts +0 -112
- package/src/server/client-module-map.ts +0 -58
- package/src/shared/head-header.ts +0 -69
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as __esmMin, o as __exportAll, s as __toCommonJS } from "./_chunks/dist-BA3u1z3W.js";
|
|
2
2
|
import { c as isDynamicMetadataExtension, l as isMetadataRouteServePath, r as canonicalize } from "./_chunks/canonicalize-P41GR6tY.js";
|
|
3
3
|
import { t as matchUrlParts } from "./_chunks/tree-match-D2l830j2.js";
|
|
4
|
-
import { h as swallow } from "./_chunks/logger-
|
|
5
|
-
import { i as scanRoutes } from "./_chunks/cli-schema-sync-
|
|
6
|
-
import { a as generateRouteMap, i as validateSchemaAgainstRoutes, n as collectInterceptionRewrites, o as
|
|
4
|
+
import { h as swallow } from "./_chunks/logger-CrRmZMlo.js";
|
|
5
|
+
import { i as scanRoutes } from "./_chunks/cli-schema-sync-Bivx4Rn7.js";
|
|
6
|
+
import { a as generateRouteMap, c as fileHasDirective, d as getPrerenderExport, i as validateSchemaAgainstRoutes, l as fileHasExport, n as collectInterceptionRewrites, o as fileHasAnyExport, s as fileHasDefaultExport, t as collectLeafRoutes, u as fileHasStarExport } from "./_chunks/walkers-DXbJkeCY.js";
|
|
7
|
+
import { a as resolveAppDir, c as createNoopTimer, n as loadTimberConfigFile, o as resolveBuildDir, r as mergeFileConfig, s as resolveClientJavascript, t as createPluginContext } from "./_chunks/plugin-context-DeAxFRMq.js";
|
|
7
8
|
import { t as fnv1aHash } from "./_chunks/fast-hash-D6hIVt1Y.js";
|
|
8
9
|
import { t as formatSize } from "./_chunks/format-CfwjgPz9.js";
|
|
9
10
|
import { basename, dirname, extname, join, normalize, relative, resolve, win32 } from "node:path";
|
|
@@ -11,9 +12,10 @@ import { builtinModules, createRequire } from "node:module";
|
|
|
11
12
|
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
|
|
12
13
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
13
14
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
|
+
import { styleText } from "node:util";
|
|
14
16
|
import { constants, createGzip, gzipSync } from "node:zlib";
|
|
15
17
|
import { Readable, pipeline } from "node:stream";
|
|
16
|
-
import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
18
|
+
import { copyFile, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
17
19
|
import { createHash, randomUUID } from "node:crypto";
|
|
18
20
|
import { parseAst } from "vite";
|
|
19
21
|
import http from "node:http";
|
|
@@ -171,12 +173,8 @@ function classifyStack(stack, projectRoot) {
|
|
|
171
173
|
*
|
|
172
174
|
* Design doc: 21-dev-server.md §"Error Overlay"
|
|
173
175
|
*/
|
|
174
|
-
var
|
|
175
|
-
var
|
|
176
|
-
var DIM$2 = "\x1B[2m";
|
|
177
|
-
var RESET$2 = "\x1B[0m";
|
|
178
|
-
var BOLD$2 = "\x1B[1m";
|
|
179
|
-
var UNDERLINE = "\x1B[4m";
|
|
176
|
+
var noValidate$1 = { validateStream: false };
|
|
177
|
+
var style$1 = (fmt, text) => styleText(fmt, text, noValidate$1);
|
|
180
178
|
/**
|
|
181
179
|
* Wrap text in an OSC 8 hyperlink escape sequence.
|
|
182
180
|
*
|
|
@@ -198,7 +196,7 @@ function hyperlink(text, url) {
|
|
|
198
196
|
function fileLink(filePath, line, col) {
|
|
199
197
|
const display = line ? `${filePath}:${line}${col ? `:${col}` : ""}` : filePath;
|
|
200
198
|
const base = pathToFileURL(filePath).href;
|
|
201
|
-
return
|
|
199
|
+
return style$1(["cyan", "underline"], hyperlink(display, line ? `${base}:${line}${col ? `:${col}` : ""}` : base));
|
|
202
200
|
}
|
|
203
201
|
var BOX = {
|
|
204
202
|
topLeft: "╭",
|
|
@@ -214,12 +212,12 @@ var BOX = {
|
|
|
214
212
|
* @param lines - Content lines (no ANSI length calculation — keeps it simple)
|
|
215
213
|
* @param width - Box width (characters). Lines longer than this are not truncated.
|
|
216
214
|
*/
|
|
217
|
-
function box(lines,
|
|
215
|
+
function box(lines, borderFormat, width = 80) {
|
|
218
216
|
const bar = BOX.horizontal.repeat(width - 2);
|
|
219
217
|
const output = [];
|
|
220
|
-
output.push(`${
|
|
221
|
-
for (const line of lines) output.push(`${
|
|
222
|
-
output.push(`${
|
|
218
|
+
output.push(style$1(borderFormat, `${BOX.topLeft}${bar}${BOX.topRight}`));
|
|
219
|
+
for (const line of lines) output.push(`${style$1(borderFormat, BOX.vertical)} ${line}`);
|
|
220
|
+
output.push(style$1(borderFormat, `${BOX.bottomLeft}${bar}${BOX.bottomRight}`));
|
|
223
221
|
return output.join("\n");
|
|
224
222
|
}
|
|
225
223
|
/**
|
|
@@ -240,32 +238,32 @@ function formatTerminalError$1(error, phase, projectRoot) {
|
|
|
240
238
|
const appFrames = frames.filter((f) => f.type === "app");
|
|
241
239
|
const internalCount = frames.filter((f) => f.type !== "app").length;
|
|
242
240
|
const boxLines = [];
|
|
243
|
-
boxLines.push(`${
|
|
241
|
+
boxLines.push(style$1(["red", "bold"], `${PHASE_LABELS$1[phase]} Error`));
|
|
244
242
|
boxLines.push("");
|
|
245
|
-
for (const msgLine of error.message.split("\n")) boxLines.push(
|
|
243
|
+
for (const msgLine of error.message.split("\n")) boxLines.push(style$1("red", msgLine));
|
|
246
244
|
if (loc) {
|
|
247
245
|
boxLines.push("");
|
|
248
246
|
const relPath = loc.file.startsWith(projectRoot) ? loc.file.slice(projectRoot.length + 1) : loc.file;
|
|
249
|
-
boxLines.push(`${
|
|
247
|
+
boxLines.push(`${style$1("bold", "→")} ${fileLink(loc.file, loc.line, loc.column)} ${style$1("dim", `(${relPath})`)}`);
|
|
250
248
|
}
|
|
251
|
-
sections.push(box(boxLines,
|
|
249
|
+
sections.push(box(boxLines, "red"));
|
|
252
250
|
if (componentStack) {
|
|
253
251
|
sections.push("");
|
|
254
|
-
sections.push(` ${
|
|
255
|
-
for (const csLine of componentStack.trim().split("\n")) sections.push(` ${
|
|
252
|
+
sections.push(` ${style$1("bold", "Component Stack:")}`);
|
|
253
|
+
for (const csLine of componentStack.trim().split("\n")) sections.push(` ${style$1("dim", csLine.trim())}`);
|
|
256
254
|
}
|
|
257
255
|
if (appFrames.length > 0) {
|
|
258
256
|
sections.push("");
|
|
259
|
-
sections.push(` ${
|
|
257
|
+
sections.push(` ${style$1("bold", "Application Frames:")}`);
|
|
260
258
|
for (let i = 0; i < appFrames.length; i++) {
|
|
261
259
|
const f = appFrames[i];
|
|
262
260
|
if (f.file && f.line) {
|
|
263
|
-
const prefix = i === 0 ?
|
|
264
|
-
sections.push(` ${prefix} ${fileLink(f.file, f.line, f.col)} ${
|
|
261
|
+
const prefix = i === 0 ? style$1("bold", "▸") : " ";
|
|
262
|
+
sections.push(` ${prefix} ${fileLink(f.file, f.line, f.col)} ${style$1("dim", extractFnName(f.raw))}`);
|
|
265
263
|
} else sections.push(` ${f.raw}`);
|
|
266
264
|
}
|
|
267
265
|
}
|
|
268
|
-
if (internalCount > 0) sections.push(` ${
|
|
266
|
+
if (internalCount > 0) sections.push(` ${style$1("dim", `… ${internalCount} internal frame${internalCount !== 1 ? "s" : ""} hidden`)}`);
|
|
269
267
|
sections.push("");
|
|
270
268
|
return sections.join("\n");
|
|
271
269
|
}
|
|
@@ -1147,11 +1145,11 @@ function validateConfig(config) {
|
|
|
1147
1145
|
function formatConfigErrors(errors) {
|
|
1148
1146
|
if (errors.length === 0) return "";
|
|
1149
1147
|
const lines = [];
|
|
1150
|
-
lines.push(`${RED
|
|
1148
|
+
lines.push(`${RED}${BOLD}[timber]${RESET} ${RED}${errors.length} config error${errors.length !== 1 ? "s" : ""} in timber.config.ts:${RESET}`);
|
|
1151
1149
|
for (const err of errors) {
|
|
1152
1150
|
lines.push("");
|
|
1153
|
-
lines.push(` ${RED
|
|
1154
|
-
if (err.suggestion) lines.push(` ${DIM
|
|
1151
|
+
lines.push(` ${RED}✗${RESET} ${BOLD}${err.field}${RESET}: ${err.message}`);
|
|
1152
|
+
if (err.suggestion) lines.push(` ${DIM}${err.suggestion}${RESET}`);
|
|
1155
1153
|
}
|
|
1156
1154
|
return lines.join("\n");
|
|
1157
1155
|
}
|
|
@@ -1196,15 +1194,15 @@ function formatMissingPeers(results) {
|
|
|
1196
1194
|
if (missing.length === 0) return "";
|
|
1197
1195
|
const names = missing.map((r) => r.name);
|
|
1198
1196
|
const lines = [];
|
|
1199
|
-
lines.push(`${RED
|
|
1197
|
+
lines.push(`${RED}${BOLD}[timber]${RESET} ${RED}Missing required dependencies:${RESET}`);
|
|
1200
1198
|
lines.push("");
|
|
1201
|
-
for (const name of names) lines.push(` ${RED
|
|
1199
|
+
for (const name of names) lines.push(` ${RED}✗${RESET} ${name}`);
|
|
1202
1200
|
lines.push("");
|
|
1203
|
-
lines.push(` ${DIM
|
|
1204
|
-
lines.push(` ${BOLD
|
|
1201
|
+
lines.push(` ${DIM}Install with:${RESET}`);
|
|
1202
|
+
lines.push(` ${BOLD}pnpm add ${names.join(" ")}${RESET}`);
|
|
1205
1203
|
return lines.join("\n");
|
|
1206
1204
|
}
|
|
1207
|
-
var knownKeys, RED
|
|
1205
|
+
var knownKeys, RED, BOLD, DIM, RESET, VIRTUAL_MODULE_NAMES, REQUIRED_PEERS;
|
|
1208
1206
|
var init_config_validation = __esmMin((() => {
|
|
1209
1207
|
knownKeys = new Set(Object.keys({
|
|
1210
1208
|
output: true,
|
|
@@ -1212,6 +1210,7 @@ var init_config_validation = __esmMin((() => {
|
|
|
1212
1210
|
clientJavascript: true,
|
|
1213
1211
|
adapter: true,
|
|
1214
1212
|
cacheHandler: true,
|
|
1213
|
+
cdnPurge: true,
|
|
1215
1214
|
allowedOrigins: true,
|
|
1216
1215
|
csrf: true,
|
|
1217
1216
|
limits: true,
|
|
@@ -1232,10 +1231,10 @@ var init_config_validation = __esmMin((() => {
|
|
|
1232
1231
|
topLoader: true,
|
|
1233
1232
|
budget: true
|
|
1234
1233
|
}));
|
|
1235
|
-
RED
|
|
1236
|
-
BOLD
|
|
1237
|
-
DIM
|
|
1238
|
-
RESET
|
|
1234
|
+
RED = "\x1B[31m";
|
|
1235
|
+
BOLD = "\x1B[1m";
|
|
1236
|
+
DIM = "\x1B[2m";
|
|
1237
|
+
RESET = "\x1B[0m";
|
|
1239
1238
|
VIRTUAL_MODULE_NAMES = {
|
|
1240
1239
|
"virtual:timber-rsc-entry": "RSC entry (server component handler)",
|
|
1241
1240
|
"virtual:timber-ssr-entry": "SSR entry (HTML renderer)",
|
|
@@ -1832,7 +1831,8 @@ function generateConfigModule(ctx) {
|
|
|
1832
1831
|
renderTimeoutMs: ctx.config.renderTimeoutMs ?? 3e4,
|
|
1833
1832
|
sitemap: ctx.config.sitemap,
|
|
1834
1833
|
forms: serializeFormsConfig(ctx.config.forms),
|
|
1835
|
-
deploymentId: ctx.deploymentId ?? null
|
|
1834
|
+
deploymentId: ctx.deploymentId ?? null,
|
|
1835
|
+
devBrowserLogs: ctx.config.devBrowserLogs
|
|
1836
1836
|
};
|
|
1837
1837
|
return [
|
|
1838
1838
|
"// Auto-generated runtime config — do not edit.",
|
|
@@ -1914,8 +1914,8 @@ function detectConfigFile(root) {
|
|
|
1914
1914
|
* cannot be JSON-serialized into virtual:timber-config, so it must
|
|
1915
1915
|
* be loaded at runtime via dynamic import. See TIM-599.
|
|
1916
1916
|
*/
|
|
1917
|
-
function generateCacheHandlerModule(configPath, hasCacheHandler) {
|
|
1918
|
-
if (configPath && hasCacheHandler) return [
|
|
1917
|
+
function generateCacheHandlerModule(configPath, hasCacheHandler, hasCdnPurge) {
|
|
1918
|
+
if (configPath && (hasCacheHandler || hasCdnPurge)) return [
|
|
1919
1919
|
"// Auto-generated cache handler loader — do not edit.",
|
|
1920
1920
|
"// Generated by timber-entries plugin.",
|
|
1921
1921
|
"",
|
|
@@ -1923,15 +1923,25 @@ function generateCacheHandlerModule(configPath, hasCacheHandler) {
|
|
|
1923
1923
|
` const mod = await import(${JSON.stringify(configPath)});`,
|
|
1924
1924
|
` const config = mod.default ?? mod;`,
|
|
1925
1925
|
` return config.cacheHandler ?? null;`,
|
|
1926
|
+
`}`,
|
|
1927
|
+
"",
|
|
1928
|
+
`export async function loadCdnPurgeHandler() {`,
|
|
1929
|
+
` const mod = await import(${JSON.stringify(configPath)});`,
|
|
1930
|
+
` const config = mod.default ?? mod;`,
|
|
1931
|
+
` return config.cdnPurge ?? null;`,
|
|
1926
1932
|
`}`
|
|
1927
1933
|
].join("\n");
|
|
1928
1934
|
return [
|
|
1929
1935
|
"// Auto-generated cache handler loader — do not edit.",
|
|
1930
1936
|
"// Generated by timber-entries plugin.",
|
|
1931
|
-
"// No cacheHandler configured in timber.config.",
|
|
1937
|
+
"// No cacheHandler or cdnPurge configured in timber.config.",
|
|
1932
1938
|
"",
|
|
1933
1939
|
`export default async function loadCacheHandler() {`,
|
|
1934
1940
|
` return null;`,
|
|
1941
|
+
`}`,
|
|
1942
|
+
"",
|
|
1943
|
+
`export async function loadCdnPurgeHandler() {`,
|
|
1944
|
+
` return null;`,
|
|
1935
1945
|
`}`
|
|
1936
1946
|
].join("\n");
|
|
1937
1947
|
}
|
|
@@ -1972,7 +1982,7 @@ function timberEntries(ctx) {
|
|
|
1972
1982
|
load(id) {
|
|
1973
1983
|
if (id === RESOLVED_CONFIG_ID) return generateConfigModule(ctx);
|
|
1974
1984
|
if (id === RESOLVED_INSTRUMENTATION_ID) return generateInstrumentationModule(detectInstrumentationFile(ctx.root));
|
|
1975
|
-
if (id === RESOLVED_CACHE_HANDLER_ID) return generateCacheHandlerModule(detectConfigFile(ctx.root), ctx.config.cacheHandler != null);
|
|
1985
|
+
if (id === RESOLVED_CACHE_HANDLER_ID) return generateCacheHandlerModule(detectConfigFile(ctx.root), ctx.config.cacheHandler != null, ctx.config.cdnPurge != null);
|
|
1976
1986
|
return null;
|
|
1977
1987
|
}
|
|
1978
1988
|
};
|
|
@@ -2335,13 +2345,7 @@ function generateSearchParamsRegistryModule(tree) {
|
|
|
2335
2345
|
return lines.join("\n");
|
|
2336
2346
|
}
|
|
2337
2347
|
function fileHasSearchParamsExport(filePath) {
|
|
2338
|
-
|
|
2339
|
-
if (!existsSync(filePath)) return false;
|
|
2340
|
-
const content = readFileCached(filePath);
|
|
2341
|
-
return /export\s+(const|let|var)\s+searchParams\b/.test(content) || /export\s*\{[^}]*\bsearchParams\b[^}]*\}/.test(content);
|
|
2342
|
-
} catch {
|
|
2343
|
-
return false;
|
|
2344
|
-
}
|
|
2348
|
+
return fileHasExport(filePath, "searchParams");
|
|
2345
2349
|
}
|
|
2346
2350
|
/**
|
|
2347
2351
|
* Generate the virtual:timber-schema module.
|
|
@@ -2399,6 +2403,8 @@ function lintConventions(tree, appDir) {
|
|
|
2399
2403
|
checkRouteExports(tree.root, warnings);
|
|
2400
2404
|
checkRootLayout(tree.root, warnings);
|
|
2401
2405
|
checkDefaultExports(tree.root, warnings);
|
|
2406
|
+
checkErrorDirectives(tree.root, warnings);
|
|
2407
|
+
if (tree.globalError && isScriptExtension(tree.globalError.extension)) checkFileUseClientDirective(tree.globalError.filePath, "global-error", warnings);
|
|
2402
2408
|
return warnings;
|
|
2403
2409
|
}
|
|
2404
2410
|
/**
|
|
@@ -2455,28 +2461,18 @@ var HTTP_METHODS = [
|
|
|
2455
2461
|
"OPTIONS"
|
|
2456
2462
|
];
|
|
2457
2463
|
/**
|
|
2458
|
-
* Pattern to detect named exports that look like HTTP method handlers.
|
|
2459
|
-
* Matches: export function GET, export const GET, export async function POST, etc.
|
|
2460
|
-
* Also matches: export { GET } or re-exports like export { GET } from './handler'.
|
|
2461
|
-
*
|
|
2462
|
-
* The re-export branch uses word boundaries (\b) around method names to avoid
|
|
2463
|
-
* matching substrings (e.g. TARGET should not match GET). See TIM-795.
|
|
2464
|
-
*/
|
|
2465
|
-
var EXPORT_PATTERN = new RegExp(`export\\s+(?:async\\s+)?(?:function|const|let|var)\\s+(${HTTP_METHODS.join("|")})\\b|export\\s*\\{[^}]*\\b(${HTTP_METHODS.join("|")})\\b[^}]*\\}`);
|
|
2466
|
-
/**
|
|
2467
2464
|
* Warn when a route.ts file doesn't appear to export any recognized HTTP methods.
|
|
2468
2465
|
*
|
|
2469
|
-
* Uses
|
|
2470
|
-
*
|
|
2471
|
-
*
|
|
2466
|
+
* Uses AST-based detection via parseAst for precise matching.
|
|
2467
|
+
* Falls back silently when the file can't be parsed (syntax errors produce
|
|
2468
|
+
* no spurious warning — the TS compiler will catch those).
|
|
2472
2469
|
*/
|
|
2473
2470
|
function checkRouteExports(node, warnings) {
|
|
2474
2471
|
if (node.route) {
|
|
2475
2472
|
const filePath = node.route.filePath;
|
|
2476
2473
|
try {
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
const hint = /export\s+default\b/.test(source) ? " It looks like you have a default export. route.ts uses named exports\n for HTTP methods (GET, POST, etc.), not a default export.\n" : "";
|
|
2474
|
+
if (fileHasAnyExport(filePath, HTTP_METHODS).size === 0 && !fileHasStarExport(filePath)) {
|
|
2475
|
+
const hint = fileHasDefaultExport(filePath) ? " It looks like you have a default export. route.ts uses named exports\n for HTTP methods (GET, POST, etc.), not a default export.\n" : "";
|
|
2480
2476
|
warnings.push({
|
|
2481
2477
|
id: "ROUTE_NO_METHODS",
|
|
2482
2478
|
summary: `route.ts has no HTTP method exports: ${filePath}`,
|
|
@@ -2511,23 +2507,14 @@ function checkRootLayout(root, warnings) {
|
|
|
2511
2507
|
});
|
|
2512
2508
|
}
|
|
2513
2509
|
/**
|
|
2514
|
-
* Pattern to detect a default export in source code.
|
|
2515
|
-
* Matches: export default function, export default class, export default
|
|
2516
|
-
* Also matches: export { X as default }, export { default } from './Impl'
|
|
2517
|
-
*
|
|
2518
|
-
* The third alternative catches bare `default` re-exports (TIM-790).
|
|
2519
|
-
* The negative lookahead (?!\s+as\b) prevents matching `export { default as X }`
|
|
2520
|
-
* which is a named export, not a default export (TIM-806).
|
|
2521
|
-
*/
|
|
2522
|
-
var DEFAULT_EXPORT_PATTERN = /export\s+default\b|export\s*\{[^}]*\bas\s+default\b|export\s*\{[^}]*\bdefault\b(?!\s+as\b)[^}]*\}/;
|
|
2523
|
-
/**
|
|
2524
2510
|
* Warn when page.tsx or layout.tsx files don't have a default export.
|
|
2525
2511
|
*
|
|
2526
2512
|
* Without a default export:
|
|
2527
2513
|
* - page.tsx: the page renders nothing (empty content)
|
|
2528
2514
|
* - layout.tsx: the layout module loads but has no component to render
|
|
2529
2515
|
*
|
|
2530
|
-
* Uses
|
|
2516
|
+
* Uses AST-based detection via parseAst for precise matching. Falls back
|
|
2517
|
+
* silently when the file can't be parsed (no spurious warning on syntax errors).
|
|
2531
2518
|
*/
|
|
2532
2519
|
function checkDefaultExports(node, warnings) {
|
|
2533
2520
|
if (node.page && isScriptExtension(node.page.extension)) checkFileDefaultExport(node.page.filePath, "page", warnings);
|
|
@@ -2540,8 +2527,7 @@ function isScriptExtension(ext) {
|
|
|
2540
2527
|
}
|
|
2541
2528
|
function checkFileDefaultExport(filePath, fileType, warnings) {
|
|
2542
2529
|
try {
|
|
2543
|
-
|
|
2544
|
-
if (!DEFAULT_EXPORT_PATTERN.test(source)) warnings.push({
|
|
2530
|
+
if (!fileHasDefaultExport(filePath)) warnings.push({
|
|
2545
2531
|
id: `NO_DEFAULT_EXPORT:${filePath}`,
|
|
2546
2532
|
summary: `${fileType}.tsx has no default export: ${filePath}`,
|
|
2547
2533
|
details: ` File: ${filePath}\n\n ${fileType}.tsx files must export a default React component.\n Without a default export, the ${fileType === "page" ? "page renders nothing" : "layout has no component to wrap children"}.\n\n To fix: Add a default export:\n\n export default function My${fileType === "page" ? "Page" : "Layout"}(${fileType === "layout" ? "{ children }" : ""}) {\n return ${fileType === "layout" ? "<div>{children}</div>" : "<h1>Hello</h1>"};\n }\n`,
|
|
@@ -2552,6 +2538,39 @@ function checkFileDefaultExport(filePath, fileType, warnings) {
|
|
|
2552
2538
|
}
|
|
2553
2539
|
}
|
|
2554
2540
|
/**
|
|
2541
|
+
* Warn when error.tsx or global-error.tsx is missing 'use client'.
|
|
2542
|
+
*
|
|
2543
|
+
* React's error boundary API requires client components. Without the directive,
|
|
2544
|
+
* the error boundary silently fails — the component renders as a server component
|
|
2545
|
+
* with no error catching. Users discover this only via cryptic runtime errors.
|
|
2546
|
+
*/
|
|
2547
|
+
function checkErrorDirectives(node, warnings) {
|
|
2548
|
+
if (node.error && isScriptExtension(node.error.extension)) checkFileUseClientDirective(node.error.filePath, "error", warnings);
|
|
2549
|
+
for (const child of node.children) checkErrorDirectives(child, warnings);
|
|
2550
|
+
for (const slot of Object.values(node.slots)) checkErrorDirectives(slot, warnings);
|
|
2551
|
+
}
|
|
2552
|
+
function checkFileUseClientDirective(filePath, fileType, warnings) {
|
|
2553
|
+
try {
|
|
2554
|
+
if (!fileHasDirective(filePath, "use client")) warnings.push({
|
|
2555
|
+
id: `ERROR_NO_USE_CLIENT:${filePath}`,
|
|
2556
|
+
summary: `${fileType}.tsx is missing 'use client': ${filePath}`,
|
|
2557
|
+
details: ` File: ${filePath}\n\n ${fileType}.tsx must start with 'use client' because React error\n boundaries require client components. Without it, the error boundary
|
|
2558
|
+
silently fails and errors are uncaught.
|
|
2559
|
+
|
|
2560
|
+
To fix: Add 'use client' as the first line of the file:
|
|
2561
|
+
|
|
2562
|
+
'use client';
|
|
2563
|
+
|
|
2564
|
+
export default function ${fileType === "global-error" ? "GlobalError" : "ErrorBoundary"}({ error, reset }) {\n return <div><h2>Something went wrong</h2><button onClick={reset}>Try again</button></div>;
|
|
2565
|
+
}
|
|
2566
|
+
`,
|
|
2567
|
+
level: "warn"
|
|
2568
|
+
});
|
|
2569
|
+
} catch (err) {
|
|
2570
|
+
swallow(err, `convention-lint: unreadable ${fileType} file ${filePath}`);
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
/**
|
|
2555
2574
|
* Check if the app/ directory exists. Called before scanning.
|
|
2556
2575
|
* Returns a warning if missing, or null if the directory exists.
|
|
2557
2576
|
*/
|
|
@@ -2571,11 +2590,8 @@ function checkAppDirExists(appDir) {
|
|
|
2571
2590
|
level: "error"
|
|
2572
2591
|
};
|
|
2573
2592
|
}
|
|
2574
|
-
var
|
|
2575
|
-
var
|
|
2576
|
-
var BOLD = "\x1B[1m";
|
|
2577
|
-
var DIM = "\x1B[2m";
|
|
2578
|
-
var RESET = "\x1B[0m";
|
|
2593
|
+
var noValidate = { validateStream: false };
|
|
2594
|
+
var style = (fmt, text) => styleText(fmt, text, noValidate);
|
|
2579
2595
|
/**
|
|
2580
2596
|
* Format warnings for terminal output.
|
|
2581
2597
|
*
|
|
@@ -2587,20 +2603,20 @@ function formatConventionWarnings(warnings) {
|
|
|
2587
2603
|
const warns = warnings.filter((w) => w.level === "warn");
|
|
2588
2604
|
const lines = [];
|
|
2589
2605
|
if (errors.length > 0) {
|
|
2590
|
-
lines.push(`${
|
|
2606
|
+
lines.push(`${style(["red", "bold"], "[timber]")} ${style("red", `${errors.length} configuration error${errors.length !== 1 ? "s" : ""}:`)}`);
|
|
2591
2607
|
for (const e of errors) {
|
|
2592
2608
|
lines.push("");
|
|
2593
|
-
lines.push(` ${
|
|
2594
|
-
lines.push(
|
|
2609
|
+
lines.push(` ${style("red", "✗")} ${e.summary}`);
|
|
2610
|
+
lines.push(style("dim", e.details));
|
|
2595
2611
|
}
|
|
2596
2612
|
}
|
|
2597
2613
|
if (warns.length > 0) {
|
|
2598
2614
|
if (errors.length > 0) lines.push("");
|
|
2599
|
-
lines.push(`${
|
|
2615
|
+
lines.push(`${style(["yellow", "bold"], "[timber]")} ${style("yellow", `${warns.length} configuration warning${warns.length !== 1 ? "s" : ""}:`)}`);
|
|
2600
2616
|
for (const w of warns) {
|
|
2601
2617
|
lines.push("");
|
|
2602
|
-
lines.push(` ${
|
|
2603
|
-
lines.push(
|
|
2618
|
+
lines.push(` ${style("yellow", "⚠")} ${w.summary}`);
|
|
2619
|
+
lines.push(style("dim", w.details));
|
|
2604
2620
|
}
|
|
2605
2621
|
}
|
|
2606
2622
|
return lines.join("\n");
|
|
@@ -2894,6 +2910,13 @@ var SUBPATH_SRC_MAP = {
|
|
|
2894
2910
|
"params": "params/index.ts",
|
|
2895
2911
|
"search-params": "search-params/index.ts",
|
|
2896
2912
|
"segment-params": "segment-params/index.ts",
|
|
2913
|
+
"cache/stores/memory": "cache/stores/memory.ts",
|
|
2914
|
+
"cache/stores/redis": "cache/stores/redis.ts",
|
|
2915
|
+
"cache/stores/cloudflare-kv": "cache/stores/cloudflare-kv.ts",
|
|
2916
|
+
"cache/stores/vercel": "cache/stores/vercel.ts",
|
|
2917
|
+
"cdn": "cdn/index.ts",
|
|
2918
|
+
"cdn/cloudflare": "cdn/cloudflare-purge.ts",
|
|
2919
|
+
"cdn/fastly": "cdn/fastly-purge.ts",
|
|
2897
2920
|
"adapters/cloudflare": "adapters/cloudflare.ts",
|
|
2898
2921
|
"adapters/cloudflare/cache": "adapters/cloudflare-kv-cache.ts",
|
|
2899
2922
|
"adapters/cloudflare/dev": "adapters/cloudflare-dev.ts",
|
|
@@ -5666,30 +5689,6 @@ function buildFontResultLiteral(extracted) {
|
|
|
5666
5689
|
return extracted.variable ? `{ className: "${extracted.className}", style: { fontFamily: "${extracted.fontFamily}" }, variable: "${extracted.variable}" }` : `{ className: "${extracted.className}", style: { fontFamily: "${extracted.fontFamily}" } }`;
|
|
5667
5690
|
}
|
|
5668
5691
|
/**
|
|
5669
|
-
* File extensions that may contain real font imports. Only JS/TS module
|
|
5670
|
-
* variants are scanned. Excluding non-JS extensions (.mdx, .md, .css,
|
|
5671
|
-
* .json, etc.) prevents false-positive matches on text that mentions a
|
|
5672
|
-
* font specifier inside documentation, code fences, or asset metadata.
|
|
5673
|
-
*
|
|
5674
|
-
* See TIM-840.
|
|
5675
|
-
*/
|
|
5676
|
-
var FONT_IMPORT_SCANNABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
5677
|
-
".js",
|
|
5678
|
-
".jsx",
|
|
5679
|
-
".ts",
|
|
5680
|
-
".tsx",
|
|
5681
|
-
".mjs",
|
|
5682
|
-
".cjs",
|
|
5683
|
-
".mts",
|
|
5684
|
-
".cts"
|
|
5685
|
-
]);
|
|
5686
|
-
function hasScannableExtension(id) {
|
|
5687
|
-
const path = id.split("?", 1)[0];
|
|
5688
|
-
const dot = path.lastIndexOf(".");
|
|
5689
|
-
if (dot === -1) return false;
|
|
5690
|
-
return FONT_IMPORT_SCANNABLE_EXTENSIONS.has(path.slice(dot).toLowerCase());
|
|
5691
|
-
}
|
|
5692
|
-
/**
|
|
5693
5692
|
* Run the timber-fonts transform pass on a single source file.
|
|
5694
5693
|
*
|
|
5695
5694
|
* Parses the source once with OXC (via vite.parseAst), walks the AST to find font imports
|
|
@@ -5698,13 +5697,12 @@ function hasScannableExtension(id) {
|
|
|
5698
5697
|
* source map alongside the transformed code.
|
|
5699
5698
|
*
|
|
5700
5699
|
* Returns `null` if the file does not import from any font virtual module.
|
|
5700
|
+
*
|
|
5701
|
+
* Pre-filtering (extension allow-list, node_modules exclusion, and code
|
|
5702
|
+
* substring checks) is handled by Vite 8's rolldown-native `transform.filter`
|
|
5703
|
+
* in the plugin hook definition (plugins/fonts.ts). See TIM-840, TIM-1147.
|
|
5701
5704
|
*/
|
|
5702
5705
|
function runFontsTransform(code, id, pipeline, emitError) {
|
|
5703
|
-
if (id.startsWith("\0") || id.includes("node_modules")) return null;
|
|
5704
|
-
if (!hasScannableExtension(id)) return null;
|
|
5705
|
-
const hasGoogleImport = code.includes("@timber/fonts/google") || code.includes("@timber-js/app/fonts/google") || code.includes("next/font/google");
|
|
5706
|
-
const hasLocalImport = code.includes("@timber/fonts/local") || code.includes("@timber-js/app/fonts/local") || code.includes("next/font/local");
|
|
5707
|
-
if (!hasGoogleImport && !hasLocalImport) return null;
|
|
5708
5706
|
const analysis = findFontCallSites(code);
|
|
5709
5707
|
if (!analysis) return null;
|
|
5710
5708
|
if (analysis.dynamicCall) emitError(`Font function calls must be statically analyzable. Found dynamic call: ${analysis.dynamicCall}. Pass a literal object with string/array values instead.`);
|
|
@@ -6052,9 +6050,27 @@ function timberFonts(ctx) {
|
|
|
6052
6050
|
*
|
|
6053
6051
|
* Delegates to `runFontsTransform`. Errors raised through the
|
|
6054
6052
|
* `this.error` callback are surfaced as Vite plugin errors.
|
|
6053
|
+
*
|
|
6054
|
+
* The `filter` option uses Vite 8's rolldown-native hook filters to
|
|
6055
|
+
* skip non-JS/TS files and files that don't mention any font module
|
|
6056
|
+
* before crossing into JS — replacing the hand-rolled pre-filter that
|
|
6057
|
+
* was previously inside `runFontsTransform` (TIM-840, TIM-1147).
|
|
6055
6058
|
*/
|
|
6056
|
-
transform
|
|
6057
|
-
|
|
6059
|
+
transform: {
|
|
6060
|
+
filter: {
|
|
6061
|
+
id: {
|
|
6062
|
+
include: /^[^?]*\.(?:js|jsx|ts|tsx|mjs|cjs|mts|cts)(?:\?|$)/,
|
|
6063
|
+
exclude: /(?:^\0|(?:^|\/)node_modules\/)/
|
|
6064
|
+
},
|
|
6065
|
+
code: [
|
|
6066
|
+
"@timber/fonts/",
|
|
6067
|
+
"@timber-js/app/fonts/",
|
|
6068
|
+
"next/font/"
|
|
6069
|
+
]
|
|
6070
|
+
},
|
|
6071
|
+
handler(code, id) {
|
|
6072
|
+
return runFontsTransform(code, id, pipeline, (msg) => this.error(msg));
|
|
6073
|
+
}
|
|
6058
6074
|
},
|
|
6059
6075
|
/**
|
|
6060
6076
|
* Emit font binaries and build-manifest entries.
|
|
@@ -6136,7 +6152,6 @@ function timberStaticBuild(ctx) {
|
|
|
6136
6152
|
*
|
|
6137
6153
|
* In static mode, we check every app/ file for:
|
|
6138
6154
|
* - Dynamic API usage (getCookies(), getHeaders()) → build error
|
|
6139
|
-
* - When client JS disabled: 'use client' / 'use server' directives → build error
|
|
6140
6155
|
*/
|
|
6141
6156
|
transform(code, id) {
|
|
6142
6157
|
if (!(ctx.config.output === "static")) return null;
|
|
@@ -6532,180 +6547,32 @@ function timberDevLogs(_ctx) {
|
|
|
6532
6547
|
}
|
|
6533
6548
|
//#endregion
|
|
6534
6549
|
//#region src/dev-tools/browser-logs.ts
|
|
6535
|
-
/** Max message size in bytes before truncation. */
|
|
6536
|
-
var MAX_MESSAGE_BYTES = 2048;
|
|
6537
|
-
/** HMR event name for browser→server log forwarding. */
|
|
6538
|
-
var HMR_EVENT = "timber:browser-log";
|
|
6539
|
-
/** ANSI color codes for terminal output. */
|
|
6540
|
-
var COLORS = {
|
|
6541
|
-
red: "\x1B[31m",
|
|
6542
|
-
yellow: "\x1B[33m",
|
|
6543
|
-
blue: "\x1B[34m",
|
|
6544
|
-
dim: "\x1B[2m",
|
|
6545
|
-
reset: "\x1B[0m"
|
|
6546
|
-
};
|
|
6547
|
-
/** Level severity ordering for threshold comparison. */
|
|
6548
|
-
var LEVEL_SEVERITY = {
|
|
6549
|
-
none: 0,
|
|
6550
|
-
info: 1,
|
|
6551
|
-
warn: 2,
|
|
6552
|
-
error: 3
|
|
6553
|
-
};
|
|
6554
|
-
/**
|
|
6555
|
-
* Format a browser log payload for server terminal output.
|
|
6556
|
-
*
|
|
6557
|
-
* Produces color-coded output like:
|
|
6558
|
-
* [browser:error] Uncaught TypeError: x is not a function
|
|
6559
|
-
* at App (app.tsx:10:5)
|
|
6560
|
-
* [browser:warn] Deprecation warning (app/page.tsx:42:12)
|
|
6561
|
-
*/
|
|
6562
|
-
function formatBrowserLog(payload) {
|
|
6563
|
-
const { level, message, stack, source } = payload;
|
|
6564
|
-
let output = `${`${level === "error" ? COLORS.red : level === "warn" ? COLORS.yellow : COLORS.blue}[browser:${level}]${COLORS.reset}`} ${message}${source ? ` ${COLORS.dim}(${source})${COLORS.reset}` : ""}`;
|
|
6565
|
-
if (stack) {
|
|
6566
|
-
const indented = stack.split("\n").map((line) => ` ${COLORS.dim}${line}${COLORS.reset}`).join("\n");
|
|
6567
|
-
output += `\n${indented}`;
|
|
6568
|
-
}
|
|
6569
|
-
return output;
|
|
6570
|
-
}
|
|
6571
|
-
/**
|
|
6572
|
-
* Check if a log at `level` should be forwarded given the configured threshold.
|
|
6573
|
-
*
|
|
6574
|
-
* The threshold acts as a minimum severity:
|
|
6575
|
-
* - 'error' → only errors
|
|
6576
|
-
* - 'warn' → errors + warnings
|
|
6577
|
-
* - 'info' → errors + warnings + info
|
|
6578
|
-
* - 'none' → nothing
|
|
6579
|
-
*/
|
|
6580
|
-
function shouldForwardLevel(level, threshold) {
|
|
6581
|
-
if (threshold === "none") return false;
|
|
6582
|
-
return LEVEL_SEVERITY[level] >= LEVEL_SEVERITY[threshold];
|
|
6583
|
-
}
|
|
6584
6550
|
/**
|
|
6585
|
-
*
|
|
6586
|
-
*
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
*
|
|
6594
|
-
*
|
|
6595
|
-
*
|
|
6596
|
-
*
|
|
6597
|
-
*
|
|
6598
|
-
*
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
if (
|
|
6606
|
-
|
|
6607
|
-
"error",
|
|
6608
|
-
"warn",
|
|
6609
|
-
"info"
|
|
6610
|
-
].filter((l) => shouldForwardLevel(l, threshold));
|
|
6611
|
-
if (levels.length === 0) return "";
|
|
6612
|
-
return `
|
|
6613
|
-
(function() {
|
|
6614
|
-
if (!import.meta.hot) return;
|
|
6615
|
-
var MAX_BYTES = ${MAX_MESSAGE_BYTES};
|
|
6616
|
-
var levels = ${JSON.stringify(levels)};
|
|
6617
|
-
var originals = {};
|
|
6618
|
-
|
|
6619
|
-
function serialize(arg) {
|
|
6620
|
-
if (arg === null) return 'null';
|
|
6621
|
-
if (arg === undefined) return 'undefined';
|
|
6622
|
-
if (arg instanceof Error) return arg.stack || arg.message || String(arg);
|
|
6623
|
-
if (typeof arg === 'object') {
|
|
6624
|
-
try { return JSON.stringify(arg); } catch(e) { return String(arg); }
|
|
6625
|
-
}
|
|
6626
|
-
return String(arg);
|
|
6627
|
-
}
|
|
6628
|
-
|
|
6629
|
-
function truncate(s) {
|
|
6630
|
-
if (s.length <= MAX_BYTES) return s;
|
|
6631
|
-
return s.slice(0, MAX_BYTES) + '... [truncated ' + (s.length - MAX_BYTES) + ' bytes]';
|
|
6632
|
-
}
|
|
6633
|
-
|
|
6634
|
-
levels.forEach(function(level) {
|
|
6635
|
-
originals[level] = console[level];
|
|
6636
|
-
console[level] = function() {
|
|
6637
|
-
originals[level].apply(console, arguments);
|
|
6638
|
-
try {
|
|
6639
|
-
var args = Array.prototype.slice.call(arguments);
|
|
6640
|
-
var firstArg = args[0];
|
|
6641
|
-
var stack = null;
|
|
6642
|
-
var source = null;
|
|
6643
|
-
|
|
6644
|
-
if (firstArg instanceof Error) {
|
|
6645
|
-
stack = firstArg.stack || null;
|
|
6646
|
-
source = null;
|
|
6647
|
-
}
|
|
6648
|
-
|
|
6649
|
-
var message = args.map(serialize).join(' ');
|
|
6650
|
-
message = truncate(message);
|
|
6651
|
-
|
|
6652
|
-
import.meta.hot.send('${HMR_EVENT}', {
|
|
6653
|
-
level: level,
|
|
6654
|
-
message: message,
|
|
6655
|
-
stack: stack,
|
|
6656
|
-
source: source,
|
|
6657
|
-
timestamp: Date.now()
|
|
6658
|
-
});
|
|
6659
|
-
} catch(e) {
|
|
6660
|
-
// Never let log forwarding break the page
|
|
6661
|
-
}
|
|
6662
|
-
};
|
|
6663
|
-
});
|
|
6664
|
-
})();
|
|
6665
|
-
`.trim();
|
|
6666
|
-
}
|
|
6667
|
-
/**
|
|
6668
|
-
* Create the timber-dev-browser-logs Vite plugin.
|
|
6669
|
-
*
|
|
6670
|
-
* - `configureServer`: Listens for HMR messages and prints them to the terminal
|
|
6671
|
-
* - `transformIndexHtml`: Injects the client-side interception script
|
|
6672
|
-
*
|
|
6673
|
-
* Only active during `vite dev` (apply: 'serve').
|
|
6674
|
-
*/
|
|
6675
|
-
function timberDevBrowserLogs(ctx) {
|
|
6551
|
+
* Map timber's devBrowserLogs threshold to Vite 8's server.forwardConsole.
|
|
6552
|
+
*
|
|
6553
|
+
* Vite defaults forwardConsole OFF for non-agent dev; timber enables it
|
|
6554
|
+
* based on the configured threshold (default: 'warn'). The threshold acts
|
|
6555
|
+
* as a minimum severity — 'warn' forwards errors + warnings, 'info'
|
|
6556
|
+
* forwards everything, 'error' forwards only errors, 'none' disables.
|
|
6557
|
+
*
|
|
6558
|
+
* Vite's unhandledErrors is explicitly disabled — timber's own
|
|
6559
|
+
* setupClientErrorForwarding (browser-dev.ts) already catches window
|
|
6560
|
+
* error/unhandledrejection, filters framework control-flow signals
|
|
6561
|
+
* (redirect/deny digests) and page-unload aborts (TIM-838), and
|
|
6562
|
+
* echoes errors to the Vite overlay. Enabling Vite's unhandledErrors
|
|
6563
|
+
* would duplicate terminal output for normal errors and reintroduce
|
|
6564
|
+
* unfiltered noise for framework signals.
|
|
6565
|
+
*/
|
|
6566
|
+
function resolveForwardConsole(threshold) {
|
|
6567
|
+
const resolved = threshold ?? "warn";
|
|
6568
|
+
if (resolved === "none") return false;
|
|
6569
|
+
const logLevels = [];
|
|
6570
|
+
if (resolved === "info") logLevels.push("info", "warn", "error");
|
|
6571
|
+
else if (resolved === "warn") logLevels.push("warn", "error");
|
|
6572
|
+
else logLevels.push("error");
|
|
6676
6573
|
return {
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
configureServer(server) {
|
|
6680
|
-
const threshold = ctx.config.devBrowserLogs ?? "warn";
|
|
6681
|
-
if (threshold === "none") {
|
|
6682
|
-
delete globalThis.__timber_dev_browser_log_script;
|
|
6683
|
-
return;
|
|
6684
|
-
}
|
|
6685
|
-
const script = generateClientScript(threshold);
|
|
6686
|
-
if (script) globalThis.__timber_dev_browser_log_script = `<script type="module">${script}<\/script>`;
|
|
6687
|
-
server.hot.on(HMR_EVENT, (payload) => {
|
|
6688
|
-
try {
|
|
6689
|
-
if (!shouldForwardLevel(payload.level, threshold)) return;
|
|
6690
|
-
payload.message = truncateMessage(payload.message, MAX_MESSAGE_BYTES);
|
|
6691
|
-
const formatted = formatBrowserLog(payload);
|
|
6692
|
-
if (payload.level === "error") process.stderr.write(formatted + "\n");
|
|
6693
|
-
else process.stdout.write(formatted + "\n");
|
|
6694
|
-
} catch (e) {
|
|
6695
|
-
process.stderr.write(`[timber] log forwarding failed: ${e instanceof Error ? e.message : e}\n`);
|
|
6696
|
-
}
|
|
6697
|
-
});
|
|
6698
|
-
},
|
|
6699
|
-
transformIndexHtml() {
|
|
6700
|
-
const script = generateClientScript(ctx.config.devBrowserLogs ?? "warn");
|
|
6701
|
-
if (!script) return [];
|
|
6702
|
-
return [{
|
|
6703
|
-
tag: "script",
|
|
6704
|
-
attrs: { type: "module" },
|
|
6705
|
-
children: script,
|
|
6706
|
-
injectTo: "head"
|
|
6707
|
-
}];
|
|
6708
|
-
}
|
|
6574
|
+
unhandledErrors: false,
|
|
6575
|
+
logLevels
|
|
6709
6576
|
};
|
|
6710
6577
|
}
|
|
6711
6578
|
//#endregion
|
|
@@ -6939,8 +6806,16 @@ function clientChunkGroup(meta, appDir, root, sharedModuleFacades) {
|
|
|
6939
6806
|
*
|
|
6940
6807
|
* Design docs: design/11-platform.md, design/25-production-deployments.md
|
|
6941
6808
|
*/
|
|
6809
|
+
var NODE_BUILTINS = builtinModules.flatMap((m) => [m, `node:${m}`]);
|
|
6810
|
+
var CLOUDFLARE_BUILTINS = [
|
|
6811
|
+
"cloudflare:workers",
|
|
6812
|
+
"cloudflare:sockets",
|
|
6813
|
+
"cloudflare:email",
|
|
6814
|
+
"cloudflare:workflows"
|
|
6815
|
+
];
|
|
6942
6816
|
var BUILD_ONLY_EXTERNALS = [
|
|
6943
|
-
...
|
|
6817
|
+
...NODE_BUILTINS,
|
|
6818
|
+
...CLOUDFLARE_BUILTINS,
|
|
6944
6819
|
"vite",
|
|
6945
6820
|
"nitro",
|
|
6946
6821
|
"rollup",
|
|
@@ -6970,6 +6845,7 @@ function timberServerBundle() {
|
|
|
6970
6845
|
noExternal: true,
|
|
6971
6846
|
external: BUILD_ONLY_EXTERNALS
|
|
6972
6847
|
},
|
|
6848
|
+
build: { rolldownOptions: { external: [/^cloudflare:/] } },
|
|
6973
6849
|
define: serverDefine
|
|
6974
6850
|
},
|
|
6975
6851
|
ssr: {
|
|
@@ -6977,6 +6853,7 @@ function timberServerBundle() {
|
|
|
6977
6853
|
noExternal: true,
|
|
6978
6854
|
external: BUILD_ONLY_EXTERNALS
|
|
6979
6855
|
},
|
|
6856
|
+
build: { rolldownOptions: { external: [/^cloudflare:/] } },
|
|
6980
6857
|
define: serverDefine
|
|
6981
6858
|
}
|
|
6982
6859
|
}
|
|
@@ -7026,6 +6903,96 @@ function timberServerBundle() {
|
|
|
7026
6903
|
];
|
|
7027
6904
|
}
|
|
7028
6905
|
//#endregion
|
|
6906
|
+
//#region src/plugins/callsite-ast.ts
|
|
6907
|
+
/**
|
|
6908
|
+
* Shared AST helpers for callsite-ID transforms.
|
|
6909
|
+
*
|
|
6910
|
+
* Both timber-cache-transform (plugins/cache.ts) and timber-prebuilt
|
|
6911
|
+
* (plugins/prebuilt.ts) detect calls on bindings imported from the cache
|
|
6912
|
+
* module and stamp them with content-derived stable IDs. The node shapes,
|
|
6913
|
+
* import-binding resolution, recursive call walk, and ID derivation are
|
|
6914
|
+
* identical — only the callee shape and rewrite differ per plugin.
|
|
6915
|
+
*
|
|
6916
|
+
* See TIM-997, TIM-1053, design/06-caching.md §"Cache Key Security".
|
|
6917
|
+
*/
|
|
6918
|
+
/** Module specifiers whose bindings the transforms track. */
|
|
6919
|
+
var CACHE_MODULE_SOURCES = /* @__PURE__ */ new Set(["@timber-js/app/cache", "timber/cache"]);
|
|
6920
|
+
/**
|
|
6921
|
+
* Find the local binding name(s) for `importedName` imported from one of
|
|
6922
|
+
* `sources` — both named imports (including aliases) and namespace imports
|
|
6923
|
+
* (TIM-1054).
|
|
6924
|
+
*/
|
|
6925
|
+
function findImportBindings(program, sources, importedName) {
|
|
6926
|
+
const named = /* @__PURE__ */ new Set();
|
|
6927
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
6928
|
+
for (const stmt of program.body) {
|
|
6929
|
+
if (stmt.type !== "ImportDeclaration") continue;
|
|
6930
|
+
const decl = stmt;
|
|
6931
|
+
if (!sources.has(decl.source.value)) continue;
|
|
6932
|
+
for (const spec of decl.specifiers) {
|
|
6933
|
+
if (spec.type === "ImportNamespaceSpecifier") {
|
|
6934
|
+
namespaces.add(spec.local.name);
|
|
6935
|
+
continue;
|
|
6936
|
+
}
|
|
6937
|
+
if (spec.type !== "ImportSpecifier") continue;
|
|
6938
|
+
if ((spec.imported?.name ?? spec.local.name) === importedName) named.add(spec.local.name);
|
|
6939
|
+
}
|
|
6940
|
+
}
|
|
6941
|
+
return {
|
|
6942
|
+
named,
|
|
6943
|
+
namespaces
|
|
6944
|
+
};
|
|
6945
|
+
}
|
|
6946
|
+
/**
|
|
6947
|
+
* Recursively walk AST nodes collecting call expressions accepted by
|
|
6948
|
+
* `matches`. Calls are typically at module scope, but calls inside
|
|
6949
|
+
* functions/blocks are handled for completeness.
|
|
6950
|
+
*
|
|
6951
|
+
* NOTE: matching is by binding NAME, so nested scopes that shadow a tracked
|
|
6952
|
+
* import can produce false positives for consumers of this walker (see
|
|
6953
|
+
* TIM-1141 for the cache transform). The prebuilt transform avoids the
|
|
6954
|
+
* class entirely by not using this walker — it allowlists top-level
|
|
6955
|
+
* declarator positions instead (plugins/prebuilt.ts).
|
|
6956
|
+
*/
|
|
6957
|
+
function findCallSites(node, matches, results) {
|
|
6958
|
+
if (node.type === "CallExpression") {
|
|
6959
|
+
const call = node;
|
|
6960
|
+
if (matches(call)) results.push(call);
|
|
6961
|
+
}
|
|
6962
|
+
for (const key of Object.keys(node)) {
|
|
6963
|
+
if (key === "type" || key === "start" || key === "end") continue;
|
|
6964
|
+
const val = node[key];
|
|
6965
|
+
if (val && typeof val === "object") {
|
|
6966
|
+
if (Array.isArray(val)) {
|
|
6967
|
+
for (const item of val) if (item && typeof item === "object" && typeof item.type === "string") findCallSites(item, matches, results);
|
|
6968
|
+
} else if (typeof val.type === "string") findCallSites(val, matches, results);
|
|
6969
|
+
}
|
|
6970
|
+
}
|
|
6971
|
+
}
|
|
6972
|
+
/**
|
|
6973
|
+
* Generate a stable callsite ID from a module path, a kind discriminator
|
|
6974
|
+
* ('cache' | 'component'), the source text of the enclosing top-level
|
|
6975
|
+
* statement, the source text of the call expression, and an occurrence index
|
|
6976
|
+
* among fully byte-identical (statement, call) pairs in the same file.
|
|
6977
|
+
*
|
|
6978
|
+
* The ID is deterministic and:
|
|
6979
|
+
* - Stable across cold starts AND across builds/deploys for unchanged calls —
|
|
6980
|
+
* inserting, deleting, or reordering other calls does not affect it
|
|
6981
|
+
* - Unique across different modules (relPath component)
|
|
6982
|
+
* - Unique across distinct calls within a module (statement + span hashes),
|
|
6983
|
+
* with fully identical duplicates disambiguated by source-order occurrence
|
|
6984
|
+
* - Fresh when the wrapped fn, opts, or enclosing statement change, which
|
|
6985
|
+
* intentionally invalidates the old persisted entries
|
|
6986
|
+
*
|
|
6987
|
+
* The enclosing-statement text is included so that byte-identical calls in
|
|
6988
|
+
* different scopes — e.g. two factory helpers with identical call spans —
|
|
6989
|
+
* get distinct, insertion-stable IDs. See plugins/cache.ts header for the
|
|
6990
|
+
* full derivation rationale (TIM-1053).
|
|
6991
|
+
*/
|
|
6992
|
+
function generateStableCallsiteId(relativeModulePath, kind, statementText, callSpanText, occurrence) {
|
|
6993
|
+
return fnv1aHash(`${relativeModulePath}:${kind}:${fnv1aHash(statementText)}:${fnv1aHash(callSpanText)}:${occurrence}`);
|
|
6994
|
+
}
|
|
6995
|
+
//#endregion
|
|
7029
6996
|
//#region src/plugins/cache.ts
|
|
7030
6997
|
/**
|
|
7031
6998
|
* timber-cache-transform — Vite plugin that injects stable callsite IDs
|
|
@@ -7076,34 +7043,11 @@ function timberServerBundle() {
|
|
|
7076
7043
|
* referencing the same bindings — the same function, where an ID swap is
|
|
7077
7044
|
* harmless.
|
|
7078
7045
|
*
|
|
7046
|
+
* AST helpers and the ID derivation are shared with timber-prebuilt
|
|
7047
|
+
* (plugins/prebuilt.ts) via plugins/callsite-ast.ts.
|
|
7048
|
+
*
|
|
7079
7049
|
* See TIM-997, TIM-1053, design/06-caching.md §"Cache Key Security".
|
|
7080
7050
|
*/
|
|
7081
|
-
var CACHE_MODULE_SOURCES = /* @__PURE__ */ new Set(["@timber-js/app/cache", "timber/cache"]);
|
|
7082
|
-
/**
|
|
7083
|
-
* Find the local binding name(s) for `cache` imported from the cache module —
|
|
7084
|
-
* both named imports and namespace imports (TIM-1054).
|
|
7085
|
-
*/
|
|
7086
|
-
function findCacheBindings(program) {
|
|
7087
|
-
const named = /* @__PURE__ */ new Set();
|
|
7088
|
-
const namespaces = /* @__PURE__ */ new Set();
|
|
7089
|
-
for (const stmt of program.body) {
|
|
7090
|
-
if (stmt.type !== "ImportDeclaration") continue;
|
|
7091
|
-
const decl = stmt;
|
|
7092
|
-
if (!CACHE_MODULE_SOURCES.has(decl.source.value)) continue;
|
|
7093
|
-
for (const spec of decl.specifiers) {
|
|
7094
|
-
if (spec.type === "ImportNamespaceSpecifier") {
|
|
7095
|
-
namespaces.add(spec.local.name);
|
|
7096
|
-
continue;
|
|
7097
|
-
}
|
|
7098
|
-
if (spec.type !== "ImportSpecifier") continue;
|
|
7099
|
-
if ((spec.imported?.name ?? spec.local.name) === "cache") named.add(spec.local.name);
|
|
7100
|
-
}
|
|
7101
|
-
}
|
|
7102
|
-
return {
|
|
7103
|
-
named,
|
|
7104
|
-
namespaces
|
|
7105
|
-
};
|
|
7106
|
-
}
|
|
7107
7051
|
/**
|
|
7108
7052
|
* Is this callee a reference to the imported `cache` function — either a bare
|
|
7109
7053
|
* identifier bound by a named import, or `ns.cache` on a namespace import?
|
|
@@ -7119,44 +7063,11 @@ function isCacheCallee(callee, bindings) {
|
|
|
7119
7063
|
return false;
|
|
7120
7064
|
}
|
|
7121
7065
|
/**
|
|
7122
|
-
*
|
|
7123
|
-
*
|
|
7124
|
-
* cache() is typically called at module scope, but we also handle calls
|
|
7125
|
-
* inside functions/blocks for completeness.
|
|
7126
|
-
*/
|
|
7127
|
-
function findCacheCallSites(node, cacheBindings, results) {
|
|
7128
|
-
if (node.type === "CallExpression") {
|
|
7129
|
-
const call = node;
|
|
7130
|
-
if (call.arguments.length === 2 && isCacheCallee(call.callee, cacheBindings)) results.push(call);
|
|
7131
|
-
}
|
|
7132
|
-
for (const key of Object.keys(node)) {
|
|
7133
|
-
if (key === "type" || key === "start" || key === "end") continue;
|
|
7134
|
-
const val = node[key];
|
|
7135
|
-
if (val && typeof val === "object") {
|
|
7136
|
-
if (Array.isArray(val)) {
|
|
7137
|
-
for (const item of val) if (item && typeof item === "object" && typeof item.type === "string") findCacheCallSites(item, cacheBindings, results);
|
|
7138
|
-
} else if (typeof val.type === "string") findCacheCallSites(val, cacheBindings, results);
|
|
7139
|
-
}
|
|
7140
|
-
}
|
|
7141
|
-
}
|
|
7142
|
-
/**
|
|
7143
|
-
* Generate a stable callsite ID from a module path, the source text of the
|
|
7144
|
-
* enclosing top-level statement, the source text of the cache(...) call
|
|
7145
|
-
* expression, and an occurrence index among fully byte-identical
|
|
7146
|
-
* (statement, call) pairs in the same file.
|
|
7147
|
-
*
|
|
7148
|
-
* The ID is deterministic and:
|
|
7149
|
-
* - Stable across cold starts AND across builds/deploys for unchanged calls —
|
|
7150
|
-
* inserting, deleting, or reordering other cache() calls does not affect it
|
|
7151
|
-
* - Unique across different modules (relPath component)
|
|
7152
|
-
* - Unique across distinct cache() calls within a module (statement + span
|
|
7153
|
-
* hashes), with fully identical duplicates disambiguated by source-order
|
|
7154
|
-
* occurrence
|
|
7155
|
-
* - Fresh when the wrapped fn, opts, or enclosing statement change, which
|
|
7156
|
-
* intentionally invalidates the old persisted entries
|
|
7066
|
+
* Generate a stable callsite ID for a cache() call. See callsite-ast.ts for
|
|
7067
|
+
* the derivation properties.
|
|
7157
7068
|
*/
|
|
7158
7069
|
function generateStableId(relativeModulePath, statementText, callSpanText, occurrence) {
|
|
7159
|
-
return
|
|
7070
|
+
return generateStableCallsiteId(relativeModulePath, "cache", statementText, callSpanText, occurrence);
|
|
7160
7071
|
}
|
|
7161
7072
|
function timberCacheTransform(ctx) {
|
|
7162
7073
|
return {
|
|
@@ -7170,12 +7081,12 @@ function timberCacheTransform(ctx) {
|
|
|
7170
7081
|
} catch {
|
|
7171
7082
|
return;
|
|
7172
7083
|
}
|
|
7173
|
-
const cacheBindings =
|
|
7084
|
+
const cacheBindings = findImportBindings(program, CACHE_MODULE_SOURCES, "cache");
|
|
7174
7085
|
if (cacheBindings.named.size === 0 && cacheBindings.namespaces.size === 0) return;
|
|
7175
7086
|
const callSites = [];
|
|
7176
7087
|
for (const stmt of program.body) {
|
|
7177
7088
|
const calls = [];
|
|
7178
|
-
|
|
7089
|
+
findCallSites(stmt, (call) => call.arguments.length === 2 && isCacheCallee(call.callee, cacheBindings), calls);
|
|
7179
7090
|
for (const call of calls) callSites.push({
|
|
7180
7091
|
call,
|
|
7181
7092
|
stmt
|
|
@@ -7204,6 +7115,915 @@ function timberCacheTransform(ctx) {
|
|
|
7204
7115
|
};
|
|
7205
7116
|
}
|
|
7206
7117
|
//#endregion
|
|
7118
|
+
//#region src/plugins/callsite-jsx.ts
|
|
7119
|
+
/**
|
|
7120
|
+
* Try to extract a static JSON-safe value from an AST expression node.
|
|
7121
|
+
* Returns `undefined` (not null — null is a valid JSON value) when the
|
|
7122
|
+
* expression is dynamic/computed and cannot be captured statically.
|
|
7123
|
+
*/
|
|
7124
|
+
function tryExtractStaticValue(node) {
|
|
7125
|
+
if (node.type === "Literal") {
|
|
7126
|
+
const val = node.value;
|
|
7127
|
+
if (val === null || typeof val === "string" || typeof val === "number" || typeof val === "boolean") return val;
|
|
7128
|
+
return;
|
|
7129
|
+
}
|
|
7130
|
+
if (node.type === "UnaryExpression") {
|
|
7131
|
+
const unary = node;
|
|
7132
|
+
if (unary.operator === "-" && unary.argument.type === "Literal") {
|
|
7133
|
+
const val = unary.argument.value;
|
|
7134
|
+
if (typeof val === "number") return -val;
|
|
7135
|
+
}
|
|
7136
|
+
return;
|
|
7137
|
+
}
|
|
7138
|
+
if (node.type === "TemplateLiteral") {
|
|
7139
|
+
const tpl = node;
|
|
7140
|
+
if (tpl.expressions.length === 0) return tpl.quasis[0]?.value.cooked ?? void 0;
|
|
7141
|
+
return;
|
|
7142
|
+
}
|
|
7143
|
+
if (node.type === "ArrayExpression") {
|
|
7144
|
+
const elements = node.elements;
|
|
7145
|
+
const arr = [];
|
|
7146
|
+
for (const el of elements) {
|
|
7147
|
+
if (el === null) return void 0;
|
|
7148
|
+
const val = tryExtractStaticValue(el);
|
|
7149
|
+
if (val === void 0) return void 0;
|
|
7150
|
+
arr.push(val);
|
|
7151
|
+
}
|
|
7152
|
+
return arr;
|
|
7153
|
+
}
|
|
7154
|
+
if (node.type === "ObjectExpression") {
|
|
7155
|
+
const properties = node.properties;
|
|
7156
|
+
const obj = {};
|
|
7157
|
+
for (const prop of properties) {
|
|
7158
|
+
if (prop.type === "SpreadElement") return void 0;
|
|
7159
|
+
if (prop.type !== "Property") return void 0;
|
|
7160
|
+
const p = prop;
|
|
7161
|
+
if (p.computed) return void 0;
|
|
7162
|
+
const keyName = p.key.type === "Identifier" ? p.key.name : p.key.type === "Literal" ? String(p.key.value) : void 0;
|
|
7163
|
+
if (keyName === void 0) return void 0;
|
|
7164
|
+
const val = tryExtractStaticValue(p.value);
|
|
7165
|
+
if (val === void 0) return void 0;
|
|
7166
|
+
obj[keyName] = val;
|
|
7167
|
+
}
|
|
7168
|
+
return obj;
|
|
7169
|
+
}
|
|
7170
|
+
}
|
|
7171
|
+
/**
|
|
7172
|
+
* Extract static props from a JSX element's attributes. Returns null if
|
|
7173
|
+
* any attribute is dynamic (spread, computed expression, JSX element, etc.)
|
|
7174
|
+
* — the component must fall back to dynamic rendering for that call site.
|
|
7175
|
+
*/
|
|
7176
|
+
function extractStaticJsxProps(attributes) {
|
|
7177
|
+
const props = {};
|
|
7178
|
+
for (const attr of attributes) {
|
|
7179
|
+
if (attr.type === "JSXSpreadAttribute") return null;
|
|
7180
|
+
if (attr.type !== "JSXAttribute") return null;
|
|
7181
|
+
const jsxAttr = attr;
|
|
7182
|
+
const name = jsxAttr.name?.name;
|
|
7183
|
+
if (!name) return null;
|
|
7184
|
+
if (name === "key" || name === "ref") continue;
|
|
7185
|
+
if (jsxAttr.value === null) {
|
|
7186
|
+
props[name] = true;
|
|
7187
|
+
continue;
|
|
7188
|
+
}
|
|
7189
|
+
if (jsxAttr.value.type === "Literal") {
|
|
7190
|
+
const val = tryExtractStaticValue(jsxAttr.value);
|
|
7191
|
+
if (val === void 0) return null;
|
|
7192
|
+
props[name] = val;
|
|
7193
|
+
continue;
|
|
7194
|
+
}
|
|
7195
|
+
if (jsxAttr.value.type === "JSXExpressionContainer") {
|
|
7196
|
+
const container = jsxAttr.value;
|
|
7197
|
+
const val = tryExtractStaticValue(container.expression);
|
|
7198
|
+
if (val === void 0) return null;
|
|
7199
|
+
props[name] = val;
|
|
7200
|
+
continue;
|
|
7201
|
+
}
|
|
7202
|
+
return null;
|
|
7203
|
+
}
|
|
7204
|
+
return props;
|
|
7205
|
+
}
|
|
7206
|
+
/**
|
|
7207
|
+
* Walk an AST tree and collect JSX usages of the given binding names,
|
|
7208
|
+
* returning the static props for each usage. Returns a map of
|
|
7209
|
+
* bindingName → array of static prop objects (or null for dynamic props).
|
|
7210
|
+
*/
|
|
7211
|
+
function collectJsxUsages(program, bindingNames) {
|
|
7212
|
+
const result = /* @__PURE__ */ new Map();
|
|
7213
|
+
const walk = (node) => {
|
|
7214
|
+
if (!node || typeof node !== "object") return;
|
|
7215
|
+
const n = node;
|
|
7216
|
+
if (n.type === "JSXElement") {
|
|
7217
|
+
const jsx = n;
|
|
7218
|
+
const tagName = jsx.openingElement.name;
|
|
7219
|
+
if (tagName.type === "JSXIdentifier") {
|
|
7220
|
+
const name = tagName.name;
|
|
7221
|
+
if (bindingNames.has(name)) {
|
|
7222
|
+
const children = jsx.children ?? [];
|
|
7223
|
+
const props = children.length > 0 && children.some((c) => {
|
|
7224
|
+
const child = c;
|
|
7225
|
+
if (child.type === "JSXText") return child.value.trim().length > 0;
|
|
7226
|
+
return true;
|
|
7227
|
+
}) ? null : extractStaticJsxProps(jsx.openingElement.attributes);
|
|
7228
|
+
const usages = result.get(name) ?? [];
|
|
7229
|
+
usages.push(props);
|
|
7230
|
+
result.set(name, usages);
|
|
7231
|
+
}
|
|
7232
|
+
}
|
|
7233
|
+
}
|
|
7234
|
+
for (const value of Object.values(n)) if (Array.isArray(value)) for (const item of value) walk(item);
|
|
7235
|
+
else if (value && typeof value === "object" && "type" in value) walk(value);
|
|
7236
|
+
};
|
|
7237
|
+
for (const stmt of program.body) walk(stmt);
|
|
7238
|
+
return result;
|
|
7239
|
+
}
|
|
7240
|
+
/**
|
|
7241
|
+
* Deep stable JSON stringification with recursively sorted keys.
|
|
7242
|
+
* `JSON.stringify` with an array replacer only includes listed keys at
|
|
7243
|
+
* ALL nesting levels, so `{config:{theme:'dark'}}` and
|
|
7244
|
+
* `{config:{theme:'light'}}` would both serialize as `{"config":{}}`.
|
|
7245
|
+
*/
|
|
7246
|
+
function stableStringify(value) {
|
|
7247
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
7248
|
+
if (Array.isArray(value)) return "[" + value.map(stableStringify).join(",") + "]";
|
|
7249
|
+
const obj = value;
|
|
7250
|
+
return "{" + Object.keys(obj).sort().map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
|
|
7251
|
+
}
|
|
7252
|
+
/**
|
|
7253
|
+
* Add a distinct props object to the call-site-props map for a component,
|
|
7254
|
+
* deduplicating by deep JSON value (recursively sorted keys).
|
|
7255
|
+
*/
|
|
7256
|
+
function recordCallSiteProps(ctx, componentId, props) {
|
|
7257
|
+
ctx.prebuiltCallSiteProps ??= /* @__PURE__ */ new Map();
|
|
7258
|
+
const existing = ctx.prebuiltCallSiteProps.get(componentId) ?? [];
|
|
7259
|
+
const canonical = stableStringify(props);
|
|
7260
|
+
if (!existing.some((p) => stableStringify(p) === canonical)) {
|
|
7261
|
+
existing.push(props);
|
|
7262
|
+
ctx.prebuiltCallSiteProps.set(componentId, existing);
|
|
7263
|
+
}
|
|
7264
|
+
}
|
|
7265
|
+
//#endregion
|
|
7266
|
+
//#region src/plugins/prebuilt-capture.ts
|
|
7267
|
+
/**
|
|
7268
|
+
* Post-build prebuilt capture — Node-side orchestration (TIM-1119).
|
|
7269
|
+
*
|
|
7270
|
+
* Runs from timber-prebuilt's `buildApp` post hook, after Vite's 5-step
|
|
7271
|
+
* build sequence. Imports the BUILT `rsc/index.js` (the bundle carries
|
|
7272
|
+
* the react-server condition resolved at build time, so a plain dynamic
|
|
7273
|
+
* import works) and calls its `__timber_capturePrebuilt` export — the
|
|
7274
|
+
* capture itself must run inside the bundle's module graph, where the
|
|
7275
|
+
* component registry and flight renderer live. See
|
|
7276
|
+
* server/prebuilt-builder.ts and design/44-render-at-build-time.md
|
|
7277
|
+
* §Build Phase.
|
|
7278
|
+
*
|
|
7279
|
+
* This side owns PERSISTENCE: each captured payload is written to
|
|
7280
|
+
* `.timber/dist/prebuilt/<componentId>/<cacheKey>.flight` atomically
|
|
7281
|
+
* (temp file + rename — a crashed build never leaves a torn artifact
|
|
7282
|
+
* under its final name), and `manifest.json` is written last, only after
|
|
7283
|
+
* every payload it references is fully on disk. Keeping node:fs on the
|
|
7284
|
+
* plugin side keeps it out of the server bundle, which must also run on
|
|
7285
|
+
* Cloudflare Workers.
|
|
7286
|
+
*
|
|
7287
|
+
* NOTE: importing the built entry runs the app's production startup —
|
|
7288
|
+
* instrumentation.ts register() and the configured cache handler load
|
|
7289
|
+
* inside the build process. That is by design (prebuilt components may
|
|
7290
|
+
* call timber.cache), and it only happens when the app actually declares
|
|
7291
|
+
* prebuilt components.
|
|
7292
|
+
*/
|
|
7293
|
+
/**
|
|
7294
|
+
* Both patterns are framework-generated (plugins/prebuilt.ts assigns IDs,
|
|
7295
|
+
* server/prebuilt/cache-key.ts hashes keys), so a mismatch is a framework
|
|
7296
|
+
* bug or a tampered registry — either way it must never reach a path join.
|
|
7297
|
+
*/
|
|
7298
|
+
var COMPONENT_ID_RE = /^[a-z0-9]+_[0-9a-f]{16}$/;
|
|
7299
|
+
var CACHE_KEY_RE = /^[0-9a-f]{16}$/;
|
|
7300
|
+
/** Write a file atomically: temp sibling + rename. */
|
|
7301
|
+
async function writeFileAtomic(path, data) {
|
|
7302
|
+
const tmp = `${path}.tmp`;
|
|
7303
|
+
await writeFile(tmp, data);
|
|
7304
|
+
await rename(tmp, path);
|
|
7305
|
+
}
|
|
7306
|
+
/**
|
|
7307
|
+
* Create the writer for `.timber/dist/prebuilt/`. Storage layout per
|
|
7308
|
+
* design/44 §Storage: one directory per component ID, one `.flight` file
|
|
7309
|
+
* per cache key, one manifest.json mapping componentId + key → file +
|
|
7310
|
+
* metadata.
|
|
7311
|
+
*/
|
|
7312
|
+
function createPrebuiltArtifactWriter(prebuiltDir) {
|
|
7313
|
+
const manifest = {};
|
|
7314
|
+
const buildTimestamp = Date.now();
|
|
7315
|
+
let count = 0;
|
|
7316
|
+
return {
|
|
7317
|
+
async write(entry) {
|
|
7318
|
+
if (!COMPONENT_ID_RE.test(entry.componentId) || !CACHE_KEY_RE.test(entry.cacheKey)) throw new Error(`[timber] prebuilt artifact writer received a malformed identity (componentId: ${JSON.stringify(entry.componentId)}, cacheKey: ${JSON.stringify(entry.cacheKey)}) — refusing to write`);
|
|
7319
|
+
const relativeFile = `${entry.componentId}/${entry.cacheKey}.flight`;
|
|
7320
|
+
const path = join(prebuiltDir, relativeFile);
|
|
7321
|
+
await mkdir(dirname(path), { recursive: true });
|
|
7322
|
+
await writeFileAtomic(path, entry.bytes);
|
|
7323
|
+
manifest[entry.componentId] ??= { entries: {} };
|
|
7324
|
+
manifest[entry.componentId].entries[entry.cacheKey] = {
|
|
7325
|
+
params: entry.params,
|
|
7326
|
+
file: relativeFile,
|
|
7327
|
+
bytes: entry.bytes.byteLength,
|
|
7328
|
+
buildTimestamp,
|
|
7329
|
+
...entry.tags ? { tags: entry.tags } : {}
|
|
7330
|
+
};
|
|
7331
|
+
count++;
|
|
7332
|
+
},
|
|
7333
|
+
async finalize() {
|
|
7334
|
+
if (count === 0) return 0;
|
|
7335
|
+
await writeFileAtomic(join(prebuiltDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
7336
|
+
return count;
|
|
7337
|
+
}
|
|
7338
|
+
};
|
|
7339
|
+
}
|
|
7340
|
+
/**
|
|
7341
|
+
* Remove any `prebuilt/` output left by a previous build in this
|
|
7342
|
+
* directory (watch mode, `emptyOutDir: false`). A stale manifest would
|
|
7343
|
+
* make adapters ship payloads from an OLD build — reviving them serves
|
|
7344
|
+
* outdated content with that build's chunk references instead of the
|
|
7345
|
+
* intended miss → live render. Runs unconditionally at the start of the
|
|
7346
|
+
* post-build hook, BEFORE the capture decision: a build whose app no
|
|
7347
|
+
* longer declares any prebuilt components must also shed the artifacts
|
|
7348
|
+
* (codex P2 on PR #836).
|
|
7349
|
+
*/
|
|
7350
|
+
async function cleanPrebuiltDir(buildDir) {
|
|
7351
|
+
await rm(join(buildDir, "prebuilt"), {
|
|
7352
|
+
recursive: true,
|
|
7353
|
+
force: true
|
|
7354
|
+
});
|
|
7355
|
+
}
|
|
7356
|
+
/**
|
|
7357
|
+
* Load the built RSC entry and run the capture pass. Throws (failing the
|
|
7358
|
+
* build) on infrastructure errors — missing/unloadable entry — and on
|
|
7359
|
+
* dynamic-context hard errors (cookies()/headers() in a prerendered
|
|
7360
|
+
* component). Per-entry data errors skip-and-report per design/44
|
|
7361
|
+
* §Build-Phase Error Handling.
|
|
7362
|
+
*/
|
|
7363
|
+
async function runPrebuiltCapture(ctx) {
|
|
7364
|
+
const routeComponents = ctx.routePrebuiltComponents;
|
|
7365
|
+
if (!routeComponents || routeComponents.size === 0) return {
|
|
7366
|
+
captured: 0,
|
|
7367
|
+
skipped: [],
|
|
7368
|
+
hardErrors: [],
|
|
7369
|
+
nestingWarnings: 0
|
|
7370
|
+
};
|
|
7371
|
+
const entryPath = join(ctx.buildDir, "rsc", "index.js");
|
|
7372
|
+
if (!existsSync(entryPath)) throw new Error(`[timber] prebuilt capture: built RSC entry not found at ${entryPath} — the app declares cache.component({ prerender: true }) call sites but the RSC build output is missing`);
|
|
7373
|
+
const g = globalThis;
|
|
7374
|
+
if (ctx.buildManifest) g.__TIMBER_BUILD_MANIFEST__ ??= ctx.buildManifest;
|
|
7375
|
+
if (ctx.deploymentId) g.__TIMBER_DEPLOYMENT_ID__ ??= ctx.deploymentId;
|
|
7376
|
+
console.log("[timber] cache.component: capturing prebuilt flight payloads…");
|
|
7377
|
+
let mod;
|
|
7378
|
+
try {
|
|
7379
|
+
mod = await import(
|
|
7380
|
+
/* @vite-ignore */
|
|
7381
|
+
`${pathToFileURL(entryPath).href}?t=${Date.now()}`
|
|
7382
|
+
);
|
|
7383
|
+
} catch (error) {
|
|
7384
|
+
throw new Error(`[timber] prebuilt capture: failed to load the built RSC entry (${entryPath}). The capture pass runs the production server module inside the build process — its startup must succeed for prerendering to work.`, { cause: error });
|
|
7385
|
+
}
|
|
7386
|
+
const capture = mod.__timber_capturePrebuilt;
|
|
7387
|
+
if (typeof capture !== "function") throw new Error("[timber] prebuilt capture: built RSC entry does not export __timber_capturePrebuilt — the build output predates this framework version; rebuild from a clean .timber/dist");
|
|
7388
|
+
const writer = createPrebuiltArtifactWriter(join(ctx.buildDir, "prebuilt"));
|
|
7389
|
+
const summary = await capture({
|
|
7390
|
+
fileComponentMap: Object.fromEntries(routeComponents),
|
|
7391
|
+
callSiteProps: ctx.prebuiltCallSiteProps ? Object.fromEntries(ctx.prebuiltCallSiteProps) : void 0,
|
|
7392
|
+
onEntry: (entry) => writer.write(entry)
|
|
7393
|
+
});
|
|
7394
|
+
if (summary.hardErrors.length > 0) {
|
|
7395
|
+
const details = summary.hardErrors.map((issue) => ` - ${issue.message}`).join("\n");
|
|
7396
|
+
throw new Error(`[timber] prebuilt capture failed: ${summary.hardErrors.length} component(s) read per-request context at build time:\n${details}`);
|
|
7397
|
+
}
|
|
7398
|
+
await writer.finalize();
|
|
7399
|
+
const total = summary.captured + summary.skipped.length;
|
|
7400
|
+
console.log(`[timber] Prebuilt ${summary.captured}/${total} entries.` + (summary.skipped.length > 0 ? ` ${summary.skipped.length} skipped due to errors.` : ""));
|
|
7401
|
+
return summary;
|
|
7402
|
+
}
|
|
7403
|
+
//#endregion
|
|
7404
|
+
//#region src/plugins/prebuilt.ts
|
|
7405
|
+
/**
|
|
7406
|
+
* timber-prebuilt — Vite plugin that detects `cache.component(...)` call
|
|
7407
|
+
* sites and rewrites them to `__prebuilt(stableId, component, options)`.
|
|
7408
|
+
*
|
|
7409
|
+
* `timber.cache.component(Component, { prerender: true })` is a build-time
|
|
7410
|
+
* transform, not a runtime function (design/44-render-at-build-time.md
|
|
7411
|
+
* §Registration, surface API renamed by design/45-cache-lifetimes.md). The
|
|
7412
|
+
* rewrite registers the component in the server-side prebuilt registry under
|
|
7413
|
+
* a stable ID that keys its cached flight payloads.
|
|
7414
|
+
*
|
|
7415
|
+
* Detected callsite forms (mirrors timber-cache-transform, TIM-1054):
|
|
7416
|
+
* - named imports, including aliases:
|
|
7417
|
+
* `import { cache } from '@timber-js/app/cache'` + `cache.component(fn, opts)`
|
|
7418
|
+
* - namespace imports:
|
|
7419
|
+
* `import * as timber` + `timber.cache.component(fn, opts)`
|
|
7420
|
+
* Computed access (`cache['component']`) is deliberately not matched — the
|
|
7421
|
+
* untransformed call reaches the runtime `cache.component` fallback
|
|
7422
|
+
* (cache/cache-api.ts), which warns once and renders dynamically.
|
|
7423
|
+
*
|
|
7424
|
+
* Matching is an ALLOWLIST of module-scope positions: the direct initializer
|
|
7425
|
+
* of a top-level variable declarator (including exported ones) and a
|
|
7426
|
+
* top-level `export default cache.component(...)`, with TS expression
|
|
7427
|
+
* wrappers (`as`, `satisfies`, `!`) unwrapped. Nothing else is walked — no
|
|
7428
|
+
* function/class/loop bodies, no params, no call arguments. Two reasons:
|
|
7429
|
+
* (1) `__prebuilt` must register exactly once at module load time; the
|
|
7430
|
+
* post-build capture pass walks the registry after loading modules, so a
|
|
7431
|
+
* call inside a function registers too late to ever be prebuilt, and a call
|
|
7432
|
+
* in a loop would re-register per iteration. (2) Name-based callee matching
|
|
7433
|
+
* is only shadow-proof in these positions — nested scopes can rebind `cache`
|
|
7434
|
+
* (`function render(cache) {...}`, `for (const cache of caches) ...`), and
|
|
7435
|
+
* rewriting those would corrupt user code; a top-level declarator initializer
|
|
7436
|
+
* cannot see a shadow of an import binding (redeclaration error). Calls in
|
|
7437
|
+
* other positions fall back to the runtime path like other untraceable forms.
|
|
7438
|
+
*
|
|
7439
|
+
* The rewrite per mode:
|
|
7440
|
+
* production: cache.component(fn, opts) → __prebuilt("<id>", fn, opts)
|
|
7441
|
+
* dev: cache.component(fn, opts) → __prebuiltDev("<name>", fn, opts)
|
|
7442
|
+
*
|
|
7443
|
+
* with `import { __prebuilt } from '#server-internal'` prepended. In dev the
|
|
7444
|
+
* wrapper is a no-op that logs once per component and renders dynamically —
|
|
7445
|
+
* no caching, no markers (design/44 §Dev Mode).
|
|
7446
|
+
*
|
|
7447
|
+
* Component identity: `<name>_<hash16>`, e.g. `blogbody_a1b2c3d4e5f6a7b8`.
|
|
7448
|
+
* The name prefix is the declaring variable's name (lowercased, for legible
|
|
7449
|
+
* manifest keys and log lines); the hash is the same content-derived callsite
|
|
7450
|
+
* scheme as the data cache transform (callsite-ast.ts) — deterministic across
|
|
7451
|
+
* builds for unchanged calls, immune to inserting/reordering other calls, and
|
|
7452
|
+
* fresh when the component or its options change. Component cache entries are
|
|
7453
|
+
* per-deploy (design/45 §Per-deploy namespacing) so cross-deploy ID reuse is
|
|
7454
|
+
* not a poisoning vector, but content-derived IDs keep prebuilt artifacts
|
|
7455
|
+
* addressable across incremental edits for free.
|
|
7456
|
+
*
|
|
7457
|
+
* The transform is skipped in the client environment: server components never
|
|
7458
|
+
* execute there, and `#server-internal` resolves to an empty stub in the
|
|
7459
|
+
* client bundle (plugins/shims.ts).
|
|
7460
|
+
*
|
|
7461
|
+
* See TIM-1118, design/44-render-at-build-time.md, design/45-cache-lifetimes.md.
|
|
7462
|
+
*/
|
|
7463
|
+
/**
|
|
7464
|
+
* Is this callee `cache.component` (named import, aliases included) or
|
|
7465
|
+
* `ns.cache.component` (namespace import)? Computed access is not matched.
|
|
7466
|
+
*/
|
|
7467
|
+
function isCacheComponentCallee(callee, bindings) {
|
|
7468
|
+
if (callee.type !== "MemberExpression") return false;
|
|
7469
|
+
const member = callee;
|
|
7470
|
+
if (member.computed || member.property.type !== "Identifier" || member.property.name !== "component") return false;
|
|
7471
|
+
if (member.object.type === "Identifier") return bindings.named.has(member.object.name);
|
|
7472
|
+
if (member.object.type === "MemberExpression") {
|
|
7473
|
+
const inner = member.object;
|
|
7474
|
+
return !inner.computed && inner.object.type === "Identifier" && bindings.namespaces.has(inner.object.name) && inner.property.type === "Identifier" && inner.property.name === "cache";
|
|
7475
|
+
}
|
|
7476
|
+
return false;
|
|
7477
|
+
}
|
|
7478
|
+
/**
|
|
7479
|
+
* Offset where an injected import may be inserted: after the directive
|
|
7480
|
+
* prologue ('use server', 'use strict', ...). Prepending at offset 0 would
|
|
7481
|
+
* demote a leading directive to a plain expression statement — silently
|
|
7482
|
+
* disabling it.
|
|
7483
|
+
*/
|
|
7484
|
+
function importInsertionOffset(program) {
|
|
7485
|
+
for (const stmt of program.body) if (!(stmt.type === "ExpressionStatement" && typeof stmt.directive === "string")) return stmt.start;
|
|
7486
|
+
return program.body.length > 0 ? program.body[program.body.length - 1].end : 0;
|
|
7487
|
+
}
|
|
7488
|
+
/** TS/paren wrappers that may sit between a declarator and the call. */
|
|
7489
|
+
var EXPRESSION_WRAPPER_TYPES = /* @__PURE__ */ new Set([
|
|
7490
|
+
"TSAsExpression",
|
|
7491
|
+
"TSSatisfiesExpression",
|
|
7492
|
+
"TSNonNullExpression",
|
|
7493
|
+
"ParenthesizedExpression"
|
|
7494
|
+
]);
|
|
7495
|
+
function unwrapExpression(node) {
|
|
7496
|
+
let current = node;
|
|
7497
|
+
while (EXPRESSION_WRAPPER_TYPES.has(current.type) && current.expression) current = current.expression;
|
|
7498
|
+
return current;
|
|
7499
|
+
}
|
|
7500
|
+
/**
|
|
7501
|
+
* Can this call site's options put the component in the prerender tier?
|
|
7502
|
+
*
|
|
7503
|
+
* Runtime-tier call sites (`ttl`/`tags` without `prerender`) must do NO
|
|
7504
|
+
* build work — recording them would make the post-build hook import the
|
|
7505
|
+
* built RSC entry (running production startup) and load route modules for
|
|
7506
|
+
* routes that have nothing to prerender (codex P2 on PR #836). The answer
|
|
7507
|
+
* is static only when the options argument is an object literal:
|
|
7508
|
+
*
|
|
7509
|
+
* - literal with `prerender: true` → yes
|
|
7510
|
+
* - literal with `prerender: false`/absent → no (spread-free)
|
|
7511
|
+
* - anything else (identifier, spread, non-literal `prerender` value)
|
|
7512
|
+
* → conservatively yes — the capture pass re-checks the real options
|
|
7513
|
+
* at runtime and skips non-prerender registrations; over-inclusion
|
|
7514
|
+
* costs a wasted entry load, under-inclusion would silently skip
|
|
7515
|
+
* prerendering.
|
|
7516
|
+
*/
|
|
7517
|
+
function mayPrerender(call) {
|
|
7518
|
+
if (call.arguments.length < 2) return false;
|
|
7519
|
+
const options = unwrapExpression(call.arguments[1]);
|
|
7520
|
+
if (options.type !== "ObjectExpression") return true;
|
|
7521
|
+
const properties = options.properties ?? [];
|
|
7522
|
+
let verdict = "absent";
|
|
7523
|
+
for (const prop of properties) {
|
|
7524
|
+
if (prop.type === "SpreadElement") {
|
|
7525
|
+
verdict = "unknown";
|
|
7526
|
+
continue;
|
|
7527
|
+
}
|
|
7528
|
+
if (prop.type !== "Property") continue;
|
|
7529
|
+
const property = prop;
|
|
7530
|
+
if (property.computed) {
|
|
7531
|
+
verdict = "unknown";
|
|
7532
|
+
continue;
|
|
7533
|
+
}
|
|
7534
|
+
if ((property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? String(property.key.value) : void 0) !== "prerender") continue;
|
|
7535
|
+
const value = unwrapExpression(property.value);
|
|
7536
|
+
if (value.type === "Literal") verdict = value.value === true ? "true" : "false";
|
|
7537
|
+
else verdict = "unknown";
|
|
7538
|
+
}
|
|
7539
|
+
return verdict === "true" || verdict === "unknown";
|
|
7540
|
+
}
|
|
7541
|
+
/**
|
|
7542
|
+
* Collect matching call sites from the allowlisted module-scope positions:
|
|
7543
|
+
* top-level (possibly exported) variable declarator initializers and
|
|
7544
|
+
* `export default` expressions. Returned in source order. The display name
|
|
7545
|
+
* is the declaring variable's name, or 'Component' for default exports.
|
|
7546
|
+
*/
|
|
7547
|
+
function collectModuleScopeSites(program, bindings) {
|
|
7548
|
+
const isMatch = (node) => node.type === "CallExpression" && node.arguments.length >= 1 && node.arguments.length <= 2 && isCacheComponentCallee(node.callee, bindings);
|
|
7549
|
+
const sites = [];
|
|
7550
|
+
for (const stmt of program.body) {
|
|
7551
|
+
if (stmt.type === "ExportDefaultDeclaration" && stmt.declaration) {
|
|
7552
|
+
const call = unwrapExpression(stmt.declaration);
|
|
7553
|
+
if (isMatch(call)) sites.push({
|
|
7554
|
+
call,
|
|
7555
|
+
stmt,
|
|
7556
|
+
displayName: "Component"
|
|
7557
|
+
});
|
|
7558
|
+
continue;
|
|
7559
|
+
}
|
|
7560
|
+
let decl = stmt;
|
|
7561
|
+
if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) decl = stmt.declaration;
|
|
7562
|
+
if (decl.type !== "VariableDeclaration" || !Array.isArray(decl.declarations)) continue;
|
|
7563
|
+
for (const d of decl.declarations) {
|
|
7564
|
+
if (!d.init) continue;
|
|
7565
|
+
const call = unwrapExpression(d.init);
|
|
7566
|
+
if (!isMatch(call)) continue;
|
|
7567
|
+
const displayName = d.id.type === "Identifier" ? d.id.name : "Component";
|
|
7568
|
+
sites.push({
|
|
7569
|
+
call,
|
|
7570
|
+
stmt,
|
|
7571
|
+
displayName
|
|
7572
|
+
});
|
|
7573
|
+
}
|
|
7574
|
+
}
|
|
7575
|
+
return sites;
|
|
7576
|
+
}
|
|
7577
|
+
/**
|
|
7578
|
+
* Generate the stable component ID for a cache.component() call site:
|
|
7579
|
+
* `<sanitized-name>_<content-hash16>`. Exported for testing.
|
|
7580
|
+
*/
|
|
7581
|
+
function generatePrebuiltId(relativeModulePath, statementText, callSpanText, occurrence, displayName) {
|
|
7582
|
+
return `${displayName.toLowerCase().replace(/[^a-z0-9]/g, "") || "component"}_${generateStableCallsiteId(relativeModulePath, "component", statementText, callSpanText, occurrence)}`;
|
|
7583
|
+
}
|
|
7584
|
+
/**
|
|
7585
|
+
* Collect the prebuilt component IDs transitively reachable from a route
|
|
7586
|
+
* file, following static AND dynamic imports. Same pattern as the
|
|
7587
|
+
* client-reference walk in build-manifest.ts; dynamic imports are included
|
|
7588
|
+
* because a dynamically-imported module still registers its components
|
|
7589
|
+
* when it eventually loads — attribution over-approximating registration
|
|
7590
|
+
* is handled at capture time (an attributed-but-unregistered component is
|
|
7591
|
+
* reported and skipped).
|
|
7592
|
+
*/
|
|
7593
|
+
function collectReachablePrebuiltComponents(entryModuleId, moduleComponents, getModuleInfo) {
|
|
7594
|
+
const visited = /* @__PURE__ */ new Set();
|
|
7595
|
+
const result = /* @__PURE__ */ new Set();
|
|
7596
|
+
const stack = [entryModuleId];
|
|
7597
|
+
while (stack.length > 0) {
|
|
7598
|
+
const moduleId = stack.pop();
|
|
7599
|
+
if (visited.has(moduleId)) continue;
|
|
7600
|
+
visited.add(moduleId);
|
|
7601
|
+
const ids = moduleComponents.get(moduleId);
|
|
7602
|
+
if (ids) for (const id of ids) result.add(id);
|
|
7603
|
+
const info = getModuleInfo(moduleId);
|
|
7604
|
+
if (!info) continue;
|
|
7605
|
+
for (const imported of info.importedIds) stack.push(imported);
|
|
7606
|
+
for (const imported of info.dynamicallyImportedIds ?? []) stack.push(imported);
|
|
7607
|
+
}
|
|
7608
|
+
return Array.from(result);
|
|
7609
|
+
}
|
|
7610
|
+
/**
|
|
7611
|
+
* Collect page and layout file paths from the scanned route tree — the
|
|
7612
|
+
* walk roots for attribution. Slots and intercepting/private segments are
|
|
7613
|
+
* excluded, mirroring the capture pass's route walk
|
|
7614
|
+
* (server/prebuilt-builder.ts §collectCandidateRoutes).
|
|
7615
|
+
*/
|
|
7616
|
+
function collectPageAndLayoutFiles(root) {
|
|
7617
|
+
const files = [];
|
|
7618
|
+
const walk = (node) => {
|
|
7619
|
+
if (node.segmentType === "intercepting" || node.segmentType === "private") return;
|
|
7620
|
+
if (node.page) files.push(node.page.filePath);
|
|
7621
|
+
if (node.layout) files.push(node.layout.filePath);
|
|
7622
|
+
for (const child of node.children) walk(child);
|
|
7623
|
+
};
|
|
7624
|
+
walk(root);
|
|
7625
|
+
return files;
|
|
7626
|
+
}
|
|
7627
|
+
/**
|
|
7628
|
+
* For a route file (page/layout), check if it imports from any prebuilt-
|
|
7629
|
+
* declaring module and extract JSX call-site props for those bindings.
|
|
7630
|
+
* Uses `getModuleInfo` to get the RESOLVED import targets and the module's
|
|
7631
|
+
* transformed code (available on Rollup's ModuleInfo at generateBundle).
|
|
7632
|
+
*/
|
|
7633
|
+
function collectCrossModuleProps(ctx, moduleId, getInfo) {
|
|
7634
|
+
const bindingNames = ctx.prebuiltBindingNames;
|
|
7635
|
+
const info = getInfo(moduleId);
|
|
7636
|
+
if (!info) return;
|
|
7637
|
+
const declaringImports = info.importedIds.filter((id) => bindingNames.has(id) && id !== moduleId);
|
|
7638
|
+
if (declaringImports.length === 0) return;
|
|
7639
|
+
const exportNameToId = /* @__PURE__ */ new Map();
|
|
7640
|
+
const collisions = /* @__PURE__ */ new Set();
|
|
7641
|
+
for (const declId of declaringImports) {
|
|
7642
|
+
const map = bindingNames.get(declId);
|
|
7643
|
+
for (const [name, compId] of map) {
|
|
7644
|
+
if (exportNameToId.has(name)) collisions.add(name);
|
|
7645
|
+
exportNameToId.set(name, compId);
|
|
7646
|
+
}
|
|
7647
|
+
}
|
|
7648
|
+
for (const name of collisions) exportNameToId.delete(name);
|
|
7649
|
+
let source = null;
|
|
7650
|
+
const extInfo = getInfo(moduleId);
|
|
7651
|
+
if (extInfo && typeof extInfo.code === "string") source = extInfo.code;
|
|
7652
|
+
else try {
|
|
7653
|
+
source = readFileSync(moduleId, "utf8");
|
|
7654
|
+
} catch {
|
|
7655
|
+
return;
|
|
7656
|
+
}
|
|
7657
|
+
if (!source) return;
|
|
7658
|
+
let program;
|
|
7659
|
+
try {
|
|
7660
|
+
program = parseAst(source, { lang: "tsx" });
|
|
7661
|
+
} catch {
|
|
7662
|
+
return;
|
|
7663
|
+
}
|
|
7664
|
+
const localToComponentId = /* @__PURE__ */ new Map();
|
|
7665
|
+
for (const stmt of program.body) {
|
|
7666
|
+
if (stmt.type !== "ImportDeclaration") continue;
|
|
7667
|
+
const importDecl = stmt;
|
|
7668
|
+
for (const spec of importDecl.specifiers) if (spec.type === "ImportSpecifier") {
|
|
7669
|
+
const exportedName = spec.imported?.name ?? spec.local.name;
|
|
7670
|
+
const compId = exportNameToId.get(exportedName);
|
|
7671
|
+
if (compId) localToComponentId.set(spec.local.name, compId);
|
|
7672
|
+
} else if (spec.type === "ImportDefaultSpecifier") {
|
|
7673
|
+
const compId = exportNameToId.get("Component") ?? exportNameToId.get("default");
|
|
7674
|
+
if (compId) localToComponentId.set(spec.local.name, compId);
|
|
7675
|
+
else if (exportNameToId.size === 1) {
|
|
7676
|
+
const [, soleId] = [...exportNameToId.entries()][0];
|
|
7677
|
+
localToComponentId.set(spec.local.name, soleId);
|
|
7678
|
+
}
|
|
7679
|
+
}
|
|
7680
|
+
}
|
|
7681
|
+
if (localToComponentId.size === 0) return;
|
|
7682
|
+
const usages = collectJsxUsages(program, new Set(localToComponentId.keys()));
|
|
7683
|
+
for (const [localName, propsList] of usages) {
|
|
7684
|
+
const componentId = localToComponentId.get(localName);
|
|
7685
|
+
if (!componentId) continue;
|
|
7686
|
+
for (const props of propsList) {
|
|
7687
|
+
if (props === null) continue;
|
|
7688
|
+
recordCallSiteProps(ctx, componentId, props);
|
|
7689
|
+
}
|
|
7690
|
+
}
|
|
7691
|
+
}
|
|
7692
|
+
function timberPrebuilt(ctx) {
|
|
7693
|
+
return {
|
|
7694
|
+
name: "timber-prebuilt",
|
|
7695
|
+
transform(code, id) {
|
|
7696
|
+
if (!ctx.dev && this?.environment?.name === "rsc") ctx.prebuiltModuleComponents?.delete(id);
|
|
7697
|
+
if (!code.includes("@timber-js/app/cache") && !code.includes("timber/cache")) return;
|
|
7698
|
+
if (id.includes("node_modules")) return;
|
|
7699
|
+
if (this?.environment?.name === "client") return;
|
|
7700
|
+
let program;
|
|
7701
|
+
try {
|
|
7702
|
+
program = parseAst(code, { lang: "tsx" });
|
|
7703
|
+
} catch {
|
|
7704
|
+
return;
|
|
7705
|
+
}
|
|
7706
|
+
const bindings = findImportBindings(program, CACHE_MODULE_SOURCES, "cache");
|
|
7707
|
+
if (bindings.named.size === 0 && bindings.namespaces.size === 0) return;
|
|
7708
|
+
const callSites = collectModuleScopeSites(program, bindings);
|
|
7709
|
+
if (callSites.length === 0) return;
|
|
7710
|
+
const relPath = relative(ctx.root || process.cwd(), id);
|
|
7711
|
+
const s = new MagicString(code);
|
|
7712
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
7713
|
+
const wrapperName = ctx.dev ? "__prebuiltDev" : "__prebuilt";
|
|
7714
|
+
const componentIds = [];
|
|
7715
|
+
for (const { call, stmt, displayName } of callSites) {
|
|
7716
|
+
const stmtText = code.slice(stmt.start, stmt.end);
|
|
7717
|
+
const spanText = code.slice(call.start, call.end);
|
|
7718
|
+
const dupKey = stmtText + String.fromCharCode(0) + spanText;
|
|
7719
|
+
const occurrence = occurrences.get(dupKey) ?? 0;
|
|
7720
|
+
occurrences.set(dupKey, occurrence + 1);
|
|
7721
|
+
const firstArg = ctx.dev ? displayName : generatePrebuiltId(relPath, stmtText, spanText, occurrence, displayName);
|
|
7722
|
+
if (!ctx.dev && mayPrerender(call)) componentIds.push(firstArg);
|
|
7723
|
+
s.overwrite(call.callee.start, call.callee.end, wrapperName);
|
|
7724
|
+
s.appendLeft(call.arguments[0].start, `${JSON.stringify(firstArg)}, `);
|
|
7725
|
+
}
|
|
7726
|
+
if (!ctx.dev && componentIds.length > 0 && this?.environment?.name === "rsc") {
|
|
7727
|
+
ctx.prebuiltModuleComponents ??= /* @__PURE__ */ new Map();
|
|
7728
|
+
ctx.prebuiltModuleComponents.set(id, componentIds);
|
|
7729
|
+
const bindingMap = /* @__PURE__ */ new Map();
|
|
7730
|
+
let componentIdIdx = 0;
|
|
7731
|
+
for (const { call, displayName } of callSites) if (!ctx.dev && mayPrerender(call)) bindingMap.set(displayName, componentIds[componentIdIdx++]);
|
|
7732
|
+
for (const stmt of program.body) if (stmt.type === "ExportDefaultDeclaration" && stmt.declaration && stmt.declaration.type === "Identifier") {
|
|
7733
|
+
const name = stmt.declaration.name;
|
|
7734
|
+
if (bindingMap.has(name) && !bindingMap.has("default")) bindingMap.set("default", bindingMap.get(name));
|
|
7735
|
+
}
|
|
7736
|
+
if (bindingMap.size > 0) {
|
|
7737
|
+
ctx.prebuiltBindingNames ??= /* @__PURE__ */ new Map();
|
|
7738
|
+
ctx.prebuiltBindingNames.set(id, bindingMap);
|
|
7739
|
+
}
|
|
7740
|
+
const declaredBindings = new Set(bindingMap.keys());
|
|
7741
|
+
if (declaredBindings.size > 0) {
|
|
7742
|
+
const usages = collectJsxUsages(program, declaredBindings);
|
|
7743
|
+
for (const [bindingName, propsList] of usages) {
|
|
7744
|
+
const compId = bindingMap.get(bindingName);
|
|
7745
|
+
if (!compId) continue;
|
|
7746
|
+
for (const props of propsList) {
|
|
7747
|
+
if (props === null) continue;
|
|
7748
|
+
recordCallSiteProps(ctx, compId, props);
|
|
7749
|
+
}
|
|
7750
|
+
}
|
|
7751
|
+
}
|
|
7752
|
+
}
|
|
7753
|
+
s.appendLeft(importInsertionOffset(program), `import { ${wrapperName} } from '#server-internal';\n`);
|
|
7754
|
+
return {
|
|
7755
|
+
code: s.toString(),
|
|
7756
|
+
map: s.generateMap({ hires: "boundary" })
|
|
7757
|
+
};
|
|
7758
|
+
},
|
|
7759
|
+
/**
|
|
7760
|
+
* RSC environment, production: walk each route file's module graph to
|
|
7761
|
+
* attribute prebuilt components to the routes that can render them.
|
|
7762
|
+
* The graph walk (rather than load-order registry deltas) is what makes
|
|
7763
|
+
* SHARED components work — a component imported by several routes is
|
|
7764
|
+
* captured under every importing route's param combos.
|
|
7765
|
+
*/
|
|
7766
|
+
generateBundle() {
|
|
7767
|
+
if (ctx.dev) return;
|
|
7768
|
+
if (this?.environment?.name !== "rsc") return;
|
|
7769
|
+
ctx.routePrebuiltComponents = void 0;
|
|
7770
|
+
const moduleComponents = ctx.prebuiltModuleComponents;
|
|
7771
|
+
if (!moduleComponents || moduleComponents.size === 0 || !ctx.routeTree) return;
|
|
7772
|
+
const self = this;
|
|
7773
|
+
const getInfo = (id) => self.getModuleInfo?.(id) ?? null;
|
|
7774
|
+
const routeComponents = /* @__PURE__ */ new Map();
|
|
7775
|
+
for (const filePath of collectPageAndLayoutFiles(ctx.routeTree.root)) {
|
|
7776
|
+
const ids = collectReachablePrebuiltComponents(filePath, moduleComponents, getInfo);
|
|
7777
|
+
if (ids.length > 0) routeComponents.set(filePath, ids);
|
|
7778
|
+
}
|
|
7779
|
+
if (routeComponents.size > 0) {
|
|
7780
|
+
ctx.routePrebuiltComponents = routeComponents;
|
|
7781
|
+
if (ctx.prebuiltBindingNames && ctx.prebuiltBindingNames.size > 0) {
|
|
7782
|
+
const scannedModules = /* @__PURE__ */ new Set();
|
|
7783
|
+
for (const filePath of routeComponents.keys()) {
|
|
7784
|
+
const visited = /* @__PURE__ */ new Set();
|
|
7785
|
+
const stack = [filePath];
|
|
7786
|
+
while (stack.length > 0) {
|
|
7787
|
+
const modId = stack.pop();
|
|
7788
|
+
if (visited.has(modId)) continue;
|
|
7789
|
+
visited.add(modId);
|
|
7790
|
+
if (!scannedModules.has(modId)) {
|
|
7791
|
+
scannedModules.add(modId);
|
|
7792
|
+
collectCrossModuleProps(ctx, modId, getInfo);
|
|
7793
|
+
}
|
|
7794
|
+
const modInfo = getInfo(modId);
|
|
7795
|
+
if (!modInfo) continue;
|
|
7796
|
+
for (const imp of modInfo.importedIds) stack.push(imp);
|
|
7797
|
+
for (const imp of modInfo.dynamicallyImportedIds ?? []) stack.push(imp);
|
|
7798
|
+
}
|
|
7799
|
+
}
|
|
7800
|
+
}
|
|
7801
|
+
} else console.warn(`[timber] cache.component: ${moduleComponents.size} module(s) declare prebuilt components, but none are reachable from any page or layout — no payloads will be prerendered`);
|
|
7802
|
+
},
|
|
7803
|
+
/**
|
|
7804
|
+
* Post-build capture (design/44 §Build Phase). `order: 'post'` runs
|
|
7805
|
+
* after the RSC plugin's buildApp — all five build steps are complete —
|
|
7806
|
+
* and BEFORE timber-adapter-build's post hook (plugin array order), so
|
|
7807
|
+
* `prebuilt/` exists when the adapter assembles its output (TIM-1122).
|
|
7808
|
+
*/
|
|
7809
|
+
buildApp: {
|
|
7810
|
+
order: "post",
|
|
7811
|
+
async handler() {
|
|
7812
|
+
if (ctx.dev) return;
|
|
7813
|
+
ctx.captureSummary = void 0;
|
|
7814
|
+
await cleanPrebuiltDir(ctx.buildDir);
|
|
7815
|
+
if (!ctx.routePrebuiltComponents || ctx.routePrebuiltComponents.size === 0) return;
|
|
7816
|
+
if (ctx.config.output === "static") {
|
|
7817
|
+
console.log(`[timber] cache.component: skipped prerender capture (output: 'static' renders all components at build time)`);
|
|
7818
|
+
return;
|
|
7819
|
+
}
|
|
7820
|
+
ctx.captureSummary = await runPrebuiltCapture(ctx);
|
|
7821
|
+
}
|
|
7822
|
+
}
|
|
7823
|
+
};
|
|
7824
|
+
}
|
|
7825
|
+
//#endregion
|
|
7826
|
+
//#region src/plugins/prerender-sugar.ts
|
|
7827
|
+
/**
|
|
7828
|
+
* timber-prerender-sugar — Vite plugin that desugars `export const prerender`
|
|
7829
|
+
* on page and layout files into `cache.component(...)` wrappers.
|
|
7830
|
+
*
|
|
7831
|
+
* Design doc: design/45-cache-lifetimes.md §"Route-Level Static" and
|
|
7832
|
+
* §"Layout Propagation".
|
|
7833
|
+
*
|
|
7834
|
+
* `export const prerender = true` on a page is pure sugar: it desugars to
|
|
7835
|
+
* wrapping the page's default export in `timber.cache.component(Component,
|
|
7836
|
+
* { prerender: true })`. Layout propagation cascades a layout's
|
|
7837
|
+
* `export const prerender` to descendant pages via most-specific-wins:
|
|
7838
|
+
* walk the segment tree from leaf to root, first explicit value wins.
|
|
7839
|
+
*
|
|
7840
|
+
* A layout that declares `export const prerender` is ALSO wrapped — the
|
|
7841
|
+
* layout's flight payload is its own static shell (`children` is a
|
|
7842
|
+
* separate component boundary), so caching it is natural (nav, sidebar,
|
|
7843
|
+
* footer benefit from build-time rendering). See design/45 §Layout
|
|
7844
|
+
* Propagation.
|
|
7845
|
+
*
|
|
7846
|
+
* This transform runs BEFORE timber-prebuilt so the generated
|
|
7847
|
+
* `cache.component(...)` call is picked up by the callsite rewrite.
|
|
7848
|
+
*/
|
|
7849
|
+
/**
|
|
7850
|
+
* Walk the segment tree and resolve the effective prerender value for
|
|
7851
|
+
* every page and layout. Returns a map from file path to resolved value.
|
|
7852
|
+
*
|
|
7853
|
+
* **Pages:** walk from leaf toward root; first explicit `export const prerender`
|
|
7854
|
+
* wins (page > nearest layout > ... > root layout). If none, dynamic.
|
|
7855
|
+
*
|
|
7856
|
+
* **Layouts:** a layout that declares `export const prerender` is wrapped
|
|
7857
|
+
* directly (its own shell is a separate component boundary from `children`).
|
|
7858
|
+
* Layouts without their own declaration are NOT wrapped — they don't
|
|
7859
|
+
* inherit from ancestors, because wrapping a layout's shell without the
|
|
7860
|
+
* author's explicit opt-in would cache content they may expect to be dynamic.
|
|
7861
|
+
*/
|
|
7862
|
+
function resolvePagePrerender(root) {
|
|
7863
|
+
const result = /* @__PURE__ */ new Map();
|
|
7864
|
+
function walk(node, ancestorStack) {
|
|
7865
|
+
if (node.segmentType === "intercepting" || node.segmentType === "private") return;
|
|
7866
|
+
const layoutValue = node.layout ? getPrerenderExport(node.layout.filePath) : void 0;
|
|
7867
|
+
const stack = layoutValue !== void 0 ? [layoutValue, ...ancestorStack] : ancestorStack;
|
|
7868
|
+
if (node.layout && layoutValue !== void 0 && layoutValue !== false) result.set(node.layout.filePath, layoutValue);
|
|
7869
|
+
if (node.page) {
|
|
7870
|
+
const pageValue = getPrerenderExport(node.page.filePath);
|
|
7871
|
+
const resolved = pageValue !== void 0 ? pageValue : stack[0];
|
|
7872
|
+
if (resolved !== void 0 && resolved !== false) result.set(node.page.filePath, resolved);
|
|
7873
|
+
}
|
|
7874
|
+
for (const child of node.children) walk(child, stack);
|
|
7875
|
+
for (const slot of Object.values(node.slots)) walk(slot, stack);
|
|
7876
|
+
}
|
|
7877
|
+
walk(root, []);
|
|
7878
|
+
return result;
|
|
7879
|
+
}
|
|
7880
|
+
/**
|
|
7881
|
+
* Build the `cache.component` options source text from a resolved value.
|
|
7882
|
+
*/
|
|
7883
|
+
function buildOptionsSource(value) {
|
|
7884
|
+
if (value === true) return "{ prerender: true }";
|
|
7885
|
+
if (typeof value === "object") {
|
|
7886
|
+
const parts = ["prerender: true"];
|
|
7887
|
+
if (value.ttl !== void 0) parts.push(`ttl: ${value.ttl}`);
|
|
7888
|
+
if (value.tags !== void 0) parts.push(`tags: ${JSON.stringify(value.tags)}`);
|
|
7889
|
+
return `{ ${parts.join(", ")} }`;
|
|
7890
|
+
}
|
|
7891
|
+
return "{ prerender: true }";
|
|
7892
|
+
}
|
|
7893
|
+
function findDefaultExport(program, source) {
|
|
7894
|
+
for (const stmt of program.body) {
|
|
7895
|
+
if (stmt.type !== "ExportDefaultDeclaration" || !stmt.declaration) continue;
|
|
7896
|
+
const decl = stmt.declaration;
|
|
7897
|
+
if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration") {
|
|
7898
|
+
const name = decl.id?.name;
|
|
7899
|
+
if (name) return {
|
|
7900
|
+
kind: "declaration",
|
|
7901
|
+
node: stmt,
|
|
7902
|
+
name
|
|
7903
|
+
};
|
|
7904
|
+
return {
|
|
7905
|
+
kind: "expression",
|
|
7906
|
+
node: stmt
|
|
7907
|
+
};
|
|
7908
|
+
}
|
|
7909
|
+
if (decl.type === "Identifier") return {
|
|
7910
|
+
kind: "identifier",
|
|
7911
|
+
node: stmt,
|
|
7912
|
+
name: decl.name
|
|
7913
|
+
};
|
|
7914
|
+
return {
|
|
7915
|
+
kind: "expression",
|
|
7916
|
+
node: stmt
|
|
7917
|
+
};
|
|
7918
|
+
}
|
|
7919
|
+
for (const stmt of program.body) {
|
|
7920
|
+
if (stmt.type !== "ExportNamedDeclaration") continue;
|
|
7921
|
+
if (stmt.source) continue;
|
|
7922
|
+
const specifiers = stmt.specifiers;
|
|
7923
|
+
if (!specifiers) continue;
|
|
7924
|
+
for (let i = 0; i < specifiers.length; i++) {
|
|
7925
|
+
const spec = specifiers[i];
|
|
7926
|
+
if (spec.exported?.name === "default" && spec.local?.name && spec.local.name !== "default") return {
|
|
7927
|
+
kind: "identifier",
|
|
7928
|
+
node: stmt,
|
|
7929
|
+
name: spec.local.name,
|
|
7930
|
+
specifierIndex: i,
|
|
7931
|
+
specifierCount: specifiers.length,
|
|
7932
|
+
specifierStart: spec.start,
|
|
7933
|
+
specifierEnd: spec.end
|
|
7934
|
+
};
|
|
7935
|
+
}
|
|
7936
|
+
}
|
|
7937
|
+
return null;
|
|
7938
|
+
}
|
|
7939
|
+
function timberPrerenderSugar(ctx) {
|
|
7940
|
+
let resolvedPages = null;
|
|
7941
|
+
function ensureResolved() {
|
|
7942
|
+
if (resolvedPages) return;
|
|
7943
|
+
if (!ctx.routeTree) {
|
|
7944
|
+
resolvedPages = /* @__PURE__ */ new Map();
|
|
7945
|
+
return;
|
|
7946
|
+
}
|
|
7947
|
+
resolvedPages = resolvePagePrerender(ctx.routeTree.root);
|
|
7948
|
+
}
|
|
7949
|
+
return {
|
|
7950
|
+
name: "timber-prerender-sugar",
|
|
7951
|
+
buildStart() {
|
|
7952
|
+
resolvedPages = null;
|
|
7953
|
+
},
|
|
7954
|
+
transform(code, id) {
|
|
7955
|
+
if (this?.environment?.name === "client") return;
|
|
7956
|
+
if (id.includes("node_modules")) return;
|
|
7957
|
+
ensureResolved();
|
|
7958
|
+
if (!resolvedPages || resolvedPages.size === 0) return;
|
|
7959
|
+
const value = resolvedPages.get(id);
|
|
7960
|
+
if (!value) return;
|
|
7961
|
+
let program;
|
|
7962
|
+
try {
|
|
7963
|
+
program = parseAst(code, { lang: "tsx" });
|
|
7964
|
+
} catch {
|
|
7965
|
+
return;
|
|
7966
|
+
}
|
|
7967
|
+
const defaultExport = findDefaultExport(program, code);
|
|
7968
|
+
if (!defaultExport) {
|
|
7969
|
+
if (value) {
|
|
7970
|
+
const relPath = id.replace(ctx.root + "/", "");
|
|
7971
|
+
console.warn(`[timber] ${relPath}: export const prerender is declared but the default export cannot be wrapped (re-export or missing). The prerender declaration will be ignored for this file.`);
|
|
7972
|
+
}
|
|
7973
|
+
return;
|
|
7974
|
+
}
|
|
7975
|
+
if (defaultExport.kind === "expression" && defaultExport.node.declaration) {
|
|
7976
|
+
if (code.slice(defaultExport.node.declaration.start, defaultExport.node.declaration.end).includes(".component(")) return;
|
|
7977
|
+
}
|
|
7978
|
+
ctx.prerenderSugarPages ??= /* @__PURE__ */ new Set();
|
|
7979
|
+
ctx.prerenderSugarPages.add(id);
|
|
7980
|
+
const s = new MagicString(code);
|
|
7981
|
+
const optionsSource = buildOptionsSource(value);
|
|
7982
|
+
const importAlias = "__timber_prerender_cache__";
|
|
7983
|
+
let insertOffset = 0;
|
|
7984
|
+
for (const stmt of program.body) if (!(stmt.type === "ExpressionStatement" && typeof stmt.directive === "string")) {
|
|
7985
|
+
insertOffset = stmt.start;
|
|
7986
|
+
break;
|
|
7987
|
+
}
|
|
7988
|
+
s.appendLeft(insertOffset, `import { cache as ${importAlias} } from '@timber-js/app/cache';\n`);
|
|
7989
|
+
switch (defaultExport.kind) {
|
|
7990
|
+
case "declaration": {
|
|
7991
|
+
const declStart = defaultExport.node.declaration.start;
|
|
7992
|
+
s.overwrite(defaultExport.node.start, declStart, "");
|
|
7993
|
+
s.append(`\nexport default ${importAlias}.component(${defaultExport.name}, ${optionsSource});\n`);
|
|
7994
|
+
break;
|
|
7995
|
+
}
|
|
7996
|
+
case "identifier":
|
|
7997
|
+
if (defaultExport.specifierCount !== void 0 && defaultExport.specifierCount > 1) {
|
|
7998
|
+
const specStart = defaultExport.specifierStart;
|
|
7999
|
+
const specEnd = defaultExport.specifierEnd;
|
|
8000
|
+
const idx = defaultExport.specifierIndex;
|
|
8001
|
+
if (idx === 0) {
|
|
8002
|
+
const nextSpec = defaultExport.node.specifiers?.[1];
|
|
8003
|
+
if (nextSpec) s.remove(specStart, nextSpec.start);
|
|
8004
|
+
} else {
|
|
8005
|
+
const prevSpec = defaultExport.node.specifiers?.[idx - 1];
|
|
8006
|
+
if (prevSpec) s.remove(prevSpec.end, specEnd);
|
|
8007
|
+
}
|
|
8008
|
+
s.appendLeft(defaultExport.node.end, `\nexport default ${importAlias}.component(${defaultExport.name}, ${optionsSource});`);
|
|
8009
|
+
} else s.overwrite(defaultExport.node.start, defaultExport.node.end, `export default ${importAlias}.component(${defaultExport.name}, ${optionsSource});`);
|
|
8010
|
+
break;
|
|
8011
|
+
case "expression": {
|
|
8012
|
+
const decl = defaultExport.node.declaration;
|
|
8013
|
+
s.overwrite(defaultExport.node.start, decl.start, "const __timber_default_page__ = ");
|
|
8014
|
+
const afterExpr = code.charAt(defaultExport.node.end - 1) === ";" ? "" : ";";
|
|
8015
|
+
s.appendLeft(defaultExport.node.end, `${afterExpr}\nexport default ${importAlias}.component(__timber_default_page__, ${optionsSource});`);
|
|
8016
|
+
break;
|
|
8017
|
+
}
|
|
8018
|
+
}
|
|
8019
|
+
return {
|
|
8020
|
+
code: s.toString(),
|
|
8021
|
+
map: s.generateMap({ hires: "boundary" })
|
|
8022
|
+
};
|
|
8023
|
+
}
|
|
8024
|
+
};
|
|
8025
|
+
}
|
|
8026
|
+
//#endregion
|
|
7207
8027
|
//#region src/plugins/cloudflare-wasm.ts
|
|
7208
8028
|
/**
|
|
7209
8029
|
* timber-cloudflare-wasm — Handle WASM modules for Cloudflare Workers builds.
|
|
@@ -7504,6 +8324,82 @@ function pad(str, width, align = "right") {
|
|
|
7504
8324
|
function stripAnsi(str) {
|
|
7505
8325
|
return str.replace(/\u001b\[\d+m/g, "");
|
|
7506
8326
|
}
|
|
8327
|
+
function loadPrebuiltManifest(buildDir) {
|
|
8328
|
+
try {
|
|
8329
|
+
const raw = readFileSync(join(buildDir, "prebuilt", "manifest.json"), "utf-8");
|
|
8330
|
+
return JSON.parse(raw);
|
|
8331
|
+
} catch {
|
|
8332
|
+
return null;
|
|
8333
|
+
}
|
|
8334
|
+
}
|
|
8335
|
+
function buildPrebuiltReportData(manifest, summary) {
|
|
8336
|
+
const hasCaptures = summary && (summary.captured > 0 || summary.skipped.length > 0);
|
|
8337
|
+
if (!manifest && !hasCaptures) return null;
|
|
8338
|
+
const components = [];
|
|
8339
|
+
let totalEntries = 0;
|
|
8340
|
+
let totalBytes = 0;
|
|
8341
|
+
if (manifest) {
|
|
8342
|
+
for (const [componentId, component] of Object.entries(manifest)) {
|
|
8343
|
+
let entryCount = 0;
|
|
8344
|
+
let componentBytes = 0;
|
|
8345
|
+
for (const entry of Object.values(component.entries)) {
|
|
8346
|
+
entryCount++;
|
|
8347
|
+
componentBytes += entry.bytes ?? 0;
|
|
8348
|
+
}
|
|
8349
|
+
components.push({
|
|
8350
|
+
componentId,
|
|
8351
|
+
entryCount,
|
|
8352
|
+
totalBytes: componentBytes
|
|
8353
|
+
});
|
|
8354
|
+
totalEntries += entryCount;
|
|
8355
|
+
totalBytes += componentBytes;
|
|
8356
|
+
}
|
|
8357
|
+
components.sort((a, b) => a.componentId.localeCompare(b.componentId));
|
|
8358
|
+
}
|
|
8359
|
+
const skippedCount = summary?.skipped.length ?? 0;
|
|
8360
|
+
const skippedReasons = (summary?.skipped ?? []).map((s) => ({
|
|
8361
|
+
componentId: s.componentId,
|
|
8362
|
+
route: s.routeUrlPath,
|
|
8363
|
+
message: s.message
|
|
8364
|
+
}));
|
|
8365
|
+
return {
|
|
8366
|
+
components,
|
|
8367
|
+
totalEntries,
|
|
8368
|
+
totalBytes,
|
|
8369
|
+
skippedCount,
|
|
8370
|
+
skippedReasons
|
|
8371
|
+
};
|
|
8372
|
+
}
|
|
8373
|
+
function buildPrebuiltReport(data) {
|
|
8374
|
+
const lines = [];
|
|
8375
|
+
const total = data.totalEntries + data.skippedCount;
|
|
8376
|
+
lines.push(`Prebuilt ${data.totalEntries}/${total} entries across ${data.components.length} component${data.components.length !== 1 ? "s" : ""}` + (data.totalBytes > 0 ? ` (${formatSize(data.totalBytes)} total)` : "") + "." + (data.skippedCount > 0 ? ` ${data.skippedCount} skipped due to errors.` : ""));
|
|
8377
|
+
if (data.components.length > 0) {
|
|
8378
|
+
lines.push("");
|
|
8379
|
+
const nameHeader = "Component";
|
|
8380
|
+
const entriesHeader = "Entries";
|
|
8381
|
+
const sizeHeader = "Size";
|
|
8382
|
+
const nameW = Math.max(11, ...data.components.map((c) => c.componentId.length + 4));
|
|
8383
|
+
const entriesW = Math.max(7, 8);
|
|
8384
|
+
const sizeW = Math.max(4, 10);
|
|
8385
|
+
const totalW = nameW + entriesW + sizeW + 4;
|
|
8386
|
+
const sep = "─".repeat(totalW);
|
|
8387
|
+
lines.push(`${pad(nameHeader, nameW)} ${pad(entriesHeader, entriesW, "left")} ${pad(sizeHeader, sizeW, "left")}`);
|
|
8388
|
+
lines.push(sep);
|
|
8389
|
+
for (const comp of data.components) {
|
|
8390
|
+
const nameStr = ` ◆ ${comp.componentId}`;
|
|
8391
|
+
const entriesStr = String(comp.entryCount);
|
|
8392
|
+
const sizeStr = formatSize(comp.totalBytes);
|
|
8393
|
+
lines.push(`${pad(nameStr, nameW)} ${pad(entriesStr, entriesW, "left")} ${pad(sizeStr, sizeW, "left")}`);
|
|
8394
|
+
}
|
|
8395
|
+
}
|
|
8396
|
+
if (data.skippedReasons.length > 0) {
|
|
8397
|
+
lines.push("");
|
|
8398
|
+
lines.push("Skipped entries:");
|
|
8399
|
+
for (const s of data.skippedReasons) lines.push(` ⚠ ${s.componentId} (${s.route}) — ${s.message}`);
|
|
8400
|
+
}
|
|
8401
|
+
return lines;
|
|
8402
|
+
}
|
|
7507
8403
|
/** Measure raw + gzip sizes for all JS chunks and CSS assets in a bundle. */
|
|
7508
8404
|
function collectChunkSizes(bundle) {
|
|
7509
8405
|
const sizes = /* @__PURE__ */ new Map();
|
|
@@ -7691,6 +8587,18 @@ function timberBuildReport(ctx) {
|
|
|
7691
8587
|
logger.info("");
|
|
7692
8588
|
logger.info(`✓ built ${entries.length} routes for all three environments (rsc, ssr, client) in ${elapsed}s`);
|
|
7693
8589
|
logger.info("");
|
|
8590
|
+
},
|
|
8591
|
+
buildApp: {
|
|
8592
|
+
order: "post",
|
|
8593
|
+
async handler() {
|
|
8594
|
+
if (ctx.dev || !logger) return;
|
|
8595
|
+
const prebuiltData = buildPrebuiltReportData(loadPrebuiltManifest(ctx.buildDir), ctx.captureSummary);
|
|
8596
|
+
if (!prebuiltData) return;
|
|
8597
|
+
logger.info("");
|
|
8598
|
+
const prebuiltLines = buildPrebuiltReport(prebuiltData);
|
|
8599
|
+
for (const line of prebuiltLines) logger.info(line);
|
|
8600
|
+
logger.info("");
|
|
8601
|
+
}
|
|
7694
8602
|
}
|
|
7695
8603
|
};
|
|
7696
8604
|
}
|
|
@@ -8202,6 +9110,7 @@ function timber(config) {
|
|
|
8202
9110
|
previewConfig.port = resolvedDevPort;
|
|
8203
9111
|
previewConfig.strictPort = resolvedDevPortExplicit;
|
|
8204
9112
|
}
|
|
9113
|
+
serverConfig.forwardConsole = resolveForwardConsole(ctx.config.devBrowserLogs);
|
|
8205
9114
|
return {
|
|
8206
9115
|
build: { outDir: buildOutDir },
|
|
8207
9116
|
environments: envOutDirs,
|
|
@@ -8247,6 +9156,8 @@ function timber(config) {
|
|
|
8247
9156
|
timberFonts(ctx),
|
|
8248
9157
|
timberMdx(ctx),
|
|
8249
9158
|
timberCacheTransform(ctx),
|
|
9159
|
+
timberPrerenderSugar(ctx),
|
|
9160
|
+
timberPrebuilt(ctx),
|
|
8250
9161
|
timberContent(ctx),
|
|
8251
9162
|
timberServerBundle(),
|
|
8252
9163
|
timberCloudflareWasm(ctx),
|
|
@@ -8254,7 +9165,6 @@ function timber(config) {
|
|
|
8254
9165
|
timberBuildReport(ctx),
|
|
8255
9166
|
timberAdapterBuild(ctx),
|
|
8256
9167
|
timberDevLogs(ctx),
|
|
8257
|
-
timberDevBrowserLogs(ctx),
|
|
8258
9168
|
timberDevServer(ctx)
|
|
8259
9169
|
];
|
|
8260
9170
|
}
|