mikuru 1.0.21 → 1.0.23

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/CHANGELOG.md CHANGED
@@ -1,6 +1,28 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 1.0.23 - 2026-05-13
4
+
5
+ - Added structured hydration diagnostic payloads with warning kind, recovery action, inferred expected/actual values, and DOM path context.
6
+ - Added hydration warning filtering to the dogfood Debug Panel and rendered hydration diagnostics in the SSR/hydration example.
7
+ - Stabilized the dogfood Debug Panel clear-events flow when background component and async debug events arrive after clearing.
8
+
9
+ ## 1.0.22 - 2026-05-13
10
+
11
+ - Generalized component slot hydration to pass default, named, dynamic, and scoped slots through `props.children` and `props.slots`.
12
+ - Improved SSR component slots so dynamic slot names support scoped props and explicit default slot templates are exposed through `props.children`.
13
+ - Added async component hydration delegation so SSR-rendered async children inside `<AsyncBoundary>` can be reused after streaming SSR.
14
+ - Improved async component hydration fallback handling for loader errors, timeouts, and retry recovery.
15
+ - Added Teleport + AsyncBoundary hydration coverage for SSR target reuse, async child hydration, and sibling stability.
16
+ - Added route SSR Teleport collection and RouterView + Teleport hydration coverage.
17
+ - Added Teleport + ErrorBoundary hydration coverage for target-side DOM reuse and cleanup.
18
+ - Added Transition and TransitionGroup async-child hydration coverage for DOM reuse, keyed order, and cleanup.
19
+ - Added nested AsyncBoundary streaming SSR hydration coverage for parent/child async DOM reuse and cleanup.
20
+ - Added nested lazy RouterView SSR hydration coverage with route-level Teleport reuse.
21
+ - Expanded SSR/hydration examples and E2E coverage for lazy route Teleport and nested AsyncBoundary Teleport patterns.
22
+ - Added nested AsyncBoundary error and timeout hydration coverage for inner fallback retry, sibling stability, and cleanup.
23
+ - Added SSR `v-model` form-control state rendering for input, textarea, checkbox, radio, select, and multiple select hydration parity.
24
+ - Improved hydration diagnostics with phase/component/file context and `hydration:warning` devtools events.
25
+ - Updated README and docs to reflect current SSR/hydration, router, diagnostics, examples, and release checklist coverage.
4
26
 
5
27
  ## 1.0.21 - 2026-05-13
6
28
 
package/README.md CHANGED
@@ -148,6 +148,7 @@ declare const Greeting: MikuruComponent<GreetingProps>;
148
148
  - Hydration through `compileHydration()` and `hydrateRoute()`, reusing existing SSR DOM while attaching events, syncing text/attributes, recovering structural mismatches with an opt-out remount fallback, hydrating component context/lifecycle hooks, `v-show`, DOM and component `v-model`, `v-pre`, `v-cloak`, initial `v-if` / `v-for` DOM, Teleport target and disabled inline content, delegating child and route components to `hydrate()` when available, and optionally starting router history listening after route hydration
149
149
  - Style injection and basic `<style scoped>` selector rewriting
150
150
  - Compile errors with filenames, line/column information, code frames, and typo suggestions for built-in attributes, directives, and modifiers
151
+ - Debug diagnostics with optional generated `sourceURL`, unstable devtools metadata/events, and hydration warnings that include phase, component, and filename context
151
152
 
152
153
  ## Package Exports
153
154
 
@@ -268,4 +269,5 @@ npm run dev:mikuru-vue-like
268
269
  - `docs/npm-usage.md` shows a manual Vite setup for package consumers.
269
270
  - `docs/app-architecture.md` describes how to keep larger Mikuru apps split across components, API modules, stores, forms, auth, and tests.
270
271
  - `docs/router.md` documents the runtime router.
272
+ - `docs/production-readiness.md` summarizes debugging, parser, package, SSR, and hydration caveats.
271
273
  - `docs/v1-api-contract.md` describes the v1 compatibility boundary used by this repository.
@@ -1,3 +1,4 @@
1
+ import { createCompileError } from "./errors.js";
1
2
  import { compileTemplateExpression, parseForExpression, validateAssignableExpression } from "./parseExpression.js";
