@timber-js/app 0.2.0-alpha.57 → 0.2.0-alpha.58
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/als-registry-Ba7URUIn.js.map +1 -1
- package/dist/_chunks/define-D5STJpIr.js +121 -0
- package/dist/_chunks/define-D5STJpIr.js.map +1 -0
- package/dist/_chunks/{define-cookie-k9btcEfI.js → define-cookie-DtAavax4.js} +4 -4
- package/dist/_chunks/{define-cookie-k9btcEfI.js.map → define-cookie-DtAavax4.js.map} +1 -1
- package/dist/_chunks/{error-boundary-B9vT_YK_.js → error-boundary-DpZJBCqh.js} +1 -1
- package/dist/_chunks/{error-boundary-B9vT_YK_.js.map → error-boundary-DpZJBCqh.js.map} +1 -1
- package/dist/_chunks/{request-context-0h-6Voad.js → request-context-0wfZsnhh.js} +3 -1
- package/dist/_chunks/request-context-0wfZsnhh.js.map +1 -0
- package/dist/_chunks/{segment-context-Bmugn-ao.js → segment-context-CyaM1mrD.js} +1 -1
- package/dist/_chunks/{segment-context-Bmugn-ao.js.map → segment-context-CyaM1mrD.js.map} +1 -1
- package/dist/_chunks/{stale-reload-Db4wqE46.js → stale-reload-DKN3aXxR.js} +1 -1
- package/dist/_chunks/{stale-reload-Db4wqE46.js.map → stale-reload-DKN3aXxR.js.map} +1 -1
- package/dist/_chunks/{tracing-JI4cYUdz.js → tracing-VYETCQsg.js} +1 -1
- package/dist/_chunks/{tracing-JI4cYUdz.js.map → tracing-VYETCQsg.js.map} +1 -1
- package/dist/_chunks/{wrappers-C9XPg7-U.js → wrappers-BaG1bnM3.js} +1 -1
- package/dist/_chunks/{wrappers-C9XPg7-U.js.map → wrappers-BaG1bnM3.js.map} +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/error-reconstituter.d.ts +54 -0
- package/dist/client/error-reconstituter.d.ts.map +1 -0
- package/dist/client/index.js +4 -4
- package/dist/client/segment-outlet.d.ts +63 -0
- package/dist/client/segment-outlet.d.ts.map +1 -0
- package/dist/cookies/index.js +1 -1
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +172 -0
- package/dist/index.js.map +1 -1
- package/dist/params/define.d.ts +24 -0
- package/dist/params/define.d.ts.map +1 -1
- package/dist/params/index.js +2 -103
- package/dist/plugins/dev-browser-logs.d.ts +84 -0
- package/dist/plugins/dev-browser-logs.d.ts.map +1 -0
- package/dist/search-params/index.js +1 -1
- package/dist/server/als-registry.d.ts +7 -0
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/index.js +4 -4
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts +9 -9
- 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/ssr-renderer.d.ts.map +1 -1
- package/dist/server/stream-utils.d.ts +36 -0
- package/dist/server/stream-utils.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/client/browser-entry.ts +6 -7
- package/src/client/error-reconstituter.tsx +65 -0
- package/src/client/segment-outlet.tsx +86 -0
- package/src/index.ts +17 -0
- package/src/params/define.ts +60 -0
- package/src/plugins/dev-browser-logs.ts +274 -0
- package/src/server/als-registry.ts +7 -0
- package/src/server/deny-renderer.ts +2 -1
- package/src/server/request-context.ts +6 -0
- package/src/server/rsc-entry/error-renderer.ts +108 -173
- package/src/server/rsc-entry/index.ts +14 -10
- package/src/server/rsc-entry/ssr-renderer.ts +5 -1
- package/src/server/stream-utils.ts +209 -0
- package/dist/_chunks/request-context-0h-6Voad.js.map +0 -1
- package/dist/params/index.js.map +0 -1
- package/dist/server/rsc-entry/ssr-error-bridge.d.ts +0 -12
- package/dist/server/rsc-entry/ssr-error-bridge.d.ts.map +0 -1
- package/dist/server/ssr-error-entry.d.ts +0 -65
- package/dist/server/ssr-error-entry.d.ts.map +0 -1
- package/src/server/rsc-entry/ssr-error-bridge.ts +0 -20
- package/src/server/ssr-error-entry.ts +0 -237
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { timberStaticBuild } from './plugins/static-build';
|
|
|
15
15
|
import { timberServerActionExports } from './plugins/server-action-exports';
|
|
16
16
|
import { timberBuildManifest } from './plugins/build-manifest';
|
|
17
17
|
import { timberDevLogs } from './plugins/dev-logs';
|
|
18
|
+
import { timberDevBrowserLogs } from './plugins/dev-browser-logs';
|
|
18
19
|
import { timberReactProd } from './plugins/react-prod';
|
|
19
20
|
import { timberChunks } from './plugins/chunks';
|
|
20
21
|
import { clientChunkGroup } from './plugins/client-chunks';
|
|
@@ -102,6 +103,21 @@ export interface TimberUserConfig {
|
|
|
102
103
|
* See design/02-rendering-pipeline.md §"Streaming Constraints".
|
|
103
104
|
*/
|
|
104
105
|
renderTimeoutMs?: number;
|
|
106
|
+
/**
|
|
107
|
+
* Forward browser console output to the server terminal in dev mode.
|
|
108
|
+
*
|
|
109
|
+
* Sets the minimum log level to forward:
|
|
110
|
+
* - `'error'` — only `console.error`
|
|
111
|
+
* - `'warn'` — `console.error` + `console.warn` (default)
|
|
112
|
+
* - `'info'` — `console.error` + `console.warn` + `console.info`
|
|
113
|
+
* - `'none'` — disabled
|
|
114
|
+
*
|
|
115
|
+
* Does not intercept `console.log` or `console.debug` (too noisy).
|
|
116
|
+
* No effect in production builds.
|
|
117
|
+
*
|
|
118
|
+
* See TIM-513.
|
|
119
|
+
*/
|
|
120
|
+
devBrowserLogs?: 'error' | 'warn' | 'info' | 'none';
|
|
105
121
|
/** Dev-mode options. These have no effect in production builds. */
|
|
106
122
|
dev?: {
|
|
107
123
|
/** Threshold in ms to highlight slow phases in dev logging output. Default: 200. */
|
|
@@ -644,6 +660,7 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
|
|
|
644
660
|
timberBuildReport(ctx), // Post-build: route table with bundle sizes
|
|
645
661
|
timberAdapterBuild(ctx), // Post-build: invoke adapter.buildOutput()
|
|
646
662
|
timberDevLogs(ctx), // Dev-only: forward server console.* to browser console
|
|
663
|
+
timberDevBrowserLogs(ctx), // Dev-only: forward browser console.* to server terminal
|
|
647
664
|
timberDevServer(ctx), // Must be last — configureServer post-hook runs after all watchers
|
|
648
665
|
];
|
|
649
666
|
}
|
package/src/params/define.ts
CHANGED
|
@@ -15,6 +15,33 @@
|
|
|
15
15
|
|
|
16
16
|
import type { Codec } from '#/codec.js';
|
|
17
17
|
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Server-only ALS reference for .load()
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
// Same pattern as search-params: eagerly registered at server startup
|
|
23
|
+
// to avoid dynamic imports that lose ALS context. See TIM-523.
|
|
24
|
+
let _rawSegmentParams: (() => Promise<Record<string, string | string[]>>) | undefined;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Register the rawSegmentParams function. Called once at module load time
|
|
28
|
+
* from request-context.ts to avoid dynamic import at call time.
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
export function _setRawSegmentParamsFn(fn: () => Promise<Record<string, string | string[]>>): void {
|
|
32
|
+
_rawSegmentParams = fn;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getRawSegmentParams(): Promise<Record<string, string | string[]>> {
|
|
36
|
+
if (!_rawSegmentParams) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
'[timber] segmentParams.load() is only available on the server. ' +
|
|
39
|
+
'Use useSegmentParams() on the client.'
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return _rawSegmentParams();
|
|
43
|
+
}
|
|
44
|
+
|
|
18
45
|
// ---------------------------------------------------------------------------
|
|
19
46
|
// Types
|
|
20
47
|
// ---------------------------------------------------------------------------
|
|
@@ -39,6 +66,25 @@ export interface ParamsDefinition<T extends Record<string, unknown>> {
|
|
|
39
66
|
/** Serialize typed values back to strings for URL construction. */
|
|
40
67
|
serialize(values: T): Record<string, string>;
|
|
41
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Load typed segment params from the current request context (ALS).
|
|
71
|
+
*
|
|
72
|
+
* Server-only. Reads rawSegmentParams() from ALS and coerces through
|
|
73
|
+
* this definition's codecs, returning fully typed params.
|
|
74
|
+
*
|
|
75
|
+
* ```ts
|
|
76
|
+
* // app/products/[id]/params.ts
|
|
77
|
+
* export const segmentParams = defineSegmentParams({ id: z.coerce.number() })
|
|
78
|
+
*
|
|
79
|
+
* // app/products/[id]/page.tsx
|
|
80
|
+
* import { segmentParams } from './params'
|
|
81
|
+
* export default async function Page() {
|
|
82
|
+
* const { id } = await segmentParams.load() // id: number
|
|
83
|
+
* }
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
load(): Promise<T>;
|
|
87
|
+
|
|
42
88
|
/** Read-only codec map. */
|
|
43
89
|
codecs: { [K in keyof T]: Codec<T[K]> };
|
|
44
90
|
}
|
|
@@ -250,9 +296,23 @@ export function defineSegmentParams<C extends Record<string, ParamField>>(
|
|
|
250
296
|
return result;
|
|
251
297
|
}
|
|
252
298
|
|
|
299
|
+
// ---- load ----
|
|
300
|
+
// ALS-backed: reads rawSegmentParams() from the current request context
|
|
301
|
+
// and parses through codecs. Server-only — throws on client.
|
|
302
|
+
async function load(): Promise<T> {
|
|
303
|
+
if (typeof window !== 'undefined') {
|
|
304
|
+
throw new Error(
|
|
305
|
+
'[timber] segmentParams.load() is server-only. ' + 'Use useSegmentParams() on the client.'
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
const raw = await getRawSegmentParams();
|
|
309
|
+
return parse(raw);
|
|
310
|
+
}
|
|
311
|
+
|
|
253
312
|
const definition: ParamsDefinition<T> = {
|
|
254
313
|
parse,
|
|
255
314
|
serialize,
|
|
315
|
+
load,
|
|
256
316
|
codecs: resolvedCodecs as { [K in keyof T]: Codec<T[K]> },
|
|
257
317
|
};
|
|
258
318
|
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* timber-dev-browser-logs — Forwards browser console output to the server terminal.
|
|
3
|
+
*
|
|
4
|
+
* Injects a small inline script in dev mode that intercepts browser
|
|
5
|
+
* `console.error`, `console.warn`, and `console.info`, then forwards
|
|
6
|
+
* messages to the server via Vite's HMR WebSocket.
|
|
7
|
+
*
|
|
8
|
+
* The server-side listener formats and prints the messages with color-coded
|
|
9
|
+
* prefixes: [browser:error], [browser:warn], [browser:info].
|
|
10
|
+
*
|
|
11
|
+
* Dev-only: this plugin only runs during `vite dev`.
|
|
12
|
+
* No runtime overhead in production.
|
|
13
|
+
*
|
|
14
|
+
* See TIM-513 for design context.
|
|
15
|
+
* See design/18-build-system.md §"Dev Server" for sub-plugin architecture.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Plugin, ViteDevServer } from 'vite';
|
|
19
|
+
import type { PluginContext } from '#/index.js';
|
|
20
|
+
|
|
21
|
+
// ─── Types ───────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/** Log levels forwarded from the browser. */
|
|
24
|
+
export type BrowserLogLevel = 'error' | 'warn' | 'info';
|
|
25
|
+
|
|
26
|
+
/** Configuration value for devBrowserLogs in timber.config.ts. */
|
|
27
|
+
export type DevBrowserLogsConfig = BrowserLogLevel | 'none' | undefined;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Payload sent from browser to server via Vite's HMR WebSocket.
|
|
31
|
+
* Kept small — truncated before sending.
|
|
32
|
+
*/
|
|
33
|
+
export interface BrowserLogPayload {
|
|
34
|
+
level: BrowserLogLevel;
|
|
35
|
+
/** Serialized message string. */
|
|
36
|
+
message: string;
|
|
37
|
+
/** Error stack trace, if the first argument was an Error. */
|
|
38
|
+
stack: string | null;
|
|
39
|
+
/** Source URL and line number, if available. */
|
|
40
|
+
source: string | null;
|
|
41
|
+
/** Timestamp in ms (Date.now()) from the browser. */
|
|
42
|
+
timestamp: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Constants ───────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/** Max message size in bytes before truncation. */
|
|
48
|
+
const MAX_MESSAGE_BYTES = 2048;
|
|
49
|
+
|
|
50
|
+
/** HMR event name for browser→server log forwarding. */
|
|
51
|
+
const HMR_EVENT = 'timber:browser-log';
|
|
52
|
+
|
|
53
|
+
/** ANSI color codes for terminal output. */
|
|
54
|
+
const COLORS = {
|
|
55
|
+
red: '\x1b[31m',
|
|
56
|
+
yellow: '\x1b[33m',
|
|
57
|
+
blue: '\x1b[34m',
|
|
58
|
+
dim: '\x1b[2m',
|
|
59
|
+
reset: '\x1b[0m',
|
|
60
|
+
} as const;
|
|
61
|
+
|
|
62
|
+
/** Level severity ordering for threshold comparison. */
|
|
63
|
+
const LEVEL_SEVERITY: Record<BrowserLogLevel | 'none', number> = {
|
|
64
|
+
none: 0,
|
|
65
|
+
info: 1,
|
|
66
|
+
warn: 2,
|
|
67
|
+
error: 3,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// ─── Formatting ──────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Format a browser log payload for server terminal output.
|
|
74
|
+
*
|
|
75
|
+
* Produces color-coded output like:
|
|
76
|
+
* [browser:error] Uncaught TypeError: x is not a function
|
|
77
|
+
* at App (app.tsx:10:5)
|
|
78
|
+
* [browser:warn] Deprecation warning (app/page.tsx:42:12)
|
|
79
|
+
*/
|
|
80
|
+
export function formatBrowserLog(payload: BrowserLogPayload): string {
|
|
81
|
+
const { level, message, stack, source } = payload;
|
|
82
|
+
|
|
83
|
+
// Color-coded prefix
|
|
84
|
+
const color = level === 'error' ? COLORS.red : level === 'warn' ? COLORS.yellow : COLORS.blue;
|
|
85
|
+
const prefix = `${color}[browser:${level}]${COLORS.reset}`;
|
|
86
|
+
|
|
87
|
+
// Source suffix
|
|
88
|
+
const sourceSuffix = source ? ` ${COLORS.dim}(${source})${COLORS.reset}` : '';
|
|
89
|
+
|
|
90
|
+
let output = `${prefix} ${message}${sourceSuffix}`;
|
|
91
|
+
|
|
92
|
+
// Append stack trace indented
|
|
93
|
+
if (stack) {
|
|
94
|
+
const indented = stack
|
|
95
|
+
.split('\n')
|
|
96
|
+
.map((line) => ` ${COLORS.dim}${line}${COLORS.reset}`)
|
|
97
|
+
.join('\n');
|
|
98
|
+
output += `\n${indented}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return output;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── Level Filtering ─────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if a log at `level` should be forwarded given the configured threshold.
|
|
108
|
+
*
|
|
109
|
+
* The threshold acts as a minimum severity:
|
|
110
|
+
* - 'error' → only errors
|
|
111
|
+
* - 'warn' → errors + warnings
|
|
112
|
+
* - 'info' → errors + warnings + info
|
|
113
|
+
* - 'none' → nothing
|
|
114
|
+
*/
|
|
115
|
+
export function shouldForwardLevel(
|
|
116
|
+
level: BrowserLogLevel,
|
|
117
|
+
threshold: BrowserLogLevel | 'none'
|
|
118
|
+
): boolean {
|
|
119
|
+
if (threshold === 'none') return false;
|
|
120
|
+
return LEVEL_SEVERITY[level] >= LEVEL_SEVERITY[threshold];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── Truncation ──────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Truncate a message to `maxBytes` to avoid flooding the terminal.
|
|
127
|
+
* Appends a suffix indicating how many bytes were dropped.
|
|
128
|
+
*/
|
|
129
|
+
export function truncateMessage(message: string, maxBytes: number): string {
|
|
130
|
+
if (message.length <= maxBytes) return message;
|
|
131
|
+
|
|
132
|
+
const truncated = message.slice(0, maxBytes);
|
|
133
|
+
const droppedBytes = message.length - maxBytes;
|
|
134
|
+
return `${truncated}… [truncated ${droppedBytes} bytes]`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─── Client Injection Script ─────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generate the inline script injected into the browser in dev mode.
|
|
141
|
+
*
|
|
142
|
+
* This script:
|
|
143
|
+
* 1. Saves references to the original console methods
|
|
144
|
+
* 2. Wraps `console.error`, `console.warn`, `console.info`
|
|
145
|
+
* 3. Calls the original first (browser devtools still work)
|
|
146
|
+
* 4. Serializes and forwards via `import.meta.hot.send()`
|
|
147
|
+
* 5. Truncates messages to MAX_MESSAGE_BYTES
|
|
148
|
+
*
|
|
149
|
+
* The script is minimal and self-contained — no imports, no dependencies.
|
|
150
|
+
*/
|
|
151
|
+
export function generateClientScript(threshold: BrowserLogLevel | 'none'): string {
|
|
152
|
+
if (threshold === 'none') return '';
|
|
153
|
+
|
|
154
|
+
// Only intercept levels that meet the threshold
|
|
155
|
+
const levels: BrowserLogLevel[] = ['error', 'warn', 'info'].filter((l) =>
|
|
156
|
+
shouldForwardLevel(l as BrowserLogLevel, threshold)
|
|
157
|
+
) as BrowserLogLevel[];
|
|
158
|
+
|
|
159
|
+
if (levels.length === 0) return '';
|
|
160
|
+
|
|
161
|
+
return `
|
|
162
|
+
(function() {
|
|
163
|
+
if (!import.meta.hot) return;
|
|
164
|
+
var MAX_BYTES = ${MAX_MESSAGE_BYTES};
|
|
165
|
+
var levels = ${JSON.stringify(levels)};
|
|
166
|
+
var originals = {};
|
|
167
|
+
|
|
168
|
+
function serialize(arg) {
|
|
169
|
+
if (arg === null) return 'null';
|
|
170
|
+
if (arg === undefined) return 'undefined';
|
|
171
|
+
if (arg instanceof Error) return arg.stack || arg.message || String(arg);
|
|
172
|
+
if (typeof arg === 'object') {
|
|
173
|
+
try { return JSON.stringify(arg); } catch(e) { return String(arg); }
|
|
174
|
+
}
|
|
175
|
+
return String(arg);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function truncate(s) {
|
|
179
|
+
if (s.length <= MAX_BYTES) return s;
|
|
180
|
+
return s.slice(0, MAX_BYTES) + '... [truncated ' + (s.length - MAX_BYTES) + ' bytes]';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
levels.forEach(function(level) {
|
|
184
|
+
originals[level] = console[level];
|
|
185
|
+
console[level] = function() {
|
|
186
|
+
originals[level].apply(console, arguments);
|
|
187
|
+
try {
|
|
188
|
+
var args = Array.prototype.slice.call(arguments);
|
|
189
|
+
var firstArg = args[0];
|
|
190
|
+
var stack = null;
|
|
191
|
+
var source = null;
|
|
192
|
+
|
|
193
|
+
if (firstArg instanceof Error) {
|
|
194
|
+
stack = firstArg.stack || null;
|
|
195
|
+
source = null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
var message = args.map(serialize).join(' ');
|
|
199
|
+
message = truncate(message);
|
|
200
|
+
|
|
201
|
+
import.meta.hot.send('${HMR_EVENT}', {
|
|
202
|
+
level: level,
|
|
203
|
+
message: message,
|
|
204
|
+
stack: stack,
|
|
205
|
+
source: source,
|
|
206
|
+
timestamp: Date.now()
|
|
207
|
+
});
|
|
208
|
+
} catch(e) {
|
|
209
|
+
// Never let log forwarding break the page
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
})();
|
|
214
|
+
`.trim();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─── Plugin ──────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Create the timber-dev-browser-logs Vite plugin.
|
|
221
|
+
*
|
|
222
|
+
* - `configureServer`: Listens for HMR messages and prints them to the terminal
|
|
223
|
+
* - `transformIndexHtml`: Injects the client-side interception script
|
|
224
|
+
*
|
|
225
|
+
* Only active during `vite dev` (apply: 'serve').
|
|
226
|
+
*/
|
|
227
|
+
export function timberDevBrowserLogs(ctx: PluginContext): Plugin {
|
|
228
|
+
return {
|
|
229
|
+
name: 'timber-dev-browser-logs',
|
|
230
|
+
apply: 'serve',
|
|
231
|
+
|
|
232
|
+
configureServer(server: ViteDevServer) {
|
|
233
|
+
const threshold = ctx.config.devBrowserLogs ?? 'warn';
|
|
234
|
+
if (threshold === 'none') return;
|
|
235
|
+
|
|
236
|
+
// Listen for browser log messages via HMR WebSocket
|
|
237
|
+
server.hot.on(HMR_EVENT, (payload: BrowserLogPayload) => {
|
|
238
|
+
try {
|
|
239
|
+
// Validate level
|
|
240
|
+
if (!shouldForwardLevel(payload.level, threshold)) return;
|
|
241
|
+
|
|
242
|
+
// Truncate server-side too (defense in depth)
|
|
243
|
+
payload.message = truncateMessage(payload.message, MAX_MESSAGE_BYTES);
|
|
244
|
+
|
|
245
|
+
const formatted = formatBrowserLog(payload);
|
|
246
|
+
|
|
247
|
+
// Use the correct console method for the log level
|
|
248
|
+
if (payload.level === 'error') {
|
|
249
|
+
process.stderr.write(formatted + '\n');
|
|
250
|
+
} else {
|
|
251
|
+
process.stdout.write(formatted + '\n');
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
// Never let log forwarding crash the server
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
transformIndexHtml() {
|
|
260
|
+
const threshold = ctx.config.devBrowserLogs ?? 'warn';
|
|
261
|
+
const script = generateClientScript(threshold);
|
|
262
|
+
if (!script) return [];
|
|
263
|
+
|
|
264
|
+
return [
|
|
265
|
+
{
|
|
266
|
+
tag: 'script',
|
|
267
|
+
attrs: { type: 'module' },
|
|
268
|
+
children: script,
|
|
269
|
+
injectTo: 'head' as const,
|
|
270
|
+
},
|
|
271
|
+
];
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
24
|
+
import type { DebugComponentEntry } from './rsc-entry/helpers.js';
|
|
24
25
|
|
|
25
26
|
// ─── Request Context ──────────────────────────────────────────────────────
|
|
26
27
|
// Used by: request-context.ts (headers(), cookies(), searchParams())
|
|
@@ -64,6 +65,12 @@ export interface RequestContextStore {
|
|
|
64
65
|
flushed: boolean;
|
|
65
66
|
/** Whether the current context allows cookie mutation. */
|
|
66
67
|
mutableContext: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Dev-only: getter for the current request's RSC debug components.
|
|
70
|
+
* Set by renderRoute() so onPipelineError can include component tree
|
|
71
|
+
* context for render-phase errors without module-level shared state.
|
|
72
|
+
*/
|
|
73
|
+
debugComponentsGetter?: () => DebugComponentEntry[];
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
/** A single outgoing cookie entry in the cookie jar. */
|
|
@@ -29,6 +29,7 @@ import type { NavContext } from './ssr-entry.js';
|
|
|
29
29
|
import { flightInitScript } from './flight-scripts.js';
|
|
30
30
|
import type { ClientBootstrapConfig } from './html-injectors.js';
|
|
31
31
|
import type { Metadata } from './types.js';
|
|
32
|
+
import { teeWithErrorPropagation } from './stream-utils.js';
|
|
32
33
|
|
|
33
34
|
/** RSC content type for client navigation payload requests. */
|
|
34
35
|
const RSC_CONTENT_TYPE = 'text/x-component';
|
|
@@ -171,7 +172,7 @@ export async function renderDenyPage(
|
|
|
171
172
|
debugChannel: createDebugChannelSink(),
|
|
172
173
|
});
|
|
173
174
|
|
|
174
|
-
const [ssrStream, inlineStream] = rscStream
|
|
175
|
+
const [ssrStream, inlineStream] = teeWithErrorPropagation(rscStream);
|
|
175
176
|
|
|
176
177
|
const navContext: NavContext = {
|
|
177
178
|
pathname: new URL(req.url).pathname,
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { requestContextAls, type RequestContextStore, type CookieEntry } from './als-registry.js';
|
|
14
14
|
import { isDebug } from './debug.js';
|
|
15
15
|
import { _setRawSearchParamsFn } from '#/search-params/define.js';
|
|
16
|
+
import { _setRawSegmentParamsFn } from '#/params/define.js';
|
|
16
17
|
|
|
17
18
|
// Re-export the ALS for framework-internal consumers that need direct access.
|
|
18
19
|
export { requestContextAls };
|
|
@@ -185,6 +186,11 @@ export function rawSearchParams(): Promise<URLSearchParams> {
|
|
|
185
186
|
// breaking rawSearchParams() in parallel slot pages. See TIM-523.
|
|
186
187
|
_setRawSearchParamsFn(rawSearchParams);
|
|
187
188
|
|
|
189
|
+
// Eagerly register rawSegmentParams with the params module so
|
|
190
|
+
// segmentParams.load() can call it synchronously without a dynamic import.
|
|
191
|
+
// Same pattern as search params — dynamic imports lose ALS context. See TIM-523.
|
|
192
|
+
_setRawSegmentParamsFn(rawSegmentParams);
|
|
193
|
+
|
|
188
194
|
/**
|
|
189
195
|
* Returns a Promise resolving to the current request's coerced segment params.
|
|
190
196
|
*
|