@timber-js/app 0.2.0-alpha.145 → 0.2.0-alpha.147
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-DzFMnUAR.js → actions-OIZ8siQu.js} +28 -43
- package/dist/_chunks/actions-OIZ8siQu.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-DH-M8Poy.js → cli-schema-sync-CBiMEtqa.js} +1 -1
- package/dist/_chunks/{cli-schema-sync-D2IpXicg.js → cli-schema-sync-CQaxupHv.js} +47 -54
- package/dist/_chunks/cli-schema-sync-CQaxupHv.js.map +1 -0
- package/dist/_chunks/{handler-store-DjISKYEB.js → handler-store-BVDgyTh7.js} +26 -2
- package/dist/_chunks/handler-store-BVDgyTh7.js.map +1 -0
- package/dist/_chunks/{href-validation-EZktELp8.js → href-validation-DyZIzfMJ.js} +18 -2
- package/dist/_chunks/{href-validation-EZktELp8.js.map → href-validation-DyZIzfMJ.js.map} +1 -1
- package/dist/_chunks/{navigation-root-DLc4s4ZC.js → navigation-root-bUDZLw74.js} +23 -2
- package/dist/_chunks/navigation-root-bUDZLw74.js.map +1 -0
- package/dist/_chunks/tracing-M_zKubhk.js.map +1 -1
- package/dist/_chunks/{walkers-7FBPM-hX.js → walkers-Ck4Y45XN.js} +49 -8
- package/dist/_chunks/walkers-Ck4Y45XN.js.map +1 -0
- package/dist/adapters/cloudflare.d.ts +1 -15
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +10 -28
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +18 -5
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/index.js +39 -12
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/redis-handler.d.ts +72 -8
- package/dist/cache/redis-handler.d.ts.map +1 -1
- package/dist/cache/stable-stringify.d.ts +9 -0
- package/dist/cache/stable-stringify.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts +3 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/browser-entry/scroll.d.ts +9 -0
- package/dist/client/browser-entry/scroll.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +24 -10
- package/dist/client/error-boundary.js.map +1 -1
- package/dist/client/error-reconstituter.d.ts +8 -1
- package/dist/client/error-reconstituter.d.ts.map +1 -1
- package/dist/client/index.js +3 -3
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +99 -24
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-api.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +13 -0
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/router.d.ts +7 -0
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/dev-tools/404-page.d.ts +6 -2
- package/dist/dev-tools/404-page.d.ts.map +1 -1
- package/dist/dev-tools/dev-page-shell.d.ts +77 -0
- package/dist/dev-tools/dev-page-shell.d.ts.map +1 -0
- package/dist/dev-tools/error-page.d.ts +4 -30
- package/dist/dev-tools/error-page.d.ts.map +1 -1
- package/dist/dev-tools/index.d.ts +2 -1
- package/dist/dev-tools/index.d.ts.map +1 -1
- package/dist/dev-tools/instrumentation.d.ts +11 -1
- package/dist/dev-tools/instrumentation.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +432 -255
- package/dist/index.js.map +1 -1
- package/dist/plugins/cache.d.ts +43 -9
- package/dist/plugins/cache.d.ts.map +1 -1
- package/dist/plugins/cloudflare-wasm.d.ts +4 -1
- package/dist/plugins/cloudflare-wasm.d.ts.map +1 -1
- package/dist/plugins/dev-server.d.ts +16 -0
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts +23 -0
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/routing/codegen-types.d.ts +4 -0
- package/dist/routing/codegen-types.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/actions.d.ts +3 -2
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/cookie-context.d.ts.map +1 -1
- package/dist/server/cookie-parsing.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +27 -2
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/fallback-error.d.ts +2 -2
- package/dist/server/fallback-error.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/internal.js +227 -179
- package/dist/server/internal.js.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +24 -6
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +11 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +1 -11
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts +6 -4
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/status-code-resolver.d.ts +6 -0
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/tracing.d.ts +2 -1
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/utils/mdx-file.d.ts +15 -0
- package/dist/server/utils/mdx-file.d.ts.map +1 -0
- package/dist/shared/head-header.d.ts +54 -0
- package/dist/shared/head-header.d.ts.map +1 -0
- package/dist/shared/href-validation.d.ts +11 -0
- package/dist/shared/href-validation.d.ts.map +1 -1
- package/dist/shared/redirect-type.d.ts +17 -0
- package/dist/shared/redirect-type.d.ts.map +1 -0
- package/dist/shims/headers.d.ts +52 -1
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/navigation-client.d.ts +33 -11
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/package.json +11 -10
- package/src/adapters/cloudflare.ts +13 -47
- package/src/adapters/compress-module.ts +9 -3
- package/src/adapters/nitro.ts +9 -2
- package/src/cache/redis-handler.ts +77 -13
- package/src/cache/stable-stringify.ts +65 -2
- package/src/cache/timber-cache.ts +25 -13
- package/src/cli.ts +0 -0
- package/src/client/browser-entry/action-dispatch.ts +7 -2
- package/src/client/browser-entry/router-init.ts +2 -1
- package/src/client/browser-entry/scroll.ts +23 -0
- package/src/client/error-boundary.tsx +33 -5
- package/src/client/error-reconstituter.tsx +15 -1
- package/src/client/link.tsx +4 -1
- package/src/client/navigation-api.ts +5 -1
- package/src/client/navigation-root.tsx +16 -0
- package/src/client/router.ts +107 -29
- package/src/client/rsc-fetch.ts +6 -6
- package/src/dev-tools/404-page.ts +22 -123
- package/src/dev-tools/dev-page-shell.ts +256 -0
- package/src/dev-tools/error-page.ts +27 -203
- package/src/dev-tools/index.ts +7 -1
- package/src/dev-tools/instrumentation.ts +39 -2
- package/src/index.ts +1 -0
- package/src/plugins/cache.ts +79 -21
- package/src/plugins/cloudflare-wasm.ts +48 -10
- package/src/plugins/dev-server.ts +32 -8
- package/src/plugins/server-bundle.ts +25 -9
- package/src/plugins/shims.ts +94 -52
- package/src/routing/codegen-types.ts +4 -0
- package/src/routing/codegen.ts +63 -6
- package/src/routing/scanner.ts +80 -63
- package/src/server/action-handler.ts +10 -2
- package/src/server/actions.ts +3 -2
- package/src/server/compress.ts +11 -2
- package/src/server/cookie-context.ts +11 -0
- package/src/server/cookie-parsing.ts +16 -4
- package/src/server/deny-boundary.ts +106 -13
- package/src/server/deny-renderer.ts +40 -10
- package/src/server/error-boundary-wrapper.ts +1 -14
- package/src/server/fallback-error.ts +9 -4
- package/src/server/node-stream-transforms.ts +61 -24
- package/src/server/pipeline.ts +12 -0
- package/src/server/primitives.ts +10 -31
- package/src/server/route-matcher.ts +22 -6
- package/src/server/rsc-entry/error-renderer.ts +1 -12
- package/src/server/rsc-entry/index.ts +7 -2
- package/src/server/rsc-entry/rsc-payload.ts +4 -3
- package/src/server/slot-resolver.ts +27 -14
- package/src/server/ssr-entry.ts +17 -28
- package/src/server/status-code-resolver.ts +3 -2
- package/src/server/tracing.ts +17 -1
- package/src/server/tree-match.ts +15 -5
- package/src/server/utils/mdx-file.ts +22 -0
- package/src/shared/head-header.ts +69 -0
- package/src/shared/href-validation.ts +24 -0
- package/src/shared/redirect-type.ts +17 -0
- package/src/shims/headers.ts +119 -2
- package/src/shims/navigation-client.ts +48 -27
- package/LICENSE +0 -8
- package/dist/_chunks/actions-DzFMnUAR.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-D2IpXicg.js.map +0 -1
- package/dist/_chunks/handler-store-DjISKYEB.js.map +0 -1
- package/dist/_chunks/navigation-root-DLc4s4ZC.js.map +0 -1
- package/dist/_chunks/walkers-7FBPM-hX.js.map +0 -1
package/src/plugins/cache.ts
CHANGED
|
@@ -10,9 +10,36 @@
|
|
|
10
10
|
* The plugin transforms:
|
|
11
11
|
* cache(fn, opts) → cache(fn, opts, "stable-id")
|
|
12
12
|
*
|
|
13
|
-
* where stable-id =
|
|
13
|
+
* where stable-id =
|
|
14
|
+
* fnv1a(relPath + ":cache:" + fnv1a(stmtText) + ":" + fnv1a(callSpanText) + ":" + occurrence)
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
+
* stmtText is the source text of the enclosing *top-level statement* and
|
|
17
|
+
* callSpanText is the source text of the cache(...) call expression itself.
|
|
18
|
+
*
|
|
19
|
+
* The ID is derived from the *content* of the call, not its ordinal position
|
|
20
|
+
* in the file. A position-based index (the original TIM-997 scheme) was stable
|
|
21
|
+
* within one build but shifted across builds: inserting, deleting, or
|
|
22
|
+
* reordering a cache() call changed the index of every call below it, so with
|
|
23
|
+
* a persistent shared CacheHandler (Redis/KV) a new function could inherit
|
|
24
|
+
* another function's old ID and serve its persisted data — cross-deploy cache
|
|
25
|
+
* poisoning (TIM-1053). Content hashing makes IDs immune to reordering and
|
|
26
|
+
* unrelated edits, while a changed call gets a fresh ID — which is exactly
|
|
27
|
+
* when invalidation is wanted.
|
|
28
|
+
*
|
|
29
|
+
* The enclosing-statement text is included so that byte-identical cache()
|
|
30
|
+
* calls in different scopes — e.g. two factory helpers that each `return
|
|
31
|
+
* cache(fn, { ttl: 60 })` — get distinct, insertion-stable IDs: their
|
|
32
|
+
* enclosing declarations differ by name/body even when the call spans are
|
|
33
|
+
* identical. Without it, a source-order occurrence counter alone would shift
|
|
34
|
+
* when an identical span is inserted above, reassigning IDs across deploys.
|
|
35
|
+
* Calls that are byte-identical INCLUDING their enclosing top-level statement
|
|
36
|
+
* are disambiguated by a source-order occurrence index. That residue is safe:
|
|
37
|
+
* identical top-level declarations are a redeclaration error, so fully
|
|
38
|
+
* identical (statement, call) pairs can only be expression statements
|
|
39
|
+
* referencing the same bindings — the same function, where an ID swap is
|
|
40
|
+
* harmless.
|
|
41
|
+
*
|
|
42
|
+
* See TIM-997, TIM-1053, design/06-caching.md §"Cache Key Security".
|
|
16
43
|
*/
|
|
17
44
|
|
|
18
45
|
import { relative } from 'node:path';
|
|
@@ -118,16 +145,30 @@ function findCacheCallSites(
|
|
|
118
145
|
}
|
|
119
146
|
|
|
120
147
|
/**
|
|
121
|
-
* Generate a stable callsite ID from a module path
|
|
148
|
+
* Generate a stable callsite ID from a module path, the source text of the
|
|
149
|
+
* enclosing top-level statement, the source text of the cache(...) call
|
|
150
|
+
* expression, and an occurrence index among fully byte-identical
|
|
151
|
+
* (statement, call) pairs in the same file.
|
|
122
152
|
*
|
|
123
|
-
* The ID is
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* - Unique across different modules
|
|
127
|
-
* - Unique across
|
|
153
|
+
* The ID is deterministic and:
|
|
154
|
+
* - Stable across cold starts AND across builds/deploys for unchanged calls —
|
|
155
|
+
* inserting, deleting, or reordering other cache() calls does not affect it
|
|
156
|
+
* - Unique across different modules (relPath component)
|
|
157
|
+
* - Unique across distinct cache() calls within a module (statement + span
|
|
158
|
+
* hashes), with fully identical duplicates disambiguated by source-order
|
|
159
|
+
* occurrence
|
|
160
|
+
* - Fresh when the wrapped fn, opts, or enclosing statement change, which
|
|
161
|
+
* intentionally invalidates the old persisted entries
|
|
128
162
|
*/
|
|
129
|
-
function generateStableId(
|
|
130
|
-
|
|
163
|
+
function generateStableId(
|
|
164
|
+
relativeModulePath: string,
|
|
165
|
+
statementText: string,
|
|
166
|
+
callSpanText: string,
|
|
167
|
+
occurrence: number
|
|
168
|
+
): string {
|
|
169
|
+
return fnv1aHash(
|
|
170
|
+
`${relativeModulePath}:cache:${fnv1aHash(statementText)}:${fnv1aHash(callSpanText)}:${occurrence}`
|
|
171
|
+
);
|
|
131
172
|
}
|
|
132
173
|
|
|
133
174
|
export function timberCacheTransform(ctx: PluginContext): Plugin {
|
|
@@ -149,23 +190,40 @@ export function timberCacheTransform(ctx: PluginContext): Plugin {
|
|
|
149
190
|
const cacheBindings = findCacheBindings(program);
|
|
150
191
|
if (cacheBindings.size === 0) return;
|
|
151
192
|
|
|
152
|
-
|
|
153
|
-
|
|
193
|
+
// Collect call sites per top-level statement so each call carries its
|
|
194
|
+
// enclosing statement's source text into the ID hash.
|
|
195
|
+
const callSites: Array<{ call: CallExpressionNode; stmt: PositionedNode }> = [];
|
|
196
|
+
for (const stmt of program.body) {
|
|
197
|
+
const calls: CallExpressionNode[] = [];
|
|
198
|
+
findCacheCallSites(stmt, cacheBindings, calls);
|
|
199
|
+
for (const call of calls) callSites.push({ call, stmt });
|
|
200
|
+
}
|
|
154
201
|
if (callSites.length === 0) return;
|
|
155
202
|
|
|
156
|
-
// Sort by position in file
|
|
157
|
-
|
|
203
|
+
// Sort by position in file so occurrence indices among byte-identical
|
|
204
|
+
// (statement, call) pairs are assigned in deterministic source order
|
|
205
|
+
callSites.sort((a, b) => a.call.start - b.call.start);
|
|
158
206
|
|
|
159
207
|
const root = ctx.root || process.cwd();
|
|
160
208
|
const relPath = relative(root, id);
|
|
161
209
|
const s = new MagicString(code);
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
210
|
+
const occurrences = new Map<string, number>();
|
|
211
|
+
|
|
212
|
+
for (const { call, stmt } of callSites) {
|
|
213
|
+
const stmtText = code.slice(stmt.start, stmt.end);
|
|
214
|
+
const spanText = code.slice(call.start, call.end);
|
|
215
|
+
const dupKey = stmtText + String.fromCharCode(0) + spanText;
|
|
216
|
+
const occurrence = occurrences.get(dupKey) ?? 0;
|
|
217
|
+
occurrences.set(dupKey, occurrence + 1);
|
|
218
|
+
const stableId = generateStableId(relPath, stmtText, spanText, occurrence);
|
|
219
|
+
// Insert the stable ID as the 3rd argument directly after the last
|
|
220
|
+
// argument — NOT before the closing ')'. The call may already have a
|
|
221
|
+
// trailing comma after the options argument (Prettier v3 default for
|
|
222
|
+
// multiline calls); inserting before ')' would emit `,, "id"`, a
|
|
223
|
+
// parse error (TIM-1021). A trailing comma after the inserted
|
|
224
|
+
// argument remains valid JS.
|
|
225
|
+
const lastArg = call.arguments[call.arguments.length - 1];
|
|
226
|
+
s.appendLeft(lastArg.end, `, "${stableId}"`);
|
|
169
227
|
}
|
|
170
228
|
|
|
171
229
|
return {
|
|
@@ -10,11 +10,15 @@
|
|
|
10
10
|
* 2. Externalizes `.wasm` imports so Rolldown skips them
|
|
11
11
|
* 3. Copies the WASM files to the build output so wrangler can find them
|
|
12
12
|
*
|
|
13
|
-
* Only active for Cloudflare adapter production builds
|
|
13
|
+
* Only active for Cloudflare adapter production builds — every hook bails
|
|
14
|
+
* unless `isCloudflareBuild` was set in configResolved. In dev and non-CF
|
|
15
|
+
* builds, `.wasm` imports are left to Vite's own handling (?init / ?url or
|
|
16
|
+
* a clear build error), never silently externalized.
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
19
|
import { basename, resolve, dirname } from 'node:path';
|
|
17
|
-
import {
|
|
20
|
+
import { createHash } from 'node:crypto';
|
|
21
|
+
import { copyFile, mkdir, readFile } from 'node:fs/promises';
|
|
18
22
|
import type { Plugin, ResolvedConfig } from 'vite';
|
|
19
23
|
import type { PluginContext } from '../plugin-context.js';
|
|
20
24
|
|
|
@@ -24,8 +28,13 @@ function isCloudflareAdapter(ctx: PluginContext): boolean {
|
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
export function timberCloudflareWasm(ctx: PluginContext): Plugin[] {
|
|
27
|
-
//
|
|
31
|
+
// Resolved WASM files, keyed by absolute source path so two files that
|
|
32
|
+
// share a basename (e.g. lib_bg.wasm from two packages) don't collide.
|
|
33
|
+
// The value is the unique output basename used for the external import
|
|
34
|
+
// specifier and the copy destination.
|
|
28
35
|
const wasmFiles = new Map<string, string>();
|
|
36
|
+
const usedBasenames = new Set<string>();
|
|
37
|
+
let isCloudflareBuild = false;
|
|
29
38
|
|
|
30
39
|
const plugin: Plugin = {
|
|
31
40
|
name: 'timber-cloudflare-wasm',
|
|
@@ -33,6 +42,7 @@ export function timberCloudflareWasm(ctx: PluginContext): Plugin[] {
|
|
|
33
42
|
configResolved(resolved: ResolvedConfig) {
|
|
34
43
|
if (resolved.command !== 'build') return;
|
|
35
44
|
if (!isCloudflareAdapter(ctx)) return;
|
|
45
|
+
isCloudflareBuild = true;
|
|
36
46
|
|
|
37
47
|
// Prepend `workerd` to resolve conditions for server environments
|
|
38
48
|
// so @takumi-rs/wasm/auto resolves to the Cloudflare-native entry
|
|
@@ -49,9 +59,23 @@ export function timberCloudflareWasm(ctx: PluginContext): Plugin[] {
|
|
|
49
59
|
}
|
|
50
60
|
},
|
|
51
61
|
|
|
62
|
+
// Strip /* @vite-ignore */ from @takumi-rs/wasm dynamic imports so
|
|
63
|
+
// Rolldown bundles them instead of leaving bare specifiers that fail
|
|
64
|
+
// at runtime on Cloudflare Workers (no node_modules).
|
|
65
|
+
transform(code, id) {
|
|
66
|
+
if (!isCloudflareBuild) return;
|
|
67
|
+
if (!id.includes('takumi')) return;
|
|
68
|
+
const stripped = code.replaceAll(
|
|
69
|
+
/\/\*\s*@vite-ignore\s*\*\/\s*\n\s*"@takumi-rs\/wasm"/g,
|
|
70
|
+
'"@takumi-rs/wasm"'
|
|
71
|
+
);
|
|
72
|
+
if (stripped !== code) return stripped;
|
|
73
|
+
},
|
|
74
|
+
|
|
52
75
|
resolveId: {
|
|
53
76
|
order: 'pre',
|
|
54
77
|
async handler(source, importer) {
|
|
78
|
+
if (!isCloudflareBuild) return;
|
|
55
79
|
if (!source.endsWith('.wasm')) return;
|
|
56
80
|
|
|
57
81
|
let absolutePath: string;
|
|
@@ -61,14 +85,28 @@ export function timberCloudflareWasm(ctx: PluginContext): Plugin[] {
|
|
|
61
85
|
return;
|
|
62
86
|
}
|
|
63
87
|
|
|
64
|
-
|
|
65
|
-
|
|
88
|
+
let outputName = wasmFiles.get(absolutePath);
|
|
89
|
+
if (!outputName) {
|
|
90
|
+
outputName = basename(absolutePath);
|
|
91
|
+
if (usedBasenames.has(outputName)) {
|
|
92
|
+
// Another wasm file already claimed this basename — disambiguate
|
|
93
|
+
// with a short content hash so both files land in the output.
|
|
94
|
+
const hash = createHash('sha256')
|
|
95
|
+
.update(await readFile(absolutePath))
|
|
96
|
+
.digest('hex')
|
|
97
|
+
.slice(0, 8);
|
|
98
|
+
outputName = outputName.replace(/\.wasm$/, `.${hash}.wasm`);
|
|
99
|
+
}
|
|
100
|
+
usedBasenames.add(outputName);
|
|
101
|
+
wasmFiles.set(absolutePath, outputName);
|
|
102
|
+
}
|
|
66
103
|
|
|
67
|
-
return { id: './' +
|
|
104
|
+
return { id: './' + outputName, external: true };
|
|
68
105
|
},
|
|
69
106
|
},
|
|
70
107
|
|
|
71
108
|
async writeBundle(options, bundle) {
|
|
109
|
+
if (!isCloudflareBuild) return;
|
|
72
110
|
if (wasmFiles.size === 0) return;
|
|
73
111
|
|
|
74
112
|
const outDir = options.dir;
|
|
@@ -79,8 +117,8 @@ export function timberCloudflareWasm(ctx: PluginContext): Plugin[] {
|
|
|
79
117
|
const targetDirs = new Set<string>();
|
|
80
118
|
for (const chunk of Object.values(bundle)) {
|
|
81
119
|
if (chunk.type !== 'chunk') continue;
|
|
82
|
-
for (const
|
|
83
|
-
if (chunk.code.includes(
|
|
120
|
+
for (const outputName of wasmFiles.values()) {
|
|
121
|
+
if (chunk.code.includes(outputName)) {
|
|
84
122
|
targetDirs.add(dirname(resolve(outDir, chunk.fileName)));
|
|
85
123
|
}
|
|
86
124
|
}
|
|
@@ -91,8 +129,8 @@ export function timberCloudflareWasm(ctx: PluginContext): Plugin[] {
|
|
|
91
129
|
const copies: Promise<void>[] = [];
|
|
92
130
|
for (const dir of targetDirs) {
|
|
93
131
|
await mkdir(dir, { recursive: true });
|
|
94
|
-
for (const [
|
|
95
|
-
copies.push(copyFile(absolutePath, resolve(dir,
|
|
132
|
+
for (const [absolutePath, outputName] of wasmFiles) {
|
|
133
|
+
copies.push(copyFile(absolutePath, resolve(dir, outputName)));
|
|
96
134
|
}
|
|
97
135
|
}
|
|
98
136
|
await Promise.all(copies);
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import type { Plugin, ViteDevServer, DevEnvironment } from 'vite';
|
|
16
16
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
17
|
+
import type { TLSSocket } from 'node:tls';
|
|
17
18
|
import { join } from 'node:path';
|
|
18
19
|
import type { PluginContext } from '../plugin-context.js';
|
|
19
20
|
import { setViteServer } from '../dev-tools/warnings.js';
|
|
@@ -24,11 +25,8 @@ import {
|
|
|
24
25
|
parseFirstAppFrame,
|
|
25
26
|
type ErrorPhase,
|
|
26
27
|
} from '../dev-tools/overlay.js';
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
extractHmrOptions,
|
|
30
|
-
type DevErrorHmrOptions,
|
|
31
|
-
} from '../dev-tools/error-page.js';
|
|
28
|
+
import { generateDevErrorPage } from '../dev-tools/error-page.js';
|
|
29
|
+
import { extractHmrOptions, type DevErrorHmrOptions } from '../dev-tools/dev-page-shell.js';
|
|
32
30
|
import { addVirtualModuleContext } from '../config-validation.js';
|
|
33
31
|
import { compressResponse } from '../server/compress.js';
|
|
34
32
|
import { isMetadataRouteServePath } from '../server/metadata-routes.js';
|
|
@@ -207,6 +205,7 @@ function createTimberMiddleware(server: ViteDevServer, projectRoot: string) {
|
|
|
207
205
|
stack: unknown[] | null;
|
|
208
206
|
}>
|
|
209
207
|
) => void;
|
|
208
|
+
devHmrOptions?: DevErrorHmrOptions;
|
|
210
209
|
}
|
|
211
210
|
| undefined;
|
|
212
211
|
if (config) {
|
|
@@ -219,6 +218,11 @@ function createTimberMiddleware(server: ViteDevServer, projectRoot: string) {
|
|
|
219
218
|
debugComponents
|
|
220
219
|
);
|
|
221
220
|
};
|
|
221
|
+
// Plumb the resolved HMR options into the RSC environment. The RSC
|
|
222
|
+
// entry can't read the Vite server config (separate module graph),
|
|
223
|
+
// but its dev 404 page and fallback error page need the HMR endpoint
|
|
224
|
+
// (including Vite 8's required WS token) for auto-reload (TIM-1067).
|
|
225
|
+
config.devHmrOptions = hmrOptions;
|
|
222
226
|
}
|
|
223
227
|
|
|
224
228
|
// Wire source-map handler so error pages show original positions.
|
|
@@ -357,15 +361,30 @@ function addTimberContext(error: Error): void {
|
|
|
357
361
|
*
|
|
358
362
|
* Constructs the full URL from the Host header and request URL,
|
|
359
363
|
* and forwards the method, headers, and body.
|
|
364
|
+
*
|
|
365
|
+
* The scheme comes from the socket's TLS state — with `server.https` the
|
|
366
|
+
* browser sends `Origin: https://host`, and a hardcoded `http://` would
|
|
367
|
+
* fail CSRF origin comparison on every POST. X-Forwarded-Proto is NOT
|
|
368
|
+
* consulted: dev connections are direct, so the header would be
|
|
369
|
+
* attacker-controlled. (TIM-1067, B32)
|
|
370
|
+
*
|
|
371
|
+
* Exported for tests.
|
|
360
372
|
*/
|
|
361
|
-
function toWebRequest(nodeReq: IncomingMessage): Request {
|
|
362
|
-
const protocol = 'http';
|
|
363
|
-
|
|
373
|
+
export function toWebRequest(nodeReq: IncomingMessage): Request {
|
|
374
|
+
const protocol = (nodeReq.socket as TLSSocket | undefined)?.encrypted ? 'https' : 'http';
|
|
375
|
+
// Under HTTP/2 there is no Host header — the authority lives in the
|
|
376
|
+
// `:authority` pseudo-header.
|
|
377
|
+
const host =
|
|
378
|
+
nodeReq.headers.host ?? (nodeReq.headers[':authority'] as string | undefined) ?? 'localhost';
|
|
364
379
|
const url = `${protocol}://${host}${nodeReq.url}`;
|
|
365
380
|
|
|
366
381
|
const headers = new Headers();
|
|
367
382
|
for (const [key, value] of Object.entries(nodeReq.headers)) {
|
|
368
383
|
if (value === undefined) continue;
|
|
384
|
+
// HTTP/2 pseudo-headers (:method, :path, :authority, :scheme) are not
|
|
385
|
+
// valid Headers names — Headers.set() throws on them. They're already
|
|
386
|
+
// reflected in the request URL/method above.
|
|
387
|
+
if (key.startsWith(':')) continue;
|
|
369
388
|
if (Array.isArray(value)) {
|
|
370
389
|
for (const v of value) {
|
|
371
390
|
headers.append(key, v);
|
|
@@ -374,6 +393,11 @@ function toWebRequest(nodeReq: IncomingMessage): Request {
|
|
|
374
393
|
headers.set(key, value);
|
|
375
394
|
}
|
|
376
395
|
}
|
|
396
|
+
// HTTP/2 requests carry the authority in `:authority`, not Host — but
|
|
397
|
+
// downstream consumers (CSRF origin validation) read the Host header.
|
|
398
|
+
if (!headers.has('host')) {
|
|
399
|
+
headers.set('host', host);
|
|
400
|
+
}
|
|
377
401
|
|
|
378
402
|
const method = nodeReq.method ?? 'GET';
|
|
379
403
|
const hasBody = method !== 'GET' && method !== 'HEAD';
|
|
@@ -11,8 +11,14 @@
|
|
|
11
11
|
* Design docs: design/11-platform.md, design/25-production-deployments.md
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { builtinModules } from 'node:module';
|
|
14
15
|
import type { Plugin } from 'vite';
|
|
15
16
|
|
|
17
|
+
// Every Node built-in as both bare (`stream`) and prefixed (`node:stream`).
|
|
18
|
+
// Available at runtime on all targets (Node natively, Workers via nodejs_compat).
|
|
19
|
+
// Listing explicitly silences Vite's "Automatically externalized" warnings.
|
|
20
|
+
const NODE_BUILTINS: string[] = builtinModules.flatMap((m) => [m, `node:${m}`]);
|
|
21
|
+
|
|
16
22
|
// Build-time-only packages that must never be bundled into the runtime
|
|
17
23
|
// server environments. With `noExternal: true` rolldown otherwise follows
|
|
18
24
|
// the transitive graph through the user's `timber.config.ts` (loaded by the
|
|
@@ -31,6 +37,7 @@ import type { Plugin } from 'vite';
|
|
|
31
37
|
//
|
|
32
38
|
// Mirrored on both rsc and ssr environments. See TIM-988.
|
|
33
39
|
const BUILD_ONLY_EXTERNALS: string[] = [
|
|
40
|
+
...NODE_BUILTINS,
|
|
34
41
|
'nitro',
|
|
35
42
|
'rollup',
|
|
36
43
|
'@mdx-js/rollup',
|
|
@@ -128,15 +135,18 @@ export function timberServerBundle(): Plugin[] {
|
|
|
128
135
|
// back to lazy retry on failure. This handles two failure modes:
|
|
129
136
|
//
|
|
130
137
|
// 1. Forward references (e.g. Zod v4): `init_iso` calls `init_schemas`
|
|
131
|
-
// which hasn't been defined yet. Eager execution fails, but
|
|
132
|
-
// function is called lazily later, all dependencies are
|
|
138
|
+
// which hasn't been defined yet. Eager execution fails silently, but
|
|
139
|
+
// when the function is called lazily later, all dependencies are
|
|
140
|
+
// available and the retry succeeds (and caches).
|
|
133
141
|
//
|
|
134
142
|
// 2. Optional peer dep shims (e.g. @emotion/is-prop-valid for
|
|
135
143
|
// framer-motion): Vite generates shims that throw for missing
|
|
136
|
-
// optional deps. The
|
|
137
|
-
// the consuming package's try/catch handles it.
|
|
144
|
+
// optional deps. The eager attempt swallows the throw; the lazy call
|
|
145
|
+
// PROPAGATES it so the consuming package's try/catch handles it.
|
|
138
146
|
//
|
|
139
|
-
// The key:
|
|
147
|
+
// The key: only the eager attempt swallows errors. Once the helper is
|
|
148
|
+
// returned, lazy calls use the original Rolldown semantics — a throwing
|
|
149
|
+
// init propagates to the caller (and clears `fn`, matching upstream).
|
|
140
150
|
// On success, `fn` is set to 0 so subsequent calls are no-ops.
|
|
141
151
|
const esmInitFixPlugin: Plugin = {
|
|
142
152
|
name: 'timber-esm-init-fix',
|
|
@@ -147,13 +157,19 @@ export function timberServerBundle(): Plugin[] {
|
|
|
147
157
|
const lazy = 'var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res);';
|
|
148
158
|
if (!code.includes(lazy)) return null;
|
|
149
159
|
|
|
150
|
-
// Eager-with-retry: attempt init immediately
|
|
151
|
-
// (
|
|
152
|
-
//
|
|
160
|
+
// Eager-with-retry: attempt init immediately, swallowing failures
|
|
161
|
+
// (forward refs aren't defined yet). After the eager attempt, flip
|
|
162
|
+
// to lazy mode where errors propagate to the caller — consumers'
|
|
163
|
+
// try/catch (framer-motion's optional @emotion dep) must see them.
|
|
153
164
|
const eager = [
|
|
154
165
|
'var __esmMin = (fn, res) => {',
|
|
155
|
-
' var
|
|
166
|
+
' var eager = 1;',
|
|
167
|
+
' var l = () => {',
|
|
168
|
+
' if (fn) { if (eager) { try { res = fn(); fn = 0; } catch(e) {} } else { res = fn(fn = 0); } }',
|
|
169
|
+
' return res;',
|
|
170
|
+
' };',
|
|
156
171
|
' l();',
|
|
172
|
+
' eager = 0;',
|
|
157
173
|
' return l;',
|
|
158
174
|
'};',
|
|
159
175
|
].join(' ');
|
package/src/plugins/shims.ts
CHANGED
|
@@ -44,19 +44,67 @@ const CLIENT_ONLY_VIRTUAL = '\0timber:client-only';
|
|
|
44
44
|
*
|
|
45
45
|
* Used by the catch-all resolveId rule to remap subpath imports to src/
|
|
46
46
|
* in server environments. When adding a new subpath export to package.json,
|
|
47
|
-
* add it here too
|
|
47
|
+
* add it here too — tests/shims-drift.test.ts enumerates package.json
|
|
48
|
+
* exports and fails if an entry is missing. See TIM-568.
|
|
49
|
+
*
|
|
50
|
+
* Exported for the drift test.
|
|
48
51
|
*/
|
|
49
|
-
const SUBPATH_SRC_MAP: Record<string, string> = {
|
|
52
|
+
export const SUBPATH_SRC_MAP: Record<string, string> = {
|
|
50
53
|
'cache': 'cache/index.ts',
|
|
54
|
+
'codec': 'codec.ts',
|
|
51
55
|
'cookies': 'cookies/index.ts',
|
|
56
|
+
'params': 'params/index.ts',
|
|
52
57
|
'search-params': 'search-params/index.ts',
|
|
53
58
|
'segment-params': 'segment-params/index.ts',
|
|
54
59
|
|
|
55
60
|
'adapters/cloudflare': 'adapters/cloudflare.ts',
|
|
61
|
+
'adapters/cloudflare/cache': 'adapters/cloudflare-kv-cache.ts',
|
|
56
62
|
'adapters/cloudflare/dev': 'adapters/cloudflare-dev.ts',
|
|
57
63
|
'adapters/nitro': 'adapters/nitro.ts',
|
|
58
64
|
};
|
|
59
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Runtime value exports of @timber-js/app/server, stubbed in the client
|
|
68
|
+
* (browser) environment so a stray client-side import fails with a clear
|
|
69
|
+
* call-time error instead of a confusing bundler error.
|
|
70
|
+
*
|
|
71
|
+
* Must stay in sync with the actual runtime exports of server/index.ts —
|
|
72
|
+
* tests/shims-drift.test.ts compares this list against
|
|
73
|
+
* Object.keys(import('../server/index.ts')) and fails on drift.
|
|
74
|
+
* Type-only exports (DenyOptions, AccessContext, …) are erased at runtime
|
|
75
|
+
* and must NOT appear here.
|
|
76
|
+
*/
|
|
77
|
+
export const SERVER_STUB_EXPORTS: readonly string[] = [
|
|
78
|
+
// request-context
|
|
79
|
+
'getHeaders',
|
|
80
|
+
'getSegmentParams',
|
|
81
|
+
// cookie-context
|
|
82
|
+
'getCookieJar',
|
|
83
|
+
// primitives
|
|
84
|
+
'deny',
|
|
85
|
+
'redirect',
|
|
86
|
+
'redirectExternal',
|
|
87
|
+
'RedirectType',
|
|
88
|
+
'waitUntil',
|
|
89
|
+
// action-client
|
|
90
|
+
'createActionClient',
|
|
91
|
+
'ActionError',
|
|
92
|
+
'validated',
|
|
93
|
+
// form-data
|
|
94
|
+
'coerce',
|
|
95
|
+
'parseFormData',
|
|
96
|
+
// form-flash
|
|
97
|
+
'getFormFlash',
|
|
98
|
+
// revalidation
|
|
99
|
+
'revalidatePath',
|
|
100
|
+
'revalidateTag',
|
|
101
|
+
// tracing
|
|
102
|
+
'getTraceId',
|
|
103
|
+
'getSpanId',
|
|
104
|
+
'withSpan',
|
|
105
|
+
'addSpanEvent',
|
|
106
|
+
];
|
|
107
|
+
|
|
60
108
|
/**
|
|
61
109
|
* Map from next/* import specifiers to shim file paths.
|
|
62
110
|
*
|
|
@@ -88,6 +136,16 @@ function stripJsExtension(id: string): string {
|
|
|
88
136
|
return id.endsWith('.js') ? id.slice(0, -3) : id;
|
|
89
137
|
}
|
|
90
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Read the Vite environment name from a plugin hook's `this` context.
|
|
141
|
+
*
|
|
142
|
+
* Duck-typed because the hook context type doesn't expose `environment`
|
|
143
|
+
* in a way that survives our Plugin typing — see Vite's Environment API.
|
|
144
|
+
*/
|
|
145
|
+
function envName(hookCtx: unknown): string | undefined {
|
|
146
|
+
return (hookCtx as { environment?: { name?: string } }).environment?.name;
|
|
147
|
+
}
|
|
148
|
+
|
|
91
149
|
/**
|
|
92
150
|
* Create the timber-shims Vite plugin.
|
|
93
151
|
*
|
|
@@ -111,14 +169,16 @@ export function timberShims(_ctx: PluginContext): Plugin {
|
|
|
111
169
|
* 3. Check next/* shim map
|
|
112
170
|
* 4. Return null (pass through) for everything else
|
|
113
171
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
172
|
+
* In server environments (RSC + SSR), @timber-js/app/* subpath imports
|
|
173
|
+
* are remapped to src/ via SUBPATH_SRC_MAP and the explicit branches
|
|
174
|
+
* below — dist/ chunks miss Vite's RSC transforms and would duplicate
|
|
175
|
+
* mutable module state (see TIM-568 and the rationale comments below).
|
|
118
176
|
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
177
|
+
* In the client (browser) environment, imports resolve to dist/ via
|
|
178
|
+
* package.json exports — except @timber-js/app/client (remapped to src/
|
|
179
|
+
* to share module instances with browser-entry) and @timber-js/app/server,
|
|
180
|
+
* which returns a virtual stub: server code must never be bundled into
|
|
181
|
+
* the browser.
|
|
122
182
|
*/
|
|
123
183
|
resolveId(id: string) {
|
|
124
184
|
// Poison pill packages — resolve to virtual modules handled by load()
|
|
@@ -133,8 +193,8 @@ export function timberShims(_ctx: PluginContext): Plugin {
|
|
|
133
193
|
// functions like redirect() and deny(). This prevents server/primitives.ts
|
|
134
194
|
// from being pulled into the browser bundle.
|
|
135
195
|
if (cleanId in SHIM_MAP) {
|
|
136
|
-
const
|
|
137
|
-
if (
|
|
196
|
+
const env = envName(this);
|
|
197
|
+
if (env === 'client' && cleanId === 'next/navigation') {
|
|
138
198
|
return resolve(SHIMS_DIR, 'navigation-client.ts');
|
|
139
199
|
}
|
|
140
200
|
return SHIM_MAP[cleanId];
|
|
@@ -171,8 +231,8 @@ export function timberShims(_ctx: PluginContext): Plugin {
|
|
|
171
231
|
// In Vite, we intercept them here to resolve to src/ for dev/build
|
|
172
232
|
// correctness (same rationale as the @timber-js/app/* remap above).
|
|
173
233
|
if (cleanId === '#server-internal') {
|
|
174
|
-
const
|
|
175
|
-
if (
|
|
234
|
+
const env = envName(this);
|
|
235
|
+
if (env === 'client') {
|
|
176
236
|
return '\0timber:server-empty';
|
|
177
237
|
}
|
|
178
238
|
return resolve(PKG_ROOT, 'src', 'server', 'internal.ts');
|
|
@@ -197,8 +257,8 @@ export function timberShims(_ctx: PluginContext): Plugin {
|
|
|
197
257
|
// specifier reaches us unresolved (e.g., in a Vite build without
|
|
198
258
|
// aliases), handle it the same way as the #imports above.
|
|
199
259
|
if (cleanId === '@timber-js/app/server/internal') {
|
|
200
|
-
const
|
|
201
|
-
if (
|
|
260
|
+
const env = envName(this);
|
|
261
|
+
if (env === 'client') {
|
|
202
262
|
return '\0timber:server-empty';
|
|
203
263
|
}
|
|
204
264
|
return resolve(PKG_ROOT, 'src', 'server', 'internal.ts');
|
|
@@ -210,8 +270,8 @@ export function timberShims(_ctx: PluginContext): Plugin {
|
|
|
210
270
|
}
|
|
211
271
|
|
|
212
272
|
if (cleanId === '@timber-js/app/server') {
|
|
213
|
-
const
|
|
214
|
-
if (
|
|
273
|
+
const env = envName(this);
|
|
274
|
+
if (env === 'client') {
|
|
215
275
|
return '\0timber:server-empty';
|
|
216
276
|
}
|
|
217
277
|
// RSC + SSR: remap to src/
|
|
@@ -219,7 +279,7 @@ export function timberShims(_ctx: PluginContext): Plugin {
|
|
|
219
279
|
}
|
|
220
280
|
|
|
221
281
|
if (cleanId === '@timber-js/app/client') {
|
|
222
|
-
const
|
|
282
|
+
const env = envName(this);
|
|
223
283
|
// RSC: return null — needs dist/ for 'use client' boundary detection.
|
|
224
284
|
// SSR + Client: resolve to src/ so that browser-entry (which uses
|
|
225
285
|
// relative imports from src/) and user code (which uses the bare
|
|
@@ -227,7 +287,7 @@ export function timberShims(_ctx: PluginContext): Plugin {
|
|
|
227
287
|
// Without this, state.ts is loaded twice (src/ and dist/) and
|
|
228
288
|
// setGlobalRouter() writes to one copy while getRouterOrNull()
|
|
229
289
|
// reads from the other (always null).
|
|
230
|
-
if (
|
|
290
|
+
if (env === 'ssr' || env === 'client') {
|
|
231
291
|
return resolve(PKG_ROOT, 'src', 'client', 'index.ts');
|
|
232
292
|
}
|
|
233
293
|
return null;
|
|
@@ -236,8 +296,8 @@ export function timberShims(_ctx: PluginContext): Plugin {
|
|
|
236
296
|
// Catch-all: remap any other @timber-js/app/<subpath> to src/
|
|
237
297
|
// in non-client environments (RSC + SSR).
|
|
238
298
|
if (cleanId.startsWith('@timber-js/app/') && cleanId !== '@timber-js/app/package.json') {
|
|
239
|
-
const
|
|
240
|
-
if (
|
|
299
|
+
const env = envName(this);
|
|
300
|
+
if (env !== 'client') {
|
|
241
301
|
const subpath = cleanId.slice('@timber-js/app/'.length);
|
|
242
302
|
const srcPath = SUBPATH_SRC_MAP[subpath];
|
|
243
303
|
if (srcPath) {
|
|
@@ -257,8 +317,8 @@ export function timberShims(_ctx: PluginContext): Plugin {
|
|
|
257
317
|
* clearly identifies the boundary violation.
|
|
258
318
|
*/
|
|
259
319
|
load(id: string) {
|
|
260
|
-
const
|
|
261
|
-
const isClient =
|
|
320
|
+
const env = envName(this);
|
|
321
|
+
const isClient = env === 'client';
|
|
262
322
|
|
|
263
323
|
if (id === SERVER_ONLY_VIRTUAL) {
|
|
264
324
|
if (isClient) {
|
|
@@ -288,41 +348,23 @@ export function timberShims(_ctx: PluginContext): Plugin {
|
|
|
288
348
|
if (id === '\0timber:server-empty') {
|
|
289
349
|
// Export named stubs instead of throwing at evaluation time.
|
|
290
350
|
// Throwing at eval breaks the module system — the browser can't
|
|
291
|
-
// resolve named imports like `import {
|
|
351
|
+
// resolve named imports like `import { deny } from '...'`.
|
|
292
352
|
// Instead, each stub throws at call time with a clear message.
|
|
353
|
+
// The export names are generated from SERVER_STUB_EXPORTS so the
|
|
354
|
+
// list cannot drift from server/index.ts (drift test enforced).
|
|
355
|
+
// RedirectType is a plain const object on the server, so it is
|
|
356
|
+
// mirrored as one here — libraries read RedirectType.push as a value.
|
|
357
|
+
const stubExports = SERVER_STUB_EXPORTS.map((name) =>
|
|
358
|
+
name === 'RedirectType'
|
|
359
|
+
? `export const RedirectType = { push: 'push', replace: 'replace' };`
|
|
360
|
+
: `export const ${name} = stub;`
|
|
361
|
+
).join('\n');
|
|
293
362
|
return `
|
|
294
363
|
const msg = "[timber] @timber-js/app/server was imported from client code. " +
|
|
295
364
|
"Server modules (headers, cookies, redirect, deny, etc.) cannot be used in client components. " +
|
|
296
365
|
"If you need these APIs, move the import to a server component or middleware.";
|
|
297
366
|
function stub() { throw new Error(msg); }
|
|
298
|
-
|
|
299
|
-
export const cookies = stub;
|
|
300
|
-
export const getHeaders = stub;
|
|
301
|
-
export const getHeader = stub;
|
|
302
|
-
export const getCookieJar = stub;
|
|
303
|
-
export const getCookie = stub;
|
|
304
|
-
export const getSearchParams = stub;
|
|
305
|
-
export const getSegmentParams = stub;
|
|
306
|
-
export const searchParams = stub;
|
|
307
|
-
export const deny = stub;
|
|
308
|
-
export const notFound = stub;
|
|
309
|
-
export const redirect = stub;
|
|
310
|
-
export const permanentRedirect = stub;
|
|
311
|
-
export const redirectExternal = stub;
|
|
312
|
-
export const waitUntil = stub;
|
|
313
|
-
export const DenyOptions = {};
|
|
314
|
-
export const RedirectType = {};
|
|
315
|
-
export const DenySignal = stub;
|
|
316
|
-
export const RedirectSignal = stub;
|
|
317
|
-
export const createPipeline = stub;
|
|
318
|
-
export const revalidatePath = stub;
|
|
319
|
-
export const revalidateTag = stub;
|
|
320
|
-
export const createActionClient = stub;
|
|
321
|
-
export const ActionError = stub;
|
|
322
|
-
export const validated = stub;
|
|
323
|
-
export const getFormFlash = stub;
|
|
324
|
-
export const parseFormData = stub;
|
|
325
|
-
export const coerce = stub;
|
|
367
|
+
${stubExports}
|
|
326
368
|
`;
|
|
327
369
|
}
|
|
328
370
|
},
|
|
@@ -21,6 +21,10 @@ export interface RouteEntry {
|
|
|
21
21
|
searchParamsPagePath?: string;
|
|
22
22
|
/** Whether this is an API route (route.ts) vs page route */
|
|
23
23
|
isApiRoute: boolean;
|
|
24
|
+
/** Whether this entry lives inside a parallel slot (@name). Slot entries
|
|
25
|
+
* share their parent's urlPath — urlPath-keyed emissions dedupe them,
|
|
26
|
+
* preferring the non-slot entry. segmentPath-keyed emissions keep all. */
|
|
27
|
+
isSlotRoute?: boolean;
|
|
24
28
|
/** Whether a global schema (app/schema.ts) exists for this app */
|
|
25
29
|
hasSchema?: boolean;
|
|
26
30
|
}
|