@coherent.js/client 1.0.0-beta.3 → 1.0.0-beta.6

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 CHANGED
@@ -23,6 +23,25 @@ Requirements:
23
23
  - Node.js >= 20
24
24
  - ESM module system
25
25
 
26
+
27
+ ## Exports
28
+
29
+ Client-side hydration and HMR utilities
30
+
31
+ ### Modular Imports (Tree-Shakable)
32
+
33
+ - Hydration utilities: `@coherent.js/client`
34
+ - Client router: `@coherent.js/client/router`
35
+ - HMR support: `@coherent.js/client/hmr`
36
+
37
+ ### Example Usage
38
+
39
+ ```javascript
40
+ import { hydrateComponent } from '@coherent.js/client';
41
+ import { createClientRouter } from '@coherent.js/client/router';
42
+ ```
43
+
44
+ > **Note**: All exports are tree-shakable. Import only what you need for optimal bundle size.
26
45
  ## Quick start
27
46
 
28
47
  The client package pairs with server-rendered HTML produced by `@coherent.js/core`.
package/dist/index.js CHANGED
@@ -1,5 +1,16 @@
1
1
  // src/hydration.js
2
2
  var componentInstances = /* @__PURE__ */ new WeakMap();
3
+ function getKey(vNode) {
4
+ if (!vNode || typeof vNode !== "object" || Array.isArray(vNode)) {
5
+ return void 0;
6
+ }
7
+ const tagName = Object.keys(vNode)[0];
8
+ const props = vNode[tagName];
9
+ if (props && typeof props === "object") {
10
+ return props.key;
11
+ }
12
+ return void 0;
13
+ }
3
14
  function extractInitialState(element, options = {}) {
4
15
  if (typeof window === "undefined") {
5
16
  return options.initialState || null;
@@ -213,7 +224,25 @@ function hydrate(element, component, props = {}, options = {}) {
213
224
  }
214
225
  });
215
226
  },
