@timber-js/app 0.2.0-alpha.203 → 0.2.0-alpha.205
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-HUdJADAD.js → actions-jNrXdzgs.js} +4 -3
- package/dist/_chunks/{actions-HUdJADAD.js.map → actions-jNrXdzgs.js.map} +1 -1
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
- package/dist/_chunks/{build-manifest-DWppEdLB.js → build-manifest-DTmSGLRz.js} +51 -2
- package/dist/_chunks/build-manifest-DTmSGLRz.js.map +1 -0
- package/dist/_chunks/{cache-api-CAPbZTga.js → cache-api-CUD9Ezaq.js} +3 -2
- package/dist/_chunks/{cache-api-CAPbZTga.js.map → cache-api-CUD9Ezaq.js.map} +1 -1
- package/dist/_chunks/{chains-CBNA0Ozj.js → chains-BjGKq9Je.js} +7 -6
- package/dist/_chunks/chains-BjGKq9Je.js.map +1 -0
- package/dist/_chunks/{cli-check-BzMGuIH6.js → cli-check-3WE_GyoL.js} +3 -3
- package/dist/_chunks/{cli-check-BzMGuIH6.js.map → cli-check-3WE_GyoL.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-DnXqcIIj.js → cli-schema-sync-D3iIZjku.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-DnXqcIIj.js.map → cli-schema-sync-D3iIZjku.js.map} +1 -1
- package/dist/_chunks/{cloudflare-DxX1SU0g.js → cloudflare-BFb__LYG.js} +3 -3
- package/dist/_chunks/{cloudflare-DxX1SU0g.js.map → cloudflare-BFb__LYG.js.map} +1 -1
- package/dist/_chunks/{convention-lint-DHOFvX5s.js → convention-lint-BnO5TyHD.js} +2 -2
- package/dist/_chunks/{convention-lint-DHOFvX5s.js.map → convention-lint-BnO5TyHD.js.map} +1 -1
- package/dist/_chunks/{dev-server-DioP7tkQ.js → dev-server-Dbdgx2dD.js} +8 -195
- package/dist/_chunks/dev-server-Dbdgx2dD.js.map +1 -0
- package/dist/_chunks/{error-boundary-BQKxl6EX.js → error-boundary-BndF-3Td.js} +2 -2
- package/dist/_chunks/{error-boundary-BQKxl6EX.js.map → error-boundary-BndF-3Td.js.map} +1 -1
- package/dist/_chunks/{graph-cache-Cv3njEH8.js → graph-cache-CP4GEmf9.js} +2 -2
- package/dist/_chunks/{graph-cache-Cv3njEH8.js.map → graph-cache-CP4GEmf9.js.map} +1 -1
- package/dist/_chunks/{live-graph-VjHFF5EV.js → live-graph-BM2UbbNx.js} +3 -3
- package/dist/_chunks/{live-graph-VjHFF5EV.js.map → live-graph-BM2UbbNx.js.map} +1 -1
- package/dist/_chunks/logger-k2DQ4EUf.js +506 -0
- package/dist/_chunks/logger-k2DQ4EUf.js.map +1 -0
- package/dist/_chunks/{poison-scan-lEbz4pQE.js → poison-scan-g83aAXhs.js} +2 -2
- package/dist/_chunks/{poison-scan-lEbz4pQE.js.map → poison-scan-g83aAXhs.js.map} +1 -1
- package/dist/_chunks/{logger-DqJ2VoAY.js → primitives-DLAnvsrk.js} +17 -518
- package/dist/_chunks/primitives-DLAnvsrk.js.map +1 -0
- package/dist/_chunks/{scanner-B_tnqFcF.js → scanner-BhNruzv0.js} +3 -3
- package/dist/_chunks/{scanner-B_tnqFcF.js.map → scanner-BhNruzv0.js.map} +1 -1
- package/dist/_chunks/{walkers-Cm3PC5JT.js → walkers-BNsswm-h.js} +2 -2
- package/dist/_chunks/{walkers-Cm3PC5JT.js.map → walkers-BNsswm-h.js.map} +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/analyze/classify.d.ts +5 -4
- package/dist/analyze/classify.d.ts.map +1 -1
- package/dist/analyze/crawl-entry.js +2 -2
- package/dist/analyze/graph-command.js +3 -3
- package/dist/cache/index.js +1 -1
- package/dist/cli.js +3 -3
- package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/internal.js +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/overlay.d.ts +1 -21
- package/dist/dev-tools/overlay.d.ts.map +1 -1
- package/dist/index.js +6 -7
- package/dist/index.js.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/server/als-registry.d.ts +0 -7
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +2 -6
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/index.js +3 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.js +180 -7
- package/dist/server/internal.js.map +1 -1
- package/dist/server/pipeline.d.ts +1 -13
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +0 -38
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts +0 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +1 -7
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +0 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/slot-subtree-contain.d.ts +1 -1
- package/dist/server/slot-subtree-contain.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/analyze/classify.ts +5 -4
- package/src/client/browser-entry/rsc-stream.ts +1 -82
- package/src/dev-tools/index.ts +0 -2
- package/src/dev-tools/overlay.ts +1 -65
- package/src/plugins/dev-server.ts +4 -45
- package/src/server/als-registry.ts +0 -8
- package/src/server/deny-renderer.ts +0 -7
- package/src/server/pipeline.ts +1 -14
- package/src/server/rsc-entry/deny-fallback.ts +1 -10
- package/src/server/rsc-entry/error-renderer.ts +1 -11
- package/src/server/rsc-entry/helpers.ts +0 -92
- package/src/server/rsc-entry/index.ts +1 -14
- package/src/server/rsc-entry/render-route.ts +2 -23
- package/src/server/rsc-entry/rsc-payload.ts +1 -2
- package/src/server/rsc-entry/rsc-stream.ts +3 -29
- package/src/server/rsc-entry/ssr-renderer.ts +2 -15
- package/src/server/slot-resolver.ts +7 -29
- package/src/server/slot-subtree-contain.ts +22 -14
- package/dist/_chunks/build-manifest-DWppEdLB.js.map +0 -1
- package/dist/_chunks/chains-CBNA0Ozj.js.map +0 -1
- package/dist/_chunks/csp-nonce-hOGniaG4.js +0 -227
- package/dist/_chunks/csp-nonce-hOGniaG4.js.map +0 -1
- package/dist/_chunks/dev-server-DioP7tkQ.js.map +0 -1
- package/dist/_chunks/logger-DqJ2VoAY.js.map +0 -1
- package/dist/dev-tools/debug-channel.d.ts +0 -55
- package/dist/dev-tools/debug-channel.d.ts.map +0 -1
- package/src/dev-tools/debug-channel.ts +0 -151
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
import { o as traceAls } from "./als-registry-C6kcfprT.js";
|
|
2
|
+
import { E as isDevMode, T as isDebug, a as isControlFlowSignal } from "./primitives-DLAnvsrk.js";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
//#region src/server/tracing.ts
|
|
5
|
+
/**
|
|
6
|
+
* Tracing — per-request trace ID via AsyncLocalStorage, OTEL span helpers.
|
|
7
|
+
*
|
|
8
|
+
* getTraceId() is always available in server code (middleware, access, components, actions).
|
|
9
|
+
* Returns a 32-char lowercase hex string — the OTEL trace ID when an SDK is active,
|
|
10
|
+
* or a crypto.randomUUID()-derived fallback otherwise.
|
|
11
|
+
*
|
|
12
|
+
* See design/17-logging.md §"trace_id is Always Set"
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Returns the current request's trace ID — always a 32-char lowercase hex string.
|
|
16
|
+
*
|
|
17
|
+
* With OTEL: the real OTEL trace ID (matches Jaeger/Honeycomb/Datadog).
|
|
18
|
+
* Without OTEL: crypto.randomUUID() with hyphens stripped.
|
|
19
|
+
*
|
|
20
|
+
* Throws if called outside a request context (no ALS store).
|
|
21
|
+
*/
|
|
22
|
+
function getTraceId() {
|
|
23
|
+
const store = traceAls.getStore();
|
|
24
|
+
if (!store) throw new Error("[timber] getTraceId() called outside of a request context. It can only be used in middleware, access checks, server components, and server actions.");
|
|
25
|
+
return store.traceId;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Returns the current OTEL span ID if available, undefined otherwise.
|
|
29
|
+
*/
|
|
30
|
+
function getSpanId() {
|
|
31
|
+
return traceAls.getStore()?.spanId;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Generate a 32-char lowercase hex ID from crypto.randomUUID().
|
|
35
|
+
* Same format as OTEL trace IDs — zero-friction upgrade path.
|
|
36
|
+
*/
|
|
37
|
+
function generateTraceId() {
|
|
38
|
+
return randomUUID().replace(/-/g, "");
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Run a callback within a trace context. Used by the pipeline to establish
|
|
42
|
+
* per-request ALS scope.
|
|
43
|
+
*/
|
|
44
|
+
function runWithTraceId(id, fn) {
|
|
45
|
+
return traceAls.run({ traceId: id }, fn);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Replace the trace ID in the current ALS store. Used when OTEL creates
|
|
49
|
+
* a root span and we want to switch from the UUID fallback to the real
|
|
50
|
+
* OTEL trace ID.
|
|
51
|
+
*/
|
|
52
|
+
function replaceTraceId(newTraceId, newSpanId) {
|
|
53
|
+
const store = traceAls.getStore();
|
|
54
|
+
if (store) {
|
|
55
|
+
store.traceId = newTraceId;
|
|
56
|
+
store.spanId = newSpanId;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Update the span ID in the current ALS store. Used when entering a new
|
|
61
|
+
* OTEL span to keep log–trace correlation accurate.
|
|
62
|
+
*/
|
|
63
|
+
function updateSpanId(newSpanId) {
|
|
64
|
+
const store = traceAls.getStore();
|
|
65
|
+
if (store) store.spanId = newSpanId;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get the current trace store, or undefined if outside a request context.
|
|
69
|
+
* Framework-internal — use getTraceId()/getSpanId() in user code.
|
|
70
|
+
*/
|
|
71
|
+
function getTraceStore() {
|
|
72
|
+
return traceAls.getStore();
|
|
73
|
+
}
|
|
74
|
+
var PLATFORM_TRACER_KEY = Symbol.for("timber:platform-tracer");
|
|
75
|
+
/** The registered native platform tracer, if any. */
|
|
76
|
+
function getPlatformTracer() {
|
|
77
|
+
return globalThis[PLATFORM_TRACER_KEY];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Attempt to get the @opentelemetry/api tracer. Returns undefined if the
|
|
81
|
+
* package is not installed or no SDK is registered.
|
|
82
|
+
*
|
|
83
|
+
* timber.js depends on @opentelemetry/api as the vendor-neutral interface.
|
|
84
|
+
* The API is a no-op by default — spans are only emitted when the developer
|
|
85
|
+
* initializes an SDK in register().
|
|
86
|
+
*/
|
|
87
|
+
var _otelApi;
|
|
88
|
+
async function getOtelApi() {
|
|
89
|
+
if (_otelApi === void 0) try {
|
|
90
|
+
_otelApi = await import("@opentelemetry/api");
|
|
91
|
+
} catch {
|
|
92
|
+
_otelApi = null;
|
|
93
|
+
}
|
|
94
|
+
return _otelApi;
|
|
95
|
+
}
|
|
96
|
+
/** OTEL tracer instance, lazily created. */
|
|
97
|
+
var _tracer;
|
|
98
|
+
/**
|
|
99
|
+
* Get the timber.js OTEL tracer. Returns null if @opentelemetry/api is not available.
|
|
100
|
+
*/
|
|
101
|
+
async function getTracer() {
|
|
102
|
+
if (_tracer === void 0) {
|
|
103
|
+
const api = await getOtelApi();
|
|
104
|
+
if (api) _tracer = api.trace.getTracer("timber.js");
|
|
105
|
+
else _tracer = null;
|
|
106
|
+
}
|
|
107
|
+
return _tracer;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Run a function within a framework span. Composes two emission channels:
|
|
111
|
+
*
|
|
112
|
+
* - **Native platform span** — when an adapter registered a PlatformTracer
|
|
113
|
+
* (Cloudflare Workers), the fn is wrapped in platformTracer.enterSpan()
|
|
114
|
+
* so it appears in the platform's native trace view.
|
|
115
|
+
* - **OTEL span** — when an OTEL SDK is active, the fn also runs inside an
|
|
116
|
+
* OTEL span (dual emission). No SDK and no platform tracer = zero overhead.
|
|
117
|
+
*
|
|
118
|
+
* Automatically:
|
|
119
|
+
* - Creates the span as a child of the current context
|
|
120
|
+
* - Updates the ALS span ID for log–trace correlation
|
|
121
|
+
* - Ends the span when the function completes
|
|
122
|
+
* - Records exceptions on error (OTEL channel)
|
|
123
|
+
*/
|
|
124
|
+
async function withSpan(name, attributes, fn) {
|
|
125
|
+
const platformTracer = getPlatformTracer();
|
|
126
|
+
if (!platformTracer) return runOtelSpan(name, attributes, fn);
|
|
127
|
+
return platformTracer.enterSpan(name, async (span) => {
|
|
128
|
+
for (const key of Object.keys(attributes)) span.setAttribute(key, attributes[key]);
|
|
129
|
+
const store = traceAls.getStore();
|
|
130
|
+
const prevPlatformSpan = store?.platformSpan;
|
|
131
|
+
if (store) store.platformSpan = span;
|
|
132
|
+
try {
|
|
133
|
+
return await runOtelSpan(name, attributes, fn);
|
|
134
|
+
} finally {
|
|
135
|
+
if (store) store.platformSpan = prevPlatformSpan;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/** The OTEL half of withSpan() — no-op passthrough when no SDK is active. */
|
|
140
|
+
async function runOtelSpan(name, attributes, fn) {
|
|
141
|
+
const tracer = await getTracer();
|
|
142
|
+
if (!tracer) return fn();
|
|
143
|
+
const api = await getOtelApi();
|
|
144
|
+
return tracer.startActiveSpan(name, { attributes }, async (span) => {
|
|
145
|
+
const prevSpanId = getSpanId();
|
|
146
|
+
updateSpanId(span.spanContext().spanId);
|
|
147
|
+
try {
|
|
148
|
+
const result = await fn();
|
|
149
|
+
span.setStatus({ code: api.SpanStatusCode.OK });
|
|
150
|
+
return result;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR });
|
|
153
|
+
if (error instanceof Error) span.recordException(error);
|
|
154
|
+
throw error;
|
|
155
|
+
} finally {
|
|
156
|
+
span.end();
|
|
157
|
+
updateSpanId(prevSpanId);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Set an attribute on the current active span (if any).
|
|
163
|
+
* Used for setting span attributes after span creation (e.g. timber.result on access spans).
|
|
164
|
+
*/
|
|
165
|
+
async function setSpanAttribute(key, value) {
|
|
166
|
+
const platformSpan = traceAls.getStore()?.platformSpan;
|
|
167
|
+
if (platformSpan) platformSpan.setAttribute(key, value);
|
|
168
|
+
const api = await getOtelApi();
|
|
169
|
+
if (!api) return;
|
|
170
|
+
const activeSpan = api.trace.getActiveSpan();
|
|
171
|
+
if (activeSpan) activeSpan.setAttribute(key, value);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Add a span event to the current active span (if any).
|
|
175
|
+
* Used for timber.cache HIT/MISS events — recorded as span events, not child spans.
|
|
176
|
+
*/
|
|
177
|
+
async function addSpanEvent(name, attributes) {
|
|
178
|
+
const api = await getOtelApi();
|
|
179
|
+
if (!api) return;
|
|
180
|
+
const activeSpan = api.trace.getActiveSpan();
|
|
181
|
+
if (activeSpan) activeSpan.addEvent(name, attributes);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Fire-and-forget span event — no await, no microtask overhead.
|
|
185
|
+
*
|
|
186
|
+
* Used on the cache hot path where awaiting addSpanEvent creates an
|
|
187
|
+
* unnecessary microtask per cache operation. If OTEL is not loaded yet,
|
|
188
|
+
* the event is silently dropped (acceptable for diagnostics).
|
|
189
|
+
*
|
|
190
|
+
* See TIM-370 for perf motivation.
|
|
191
|
+
*/
|
|
192
|
+
function addSpanEventSync(name, attributes) {
|
|
193
|
+
if (!_otelApi) return;
|
|
194
|
+
const activeSpan = _otelApi.trace.getActiveSpan();
|
|
195
|
+
if (activeSpan) activeSpan.addEvent(name, attributes);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Try to extract the OTEL trace ID from the current active span context.
|
|
199
|
+
* Returns undefined if OTEL is not active or no span exists.
|
|
200
|
+
*/
|
|
201
|
+
async function getOtelTraceId() {
|
|
202
|
+
const api = await getOtelApi();
|
|
203
|
+
if (!api) return void 0;
|
|
204
|
+
const activeSpan = api.trace.getActiveSpan();
|
|
205
|
+
if (!activeSpan) return void 0;
|
|
206
|
+
const ctx = activeSpan.spanContext();
|
|
207
|
+
if (!ctx.traceId || ctx.traceId === "00000000000000000000000000000000") return;
|
|
208
|
+
return {
|
|
209
|
+
traceId: ctx.traceId,
|
|
210
|
+
spanId: ctx.spanId
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region src/server/error-formatter.ts
|
|
215
|
+
/**
|
|
216
|
+
* Error Formatter — rewrites SSR/RSC error messages to surface user code.
|
|
217
|
+
*
|
|
218
|
+
* When React or Vite throw errors during SSR, stack traces reference
|
|
219
|
+
* vendored dependency paths (e.g. `.vite/deps_ssr/@vitejs_plugin-rsc_vendor_...`)
|
|
220
|
+
* and mangled export names (`__vite_ssr_export_default__`). This module
|
|
221
|
+
* rewrites error messages and stack traces to point at user code instead.
|
|
222
|
+
*
|
|
223
|
+
* Dev-only — in production, errors go through the structured logger
|
|
224
|
+
* without formatting.
|
|
225
|
+
*/
|
|
226
|
+
/**
|
|
227
|
+
* Patterns that identify internal Vite/RSC vendor paths in stack traces.
|
|
228
|
+
* These are replaced with human-readable labels.
|
|
229
|
+
*/
|
|
230
|
+
var VENDOR_PATH_PATTERNS = [
|
|
231
|
+
{
|
|
232
|
+
pattern: /node_modules\/\.vite\/deps_ssr\/@vitejs_plugin-rsc_vendor_react-server-dom[^\s)]+/g,
|
|
233
|
+
replacement: "<react-server-dom>"
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
pattern: /node_modules\/\.vite\/deps_ssr\/@vitejs_plugin-rsc_vendor[^\s)]+/g,
|
|
237
|
+
replacement: "<rsc-vendor>"
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
pattern: /node_modules\/\.vite\/deps_ssr\/[^\s)]+/g,
|
|
241
|
+
replacement: "<vite-dep>"
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
pattern: /node_modules\/\.vite\/deps\/[^\s)]+/g,
|
|
245
|
+
replacement: "<vite-dep>"
|
|
246
|
+
}
|
|
247
|
+
];
|
|
248
|
+
/**
|
|
249
|
+
* Patterns that identify Vite-mangled export names in error messages.
|
|
250
|
+
*/
|
|
251
|
+
var MANGLED_NAME_PATTERNS = [{
|
|
252
|
+
pattern: /__vite_ssr_export_default__/g,
|
|
253
|
+
replacement: "<default export>"
|
|
254
|
+
}, {
|
|
255
|
+
pattern: /__vite_ssr_export_(\w+)__/g,
|
|
256
|
+
replacement: "<export $1>"
|
|
257
|
+
}];
|
|
258
|
+
/**
|
|
259
|
+
* Rewrite an error's message and stack to replace internal Vite paths
|
|
260
|
+
* and mangled names with human-readable labels.
|
|
261
|
+
*/
|
|
262
|
+
function formatSsrError(error) {
|
|
263
|
+
if (!(error instanceof Error)) return String(error);
|
|
264
|
+
let message = error.message;
|
|
265
|
+
let stack = error.stack ?? "";
|
|
266
|
+
for (const { pattern, replacement } of MANGLED_NAME_PATTERNS) message = message.replace(pattern, replacement);
|
|
267
|
+
for (const { pattern, replacement } of VENDOR_PATH_PATTERNS) stack = stack.replace(pattern, replacement);
|
|
268
|
+
for (const { pattern, replacement } of MANGLED_NAME_PATTERNS) stack = stack.replace(pattern, replacement);
|
|
269
|
+
const hint = extractErrorHint(error.message);
|
|
270
|
+
const parts = [];
|
|
271
|
+
parts.push(message);
|
|
272
|
+
if (hint) parts.push(` → ${hint}`);
|
|
273
|
+
const userFrames = extractUserFrames(stack);
|
|
274
|
+
if (userFrames.length > 0) {
|
|
275
|
+
parts.push("");
|
|
276
|
+
parts.push(" User code in stack:");
|
|
277
|
+
for (const frame of userFrames) parts.push(` ${frame}`);
|
|
278
|
+
}
|
|
279
|
+
return parts.join("\n");
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Extract a human-readable hint from common React/RSC error messages.
|
|
283
|
+
*
|
|
284
|
+
* React error messages contain useful information but the surrounding
|
|
285
|
+
* context (vendor paths, mangled names) obscures it. This extracts the
|
|
286
|
+
* actionable part as a one-line hint.
|
|
287
|
+
*/
|
|
288
|
+
function extractErrorHint(message) {
|
|
289
|
+
if (message.match(/Functions cannot be passed directly to Client Components/)) {
|
|
290
|
+
const propMatch = message.match(/<[^>]*?\s(\w+)=\{function/);
|
|
291
|
+
if (propMatch) return `Prop "${propMatch[1]}" is a function — mark it "use server" or call it before passing`;
|
|
292
|
+
return "A function prop was passed to a Client Component — mark it \"use server\" or call it before passing";
|
|
293
|
+
}
|
|
294
|
+
if (message.includes("Objects are not valid as a React child")) return "An object was rendered as JSX children — convert to string or extract the value";
|
|
295
|
+
const nullRefMatch = message.match(/Cannot read propert(?:y|ies) of (undefined|null) \(reading '(\w+)'\)/);
|
|
296
|
+
if (nullRefMatch) return `Accessed .${nullRefMatch[2]} on ${nullRefMatch[1]} — check that the value exists`;
|
|
297
|
+
const notFnMatch = message.match(/(\w+) is not a function/);
|
|
298
|
+
if (notFnMatch) return `"${notFnMatch[1]}" is not a function — check imports and exports`;
|
|
299
|
+
if (message.includes("Element type is invalid")) return "A component resolved to undefined/null — check default exports and import paths";
|
|
300
|
+
if (message.includes("Invalid hook call")) return "A hook was called outside of a React component render. If this is a 'use client' component, ensure the directive is at the very top of the file (before any imports) and that @vitejs/plugin-rsc is loaded correctly. Barrel re-exports from non-'use client' files do not propagate the directive.";
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Extract stack frames that reference user code (not node_modules,
|
|
305
|
+
* not framework internals).
|
|
306
|
+
*
|
|
307
|
+
* Returns at most 5 frames to keep output concise.
|
|
308
|
+
*/
|
|
309
|
+
function extractUserFrames(stack) {
|
|
310
|
+
const lines = stack.split("\n");
|
|
311
|
+
const userFrames = [];
|
|
312
|
+
for (const line of lines) {
|
|
313
|
+
const trimmed = line.trim();
|
|
314
|
+
if (!trimmed.startsWith("at ")) continue;
|
|
315
|
+
if (trimmed.includes("node_modules") || trimmed.includes("<react-server-dom>") || trimmed.includes("<rsc-vendor>") || trimmed.includes("<vite-dep>") || trimmed.includes("node:internal")) continue;
|
|
316
|
+
userFrames.push(trimmed);
|
|
317
|
+
if (userFrames.length >= 5) break;
|
|
318
|
+
}
|
|
319
|
+
return userFrames;
|
|
320
|
+
}
|
|
321
|
+
//#endregion
|
|
322
|
+
//#region src/server/default-logger.ts
|
|
323
|
+
/**
|
|
324
|
+
* DefaultLogger — human-readable stderr logging when no custom logger is configured.
|
|
325
|
+
*
|
|
326
|
+
* Ships as the fallback so production deployments always have error visibility,
|
|
327
|
+
* even without an `instrumentation.ts` logger export. Output is one line per
|
|
328
|
+
* event, designed for `fly logs`, `kubectl logs`, Cloudflare dashboard tails, etc.
|
|
329
|
+
*
|
|
330
|
+
* Format:
|
|
331
|
+
* [timber] ERROR message key=value key=value trace_id=4bf92f35
|
|
332
|
+
* [timber] WARN message key=value key=value trace_id=4bf92f35
|
|
333
|
+
* [timber] INFO message method=GET path=/dashboard status=200 durationMs=43 trace_id=4bf92f35
|
|
334
|
+
*
|
|
335
|
+
* Behavior:
|
|
336
|
+
* - Suppressed entirely in dev mode (dev logging handles all output)
|
|
337
|
+
* - `debug` suppressed unless TIMBER_DEBUG is set
|
|
338
|
+
* - Replaced entirely when a custom logger is set via `setLogger()`
|
|
339
|
+
*
|
|
340
|
+
* See design/17-logging.md §"DefaultLogger"
|
|
341
|
+
*/
|
|
342
|
+
/**
|
|
343
|
+
* Format data fields as `key=value` pairs for human-readable output.
|
|
344
|
+
* - `error` key is serialized via formatSsrError for stack trace cleanup
|
|
345
|
+
* - `trace_id` is truncated to 8 chars for readability (full ID in OTEL)
|
|
346
|
+
* - Other values are stringified inline
|
|
347
|
+
*/
|
|
348
|
+
function formatDataFields(data) {
|
|
349
|
+
if (!data) return "";
|
|
350
|
+
const parts = [];
|
|
351
|
+
let traceId;
|
|
352
|
+
for (const [key, value] of Object.entries(data)) {
|
|
353
|
+
if (key === "trace_id") {
|
|
354
|
+
traceId = typeof value === "string" ? value : String(value);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (key === "error") {
|
|
358
|
+
parts.push(`error=${formatSsrError(value)}`);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (value === void 0 || value === null) continue;
|
|
362
|
+
parts.push(`${key}=${value}`);
|
|
363
|
+
}
|
|
364
|
+
if (traceId) parts.push(`trace_id=${traceId.slice(0, 8)}`);
|
|
365
|
+
return parts.length > 0 ? " " + parts.join(" ") : "";
|
|
366
|
+
}
|
|
367
|
+
/** Pad level string to fixed width for alignment. */
|
|
368
|
+
function padLevel(level) {
|
|
369
|
+
return level.padEnd(5);
|
|
370
|
+
}
|
|
371
|
+
function createDefaultLogger() {
|
|
372
|
+
return {
|
|
373
|
+
error(msg, data) {
|
|
374
|
+
const fields = formatDataFields(data);
|
|
375
|
+
process.stderr.write(`[timber] ${padLevel("ERROR")} ${msg}${fields}\n`);
|
|
376
|
+
},
|
|
377
|
+
warn(msg, data) {
|
|
378
|
+
const fields = formatDataFields(data);
|
|
379
|
+
process.stderr.write(`[timber] ${padLevel("WARN")} ${msg}${fields}\n`);
|
|
380
|
+
},
|
|
381
|
+
info(msg, data) {
|
|
382
|
+
if (isDevMode()) return;
|
|
383
|
+
if (!isDebug()) return;
|
|
384
|
+
const fields = formatDataFields(data);
|
|
385
|
+
process.stderr.write(`[timber] ${padLevel("INFO")} ${msg}${fields}\n`);
|
|
386
|
+
},
|
|
387
|
+
debug(msg, data) {
|
|
388
|
+
if (isDevMode()) return;
|
|
389
|
+
if (!isDebug()) return;
|
|
390
|
+
const fields = formatDataFields(data);
|
|
391
|
+
process.stderr.write(`[timber] ${padLevel("DEBUG")} ${msg}${fields}\n`);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
//#endregion
|
|
396
|
+
//#region src/server/logger.ts
|
|
397
|
+
/**
|
|
398
|
+
* Logger — structured logging with environment-aware formatting.
|
|
399
|
+
*
|
|
400
|
+
* timber.js ships a DefaultLogger that writes human-readable lines to stderr
|
|
401
|
+
* in production. Users can export a custom logger from instrumentation.ts to
|
|
402
|
+
* replace it with pino, winston, or any TimberLogger-compatible object.
|
|
403
|
+
*
|
|
404
|
+
* See design/17-logging.md §"Production Logging"
|
|
405
|
+
*/
|
|
406
|
+
var _logger = createDefaultLogger();
|
|
407
|
+
/**
|
|
408
|
+
* Set the user-provided logger. Called by the instrumentation loader
|
|
409
|
+
* when it finds a `logger` export in instrumentation.ts. Replaces
|
|
410
|
+
* the DefaultLogger entirely.
|
|
411
|
+
*/
|
|
412
|
+
function setLogger(logger) {
|
|
413
|
+
_logger = logger;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Get the current logger. Always non-null — returns DefaultLogger when
|
|
417
|
+
* no custom logger is configured.
|
|
418
|
+
*/
|
|
419
|
+
function getLogger() {
|
|
420
|
+
return _logger;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Inject trace_id and span_id into log data for log–trace correlation.
|
|
424
|
+
* Always injects trace_id (never undefined). Injects span_id only when OTEL is active.
|
|
425
|
+
*/
|
|
426
|
+
function withTraceContext(data) {
|
|
427
|
+
const store = getTraceStore();
|
|
428
|
+
const enriched = { ...data };
|
|
429
|
+
if (store) {
|
|
430
|
+
enriched.trace_id = store.traceId;
|
|
431
|
+
if (store.spanId) enriched.span_id = store.spanId;
|
|
432
|
+
}
|
|
433
|
+
return enriched;
|
|
434
|
+
}
|
|
435
|
+
/** Log a completed request. Level: info. */
|
|
436
|
+
function logRequestCompleted(data) {
|
|
437
|
+
_logger.info("request completed", withTraceContext(data));
|
|
438
|
+
}
|
|
439
|
+
/** Log request received. Level: debug. */
|
|
440
|
+
function logRequestReceived(data) {
|
|
441
|
+
_logger.debug("request received", withTraceContext(data));
|
|
442
|
+
}
|
|
443
|
+
/** Log a slow request warning. Level: warn. */
|
|
444
|
+
function logSlowRequest(data) {
|
|
445
|
+
_logger.warn("slow request exceeded threshold", withTraceContext(data));
|
|
446
|
+
}
|
|
447
|
+
/** Log middleware short-circuit. Level: debug. */
|
|
448
|
+
function logMiddlewareShortCircuit(data) {
|
|
449
|
+
_logger.debug("middleware short-circuited", withTraceContext(data));
|
|
450
|
+
}
|
|
451
|
+
/** Log unhandled error in middleware phase. Level: error. */
|
|
452
|
+
function logMiddlewareError(data) {
|
|
453
|
+
if (isControlFlowSignal(data.error)) return;
|
|
454
|
+
_logger.error("unhandled error in middleware phase", withTraceContext(data));
|
|
455
|
+
}
|
|
456
|
+
/** Log unhandled render-phase error. Level: error. */
|
|
457
|
+
function logRenderError(data) {
|
|
458
|
+
if (isControlFlowSignal(data.error)) return;
|
|
459
|
+
_logger.error("unhandled render-phase error", withTraceContext(data));
|
|
460
|
+
}
|
|
461
|
+
/** Log proxy.ts uncaught error. Level: error. */
|
|
462
|
+
function logProxyError(data) {
|
|
463
|
+
_logger.error("proxy.ts threw uncaught error", withTraceContext(data));
|
|
464
|
+
}
|
|
465
|
+
/** Log unhandled error in route handler. Level: error. */
|
|
466
|
+
function logRouteError(data) {
|
|
467
|
+
if (isControlFlowSignal(data.error)) return;
|
|
468
|
+
_logger.error("unhandled route handler error", withTraceContext(data));
|
|
469
|
+
}
|
|
470
|
+
/** Log waitUntil() adapter missing (once at startup). Level: warn. */
|
|
471
|
+
function logWaitUntilUnsupported() {
|
|
472
|
+
_logger.warn("adapter does not support waitUntil()");
|
|
473
|
+
}
|
|
474
|
+
/** Log waitUntil() promise rejection. Level: warn. */
|
|
475
|
+
function logWaitUntilRejected(data) {
|
|
476
|
+
_logger.warn("waitUntil() promise rejected", withTraceContext(data));
|
|
477
|
+
}
|
|
478
|
+
/** Log staleWhileRevalidate refetch failure. Level: warn. */
|
|
479
|
+
function logSwrRefetchFailed(data) {
|
|
480
|
+
_logger.warn("staleWhileRevalidate refetch failed", withTraceContext(data));
|
|
481
|
+
}
|
|
482
|
+
/** Log cache miss. Level: debug. */
|
|
483
|
+
function logCacheMiss(data) {
|
|
484
|
+
_logger.debug("timber.cache MISS", withTraceContext(data));
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Log an intentionally swallowed error. Provides observability into catch
|
|
488
|
+
* blocks that are deliberately empty — the error is consumed, never rethrown.
|
|
489
|
+
*
|
|
490
|
+
* Default level: `warn` in dev (so the overlay surfaces patterns), `debug`
|
|
491
|
+
* in production (low noise unless TIMBER_DEBUG is set). Pass `opts.level`
|
|
492
|
+
* to override.
|
|
493
|
+
*
|
|
494
|
+
* **Infallible** — swallow() itself never throws, even if the logger is
|
|
495
|
+
* broken. A thrown swallow would turn a benign catch into a crash.
|
|
496
|
+
*/
|
|
497
|
+
function swallow(err, reason, opts) {
|
|
498
|
+
try {
|
|
499
|
+
const level = opts?.level ?? (isDevMode() ? "warn" : "debug");
|
|
500
|
+
_logger[level](`swallowed: ${reason}`, withTraceContext({ error: err }));
|
|
501
|
+
} catch {}
|
|
502
|
+
}
|
|
503
|
+
//#endregion
|
|
504
|
+
export { runWithTraceId as C, replaceTraceId as S, withSpan as T, addSpanEventSync as _, logProxyError as a, getSpanId as b, logRequestReceived as c, logSwrRefetchFailed as d, logWaitUntilRejected as f, addSpanEvent as g, swallow as h, logMiddlewareShortCircuit as i, logRouteError as l, setLogger as m, logCacheMiss as n, logRenderError as o, logWaitUntilUnsupported as p, logMiddlewareError as r, logRequestCompleted as s, getLogger as t, logSlowRequest as u, generateTraceId as v, setSpanAttribute as w, getTraceId as x, getOtelTraceId as y };
|
|
505
|
+
|
|
506
|
+
//# sourceMappingURL=logger-k2DQ4EUf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger-k2DQ4EUf.js","names":[],"sources":["../../src/server/tracing.ts","../../src/server/error-formatter.ts","../../src/server/default-logger.ts","../../src/server/logger.ts"],"sourcesContent":["/**\n * Tracing — per-request trace ID via AsyncLocalStorage, OTEL span helpers.\n *\n * getTraceId() is always available in server code (middleware, access, components, actions).\n * Returns a 32-char lowercase hex string — the OTEL trace ID when an SDK is active,\n * or a crypto.randomUUID()-derived fallback otherwise.\n *\n * See design/17-logging.md §\"trace_id is Always Set\"\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { traceAls, type TraceStore } from './als-registry.ts';\n\n// Re-export the TraceStore type for public API consumers.\nexport type { TraceStore } from './als-registry.ts';\n\n// ─── Public API ───────────────────────────────────────────────────────────\n\n/**\n * Returns the current request's trace ID — always a 32-char lowercase hex string.\n *\n * With OTEL: the real OTEL trace ID (matches Jaeger/Honeycomb/Datadog).\n * Without OTEL: crypto.randomUUID() with hyphens stripped.\n *\n * Throws if called outside a request context (no ALS store).\n */\nexport function getTraceId(): string {\n const store = traceAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] getTraceId() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n return store.traceId;\n}\n\n/**\n * Returns the current OTEL span ID if available, undefined otherwise.\n */\nexport function getSpanId(): string | undefined {\n return traceAls.getStore()?.spanId;\n}\n\n// ─── Framework-Internal Helpers ───────────────────────────────────────────\n\n/**\n * Generate a 32-char lowercase hex ID from crypto.randomUUID().\n * Same format as OTEL trace IDs — zero-friction upgrade path.\n */\nexport function generateTraceId(): string {\n return randomUUID().replace(/-/g, '');\n}\n\n/**\n * Run a callback within a trace context. Used by the pipeline to establish\n * per-request ALS scope.\n */\nexport function runWithTraceId<T>(id: string, fn: () => T): T {\n return traceAls.run({ traceId: id }, fn);\n}\n\n/**\n * Replace the trace ID in the current ALS store. Used when OTEL creates\n * a root span and we want to switch from the UUID fallback to the real\n * OTEL trace ID.\n */\nexport function replaceTraceId(newTraceId: string, newSpanId?: string): void {\n const store = traceAls.getStore();\n if (store) {\n store.traceId = newTraceId;\n store.spanId = newSpanId;\n }\n}\n\n/**\n * Update the span ID in the current ALS store. Used when entering a new\n * OTEL span to keep log–trace correlation accurate.\n */\nexport function updateSpanId(newSpanId: string | undefined): void {\n const store = traceAls.getStore();\n if (store) {\n store.spanId = newSpanId;\n }\n}\n\n/**\n * Get the current trace store, or undefined if outside a request context.\n * Framework-internal — use getTraceId()/getSpanId() in user code.\n */\nexport function getTraceStore(): TraceStore | undefined {\n return traceAls.getStore();\n}\n\n// ─── Dev-Mode OTEL Auto-Init ─────────────────────────────────────────────\n\n/**\n * Well-known key marking dev tracing as initialized.\n *\n * The RSC entry module re-evaluates on every HMR invalidation and calls\n * initDevTracing() again — without this guard, each call would register a\n * fresh provider/processor and duplicate every span's output. Symbol.for()\n * survives module re-evaluation. See TIM-1067, B49.\n */\nconst DEV_TRACING_INIT_KEY = Symbol.for('timber.dev.tracing-initialized');\n\n/**\n * Initialize a minimal OTEL SDK in dev mode so spans are recorded and\n * fed to the DevSpanProcessor for dev log output.\n *\n * If the user already configured an OTEL SDK in register(), we add\n * our DevSpanProcessor alongside theirs. If no SDK is configured,\n * we create a BasicTracerProvider with our processor.\n *\n * Idempotent across module re-evaluations (HMR) — only the first call\n * registers. Only called in dev mode — zero overhead in production.\n */\nexport async function initDevTracing(\n config: import('../dev-tools/logger.ts').DevLoggerConfig\n): Promise<void> {\n const globals = globalThis as Record<symbol, unknown>;\n if (globals[DEV_TRACING_INIT_KEY]) return;\n\n const api = await getOtelApi();\n if (!api) return;\n\n let DevSpanProcessor: typeof import('../dev-tools/instrumentation.ts').DevSpanProcessor;\n let BasicTracerProvider: typeof import('@opentelemetry/sdk-trace-base').BasicTracerProvider;\n let AsyncLocalStorageContextManager: typeof import('@opentelemetry/context-async-hooks').AsyncLocalStorageContextManager;\n\n try {\n ({ DevSpanProcessor } = await import('../dev-tools/instrumentation.ts'));\n ({ BasicTracerProvider } = await import('@opentelemetry/sdk-trace-base'));\n ({ AsyncLocalStorageContextManager } = await import('@opentelemetry/context-async-hooks'));\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.warn(`[timber] Dev tracing disabled — failed to load OTEL packages:\\n ${msg}`);\n return;\n }\n\n const processor = new DevSpanProcessor(config);\n\n // Register a context manager so OTEL can propagate the active span\n // across async boundaries. Without this, startActiveSpan can't make\n // spans \"active\" — child spans get random trace IDs and getActiveSpan()\n // returns undefined.\n const contextManager = new AsyncLocalStorageContextManager();\n contextManager.enable();\n api.context.setGlobalContextManager(contextManager);\n\n // Create a minimal TracerProvider with our DevSpanProcessor.\n // If the user also configures an SDK in register(), their provider\n // will coexist — the global provider set last wins for new tracers,\n // but our processor captures all spans from the timber.js tracer.\n const provider = new BasicTracerProvider({\n spanProcessors: [processor],\n });\n api.trace.setGlobalTracerProvider(provider);\n\n // Reset cached tracer so next getTracer() picks up the new provider\n _tracer = undefined;\n\n globals[DEV_TRACING_INIT_KEY] = true;\n}\n\n// ─── Platform Tracer ─────────────────────────────────────────────────────\n\n/**\n * A native platform span. Mirrors the subset of the Cloudflare Workers\n * `Span` API that timber uses. See design/17-logging.md §\"Cloudflare Native\n * Spans\" and TIM-1135.\n */\nexport interface PlatformSpan {\n setAttribute(key: string, value: string | number | boolean): void;\n}\n\n/**\n * Adapter-provided native tracer. Callback-scoped like Cloudflare's\n * `tracing.enterSpan()` — the span starts when the callback is invoked and\n * ends when it returns or its promise settles. Nesting follows the\n * platform's async context.\n *\n * When registered, withSpan() wraps every framework span in a native span\n * *in addition to* the OTEL emission — dual emission, so external OTEL\n * collectors keep working alongside the platform's native trace view.\n *\n * Register via setPlatformTracer(), or by writing the\n * Symbol.for('timber:platform-tracer') key on globalThis from\n * adapter-generated entry code (what the Cloudflare _worker.js does).\n */\nexport interface PlatformTracer {\n enterSpan<T>(name: string, fn: (span: PlatformSpan) => T): T;\n}\n\n// globalThis + Symbol.for so a tracer registered by adapter-generated entry\n// code (a separate module instance) is visible in both the RSC and SSR\n// environments — same pattern as the cf-bindings ALS.\nconst PLATFORM_TRACER_KEY = Symbol.for('timber:platform-tracer');\n\n/** Register (or clear) the native platform tracer for this runtime. */\nexport function setPlatformTracer(tracer: PlatformTracer | undefined): void {\n (globalThis as Record<symbol, unknown>)[PLATFORM_TRACER_KEY] = tracer;\n}\n\n/** The registered native platform tracer, if any. */\nexport function getPlatformTracer(): PlatformTracer | undefined {\n return (globalThis as Record<symbol, unknown>)[PLATFORM_TRACER_KEY] as PlatformTracer | undefined;\n}\n\n// ─── OTEL Span Helpers ───────────────────────────────────────────────────\n\n/**\n * Attempt to get the @opentelemetry/api tracer. Returns undefined if the\n * package is not installed or no SDK is registered.\n *\n * timber.js depends on @opentelemetry/api as the vendor-neutral interface.\n * The API is a no-op by default — spans are only emitted when the developer\n * initializes an SDK in register().\n */\nlet _otelApi: typeof import('@opentelemetry/api') | null | undefined;\n\nasync function getOtelApi(): Promise<typeof import('@opentelemetry/api') | null> {\n if (_otelApi === undefined) {\n try {\n _otelApi = await import('@opentelemetry/api');\n } catch {\n _otelApi = null;\n }\n }\n return _otelApi;\n}\n\n/** OTEL tracer instance, lazily created. */\nlet _tracer: import('@opentelemetry/api').Tracer | null | undefined;\n\n/**\n * Get the timber.js OTEL tracer. Returns null if @opentelemetry/api is not available.\n */\nexport async function getTracer(): Promise<import('@opentelemetry/api').Tracer | null> {\n if (_tracer === undefined) {\n const api = await getOtelApi();\n if (api) {\n _tracer = api.trace.getTracer('timber.js');\n } else {\n _tracer = null;\n }\n }\n return _tracer;\n}\n\n/**\n * Run a function within a framework span. Composes two emission channels:\n *\n * - **Native platform span** — when an adapter registered a PlatformTracer\n * (Cloudflare Workers), the fn is wrapped in platformTracer.enterSpan()\n * so it appears in the platform's native trace view.\n * - **OTEL span** — when an OTEL SDK is active, the fn also runs inside an\n * OTEL span (dual emission). No SDK and no platform tracer = zero overhead.\n *\n * Automatically:\n * - Creates the span as a child of the current context\n * - Updates the ALS span ID for log–trace correlation\n * - Ends the span when the function completes\n * - Records exceptions on error (OTEL channel)\n */\nexport async function withSpan<T>(\n name: string,\n attributes: Record<string, string | number | boolean>,\n fn: () => T | Promise<T>\n): Promise<T> {\n const platformTracer = getPlatformTracer();\n if (!platformTracer) {\n return runOtelSpan(name, attributes, fn);\n }\n\n // Native platform span wraps the OTEL emission (dual emission). The\n // innermost platform span is tracked on the trace store so\n // setSpanAttribute() can reach it after creation — the platform API has\n // no getActiveSpan() equivalent.\n return platformTracer.enterSpan(name, async (span) => {\n for (const key of Object.keys(attributes)) {\n span.setAttribute(key, attributes[key]);\n }\n const store = traceAls.getStore();\n const prevPlatformSpan = store?.platformSpan;\n if (store) store.platformSpan = span;\n try {\n return await runOtelSpan(name, attributes, fn);\n } finally {\n if (store) store.platformSpan = prevPlatformSpan;\n }\n });\n}\n\n/** The OTEL half of withSpan() — no-op passthrough when no SDK is active. */\nasync function runOtelSpan<T>(\n name: string,\n attributes: Record<string, string | number | boolean>,\n fn: () => T | Promise<T>\n): Promise<T> {\n const tracer = await getTracer();\n if (!tracer) {\n return fn();\n }\n\n const api = (await getOtelApi())!;\n return tracer.startActiveSpan(name, { attributes }, async (span) => {\n const prevSpanId = getSpanId();\n updateSpanId(span.spanContext().spanId);\n try {\n const result = await fn();\n span.setStatus({ code: api.SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.setStatus({ code: api.SpanStatusCode.ERROR });\n if (error instanceof Error) {\n span.recordException(error);\n }\n throw error;\n } finally {\n span.end();\n updateSpanId(prevSpanId);\n }\n });\n}\n\n/**\n * Set an attribute on the current active span (if any).\n * Used for setting span attributes after span creation (e.g. timber.result on access spans).\n */\nexport async function setSpanAttribute(\n key: string,\n value: string | number | boolean\n): Promise<void> {\n // Forward to the innermost active native platform span, if any.\n const platformSpan = traceAls.getStore()?.platformSpan;\n if (platformSpan) {\n platformSpan.setAttribute(key, value);\n }\n\n const api = await getOtelApi();\n if (!api) return;\n\n const activeSpan = api.trace.getActiveSpan();\n if (activeSpan) {\n activeSpan.setAttribute(key, value);\n }\n}\n\n/**\n * Add a span event to the current active span (if any).\n * Used for timber.cache HIT/MISS events — recorded as span events, not child spans.\n */\nexport async function addSpanEvent(\n name: string,\n attributes?: Record<string, string | number | boolean>\n): Promise<void> {\n const api = await getOtelApi();\n if (!api) return;\n\n const activeSpan = api.trace.getActiveSpan();\n if (activeSpan) {\n activeSpan.addEvent(name, attributes);\n }\n}\n\n/**\n * Fire-and-forget span event — no await, no microtask overhead.\n *\n * Used on the cache hot path where awaiting addSpanEvent creates an\n * unnecessary microtask per cache operation. If OTEL is not loaded yet,\n * the event is silently dropped (acceptable for diagnostics).\n *\n * See TIM-370 for perf motivation.\n */\nexport function addSpanEventSync(\n name: string,\n attributes?: Record<string, string | number | boolean>\n): void {\n // Fast path: if OTEL API hasn't been loaded yet, skip entirely.\n // _otelApi is undefined (not yet loaded), null (failed to load), or the module.\n if (!_otelApi) return;\n\n const activeSpan = _otelApi.trace.getActiveSpan();\n if (activeSpan) {\n activeSpan.addEvent(name, attributes);\n }\n}\n\n/**\n * Try to extract the OTEL trace ID from the current active span context.\n * Returns undefined if OTEL is not active or no span exists.\n */\nexport async function getOtelTraceId(): Promise<{ traceId: string; spanId: string } | undefined> {\n const api = await getOtelApi();\n if (!api) return undefined;\n\n const activeSpan = api.trace.getActiveSpan();\n if (!activeSpan) return undefined;\n\n const ctx = activeSpan.spanContext();\n // OTEL uses \"0000000000000000\" as invalid trace IDs\n if (!ctx.traceId || ctx.traceId === '00000000000000000000000000000000') {\n return undefined;\n }\n\n return { traceId: ctx.traceId, spanId: ctx.spanId };\n}\n","/**\n * Error Formatter — rewrites SSR/RSC error messages to surface user code.\n *\n * When React or Vite throw errors during SSR, stack traces reference\n * vendored dependency paths (e.g. `.vite/deps_ssr/@vitejs_plugin-rsc_vendor_...`)\n * and mangled export names (`__vite_ssr_export_default__`). This module\n * rewrites error messages and stack traces to point at user code instead.\n *\n * Dev-only — in production, errors go through the structured logger\n * without formatting.\n */\n\n// ─── Stack Trace Rewriting ──────────────────────────────────────────────────\n\n/**\n * Patterns that identify internal Vite/RSC vendor paths in stack traces.\n * These are replaced with human-readable labels.\n */\nconst VENDOR_PATH_PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [\n {\n pattern: /node_modules\\/\\.vite\\/deps_ssr\\/@vitejs_plugin-rsc_vendor_react-server-dom[^\\s)]+/g,\n replacement: '<react-server-dom>',\n },\n {\n pattern: /node_modules\\/\\.vite\\/deps_ssr\\/@vitejs_plugin-rsc_vendor[^\\s)]+/g,\n replacement: '<rsc-vendor>',\n },\n {\n pattern: /node_modules\\/\\.vite\\/deps_ssr\\/[^\\s)]+/g,\n replacement: '<vite-dep>',\n },\n {\n pattern: /node_modules\\/\\.vite\\/deps\\/[^\\s)]+/g,\n replacement: '<vite-dep>',\n },\n];\n\n/**\n * Patterns that identify Vite-mangled export names in error messages.\n */\nconst MANGLED_NAME_PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [\n {\n pattern: /__vite_ssr_export_default__/g,\n replacement: '<default export>',\n },\n {\n pattern: /__vite_ssr_export_(\\w+)__/g,\n replacement: '<export $1>',\n },\n];\n\n/**\n * Rewrite an error's message and stack to replace internal Vite paths\n * and mangled names with human-readable labels.\n */\nexport function formatSsrError(error: unknown): string {\n if (!(error instanceof Error)) {\n return String(error);\n }\n\n let message = error.message;\n let stack = error.stack ?? '';\n\n // Rewrite mangled names in the message\n for (const { pattern, replacement } of MANGLED_NAME_PATTERNS) {\n message = message.replace(pattern, replacement);\n }\n\n // Rewrite vendor paths in the stack\n for (const { pattern, replacement } of VENDOR_PATH_PATTERNS) {\n stack = stack.replace(pattern, replacement);\n }\n\n // Rewrite mangled names in the stack too\n for (const { pattern, replacement } of MANGLED_NAME_PATTERNS) {\n stack = stack.replace(pattern, replacement);\n }\n\n // Extract hints from React-specific error patterns\n const hint = extractErrorHint(error.message);\n\n // Build formatted output: cleaned message, hint (if any), then cleaned stack\n const parts: string[] = [];\n parts.push(message);\n if (hint) {\n parts.push(` → ${hint}`);\n }\n\n // Include only the user-code frames from the stack (skip the first line\n // which is the message itself, and filter out vendor-only frames)\n const userFrames = extractUserFrames(stack);\n if (userFrames.length > 0) {\n parts.push('');\n parts.push(' User code in stack:');\n for (const frame of userFrames) {\n parts.push(` ${frame}`);\n }\n }\n\n return parts.join('\\n');\n}\n\n// ─── Error Hint Extraction ──────────────────────────────────────────────────\n\n/**\n * Extract a human-readable hint from common React/RSC error messages.\n *\n * React error messages contain useful information but the surrounding\n * context (vendor paths, mangled names) obscures it. This extracts the\n * actionable part as a one-line hint.\n */\nfunction extractErrorHint(message: string): string | null {\n // \"Functions cannot be passed directly to Client Components\"\n // Extract the component and prop name from the JSX-like syntax in the message\n const fnPassedMatch = message.match(/Functions cannot be passed directly to Client Components/);\n if (fnPassedMatch) {\n // Try to extract the prop name from the message\n // React formats: <... propName={function ...} ...>\n const propMatch = message.match(/<[^>]*?\\s(\\w+)=\\{function/);\n if (propMatch) {\n return `Prop \"${propMatch[1]}\" is a function — mark it \"use server\" or call it before passing`;\n }\n return 'A function prop was passed to a Client Component — mark it \"use server\" or call it before passing';\n }\n\n // \"Objects are not valid as a React child\"\n if (message.includes('Objects are not valid as a React child')) {\n return 'An object was rendered as JSX children — convert to string or extract the value';\n }\n\n // \"Cannot read properties of undefined/null\"\n const nullRefMatch = message.match(\n /Cannot read propert(?:y|ies) of (undefined|null) \\(reading '(\\w+)'\\)/\n );\n if (nullRefMatch) {\n return `Accessed .${nullRefMatch[2]} on ${nullRefMatch[1]} — check that the value exists`;\n }\n\n // \"X is not a function\"\n const notFnMatch = message.match(/(\\w+) is not a function/);\n if (notFnMatch) {\n return `\"${notFnMatch[1]}\" is not a function — check imports and exports`;\n }\n\n // \"Element type is invalid\"\n if (message.includes('Element type is invalid')) {\n return 'A component resolved to undefined/null — check default exports and import paths';\n }\n\n // \"Invalid hook call\" — hooks called outside React's render context.\n // In RSC, this typically means a 'use client' component was executed as a\n // server component instead of being serialized as a client reference.\n if (message.includes('Invalid hook call')) {\n return (\n 'A hook was called outside of a React component render. ' +\n \"If this is a 'use client' component, ensure the directive is at the very top of the file \" +\n '(before any imports) and that @vitejs/plugin-rsc is loaded correctly. ' +\n \"Barrel re-exports from non-'use client' files do not propagate the directive.\"\n );\n }\n\n return null;\n}\n\n// ─── Stack Frame Filtering ──────────────────────────────────────────────────\n\n/**\n * Extract stack frames that reference user code (not node_modules,\n * not framework internals).\n *\n * Returns at most 5 frames to keep output concise.\n */\nfunction extractUserFrames(stack: string): string[] {\n const lines = stack.split('\\n');\n const userFrames: string[] = [];\n\n for (const line of lines) {\n const trimmed = line.trim();\n // Skip non-frame lines\n if (!trimmed.startsWith('at ')) continue;\n // Skip node_modules, vendor, and internal frames\n if (\n trimmed.includes('node_modules') ||\n trimmed.includes('<react-server-dom>') ||\n trimmed.includes('<rsc-vendor>') ||\n trimmed.includes('<vite-dep>') ||\n trimmed.includes('node:internal')\n ) {\n continue;\n }\n userFrames.push(trimmed);\n if (userFrames.length >= 5) break;\n }\n\n return userFrames;\n}\n","/**\n * DefaultLogger — human-readable stderr logging when no custom logger is configured.\n *\n * Ships as the fallback so production deployments always have error visibility,\n * even without an `instrumentation.ts` logger export. Output is one line per\n * event, designed for `fly logs`, `kubectl logs`, Cloudflare dashboard tails, etc.\n *\n * Format:\n * [timber] ERROR message key=value key=value trace_id=4bf92f35\n * [timber] WARN message key=value key=value trace_id=4bf92f35\n * [timber] INFO message method=GET path=/dashboard status=200 durationMs=43 trace_id=4bf92f35\n *\n * Behavior:\n * - Suppressed entirely in dev mode (dev logging handles all output)\n * - `debug` suppressed unless TIMBER_DEBUG is set\n * - Replaced entirely when a custom logger is set via `setLogger()`\n *\n * See design/17-logging.md §\"DefaultLogger\"\n */\n\nimport { isDevMode, isDebug } from './debug.ts';\nimport { formatSsrError } from './error-formatter.ts';\nimport type { TimberLogger } from './logger.ts';\n\n/**\n * Format data fields as `key=value` pairs for human-readable output.\n * - `error` key is serialized via formatSsrError for stack trace cleanup\n * - `trace_id` is truncated to 8 chars for readability (full ID in OTEL)\n * - Other values are stringified inline\n */\nfunction formatDataFields(data?: Record<string, unknown>): string {\n if (!data) return '';\n\n const parts: string[] = [];\n let traceId: string | undefined;\n\n for (const [key, value] of Object.entries(data)) {\n if (key === 'trace_id') {\n // Defer trace_id to the end\n traceId = typeof value === 'string' ? value : String(value);\n continue;\n }\n if (key === 'error') {\n // Serialize errors with formatSsrError for clean output\n parts.push(`error=${formatSsrError(value)}`);\n continue;\n }\n if (value === undefined || value === null) continue;\n parts.push(`${key}=${value}`);\n }\n\n // trace_id always last, truncated to 8 chars for readability\n if (traceId) {\n parts.push(`trace_id=${traceId.slice(0, 8)}`);\n }\n\n return parts.length > 0 ? ' ' + parts.join(' ') : '';\n}\n\n/** Pad level string to fixed width for alignment. */\nfunction padLevel(level: string): string {\n return level.padEnd(5);\n}\n\nexport function createDefaultLogger(): TimberLogger {\n return {\n error(msg: string, data?: Record<string, unknown>): void {\n // Errors are ALWAYS logged, including dev mode. Suppressing errors\n // in dev causes silent 500s with no stack trace, making route.ts\n // and render errors impossible to debug. See TIM-555.\n const fields = formatDataFields(data);\n process.stderr.write(`[timber] ${padLevel('ERROR')} ${msg}${fields}\\n`);\n },\n\n warn(msg: string, data?: Record<string, unknown>): void {\n // Warnings are always logged — same rationale as errors.\n const fields = formatDataFields(data);\n process.stderr.write(`[timber] ${padLevel('WARN')} ${msg}${fields}\\n`);\n },\n\n info(msg: string, data?: Record<string, unknown>): void {\n // info is suppressed by default — per-request lines are too noisy\n // without a custom logger. Enable with TIMBER_DEBUG.\n if (isDevMode()) return;\n if (!isDebug()) return;\n const fields = formatDataFields(data);\n process.stderr.write(`[timber] ${padLevel('INFO')} ${msg}${fields}\\n`);\n },\n\n debug(msg: string, data?: Record<string, unknown>): void {\n // debug is suppressed in dev (dev logger handles it) and in\n // production unless TIMBER_DEBUG is explicitly set.\n if (isDevMode()) return;\n if (!isDebug()) return;\n const fields = formatDataFields(data);\n process.stderr.write(`[timber] ${padLevel('DEBUG')} ${msg}${fields}\\n`);\n },\n };\n}\n","/**\n * Logger — structured logging with environment-aware formatting.\n *\n * timber.js ships a DefaultLogger that writes human-readable lines to stderr\n * in production. Users can export a custom logger from instrumentation.ts to\n * replace it with pino, winston, or any TimberLogger-compatible object.\n *\n * See design/17-logging.md §\"Production Logging\"\n */\n\nimport { getTraceStore } from './tracing.ts';\nimport { createDefaultLogger } from './default-logger.ts';\nimport { isDevMode } from './debug.ts';\nimport { isControlFlowSignal } from './primitives.ts';\n\n// ─── Logger Interface ─────────────────────────────────────────────────────\n\n/** Any object with standard log methods satisfies this — pino, winston, consola, console. */\nexport interface TimberLogger {\n info(msg: string, data?: Record<string, unknown>): void;\n warn(msg: string, data?: Record<string, unknown>): void;\n error(msg: string, data?: Record<string, unknown>): void;\n debug(msg: string, data?: Record<string, unknown>): void;\n}\n\n// ─── Logger Registry ──────────────────────────────────────────────────────\n\n// Initialize with DefaultLogger so production errors are never silent.\n// Replaced when setLogger() is called from instrumentation.ts.\nlet _logger: TimberLogger = createDefaultLogger();\n\n/**\n * Set the user-provided logger. Called by the instrumentation loader\n * when it finds a `logger` export in instrumentation.ts. Replaces\n * the DefaultLogger entirely.\n */\nexport function setLogger(logger: TimberLogger): void {\n _logger = logger;\n}\n\n/**\n * Get the current logger. Always non-null — returns DefaultLogger when\n * no custom logger is configured.\n */\nexport function getLogger(): TimberLogger {\n return _logger;\n}\n\n// ─── Framework Log Helpers ────────────────────────────────────────────────\n\n/**\n * Inject trace_id and span_id into log data for log–trace correlation.\n * Always injects trace_id (never undefined). Injects span_id only when OTEL is active.\n */\nfunction withTraceContext(data?: Record<string, unknown>): Record<string, unknown> {\n const store = getTraceStore();\n const enriched: Record<string, unknown> = { ...data };\n if (store) {\n enriched.trace_id = store.traceId;\n if (store.spanId) {\n enriched.span_id = store.spanId;\n }\n }\n return enriched;\n}\n\n// ─── Framework Event Emitters ─────────────────────────────────────────────\n\n/** Log a completed request. Level: info. */\nexport function logRequestCompleted(data: {\n method: string;\n path: string;\n status: number;\n durationMs: number;\n /** Number of concurrent in-flight requests (including this one) at completion time. */\n concurrency?: number;\n}): void {\n _logger.info('request completed', withTraceContext(data));\n}\n\n/** Log request received. Level: debug. */\nexport function logRequestReceived(data: { method: string; path: string }): void {\n _logger.debug('request received', withTraceContext(data));\n}\n\n/** Log a slow request warning. Level: warn. */\nexport function logSlowRequest(data: {\n method: string;\n path: string;\n durationMs: number;\n threshold: number;\n /** Number of concurrent in-flight requests at the time the slow request completed. */\n concurrency?: number;\n}): void {\n _logger.warn('slow request exceeded threshold', withTraceContext(data));\n}\n\n/** Log middleware short-circuit. Level: debug. */\nexport function logMiddlewareShortCircuit(data: {\n method: string;\n path: string;\n status: number;\n}): void {\n _logger.debug('middleware short-circuited', withTraceContext(data));\n}\n\n/** Log unhandled error in middleware phase. Level: error. */\nexport function logMiddlewareError(data: { method: string; path: string; error: unknown }): void {\n if (isControlFlowSignal(data.error)) return;\n _logger.error('unhandled error in middleware phase', withTraceContext(data));\n}\n\n/** Log unhandled render-phase error. Level: error. */\nexport function logRenderError(data: {\n method: string;\n path: string;\n error: unknown;\n errorId?: string;\n}): void {\n if (isControlFlowSignal(data.error)) return;\n _logger.error('unhandled render-phase error', withTraceContext(data));\n}\n\n/** Log proxy.ts uncaught error. Level: error. */\nexport function logProxyError(data: { error: unknown }): void {\n _logger.error('proxy.ts threw uncaught error', withTraceContext(data));\n}\n\n/** Log unhandled error in server action. Level: error. */\nexport function logActionError(data: { method: string; path: string; error: unknown }): void {\n if (isControlFlowSignal(data.error)) return;\n _logger.error('unhandled server action error', withTraceContext(data));\n}\n\n/** Log unhandled error in route handler. Level: error. */\nexport function logRouteError(data: { method: string; path: string; error: unknown }): void {\n if (isControlFlowSignal(data.error)) return;\n _logger.error('unhandled route handler error', withTraceContext(data));\n}\n\n/** Log SSR streaming error (post-shell). Level: error. */\nexport function logStreamingError(data: { error: unknown }): void {\n _logger.error('SSR streaming error (post-shell)', withTraceContext(data));\n}\n\n/** Log waitUntil() adapter missing (once at startup). Level: warn. */\nexport function logWaitUntilUnsupported(): void {\n _logger.warn('adapter does not support waitUntil()');\n}\n\n/** Log waitUntil() promise rejection. Level: warn. */\nexport function logWaitUntilRejected(data: { error: unknown }): void {\n _logger.warn('waitUntil() promise rejected', withTraceContext(data));\n}\n\n/** Log staleWhileRevalidate refetch failure. Level: warn. */\nexport function logSwrRefetchFailed(data: { cacheKey: string; error: unknown }): void {\n _logger.warn('staleWhileRevalidate refetch failed', withTraceContext(data));\n}\n\n/** Log cache miss. Level: debug. */\nexport function logCacheMiss(data: { cacheKey: string }): void {\n _logger.debug('timber.cache MISS', withTraceContext(data));\n}\n\n// ─── Swallow Helper ───────────────────────────────────────────────────────\n\n/**\n * Log an intentionally swallowed error. Provides observability into catch\n * blocks that are deliberately empty — the error is consumed, never rethrown.\n *\n * Default level: `warn` in dev (so the overlay surfaces patterns), `debug`\n * in production (low noise unless TIMBER_DEBUG is set). Pass `opts.level`\n * to override.\n *\n * **Infallible** — swallow() itself never throws, even if the logger is\n * broken. A thrown swallow would turn a benign catch into a crash.\n */\nexport function swallow(err: unknown, reason: string, opts?: { level?: 'debug' | 'warn' }): void {\n try {\n const level = opts?.level ?? (isDevMode() ? 'warn' : 'debug');\n _logger[level](`swallowed: ${reason}`, withTraceContext({ error: err }));\n } catch {\n // swallow() must never throw.\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as INFERRED_RULES, t as EXPLICIT_MARKERS } from "./poison-rules-DoEhbqaY.js";
|
|
2
|
-
import { o as langForFile } from "./chains-
|
|
2
|
+
import { o as langForFile } from "./chains-BjGKq9Je.js";
|
|
3
3
|
import { parseAst } from "vite";
|
|
4
4
|
//#region src/analyze/poison-scan.ts
|
|
5
5
|
/**
|
|
@@ -98,4 +98,4 @@ function poisoningsFor(kind, pending, signals) {
|
|
|
98
98
|
//#endregion
|
|
99
99
|
export { importSignals as n, poisoningsFor as r, collectImportSpecifiers as t };
|
|
100
100
|
|
|
101
|
-
//# sourceMappingURL=poison-scan-
|
|
101
|
+
//# sourceMappingURL=poison-scan-g83aAXhs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"poison-scan-
|
|
1
|
+
{"version":3,"file":"poison-scan-g83aAXhs.js","names":[],"sources":["../../src/analyze/poison-scan.ts"],"sourcesContent":["/**\n * poison-scan — report-only poisoning detection for `timber graph`.\n *\n * Matches a module's static import specifiers against the shared rule\n * lists in poison-rules.ts (the single source of truth — design/47 §3)\n * and reports a poisoning only when the implied exclusivity CONFLICTS\n * with where the module actually runs: a server-only signal in a\n * client-reachable module, or a client-only signal in a server-reachable\n * one. A `node:fs` import in a file only the RSC environment loads is\n * working code, not a finding.\n *\n * Scope: import-specifier signals only (explicit markers + the inferred\n * rules' `matchesImport`). The inferred browser-global identifier rules\n * need shadow-safe allowlisted-position AST matching and are a separate\n * ticket — reporting them from a text scan would flag legitimate code.\n *\n * Design docs: 47-module-environment-tooling.md §3.\n */\n\nimport { parseAst } from 'vite';\nimport { EXPLICIT_MARKERS, INFERRED_RULES } from './poison-rules.ts';\nimport { langForFile, type ModuleKind } from './classify.ts';\n\n/** One poisoning finding on a module, in the JSON-contract shape. */\nexport interface Poisoning {\n /** Rule id — an inferred rule's id, or the explicit marker specifier. */\n rule: string;\n /** The import specifier that matched. */\n specifier: string;\n /** Which exclusivity the signal implies for the containing module. */\n implies: 'server-only' | 'client-only';\n /** Human-readable description for reporter output. */\n description: string;\n}\n\n/** File extensions the specifier scan can parse. */\nconst PARSEABLE_EXTENSIONS = /\\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts)$/;\n\n/**\n * A NON-EMPTY specifier list where every entry is type-only. Empty or\n * absent lists return false — a bare `import 'pkg'` is a side-effect\n * import and very much runs.\n */\nfunction isAllSpecifiersTypeOnly(\n specifiers: Array<{ importKind?: string; exportKind?: string }> | undefined\n): boolean {\n if (!specifiers || specifiers.length === 0) return false;\n return specifiers.every((s) => s.importKind === 'type' || s.exportKind === 'type');\n}\n\n/**\n * Collect every static import/export source and string-literal dynamic\n * import from a module's original source. Sources that fail to parse\n * (or are not JS/TS at all) return [] — no specifiers is a safe\n * default for a report-only scan.\n */\nexport function collectImportSpecifiers(code: string, file: string): string[] {\n if (!PARSEABLE_EXTENSIONS.test(file)) return [];\n let program;\n try {\n // Language from the extension, never forced tsx — see langForFile.\n program = parseAst(code, { lang: langForFile(file) });\n } catch {\n return [];\n }\n const specifiers: string[] = [];\n const visit = (node: unknown): void => {\n if (Array.isArray(node)) {\n for (const child of node) visit(child);\n return;\n }\n if (typeof node !== 'object' || node === null) return;\n const record = node as Record<string, unknown> & {\n type?: string;\n importKind?: string;\n exportKind?: string;\n specifiers?: Array<{ importKind?: string; exportKind?: string }>;\n };\n if (\n (record.type === 'ImportDeclaration' ||\n record.type === 'ExportNamedDeclaration' ||\n record.type === 'ExportAllDeclaration') &&\n // Type-only imports/exports are erased at runtime — `import type\n // { Stats } from 'node:fs'` in client code is working code, and\n // flagging it would be a false positive in a report-only scan.\n // Covers both the declaration-level form (`import type {…}`,\n // importKind on the declaration) and the inline form\n // (`import { type Stats }`, where the declaration stays 'value'\n // and each SPECIFIER carries the kind) — a declaration whose\n // specifiers are all type-only is erased just the same.\n record.importKind !== 'type' &&\n record.exportKind !== 'type' &&\n !isAllSpecifiersTypeOnly(record.specifiers) &&\n typeof (record.source as { value?: unknown } | null)?.value === 'string'\n ) {\n specifiers.push((record.source as { value: string }).value);\n } else if (\n record.type === 'ImportExpression' &&\n (record.source as { type?: string; value?: unknown })?.type === 'Literal' &&\n typeof (record.source as { value?: unknown }).value === 'string'\n ) {\n specifiers.push((record.source as { value: string }).value);\n }\n for (const key of Object.keys(record)) {\n if (key === 'type') continue;\n visit(record[key]);\n }\n };\n visit(program.body);\n return specifiers;\n}\n\n/** Exclusivity signals a module's import specifiers carry. */\nexport function importSignals(specifiers: string[]): Poisoning[] {\n const signals: Poisoning[] = [];\n for (const specifier of specifiers) {\n for (const marker of EXPLICIT_MARKERS) {\n if (specifier === marker.specifier) {\n signals.push({\n rule: marker.specifier,\n specifier,\n implies: marker.specifier === 'server-only' ? 'server-only' : 'client-only',\n description: `imports the ${marker.specifier} poison-pill marker`,\n });\n }\n }\n for (const rule of INFERRED_RULES) {\n if (rule.matchesImport?.(specifier)) {\n signals.push({\n rule: rule.id,\n specifier,\n implies: rule.implies,\n description: rule.description,\n });\n }\n }\n }\n return signals;\n}\n\n/**\n * Filter a module's signals down to actual conflicts with its\n * classification. Environment reach follows the taxonomy: boundary /\n * client-internal / shared modules run client-side; server / shared /\n * server-action modules run server-side. Pending (provisional) modules\n * run nowhere confirmed, so nothing conflicts yet.\n */\nexport function poisoningsFor(\n kind: ModuleKind,\n pending: boolean,\n signals: Poisoning[]\n): Poisoning[] {\n if (pending || signals.length === 0) return [];\n const clientReachable =\n kind === 'client-boundary' || kind === 'client-internal' || kind === 'shared';\n const serverReachable = kind === 'server' || kind === 'shared' || kind === 'server-action';\n return signals.filter(\n (signal) =>\n (signal.implies === 'server-only' && clientReachable) ||\n (signal.implies === 'client-only' && serverReachable)\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAoCA,IAAM,uBAAuB;;;;;;AAO7B,SAAS,wBACP,YACS;CACT,IAAI,CAAC,cAAc,WAAW,WAAW,GAAG,OAAO;CACnD,OAAO,WAAW,OAAO,MAAM,EAAE,eAAe,UAAU,EAAE,eAAe,MAAM;AACnF;;;;;;;AAQA,SAAgB,wBAAwB,MAAc,MAAwB;CAC5E,IAAI,CAAC,qBAAqB,KAAK,IAAI,GAAG,OAAO,CAAC;CAC9C,IAAI;CACJ,IAAI;EAEF,UAAU,SAAS,MAAM,EAAE,MAAM,YAAY,IAAI,EAAE,CAAC;CACtD,QAAQ;EACN,OAAO,CAAC;CACV;CACA,MAAM,aAAuB,CAAC;CAC9B,MAAM,SAAS,SAAwB;EACrC,IAAI,MAAM,QAAQ,IAAI,GAAG;GACvB,KAAK,MAAM,SAAS,MAAM,MAAM,KAAK;GACrC;EACF;EACA,IAAI,OAAO,SAAS,YAAY,SAAS,MAAM;EAC/C,MAAM,SAAS;EAMf,KACG,OAAO,SAAS,uBACf,OAAO,SAAS,4BAChB,OAAO,SAAS,2BASlB,OAAO,eAAe,UACtB,OAAO,eAAe,UACtB,CAAC,wBAAwB,OAAO,UAAU,KAC1C,OAAQ,OAAO,QAAuC,UAAU,UAEhE,WAAW,KAAM,OAAO,OAA6B,KAAK;OACrD,IACL,OAAO,SAAS,sBACf,OAAO,QAA+C,SAAS,aAChE,OAAQ,OAAO,OAA+B,UAAU,UAExD,WAAW,KAAM,OAAO,OAA6B,KAAK;EAE5D,KAAK,MAAM,OAAO,OAAO,KAAK,MAAM,GAAG;GACrC,IAAI,QAAQ,QAAQ;GACpB,MAAM,OAAO,IAAI;EACnB;CACF;CACA,MAAM,QAAQ,IAAI;CAClB,OAAO;AACT;;AAGA,SAAgB,cAAc,YAAmC;CAC/D,MAAM,UAAuB,CAAC;CAC9B,KAAK,MAAM,aAAa,YAAY;EAClC,KAAK,MAAM,UAAU,kBACnB,IAAI,cAAc,OAAO,WACvB,QAAQ,KAAK;GACX,MAAM,OAAO;GACb;GACA,SAAS,OAAO,cAAc,gBAAgB,gBAAgB;GAC9D,aAAa,eAAe,OAAO,UAAU;EAC/C,CAAC;EAGL,KAAK,MAAM,QAAQ,gBACjB,IAAI,KAAK,gBAAgB,SAAS,GAChC,QAAQ,KAAK;GACX,MAAM,KAAK;GACX;GACA,SAAS,KAAK;GACd,aAAa,KAAK;EACpB,CAAC;CAGP;CACA,OAAO;AACT;;;;;;;;AASA,SAAgB,cACd,MACA,SACA,SACa;CACb,IAAI,WAAW,QAAQ,WAAW,GAAG,OAAO,CAAC;CAC7C,MAAM,kBACJ,SAAS,qBAAqB,SAAS,qBAAqB,SAAS;CACvE,MAAM,kBAAkB,SAAS,YAAY,SAAS,YAAY,SAAS;CAC3E,OAAO,QAAQ,QACZ,WACE,OAAO,YAAY,iBAAiB,mBACpC,OAAO,YAAY,iBAAiB,eACzC;AACF"}
|