@timber-js/app 0.2.0-alpha.146 → 0.2.0-alpha.148
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-OIZ8siQu.js → actions-a0RczDnB.js} +8 -34
- package/dist/_chunks/{actions-OIZ8siQu.js.map → actions-a0RczDnB.js.map} +1 -1
- package/dist/_chunks/cache-api-Co-yI-gP.js +424 -0
- package/dist/_chunks/cache-api-Co-yI-gP.js.map +1 -0
- package/dist/_chunks/{metadata-routes-BjepjefT.js → canonicalize-P41GR6tY.js} +65 -2
- package/dist/_chunks/canonicalize-P41GR6tY.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-D2IpXicg.js → cli-schema-sync-DJDdyEae.js} +157 -313
- package/dist/_chunks/cli-schema-sync-DJDdyEae.js.map +1 -0
- package/dist/_chunks/cli-schema-sync-D_IhHEUW.js +2 -0
- package/dist/_chunks/{define-DMVhsipD.js → define-Bssfp6ot.js} +3 -33
- package/dist/_chunks/define-Bssfp6ot.js.map +1 -0
- package/dist/_chunks/{define-C3RvHQPK.js → define-CFmvb4Bt.js} +15 -7
- package/dist/_chunks/define-CFmvb4Bt.js.map +1 -0
- package/dist/_chunks/{href-validation-DyZIzfMJ.js → href-validation-FiA_rToW.js} +2 -1
- package/dist/_chunks/{href-validation-DyZIzfMJ.js.map → href-validation-FiA_rToW.js.map} +1 -1
- package/dist/_chunks/{tracing-M_zKubhk.js → logger-CM0clugc.js} +394 -107
- package/dist/_chunks/logger-CM0clugc.js.map +1 -0
- package/dist/_chunks/navigation-root-bUDZLw74.js.map +1 -1
- package/dist/_chunks/plugin-context-DurqFBuY.js +2 -0
- package/dist/_chunks/plugin-context-GfDWsdd2.js +268 -0
- package/dist/_chunks/plugin-context-GfDWsdd2.js.map +1 -0
- package/dist/_chunks/{resolve-schema-Dj8tQloi.js → resolve-schema-3iUvBV5T.js} +3 -30
- package/dist/_chunks/resolve-schema-3iUvBV5T.js.map +1 -0
- package/dist/_chunks/{schema-bridge-DK_89h8O.js → schema-bridge-BY3QLBL7.js} +35 -2
- package/dist/_chunks/schema-bridge-BY3QLBL7.js.map +1 -0
- package/dist/_chunks/tree-match-D2l830j2.js +102 -0
- package/dist/_chunks/tree-match-D2l830j2.js.map +1 -0
- package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +1 -1
- package/dist/_chunks/{walkers-7FBPM-hX.js → walkers-DvWDj453.js} +154 -88
- package/dist/_chunks/walkers-DvWDj453.js.map +1 -0
- package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +36 -14
- package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
- package/dist/adapters/compress-module.d.ts +5 -4
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/nitro.d.ts +1 -3
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +14 -134
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/cache-api.d.ts.map +1 -1
- package/dist/cache/index.d.ts +3 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +20 -235
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/invalidation-epoch.d.ts +43 -0
- package/dist/cache/invalidation-epoch.d.ts.map +1 -0
- package/dist/cache/redis-handler.d.ts +13 -0
- package/dist/cache/redis-handler.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts +17 -9
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/cli-schema-sync.d.ts +21 -1
- package/dist/cli-schema-sync.d.ts.map +1 -1
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +26 -31
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/hmr.d.ts.map +1 -1
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +13 -4
- package/dist/client/error-boundary.js.map +1 -1
- package/dist/client/error-reconstituter.d.ts +8 -1
- package/dist/client/error-reconstituter.d.ts.map +1 -1
- package/dist/client/history.d.ts +10 -17
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +30 -33
- package/dist/client/internal.js.map +1 -1
- package/dist/client/navigation-api.d.ts +9 -2
- package/dist/client/navigation-api.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +0 -11
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +0 -5
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/use-search-params.d.ts +10 -5
- package/dist/client/use-search-params.d.ts.map +1 -1
- package/dist/codec.js +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.js +2 -4
- package/dist/cookies/index.js.map +1 -1
- package/dist/dev-tools/dev-page-shell.d.ts +1 -0
- package/dist/dev-tools/dev-page-shell.d.ts.map +1 -1
- package/dist/dev-tools/hydration-overlay.d.ts +11 -0
- package/dist/dev-tools/hydration-overlay.d.ts.map +1 -0
- package/dist/dev-tools/index.d.ts +1 -2
- package/dist/dev-tools/index.d.ts.map +1 -1
- package/dist/fonts/ast-walk.d.ts +68 -0
- package/dist/fonts/ast-walk.d.ts.map +1 -0
- package/dist/fonts/ast.d.ts +23 -11
- package/dist/fonts/ast.d.ts.map +1 -1
- package/dist/fonts/bundle.d.ts +3 -1
- package/dist/fonts/bundle.d.ts.map +1 -1
- package/dist/fonts/dev-middleware.d.ts +1 -1
- package/dist/fonts/dev-middleware.d.ts.map +1 -1
- package/dist/fonts/fallbacks.d.ts +0 -7
- package/dist/fonts/fallbacks.d.ts.map +1 -1
- package/dist/fonts/google.d.ts +4 -2
- package/dist/fonts/google.d.ts.map +1 -1
- package/dist/fonts/local.d.ts +3 -3
- package/dist/fonts/local.d.ts.map +1 -1
- package/dist/fonts/paths.d.ts +43 -0
- package/dist/fonts/paths.d.ts.map +1 -0
- package/dist/fonts/pipeline.d.ts +7 -21
- package/dist/fonts/pipeline.d.ts.map +1 -1
- package/dist/fonts/transform.d.ts.map +1 -1
- package/dist/fonts/virtual-modules.d.ts +1 -18
- package/dist/fonts/virtual-modules.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +609 -406
- package/dist/index.js.map +1 -1
- package/dist/params/index.js +1 -1
- package/dist/params/resolve-schema.d.ts.map +1 -1
- package/dist/plugin-context.d.ts +28 -5
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/cache.d.ts +13 -5
- package/dist/plugins/cache.d.ts.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +1 -2
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/routing/codegen-types.d.ts +4 -0
- package/dist/routing/codegen-types.d.ts.map +1 -1
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/file-cache.d.ts +8 -0
- package/dist/routing/file-cache.d.ts.map +1 -0
- package/dist/routing/index.js +2 -2
- package/dist/routing/interception.d.ts +0 -5
- package/dist/routing/interception.d.ts.map +1 -1
- package/dist/routing/manifest-codegen.d.ts +32 -0
- package/dist/routing/manifest-codegen.d.ts.map +1 -0
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/schema-bridge.d.ts +15 -0
- package/dist/schema-bridge.d.ts.map +1 -1
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.js +7 -3
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/wrappers.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/server/actions.d.ts +0 -14
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +0 -29
- package/dist/server/build-manifest.d.ts.map +1 -1
- package/dist/server/canonicalize.d.ts +10 -0
- package/dist/server/canonicalize.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +27 -2
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts +8 -0
- package/dist/server/html-injector-core.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.d.ts +1 -5
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +238 -349
- package/dist/server/internal.js.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +0 -11
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/render-utils.d.ts +25 -0
- package/dist/server/render-utils.d.ts.map +1 -0
- package/dist/server/route-element-builder.d.ts +11 -25
- package/dist/server/route-element-builder.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 +2 -18
- 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.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/status-code-resolver.d.ts +6 -0
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/utils/mdx-file.d.ts +15 -0
- package/dist/server/utils/mdx-file.d.ts.map +1 -0
- package/dist/shared/href-validation.d.ts.map +1 -1
- package/dist/shims/image.d.ts +15 -15
- package/package.json +3 -3
- package/src/adapters/cloudflare-kv-cache.ts +56 -19
- package/src/adapters/compress-module.ts +27 -118
- package/src/adapters/nitro.ts +3 -41
- package/src/adapters/raw-imports.d.ts +5 -0
- package/src/cache/cache-api.ts +9 -1
- package/src/cache/index.ts +5 -2
- package/src/cache/invalidation-epoch.ts +93 -0
- package/src/cache/redis-handler.ts +33 -2
- package/src/cache/timber-cache.ts +109 -53
- package/src/cli-schema-sync.ts +123 -30
- package/src/cli.ts +33 -37
- package/src/client/browser-entry/hmr.ts +0 -11
- package/src/client/browser-entry/hydrate.ts +39 -5
- package/src/client/browser-entry/post-hydration.ts +6 -15
- package/src/client/browser-entry/router-init.ts +98 -3
- package/src/client/error-boundary.tsx +14 -0
- package/src/client/error-reconstituter.tsx +15 -1
- package/src/client/history.ts +10 -24
- package/src/client/navigation-api.ts +14 -9
- package/src/client/navigation-context.ts +1 -21
- package/src/client/navigation-root.tsx +0 -8
- package/src/client/router.ts +18 -8
- package/src/client/rsc-fetch.ts +7 -2
- package/src/client/use-search-params.ts +10 -5
- package/src/config-validation.ts +33 -25
- package/src/cookies/define-cookie.ts +2 -18
- package/src/dev-tools/dev-page-shell.ts +6 -2
- package/src/dev-tools/hydration-overlay.ts +158 -0
- package/src/dev-tools/index.ts +3 -16
- package/src/fonts/ast-walk.ts +235 -0
- package/src/fonts/ast.ts +91 -249
- package/src/fonts/bundle.ts +16 -9
- package/src/fonts/dev-middleware.ts +16 -5
- package/src/fonts/fallbacks.ts +0 -23
- package/src/fonts/google.ts +7 -3
- package/src/fonts/local.ts +7 -6
- package/src/fonts/paths.ts +58 -0
- package/src/fonts/pipeline.ts +27 -48
- package/src/fonts/transform.ts +27 -0
- package/src/fonts/virtual-modules.ts +1 -57
- package/src/index.ts +5 -11
- package/src/params/resolve-schema.ts +2 -34
- package/src/plugin-context.ts +95 -17
- package/src/plugins/cache.ts +64 -17
- package/src/plugins/dev-server.ts +61 -9
- package/src/plugins/fonts.ts +21 -6
- package/src/plugins/routing.ts +5 -348
- package/src/routing/codegen-types.ts +4 -0
- package/src/routing/codegen.ts +66 -8
- package/src/routing/convention-lint.ts +4 -3
- package/src/routing/file-cache.ts +23 -0
- package/src/routing/interception.ts +2 -21
- package/src/routing/manifest-codegen.ts +290 -0
- package/src/routing/scanner.ts +85 -80
- package/src/routing/schema-validation.ts +2 -2
- package/src/schema-bridge.ts +63 -2
- package/src/search-params/define.ts +34 -2
- package/src/search-params/wrappers.ts +21 -3
- package/src/segment-params/define.ts +2 -62
- package/src/server/access-gate.tsx +8 -0
- package/src/server/actions.ts +8 -31
- package/src/server/build-manifest.ts +0 -56
- package/src/server/canonicalize.ts +6 -3
- package/src/server/deny-boundary.ts +106 -13
- package/src/server/deny-renderer.ts +46 -10
- package/src/server/error-boundary-wrapper.ts +1 -14
- package/src/server/html-injector-core.ts +19 -0
- package/src/server/html-injectors.ts +14 -4
- package/src/server/internal.ts +1 -18
- package/src/server/node-stream-transforms.ts +5 -72
- package/src/server/render-utils.ts +31 -0
- package/src/server/route-element-builder.ts +77 -47
- package/src/server/route-matcher.ts +22 -6
- package/src/server/rsc-entry/error-renderer.ts +2 -12
- package/src/server/rsc-entry/helpers.ts +6 -34
- package/src/server/rsc-entry/index.ts +11 -0
- package/src/server/rsc-entry/render-route.ts +7 -9
- package/src/server/rsc-entry/rsc-payload.ts +2 -1
- package/src/server/rsc-entry/ssr-renderer.ts +2 -2
- package/src/server/ssr-render.ts +1 -11
- package/src/server/status-code-resolver.ts +3 -2
- package/src/server/tree-match.ts +15 -5
- package/src/server/utils/mdx-file.ts +22 -0
- package/src/shared/href-validation.ts +8 -0
- package/dist/_chunks/cli-schema-sync-D2IpXicg.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-DH-M8Poy.js +0 -2
- package/dist/_chunks/define-C3RvHQPK.js.map +0 -1
- package/dist/_chunks/define-DMVhsipD.js.map +0 -1
- package/dist/_chunks/handler-store-BVDgyTh7.js +0 -78
- package/dist/_chunks/handler-store-BVDgyTh7.js.map +0 -1
- package/dist/_chunks/logger-Dt-jQaBO.js +0 -291
- package/dist/_chunks/logger-Dt-jQaBO.js.map +0 -1
- package/dist/_chunks/metadata-routes-BjepjefT.js.map +0 -1
- package/dist/_chunks/resolve-schema-Dj8tQloi.js.map +0 -1
- package/dist/_chunks/schema-bridge-DK_89h8O.js.map +0 -1
- package/dist/_chunks/tracing-M_zKubhk.js.map +0 -1
- package/dist/_chunks/walkers-7FBPM-hX.js.map +0 -1
- package/dist/_chunks/warnings-CNPXaypM.js +0 -125
- package/dist/_chunks/warnings-CNPXaypM.js.map +0 -1
- package/dist/client/link-pending-store.d.ts +0 -65
- package/dist/client/link-pending-store.d.ts.map +0 -1
- package/dist/client/nav-link-store.d.ts +0 -27
- package/dist/client/nav-link-store.d.ts.map +0 -1
- package/dist/dev-tools/warnings.d.ts +0 -109
- package/dist/dev-tools/warnings.d.ts.map +0 -1
- package/dist/server/flush.d.ts +0 -74
- package/dist/server/flush.d.ts.map +0 -1
- package/src/client/link-pending-store.ts +0 -116
- package/src/client/nav-link-store.ts +0 -37
- package/src/dev-tools/warnings.ts +0 -238
- package/src/server/flush.ts +0 -183
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hydration mismatch diff overlay — dev-only.
|
|
3
|
+
*
|
|
4
|
+
* Parses React 19's structured diff from hydration error messages and
|
|
5
|
+
* renders a color-coded + Client / - Server diff in a dismissible overlay.
|
|
6
|
+
*
|
|
7
|
+
* See design/21-dev-server.md §"Error Overlay"
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const REACT_HYDRATION_LINK = 'https://react.dev/link/hydration-mismatch';
|
|
11
|
+
|
|
12
|
+
const HYDRATION_PATTERNS = [
|
|
13
|
+
/^In HTML, (.+?) cannot be a child of <(.+?)>\./,
|
|
14
|
+
/^In HTML, (.+?) cannot be a descendant of <(.+?)>\./,
|
|
15
|
+
/^In HTML, text nodes cannot be a child of <(.+?)>\./,
|
|
16
|
+
/^In HTML, whitespace text nodes cannot be a child of <(.+?)>\./,
|
|
17
|
+
/Hydration failed because the server rendered (text|HTML) didn't match the client\./,
|
|
18
|
+
/A tree hydrated but some attributes of the server rendered HTML didn't match the client properties/,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export function isHydrationError(error: unknown): boolean {
|
|
22
|
+
if (!error || typeof error !== 'object') return false;
|
|
23
|
+
const msg = (error as { message?: string }).message;
|
|
24
|
+
if (typeof msg !== 'string') return false;
|
|
25
|
+
return HYDRATION_PATTERNS.some((re) => re.test(msg));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ParsedHydrationError {
|
|
29
|
+
message: string;
|
|
30
|
+
diff: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseHydrationError(msg: string): ParsedHydrationError {
|
|
34
|
+
const hasComponentStackDiff = [
|
|
35
|
+
/^In HTML, (.+?) cannot be a child of <(.+?)>\.(.*)\nThis will cause a hydration error\.(.*)/,
|
|
36
|
+
/^In HTML, (.+?) cannot be a descendant of <(.+?)>\.\nThis will cause a hydration error\.(.*)/,
|
|
37
|
+
/^In HTML, text nodes cannot be a child of <(.+?)>\.\nThis will cause a hydration error\./,
|
|
38
|
+
/^In HTML, whitespace text nodes cannot be a child of <(.+?)>\./,
|
|
39
|
+
].some((re) => re.test(msg));
|
|
40
|
+
|
|
41
|
+
if (hasComponentStackDiff) {
|
|
42
|
+
const [message, diffLog = ''] = msg.split('\n\n');
|
|
43
|
+
const diff = diffLog.trim();
|
|
44
|
+
return { message: message.trim(), diff: diff || null };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const [before, after] = msg.split(REACT_HYDRATION_LINK);
|
|
48
|
+
if (after !== undefined && after.length > 1) {
|
|
49
|
+
const lines: string[] = [];
|
|
50
|
+
for (const line of after.split('\n')) {
|
|
51
|
+
const trimmed = line.trim();
|
|
52
|
+
if (trimmed === '' || trimmed.startsWith('at ')) continue;
|
|
53
|
+
lines.push(line);
|
|
54
|
+
}
|
|
55
|
+
return { message: before.trim(), diff: lines.join('\n') || null };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { message: msg.trim(), diff: null };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let overlayEl: HTMLDivElement | null = null;
|
|
62
|
+
let errors: ParsedHydrationError[] = [];
|
|
63
|
+
|
|
64
|
+
function escapeHtml(s: string): string {
|
|
65
|
+
return s
|
|
66
|
+
.replace(/&/g, '&')
|
|
67
|
+
.replace(/</g, '<')
|
|
68
|
+
.replace(/>/g, '>')
|
|
69
|
+
.replace(/"/g, '"');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function renderDiffHtml(diff: string): string {
|
|
73
|
+
return diff
|
|
74
|
+
.split('\n')
|
|
75
|
+
.map((line) => {
|
|
76
|
+
const trimmed = line.trimStart();
|
|
77
|
+
if (trimmed.startsWith('+')) {
|
|
78
|
+
return `<span style="color:#4ade80;display:block">${escapeHtml(line)}</span>`;
|
|
79
|
+
}
|
|
80
|
+
if (trimmed.startsWith('-')) {
|
|
81
|
+
return `<span style="color:#f87171;display:block">${escapeHtml(line)}</span>`;
|
|
82
|
+
}
|
|
83
|
+
return `<span style="color:#a1a1aa;display:block">${escapeHtml(line)}</span>`;
|
|
84
|
+
})
|
|
85
|
+
.join('');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function render(): void {
|
|
89
|
+
if (errors.length === 0) {
|
|
90
|
+
if (overlayEl) {
|
|
91
|
+
overlayEl.remove();
|
|
92
|
+
overlayEl = null;
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!overlayEl) {
|
|
98
|
+
overlayEl = document.createElement('div');
|
|
99
|
+
overlayEl.setAttribute('data-timber-hydration-overlay', '');
|
|
100
|
+
document.body.appendChild(overlayEl);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const count = errors.length;
|
|
104
|
+
const errorsHtml = errors
|
|
105
|
+
.map(
|
|
106
|
+
(err, i) => `
|
|
107
|
+
<div style="margin-bottom:${i < count - 1 ? '16px' : '0'};padding-bottom:${i < count - 1 ? '16px' : '0'};border-bottom:${i < count - 1 ? '1px solid #333' : 'none'}">
|
|
108
|
+
<div style="color:#fbbf24;font-weight:600;margin-bottom:8px">${escapeHtml(err.message)}</div>
|
|
109
|
+
${
|
|
110
|
+
err.diff
|
|
111
|
+
? `<div style="margin-top:8px">
|
|
112
|
+
<div style="display:flex;gap:16px;margin-bottom:6px;font-size:11px">
|
|
113
|
+
<span><span style="color:#4ade80">+</span> Client</span>
|
|
114
|
+
<span><span style="color:#f87171">-</span> Server</span>
|
|
115
|
+
</div>
|
|
116
|
+
<pre style="margin:0;padding:10px;background:#1a1a1a;border-radius:4px;overflow-x:auto;font-size:12px;line-height:1.5">${renderDiffHtml(err.diff)}</pre>
|
|
117
|
+
</div>`
|
|
118
|
+
: ''
|
|
119
|
+
}
|
|
120
|
+
</div>`
|
|
121
|
+
)
|
|
122
|
+
.join('');
|
|
123
|
+
|
|
124
|
+
overlayEl.innerHTML = `
|
|
125
|
+
<div style="position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:2147483646;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif" data-timber-hydration-backdrop>
|
|
126
|
+
<div style="background:#18181b;color:#e4e4e7;border:1px solid #333;border-radius:8px;max-width:720px;width:90vw;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 8px 32px rgba(0,0,0,0.5)">
|
|
127
|
+
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid #333">
|
|
128
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
129
|
+
<span style="background:#dc2626;color:#fff;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;text-transform:uppercase">Hydration Mismatch</span>
|
|
130
|
+
${count > 1 ? `<span style="color:#71717a;font-size:12px">${count} errors</span>` : ''}
|
|
131
|
+
</div>
|
|
132
|
+
<button data-timber-hydration-close style="background:none;border:1px solid #333;color:#a1a1aa;cursor:pointer;padding:4px 10px;border-radius:4px;font-size:12px;line-height:1">Dismiss</button>
|
|
133
|
+
</div>
|
|
134
|
+
<div style="padding:16px;overflow-y:auto;font-size:13px;line-height:1.6">
|
|
135
|
+
${errorsHtml}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>`;
|
|
139
|
+
|
|
140
|
+
overlayEl.querySelector('[data-timber-hydration-close]')?.addEventListener('click', dismiss);
|
|
141
|
+
overlayEl.querySelector('[data-timber-hydration-backdrop]')?.addEventListener('click', (e) => {
|
|
142
|
+
if ((e.target as HTMLElement).hasAttribute('data-timber-hydration-backdrop')) dismiss();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function dismiss(): void {
|
|
147
|
+
errors = [];
|
|
148
|
+
render();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function showHydrationError(error: unknown): void {
|
|
152
|
+
if (!error || typeof error !== 'object') return;
|
|
153
|
+
const msg = (error as { message?: string }).message;
|
|
154
|
+
if (typeof msg !== 'string') return;
|
|
155
|
+
const parsed = parseHydrationError(msg);
|
|
156
|
+
errors.push(parsed);
|
|
157
|
+
render();
|
|
158
|
+
}
|
package/src/dev-tools/index.ts
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
* - 404-page: self-contained HTML 404 page
|
|
9
9
|
* - terminal: boxed terminal error output
|
|
10
10
|
* - logger: structured request tree logger
|
|
11
|
-
* - warnings: dev-mode footgun warnings
|
|
12
11
|
* - browser-logs: forward browser console to server
|
|
13
12
|
* - logs: forward server console to browser
|
|
14
13
|
* - holding-server: startup holding server
|
|
@@ -64,21 +63,6 @@ export {
|
|
|
64
63
|
type DevLoggerConfig,
|
|
65
64
|
} from './logger.js';
|
|
66
65
|
|
|
67
|
-
// Dev-mode warnings
|
|
68
|
-
export {
|
|
69
|
-
warnSuspenseWrappingChildren,
|
|
70
|
-
warnDenyInSuspense,
|
|
71
|
-
warnRedirectInSuspense,
|
|
72
|
-
warnRedirectInAccess,
|
|
73
|
-
warnStaticRequestApi,
|
|
74
|
-
warnSlowSlotWithoutSuspense,
|
|
75
|
-
setViteServer,
|
|
76
|
-
WarningId,
|
|
77
|
-
type DevWarningConfig,
|
|
78
|
-
_resetWarnings,
|
|
79
|
-
_getEmitted,
|
|
80
|
-
} from './warnings.js';
|
|
81
|
-
|
|
82
66
|
// Browser log forwarding
|
|
83
67
|
export {
|
|
84
68
|
timberDevBrowserLogs,
|
|
@@ -94,3 +78,6 @@ export { createHoldingServer, type HoldingServer } from './holding-server.js';
|
|
|
94
78
|
|
|
95
79
|
// OTEL instrumentation
|
|
96
80
|
export { instrumentDevFetch, DevSpanProcessor, type DevFetchCleanup } from './instrumentation.js';
|
|
81
|
+
|
|
82
|
+
// Hydration mismatch overlay (client-side, dev-only)
|
|
83
|
+
export { isHydrationError, showHydrationError } from './hydration-overlay.js';
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic AST node types and recursive walkers for font static analysis.
|
|
3
|
+
*
|
|
4
|
+
* `ast.ts` owns parsing, config extraction, and call-site discovery; this
|
|
5
|
+
* module owns the recursive walks that police where font bindings may be
|
|
6
|
+
* used (dynamic configs, nested scopes, unconsumed references). Split out
|
|
7
|
+
* to keep `ast.ts` under the 500-code-line limit (design/18-build-system.md).
|
|
8
|
+
*
|
|
9
|
+
* Design doc: 24-fonts.md §"Step 1: Static Analysis"
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ── AST node types (minimal subset of estree) ────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export interface AstNode {
|
|
15
|
+
type: string;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface IdentifierNode extends AstNode {
|
|
20
|
+
type: 'Identifier';
|
|
21
|
+
name: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PositionedNode extends AstNode {
|
|
25
|
+
start: number;
|
|
26
|
+
end: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A flagged reference/usage for build-error reporting. */
|
|
30
|
+
export interface FlaggedUsage {
|
|
31
|
+
name: string;
|
|
32
|
+
line: number;
|
|
33
|
+
column: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Offset → line/column helper ──────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/** Convert a 0-based byte offset into a 1-based line and 0-based column. */
|
|
39
|
+
export function offsetToLineColumn(
|
|
40
|
+
source: string,
|
|
41
|
+
offset: number
|
|
42
|
+
): { line: number; column: number } {
|
|
43
|
+
let line = 1;
|
|
44
|
+
let lastNewline = -1;
|
|
45
|
+
for (let i = 0; i < offset; i++) {
|
|
46
|
+
if (source[i] === '\n') {
|
|
47
|
+
line++;
|
|
48
|
+
lastNewline = i;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { line, column: offset - lastNewline - 1 };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Shared recursion plumbing ────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Invoke `visit` on every direct child AST node of `node` (array elements
|
|
58
|
+
* and object-valued properties with a `type` field).
|
|
59
|
+
*/
|
|
60
|
+
export function walkChildNodes(node: AstNode, visit: (child: AstNode) => void): void {
|
|
61
|
+
for (const key of Object.keys(node)) {
|
|
62
|
+
if (key === 'type') continue;
|
|
63
|
+
const child = node[key];
|
|
64
|
+
if (Array.isArray(child)) {
|
|
65
|
+
for (const item of child) {
|
|
66
|
+
if (item && typeof item === 'object' && (item as AstNode).type) {
|
|
67
|
+
visit(item as AstNode);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} else if (child && typeof child === 'object' && (child as AstNode).type) {
|
|
71
|
+
visit(child as AstNode);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Walkers ──────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Recursively walk the AST looking for CallExpression nodes where
|
|
80
|
+
* the callee is one of the imported font names and the first argument
|
|
81
|
+
* is not an ObjectExpression (i.e. it's dynamic).
|
|
82
|
+
*/
|
|
83
|
+
export function walkForDynamicCalls(
|
|
84
|
+
node: AstNode,
|
|
85
|
+
names: ReadonlySet<string>,
|
|
86
|
+
source: string
|
|
87
|
+
): string | null {
|
|
88
|
+
if (!node || typeof node !== 'object') return null;
|
|
89
|
+
|
|
90
|
+
if (node.type === 'CallExpression') {
|
|
91
|
+
const callee = node.callee as AstNode;
|
|
92
|
+
if (callee.type === 'Identifier' && names.has((callee as IdentifierNode).name)) {
|
|
93
|
+
const args = node.arguments as AstNode[];
|
|
94
|
+
if (args.length > 0 && args[0].type !== 'ObjectExpression') {
|
|
95
|
+
// Extract the argument source text
|
|
96
|
+
const argStart = (args[0] as PositionedNode).start;
|
|
97
|
+
const argEnd = (args[0] as PositionedNode).end;
|
|
98
|
+
const argText = source.slice(argStart, argEnd);
|
|
99
|
+
const calleeName = (callee as IdentifierNode).name;
|
|
100
|
+
return `${calleeName}(${argText})`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let result: string | null = null;
|
|
106
|
+
walkChildNodes(node, (child) => {
|
|
107
|
+
if (!result) result = walkForDynamicCalls(child, names, source);
|
|
108
|
+
});
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Node types that introduce a nested scope — any font call inside is an
|
|
114
|
+
* error (font calls must be module-level, same constraint as Next.js).
|
|
115
|
+
* Module scope so it isn't rebuilt on every recursive walk call.
|
|
116
|
+
*/
|
|
117
|
+
const NESTED_SCOPE_TYPES = new Set([
|
|
118
|
+
'FunctionDeclaration',
|
|
119
|
+
'FunctionExpression',
|
|
120
|
+
'ArrowFunctionExpression',
|
|
121
|
+
'IfStatement',
|
|
122
|
+
'ForStatement',
|
|
123
|
+
'ForInStatement',
|
|
124
|
+
'ForOfStatement',
|
|
125
|
+
'WhileStatement',
|
|
126
|
+
'DoWhileStatement',
|
|
127
|
+
'SwitchStatement',
|
|
128
|
+
'TryStatement',
|
|
129
|
+
'BlockStatement',
|
|
130
|
+
'ClassDeclaration',
|
|
131
|
+
'ClassExpression',
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Recursively walk an AST node looking for font CallExpressions in nested
|
|
136
|
+
* scopes (function bodies, if-blocks, loops, etc.). Font calls must be
|
|
137
|
+
* module-level; nested calls are collected as errors.
|
|
138
|
+
*
|
|
139
|
+
* `isNested` tracks whether we've descended past the top-level statement.
|
|
140
|
+
* Top-level VariableDeclarations and ExpressionStatements are already
|
|
141
|
+
* handled by the main loop in `findFontCallSites`, so we only flag calls
|
|
142
|
+
* found deeper.
|
|
143
|
+
*/
|
|
144
|
+
export function walkForNestedFontCalls(
|
|
145
|
+
node: AstNode,
|
|
146
|
+
bindingNames: ReadonlyMap<string, unknown>,
|
|
147
|
+
source: string,
|
|
148
|
+
out: FlaggedUsage[],
|
|
149
|
+
isNested: boolean
|
|
150
|
+
): void {
|
|
151
|
+
if (!node || typeof node !== 'object') return;
|
|
152
|
+
|
|
153
|
+
if (node.type === 'CallExpression') {
|
|
154
|
+
const callee = node.callee as AstNode;
|
|
155
|
+
if (
|
|
156
|
+
isNested &&
|
|
157
|
+
callee.type === 'Identifier' &&
|
|
158
|
+
bindingNames.has((callee as IdentifierNode).name)
|
|
159
|
+
) {
|
|
160
|
+
const pos = offsetToLineColumn(source, (node as PositionedNode).start);
|
|
161
|
+
out.push({ name: (callee as IdentifierNode).name, line: pos.line, column: pos.column });
|
|
162
|
+
return; // Don't recurse into this call's children
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const enterNested = isNested || NESTED_SCOPE_TYPES.has(node.type);
|
|
167
|
+
walkChildNodes(node, (child) =>
|
|
168
|
+
walkForNestedFontCalls(child, bindingNames, source, out, enterNested)
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Recursively walk an AST node looking for references to font bindings
|
|
174
|
+
* that were not consumed by a recognized call site.
|
|
175
|
+
*
|
|
176
|
+
* Skips:
|
|
177
|
+
* - recognized CallExpressions (the transform rewrites them wholesale)
|
|
178
|
+
* - TS type-position nodes (erased at runtime)
|
|
179
|
+
* - non-computed member properties (`obj.Inter` doesn't reference the binding)
|
|
180
|
+
* - non-computed object keys (`{ Inter: 1 }` doesn't reference the binding)
|
|
181
|
+
* - export aliases (`export { inter as Inter }` — only `local` is a reference)
|
|
182
|
+
*/
|
|
183
|
+
export function walkForUnconsumedReferences(
|
|
184
|
+
node: AstNode,
|
|
185
|
+
bindingNames: ReadonlyMap<string, unknown>,
|
|
186
|
+
consumedCallStarts: ReadonlySet<number>,
|
|
187
|
+
source: string,
|
|
188
|
+
out: FlaggedUsage[]
|
|
189
|
+
): void {
|
|
190
|
+
if (!node || typeof node !== 'object') return;
|
|
191
|
+
if (node.type === 'ImportDeclaration' || node.type.startsWith('TS')) return;
|
|
192
|
+
|
|
193
|
+
if (node.type === 'Identifier') {
|
|
194
|
+
const name = (node as IdentifierNode).name;
|
|
195
|
+
if (bindingNames.has(name)) {
|
|
196
|
+
const pos = offsetToLineColumn(source, (node as PositionedNode).start);
|
|
197
|
+
out.push({ name, line: pos.line, column: pos.column });
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (node.type === 'CallExpression' && consumedCallStarts.has((node as PositionedNode).start)) {
|
|
203
|
+
return; // The whole call (callee + config object) is rewritten.
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (node.type === 'MemberExpression' && !(node as { computed?: boolean }).computed) {
|
|
207
|
+
const object = (node as { object?: AstNode }).object;
|
|
208
|
+
if (object) {
|
|
209
|
+
walkForUnconsumedReferences(object, bindingNames, consumedCallStarts, source, out);
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (node.type === 'Property' && !(node as { computed?: boolean }).computed) {
|
|
215
|
+
const value = (node as { value?: AstNode }).value;
|
|
216
|
+
if (value) {
|
|
217
|
+
walkForUnconsumedReferences(value, bindingNames, consumedCallStarts, source, out);
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (node.type === 'ExportSpecifier') {
|
|
223
|
+
// Only `local` references a module binding. `exported` is just the
|
|
224
|
+
// public name — `export { inter as Inter }` must not flag `Inter`.
|
|
225
|
+
const local = (node as { local?: AstNode }).local;
|
|
226
|
+
if (local) {
|
|
227
|
+
walkForUnconsumedReferences(local, bindingNames, consumedCallStarts, source, out);
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
walkChildNodes(node, (child) =>
|
|
233
|
+
walkForUnconsumedReferences(child, bindingNames, consumedCallStarts, source, out)
|
|
234
|
+
);
|
|
235
|
+
}
|