216
- // Patch children with intelligent list diffing
227
+ // Get children array from a vNode
228
+ getVNodeChildren(vNode) {
229
+ if (!vNode || typeof vNode !== "object" || Array.isArray(vNode)) {
230
+ return [];
231
+ }
232
+ const tagName = Object.keys(vNode)[0];
233
+ const props2 = vNode[tagName];
234
+ if (!props2 || typeof props2 !== "object") {
235
+ return [];
236
+ }
237
+ if (props2.children) {
238
+ return Array.isArray(props2.children) ? props2.children : [props2.children];
239
+ }
240
+ if (props2.text !== void 0) {
241
+ return [props2.text];
242
+ }
243
+ return [];
244
+ },
245
+ // Patch children with key-based reconciliation
217
246
  patchChildren(domElement, oldVNode, newVNode) {
218
247
  if (typeof window === "undefined" || typeof Node === "undefined" || typeof document === "undefined") {
219
248
  return;
@@ -221,22 +250,8 @@ function hydrate(element, component, props = {}, options = {}) {
221
250
  if (!domElement || typeof domElement.childNodes === "undefined" || typeof domElement.appendChild !== "function") {
222
251
  return;
223
252
  }
224
- const oldTagName = oldVNode ? Object.keys(oldVNode)[0] : null;
225
- const newTagName = Object.keys(newVNode)[0];
226
- const oldProps = oldVNode && oldTagName ? oldVNode[oldTagName] || {} : {};
227
- const newProps = newVNode[newTagName] || {};
228
- let oldChildren = [];
229
- let newChildren = [];
230
- if (oldProps.children) {
231
- oldChildren = Array.isArray(oldProps.children) ? oldProps.children : [oldProps.children];
232
- } else if (oldProps.text) {
233
- oldChildren = [oldProps.text];
234
- }
235
- if (newProps.children) {
236
- newChildren = Array.isArray(newProps.children) ? newProps.children : [newProps.children];
237
- } else if (newProps.text) {
238
- newChildren = [newProps.text];
239
- }
253
+ const oldChildren = this.getVNodeChildren(oldVNode);
254
+ const newChildren = this.getVNodeChildren(newVNode);
240
255
  let domChildren = [];
241
256
  if (typeof Array.from === "function" && domElement.childNodes) {
242
257
  try {
@@ -248,24 +263,62 @@ function hydrate(element, component, props = {}, options = {}) {
248
263
  domChildren = [];
249
264
  }
250
265
  }
251
- const maxLength = Math.max(oldChildren.length, newChildren.length, domChildren.length);
252
- for (let i = 0; i < maxLength; i++) {
253
- const oldChild = oldChildren[i];
254
- const newChild = newChildren[i];
255
- const domChild = domChildren[i];
256
- if (newChild === void 0) {
257
- if (domChild && typeof domChild.remove === "function") {
258
- domChild.remove();
266
+ const oldKeyMap = /* @__PURE__ */ new Map();
267
+ const oldIndexMap = /* @__PURE__ */ new Map();
268
+ oldChildren.forEach((child, i) => {
269
+ const key = getKey(child);
270
+ if (key !== void 0) {
271
+ oldKeyMap.set(key, { vNode: child, index: i, domNode: domChildren[i] });
272
+ } else {
273
+ oldIndexMap.set(i, { vNode: child, index: i, domNode: domChildren[i] });
274
+ }
275
+ });
276
+ const usedOldNodes = /* @__PURE__ */ new Set();
277
+ newChildren.forEach((newChild, newIndex) => {
278
+ const newKey = getKey(newChild);
279
+ let oldEntry = null;
280
+ if (newKey !== void 0 && oldKeyMap.has(newKey)) {
281
+ oldEntry = oldKeyMap.get(newKey);
282
+ usedOldNodes.add(newKey);
283
+ } else if (newKey === void 0 && oldIndexMap.has(newIndex)) {
284
+ oldEntry = oldIndexMap.get(newIndex);
285
+ usedOldNodes.add(`index:${newIndex}`);
286
+ }
287
+ if (oldEntry && oldEntry.domNode) {
288
+ this.patchDOM(oldEntry.domNode, oldEntry.vNode, newChild);
289
+ const currentPosition = Array.from(domElement.childNodes).filter((node) => {
290
+ return node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim();
291
+ }).indexOf(oldEntry.domNode);
292
+ if (currentPosition !== newIndex) {
293
+ const referenceNode = domElement.childNodes[newIndex];
294
+ if (referenceNode) {
295
+ domElement.insertBefore(oldEntry.domNode, referenceNode);
296
+ } else {
297
+ domElement.appendChild(oldEntry.domNode);
298
+ }
259
299
  }
260
- } else if (domChild === void 0) {
300
+ } else {
261
301
  const newElement = this.createDOMElement(newChild);
262
302
  if (newElement) {
263
- domElement.appendChild(newElement);
303
+ const referenceNode = domElement.childNodes[newIndex];
304
+ if (referenceNode) {
305
+ domElement.insertBefore(newElement, referenceNode);
306
+ } else {
307
+ domElement.appendChild(newElement);
308
+ }
264
309
  }
265
- } else {
266
- this.patchDOM(domChild, oldChild, newChild);
267
310
  }
268
- }
311
+ });
312
+ oldKeyMap.forEach((entry, key) => {
313
+ if (!usedOldNodes.has(key) && entry.domNode && entry.domNode.parentNode) {
314
+ entry.domNode.remove();
315
+ }
316
+ });
317
+ oldIndexMap.forEach((entry, index) => {
318
+ if (!usedOldNodes.has(`index:${index}`) && entry.domNode && entry.domNode.parentNode) {
319
+ entry.domNode.remove();
320
+ }
321
+ });
269
322
  },
270
323
  // Create DOM element from virtual element
271
324
  createDOMElement(vNode) {