@timber-js/app 0.2.0-alpha.98 → 0.2.0-alpha.99
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/LICENSE +8 -0
- package/dist/_chunks/actions-CQ8Z8VGL.js +1061 -0
- package/dist/_chunks/actions-CQ8Z8VGL.js.map +1 -0
- package/dist/_chunks/build-output-helper-DXnW0qjz.js +61 -0
- package/dist/_chunks/build-output-helper-DXnW0qjz.js.map +1 -0
- package/dist/_chunks/{define-Itxvcd7F.js → define-B-Q_UMOD.js} +19 -23
- package/dist/_chunks/define-B-Q_UMOD.js.map +1 -0
- package/dist/_chunks/{define-C77ScO0m.js → define-CfBPoJb0.js} +24 -7
- package/dist/_chunks/define-CfBPoJb0.js.map +1 -0
- package/dist/_chunks/define-cookie-BjpIt4UC.js +194 -0
- package/dist/_chunks/define-cookie-BjpIt4UC.js.map +1 -0
- package/dist/_chunks/{format-CYBGxKtc.js → format-Bcn-Iv1x.js} +1 -1
- package/dist/_chunks/{format-CYBGxKtc.js.map → format-Bcn-Iv1x.js.map} +1 -1
- package/dist/_chunks/handler-store-B-lqaGyh.js +54 -0
- package/dist/_chunks/handler-store-B-lqaGyh.js.map +1 -0
- package/dist/_chunks/logger-0m8MsKdc.js +291 -0
- package/dist/_chunks/logger-0m8MsKdc.js.map +1 -0
- package/dist/_chunks/merge-search-params-BphMdht_.js +122 -0
- package/dist/_chunks/merge-search-params-BphMdht_.js.map +1 -0
- package/dist/_chunks/navigation-root-BCYczjml.js +96 -0
- package/dist/_chunks/navigation-root-BCYczjml.js.map +1 -0
- package/dist/_chunks/registry-I2ss-lvy.js +20 -0
- package/dist/_chunks/registry-I2ss-lvy.js.map +1 -0
- package/dist/_chunks/router-ref-h3-UaCQv.js +28 -0
- package/dist/_chunks/router-ref-h3-UaCQv.js.map +1 -0
- package/dist/_chunks/{schema-bridge-C3xl_vfb.js → schema-bridge-Cxu4l-7p.js} +1 -1
- package/dist/_chunks/{schema-bridge-C3xl_vfb.js.map → schema-bridge-Cxu4l-7p.js.map} +1 -1
- package/dist/_chunks/{segment-context-fHFLF1PE.js → segment-context-Dx_OizxD.js} +1 -1
- package/dist/_chunks/{segment-context-fHFLF1PE.js.map → segment-context-Dx_OizxD.js.map} +1 -1
- package/dist/_chunks/{router-ref-C8OCm7g7.js → ssr-data-B4CdH7rE.js} +2 -26
- package/dist/_chunks/ssr-data-B4CdH7rE.js.map +1 -0
- package/dist/_chunks/{stale-reload-BX5gL1r-.js → stale-reload-Bab885FO.js} +1 -1
- package/dist/_chunks/{stale-reload-BX5gL1r-.js.map → stale-reload-Bab885FO.js.map} +1 -1
- package/dist/_chunks/tracing-C8V-YGsP.js +329 -0
- package/dist/_chunks/tracing-C8V-YGsP.js.map +1 -0
- package/dist/_chunks/{use-query-states-BiV5GJgm.js → use-query-states-B2XTqxDR.js} +3 -19
- package/dist/_chunks/use-query-states-B2XTqxDR.js.map +1 -0
- package/dist/_chunks/{use-params-IOPu7E8t.js → use-segment-params-BkpKAQ7D.js} +9 -95
- package/dist/_chunks/use-segment-params-BkpKAQ7D.js.map +1 -0
- package/dist/_chunks/{walkers-VOXgavMF.js → walkers-Tg0Alwcg.js} +6 -3
- package/dist/_chunks/walkers-Tg0Alwcg.js.map +1 -0
- package/dist/_chunks/{dev-warnings-DpGRGoDi.js → warnings-Cg47l5sk.js} +3 -3
- package/dist/_chunks/warnings-Cg47l5sk.js.map +1 -0
- package/dist/adapters/build-output-helper.d.ts +28 -0
- package/dist/adapters/build-output-helper.d.ts.map +1 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +8 -28
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +8 -26
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/adapters/shared.d.ts +16 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/cache/index.js +9 -2
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/client/error-boundary.js +2 -1
- package/dist/client/error-boundary.js.map +1 -1
- package/dist/client/form.d.ts +10 -24
- package/dist/client/form.d.ts.map +1 -1
- package/dist/client/index.d.ts +1 -5
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +40 -90
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +2 -1
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +81 -7
- package/dist/client/internal.js.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/state.d.ts +1 -1
- package/dist/client/use-cookie.d.ts +8 -0
- package/dist/client/use-cookie.d.ts.map +1 -1
- package/dist/client/{use-params.d.ts → use-segment-params.d.ts} +1 -1
- package/dist/client/use-segment-params.d.ts.map +1 -0
- package/dist/codec.d.ts +1 -1
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +2 -2
- package/dist/config-types.d.ts +28 -0
- package/dist/config-types.d.ts.map +1 -1
- package/dist/cookies/define-cookie.d.ts +87 -35
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.d.ts +2 -1
- package/dist/cookies/index.d.ts.map +1 -1
- package/dist/cookies/index.js +48 -2
- package/dist/cookies/index.js.map +1 -0
- package/dist/cookies/json-cookie.d.ts +64 -0
- package/dist/cookies/json-cookie.d.ts.map +1 -0
- package/dist/cookies/validation.d.ts +46 -0
- package/dist/cookies/validation.d.ts.map +1 -0
- package/dist/{plugins/dev-404-page.d.ts → dev-tools/404-page.d.ts} +1 -1
- package/dist/dev-tools/404-page.d.ts.map +1 -0
- package/dist/{plugins/dev-browser-logs.d.ts → dev-tools/browser-logs.d.ts} +1 -1
- package/dist/dev-tools/browser-logs.d.ts.map +1 -0
- package/dist/{plugins/dev-error-page.d.ts → dev-tools/error-page.d.ts} +2 -2
- package/dist/dev-tools/error-page.d.ts.map +1 -0
- package/dist/{server/dev-holding-server.d.ts → dev-tools/holding-server.d.ts} +1 -1
- package/dist/dev-tools/holding-server.d.ts.map +1 -0
- package/dist/dev-tools/index.d.ts +31 -0
- package/dist/dev-tools/index.d.ts.map +1 -0
- package/dist/{server/dev-span-processor.d.ts → dev-tools/instrumentation.d.ts} +26 -6
- package/dist/dev-tools/instrumentation.d.ts.map +1 -0
- package/dist/{server/dev-logger.d.ts → dev-tools/logger.d.ts} +1 -1
- package/dist/dev-tools/logger.d.ts.map +1 -0
- package/dist/{plugins/dev-logs.d.ts → dev-tools/logs.d.ts} +1 -1
- package/dist/dev-tools/logs.d.ts.map +1 -0
- package/dist/{plugins/dev-error-overlay.d.ts → dev-tools/overlay.d.ts} +3 -12
- package/dist/dev-tools/overlay.d.ts.map +1 -0
- package/dist/dev-tools/stack-classifier.d.ts +34 -0
- package/dist/dev-tools/stack-classifier.d.ts.map +1 -0
- package/dist/{plugins/dev-terminal-error.d.ts → dev-tools/terminal.d.ts} +2 -2
- package/dist/dev-tools/terminal.d.ts.map +1 -0
- package/dist/{server/dev-warnings.d.ts → dev-tools/warnings.d.ts} +1 -1
- package/dist/dev-tools/warnings.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +97 -72
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +1 -1
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/index.js +1 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/status-file-lint.d.ts.map +1 -1
- package/dist/search-params/define.d.ts +25 -7
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.js +5 -3
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/wrappers.d.ts +2 -2
- package/dist/search-params/wrappers.d.ts.map +1 -1
- package/dist/segment-params/define.d.ts +23 -6
- package/dist/segment-params/define.d.ts.map +1 -1
- package/dist/segment-params/index.js +1 -1
- package/dist/server/access-gate.d.ts +4 -3
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts +15 -6
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +5 -5
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/asset-headers.d.ts +1 -15
- package/dist/server/asset-headers.d.ts.map +1 -1
- package/dist/server/cookie-context.d.ts +170 -0
- package/dist/server/cookie-context.d.ts.map +1 -0
- package/dist/server/cookie-parsing.d.ts +51 -0
- package/dist/server/cookie-parsing.d.ts.map +1 -0
- package/dist/server/deny-boundary.d.ts +90 -0
- package/dist/server/deny-boundary.d.ts.map +1 -0
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/early-hints-sender.d.ts.map +1 -1
- package/dist/server/index.d.ts +5 -4
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +4 -149
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +6 -4
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +261 -408
- package/dist/server/internal.js.map +1 -1
- package/dist/server/logger.d.ts +14 -0
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/middleware-runner.d.ts +17 -0
- package/dist/server/middleware-runner.d.ts.map +1 -1
- package/dist/server/param-coercion.d.ts +26 -0
- package/dist/server/param-coercion.d.ts.map +1 -0
- package/dist/server/pipeline-helpers.d.ts +14 -7
- package/dist/server/pipeline-helpers.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts +49 -0
- package/dist/server/pipeline-outcome.d.ts.map +1 -0
- package/dist/server/pipeline-phases.d.ts +4 -49
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +0 -2
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +22 -159
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts +66 -0
- package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -0
- package/dist/server/rsc-entry/helpers.d.ts +1 -1
- 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 +50 -0
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -0
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +59 -14
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -1
- package/dist/server/state-tree-diff.d.ts.map +1 -1
- package/dist/server/tracing.d.ts +1 -1
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +45 -16
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/server/types.d.ts +48 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/utils/escape-html.d.ts +14 -0
- package/dist/server/utils/escape-html.d.ts.map +1 -0
- package/dist/shims/headers.d.ts +2 -2
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/navigation-client.d.ts +3 -1
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +9 -4
- package/dist/shims/navigation.d.ts.map +1 -1
- package/package.json +6 -7
- package/src/adapters/build-output-helper.ts +77 -0
- package/src/adapters/cloudflare.ts +10 -50
- package/src/adapters/nitro.ts +11 -45
- package/src/adapters/shared.ts +40 -0
- package/src/cache/timber-cache.ts +3 -2
- package/src/cli.ts +0 -0
- package/src/client/form.tsx +17 -25
- package/src/client/index.ts +16 -9
- package/src/client/internal.ts +3 -2
- package/src/client/router.ts +1 -1
- package/src/client/rsc-fetch.ts +15 -0
- package/src/client/state.ts +2 -2
- package/src/client/use-cookie.ts +29 -0
- package/src/codec.ts +3 -7
- package/src/config-types.ts +28 -0
- package/src/cookies/define-cookie.ts +271 -78
- package/src/cookies/index.ts +11 -8
- package/src/cookies/json-cookie.ts +105 -0
- package/src/cookies/validation.ts +134 -0
- package/src/{plugins/dev-404-page.ts → dev-tools/404-page.ts} +2 -7
- package/src/{plugins/dev-error-page.ts → dev-tools/error-page.ts} +5 -32
- package/src/dev-tools/index.ts +90 -0
- package/src/dev-tools/instrumentation.ts +176 -0
- package/src/{plugins/dev-logs.ts → dev-tools/logs.ts} +2 -2
- package/src/{plugins/dev-error-overlay.ts → dev-tools/overlay.ts} +5 -23
- package/src/dev-tools/stack-classifier.ts +75 -0
- package/src/{plugins/dev-terminal-error.ts → dev-tools/terminal.ts} +4 -38
- package/src/{server/dev-warnings.ts → dev-tools/warnings.ts} +1 -1
- package/src/index.ts +11 -3
- package/src/plugin-context.ts +1 -1
- package/src/plugins/adapter-build.ts +3 -1
- package/src/plugins/dev-server.ts +3 -3
- package/src/plugins/shims.ts +1 -1
- package/src/plugins/static-build.ts +1 -1
- package/src/routing/convention-lint.ts +5 -4
- package/src/routing/scanner.ts +5 -2
- package/src/routing/status-file-lint.ts +4 -2
- package/src/search-params/define.ts +71 -15
- package/src/search-params/wrappers.ts +9 -2
- package/src/segment-params/define.ts +66 -13
- package/src/server/access-gate.tsx +9 -8
- package/src/server/action-handler.ts +28 -38
- package/src/server/als-registry.ts +5 -5
- package/src/server/asset-headers.ts +8 -34
- package/src/server/cookie-context.ts +468 -0
- package/src/server/cookie-parsing.ts +135 -0
- package/src/server/{deny-page-resolver.ts → deny-boundary.ts} +78 -14
- package/src/server/deny-renderer.ts +2 -7
- package/src/server/early-hints-sender.ts +3 -2
- package/src/server/fallback-error.ts +1 -1
- package/src/server/index.ts +13 -14
- package/src/server/internal.ts +10 -3
- package/src/server/logger.ts +23 -0
- package/src/server/middleware-runner.ts +44 -0
- package/src/server/param-coercion.ts +76 -0
- package/src/server/pipeline-helpers.ts +37 -13
- package/src/server/pipeline-outcome.ts +167 -0
- package/src/server/pipeline-phases.ts +27 -209
- package/src/server/pipeline.ts +2 -9
- package/src/server/request-context.ts +46 -451
- package/src/server/route-element-builder.ts +7 -3
- package/src/server/rsc-entry/action-middleware-runner.ts +167 -0
- package/src/server/rsc-entry/error-renderer.ts +1 -1
- package/src/server/rsc-entry/helpers.ts +2 -7
- package/src/server/rsc-entry/index.ts +34 -273
- package/src/server/rsc-entry/render-route.ts +304 -0
- package/src/server/rsc-entry/rsc-payload.ts +1 -1
- package/src/server/rsc-entry/ssr-renderer.ts +2 -2
- package/src/server/rsc-entry/wrap-action-dispatch.ts +316 -23
- package/src/server/ssr-entry.ts +1 -1
- package/src/server/state-tree-diff.ts +4 -1
- package/src/server/tracing.ts +3 -3
- package/src/server/tree-builder.ts +128 -52
- package/src/server/types.ts +52 -0
- package/src/server/utils/escape-html.ts +20 -0
- package/src/shims/headers.ts +3 -3
- package/src/shims/navigation-client.ts +4 -3
- package/src/shims/navigation.ts +9 -7
- package/dist/_chunks/actions-DLnUaR65.js +0 -421
- package/dist/_chunks/actions-DLnUaR65.js.map +0 -1
- package/dist/_chunks/als-registry-HS0LGUl2.js +0 -41
- package/dist/_chunks/als-registry-HS0LGUl2.js.map +0 -1
- package/dist/_chunks/debug-ECi_61pb.js +0 -108
- package/dist/_chunks/debug-ECi_61pb.js.map +0 -1
- package/dist/_chunks/define-C77ScO0m.js.map +0 -1
- package/dist/_chunks/define-Itxvcd7F.js.map +0 -1
- package/dist/_chunks/define-cookie-BowvzoP0.js +0 -94
- package/dist/_chunks/define-cookie-BowvzoP0.js.map +0 -1
- package/dist/_chunks/dev-warnings-DpGRGoDi.js.map +0 -1
- package/dist/_chunks/merge-search-params-Cm_KIWDX.js +0 -41
- package/dist/_chunks/merge-search-params-Cm_KIWDX.js.map +0 -1
- package/dist/_chunks/request-context-CK5tZqIP.js +0 -478
- package/dist/_chunks/request-context-CK5tZqIP.js.map +0 -1
- package/dist/_chunks/router-ref-C8OCm7g7.js.map +0 -1
- package/dist/_chunks/tracing-CCYbKn5n.js +0 -238
- package/dist/_chunks/tracing-CCYbKn5n.js.map +0 -1
- package/dist/_chunks/use-params-IOPu7E8t.js.map +0 -1
- package/dist/_chunks/use-query-states-BiV5GJgm.js.map +0 -1
- package/dist/_chunks/walkers-VOXgavMF.js.map +0 -1
- package/dist/client/use-params.d.ts.map +0 -1
- package/dist/plugins/dev-404-page.d.ts.map +0 -1
- package/dist/plugins/dev-browser-logs.d.ts.map +0 -1
- package/dist/plugins/dev-error-overlay.d.ts.map +0 -1
- package/dist/plugins/dev-error-page.d.ts.map +0 -1
- package/dist/plugins/dev-logs.d.ts.map +0 -1
- package/dist/plugins/dev-terminal-error.d.ts.map +0 -1
- package/dist/server/deny-page-resolver.d.ts +0 -52
- package/dist/server/deny-page-resolver.d.ts.map +0 -1
- package/dist/server/dev-fetch-instrumentation.d.ts +0 -22
- package/dist/server/dev-fetch-instrumentation.d.ts.map +0 -1
- package/dist/server/dev-holding-server.d.ts.map +0 -1
- package/dist/server/dev-logger.d.ts.map +0 -1
- package/dist/server/dev-span-processor.d.ts.map +0 -1
- package/dist/server/dev-warnings.d.ts.map +0 -1
- package/dist/server/page-deny-boundary.d.ts +0 -31
- package/dist/server/page-deny-boundary.d.ts.map +0 -1
- package/src/server/dev-fetch-instrumentation.ts +0 -96
- package/src/server/dev-span-processor.ts +0 -78
- package/src/server/page-deny-boundary.tsx +0 -56
- /package/src/client/{use-params.ts → use-segment-params.ts} +0 -0
- /package/src/{plugins/dev-browser-logs.ts → dev-tools/browser-logs.ts} +0 -0
- /package/src/{server/dev-holding-server.ts → dev-tools/holding-server.ts} +0 -0
- /package/src/{server/dev-logger.ts → dev-tools/logger.ts} +0 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie name and value validation per RFC 6265 §4.1.1.
|
|
3
|
+
*
|
|
4
|
+
* This module is pure — no server or client imports — so it can be loaded
|
|
5
|
+
* by both `server/request-context.ts` (which guards `getCookies().set()`)
|
|
6
|
+
* and `client/use-cookie.ts` (which guards `useCookie()`'s setter). The
|
|
7
|
+
* shared validator gives both sides identical encoding contracts:
|
|
8
|
+
* **the framework never silently encodes — callers emit `cookie-octet`
|
|
9
|
+
* bytes or get a developer-facing error.**
|
|
10
|
+
*
|
|
11
|
+
* Why this matters: see ONGOING_SECURITY.md H-3 (TIM-868) and
|
|
12
|
+
* design/29-cookies.md §"Cookie Name and Value Validation".
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ─── RFC 6265 cookie-octet ────────────────────────────────────────────────
|
|
16
|
+
//
|
|
17
|
+
// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
|
|
18
|
+
//
|
|
19
|
+
// Equivalently: US-ASCII printable bytes excluding whitespace, `"` (0x22),
|
|
20
|
+
// `,` (0x2C), `;` (0x3B), `\` (0x5C), and DEL (0x7F).
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* RFC 7230 §3.2.6 token characters used as the cookie-name production in
|
|
24
|
+
* RFC 6265 §4.1.1. Anchored, non-empty match.
|
|
25
|
+
*
|
|
26
|
+
* Allowed: US-ASCII letters, digits, and `!#$%&'*+-.^_` `` ` `` `|~`.
|
|
27
|
+
* Disallowed: whitespace, controls, delimiters
|
|
28
|
+
* (`(` `)` `<` `>` `@` `,` `;` `:` `\` `"` `/` `[` `]` `?` `=` `{` `}`).
|
|
29
|
+
*/
|
|
30
|
+
const COOKIE_NAME_RE = /^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$/;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Format a single byte for a clear error message — escape control bytes
|
|
34
|
+
* and non-ASCII as `0x..` and quote printables.
|
|
35
|
+
*/
|
|
36
|
+
function describeChar(value: string, index: number): string {
|
|
37
|
+
const code = value.charCodeAt(index);
|
|
38
|
+
if (code < 0x20 || code === 0x7f || code > 0x7e) {
|
|
39
|
+
return `0x${code.toString(16).padStart(2, '0')}`;
|
|
40
|
+
}
|
|
41
|
+
return JSON.stringify(value[index]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validate a cookie name against RFC 6265 §4.1.1 / RFC 7230 token rules.
|
|
46
|
+
* Throws a developer-facing error naming the offending byte and its index.
|
|
47
|
+
*/
|
|
48
|
+
export function assertValidCookieName(name: string): void {
|
|
49
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
50
|
+
throw new Error('[timber] cookie name must be a non-empty string.');
|
|
51
|
+
}
|
|
52
|
+
if (!COOKIE_NAME_RE.test(name)) {
|
|
53
|
+
for (let i = 0; i < name.length; i++) {
|
|
54
|
+
if (!COOKIE_NAME_RE.test(name[i])) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`[timber] invalid cookie name ${JSON.stringify(name)} — ` +
|
|
57
|
+
`disallowed character ${describeChar(name, i)} at index ${i}. ` +
|
|
58
|
+
`Cookie names must match the RFC 6265 §4.1.1 token grammar (US-ASCII letters, digits, ` +
|
|
59
|
+
`and \`!#$%&'*+-.^_\`|~\`; no whitespace, controls, or delimiters).`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
throw new Error(
|
|
64
|
+
`[timber] invalid cookie name ${JSON.stringify(name)} — ` +
|
|
65
|
+
`must match RFC 6265 §4.1.1 token grammar.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Single-byte test for RFC 6265 §4.1.1 cookie-octet. */
|
|
71
|
+
function isCookieOctet(code: number): boolean {
|
|
72
|
+
return (
|
|
73
|
+
code === 0x21 ||
|
|
74
|
+
(code >= 0x23 && code <= 0x2b) ||
|
|
75
|
+
(code >= 0x2d && code <= 0x3a) ||
|
|
76
|
+
(code >= 0x3c && code <= 0x5b) ||
|
|
77
|
+
(code >= 0x5d && code <= 0x7e)
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Validate a cookie value against the RFC 6265 §4.1.1 `cookie-value`
|
|
83
|
+
* production:
|
|
84
|
+
*
|
|
85
|
+
* cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
|
|
86
|
+
* cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
|
|
87
|
+
*
|
|
88
|
+
* Both the bare `*cookie-octet` form and the DQUOTE-wrapped form are
|
|
89
|
+
* valid. The wrapped form is common in upstream `Set-Cookie` headers
|
|
90
|
+
* (e.g. `sid="abc"; Path=/`) and must be forwardable through
|
|
91
|
+
* `setFromHeaders` verbatim — stripping or rejecting the surrounding
|
|
92
|
+
* quotes would corrupt the on-the-wire bytes and break value semantics
|
|
93
|
+
* for the upstream service.
|
|
94
|
+
*
|
|
95
|
+
* Excluded bytes (in either form): CTL, whitespace (CR/LF/TAB/SP),
|
|
96
|
+
* interior DQUOTE, comma, semicolon, backslash, DEL, and anything
|
|
97
|
+
* non-ASCII. The smuggling primitive (`;`) is rejected regardless of
|
|
98
|
+
* wrapping.
|
|
99
|
+
*
|
|
100
|
+
* Throws a developer-facing error naming the offending byte and its
|
|
101
|
+
* index. Callers passing user-controlled data through the default
|
|
102
|
+
* `set()` path don't need to call this — `set()` auto-encodes via
|
|
103
|
+
* `encodeURIComponent`, whose output is always valid cookie-octet.
|
|
104
|
+
* This validator is for the `{ raw: true }` opt-out and for
|
|
105
|
+
* `setFromHeaders` forwarding.
|
|
106
|
+
*/
|
|
107
|
+
export function assertValidCookieValue(name: string, value: string): void {
|
|
108
|
+
if (typeof value !== 'string') {
|
|
109
|
+
throw new Error(`[timber] cookie ${JSON.stringify(name)}: cookie value must be a string.`);
|
|
110
|
+
}
|
|
111
|
+
// Detect the DQUOTE-wrapped form: at least two characters and both
|
|
112
|
+
// endpoints are `"`. The interior must be pure cookie-octet (no
|
|
113
|
+
// interior DQUOTEs allowed — the grammar says *cookie-octet, and
|
|
114
|
+
// cookie-octet excludes 0x22).
|
|
115
|
+
const wrapped =
|
|
116
|
+
value.length >= 2 &&
|
|
117
|
+
value.charCodeAt(0) === 0x22 &&
|
|
118
|
+
value.charCodeAt(value.length - 1) === 0x22;
|
|
119
|
+
const start = wrapped ? 1 : 0;
|
|
120
|
+
const end = wrapped ? value.length - 1 : value.length;
|
|
121
|
+
for (let i = start; i < end; i++) {
|
|
122
|
+
const code = value.charCodeAt(i);
|
|
123
|
+
if (!isCookieOctet(code)) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`[timber] cookie ${JSON.stringify(name)}: invalid cookie value — ` +
|
|
126
|
+
`disallowed character ${describeChar(value, i)} at index ${i}. ` +
|
|
127
|
+
`Cookie values must contain only RFC 6265 §4.1.1 cookie-octet bytes ` +
|
|
128
|
+
`(US-ASCII printable, excluding whitespace, \`"\`, \`,\`, \`;\`, and \`\\\`), ` +
|
|
129
|
+
`optionally enclosed in a single DQUOTE pair. ` +
|
|
130
|
+
`Encode the value (e.g. encodeURIComponent or jsonCookieCodec) before storing it.`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import type { SegmentNode } from '../routing/types.js';
|
|
17
17
|
import { collectLeafRoutes } from '../routing/walkers.js';
|
|
18
|
+
import { escapeHtml } from '../server/utils/escape-html.js';
|
|
18
19
|
|
|
19
20
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
20
21
|
|
|
@@ -104,13 +105,7 @@ export function findSimilarRoutes(requestedPath: string, routes: RouteInfo[]): R
|
|
|
104
105
|
|
|
105
106
|
// ─── HTML Escaping ──────────────────────────────────────────────────────────
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
return str
|
|
109
|
-
.replace(/&/g, '&')
|
|
110
|
-
.replace(/</g, '<')
|
|
111
|
-
.replace(/>/g, '>')
|
|
112
|
-
.replace(/"/g, '"');
|
|
113
|
-
}
|
|
108
|
+
const esc = escapeHtml;
|
|
114
109
|
|
|
115
110
|
// ─── HTML Generation ────────────────────────────────────────────────────────
|
|
116
111
|
|
|
@@ -15,19 +15,12 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import {
|
|
18
|
-
classifyFrame,
|
|
19
18
|
extractComponentStack,
|
|
20
19
|
parseFirstAppFrame,
|
|
21
20
|
type ErrorPhase,
|
|
22
|
-
|
|
23
|
-
} from './
|
|
24
|
-
|
|
25
|
-
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
interface ClassifiedFrame {
|
|
28
|
-
raw: string;
|
|
29
|
-
type: FrameType;
|
|
30
|
-
}
|
|
21
|
+
} from './overlay.js';
|
|
22
|
+
import { classifyStack } from './stack-classifier.js';
|
|
23
|
+
import { escapeHtml } from '../server/utils/escape-html.js';
|
|
31
24
|
|
|
32
25
|
// ─── Phase Labels ───────────────────────────────────────────────────────────
|
|
33
26
|
|
|
@@ -52,19 +45,6 @@ const PHASE_HINTS: Record<ErrorPhase, string> = {
|
|
|
52
45
|
'handler': 'This error occurred in a route handler (route.ts). Check the handler function.',
|
|
53
46
|
};
|
|
54
47
|
|
|
55
|
-
// ─── Stack Trace Processing ─────────────────────────────────────────────────
|
|
56
|
-
|
|
57
|
-
function classifyStack(stack: string, projectRoot: string): ClassifiedFrame[] {
|
|
58
|
-
return stack
|
|
59
|
-
.split('\n')
|
|
60
|
-
.slice(1) // skip first line (error message)
|
|
61
|
-
.filter((line) => line.trim().startsWith('at '))
|
|
62
|
-
.map((line) => ({
|
|
63
|
-
raw: line,
|
|
64
|
-
type: classifyFrame(line, projectRoot),
|
|
65
|
-
}));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
48
|
// ─── Source Context ─────────────────────────────────────────────────────────
|
|
69
49
|
|
|
70
50
|
/**
|
|
@@ -97,15 +77,8 @@ function readSourceContext(
|
|
|
97
77
|
}
|
|
98
78
|
}
|
|
99
79
|
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
function esc(str: string): string {
|
|
103
|
-
return str
|
|
104
|
-
.replace(/&/g, '&')
|
|
105
|
-
.replace(/</g, '<')
|
|
106
|
-
.replace(/>/g, '>')
|
|
107
|
-
.replace(/"/g, '"');
|
|
108
|
-
}
|
|
80
|
+
// HTML escaping provided by server/utils/escape-html.ts
|
|
81
|
+
const esc = escapeHtml;
|
|
109
82
|
|
|
110
83
|
/**
|
|
111
84
|
* Escape a JSON string for safe embedding in an HTML `<script>` block.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev tools barrel — re-exports all dev-only modules.
|
|
3
|
+
*
|
|
4
|
+
* Organized by concern, not by Vite plugin vs server module:
|
|
5
|
+
* - stack-classifier: frame classification for error display
|
|
6
|
+
* - overlay: Vite browser error overlay integration
|
|
7
|
+
* - error-page: self-contained HTML 500 page
|
|
8
|
+
* - 404-page: self-contained HTML 404 page
|
|
9
|
+
* - terminal: boxed terminal error output
|
|
10
|
+
* - logger: structured request tree logger
|
|
11
|
+
* - warnings: dev-mode footgun warnings
|
|
12
|
+
* - browser-logs: forward browser console to server
|
|
13
|
+
* - logs: forward server console to browser
|
|
14
|
+
* - holding-server: startup holding server
|
|
15
|
+
* - instrumentation: fetch tracing + OTEL span processor
|
|
16
|
+
*
|
|
17
|
+
* Dev-only: all modules in this subtree are only active during `vite dev`.
|
|
18
|
+
* They are never included in production builds.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// Stack trace classification
|
|
22
|
+
export {
|
|
23
|
+
classifyFrame,
|
|
24
|
+
classifyStack,
|
|
25
|
+
type FrameType,
|
|
26
|
+
type ClassifiedFrame,
|
|
27
|
+
} from './stack-classifier.js';
|
|
28
|
+
|
|
29
|
+
// Error overlay (Vite integration)
|
|
30
|
+
export {
|
|
31
|
+
sendErrorToOverlay,
|
|
32
|
+
fixErrorStacktrace,
|
|
33
|
+
classifyErrorPhase,
|
|
34
|
+
extractComponentStack,
|
|
35
|
+
parseFirstAppFrame,
|
|
36
|
+
calculateModuleRunnerOffset,
|
|
37
|
+
formatRscDebugContext,
|
|
38
|
+
formatTerminalError,
|
|
39
|
+
PHASE_LABELS,
|
|
40
|
+
type ErrorPhase,
|
|
41
|
+
type RscDebugComponentInfo,
|
|
42
|
+
} from './overlay.js';
|
|
43
|
+
|
|
44
|
+
// HTML error pages
|
|
45
|
+
export { generateDevErrorPage, extractHmrOptions, type DevErrorHmrOptions } from './error-page.js';
|
|
46
|
+
export { generateDev404Page, collectRoutes, findSimilarRoutes } from './404-page.js';
|
|
47
|
+
|
|
48
|
+
// Terminal error formatting
|
|
49
|
+
export { formatTerminalError as formatTerminalErrorDirect } from './terminal.js';
|
|
50
|
+
|
|
51
|
+
// Request tree logger
|
|
52
|
+
export {
|
|
53
|
+
formatSpanTree,
|
|
54
|
+
formatSpanSummary,
|
|
55
|
+
formatJson,
|
|
56
|
+
resolveLogMode,
|
|
57
|
+
type DevLogMode,
|
|
58
|
+
type DevLoggerConfig,
|
|
59
|
+
} from './logger.js';
|
|
60
|
+
|
|
61
|
+
// Dev-mode warnings
|
|
62
|
+
export {
|
|
63
|
+
warnSuspenseWrappingChildren,
|
|
64
|
+
warnDenyInSuspense,
|
|
65
|
+
warnRedirectInSuspense,
|
|
66
|
+
warnRedirectInAccess,
|
|
67
|
+
warnStaticRequestApi,
|
|
68
|
+
warnSlowSlotWithoutSuspense,
|
|
69
|
+
setViteServer,
|
|
70
|
+
WarningId,
|
|
71
|
+
type DevWarningConfig,
|
|
72
|
+
_resetWarnings,
|
|
73
|
+
_getEmitted,
|
|
74
|
+
} from './warnings.js';
|
|
75
|
+
|
|
76
|
+
// Browser log forwarding
|
|
77
|
+
export {
|
|
78
|
+
timberDevBrowserLogs,
|
|
79
|
+
type BrowserLogLevel,
|
|
80
|
+
type DevBrowserLogsConfig,
|
|
81
|
+
} from './browser-logs.js';
|
|
82
|
+
|
|
83
|
+
// Server log forwarding
|
|
84
|
+
export { timberDevLogs } from './logs.js';
|
|
85
|
+
|
|
86
|
+
// Holding server
|
|
87
|
+
export { createHoldingServer, type HoldingServer } from './holding-server.js';
|
|
88
|
+
|
|
89
|
+
// OTEL instrumentation
|
|
90
|
+
export { instrumentDevFetch, DevSpanProcessor, type DevFetchCleanup } from './instrumentation.js';
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-mode OTEL instrumentation — fetch tracing and span processing.
|
|
3
|
+
*
|
|
4
|
+
* Combines two concerns:
|
|
5
|
+
* 1. Fetch instrumentation: patches globalThis.fetch to create OTEL spans
|
|
6
|
+
* for every fetch call, giving visibility into async data fetching in
|
|
7
|
+
* the dev request log tree.
|
|
8
|
+
* 2. Span processing: collects completed spans per-request and drives
|
|
9
|
+
* dev log output when the root span ends.
|
|
10
|
+
*
|
|
11
|
+
* Only activated in dev mode — zero overhead in production.
|
|
12
|
+
*
|
|
13
|
+
* Design ref: 17-logging.md §"Dev Logging", 21-dev-server.md §"Dev Logging"
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as api from '@opentelemetry/api';
|
|
17
|
+
import type { SpanProcessor, ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
18
|
+
import type { Span, Context } from '@opentelemetry/api';
|
|
19
|
+
import {
|
|
20
|
+
formatSpanTree,
|
|
21
|
+
formatSpanSummary,
|
|
22
|
+
formatJson,
|
|
23
|
+
type DevLogMode,
|
|
24
|
+
type DevLoggerConfig,
|
|
25
|
+
} from './logger.js';
|
|
26
|
+
|
|
27
|
+
// ─── Fetch Instrumentation ──────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export type DevFetchCleanup = () => void;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Patch globalThis.fetch to wrap every call in an OTEL span.
|
|
33
|
+
*
|
|
34
|
+
* Returns a cleanup function that restores the original fetch.
|
|
35
|
+
* Only call this in dev mode.
|
|
36
|
+
*/
|
|
37
|
+
export function instrumentDevFetch(): DevFetchCleanup {
|
|
38
|
+
const originalFetch = globalThis.fetch;
|
|
39
|
+
const tracer = api.trace.getTracer('timber.js');
|
|
40
|
+
|
|
41
|
+
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
42
|
+
const { method, url } = extractFetchInfo(input, init);
|
|
43
|
+
|
|
44
|
+
return tracer.startActiveSpan(
|
|
45
|
+
'timber.fetch',
|
|
46
|
+
{
|
|
47
|
+
attributes: {
|
|
48
|
+
'http.request.method': method,
|
|
49
|
+
'http.url': url,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
async (span) => {
|
|
53
|
+
try {
|
|
54
|
+
const response = await originalFetch(input, init);
|
|
55
|
+
|
|
56
|
+
span.setAttribute('http.response.status_code', response.status);
|
|
57
|
+
|
|
58
|
+
// Surface cache status from standard headers
|
|
59
|
+
const cacheStatus =
|
|
60
|
+
response.headers.get('X-Cache') ?? response.headers.get('CF-Cache-Status');
|
|
61
|
+
if (cacheStatus) {
|
|
62
|
+
span.setAttribute('timber.cache_status', cacheStatus);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
span.setStatus({ code: api.SpanStatusCode.OK });
|
|
66
|
+
span.end();
|
|
67
|
+
return response;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR });
|
|
70
|
+
if (error instanceof Error) {
|
|
71
|
+
span.setAttribute('timber.fetch_error', error.message);
|
|
72
|
+
span.recordException(error);
|
|
73
|
+
}
|
|
74
|
+
span.end();
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return () => {
|
|
82
|
+
globalThis.fetch = originalFetch;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extract method and URL from the various fetch() call signatures.
|
|
88
|
+
*/
|
|
89
|
+
function extractFetchInfo(
|
|
90
|
+
input: RequestInfo | URL,
|
|
91
|
+
init?: RequestInit
|
|
92
|
+
): { method: string; url: string } {
|
|
93
|
+
let method = init?.method ?? 'GET';
|
|
94
|
+
let url: string;
|
|
95
|
+
|
|
96
|
+
if (input instanceof Request) {
|
|
97
|
+
url = input.url;
|
|
98
|
+
if (!init?.method) {
|
|
99
|
+
method = input.method;
|
|
100
|
+
}
|
|
101
|
+
} else if (input instanceof URL) {
|
|
102
|
+
url = input.toString();
|
|
103
|
+
} else {
|
|
104
|
+
url = input;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { method: method.toUpperCase(), url };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Span Processor ─────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* DevSpanProcessor — Custom OTEL SpanProcessor that drives dev log output.
|
|
114
|
+
*
|
|
115
|
+
* Collects completed spans per-request (correlated by trace ID). When the
|
|
116
|
+
* root span (http.server.request) ends, all child spans are already collected
|
|
117
|
+
* (child spans end before parent in OTEL). The processor formats the span
|
|
118
|
+
* tree and writes it to stderr.
|
|
119
|
+
*
|
|
120
|
+
* This replaces the old DevLogEmitter/DevLogEvents system. OTEL spans are
|
|
121
|
+
* now the single source of truth for dev logging — no more parallel event
|
|
122
|
+
* systems that can drift.
|
|
123
|
+
*/
|
|
124
|
+
export class DevSpanProcessor implements SpanProcessor {
|
|
125
|
+
private spansByTrace = new Map<string, ReadableSpan[]>();
|
|
126
|
+
private mode: DevLogMode;
|
|
127
|
+
private config: DevLoggerConfig;
|
|
128
|
+
|
|
129
|
+
constructor(config: DevLoggerConfig) {
|
|
130
|
+
this.config = config;
|
|
131
|
+
this.mode = config.mode ?? 'tree';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
onStart(_span: Span, _context: Context): void {
|
|
135
|
+
// No action needed on span start — we collect on end.
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
onEnd(span: ReadableSpan): void {
|
|
139
|
+
const traceId = span.spanContext().traceId;
|
|
140
|
+
|
|
141
|
+
let spans = this.spansByTrace.get(traceId);
|
|
142
|
+
if (!spans) {
|
|
143
|
+
spans = [];
|
|
144
|
+
this.spansByTrace.set(traceId, spans);
|
|
145
|
+
}
|
|
146
|
+
spans.push(span);
|
|
147
|
+
|
|
148
|
+
// Root span signals request completion — all child spans are already
|
|
149
|
+
// collected because OTEL ends child spans before parent spans.
|
|
150
|
+
if (span.name === 'http.server.request') {
|
|
151
|
+
const output = this.format(spans);
|
|
152
|
+
if (output) {
|
|
153
|
+
process.stderr.write(output);
|
|
154
|
+
}
|
|
155
|
+
this.spansByTrace.delete(traceId);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private format(spans: ReadableSpan[]): string {
|
|
160
|
+
if (this.mode === 'quiet') return '';
|
|
161
|
+
if (this.mode === 'json') return formatJson(spans);
|
|
162
|
+
if (this.mode === 'summary') return formatSpanSummary(spans, this.config);
|
|
163
|
+
// Both 'tree' and 'verbose' use the tree formatter.
|
|
164
|
+
// verbose will show additional detail (every component render) once
|
|
165
|
+
// component-level spans are wired.
|
|
166
|
+
return formatSpanTree(spans, this.config);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async shutdown(): Promise<void> {
|
|
170
|
+
this.spansByTrace.clear();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async forceFlush(): Promise<void> {
|
|
174
|
+
// Nothing to flush — output happens synchronously in onEnd.
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -149,7 +149,7 @@ function extractCallerLocation(projectRoot: string): string | null {
|
|
|
149
149
|
for (let i = 1; i < lines.length; i++) {
|
|
150
150
|
const line = lines[i].trim();
|
|
151
151
|
// Skip our own patching frames
|
|
152
|
-
if (line.includes('dev-logs')) continue;
|
|
152
|
+
if (line.includes('dev-tools/logs')) continue;
|
|
153
153
|
// Skip node internals
|
|
154
154
|
if (line.includes('node:') || line.includes('node_modules')) continue;
|
|
155
155
|
|
|
@@ -189,7 +189,7 @@ export function isFrameworkInternalCaller(): boolean {
|
|
|
189
189
|
for (let i = 1; i < lines.length; i++) {
|
|
190
190
|
const line = lines[i].trim();
|
|
191
191
|
// Skip our own patching frames
|
|
192
|
-
if (line.includes('dev-logs')) continue;
|
|
192
|
+
if (line.includes('dev-tools/logs')) continue;
|
|
193
193
|
// Skip node internals (but NOT node_modules — we need to inspect those)
|
|
194
194
|
if (line.includes('node:')) continue;
|
|
195
195
|
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import type { ViteDevServer, DevEnvironment } from 'vite';
|
|
19
19
|
import { createRequire } from 'node:module';
|
|
20
20
|
import { resolve, dirname } from 'node:path';
|
|
21
|
+
import { classifyFrame, type FrameType } from './stack-classifier.js';
|
|
21
22
|
|
|
22
23
|
// ─── Trace Mapping (lazy-loaded) ─────────────────────────────────────────────
|
|
23
24
|
|
|
@@ -70,28 +71,9 @@ export const PHASE_LABELS: Record<ErrorPhase, string> = {
|
|
|
70
71
|
'handler': 'Route Handler',
|
|
71
72
|
};
|
|
72
73
|
|
|
73
|
-
// ─── Frame Classification
|
|
74
|
+
// ─── Frame Classification (re-exported from stack-classifier) ───────────────
|
|
74
75
|
|
|
75
|
-
export type FrameType
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Classify a stack frame line by origin.
|
|
79
|
-
*
|
|
80
|
-
* - 'app': user application code (in project root, not node_modules)
|
|
81
|
-
* - 'framework': timber-app internal code
|
|
82
|
-
* - 'internal': node_modules, Node.js internals
|
|
83
|
-
*/
|
|
84
|
-
export function classifyFrame(frameLine: string, projectRoot: string): FrameType {
|
|
85
|
-
// Strip leading whitespace and "at "
|
|
86
|
-
const trimmed = frameLine.trim();
|
|
87
|
-
|
|
88
|
-
if (trimmed.includes('packages/timber-app/')) return 'framework';
|
|
89
|
-
if (trimmed.includes('node_modules/')) return 'internal';
|
|
90
|
-
if (trimmed.startsWith('at node:') || trimmed.includes('(node:')) return 'internal';
|
|
91
|
-
if (trimmed.includes(projectRoot)) return 'app';
|
|
92
|
-
|
|
93
|
-
return 'internal';
|
|
94
|
-
}
|
|
76
|
+
export { classifyFrame, classifyStack, type FrameType, type ClassifiedFrame } from './stack-classifier.js';
|
|
95
77
|
|
|
96
78
|
// ─── Component Stack Extraction ─────────────────────────────────────────────
|
|
97
79
|
|
|
@@ -172,9 +154,9 @@ export function classifyErrorPhase(error: Error, projectRoot: string): ErrorPhas
|
|
|
172
154
|
// ─── Terminal Formatting ────────────────────────────────────────────────────
|
|
173
155
|
|
|
174
156
|
// formatTerminalError is implemented in the dedicated terminal formatter module
|
|
175
|
-
// (
|
|
157
|
+
// (terminal.ts) to keep this file under 500 lines.
|
|
176
158
|
// Import for use in sendErrorToOverlay, and re-export for external consumers.
|
|
177
|
-
import { formatTerminalError as _formatTerminalError } from './
|
|
159
|
+
import { formatTerminalError as _formatTerminalError } from './terminal.js';
|
|
178
160
|
export const formatTerminalError = _formatTerminalError;
|
|
179
161
|
|
|
180
162
|
// ─── RSC Debug Context ──────────────────────────────────────────────────────
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stack trace frame classifier — categorizes stack frames by origin.
|
|
3
|
+
*
|
|
4
|
+
* Provides `classifyFrame` (single frame) and `classifyStack` (full stack
|
|
5
|
+
* string) as the shared primitives used by the error page, terminal
|
|
6
|
+
* formatter, and overlay.
|
|
7
|
+
*
|
|
8
|
+
* Dev-only: this module is only imported by other dev-tools modules.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export type FrameType = 'app' | 'framework' | 'internal';
|
|
14
|
+
|
|
15
|
+
export interface ClassifiedFrame {
|
|
16
|
+
raw: string;
|
|
17
|
+
type: FrameType;
|
|
18
|
+
file?: string;
|
|
19
|
+
line?: number;
|
|
20
|
+
col?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ─── Frame Parsing ──────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/** Parse file/line/col from a stack frame line. */
|
|
26
|
+
function parseFrame(frameLine: string): { file?: string; line?: number; col?: number } {
|
|
27
|
+
const parenMatch = /\(([^)]+):(\d+):(\d+)\)/.exec(frameLine);
|
|
28
|
+
if (parenMatch) {
|
|
29
|
+
return { file: parenMatch[1], line: Number(parenMatch[2]), col: Number(parenMatch[3]) };
|
|
30
|
+
}
|
|
31
|
+
const bareMatch = /at (\/[^:]+):(\d+):(\d+)/.exec(frameLine);
|
|
32
|
+
if (bareMatch) {
|
|
33
|
+
return { file: bareMatch[1], line: Number(bareMatch[2]), col: Number(bareMatch[3]) };
|
|
34
|
+
}
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Classification ─────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Classify a single stack frame line by origin.
|
|
42
|
+
*
|
|
43
|
+
* - 'app': user application code (in project root, not node_modules)
|
|
44
|
+
* - 'framework': timber-app internal code
|
|
45
|
+
* - 'internal': node_modules, Node.js internals
|
|
46
|
+
*/
|
|
47
|
+
export function classifyFrame(frameLine: string, projectRoot: string): FrameType {
|
|
48
|
+
const trimmed = frameLine.trim();
|
|
49
|
+
|
|
50
|
+
if (trimmed.includes('packages/timber-app/')) return 'framework';
|
|
51
|
+
if (trimmed.includes('node_modules/')) return 'internal';
|
|
52
|
+
if (trimmed.startsWith('at node:') || trimmed.includes('(node:')) return 'internal';
|
|
53
|
+
if (trimmed.includes(projectRoot)) return 'app';
|
|
54
|
+
|
|
55
|
+
return 'internal';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Classify all frames in a full stack trace string.
|
|
60
|
+
*
|
|
61
|
+
* Parses the stack, skips the first line (error message), filters to
|
|
62
|
+
* lines starting with "at ", and returns classified frames with optional
|
|
63
|
+
* file/line/col metadata.
|
|
64
|
+
*/
|
|
65
|
+
export function classifyStack(stack: string, projectRoot: string): ClassifiedFrame[] {
|
|
66
|
+
return stack
|
|
67
|
+
.split('\n')
|
|
68
|
+
.slice(1) // skip first line (error message)
|
|
69
|
+
.filter((line) => line.trim().startsWith('at '))
|
|
70
|
+
.map((raw) => {
|
|
71
|
+
const type = classifyFrame(raw, projectRoot);
|
|
72
|
+
const { file, line, col } = parseFrame(raw);
|
|
73
|
+
return { raw, type, file, line, col };
|
|
74
|
+
});
|
|
75
|
+
}
|