@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/src/server/form-data.ts
CHANGED
|
@@ -41,8 +41,9 @@ export function parseFormData(formData: FormData): Record<string, unknown> {
|
|
|
41
41
|
if (processed.length === 1) {
|
|
42
42
|
flat[key] = processed[0];
|
|
43
43
|
} else {
|
|
44
|
-
// Filter out undefined
|
|
45
|
-
|
|
44
|
+
// Filter out undefined (empty Files) and empty strings from multi-value fields.
|
|
45
|
+
// Multi-value fields (e.g. tags=js&tags=&tags=ts) should not include blanks.
|
|
46
|
+
flat[key] = processed.filter((v) => v !== undefined && v !== '');
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -52,15 +53,13 @@ export function parseFormData(formData: FormData): Record<string, unknown> {
|
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
55
|
* Normalize a single FormData entry value.
|
|
55
|
-
* - Empty strings → undefined (enables .optional() semantics)
|
|
56
56
|
* - Empty File objects (no selection) → undefined
|
|
57
|
+
* - Strings pass through as-is (empty strings stay as empty strings so
|
|
58
|
+
* schema validators like `z.string().min(1, 'Required')` can produce
|
|
59
|
+
* their custom error messages instead of a generic type mismatch)
|
|
57
60
|
* - Everything else passes through as-is
|
|
58
61
|
*/
|
|
59
62
|
function normalizeValue(value: FormDataEntryValue): unknown {
|
|
60
|
-
if (typeof value === 'string') {
|
|
61
|
-
return value === '' ? undefined : value;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
63
|
// File input with no selection: browsers submit a File with name="" and size=0
|
|
65
64
|
if (value instanceof File && value.size === 0 && value.name === '') {
|
|
66
65
|
return undefined;
|
|
@@ -149,6 +148,23 @@ const DANGEROUS_KEYS = new Set(['__proto__']);
|
|
|
149
148
|
* ```
|
|
150
149
|
*/
|
|
151
150
|
export const coerce = {
|
|
151
|
+
/**
|
|
152
|
+
* Coerce an empty string to undefined for optional text fields.
|
|
153
|
+
* Use with `.optional()` schemas where an empty input means "not provided".
|
|
154
|
+
*
|
|
155
|
+
* ```ts
|
|
156
|
+
* // Zod
|
|
157
|
+
* z.unknown().transform(coerce.text).pipe(z.string().optional())
|
|
158
|
+
* // Valibot
|
|
159
|
+
* v.pipe(v.unknown(), v.transform(coerce.text), v.optional(v.string()))
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
text(value: unknown): string | undefined {
|
|
163
|
+
if (value === undefined || value === null || value === '') return undefined;
|
|
164
|
+
if (typeof value === 'string') return value;
|
|
165
|
+
return undefined;
|
|
166
|
+
},
|
|
167
|
+
|
|
152
168
|
/**
|
|
153
169
|
* Coerce a string to a number.
|
|
154
170
|
* - `"42"` → `42`
|
|
@@ -409,12 +409,20 @@ export function buildClientScripts(runtimeConfig: {
|
|
|
409
409
|
output: string;
|
|
410
410
|
clientJavascript: { disabled: boolean; enableHMRInDev: boolean };
|
|
411
411
|
dev: boolean;
|
|
412
|
+
devBrowserLogs?: string;
|
|
412
413
|
buildManifest?: import('./build-manifest.js').BuildManifest;
|
|
413
414
|
}): ClientBootstrapConfig {
|
|
414
415
|
if (runtimeConfig.clientJavascript.disabled) {
|
|
415
|
-
// When client JS is disabled
|
|
416
|
-
//
|
|
417
|
-
|
|
416
|
+
// When client JS is disabled, import /@vite/client in dev mode if
|
|
417
|
+
// any dev feature needs it: HMR for CSS (enableHMRInDev) or
|
|
418
|
+
// forwardConsole for browser→terminal log forwarding (devBrowserLogs
|
|
419
|
+
// not 'none'). Without /@vite/client, server.forwardConsole's
|
|
420
|
+
// client-side console patching never activates. When both features
|
|
421
|
+
// are off, honor the zero-JS opt-out. See TIM-1152.
|
|
422
|
+
const needsViteClient =
|
|
423
|
+
runtimeConfig.clientJavascript.enableHMRInDev ||
|
|
424
|
+
(runtimeConfig.devBrowserLogs ?? 'warn') !== 'none';
|
|
425
|
+
if (runtimeConfig.dev && needsViteClient) {
|
|
418
426
|
return {
|
|
419
427
|
bootstrapScriptContent: 'import("/@vite/client")',
|
|
420
428
|
preloadLinks: '',
|
package/src/server/internal.ts
CHANGED
|
@@ -101,7 +101,7 @@ export {
|
|
|
101
101
|
resolveMetadataUrls,
|
|
102
102
|
renderMetadataToElements,
|
|
103
103
|
} from './metadata.js';
|
|
104
|
-
export type { SegmentMetadataEntry, ResolveMetadataOptions
|
|
104
|
+
export type { SegmentMetadataEntry, ResolveMetadataOptions } from './metadata.js';
|
|
105
105
|
|
|
106
106
|
// ── Metadata Routes ──────────────────────────────────────────────────────
|
|
107
107
|
export {
|
|
@@ -163,3 +163,19 @@ export type { RouteModule, RouteHandler, HttpMethod } from './route-handler.js';
|
|
|
163
163
|
// ── Render Timeout ───────────────────────────────────────────────────────
|
|
164
164
|
export { RenderTimeoutError } from './render-timeout.js';
|
|
165
165
|
export type { RenderTimeout } from './render-timeout.js';
|
|
166
|
+
|
|
167
|
+
// ── Prebuilt Components (cache.component transform targets) ─────────────
|
|
168
|
+
export {
|
|
169
|
+
__prebuilt,
|
|
170
|
+
__prebuiltDev,
|
|
171
|
+
getPrebuiltRegistration,
|
|
172
|
+
getPrebuiltRegistry,
|
|
173
|
+
} from './prebuilt-runtime.js';
|
|
174
|
+
export type { PrebuiltComponentOptions, PrebuiltRegistration } from './prebuilt-runtime.js';
|
|
175
|
+
export { setPrebuiltPayloadSource, hasPrebuiltPayloadSource } from './prebuilt/payload-source.js';
|
|
176
|
+
export type {
|
|
177
|
+
PrebuiltManifest,
|
|
178
|
+
PrebuiltManifestEntry,
|
|
179
|
+
PrebuiltPayloadSource,
|
|
180
|
+
} from './prebuilt/payload-source.js';
|
|
181
|
+
export { computeComponentCacheKey } from './prebuilt/cache-key.js';
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component cache-key hashing for prebuilt (cache.component) entries.
|
|
3
|
+
*
|
|
4
|
+
* Key: `sha256(canonicalJson({ props, params })).slice(0, 16)` — see
|
|
5
|
+
* design/44-render-at-build-time.md §Cache Key. Both the request-time
|
|
6
|
+
* lookup (prebuilt-runtime.ts) and the post-build capture pass (TIM-1119)
|
|
7
|
+
* MUST use this exact function, or build artifacts become unaddressable.
|
|
8
|
+
*
|
|
9
|
+
* THE IDENTITY CONTRACT (design/44 §Cache Key): cache identity is **JSON
|
|
10
|
+
* value semantics** — two inputs are the same entry iff they are the same
|
|
11
|
+
* JSON value. This is an ALLOWLIST, not a denylist: a value participates
|
|
12
|
+
* in caching iff it is plain JSON data — null, boolean, finite number,
|
|
13
|
+
* string, dense array, or a plain `Object.prototype` object with only
|
|
14
|
+
* enumerable string-keyed data properties — plus `undefined` and `-0`,
|
|
15
|
+
* which hash as distinct sentinels because flight preserves them. Every
|
|
16
|
+
* value outside that domain (functions, symbols, bigints, accessors,
|
|
17
|
+
* hidden or symbol-keyed properties, non-`Object.prototype` prototypes
|
|
18
|
+
* including null, aliased references, sparse arrays, cycles, non-finite
|
|
19
|
+
* numbers) is UNKEYABLE by definition: `computeComponentCacheKey` returns
|
|
20
|
+
* null and the caller renders live — slower, never wrong.
|
|
21
|
+
*
|
|
22
|
+
* The line between rejected and declared-out-of-domain: values are
|
|
23
|
+
* REJECTED when the JSON projection itself would be ambiguous or lossy
|
|
24
|
+
* (hidden/symbol/non-enumerable properties, accessors, exotic
|
|
25
|
+
* prototypes). Observable state that leaves the projection intact — key
|
|
26
|
+
* order, reference identity, descriptor metadata (writable/configurable,
|
|
27
|
+
* frozen/sealed/extensible), proxy behavior — is DECLARED outside
|
|
28
|
+
* cache.component's supported input domain rather than rejected:
|
|
29
|
+
* key-order insensitivity is required for determinism, and rejecting
|
|
30
|
+
* frozen objects would unkey ubiquitous legitimate inputs
|
|
31
|
+
* (Object.freeze'd config props) to defend against components that
|
|
32
|
+
* render differently based on property attributes.
|
|
33
|
+
*
|
|
34
|
+
* Two hard rules for this walk itself: it must be TOTAL (never throw
|
|
35
|
+
* anything but UnkeyableValueError) and SIDE-EFFECT-FREE (descriptor
|
|
36
|
+
* reads only; never invoke user code such as getters).
|
|
37
|
+
*
|
|
38
|
+
* Object keys sort recursively so `{a, b}` and `{b, a}` hash identically
|
|
39
|
+
* (design/13-security.md checklist #14 — cache key determinism; key order
|
|
40
|
+
* is deliberately NOT part of identity).
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { createHash } from 'node:crypto';
|
|
44
|
+
|
|
45
|
+
class UnkeyableValueError extends Error {}
|
|
46
|
+
|
|
47
|
+
function canonicalize(value: unknown, seen: Set<object>): string {
|
|
48
|
+
if (value === null) return 'null';
|
|
49
|
+
switch (typeof value) {
|
|
50
|
+
case 'string':
|
|
51
|
+
return JSON.stringify(value);
|
|
52
|
+
case 'boolean':
|
|
53
|
+
return value ? 'true' : 'false';
|
|
54
|
+
case 'number':
|
|
55
|
+
if (!Number.isFinite(value)) throw new UnkeyableValueError('non-finite number');
|
|
56
|
+
// -0 is distinct: flight preserves it ("$-0"), and String(-0) would
|
|
57
|
+
// collapse it to "0" (codex review round 4 on PR #835).
|
|
58
|
+
return Object.is(value, -0) ? '-0' : String(value);
|
|
59
|
+
case 'object':
|
|
60
|
+
break;
|
|
61
|
+
case 'undefined':
|
|
62
|
+
// Distinct sentinel, NOT collapsed to null or omitted: flight
|
|
63
|
+
// serialization preserves `undefined` ("$undefined"), so a component
|
|
64
|
+
// can render [undefined] differently from [null] and {a: undefined}
|
|
65
|
+
// differently from {}. Bare `undefined` cannot collide with any
|
|
66
|
+
// other canonical output (strings are always quoted). Codex review
|
|
67
|
+
// round 3 on PR #835.
|
|
68
|
+
return 'undefined';
|
|
69
|
+
default:
|
|
70
|
+
// function, symbol, bigint
|
|
71
|
+
throw new UnkeyableValueError(`unsupported type: ${typeof value}`);
|
|
72
|
+
}
|
|
73
|
+
const obj = value as object;
|
|
74
|
+
// Any REPEATED reference — cycle or alias — is unkeyable. Flight
|
|
75
|
+
// preserves reference identity (the second occurrence serializes as a
|
|
76
|
+
// row reference), so a component can observe `props.a === props.b`;
|
|
77
|
+
// hashing an aliased shape identically to a structurally-equal
|
|
78
|
+
// non-aliased one could splice the wrong subtree (codex review round 5
|
|
79
|
+
// on PR #835). Objects are never removed from `seen`: revisiting one
|
|
80
|
+
// anywhere in the walk throws.
|
|
81
|
+
if (seen.has(obj)) throw new UnkeyableValueError('repeated object reference (cycle or alias)');
|
|
82
|
+
seen.add(obj);
|
|
83
|
+
// Hidden own properties — symbol keys and non-enumerable props — are
|
|
84
|
+
// readable by a live-rendered component but invisible to this walk, so
|
|
85
|
+
// two observably different inputs would collide on one key (codex
|
|
86
|
+
// review on PR #835). Unkeyable.
|
|
87
|
+
if (Object.getOwnPropertySymbols(obj).length > 0) {
|
|
88
|
+
throw new UnkeyableValueError('symbol-keyed property');
|
|
89
|
+
}
|
|
90
|
+
{
|
|
91
|
+
if (Array.isArray(obj)) {
|
|
92
|
+
// Dense-array own names are exactly ['0'..'n-1', 'length'] — anything
|
|
93
|
+
// else is a custom expando property this walk would silently drop.
|
|
94
|
+
if (Object.getOwnPropertyNames(obj).length !== obj.length + 1) {
|
|
95
|
+
throw new UnkeyableValueError('non-index array property');
|
|
96
|
+
}
|
|
97
|
+
const parts: string[] = [];
|
|
98
|
+
for (let i = 0; i < obj.length; i++) {
|
|
99
|
+
// Sparse holes are unkeyable: a server component receiving the
|
|
100
|
+
// original array can observe `i in arr`, which no canonical form
|
|
101
|
+
// survives faithfully. (An explicit undefined element is fine —
|
|
102
|
+
// handled by the sentinel above.)
|
|
103
|
+
const desc = Object.getOwnPropertyDescriptor(obj, i);
|
|
104
|
+
if (desc === undefined) throw new UnkeyableValueError('sparse array hole');
|
|
105
|
+
// Accessors are rejected WITHOUT invoking them — a getter may
|
|
106
|
+
// throw or side-effect, and the key walk must never alter a
|
|
107
|
+
// render (codex review on PR #835).
|
|
108
|
+
if (desc.get !== undefined || desc.set !== undefined) {
|
|
109
|
+
throw new UnkeyableValueError('accessor property');
|
|
110
|
+
}
|
|
111
|
+
// Non-enumerable elements are invisible to Object.keys/spread/
|
|
112
|
+
// JSON.stringify — the JSON projection would be ambiguous, same
|
|
113
|
+
// rule as the object branch's keys-vs-names check.
|
|
114
|
+
if (!desc.enumerable) throw new UnkeyableValueError('non-enumerable array element');
|
|
115
|
+
parts.push(canonicalize(desc.value, seen));
|
|
116
|
+
}
|
|
117
|
+
return '[' + parts.join(',') + ']';
|
|
118
|
+
}
|
|
119
|
+
// Allowlist: exactly Object.prototype. Dates, Maps, class instances —
|
|
120
|
+
// and null-prototype objects, which a component can distinguish from
|
|
121
|
+
// literals via Object.getPrototypeOf — are all outside the JSON
|
|
122
|
+
// domain.
|
|
123
|
+
if (Object.getPrototypeOf(obj) !== Object.prototype) {
|
|
124
|
+
throw new UnkeyableValueError('non-plain object');
|
|
125
|
+
}
|
|
126
|
+
const keys = Object.keys(obj).sort();
|
|
127
|
+
if (keys.length !== Object.getOwnPropertyNames(obj).length) {
|
|
128
|
+
throw new UnkeyableValueError('non-enumerable property');
|
|
129
|
+
}
|
|
130
|
+
const parts: string[] = [];
|
|
131
|
+
for (const key of keys) {
|
|
132
|
+
// Descriptor read, not property read: accessors are rejected WITHOUT
|
|
133
|
+
// invoking them — a getter may throw or side-effect, and the key
|
|
134
|
+
// walk must never alter a render (codex review on PR #835).
|
|
135
|
+
const desc = Object.getOwnPropertyDescriptor(obj, key)!;
|
|
136
|
+
if (desc.get !== undefined || desc.set !== undefined) {
|
|
137
|
+
throw new UnkeyableValueError('accessor property');
|
|
138
|
+
}
|
|
139
|
+
parts.push(JSON.stringify(key) + ':' + canonicalize(desc.value, seen));
|
|
140
|
+
}
|
|
141
|
+
return '{' + parts.join(',') + '}';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Canonicalize a single value, or null if it is unkeyable. The ONE
|
|
147
|
+
* definition of "faithfully comparable" — every prebuilt code path that
|
|
148
|
+
* needs value equality (cache keys, slot-param divergence) goes through
|
|
149
|
+
* this, so the unkeyable rules (accessors, hidden props, aliases,
|
|
150
|
+
* bigints, …) live in exactly one place.
|
|
151
|
+
*/
|
|
152
|
+
export function tryCanonicalize(value: unknown): string | null {
|
|
153
|
+
try {
|
|
154
|
+
return canonicalize(value, new Set());
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return toUnkeyable(error);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Totality by construction: ANY throw from the walk means the input is
|
|
162
|
+
* outside the keyable domain. The walk touches only user-controlled
|
|
163
|
+
* values through reflection ops — and a Proxy trap can throw arbitrary
|
|
164
|
+
* errors from any of them (codex review on PR #835) — so converting only
|
|
165
|
+
* UnkeyableValueError would let hostile/exotic inputs abort a render
|
|
166
|
+
* that plain live rendering would have served fine. Unexpected error
|
|
167
|
+
* types are logged for visibility (they would indicate either a proxy
|
|
168
|
+
* trap or a canonicalizer bug) but still degrade to unkeyable.
|
|
169
|
+
*/
|
|
170
|
+
function toUnkeyable(error: unknown): null {
|
|
171
|
+
if (!(error instanceof UnkeyableValueError)) {
|
|
172
|
+
console.error(
|
|
173
|
+
'[timber] cache key canonicalization threw — treating input as unkeyable:',
|
|
174
|
+
error
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Compute the cache-entry hash for a component render:
|
|
182
|
+
* `sha256({props, params}).slice(0, 16)` over canonical JSON.
|
|
183
|
+
*
|
|
184
|
+
* Returns null when props (or params) contain unkeyable values — the
|
|
185
|
+
* caller must fall back to dynamic rendering.
|
|
186
|
+
*/
|
|
187
|
+
export function computeComponentCacheKey(
|
|
188
|
+
props: Record<string, unknown>,
|
|
189
|
+
params: Record<string, string | string[]>
|
|
190
|
+
): string | null {
|
|
191
|
+
let canonical: string;
|
|
192
|
+
try {
|
|
193
|
+
// One shared `seen` set: an object aliased between params and props is
|
|
194
|
+
// just as observable as one aliased within either.
|
|
195
|
+
const seen = new Set<object>();
|
|
196
|
+
// The params RECORD is framework-produced: coerceSegmentParams builds
|
|
197
|
+
// it with Object.create(null) as a prototype-pollution defense
|
|
198
|
+
// (design/13 checklist #36b), uniformly on every request — its
|
|
199
|
+
// prototype is an implementation detail, not user-observable input
|
|
200
|
+
// variation, so the record is normalized to a plain object before the
|
|
201
|
+
// walk (codex P1 on PR #835: rejecting it made every real request
|
|
202
|
+
// unkeyable). Nested VALUES still face the full allowlist, and user
|
|
203
|
+
// props stay strict — a null-prototype prop object remains unkeyable.
|
|
204
|
+
canonical =
|
|
205
|
+
'{"params":' +
|
|
206
|
+
canonicalize({ ...params }, seen) +
|
|
207
|
+
',"props":' +
|
|
208
|
+
canonicalize(props, seen) +
|
|
209
|
+
'}';
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return toUnkeyable(error);
|
|
212
|
+
}
|
|
213
|
+
// NOTE(hash choice — DECIDED, TIM-1119): 64 bits of truncated sha256 is
|
|
214
|
+
// the SHIPPED artifact format; the capture pass writes .flight files and
|
|
215
|
+
// manifest entries named by this hash, so changing the width is now a
|
|
216
|
+
// build-artifact format break. The width is adequate: entries are
|
|
217
|
+
// namespaced per component AND per deploy, so birthday odds need ~2^32
|
|
218
|
+
// entries under ONE component to matter, and design/13 #24 already
|
|
219
|
+
// accepts 64-bit non-crypto FNV-1a for data-cache keys on the same
|
|
220
|
+
// namespacing argument. Only revisit (with a format migration) if the
|
|
221
|
+
// phase-3 runtime tier ever stores attacker-influenced keys more
|
|
222
|
+
// aggressively.
|
|
223
|
+
return createHash('sha256').update(canonical).digest('hex').slice(0, 16);
|
|
224
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time capture state — the channel between the post-build capture
|
|
3
|
+
* pass (server/prebuilt-builder.ts) and the `__prebuilt` wrapper
|
|
4
|
+
* (server/prebuilt-runtime.ts).
|
|
5
|
+
*
|
|
6
|
+
* During capture, a wrapped component's subtree may render OTHER wrapped
|
|
7
|
+
* components. With no payload source registered (the capture pass runs
|
|
8
|
+
* before artifacts exist), those inner wrappers render live and their
|
|
9
|
+
* output is baked flat into the outer entry — the nesting clamp of
|
|
10
|
+
* design/45-cache-lifetimes.md §Nesting Semantics (TIM-1136). The capture
|
|
11
|
+
* pass is the designated detection point for that clamp, so the wrapper
|
|
12
|
+
* reports every render that happens while a capture is active:
|
|
13
|
+
*
|
|
14
|
+
* - **Cycle**: the inner wrapper IS the component being captured (directly
|
|
15
|
+
* or transitively — the transitive case still renders the outer's own
|
|
16
|
+
* wrapper, so the identity check catches both). Left alone this recurses
|
|
17
|
+
* until the render times out; instead the wrapper throws
|
|
18
|
+
* `PrebuiltCaptureCycleError`, failing that entry with an error naming
|
|
19
|
+
* the cycle while the build continues (doc 44 skip-and-report policy).
|
|
20
|
+
* - **Nesting**: any other wrapper render is recorded as an (outer, inner)
|
|
21
|
+
* pair. The builder warns after the entry completes when the inner
|
|
22
|
+
* declares a refresh channel the outer cannot honor (doc 45 clamp rule).
|
|
23
|
+
*
|
|
24
|
+
* Captures run strictly sequentially (one entry at a time), so a single
|
|
25
|
+
* module-level slot is sufficient — there is never a second active capture
|
|
26
|
+
* to confuse renders between. At request time `active` is null and
|
|
27
|
+
* `noteWrapperRender` is a single null-check.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import type { PrebuiltComponentOptions } from '../prebuilt-runtime.js';
|
|
31
|
+
|
|
32
|
+
/** A wrapped component that rendered inside an active capture. */
|
|
33
|
+
export interface NestedWrapperRender {
|
|
34
|
+
innerId: string;
|
|
35
|
+
options: PrebuiltComponentOptions;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Thrown by the wrapper when a capture renders its own component. */
|
|
39
|
+
export class PrebuiltCaptureCycleError extends Error {
|
|
40
|
+
constructor(componentId: string) {
|
|
41
|
+
super(
|
|
42
|
+
`[timber] cache.component "${componentId}" renders itself (directly or through ` +
|
|
43
|
+
`other components) — prerendering it would recurse forever. Remove the cycle ` +
|
|
44
|
+
`or drop { prerender: true } on this component.`
|
|
45
|
+
);
|
|
46
|
+
this.name = 'PrebuiltCaptureCycleError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let active: { outerId: string; nested: Map<string, PrebuiltComponentOptions> } | null = null;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Mark a capture as active. The builder calls this immediately before
|
|
54
|
+
* rendering an entry and MUST pair it with `endCapture()` in a finally.
|
|
55
|
+
*/
|
|
56
|
+
export function beginCapture(outerId: string): void {
|
|
57
|
+
active = { outerId, nested: new Map() };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* End the active capture and return the wrapped components that rendered
|
|
62
|
+
* inside it (first options object wins per inner ID).
|
|
63
|
+
*/
|
|
64
|
+
export function endCapture(): NestedWrapperRender[] {
|
|
65
|
+
const nested = active?.nested;
|
|
66
|
+
active = null;
|
|
67
|
+
if (!nested) return [];
|
|
68
|
+
return Array.from(nested, ([innerId, options]) => ({ innerId, options }));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Whether a build-time capture is currently rendering. */
|
|
72
|
+
export function isCaptureActive(): boolean {
|
|
73
|
+
return active !== null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Called by the `__prebuilt` wrapper on every render. No-op unless a
|
|
78
|
+
* capture is active. Throws `PrebuiltCaptureCycleError` when the wrapper
|
|
79
|
+
* belongs to the component currently being captured.
|
|
80
|
+
*/
|
|
81
|
+
export function noteWrapperRender(innerId: string, options: PrebuiltComponentOptions): void {
|
|
82
|
+
if (active === null) return;
|
|
83
|
+
if (innerId === active.outerId) {
|
|
84
|
+
throw new PrebuiltCaptureCycleError(innerId);
|
|
85
|
+
}
|
|
86
|
+
if (!active.nested.has(innerId)) {
|
|
87
|
+
active.nested.set(innerId, options);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest tag index — maps tags to seed cache keys for tombstone writes.
|
|
3
|
+
*
|
|
4
|
+
* When `revalidateTag(tag)` fires, the framework must write tombstones for
|
|
5
|
+
* every build-seed entry carrying that tag. The manifest records tags per
|
|
6
|
+
* entry; this module inverts the index at startup so the lookup is O(1) per
|
|
7
|
+
* tag instead of a full manifest scan per invalidation.
|
|
8
|
+
*
|
|
9
|
+
* The index is built lazily on first access and cached for the process
|
|
10
|
+
* lifetime — the manifest is immutable within a deploy.
|
|
11
|
+
*
|
|
12
|
+
* See design/45-cache-lifetimes.md §ISR mechanics.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { PrebuiltManifest } from './payload-source.js';
|
|
16
|
+
|
|
17
|
+
export interface SeedKeyInfo {
|
|
18
|
+
componentId: string;
|
|
19
|
+
cacheKey: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build the inverted tag → seed key index from the manifest.
|
|
24
|
+
*
|
|
25
|
+
* Returns a Map where each tag maps to the set of (componentId, cacheKey)
|
|
26
|
+
* pairs whose manifest entries carry that tag. Only entries with non-empty
|
|
27
|
+
* `tags` arrays participate.
|
|
28
|
+
*/
|
|
29
|
+
export function buildTagIndex(manifest: PrebuiltManifest): Map<string, SeedKeyInfo[]> {
|
|
30
|
+
const index = new Map<string, SeedKeyInfo[]>();
|
|
31
|
+
for (const [componentId, component] of Object.entries(manifest)) {
|
|
32
|
+
if (!component || typeof component !== 'object' || !('entries' in component)) continue;
|
|
33
|
+
for (const [cacheKey, entry] of Object.entries(component.entries)) {
|
|
34
|
+
if (!entry.tags || entry.tags.length === 0) continue;
|
|
35
|
+
for (const tag of entry.tags) {
|
|
36
|
+
let list = index.get(tag);
|
|
37
|
+
if (!list) {
|
|
38
|
+
list = [];
|
|
39
|
+
index.set(tag, list);
|
|
40
|
+
}
|
|
41
|
+
list.push({ componentId, cacheKey });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return index;
|
|
46
|
+
}
|