2
3
  export function generateHydration(descriptor, root, options = {}) {
3
4
  const context = {
@@ -36,14 +37,18 @@ export function generateHydration(descriptor, root, options = {}) {
36
37
  emit(context, 2, "},");
37
38
  emit(context, 2, "registerEffect: (fn) => Promise.resolve().then(fn)");
38
39
  emit(context, 1, "};");
39
- emit(context, 1, "const __mikuru_warn = (message) => { if (typeof console !== \"undefined\" && console.warn) console.warn(`[Mikuru hydration] ${message}`); };");
40
+ emit(context, 1, "const __mikuru_emitDebug = (type, payload) => { const hook = globalThis.__MIKURU_DEVTOOLS__; if (!hook) return; const event = { type, timestamp: Date.now(), payload }; hook.events ??= []; hook.events.push(event); if (typeof hook.emit === \"function\") hook.emit(event); for (const listener of hook.listeners ?? []) { try { listener(event); } catch (error) { setTimeout(() => { throw error; }); } } };");
41
+ emit(context, 1, "const __mikuru_domPath = (node) => { if (!node) return \"missing\"; const parts = []; for (let cursor = node.nodeType === 1 ? node : node.parentElement; cursor && cursor.nodeType === 1; cursor = cursor.parentElement) { const tag = cursor.tagName.toLowerCase(); const siblings = cursor.parentElement ? Array.from(cursor.parentElement.children).filter((child) => child.tagName === cursor.tagName) : []; const index = siblings.length > 1 ? `:nth-of-type(${siblings.indexOf(cursor) + 1})` : \"\"; parts.unshift(`${tag}${index}`); } return parts.join(\" > \") || __mikuru_describeNode(node); };");
42
+ emit(context, 1, "const __mikuru_inferDiagnostic = (message) => { const action = message.includes(\"falling back to mount\") ? \"mount-fallback\" : message.includes(\"remounting\") ? \"remount\" : message.includes(\"Recovery was disabled\") || message.endsWith(\".\") ? \"warn-only\" : \"sync-dom\"; const kind = message.startsWith(\"Root mismatch\") ? \"root\" : message.startsWith(\"Element mismatch\") ? \"element\" : message.startsWith(\"Text mismatch\") || message.startsWith(\"Text content mismatch\") ? \"text\" : message.startsWith(\"Attribute mismatch\") ? \"attribute\" : message.startsWith(\"v-model\") ? \"model\" : message.startsWith(\"Extra DOM nodes\") ? \"extra-node\" : message.startsWith(\"Teleport\") ? \"teleport\" : message.startsWith(\"Component\") || message.startsWith(\"Dynamic component\") ? \"component\" : message.startsWith(\"Branch\") ? \"branch\" : message.startsWith(\"List\") ? \"list\" : \"hydration\"; const match = message.match(/expected\\s+(.+?),\\s+got\\s+(.+?)(?:[.;]|$)/); return { action, kind, expected: match?.[1], actual: match?.[2] }; };");
43
+ emit(context, 1, "const __mikuru_hydrationDiagnostic = (message, details = {}) => { const inferred = __mikuru_inferDiagnostic(message); const node = details.node; const diagnostic = { ...__mikuru_componentInfo, phase: \"hydration\", message, ...inferred, ...details }; delete diagnostic.node; if (!diagnostic.domPath && node) diagnostic.domPath = __mikuru_domPath(node); return diagnostic; };");
44
+ emit(context, 1, "const __mikuru_warn = (message, details = {}) => { const diagnostic = __mikuru_hydrationDiagnostic(message, details); __mikuru_emitDebug(\"hydration:warning\", diagnostic); if (typeof console !== \"undefined\" && console.warn) console.warn(`[Mikuru hydration] ${message} (phase: ${diagnostic.phase}, component: ${diagnostic.component}, file: ${diagnostic.filename})`); };");
40
45
  emit(context, 1, "const __mikuru_describeNode = (node) => { if (!node) return \"missing\"; if (node.nodeType === 1) return `<${node.tagName?.toLowerCase?.() ?? \"element\"}>`; if (node.nodeType === 3) return `text(${JSON.stringify(node.nodeValue ?? \"\")})`; if (node.nodeType === 8) return `comment(${JSON.stringify(node.nodeValue ?? \"\")})`; return `nodeType(${node.nodeType})`; };");
41
46
  emit(context, 1, "const __mikuru_restoreRegistrar = () => { if (__mikuru_previousRegistrar === undefined) { delete globalThis.__mikuru_currentRegistrar; } else { globalThis.__mikuru_currentRegistrar = __mikuru_previousRegistrar; } };");
42
47
  emit(context, 1, "const __mikuru_recovery = {};");
43
48
  emit(context, 1, "let __mikuru_recovered;");
44
- emit(context, 1, "const __mikuru_recover = (message) => {");
45
- emit(context, 2, "if (props.__mikuru_hydration?.recover === false) { __mikuru_warn(message + \".\"); return; }");
46
- emit(context, 2, "__mikuru_warn(message + \"; remounting.\");");
49
+ emit(context, 1, "const __mikuru_recover = (message, details = {}) => {");
50
+ emit(context, 2, "if (props.__mikuru_hydration?.recover === false) { __mikuru_warn(message + \".\", { action: \"warn-only\", ...details }); return; }");
51
+ emit(context, 2, "__mikuru_warn(message + \"; remounting.\", { action: \"remount\", ...details });");
47
52
  emit(context, 2, "for (const cleanup of __mikuru_cleanup.splice(0).reverse()) __mikuru_try(cleanup);");
48
53
  emit(context, 2, "for (const cb of __mikuru_afterUnmount.splice(0).reverse()) __mikuru_try(cb);");
49
54
  emit(context, 2, "__mikuru_restoreRegistrar();");
@@ -75,7 +80,7 @@ export function generateHydration(descriptor, root, options = {}) {
75
80
  emit(context, 1, "");
76
81
  }
77
82
  emit(context, 1, "const __mikuru_root = target.nodeType === 1 && target.tagName?.toLowerCase() === " + quote(root.tag.toLowerCase()) + " ? target : target.firstElementChild;");
78
- emit(context, 1, `if (!__mikuru_root || __mikuru_root.tagName?.toLowerCase() !== ${quote(root.tag.toLowerCase())}) { __mikuru_warn("Root mismatch: expected <${root.tag.toLowerCase()}>, got " + __mikuru_describeNode(__mikuru_root) + "; falling back to mount()."); __mikuru_restoreRegistrar(); return mount(target, props); }`);
83
+ emit(context, 1, `if (!__mikuru_root || __mikuru_root.tagName?.toLowerCase() !== ${quote(root.tag.toLowerCase())}) { __mikuru_warn("Root mismatch: expected <${root.tag.toLowerCase()}>, got " + __mikuru_describeNode(__mikuru_root) + "; falling back to mount().", { kind: "root", expected: "<${root.tag.toLowerCase()}>", actual: __mikuru_describeNode(__mikuru_root), action: "mount-fallback", node: __mikuru_root }); __mikuru_restoreRegistrar(); return mount(target, props); }`);
79
84
  emit(context, 1, "try {");
80
85
  hydrateElement(context, root, "__mikuru_root", 2);
81
86
  emit(context, 1, "} catch (error) {");
@@ -134,6 +139,7 @@ function hydrateElement(context, node, elementVar, indent) {
134
139
  hydrateAttrs(context, node, elementVar, indent);
135
140
  hydrateEvents(context, node, elementVar, indent);
136
141
  const contentDirective = getContentDirectiveAttr(node);
142
+ const textareaModel = node.tag === "textarea" && hasElementModel(node);
137
143
  const hydrateChildrenBeforeModel = node.tag === "select" && !contentDirective;
138
144
  if (hydrateChildrenBeforeModel) {
139
145
  hydrateChildren(context, node.children, elementVar, indent);
@@ -141,7 +147,10 @@ function hydrateElement(context, node, elementVar, indent) {
141
147
  hydrateModelAndShow(context, node, elementVar, indent);
142
148
  hydrateContentDirective(context, node, elementVar, indent);
143
149
  hydrateTemplateRef(context, node, elementVar, indent);
144
- if (!contentDirective && !hydrateChildrenBeforeModel) {
150
+ if (textareaModel) {
151
+ emit(context, indent, `if (${elementVar}.childNodes.length > 0) { const __mikuru_textarea_value = ${elementVar}.value; ${elementVar}.textContent = ""; ${elementVar}.value = __mikuru_textarea_value; }`);
152
+ }
153
+ if (!contentDirective && !hydrateChildrenBeforeModel && !textareaModel) {
145
154
  hydrateChildren(context, node.children, elementVar, indent);
146
155
  }
147
156
  }
@@ -193,12 +202,12 @@ function hydrateChildren(context, rawChildren, parentVar, indent) {
193
202
  const elementCheck = isComponentTag(child.tag)
194
203
  ? `!${childVar} || ${childVar}.nodeType !== 1`
195
204
  : `!${childVar} || ${childVar}.nodeType !== 1 || ${childVar}.tagName?.toLowerCase() !== ${quote(child.tag.toLowerCase())}`;
196
- emit(context, indent, `if (${elementCheck}) { __mikuru_recover(${quote(`Element mismatch: expected <${child.tag.toLowerCase()}>, got `)} + __mikuru_describeNode(${childVar})); } else {`);
205
+ emit(context, indent, `if (${elementCheck}) { __mikuru_recover(${quote(`Element mismatch: expected <${child.tag.toLowerCase()}>, got `)} + __mikuru_describeNode(${childVar}), { kind: "element", expected: "<${child.tag.toLowerCase()}>", actual: __mikuru_describeNode(${childVar}), node: ${childVar} }); } else {`);
197
206
  hydrateNode(context, child, childVar, indent + 1);
198
207
  emit(context, indent, "}");
199
208
  }
200
209
  else {
201
- emit(context, indent, `if (!${childVar} || ${childVar}.nodeType !== 3) { __mikuru_recover("Text mismatch: expected text, got " + __mikuru_describeNode(${childVar})); } else {`);
210
+ emit(context, indent, `if (!${childVar} || ${childVar}.nodeType !== 3) { __mikuru_recover("Text mismatch: expected text, got " + __mikuru_describeNode(${childVar}), { kind: "text", expected: "text", actual: __mikuru_describeNode(${childVar}), node: ${childVar} }); } else {`);
202
211
  hydrateNode(context, child, childVar, indent + 1);
203
212
  emit(context, indent, "}");
204
213
  }
@@ -446,35 +455,134 @@ function emitRouterViewRouteSlot(context, node, propsVar, indent) {
446
455
  emit(context, indent, `if (typeof props.children === "function") { ${propsVar}.children = props.children; ${propsVar}.slots = { ...(${propsVar}.slots ?? {}), default: props.children }; }`);
447
456
  }
448
457
  function emitHydrationComponentSlots(context, node, propsVar, indent) {
449
- const children = getDefaultHydrationSlotChildren(node);
450
- if (!children || !hasMeaningfulHydrationChildren(children)) {
458
+ const slots = collectHydrationComponentSlots(context, node);
459
+ if (slots.length === 0) {
451
460
  return;
452
461
  }
462
+ const defaultSlot = slots.find((slot) => !slot.nameExpression && slot.name === "default");
463
+ if (defaultSlot) {
464
+ emitHydrationSlotFunction(context, `${propsVar}.children`, defaultSlot, indent);
465
+ }
466
+ emit(context, indent, `${propsVar}.slots = { ...(${propsVar}.slots ?? {}) };`);
467
+ for (const slot of slots) {
468
+ const property = slot.nameExpression ? `[${slot.nameExpression}]` : `[${quote(slot.name)}]`;
469
+ emitHydrationSlotFunction(context, `${propsVar}.slots${property}`, slot, indent);
470
+ }
471
+ }
472
+ function emitHydrationSlotFunction(context, target, slot, indent) {
453
473
  const slotTargetVar = nextName(context, "slotTarget");
454
474
  const slotPropsVar = nextName(context, "slotProps");
455
- emit(context, indent, `${propsVar}.children = (${slotTargetVar}, ${slotPropsVar} = {}) => {`);
456
- hydrateChildren(context, children, slotTargetVar, indent + 1);
475
+ emit(context, indent, `${target} = (${slotTargetVar}, ${slotPropsVar} = {}) => {`);
476
+ emitHydrationSlotScopeBindings(context, slot, slotPropsVar, indent + 1);
477
+ hydrateChildren(context, slot.children, slotTargetVar, indent + 1);
457
478
  emit(context, indent, "};");
458
- emit(context, indent, `${propsVar}.slots = { ...(${propsVar}.slots ?? {}), default: ${propsVar}.children };`);
459
479
  }
460
- function getDefaultHydrationSlotChildren(node) {
480
+ function emitHydrationSlotScopeBindings(context, slot, slotPropsVar, indent) {
481
+ for (const binding of parseHydrationSlotScopeBindings(slot.scope, context, slot.scopeLoc ?? slot.loc)) {
482
+ if (binding.kind === "props") {
483
+ emit(context, indent, `const ${binding.alias} = ${slotPropsVar};`);
484
+ continue;
485
+ }
486
+ if (binding.kind === "rest") {
487
+ emit(context, indent, `const ${binding.alias} = { get value() { const rest = { ...${slotPropsVar} }; ${binding.exclude.map((key) => `delete rest[${quote(key)}];`).join(" ")} return rest; } };`);
488
+ continue;
489
+ }
490
+ const valueExpression = slotScopePathExpression(slotPropsVar, binding.path);
491
+ if (binding.defaultValue) {
492
+ emit(context, indent, `const ${binding.alias} = { get value() { const value = ${valueExpression}; return value === undefined ? (${binding.defaultValue}) : value; } };`);
493
+ continue;
494
+ }
495
+ emit(context, indent, `const ${binding.alias} = { get value() { return ${valueExpression}; } };`);
496
+ }
497
+ }
498
+ function slotScopePathExpression(rootVar, path) {
499
+ return path.reduce((expression, key, index) => {
500
+ const property = /^[A-Za-z_$][\w$]*$/.test(key) ? `.${key}` : `[${quote(key)}]`;
501
+ return `${expression}${index === 0 ? property : `?.${property.slice(1)}`}`;
502
+ }, rootVar);
503
+ }
504
+ function collectHydrationComponentSlots(context, node) {
505
+ const slots = [];
506
+ const usedNames = new Set();
461
507
  const defaultChildren = [];
462
508
  for (const child of node.children) {
463
509
  if (child.type === "element" && child.tag === "template") {
464
- const defaultSlotAttr = child.attrs.find(isDefaultSlotTemplateAttr);
465
- if (defaultSlotAttr) {
466
- return child.children;
467
- }
468
- if (child.attrs.some(isSlotTemplateAttr)) {
510
+ const slotDirective = getHydrationSlotTemplateDirective(child, context);
511
+ if (slotDirective) {
512
+ if (usedNames.has(slotDirective.name)) {
513
+ throwTemplateError(`Duplicate slot template: ${slotDirective.name}`, context, slotDirective.loc);
514
+ }
515
+ usedNames.add(slotDirective.name);
516
+ slots.push({
517
+ name: slotDirective.name,
518
+ nameExpression: slotDirective.nameExpression,
519
+ children: child.children,
520
+ scope: slotDirective.scope,
521
+ loc: child.loc,
522
+ scopeLoc: slotDirective.scopeLoc
523
+ });
469
524
  continue;
470
525
  }
471
526
  }
472
527
  defaultChildren.push(child);
473
528
  }
474
- return defaultChildren;
529
+ if (hasMeaningfulHydrationChildren(defaultChildren)) {
530
+ if (usedNames.has("default")) {
531
+ throwTemplateError("Duplicate slot template: default", context, node.loc);
532
+ }
533
+ slots.unshift({
534
+ name: "default",
535
+ children: defaultChildren,
536
+ scope: true,
537
+ loc: node.loc
538
+ });
539
+ }
540
+ return slots;
541
+ }
542
+ function getHydrationSlotTemplateDirective(node, context) {
543
+ const slotAttrs = node.attrs.filter((attr) => isSlotTemplateAttr(attr));
544
+ if (slotAttrs.length === 0) {
545
+ return undefined;
546
+ }
547
+ if (slotAttrs.length > 1) {
548
+ throwTemplateError("A slot template can only declare one slot target", context, slotAttrs[1]?.loc);
549
+ }
550
+ const attr = slotAttrs[0];
551
+ const name = getHydrationSlotTemplateName(attr, context);
552
+ return {
553
+ ...name,
554
+ scope: attr.value,
555
+ loc: attr.loc,
556
+ scopeLoc: attr.valueLoc
557
+ };
558
+ }
559
+ function getHydrationSlotTemplateName(attr, context) {
560
+ if (attr.name === "v-slot") {
561
+ return { name: "default" };
562
+ }
563
+ if (attr.name.startsWith("v-slot:")) {
564
+ return parseHydrationSlotTemplateName(attr.name.slice("v-slot:".length) || "default", attr, context);
565
+ }
566
+ if (attr.name.startsWith("#")) {
567
+ return parseHydrationSlotTemplateName(attr.name.slice(1) || "default", attr, context);
568
+ }
569
+ return { name: "default" };
475
570
  }
476
- function isDefaultSlotTemplateAttr(attr) {
477
- return attr.name === "v-slot" || attr.name === "v-slot:default" || attr.name === "#default";
571
+ function parseHydrationSlotTemplateName(rawName, attr, context) {
572
+ const name = rawName.trim();
573
+ const dynamicStart = name.indexOf("[");
574
+ const dynamicEnd = name.lastIndexOf("]");
575
+ if (dynamicStart >= 0 && dynamicEnd > dynamicStart) {
576
+ const expression = name.slice(dynamicStart + 1, dynamicEnd).trim();
577
+ if (!expression) {
578
+ throwTemplateError("Dynamic slot name requires an expression", context, attr.loc);
579
+ }
580
+ return {
581
+ name: `[${expression}]`,
582
+ nameExpression: compileTemplateExpression(expression, attr.name, toExpressionContext(context, attr.loc))
583
+ };
584
+ }
585
+ return { name };
478
586
  }
479
587
  function isSlotTemplateAttr(attr) {
480
588
  return attr.name === "v-slot" || attr.name.startsWith("v-slot:") || attr.name.startsWith("#");
@@ -482,6 +590,102 @@ function isSlotTemplateAttr(attr) {
482
590
  function hasMeaningfulHydrationChildren(children) {
483
591
  return children.some((child) => child.type === "element" || child.parts.some((part) => part.value.trim()));
484
592
  }
593
+ function parseHydrationSlotScopeBindings(scope, context, location) {
594
+ if (scope === true || !scope.trim()) {
595
+ return [];
596
+ }
597
+ const source = scope.trim();
598
+ if (isIdentifier(source)) {
599
+ return [{ kind: "props", alias: source }];
600
+ }
601
+ if (!source.startsWith("{") || !source.endsWith("}")) {
602
+ throwTemplateError("Slot scope must be an identifier or object destructuring pattern", context, location);
603
+ }
604
+ const body = source.slice(1, -1).trim();
605
+ if (!body) {
606
+ return [];
607
+ }
608
+ return parseHydrationSlotScopeObjectPattern(body, context, location, []);
609
+ }
610
+ function parseHydrationSlotScopeObjectPattern(body, context, location, pathPrefix) {
611
+ const bindings = [];
612
+ const excludedTopLevelKeys = [];
613
+ for (const part of splitTopLevel(body, ",")) {
614
+ const sourcePart = part.trim();
615
+ if (!sourcePart) {
616
+ continue;
617
+ }
618
+ if (sourcePart.startsWith("...")) {
619
+ const alias = sourcePart.slice(3).trim();
620
+ if (pathPrefix.length > 0) {
621
+ throwTemplateError("Slot scope rest destructuring is only supported at the top level", context, location);
622
+ }
623
+ if (!isIdentifier(alias)) {
624
+ throwTemplateError("Slot scope rest destructuring must use a simple identifier like ...rest", context, location);
625
+ }
626
+ bindings.push({ kind: "rest", alias, exclude: excludedTopLevelKeys });
627
+ continue;
628
+ }
629
+ const { left, right } = splitHydrationSlotScopeEntry(sourcePart, context, location);
630
+ if (!isIdentifier(left)) {
631
+ throwTemplateError(`Unsupported slot scope key "${left}". Use identifier keys in slot scope destructuring`, context, location);
632
+ }
633
+ if (pathPrefix.length === 0) {
634
+ excludedTopLevelKeys.push(left);
635
+ }
636
+ const path = [...pathPrefix, left];
637
+ if (right === undefined) {
638
+ bindings.push(...hydrationSlotScopeLeafBindings(left, path, context, location));
639
+ continue;
640
+ }
641
+ const value = right.trim();
642
+ if (value.startsWith("{") && value.endsWith("}")) {
643
+ bindings.push(...parseHydrationSlotScopeObjectPattern(value.slice(1, -1), context, location, path));
644
+ continue;
645
+ }
646
+ if (value.startsWith("[") || value.includes("{")) {
647
+ throwTemplateError("Slot scope destructuring supports nested object patterns only; array and mixed patterns are not supported", context, location);
648
+ }
649
+ bindings.push(...hydrationSlotScopeLeafBindings(value, path, context, location));
650
+ }
651
+ return bindings;
652
+ }
653
+ function splitHydrationSlotScopeEntry(source, context, location) {
654
+ const colonIndex = findTopLevelToken(source, ":");
655
+ if (colonIndex >= 0) {
656
+ return {
657
+ left: source.slice(0, colonIndex).trim(),
658
+ right: source.slice(colonIndex + 1).trim()
659
+ };
660
+ }
661
+ const equalsIndex = findTopLevelToken(source, "=");
662
+ if (equalsIndex >= 0) {
663
+ const left = source.slice(0, equalsIndex).trim();
664
+ if (!isIdentifier(left)) {
665
+ throwTemplateError("Slot scope default values can only be assigned to simple identifiers", context, location);
666
+ }
667
+ return { left, right: source };
668
+ }
669
+ return { left: source.trim() };
670
+ }
671
+ function hydrationSlotScopeLeafBindings(source, path, context, location) {
672
+ const equalsIndex = findTopLevelToken(source, "=");
673
+ const localSource = equalsIndex >= 0 ? source.slice(0, equalsIndex).trim() : source.trim();
674
+ const defaultSource = equalsIndex >= 0 ? source.slice(equalsIndex + 1).trim() : undefined;
675
+ if (!isIdentifier(localSource)) {
676
+ throwTemplateError(`Unsupported slot scope binding "${source}". Use identifiers, aliases, defaults, nested objects, or top-level ...rest`, context, location);
677
+ }
678
+ return [
679
+ {
680
+ kind: "property",
681
+ path,
682
+ alias: localSource,
683
+ defaultValue: defaultSource
684
+ ? compileTemplateExpression(defaultSource, "slot scope default", toExpressionContext(context, location))
685
+ : undefined
686
+ }
687
+ ];
688
+ }
485
689
  function hydrateDynamicComponentAtIndex(context, node, parentVar, domIndex, indent) {
486
690
  const isAttr = getDynamicComponentIsAttr(node);
487
691
  if (!isAttr) {
@@ -688,12 +892,12 @@ function hydrateFragmentChildrenAtIndex(context, rawChildren, parentVar, domInde
688
892
  const elementCheck = isComponentTag(child.tag)
689
893
  ? `!${childVar} || ${childVar}.nodeType !== 1`
690
894
  : `!${childVar} || ${childVar}.nodeType !== 1 || ${childVar}.tagName?.toLowerCase() !== ${quote(child.tag.toLowerCase())}`;
691
- emit(context, indent, `if (${elementCheck}) { __mikuru_recover(${quote(`Element mismatch: expected <${child.tag.toLowerCase()}>, got `)} + __mikuru_describeNode(${childVar})); } else {`);
895
+ emit(context, indent, `if (${elementCheck}) { __mikuru_recover(${quote(`Element mismatch: expected <${child.tag.toLowerCase()}>, got `)} + __mikuru_describeNode(${childVar}), { kind: "element", expected: "<${child.tag.toLowerCase()}>", actual: __mikuru_describeNode(${childVar}), node: ${childVar} }); } else {`);
692
896
  hydrateNode(context, child, childVar, indent + 1);
693
897
  emit(context, indent, "}");
694
898
  }
695
899
  else {
696
- emit(context, indent, `if (!${childVar} || ${childVar}.nodeType !== 3) { __mikuru_recover("Text mismatch: expected text, got " + __mikuru_describeNode(${childVar})); } else {`);
900
+ emit(context, indent, `if (!${childVar} || ${childVar}.nodeType !== 3) { __mikuru_recover("Text mismatch: expected text, got " + __mikuru_describeNode(${childVar}), { kind: "text", expected: "text", actual: __mikuru_describeNode(${childVar}), node: ${childVar} }); } else {`);
697
901
  hydrateNode(context, child, childVar, indent + 1);
698
902
  emit(context, indent, "}");
699
903
  }
@@ -1438,6 +1642,9 @@ function parseModelDirective(name) {
1438
1642
  const [argument = "", ...modifiers] = name.slice("v-model:".length).split(".");
1439
1643
  return { argument, modifiers: modifiers.filter(Boolean) };
1440
1644
  }
1645
+ function hasElementModel(node) {
1646
+ return node.attrs.some((attr) => Boolean(parseModelDirective(attr.name)));
1647
+ }
1441
1648
  function toComponentEventProp(eventName) {
1442
1649
  return `on${eventName.split(":").map((part) => part ? part[0].toUpperCase() + part.slice(1) : "").join("")}`;
1443
1650
  }
@@ -1510,9 +1717,90 @@ function compileHydrationExpression(context, expression, usage) {
1510
1717
  filename: context.filename
1511
1718
  });
1512
1719
  }
1720
+ function toExpressionContext(context, location) {
1721
+ if (!context.source || !location) {
1722
+ return undefined;
1723
+ }
1724
+ return {
1725
+ source: context.source,
1726
+ offset: location.offset,
1727
+ filename: context.filename
1728
+ };
1729
+ }
1730
+ function throwTemplateError(message, context, location) {
1731
+ if (context.source && location) {
1732
+ throw createCompileError(message, context.source, location.offset, context.filename);
1733
+ }
1734
+ throw new Error(message);
1735
+ }
1513
1736
  function isIdentifier(value) {
1514
1737
  return /^[A-Za-z_$][\w$]*$/.test(value);
1515
1738
  }
1739
+ function splitTopLevel(source, delimiter) {
1740
+ const parts = [];
1741
+ let depth = 0;
1742
+ let quoteChar = "";
1743
+ let start = 0;
1744
+ for (let index = 0; index < source.length; index += 1) {
1745
+ const char = source[index];
1746
+ const previous = source[index - 1];
1747
+ if (quoteChar) {
1748
+ if (char === quoteChar && previous !== "\\") {
1749
+ quoteChar = "";
1750
+ }
1751
+ continue;
1752
+ }
1753
+ if (char === "\"" || char === "'" || char === "`") {
1754
+ quoteChar = char;
1755
+ continue;
1756
+ }
1757
+ if (char === "{" || char === "[" || char === "(") {
1758
+ depth += 1;
1759
+ continue;
1760
+ }
1761
+ if (char === "}" || char === "]" || char === ")") {
1762
+ depth -= 1;
1763
+ continue;
1764
+ }
1765
+ if (depth === 0 && source.startsWith(delimiter, index)) {
1766
+ parts.push(source.slice(start, index));
1767
+ start = index + delimiter.length;
1768
+ index += delimiter.length - 1;
1769
+ }
1770
+ }
1771
+ parts.push(source.slice(start));
1772
+ return parts;
1773
+ }
1774
+ function findTopLevelToken(source, token) {
1775
+ let depth = 0;
1776
+ let quoteChar = "";
1777
+ for (let index = 0; index < source.length; index += 1) {
1778
+ const char = source[index];
1779
+ const previous = source[index - 1];
1780
+ if (quoteChar) {
1781
+ if (char === quoteChar && previous !== "\\") {
1782
+ quoteChar = "";
1783
+ }
1784
+ continue;
1785
+ }
1786
+ if (char === "\"" || char === "'" || char === "`") {
1787
+ quoteChar = char;
1788
+ continue;
1789
+ }
1790
+ if (char === "{" || char === "[" || char === "(") {
1791
+ depth += 1;
1792
+ continue;
1793
+ }
1794
+ if (char === "}" || char === "]" || char === ")") {
1795
+ depth -= 1;
1796
+ continue;
1797
+ }
1798
+ if (depth === 0 && source.startsWith(token, index)) {
1799
+ return index;
1800
+ }
1801
+ }
1802
+ return -1;
1803
+ }
1516
1804
  function withTemplateRefMode(context, mode, callback) {
1517
1805
  const previousMode = context.templateRefMode;
1518
1806
  context.templateRefMode = mode;