@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
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared AST helpers for callsite-ID transforms.
|
|
3
|
+
*
|
|
4
|
+
* Both timber-cache-transform (plugins/cache.ts) and timber-prebuilt
|
|
5
|
+
* (plugins/prebuilt.ts) detect calls on bindings imported from the cache
|
|
6
|
+
* module and stamp them with content-derived stable IDs. The node shapes,
|
|
7
|
+
* import-binding resolution, recursive call walk, and ID derivation are
|
|
8
|
+
* identical — only the callee shape and rewrite differ per plugin.
|
|
9
|
+
*
|
|
10
|
+
* See TIM-997, TIM-1053, design/06-caching.md §"Cache Key Security".
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { fnv1aHash } from '../cache/fast-hash.js';
|
|
14
|
+
|
|
15
|
+
/** Module specifiers whose bindings the transforms track. */
|
|
16
|
+
export const CACHE_MODULE_SOURCES = new Set(['@timber-js/app/cache', 'timber/cache']);
|
|
17
|
+
|
|
18
|
+
export interface PositionedNode {
|
|
19
|
+
type: string;
|
|
20
|
+
start: number;
|
|
21
|
+
end: number;
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface IdentifierNode extends PositionedNode {
|
|
26
|
+
type: 'Identifier';
|
|
27
|
+
name: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ImportDeclarationNode extends PositionedNode {
|
|
31
|
+
type: 'ImportDeclaration';
|
|
32
|
+
source: { value: string };
|
|
33
|
+
specifiers: Array<
|
|
34
|
+
PositionedNode & {
|
|
35
|
+
type: string;
|
|
36
|
+
local: IdentifierNode;
|
|
37
|
+
imported?: IdentifierNode;
|
|
38
|
+
}
|
|
39
|
+
>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CallExpressionNode extends PositionedNode {
|
|
43
|
+
type: 'CallExpression';
|
|
44
|
+
callee: PositionedNode;
|
|
45
|
+
arguments: PositionedNode[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface MemberExpressionNode extends PositionedNode {
|
|
49
|
+
type: 'MemberExpression';
|
|
50
|
+
object: PositionedNode;
|
|
51
|
+
property: PositionedNode;
|
|
52
|
+
computed: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ProgramNode extends PositionedNode {
|
|
56
|
+
type: 'Program';
|
|
57
|
+
body: PositionedNode[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ImportBindings {
|
|
61
|
+
/** Local names bound to the named import (`import { cache as c }`). */
|
|
62
|
+
named: Set<string>;
|
|
63
|
+
/** Local names of namespace imports (`import * as ns`) of a tracked module. */
|
|
64
|
+
namespaces: Set<string>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Find the local binding name(s) for `importedName` imported from one of
|
|
69
|
+
* `sources` — both named imports (including aliases) and namespace imports
|
|
70
|
+
* (TIM-1054).
|
|
71
|
+
*/
|
|
72
|
+
export function findImportBindings(
|
|
73
|
+
program: ProgramNode,
|
|
74
|
+
sources: ReadonlySet<string>,
|
|
75
|
+
importedName: string
|
|
76
|
+
): ImportBindings {
|
|
77
|
+
const named = new Set<string>();
|
|
78
|
+
const namespaces = new Set<string>();
|
|
79
|
+
for (const stmt of program.body) {
|
|
80
|
+
if (stmt.type !== 'ImportDeclaration') continue;
|
|
81
|
+
const decl = stmt as unknown as ImportDeclarationNode;
|
|
82
|
+
if (!sources.has(decl.source.value)) continue;
|
|
83
|
+
for (const spec of decl.specifiers) {
|
|
84
|
+
if (spec.type === 'ImportNamespaceSpecifier') {
|
|
85
|
+
namespaces.add(spec.local.name);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (spec.type !== 'ImportSpecifier') continue;
|
|
89
|
+
const specImportedName = spec.imported?.name ?? spec.local.name;
|
|
90
|
+
if (specImportedName === importedName) {
|
|
91
|
+
named.add(spec.local.name);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { named, namespaces };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Recursively walk AST nodes collecting call expressions accepted by
|
|
100
|
+
* `matches`. Calls are typically at module scope, but calls inside
|
|
101
|
+
* functions/blocks are handled for completeness.
|
|
102
|
+
*
|
|
103
|
+
* NOTE: matching is by binding NAME, so nested scopes that shadow a tracked
|
|
104
|
+
* import can produce false positives for consumers of this walker (see
|
|
105
|
+
* TIM-1141 for the cache transform). The prebuilt transform avoids the
|
|
106
|
+
* class entirely by not using this walker — it allowlists top-level
|
|
107
|
+
* declarator positions instead (plugins/prebuilt.ts).
|
|
108
|
+
*/
|
|
109
|
+
export function findCallSites(
|
|
110
|
+
node: PositionedNode,
|
|
111
|
+
matches: (call: CallExpressionNode) => boolean,
|
|
112
|
+
results: CallExpressionNode[]
|
|
113
|
+
): void {
|
|
114
|
+
if (node.type === 'CallExpression') {
|
|
115
|
+
const call = node as unknown as CallExpressionNode;
|
|
116
|
+
if (matches(call)) {
|
|
117
|
+
results.push(call);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const key of Object.keys(node)) {
|
|
121
|
+
if (key === 'type' || key === 'start' || key === 'end') continue;
|
|
122
|
+
const val = node[key];
|
|
123
|
+
if (val && typeof val === 'object') {
|
|
124
|
+
if (Array.isArray(val)) {
|
|
125
|
+
for (const item of val) {
|
|
126
|
+
if (item && typeof item === 'object' && typeof item.type === 'string') {
|
|
127
|
+
findCallSites(item as PositionedNode, matches, results);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} else if (typeof (val as PositionedNode).type === 'string') {
|
|
131
|
+
findCallSites(val as PositionedNode, matches, results);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Generate a stable callsite ID from a module path, a kind discriminator
|
|
139
|
+
* ('cache' | 'component'), the source text of the enclosing top-level
|
|
140
|
+
* statement, the source text of the call expression, and an occurrence index
|
|
141
|
+
* among fully byte-identical (statement, call) pairs in the same file.
|
|
142
|
+
*
|
|
143
|
+
* The ID is deterministic and:
|
|
144
|
+
* - Stable across cold starts AND across builds/deploys for unchanged calls —
|
|
145
|
+
* inserting, deleting, or reordering other calls does not affect it
|
|
146
|
+
* - Unique across different modules (relPath component)
|
|
147
|
+
* - Unique across distinct calls within a module (statement + span hashes),
|
|
148
|
+
* with fully identical duplicates disambiguated by source-order occurrence
|
|
149
|
+
* - Fresh when the wrapped fn, opts, or enclosing statement change, which
|
|
150
|
+
* intentionally invalidates the old persisted entries
|
|
151
|
+
*
|
|
152
|
+
* The enclosing-statement text is included so that byte-identical calls in
|
|
153
|
+
* different scopes — e.g. two factory helpers with identical call spans —
|
|
154
|
+
* get distinct, insertion-stable IDs. See plugins/cache.ts header for the
|
|
155
|
+
* full derivation rationale (TIM-1053).
|
|
156
|
+
*/
|
|
157
|
+
export function generateStableCallsiteId(
|
|
158
|
+
relativeModulePath: string,
|
|
159
|
+
kind: string,
|
|
160
|
+
statementText: string,
|
|
161
|
+
callSpanText: string,
|
|
162
|
+
occurrence: number
|
|
163
|
+
): string {
|
|
164
|
+
return fnv1aHash(
|
|
165
|
+
`${relativeModulePath}:${kind}:${fnv1aHash(statementText)}:${fnv1aHash(callSpanText)}:${occurrence}`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSX call-site props extraction for prebuilt components (TIM-1142).
|
|
3
|
+
*
|
|
4
|
+
* At build time, the timber-prebuilt transform captures the exact props
|
|
5
|
+
* from each JSX usage site of a cache.component binding so the post-build
|
|
6
|
+
* capture pass can prerender entries for each distinct prop set. Only
|
|
7
|
+
* statically analyzable props (JSON-safe literals, arrays, and plain
|
|
8
|
+
* objects) are captured; dynamic/computed props bail — those call sites
|
|
9
|
+
* fall back to live rendering per design/44 §Props Derivation.
|
|
10
|
+
*
|
|
11
|
+
* See design/44-render-at-build-time.md §Props Derivation at Build Time.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { PluginContext } from '../plugin-context.js';
|
|
15
|
+
import type { IdentifierNode, PositionedNode, ProgramNode } from './callsite-ast.js';
|
|
16
|
+
|
|
17
|
+
interface JSXElementNode extends PositionedNode {
|
|
18
|
+
type: 'JSXElement';
|
|
19
|
+
openingElement: {
|
|
20
|
+
type: 'JSXOpeningElement';
|
|
21
|
+
name: PositionedNode;
|
|
22
|
+
attributes: PositionedNode[];
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface JSXAttributeNode extends PositionedNode {
|
|
27
|
+
type: 'JSXAttribute';
|
|
28
|
+
name: { type: string; name?: string };
|
|
29
|
+
value: PositionedNode | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface JSXExpressionContainerNode extends PositionedNode {
|
|
33
|
+
type: 'JSXExpressionContainer';
|
|
34
|
+
expression: PositionedNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type JsonValue =
|
|
38
|
+
| string
|
|
39
|
+
| number
|
|
40
|
+
| boolean
|
|
41
|
+
| null
|
|
42
|
+
| JsonValue[]
|
|
43
|
+
| { [key: string]: JsonValue };
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Try to extract a static JSON-safe value from an AST expression node.
|
|
47
|
+
* Returns `undefined` (not null — null is a valid JSON value) when the
|
|
48
|
+
* expression is dynamic/computed and cannot be captured statically.
|
|
49
|
+
*/
|
|
50
|
+
function tryExtractStaticValue(node: PositionedNode): JsonValue | undefined {
|
|
51
|
+
if (node.type === 'Literal') {
|
|
52
|
+
const val = (node as { value?: unknown }).value;
|
|
53
|
+
if (
|
|
54
|
+
val === null ||
|
|
55
|
+
typeof val === 'string' ||
|
|
56
|
+
typeof val === 'number' ||
|
|
57
|
+
typeof val === 'boolean'
|
|
58
|
+
) {
|
|
59
|
+
return val as JsonValue;
|
|
60
|
+
}
|
|
61
|
+
return undefined; // regex or bigint literal
|
|
62
|
+
}
|
|
63
|
+
if (node.type === 'UnaryExpression') {
|
|
64
|
+
const unary = node as unknown as { operator: string; argument: PositionedNode };
|
|
65
|
+
if (unary.operator === '-' && unary.argument.type === 'Literal') {
|
|
66
|
+
const val = (unary.argument as { value?: unknown }).value;
|
|
67
|
+
if (typeof val === 'number') return -val;
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
if (node.type === 'TemplateLiteral') {
|
|
72
|
+
const tpl = node as unknown as {
|
|
73
|
+
quasis: { value: { cooked: string } }[];
|
|
74
|
+
expressions: unknown[];
|
|
75
|
+
};
|
|
76
|
+
if (tpl.expressions.length === 0) return tpl.quasis[0]?.value.cooked ?? undefined;
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
if (node.type === 'ArrayExpression') {
|
|
80
|
+
const elements = (node as unknown as { elements: (PositionedNode | null)[] }).elements;
|
|
81
|
+
const arr: JsonValue[] = [];
|
|
82
|
+
for (const el of elements) {
|
|
83
|
+
if (el === null) return undefined; // sparse
|
|
84
|
+
const val = tryExtractStaticValue(el);
|
|
85
|
+
if (val === undefined) return undefined;
|
|
86
|
+
arr.push(val);
|
|
87
|
+
}
|
|
88
|
+
return arr;
|
|
89
|
+
}
|
|
90
|
+
if (node.type === 'ObjectExpression') {
|
|
91
|
+
const properties = (node as unknown as { properties: PositionedNode[] }).properties;
|
|
92
|
+
const obj: Record<string, JsonValue> = {};
|
|
93
|
+
for (const prop of properties) {
|
|
94
|
+
if (prop.type === 'SpreadElement') return undefined;
|
|
95
|
+
if (prop.type !== 'Property') return undefined;
|
|
96
|
+
const p = prop as unknown as {
|
|
97
|
+
computed?: boolean;
|
|
98
|
+
key: PositionedNode & { name?: string; value?: unknown };
|
|
99
|
+
value: PositionedNode;
|
|
100
|
+
};
|
|
101
|
+
if (p.computed) return undefined;
|
|
102
|
+
const keyName =
|
|
103
|
+
p.key.type === 'Identifier'
|
|
104
|
+
? p.key.name
|
|
105
|
+
: p.key.type === 'Literal'
|
|
106
|
+
? String(p.key.value)
|
|
107
|
+
: undefined;
|
|
108
|
+
if (keyName === undefined) return undefined;
|
|
109
|
+
const val = tryExtractStaticValue(p.value);
|
|
110
|
+
if (val === undefined) return undefined;
|
|
111
|
+
obj[keyName] = val;
|
|
112
|
+
}
|
|
113
|
+
return obj;
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Extract static props from a JSX element's attributes. Returns null if
|
|
120
|
+
* any attribute is dynamic (spread, computed expression, JSX element, etc.)
|
|
121
|
+
* — the component must fall back to dynamic rendering for that call site.
|
|
122
|
+
*/
|
|
123
|
+
export function extractStaticJsxProps(
|
|
124
|
+
attributes: PositionedNode[]
|
|
125
|
+
): Record<string, JsonValue> | null {
|
|
126
|
+
const props: Record<string, JsonValue> = {};
|
|
127
|
+
for (const attr of attributes) {
|
|
128
|
+
if (attr.type === 'JSXSpreadAttribute') return null;
|
|
129
|
+
if (attr.type !== 'JSXAttribute') return null;
|
|
130
|
+
const jsxAttr = attr as unknown as JSXAttributeNode;
|
|
131
|
+
const name = jsxAttr.name?.name;
|
|
132
|
+
if (!name) return null;
|
|
133
|
+
// React strips key and ref before passing to component props — including
|
|
134
|
+
// them would produce cache entries unaddressable at request time.
|
|
135
|
+
if (name === 'key' || name === 'ref') continue;
|
|
136
|
+
if (jsxAttr.value === null) {
|
|
137
|
+
props[name] = true;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (jsxAttr.value.type === 'Literal') {
|
|
141
|
+
const val = tryExtractStaticValue(jsxAttr.value);
|
|
142
|
+
if (val === undefined) return null;
|
|
143
|
+
props[name] = val;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (jsxAttr.value.type === 'JSXExpressionContainer') {
|
|
147
|
+
const container = jsxAttr.value as unknown as JSXExpressionContainerNode;
|
|
148
|
+
const val = tryExtractStaticValue(container.expression);
|
|
149
|
+
if (val === undefined) return null;
|
|
150
|
+
props[name] = val;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
return props;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Walk an AST tree and collect JSX usages of the given binding names,
|
|
160
|
+
* returning the static props for each usage. Returns a map of
|
|
161
|
+
* bindingName → array of static prop objects (or null for dynamic props).
|
|
162
|
+
*/
|
|
163
|
+
export function collectJsxUsages(
|
|
164
|
+
program: ProgramNode,
|
|
165
|
+
bindingNames: Set<string>
|
|
166
|
+
): Map<string, Array<Record<string, JsonValue> | null>> {
|
|
167
|
+
const result = new Map<string, Array<Record<string, JsonValue> | null>>();
|
|
168
|
+
const walk = (node: unknown) => {
|
|
169
|
+
if (!node || typeof node !== 'object') return;
|
|
170
|
+
const n = node as PositionedNode;
|
|
171
|
+
if (n.type === 'JSXElement') {
|
|
172
|
+
const jsx = n as unknown as JSXElementNode;
|
|
173
|
+
const tagName = jsx.openingElement.name;
|
|
174
|
+
if (tagName.type === 'JSXIdentifier') {
|
|
175
|
+
const name = (tagName as IdentifierNode).name;
|
|
176
|
+
if (bindingNames.has(name)) {
|
|
177
|
+
// Children become props.children at runtime — if the element has
|
|
178
|
+
// children we can't capture the full props shape statically, so
|
|
179
|
+
// treat it as a dynamic call site (bail to null).
|
|
180
|
+
const children = (jsx as unknown as { children?: unknown[] }).children ?? [];
|
|
181
|
+
const hasChildren =
|
|
182
|
+
children.length > 0 &&
|
|
183
|
+
children.some((c: unknown) => {
|
|
184
|
+
const child = c as PositionedNode;
|
|
185
|
+
if (child.type === 'JSXText') {
|
|
186
|
+
// Whitespace-only text nodes are not passed as children
|
|
187
|
+
return (child as unknown as { value: string }).value.trim().length > 0;
|
|
188
|
+
}
|
|
189
|
+
return true;
|
|
190
|
+
});
|
|
191
|
+
const props = hasChildren ? null : extractStaticJsxProps(jsx.openingElement.attributes);
|
|
192
|
+
const usages = result.get(name) ?? [];
|
|
193
|
+
usages.push(props);
|
|
194
|
+
result.set(name, usages);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
for (const value of Object.values(n)) {
|
|
199
|
+
if (Array.isArray(value)) {
|
|
200
|
+
for (const item of value) walk(item);
|
|
201
|
+
} else if (value && typeof value === 'object' && 'type' in value) {
|
|
202
|
+
walk(value);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
for (const stmt of program.body) walk(stmt);
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Deep stable JSON stringification with recursively sorted keys.
|
|
212
|
+
* `JSON.stringify` with an array replacer only includes listed keys at
|
|
213
|
+
* ALL nesting levels, so `{config:{theme:'dark'}}` and
|
|
214
|
+
* `{config:{theme:'light'}}` would both serialize as `{"config":{}}`.
|
|
215
|
+
*/
|
|
216
|
+
function stableStringify(value: unknown): string {
|
|
217
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
218
|
+
if (Array.isArray(value)) return '[' + value.map(stableStringify).join(',') + ']';
|
|
219
|
+
const obj = value as Record<string, unknown>;
|
|
220
|
+
const parts = Object.keys(obj)
|
|
221
|
+
.sort()
|
|
222
|
+
.map((k) => JSON.stringify(k) + ':' + stableStringify(obj[k]));
|
|
223
|
+
return '{' + parts.join(',') + '}';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Add a distinct props object to the call-site-props map for a component,
|
|
228
|
+
* deduplicating by deep JSON value (recursively sorted keys).
|
|
229
|
+
*/
|
|
230
|
+
export function recordCallSiteProps(
|
|
231
|
+
ctx: PluginContext,
|
|
232
|
+
componentId: string,
|
|
233
|
+
props: Record<string, JsonValue>
|
|
234
|
+
): void {
|
|
235
|
+
ctx.prebuiltCallSiteProps ??= new Map();
|
|
236
|
+
const existing = ctx.prebuiltCallSiteProps.get(componentId) ?? [];
|
|
237
|
+
const canonical = stableStringify(props);
|
|
238
|
+
const isDuplicate = existing.some((p) => stableStringify(p) === canonical);
|
|
239
|
+
if (!isDuplicate) {
|
|
240
|
+
existing.push(props);
|
|
241
|
+
ctx.prebuiltCallSiteProps.set(componentId, existing);
|
|
242
|
+
}
|
|
243
|
+
}
|
package/src/plugins/entries.ts
CHANGED
|
@@ -154,6 +154,10 @@ function generateConfigModule(ctx: PluginContext): string {
|
|
|
154
154
|
// Per-build deployment ID for version skew detection (TIM-446).
|
|
155
155
|
// Null in dev mode — HMR handles code updates without full reloads.
|
|
156
156
|
deploymentId: ctx.deploymentId ?? null,
|
|
157
|
+
// Browser log forwarding threshold — used by buildClientScripts to
|
|
158
|
+
// decide whether /@vite/client is needed when clientJavascript is
|
|
159
|
+
// disabled. See TIM-1152.
|
|
160
|
+
devBrowserLogs: ctx.config.devBrowserLogs,
|
|
157
161
|
// Note: project root is NOT included here — it was leaking server
|
|
158
162
|
// filesystem paths to client bundles (TIM-787). Dev error pages get
|
|
159
163
|
// the root through the dev server, not the runtime config.
|
|
@@ -244,8 +248,12 @@ function detectConfigFile(root: string): string | null {
|
|
|
244
248
|
* cannot be JSON-serialized into virtual:timber-config, so it must
|
|
245
249
|
* be loaded at runtime via dynamic import. See TIM-599.
|
|
246
250
|
*/
|
|
247
|
-
function generateCacheHandlerModule(
|
|
248
|
-
|
|
251
|
+
function generateCacheHandlerModule(
|
|
252
|
+
configPath: string | null,
|
|
253
|
+
hasCacheHandler: boolean,
|
|
254
|
+
hasCdnPurge: boolean
|
|
255
|
+
): string {
|
|
256
|
+
if (configPath && (hasCacheHandler || hasCdnPurge)) {
|
|
249
257
|
return [
|
|
250
258
|
'// Auto-generated cache handler loader — do not edit.',
|
|
251
259
|
'// Generated by timber-entries plugin.',
|
|
@@ -255,17 +263,27 @@ function generateCacheHandlerModule(configPath: string | null, hasCacheHandler:
|
|
|
255
263
|
` const config = mod.default ?? mod;`,
|
|
256
264
|
` return config.cacheHandler ?? null;`,
|
|
257
265
|
`}`,
|
|
266
|
+
'',
|
|
267
|
+
`export async function loadCdnPurgeHandler() {`,
|
|
268
|
+
` const mod = await import(${JSON.stringify(configPath)});`,
|
|
269
|
+
` const config = mod.default ?? mod;`,
|
|
270
|
+
` return config.cdnPurge ?? null;`,
|
|
271
|
+
`}`,
|
|
258
272
|
].join('\n');
|
|
259
273
|
}
|
|
260
274
|
|
|
261
275
|
return [
|
|
262
276
|
'// Auto-generated cache handler loader — do not edit.',
|
|
263
277
|
'// Generated by timber-entries plugin.',
|
|
264
|
-
'// No cacheHandler configured in timber.config.',
|
|
278
|
+
'// No cacheHandler or cdnPurge configured in timber.config.',
|
|
265
279
|
'',
|
|
266
280
|
`export default async function loadCacheHandler() {`,
|
|
267
281
|
` return null;`,
|
|
268
282
|
`}`,
|
|
283
|
+
'',
|
|
284
|
+
`export async function loadCdnPurgeHandler() {`,
|
|
285
|
+
` return null;`,
|
|
286
|
+
`}`,
|
|
269
287
|
].join('\n');
|
|
270
288
|
}
|
|
271
289
|
|
|
@@ -336,7 +354,8 @@ export function timberEntries(ctx: PluginContext): Plugin {
|
|
|
336
354
|
if (id === RESOLVED_CACHE_HANDLER_ID) {
|
|
337
355
|
const configPath = detectConfigFile(ctx.root);
|
|
338
356
|
const hasCacheHandler = ctx.config.cacheHandler != null;
|
|
339
|
-
|
|
357
|
+
const hasCdnPurge = ctx.config.cdnPurge != null;
|
|
358
|
+
return generateCacheHandlerModule(configPath, hasCacheHandler, hasCdnPurge);
|
|
340
359
|
}
|
|
341
360
|
return null;
|
|
342
361
|
},
|
package/src/plugins/fonts.ts
CHANGED
|
@@ -221,9 +221,23 @@ export function timberFonts(ctx: PluginContext): Plugin {
|
|
|
221
221
|
*
|
|
222
222
|
* Delegates to `runFontsTransform`. Errors raised through the
|
|
223
223
|
* `this.error` callback are surfaced as Vite plugin errors.
|
|
224
|
+
*
|
|
225
|
+
* The `filter` option uses Vite 8's rolldown-native hook filters to
|
|
226
|
+
* skip non-JS/TS files and files that don't mention any font module
|
|
227
|
+
* before crossing into JS — replacing the hand-rolled pre-filter that
|
|
228
|
+
* was previously inside `runFontsTransform` (TIM-840, TIM-1147).
|
|
224
229
|
*/
|
|
225
|
-
transform
|
|
226
|
-
|
|
230
|
+
transform: {
|
|
231
|
+
filter: {
|
|
232
|
+
id: {
|
|
233
|
+
include: /^[^?]*\.(?:js|jsx|ts|tsx|mjs|cjs|mts|cts)(?:\?|$)/,
|
|
234
|
+
exclude: /(?:^\0|(?:^|\/)node_modules\/)/,
|
|
235
|
+
},
|
|
236
|
+
code: ['@timber/fonts/', '@timber-js/app/fonts/', 'next/font/'],
|
|
237
|
+
},
|
|
238
|
+
handler(code: string, id: string) {
|
|
239
|
+
return runFontsTransform(code, id, pipeline, (msg) => this.error(msg));
|
|
240
|
+
},
|
|
227
241
|
},
|
|
228
242
|
|
|
229
243
|
/**
|