@timber-js/app 0.2.0-alpha.158 → 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-BA5tIpVu.js → actions-CBbP1hHO.js} +3 -3
- package/dist/_chunks/{actions-BA5tIpVu.js.map → actions-CBbP1hHO.js.map} +1 -1
- 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/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/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/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,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST-based export detection for route files.
|
|
3
|
+
*
|
|
4
|
+
* Uses Vite's `parseAst` (backed by oxc) to precisely detect named and
|
|
5
|
+
* default exports, replacing the regex-based approach that was fragile
|
|
6
|
+
* with TypeScript syntax (type exports, `as` aliases, comments, etc.).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { parseAst } from 'vite';
|
|
11
|
+
import { readFileCached } from './file-cache.js';
|
|
12
|
+
|
|
13
|
+
interface AstNode {
|
|
14
|
+
type: string;
|
|
15
|
+
exportKind?: string;
|
|
16
|
+
declaration?: AstNode;
|
|
17
|
+
specifiers?: Array<{
|
|
18
|
+
exported?: { name: string };
|
|
19
|
+
local?: { name: string };
|
|
20
|
+
exportKind?: string;
|
|
21
|
+
}>;
|
|
22
|
+
id?: { name: string };
|
|
23
|
+
declarations?: Array<{ id?: { name: string } }>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ProgramNode {
|
|
27
|
+
body: AstNode[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function tryParse(source: string): ProgramNode | null {
|
|
31
|
+
try {
|
|
32
|
+
return parseAst(source, { lang: 'tsx' }) as unknown as ProgramNode;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Collect all named export identifiers from a parsed program.
|
|
40
|
+
* Handles: `export function X`, `export const X`, `export { X }`,
|
|
41
|
+
* `export { X } from '...'`, `export { Y as X }`.
|
|
42
|
+
*/
|
|
43
|
+
function collectNamedExports(program: ProgramNode): Set<string> {
|
|
44
|
+
const names = new Set<string>();
|
|
45
|
+
for (const stmt of program.body) {
|
|
46
|
+
if (stmt.type !== 'ExportNamedDeclaration') continue;
|
|
47
|
+
// Skip `export type ...` declarations — they're erased at runtime
|
|
48
|
+
if (stmt.exportKind === 'type') continue;
|
|
49
|
+
|
|
50
|
+
if (stmt.declaration) {
|
|
51
|
+
if (stmt.declaration.id?.name) {
|
|
52
|
+
names.add(stmt.declaration.id.name);
|
|
53
|
+
}
|
|
54
|
+
if (stmt.declaration.declarations) {
|
|
55
|
+
for (const decl of stmt.declaration.declarations) {
|
|
56
|
+
if (decl.id?.name) names.add(decl.id.name);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (stmt.specifiers) {
|
|
62
|
+
for (const spec of stmt.specifiers) {
|
|
63
|
+
// Skip `export { type X }` — per-specifier type exports
|
|
64
|
+
if (spec.exportKind === 'type') continue;
|
|
65
|
+
if (spec.exported?.name && spec.exported.name !== 'default') {
|
|
66
|
+
names.add(spec.exported.name);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return names;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check whether a program has a default export.
|
|
76
|
+
* Handles: `export default ...`, `export { X as default }`,
|
|
77
|
+
* `export { default } from '...'`.
|
|
78
|
+
*/
|
|
79
|
+
function hasDefaultExport(program: ProgramNode): boolean {
|
|
80
|
+
for (const stmt of program.body) {
|
|
81
|
+
if (stmt.type === 'ExportDefaultDeclaration') return true;
|
|
82
|
+
if (stmt.type === 'ExportNamedDeclaration' && stmt.specifiers) {
|
|
83
|
+
for (const spec of stmt.specifiers) {
|
|
84
|
+
if (spec.exported?.name === 'default') return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check whether a program has any bare `export * from '...'` declarations.
|
|
93
|
+
* Excludes `export * as X from '...'` (namespace re-exports) since those
|
|
94
|
+
* create a namespace object, not individual top-level exports.
|
|
95
|
+
*/
|
|
96
|
+
function hasStarExport(program: ProgramNode): boolean {
|
|
97
|
+
return program.body.some(
|
|
98
|
+
(stmt) =>
|
|
99
|
+
stmt.type === 'ExportAllDeclaration' && !(stmt as any).exported && stmt.exportKind !== 'type'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check if a file exports a specific named export.
|
|
105
|
+
* Returns false if the file doesn't exist or can't be parsed.
|
|
106
|
+
*/
|
|
107
|
+
export function fileHasExport(filePath: string, exportName: string): boolean {
|
|
108
|
+
if (!existsSync(filePath)) return false;
|
|
109
|
+
try {
|
|
110
|
+
const source = readFileCached(filePath);
|
|
111
|
+
const program = tryParse(source);
|
|
112
|
+
if (!program) return false;
|
|
113
|
+
return collectNamedExports(program).has(exportName);
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if a file has any of the given named exports.
|
|
121
|
+
* Returns the set of matching export names.
|
|
122
|
+
*/
|
|
123
|
+
export function fileHasAnyExport(filePath: string, exportNames: readonly string[]): Set<string> {
|
|
124
|
+
const matches = new Set<string>();
|
|
125
|
+
try {
|
|
126
|
+
const source = readFileCached(filePath);
|
|
127
|
+
const program = tryParse(source);
|
|
128
|
+
if (!program) return matches;
|
|
129
|
+
const exports = collectNamedExports(program);
|
|
130
|
+
for (const name of exportNames) {
|
|
131
|
+
if (exports.has(name)) matches.add(name);
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
// Graceful fallback: return empty set
|
|
135
|
+
}
|
|
136
|
+
return matches;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if a file has a default export.
|
|
141
|
+
* Returns false if the file doesn't exist or can't be parsed.
|
|
142
|
+
*/
|
|
143
|
+
export function fileHasDefaultExport(filePath: string): boolean {
|
|
144
|
+
if (!existsSync(filePath)) return false;
|
|
145
|
+
try {
|
|
146
|
+
const source = readFileCached(filePath);
|
|
147
|
+
const program = tryParse(source);
|
|
148
|
+
if (!program) return false;
|
|
149
|
+
return hasDefaultExport(program);
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check if a file has any `export * from '...'` declarations.
|
|
157
|
+
* Returns false if the file doesn't exist or can't be parsed.
|
|
158
|
+
*/
|
|
159
|
+
export function fileHasStarExport(filePath: string): boolean {
|
|
160
|
+
if (!existsSync(filePath)) return false;
|
|
161
|
+
try {
|
|
162
|
+
const source = readFileCached(filePath);
|
|
163
|
+
const program = tryParse(source);
|
|
164
|
+
if (!program) return false;
|
|
165
|
+
return hasStarExport(program);
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Parsed value of `export const prerender` from a route file.
|
|
173
|
+
*
|
|
174
|
+
* - `true` / `false` — literal boolean
|
|
175
|
+
* - `{ ttl?, tags? }` — ISR options object (prerender: true is implied)
|
|
176
|
+
* - `undefined` — no `export const prerender` found
|
|
177
|
+
*
|
|
178
|
+
* See design/45-cache-lifetimes.md §"Route-Level Static".
|
|
179
|
+
*/
|
|
180
|
+
export type PrerenderExportValue = boolean | { ttl?: number; tags?: string[] };
|
|
181
|
+
|
|
182
|
+
function extractLiteralValue(node: AstNode): unknown {
|
|
183
|
+
if (!node) return undefined;
|
|
184
|
+
const n = node as any;
|
|
185
|
+
if (n.type === 'Literal') return n.value;
|
|
186
|
+
if (n.type === 'UnaryExpression' && n.operator === '-' && n.argument?.type === 'Literal') {
|
|
187
|
+
return -(n.argument.value as number);
|
|
188
|
+
}
|
|
189
|
+
if (n.type === 'ArrayExpression') {
|
|
190
|
+
const elements = n.elements as AstNode[] | undefined;
|
|
191
|
+
if (!elements) return undefined;
|
|
192
|
+
const result: unknown[] = [];
|
|
193
|
+
for (const el of elements) {
|
|
194
|
+
const v = extractLiteralValue(el);
|
|
195
|
+
if (v === undefined) return undefined;
|
|
196
|
+
result.push(v);
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function extractObjectLiteral(node: AstNode): Record<string, unknown> | undefined {
|
|
204
|
+
if ((node as any).type !== 'ObjectExpression') return undefined;
|
|
205
|
+
const props = (node as any).properties as AstNode[] | undefined;
|
|
206
|
+
if (!props) return undefined;
|
|
207
|
+
const result: Record<string, unknown> = {};
|
|
208
|
+
for (const prop of props) {
|
|
209
|
+
const p = prop as any;
|
|
210
|
+
if (p.type === 'SpreadElement') return undefined;
|
|
211
|
+
if (p.type !== 'Property' || p.computed) return undefined;
|
|
212
|
+
const key =
|
|
213
|
+
p.key?.type === 'Identifier'
|
|
214
|
+
? p.key.name
|
|
215
|
+
: p.key?.type === 'Literal'
|
|
216
|
+
? String(p.key.value)
|
|
217
|
+
: undefined;
|
|
218
|
+
if (!key) return undefined;
|
|
219
|
+
const value = extractLiteralValue(p.value);
|
|
220
|
+
if (value === undefined) return undefined;
|
|
221
|
+
result[key] = value;
|
|
222
|
+
}
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Extract the value of `export const prerender` from a file.
|
|
228
|
+
* Returns undefined if the file doesn't export `prerender` or can't be parsed.
|
|
229
|
+
*
|
|
230
|
+
* Supports:
|
|
231
|
+
* export const prerender = true;
|
|
232
|
+
* export const prerender = false;
|
|
233
|
+
* export const prerender = { ttl: 3600, tags: ['docs'] };
|
|
234
|
+
*/
|
|
235
|
+
export function getPrerenderExport(filePath: string): PrerenderExportValue | undefined {
|
|
236
|
+
if (!existsSync(filePath)) return undefined;
|
|
237
|
+
try {
|
|
238
|
+
const source = readFileCached(filePath);
|
|
239
|
+
const program = tryParse(source);
|
|
240
|
+
if (!program) return undefined;
|
|
241
|
+
|
|
242
|
+
for (const stmt of program.body) {
|
|
243
|
+
if (stmt.type !== 'ExportNamedDeclaration') continue;
|
|
244
|
+
if (stmt.exportKind === 'type') continue;
|
|
245
|
+
if (!stmt.declaration) continue;
|
|
246
|
+
const decl = stmt.declaration;
|
|
247
|
+
if (decl.type !== 'VariableDeclaration') continue;
|
|
248
|
+
const declarations = decl.declarations as
|
|
249
|
+
| Array<{
|
|
250
|
+
id?: { name?: string };
|
|
251
|
+
init?: AstNode;
|
|
252
|
+
}>
|
|
253
|
+
| undefined;
|
|
254
|
+
if (!declarations) continue;
|
|
255
|
+
for (const d of declarations) {
|
|
256
|
+
if (d.id?.name !== 'prerender' || !d.init) continue;
|
|
257
|
+
const init = d.init as any;
|
|
258
|
+
if (init.type === 'Literal' && typeof init.value === 'boolean') {
|
|
259
|
+
return init.value;
|
|
260
|
+
}
|
|
261
|
+
if (init.type === 'ObjectExpression') {
|
|
262
|
+
const obj = extractObjectLiteral(init);
|
|
263
|
+
if (!obj) return undefined;
|
|
264
|
+
const result: { ttl?: number; tags?: string[] } = {};
|
|
265
|
+
if ('ttl' in obj && typeof obj.ttl === 'number') result.ttl = obj.ttl;
|
|
266
|
+
if (
|
|
267
|
+
'tags' in obj &&
|
|
268
|
+
Array.isArray(obj.tags) &&
|
|
269
|
+
obj.tags.every((t: unknown) => typeof t === 'string')
|
|
270
|
+
) {
|
|
271
|
+
result.tags = obj.tags as string[];
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return undefined;
|
|
279
|
+
} catch {
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Check if a file starts with a specific directive (e.g. "use client").
|
|
286
|
+
* Directives are string literal expression statements at the top of the file.
|
|
287
|
+
*/
|
|
288
|
+
export function fileHasDirective(filePath: string, directive: string): boolean {
|
|
289
|
+
if (!existsSync(filePath)) return false;
|
|
290
|
+
try {
|
|
291
|
+
const source = readFileCached(filePath);
|
|
292
|
+
const program = tryParse(source);
|
|
293
|
+
if (!program) return false;
|
|
294
|
+
for (const stmt of program.body) {
|
|
295
|
+
if (stmt.type !== 'ExpressionStatement') break;
|
|
296
|
+
const expr = (stmt as any).expression;
|
|
297
|
+
if (expr?.type !== 'Literal' || typeof expr.value !== 'string') break;
|
|
298
|
+
if (expr.value === directive) return true;
|
|
299
|
+
}
|
|
300
|
+
return false;
|
|
301
|
+
} catch {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
import { existsSync } from 'node:fs';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import { collectInterceptionRewrites } from './interception.js';
|
|
14
|
+
import { fileHasExport } from './export-detect.js';
|
|
14
15
|
import type { RouteTree, SegmentNode, RouteFile } from './types.js';
|
|
15
|
-
import { readFileCached } from './file-cache.js';
|
|
16
16
|
|
|
17
17
|
// ── Single-file conventions ─────────────────────────────────────────────
|
|
18
18
|
// Each entry maps to a node property that holds a single RouteFile.
|
|
@@ -236,16 +236,7 @@ export function generateSearchParamsRegistryModule(tree: RouteTree): string {
|
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
function fileHasSearchParamsExport(filePath: string): boolean {
|
|
239
|
-
|
|
240
|
-
if (!existsSync(filePath)) return false;
|
|
241
|
-
const content = readFileCached(filePath);
|
|
242
|
-
return (
|
|
243
|
-
/export\s+(const|let|var)\s+searchParams\b/.test(content) ||
|
|
244
|
-
/export\s*\{[^}]*\bsearchParams\b[^}]*\}/.test(content)
|
|
245
|
-
);
|
|
246
|
-
} catch {
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
239
|
+
return fileHasExport(filePath, 'searchParams');
|
|
249
240
|
}
|
|
250
241
|
|
|
251
242
|
// ── Schema module ───────────────────────────────────────────────────────
|
package/src/rsc-runtime/rsc.ts
CHANGED
|
@@ -18,4 +18,11 @@ export {
|
|
|
18
18
|
decodeAction,
|
|
19
19
|
encryptActionBoundArgs,
|
|
20
20
|
decryptActionBoundArgs,
|
|
21
|
+
// Flight client running INSIDE the RSC environment — decodes a cached
|
|
22
|
+
// flight payload back into React elements. Client references revive as
|
|
23
|
+
// re-serializable reference proxies via plugin-rsc's server-decode
|
|
24
|
+
// manifest (registerClientReference under the hood). Used by the
|
|
25
|
+
// prebuilt component runtime (TIM-1140); same mechanism as Next.js
|
|
26
|
+
// "use cache" (use-cache-wrapper.ts).
|
|
27
|
+
createFromReadableStream,
|
|
21
28
|
} from '@vitejs/plugin-rsc/rsc';
|
|
@@ -35,7 +35,6 @@ import {
|
|
|
35
35
|
} from './sensitive-fields.js';
|
|
36
36
|
import type { FormFlashData } from './form-flash.js';
|
|
37
37
|
import { checkVersionSkew, applyReloadHeaders } from './version-skew.js';
|
|
38
|
-
import { encodeHeadHeader, HEAD_HEADER } from '../shared/head-header.js';
|
|
39
38
|
import { logActionError } from './logger.js';
|
|
40
39
|
|
|
41
40
|
// ─── Types ────────────────────────────────────────────────────────────────
|
|
@@ -300,17 +299,9 @@ async function handleRscAction(
|
|
|
300
299
|
_tree: result.revalidation.element,
|
|
301
300
|
};
|
|
302
301
|
headers['X-Timber-Revalidation'] = '1';
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
// `new Response(...)` throw — a 500 after the action's mutation already
|
|
307
|
-
// committed, dropping Set-Cookie too. See TIM-1058.
|
|
308
|
-
if (result.revalidation.headElements.length > 0) {
|
|
309
|
-
const encodedHead = encodeHeadHeader(result.revalidation.headElements);
|
|
310
|
-
if (encodedHead !== null) {
|
|
311
|
-
headers[HEAD_HEADER] = encodedHead;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
302
|
+
// Metadata (<title>/<meta>/<link>) now rides the revalidation element
|
|
303
|
+
// tree as React elements — React 19 Float manages them in document.head
|
|
304
|
+
// during client rendering. No X-Timber-Head header needed. See TIM-1151.
|
|
314
305
|
} else {
|
|
315
306
|
payload = result.actionResult;
|
|
316
307
|
}
|
|
@@ -347,19 +338,6 @@ async function handleFormAction(
|
|
|
347
338
|
return new Response(null, { status: fieldResult.status });
|
|
348
339
|
}
|
|
349
340
|
|
|
350
|
-
// Check if this is actually a React server action form.
|
|
351
|
-
// If there's no $ACTION_REF_* or $ACTION_KEY, it's a regular form POST
|
|
352
|
-
// that should be handled by the route's route handler, not here.
|
|
353
|
-
// React uses `$ACTION_REF_` + identifierPrefix — since we don't set one,
|
|
354
|
-
// the suffix is empty. Check for any key starting with $ACTION_REF_ or $ACTION_ID_.
|
|
355
|
-
const allKeys = [...formData.keys()];
|
|
356
|
-
const hasActionField = allKeys.some(
|
|
357
|
-
(k) => k.startsWith('$ACTION_REF_') || k.startsWith('$ACTION_ID_')
|
|
358
|
-
);
|
|
359
|
-
if (!hasActionField && !formData.has('$ACTION_KEY')) {
|
|
360
|
-
return null;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
341
|
// Capture submitted values for re-render on validation failure.
|
|
364
342
|
// Parse before decodeAction consumes the FormData, then strip sensitive
|
|
365
343
|
// fields (passwords, tokens, CVV, etc.) so they are never rendered back
|
|
@@ -371,8 +349,14 @@ async function handleFormAction(
|
|
|
371
349
|
const submittedValues = stripSensitiveFields(parseFormData(formData), sensitivePredicate);
|
|
372
350
|
|
|
373
351
|
// decodeAction resolves the action function from the form data's hidden fields.
|
|
374
|
-
//
|
|
375
|
-
|
|
352
|
+
// Returns null when no $ACTION_REF_/$ACTION_ID_ fields are present — meaning
|
|
353
|
+
// this is a regular form POST, not a React server action.
|
|
354
|
+
const actionFn = (await decodeAction(formData)) as
|
|
355
|
+
| ((...args: unknown[]) => Promise<unknown>)
|
|
356
|
+
| null;
|
|
357
|
+
if (!actionFn) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
376
360
|
|
|
377
361
|
// Execute the action — no additional args needed (form data is already bound).
|
|
378
362
|
// Errors are caught to prevent stack traces from leaking in the response.
|
package/src/server/actions.ts
CHANGED
|
@@ -18,16 +18,12 @@ import { cache } from '../cache/cache-api';
|
|
|
18
18
|
import { DenySignal, RedirectSignal } from './primitives';
|
|
19
19
|
import { withSpan } from './tracing';
|
|
20
20
|
import { revalidationAls, type RevalidationState } from './als-registry.js';
|
|
21
|
-
import type { HeadElementPayload } from '../shared/head-header.js';
|
|
22
|
-
|
|
23
21
|
// ─── Types ───────────────────────────────────────────────────────────────
|
|
24
22
|
|
|
25
23
|
/** Result of rendering a revalidation — element tree before RSC serialization. */
|
|
26
24
|
export interface RevalidationResult {
|
|
27
25
|
/** React element tree (pre-serialization — passed to renderToReadableStream). */
|
|
28
26
|
element: unknown;
|
|
29
|
-
/** Resolved head elements for metadata (forwarded via X-Timber-Head). */
|
|
30
|
-
headElements: HeadElementPayload[];
|
|
31
27
|
}
|
|
32
28
|
|
|
33
29
|
/** Renderer function that builds a React element tree for a given path. */
|
|
@@ -168,6 +164,9 @@ export async function executeAction(
|
|
|
168
164
|
// rsc-entry when timber.config.ts provides a cacheHandler; otherwise falls
|
|
169
165
|
// back to in-memory LRU (TIM-599).
|
|
170
166
|
if (state.tags.length > 0) {
|
|
167
|
+
// cache.invalidate handles both data-cache clearing AND component-seed
|
|
168
|
+
// tombstone writes (design/45 §ISR mechanics) — no separate tombstone
|
|
169
|
+
// step needed here.
|
|
171
170
|
await Promise.all(state.tags.map((tag) => cache.invalidate({ tag })));
|
|
172
171
|
}
|
|
173
172
|
|
|
@@ -150,6 +150,13 @@ export interface TraceStore {
|
|
|
150
150
|
traceId: string;
|
|
151
151
|
/** OTEL span ID if available, undefined otherwise. */
|
|
152
152
|
spanId?: string;
|
|
153
|
+
/**
|
|
154
|
+
* Innermost active native platform span (e.g. Cloudflare's `Span` from
|
|
155
|
+
* tracing.enterSpan()). Tracked so setSpanAttribute() can set attributes
|
|
156
|
+
* after span creation — the platform API has no getActiveSpan() equivalent.
|
|
157
|
+
* Saved/restored by withSpan() around each callback. See TIM-1135.
|
|
158
|
+
*/
|
|
159
|
+
platformSpan?: import('./tracing.js').PlatformSpan;
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
/** @internal — import via tracing.ts public API */
|
|
@@ -9,8 +9,14 @@
|
|
|
9
9
|
* live in the API surface — `cookie-context.ts` invokes them at every
|
|
10
10
|
* jar entry point so the smuggling-primitive invariant from TIM-868
|
|
11
11
|
* is enforced regardless of which path produced the bytes).
|
|
12
|
+
*
|
|
13
|
+
* Delegates to the `cookie` package (RFC 6265, dependency-free,
|
|
14
|
+
* browser-safe) for wire codecs. Adapts at the boundary to preserve
|
|
15
|
+
* timber's Map-based API and never-throw contract.
|
|
12
16
|
*/
|
|
13
17
|
|
|
18
|
+
import { parseCookie, parseSetCookie as upstreamParseSetCookie, stringifySetCookie } from 'cookie';
|
|
19
|
+
|
|
14
20
|
import type { CookieEntry } from './als-registry.js';
|
|
15
21
|
import type { CookieOptions } from './cookie-context.js';
|
|
16
22
|
|
|
@@ -28,14 +34,13 @@ export function parseCookieHeader(header: string): Map<string, string> {
|
|
|
28
34
|
const map = new Map<string, string>();
|
|
29
35
|
if (!header) return map;
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
37
|
+
// cookie.parseCookie returns a plain object with first-wins semantics
|
|
38
|
+
// and uses safeDecodeURIComponent by default (try/catch around
|
|
39
|
+
// decodeURIComponent, falls back to raw). This matches our contract.
|
|
40
|
+
const parsed = parseCookie(header);
|
|
41
|
+
for (const name in parsed) {
|
|
42
|
+
const value = parsed[name];
|
|
43
|
+
if (value !== undefined) map.set(name, value);
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
return map;
|
|
@@ -48,6 +53,9 @@ export function parseCookieHeader(header: string): Map<string, string> {
|
|
|
48
53
|
* Used by both `parseCookieHeader` (incoming Cookie: header) and the
|
|
49
54
|
* `setRaw` forwarding path (outgoing Set-Cookie from upstream services).
|
|
50
55
|
* Total — never throws.
|
|
56
|
+
*
|
|
57
|
+
* cookie's internal decode has identical semantics but is not exported,
|
|
58
|
+
* so we keep this standalone helper.
|
|
51
59
|
*/
|
|
52
60
|
export function safeDecodeCookieValue(raw: string): string {
|
|
53
61
|
try {
|
|
@@ -57,8 +65,43 @@ export function safeDecodeCookieValue(raw: string): string {
|
|
|
57
65
|
}
|
|
58
66
|
}
|
|
59
67
|
|
|
60
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Serialize a CookieEntry into a Set-Cookie header value.
|
|
70
|
+
*
|
|
71
|
+
* Total — never throws. If the cookie package's serializer rejects
|
|
72
|
+
* the input (e.g. non-integer maxAge, forbidden chars in name/value),
|
|
73
|
+
* falls back to a minimal hand-rolled serialization so the response
|
|
74
|
+
* path is never interrupted by a codec error.
|
|
75
|
+
*/
|
|
61
76
|
export function serializeCookieEntry(entry: CookieEntry): string {
|
|
77
|
+
try {
|
|
78
|
+
return stringifySetCookie(
|
|
79
|
+
{
|
|
80
|
+
name: entry.name,
|
|
81
|
+
value: entry.value,
|
|
82
|
+
domain: entry.options.domain,
|
|
83
|
+
path: entry.options.path,
|
|
84
|
+
expires: entry.options.expires,
|
|
85
|
+
maxAge: entry.options.maxAge,
|
|
86
|
+
httpOnly: entry.options.httpOnly,
|
|
87
|
+
secure: entry.options.secure,
|
|
88
|
+
sameSite: entry.options.sameSite,
|
|
89
|
+
partitioned: entry.options.partitioned,
|
|
90
|
+
},
|
|
91
|
+
// timber pre-encodes values at the API surface (set() calls
|
|
92
|
+
// encodeURIComponent), so pass identity to avoid double-encoding.
|
|
93
|
+
{ encode: (v: string) => v }
|
|
94
|
+
);
|
|
95
|
+
} catch {
|
|
96
|
+
// Fallback: hand-roll a minimal Set-Cookie so we never drop the
|
|
97
|
+
// header entirely. The entry-point validators in cookie-context.ts
|
|
98
|
+
// already reject truly dangerous input — this path only fires for
|
|
99
|
+
// edge-case formatting the upstream serializer is strict about.
|
|
100
|
+
return serializeCookieEntryFallback(entry);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function serializeCookieEntryFallback(entry: CookieEntry): string {
|
|
62
105
|
const parts = [`${entry.name}=${entry.value}`];
|
|
63
106
|
const opts = entry.options;
|
|
64
107
|
|
|
@@ -88,60 +131,33 @@ export function serializeCookieEntry(entry: CookieEntry): string {
|
|
|
88
131
|
export function parseSetCookie(
|
|
89
132
|
header: string
|
|
90
133
|
): { name: string; value: string; options: CookieOptions } | null {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
134
|
+
const parsed = upstreamParseSetCookie(header, {
|
|
135
|
+
// Don't decode — setRaw expects the wire-form value so it can
|
|
136
|
+
// re-emit verbatim and validate against cookie-octet.
|
|
137
|
+
decode: (v: string) => v,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// cookie.parseSetCookie always returns a SetCookie object, but
|
|
141
|
+
// with name/value as empty strings when the header has no `=`.
|
|
142
|
+
if (!parsed.name) return null;
|
|
95
143
|
|
|
96
|
-
const name = nameValue.slice(0, eqIdx).trim();
|
|
97
|
-
const value = nameValue.slice(eqIdx + 1).trim();
|
|
98
144
|
const options: CookieOptions = {};
|
|
99
145
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
case 'domain':
|
|
111
|
-
options.domain = val;
|
|
112
|
-
break;
|
|
113
|
-
case 'max-age': {
|
|
114
|
-
// RFC 6265 §5.2.2: a non-numeric Max-Age is ignored, not fatal.
|
|
115
|
-
// Dropping NaN/Infinity here keeps this parser total and keeps
|
|
116
|
-
// `Max-Age=NaN` out of the serialized Set-Cookie header.
|
|
117
|
-
const maxAge = Number(val);
|
|
118
|
-
if (Number.isFinite(maxAge)) options.maxAge = maxAge;
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
case 'expires':
|
|
122
|
-
options.expires = new Date(val);
|
|
123
|
-
break;
|
|
124
|
-
case 'samesite': {
|
|
125
|
-
// Unknown SameSite values are ignored (browsers treat them as
|
|
126
|
-
// unspecified) so forwarding a quirky upstream header via
|
|
127
|
-
// setFromHeaders never trips the entry-point validator.
|
|
128
|
-
const sameSite = val.toLowerCase();
|
|
129
|
-
if (sameSite === 'strict' || sameSite === 'lax' || sameSite === 'none') {
|
|
130
|
-
options.sameSite = sameSite;
|
|
131
|
-
}
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
case 'secure':
|
|
135
|
-
options.secure = true;
|
|
136
|
-
break;
|
|
137
|
-
case 'httponly':
|
|
138
|
-
options.httpOnly = true;
|
|
139
|
-
break;
|
|
140
|
-
case 'partitioned':
|
|
141
|
-
options.partitioned = true;
|
|
142
|
-
break;
|
|
146
|
+
if (parsed.path !== undefined) options.path = parsed.path || '/';
|
|
147
|
+
if (parsed.domain !== undefined) options.domain = parsed.domain;
|
|
148
|
+
if (parsed.maxAge !== undefined && Number.isFinite(parsed.maxAge)) {
|
|
149
|
+
options.maxAge = parsed.maxAge;
|
|
150
|
+
}
|
|
151
|
+
if (parsed.expires !== undefined) options.expires = parsed.expires;
|
|
152
|
+
if (parsed.sameSite !== undefined && parsed.sameSite !== true) {
|
|
153
|
+
const sameSite = (parsed.sameSite as string).toLowerCase();
|
|
154
|
+
if (sameSite === 'strict' || sameSite === 'lax' || sameSite === 'none') {
|
|
155
|
+
options.sameSite = sameSite;
|
|
143
156
|
}
|
|
144
157
|
}
|
|
158
|
+
if (parsed.secure) options.secure = true;
|
|
159
|
+
if (parsed.httpOnly) options.httpOnly = true;
|
|
160
|
+
if (parsed.partitioned) options.partitioned = true;
|
|
145
161
|
|
|
146
|
-
return { name, value, options };
|
|
162
|
+
return { name: parsed.name, value: parsed.value ?? '', options };
|
|
147
163
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* See design/10-error-handling.md §"Status-Code Files"
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { createElement } from 'react';
|
|
18
|
+
import { createElement, Fragment } from 'react';
|
|
19
19
|
import { renderToReadableStream } from '../rsc-runtime/rsc.js';
|
|
20
20
|
|
|
21
21
|
import { DenySignal } from './primitives.js';
|
|
@@ -25,8 +25,8 @@ import { logRenderError } from './logger.js';
|
|
|
25
25
|
import { loadModule } from './safe-load.js';
|
|
26
26
|
import { isMdxFilePath } from './utils/mdx-file.js';
|
|
27
27
|
import { isDebug } from './debug.js';
|
|
28
|
-
import { escapeHtml } from './utils/escape-html.js';
|
|
29
28
|
import { resolveMetadata, renderMetadataToElements } from './metadata.js';
|
|
29
|
+
import { headElementsToReact } from './route-element-builder.js';
|
|
30
30
|
import { resolveStatusFile } from './status-code-resolver.js';
|
|
31
31
|
import type { ManifestSegmentNode } from './route-matcher.js';
|
|
32
32
|
import type { RouteMatch } from './pipeline.js';
|
|
@@ -166,21 +166,13 @@ export async function renderDenyPage(
|
|
|
166
166
|
element = h(component, null, element);
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
//
|
|
170
|
-
|
|
169
|
+
// Embed metadata as React elements in the tree — React 19 Float hoists
|
|
170
|
+
// them into <head> during SSR. See TIM-1151.
|
|
171
171
|
if (mod.metadata) {
|
|
172
172
|
const resolvedMeta = resolveMetadata([{ metadata: mod.metadata as Metadata, isPage: true }]);
|
|
173
173
|
const headElements = renderMetadataToElements(resolvedMeta);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
headHtml += `<title>${escapeHtml(el.content)}</title>`;
|
|
177
|
-
} else if (el.attrs) {
|
|
178
|
-
const attrs = Object.entries(el.attrs)
|
|
179
|
-
.map(([k, v]) => `${k}="${escapeHtml(v)}"`)
|
|
180
|
-
.join(' ');
|
|
181
|
-
headHtml += `<${el.tag} ${attrs}>`;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
174
|
+
const metaEl = headElementsToReact(headElements);
|
|
175
|
+
element = h(Fragment, null, metaEl, element);
|
|
184
176
|
}
|
|
185
177
|
|
|
186
178
|
// Render the error page to a fresh RSC Flight stream.
|
|
@@ -200,7 +192,7 @@ export async function renderDenyPage(
|
|
|
200
192
|
searchParams: Object.fromEntries(new URL(req.url).searchParams),
|
|
201
193
|
statusCode: deny.status,
|
|
202
194
|
responseHeaders,
|
|
203
|
-
headHtml:
|
|
195
|
+
headHtml: flightInitScript(),
|
|
204
196
|
bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
|
|
205
197
|
rscStream: inlineStream,
|
|
206
198
|
signal: req.signal,
|