what-core 0.6.2 → 0.6.3
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/README.md +2 -0
- package/compiler.d.ts +30 -0
- package/devtools.d.ts +2 -0
- package/dist/compiler.js +1787 -0
- package/dist/compiler.js.map +7 -0
- package/dist/compiler.min.js +2 -0
- package/dist/compiler.min.js.map +7 -0
- package/dist/devtools.js +10 -0
- package/dist/devtools.js.map +7 -0
- package/dist/devtools.min.js +2 -0
- package/dist/devtools.min.js.map +7 -0
- package/dist/index.js +330 -382
- package/dist/index.js.map +4 -4
- package/dist/index.min.js +62 -62
- package/dist/index.min.js.map +4 -4
- package/dist/render.js +262 -21
- package/dist/render.js.map +4 -4
- package/dist/render.min.js +58 -1
- package/dist/render.min.js.map +4 -4
- package/dist/testing.js +3 -0
- package/dist/testing.js.map +2 -2
- package/dist/testing.min.js +1 -1
- package/dist/testing.min.js.map +2 -2
- package/index.d.ts +176 -1
- package/jsx-runtime.d.ts +622 -0
- package/package.json +20 -2
- package/src/agent-context.js +1 -1
- package/src/compiler.js +18 -0
- package/src/components.js +73 -27
- package/src/devtools.js +4 -0
- package/src/dom.js +7 -0
- package/src/guardrails.js +3 -4
- package/src/hooks.js +0 -11
- package/src/index.js +5 -9
- package/src/render.js +91 -24
- package/dist/a11y.js +0 -440
- package/dist/animation.js +0 -548
- package/dist/components.js +0 -229
- package/dist/data.js +0 -638
- package/dist/dom.js +0 -439
- package/dist/form.js +0 -509
- package/dist/h.js +0 -152
- package/dist/head.js +0 -51
- package/dist/helpers.js +0 -140
- package/dist/hooks.js +0 -210
- package/dist/reactive.js +0 -432
- package/dist/scheduler.js +0 -246
- package/dist/skeleton.js +0 -363
- package/dist/store.js +0 -83
- package/dist/what.js +0 -117
package/src/components.js
CHANGED
|
@@ -154,42 +154,73 @@ export function reportError(error, startCtx) {
|
|
|
154
154
|
|
|
155
155
|
// --- Show ---
|
|
156
156
|
// Conditional rendering component. Cleaner than ternaries.
|
|
157
|
+
// Reactively shows/hides children based on the `when` condition.
|
|
157
158
|
|
|
158
159
|
export function Show({ when, fallback = null, children }) {
|
|
159
|
-
// when
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
// If `when` is a signal or function, return a reactive function
|
|
161
|
+
// so the DOM runtime tracks changes via its effect wrapper
|
|
162
|
+
if (typeof when === 'function') {
|
|
163
|
+
return () => when() ? children : fallback;
|
|
164
|
+
}
|
|
165
|
+
// Static value — just return directly
|
|
166
|
+
return when ? children : fallback;
|
|
162
167
|
}
|
|
163
168
|
|
|
164
169
|
// --- For ---
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
// Reactive list rendering with keyed reconciliation.
|
|
171
|
+
// Takes a signal (or function returning an array) as `each` and reactively
|
|
172
|
+
// adds/removes/moves DOM nodes when the array changes.
|
|
173
|
+
// Uses mapArray from render.js for efficient keyed reconciliation with LIS.
|
|
174
|
+
//
|
|
175
|
+
// Usage: <For each={items}>{(item, index) => <div>{item}</div>}</For>
|
|
176
|
+
// - `each`: signal or function returning an array
|
|
177
|
+
// - `children`: render function (item, index) => vnode
|
|
178
|
+
// - `fallback`: shown when array is empty
|
|
179
|
+
// - `key`: optional key function (item) => key for keyed reconciliation
|
|
180
|
+
|
|
181
|
+
export function For({ each, fallback = null, children, key: keyFn }) {
|
|
171
182
|
// children should be a function (item, index) => vnode
|
|
172
183
|
const renderFn = Array.isArray(children) ? children[0] : children;
|
|
173
184
|
if (typeof renderFn !== 'function') {
|
|
174
|
-
|
|
185
|
+
if (__DEV__) {
|
|
186
|
+
console.warn('[what] For: children must be a render function, e.g. <For each={items}>{(item) => ...}</For>');
|
|
187
|
+
}
|
|
175
188
|
return fallback;
|
|
176
189
|
}
|
|
177
190
|
|
|
178
|
-
|
|
191
|
+
// Normalize `each` to a function that returns the current array
|
|
192
|
+
const source = typeof each === 'function' ? each : () => each;
|
|
193
|
+
|
|
194
|
+
// Build the map function that wraps renderFn with auto-key detection
|
|
195
|
+
const mapFn = (item, index) => {
|
|
179
196
|
const vnode = renderFn(item, index);
|
|
180
197
|
// Auto-detect keys for efficient keyed reconciliation
|
|
181
198
|
if (vnode && typeof vnode === 'object' && vnode.key == null) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (
|
|
185
|
-
else if (
|
|
186
|
-
} else if (typeof
|
|
187
|
-
|
|
188
|
-
vnode.key = item;
|
|
199
|
+
const rawItem = typeof item === 'function' && item._signal ? item() : item;
|
|
200
|
+
if (rawItem != null && typeof rawItem === 'object') {
|
|
201
|
+
if (rawItem.id != null) vnode.key = rawItem.id;
|
|
202
|
+
else if (rawItem.key != null) vnode.key = rawItem.key;
|
|
203
|
+
} else if (typeof rawItem === 'string' || typeof rawItem === 'number') {
|
|
204
|
+
vnode.key = rawItem;
|
|
189
205
|
}
|
|
190
206
|
}
|
|
191
207
|
return vnode;
|
|
192
|
-
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Return a reactive function. The DOM runtime (createDOM in dom.js)
|
|
211
|
+
// wraps functions in effects, so this will re-evaluate whenever the
|
|
212
|
+
// source signal changes. For simple cases (no mapArray integration),
|
|
213
|
+
// this provides correct reactivity by re-rendering the list on change.
|
|
214
|
+
//
|
|
215
|
+
// The effect wrapper in createDOM (lines 140-190 in dom.js) handles:
|
|
216
|
+
// - Creating DOM nodes for new vnodes
|
|
217
|
+
// - Removing old DOM nodes
|
|
218
|
+
// - Inserting between comment markers
|
|
219
|
+
return () => {
|
|
220
|
+
const list = source();
|
|
221
|
+
if (!list || list.length === 0) return fallback;
|
|
222
|
+
return list.map((item, i) => mapFn(item, i));
|
|
223
|
+
};
|
|
193
224
|
}
|
|
194
225
|
|
|
195
226
|
// --- Switch / Match ---
|
|
@@ -198,12 +229,29 @@ export function For({ each, fallback = null, children }) {
|
|
|
198
229
|
export function Switch({ fallback = null, children }) {
|
|
199
230
|
const kids = Array.isArray(children) ? children : [children];
|
|
200
231
|
|
|
232
|
+
const hasReactiveCondition = kids.some(
|
|
233
|
+
child => child && child.tag === Match && typeof child.props.when === 'function'
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
if (hasReactiveCondition) {
|
|
237
|
+
return () => {
|
|
238
|
+
for (const child of kids) {
|
|
239
|
+
if (child && child.tag === Match) {
|
|
240
|
+
const condition = typeof child.props.when === 'function'
|
|
241
|
+
? child.props.when()
|
|
242
|
+
: child.props.when;
|
|
243
|
+
if (condition) {
|
|
244
|
+
return child.children;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return fallback;
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
201
252
|
for (const child of kids) {
|
|
202
253
|
if (child && child.tag === Match) {
|
|
203
|
-
|
|
204
|
-
? child.props.when()
|
|
205
|
-
: child.props.when;
|
|
206
|
-
if (condition) {
|
|
254
|
+
if (child.props.when) {
|
|
207
255
|
return child.children;
|
|
208
256
|
}
|
|
209
257
|
}
|
|
@@ -223,8 +271,6 @@ export function Match({ when, children }) {
|
|
|
223
271
|
// The babel plugin compiles <Counter client:idle /> into this.
|
|
224
272
|
|
|
225
273
|
export function Island({ component: Component, mode, mediaQuery, ...props }) {
|
|
226
|
-
const placeholder = h('div', { 'data-island': Component.name || 'Island', 'data-hydrate': mode });
|
|
227
|
-
|
|
228
274
|
// We need to return a vnode that the reconciler can handle.
|
|
229
275
|
// The actual hydration scheduling happens after mount via an effect.
|
|
230
276
|
const wrapper = signal(null);
|
|
@@ -304,8 +350,8 @@ export function Island({ component: Component, mode, mediaQuery, ...props }) {
|
|
|
304
350
|
if (el) scheduleHydration(el);
|
|
305
351
|
};
|
|
306
352
|
|
|
307
|
-
// Return:
|
|
353
|
+
// Return: reactive function so DOM runtime tracks hydration state changes
|
|
308
354
|
return h('div', { 'data-island': Component.name || 'Island', 'data-hydrate': mode, ref: refCallback },
|
|
309
|
-
hydrated() ? wrapper() : null
|
|
355
|
+
() => hydrated() ? wrapper() : null
|
|
310
356
|
);
|
|
311
357
|
}
|
package/src/devtools.js
ADDED
package/src/dom.js
CHANGED
|
@@ -204,6 +204,13 @@ export function createDOM(vnode, parent, isSvg) {
|
|
|
204
204
|
return createComponent(vnode, parent, isSvg);
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
// Special boundary vnodes (returned by ErrorBoundary, Suspense, Portal components)
|
|
208
|
+
// These have string tags like '__errorBoundary' and must be routed to createComponent
|
|
209
|
+
// which contains the special-case handlers for them.
|
|
210
|
+
if (isVNode(vnode) && (vnode.tag === '__errorBoundary' || vnode.tag === '__suspense' || vnode.tag === '__portal')) {
|
|
211
|
+
return createComponent(vnode, parent, isSvg);
|
|
212
|
+
}
|
|
213
|
+
|
|
207
214
|
// VNode with string tag — create element
|
|
208
215
|
if (isVNode(vnode)) {
|
|
209
216
|
return createElementFromVNode(vnode, parent, isSvg);
|
package/src/guardrails.js
CHANGED
|
@@ -125,11 +125,10 @@ function capitalize(str) {
|
|
|
125
125
|
const VALID_EXPORTS = new Set([
|
|
126
126
|
// Reactive primitives
|
|
127
127
|
'signal', 'computed', 'effect', 'signalMemo', 'batch', 'untrack', 'flushSync',
|
|
128
|
-
'createRoot', 'getOwner', 'runWithOwner', 'onRootCleanup',
|
|
128
|
+
'createRoot', 'getOwner', 'runWithOwner', 'onRootCleanup',
|
|
129
129
|
// Rendering
|
|
130
|
-
'template', '
|
|
130
|
+
'template', 'svgTemplate', 'insert', 'mapArray', 'spread',
|
|
131
131
|
'setProp', 'delegateEvents', 'on', 'classList', 'hydrate', 'isHydrating',
|
|
132
|
-
'_$createComponent',
|
|
133
132
|
// JSX
|
|
134
133
|
'h', 'Fragment', 'html',
|
|
135
134
|
// DOM
|
|
@@ -163,7 +162,7 @@ const VALID_EXPORTS = new Set([
|
|
|
163
162
|
'IslandSkeleton', 'useSkeleton', 'Placeholder', 'LoadingDots', 'Spinner',
|
|
164
163
|
// Data fetching
|
|
165
164
|
'useFetch', 'useSWR', 'useQuery', 'useInfiniteQuery', 'invalidateQueries',
|
|
166
|
-
'prefetchQuery', 'setQueryData', 'getQueryData', 'clearCache',
|
|
165
|
+
'prefetchQuery', 'setQueryData', 'getQueryData', 'clearCache',
|
|
167
166
|
// Form
|
|
168
167
|
'useForm', 'useField', 'rules', 'simpleResolver', 'zodResolver', 'yupResolver',
|
|
169
168
|
'Input', 'Textarea', 'Select', 'Checkbox', 'Radio', 'ErrorMessage',
|
package/src/hooks.js
CHANGED
|
@@ -371,14 +371,3 @@ export function createResource(fetcher, options = {}) {
|
|
|
371
371
|
return [data, { loading, error, refetch, mutate }];
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
-
// --- Dep comparison (kept for potential external use) ---
|
|
375
|
-
|
|
376
|
-
function depsChanged(oldDeps, newDeps) {
|
|
377
|
-
if (oldDeps === undefined) return true;
|
|
378
|
-
if (!oldDeps || !newDeps) return true;
|
|
379
|
-
if (oldDeps.length !== newDeps.length) return true;
|
|
380
|
-
for (let i = 0; i < oldDeps.length; i++) {
|
|
381
|
-
if (!Object.is(oldDeps[i], newDeps[i])) return true;
|
|
382
|
-
}
|
|
383
|
-
return false;
|
|
384
|
-
}
|
package/src/index.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// The closest framework to vanilla JS.
|
|
3
3
|
|
|
4
4
|
// Reactive primitives
|
|
5
|
-
export { signal, computed, effect, memo as signalMemo, batch, untrack, flushSync, createRoot, getOwner, runWithOwner, onCleanup as onRootCleanup
|
|
5
|
+
export { signal, computed, effect, memo as signalMemo, batch, untrack, flushSync, createRoot, getOwner, runWithOwner, onCleanup as onRootCleanup } from './reactive.js';
|
|
6
6
|
|
|
7
7
|
// Fine-grained rendering primitives
|
|
8
|
-
export { template,
|
|
8
|
+
export { template, svgTemplate, insert, mapArray, spread, setProp, delegateEvents, on, classList, hydrate, isHydrating, getHydrationMismatchCount } from './render.js';
|
|
9
9
|
|
|
10
10
|
// JSX factory — Fragment and html tagged template are public APIs.
|
|
11
11
|
// h is exported for internal package use only (jsx-runtime, server, router, react-compat).
|
|
@@ -15,15 +15,12 @@ export { h, Fragment, html } from './h.js';
|
|
|
15
15
|
// DOM mounting & rendering (fine-grained, no VDOM reconciler)
|
|
16
16
|
export { mount } from './dom.js';
|
|
17
17
|
|
|
18
|
-
// Hooks
|
|
18
|
+
// Hooks — Solid-style API (signal-first)
|
|
19
|
+
// React-style hooks (useState, useEffect, useMemo, useCallback, useRef)
|
|
20
|
+
// are available via 'what-framework/react-compat' only.
|
|
19
21
|
export {
|
|
20
|
-
useState,
|
|
21
22
|
useSignal,
|
|
22
23
|
useComputed,
|
|
23
|
-
useEffect,
|
|
24
|
-
useMemo,
|
|
25
|
-
useCallback,
|
|
26
|
-
useRef,
|
|
27
24
|
useContext,
|
|
28
25
|
useReducer,
|
|
29
26
|
createContext,
|
|
@@ -134,7 +131,6 @@ export {
|
|
|
134
131
|
setQueryData,
|
|
135
132
|
getQueryData,
|
|
136
133
|
clearCache,
|
|
137
|
-
__getCacheSnapshot,
|
|
138
134
|
} from './data.js';
|
|
139
135
|
|
|
140
136
|
// Form utilities
|
package/src/render.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// Solid-style rendering: components run once, signals create individual DOM effects.
|
|
3
3
|
// No VDOM diffing — direct DOM manipulation with surgical signal-driven updates.
|
|
4
4
|
|
|
5
|
-
import { effect, untrack, createRoot, signal, __DEV__ } from './reactive.js';
|
|
5
|
+
import { effect, untrack, createRoot, signal, __DEV__, __devtools } from './reactive.js';
|
|
6
6
|
import { createDOM, disposeTree, getCurrentComponent, getComponentStack } from './dom.js';
|
|
7
|
+
import { createWhatError, collectError } from './errors.js';
|
|
7
8
|
|
|
8
9
|
export { effect, untrack };
|
|
9
10
|
|
|
@@ -980,9 +981,16 @@ export function isHydrating() {
|
|
|
980
981
|
export function hydrate(vnode, container) {
|
|
981
982
|
_isHydrating = true;
|
|
982
983
|
_hydrationCursor = { parent: container, index: 0 };
|
|
984
|
+
_hydrationMismatchCount = 0;
|
|
983
985
|
|
|
984
986
|
try {
|
|
985
987
|
const result = hydrateNode(vnode, container);
|
|
988
|
+
if (__DEV__ && _hydrationMismatchCount > 0) {
|
|
989
|
+
console.warn(
|
|
990
|
+
`[what] Hydration completed with ${_hydrationMismatchCount} mismatch${_hydrationMismatchCount === 1 ? '' : 'es'}. ` +
|
|
991
|
+
'See previous warnings for details. This usually means server and client render different initial HTML.'
|
|
992
|
+
);
|
|
993
|
+
}
|
|
986
994
|
return result;
|
|
987
995
|
} finally {
|
|
988
996
|
_isHydrating = false;
|
|
@@ -1012,11 +1020,46 @@ function claimNode(parent) {
|
|
|
1012
1020
|
return null;
|
|
1013
1021
|
}
|
|
1014
1022
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1023
|
+
// Track hydration mismatch count for diagnostics
|
|
1024
|
+
let _hydrationMismatchCount = 0;
|
|
1025
|
+
|
|
1026
|
+
/** Returns the number of hydration mismatches encountered during the last hydrate() call. */
|
|
1027
|
+
export function getHydrationMismatchCount() {
|
|
1028
|
+
return _hydrationMismatchCount;
|
|
1017
1029
|
}
|
|
1018
1030
|
|
|
1019
|
-
function
|
|
1031
|
+
function reportHydrationMismatch(expected, actual, componentName) {
|
|
1032
|
+
_hydrationMismatchCount++;
|
|
1033
|
+
if (__DEV__) {
|
|
1034
|
+
const context = {
|
|
1035
|
+
component: componentName || 'unknown',
|
|
1036
|
+
serverHTML: actual,
|
|
1037
|
+
clientHTML: expected,
|
|
1038
|
+
};
|
|
1039
|
+
const whatError = createWhatError('HYDRATION_MISMATCH', context);
|
|
1040
|
+
collectError(whatError);
|
|
1041
|
+
// Notify devtools so MCP agents see hydration mismatches as live events
|
|
1042
|
+
if (__devtools?.onHydrationMismatch) {
|
|
1043
|
+
__devtools.onHydrationMismatch({
|
|
1044
|
+
component: componentName || 'unknown',
|
|
1045
|
+
expected,
|
|
1046
|
+
actual,
|
|
1047
|
+
mismatchCount: _hydrationMismatchCount,
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
// Also notify the error handler so it appears in devtools error log
|
|
1051
|
+
if (__devtools?.onError) {
|
|
1052
|
+
__devtools.onError(whatError, { type: 'hydration', component: componentName });
|
|
1053
|
+
}
|
|
1054
|
+
console.warn(
|
|
1055
|
+
`[what] Hydration mismatch: expected ${expected}, got ${actual}` +
|
|
1056
|
+
(componentName ? ` (in ${componentName})` : '') +
|
|
1057
|
+
'. Falling back to client render.'
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function hydrateNode(vnode, parent, _componentName) {
|
|
1020
1063
|
if (vnode == null || typeof vnode === 'boolean') {
|
|
1021
1064
|
return null;
|
|
1022
1065
|
}
|
|
@@ -1028,9 +1071,9 @@ function hydrateNode(vnode, parent) {
|
|
|
1028
1071
|
|
|
1029
1072
|
if (existing && existing.nodeType === 3) {
|
|
1030
1073
|
// Reuse text node — check for mismatch in dev
|
|
1031
|
-
if (
|
|
1032
|
-
|
|
1033
|
-
`
|
|
1074
|
+
if (__DEV__ && existing.textContent !== text) {
|
|
1075
|
+
reportHydrationMismatch(
|
|
1076
|
+
`text "${text}"`, `text "${existing.textContent}"`, _componentName
|
|
1034
1077
|
);
|
|
1035
1078
|
existing.textContent = text;
|
|
1036
1079
|
}
|
|
@@ -1038,11 +1081,9 @@ function hydrateNode(vnode, parent) {
|
|
|
1038
1081
|
}
|
|
1039
1082
|
|
|
1040
1083
|
// Mismatch: expected text node, got element or nothing
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
);
|
|
1045
|
-
}
|
|
1084
|
+
reportHydrationMismatch(
|
|
1085
|
+
`text node "${text}"`, existing ? existing.nodeName : 'nothing', _componentName
|
|
1086
|
+
);
|
|
1046
1087
|
const textNode = document.createTextNode(text);
|
|
1047
1088
|
if (existing) {
|
|
1048
1089
|
parent.replaceChild(textNode, existing);
|
|
@@ -1056,7 +1097,7 @@ function hydrateNode(vnode, parent) {
|
|
|
1056
1097
|
if (typeof vnode === 'function') {
|
|
1057
1098
|
// Unwrap to get the initial value for hydration
|
|
1058
1099
|
const initialValue = vnode();
|
|
1059
|
-
let current = hydrateNode(initialValue, parent);
|
|
1100
|
+
let current = hydrateNode(initialValue, parent, _componentName);
|
|
1060
1101
|
|
|
1061
1102
|
// Set up reactive effect for future updates (normal rendering path)
|
|
1062
1103
|
effect(() => {
|
|
@@ -1073,7 +1114,7 @@ function hydrateNode(vnode, parent) {
|
|
|
1073
1114
|
if (Array.isArray(vnode)) {
|
|
1074
1115
|
const nodes = [];
|
|
1075
1116
|
for (const child of vnode) {
|
|
1076
|
-
const node = hydrateNode(child, parent);
|
|
1117
|
+
const node = hydrateNode(child, parent, _componentName);
|
|
1077
1118
|
if (node) nodes.push(node);
|
|
1078
1119
|
}
|
|
1079
1120
|
return nodes.length === 1 ? nodes[0] : nodes;
|
|
@@ -1085,6 +1126,7 @@ function hydrateNode(vnode, parent) {
|
|
|
1085
1126
|
if (typeof vnode.tag === 'function') {
|
|
1086
1127
|
const componentStack = getComponentStack();
|
|
1087
1128
|
const Component = vnode.tag;
|
|
1129
|
+
const compName = Component.displayName || Component.name || 'Anonymous';
|
|
1088
1130
|
const props = vnode.props || {};
|
|
1089
1131
|
const children = vnode.children || [];
|
|
1090
1132
|
|
|
@@ -1111,7 +1153,9 @@ function hydrateNode(vnode, parent) {
|
|
|
1111
1153
|
result = Component({ ...props, children: propsChildren });
|
|
1112
1154
|
} catch (error) {
|
|
1113
1155
|
componentStack.pop();
|
|
1114
|
-
|
|
1156
|
+
if (__DEV__) {
|
|
1157
|
+
console.error('[what] Error in component during hydration:', compName, error);
|
|
1158
|
+
}
|
|
1115
1159
|
return null;
|
|
1116
1160
|
}
|
|
1117
1161
|
|
|
@@ -1128,7 +1172,7 @@ function hydrateNode(vnode, parent) {
|
|
|
1128
1172
|
});
|
|
1129
1173
|
}
|
|
1130
1174
|
|
|
1131
|
-
return hydrateNode(result, parent);
|
|
1175
|
+
return hydrateNode(result, parent, compName);
|
|
1132
1176
|
}
|
|
1133
1177
|
|
|
1134
1178
|
// Element — claim existing DOM element
|
|
@@ -1139,6 +1183,16 @@ function hydrateNode(vnode, parent) {
|
|
|
1139
1183
|
// Match! Reuse this element. Apply props/bindings.
|
|
1140
1184
|
hydrateElementProps(existing, vnode.props || {});
|
|
1141
1185
|
|
|
1186
|
+
// Handle ref callback — this was previously missing during hydration
|
|
1187
|
+
if (vnode.props?.ref) {
|
|
1188
|
+
const ref = vnode.props.ref;
|
|
1189
|
+
if (typeof ref === 'function') {
|
|
1190
|
+
ref(existing);
|
|
1191
|
+
} else if (typeof ref === 'object' && ref !== null) {
|
|
1192
|
+
ref.current = existing;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1142
1196
|
// Hydrate children
|
|
1143
1197
|
const savedCursor = _hydrationCursor;
|
|
1144
1198
|
_hydrationCursor = { parent: existing, index: 0 };
|
|
@@ -1146,7 +1200,7 @@ function hydrateNode(vnode, parent) {
|
|
|
1146
1200
|
const rawInner = vnode.props?.dangerouslySetInnerHTML?.__html;
|
|
1147
1201
|
if (rawInner == null) {
|
|
1148
1202
|
for (const child of vnode.children) {
|
|
1149
|
-
hydrateNode(child, existing);
|
|
1203
|
+
hydrateNode(child, existing, _componentName);
|
|
1150
1204
|
}
|
|
1151
1205
|
}
|
|
1152
1206
|
|
|
@@ -1155,11 +1209,9 @@ function hydrateNode(vnode, parent) {
|
|
|
1155
1209
|
}
|
|
1156
1210
|
|
|
1157
1211
|
// Mismatch — fall back to client render for this subtree
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
);
|
|
1162
|
-
}
|
|
1212
|
+
reportHydrationMismatch(
|
|
1213
|
+
`<${vnode.tag}>`, existing ? existing.nodeName : 'nothing', _componentName
|
|
1214
|
+
);
|
|
1163
1215
|
|
|
1164
1216
|
// Create the element from scratch
|
|
1165
1217
|
const newEl = document.createElement(vnode.tag);
|
|
@@ -1230,8 +1282,23 @@ function hydrateElementProps(el, props) {
|
|
|
1230
1282
|
continue;
|
|
1231
1283
|
}
|
|
1232
1284
|
|
|
1233
|
-
// Static props —
|
|
1234
|
-
// Only attach non-serializable props or ones that may differ
|
|
1285
|
+
// Static props — verify attributes match in dev mode
|
|
1235
1286
|
if (key === 'data-hk') continue;
|
|
1287
|
+
|
|
1288
|
+
// In dev mode, check that the server-rendered attribute matches the client value
|
|
1289
|
+
// to catch hydration mismatches early (e.g., class="foo" vs class="bar")
|
|
1290
|
+
if (__DEV__ && typeof value === 'string') {
|
|
1291
|
+
const attrName = key === 'className' ? 'class' : key === 'htmlFor' ? 'for' : key;
|
|
1292
|
+
const serverValue = el.getAttribute(attrName);
|
|
1293
|
+
if (serverValue !== null && serverValue !== value) {
|
|
1294
|
+
reportHydrationMismatch(
|
|
1295
|
+
`${attrName}="${value}"`,
|
|
1296
|
+
`${attrName}="${serverValue}"`,
|
|
1297
|
+
el.tagName.toLowerCase()
|
|
1298
|
+
);
|
|
1299
|
+
// Apply client value to fix the mismatch
|
|
1300
|
+
setProp(el, key, value);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1236
1303
|
}
|
|
1237
1304
|
}
|