mikuru 1.0.21 → 1.0.22

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,22 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 1.0.22 - 2026-05-13
4
+
5
+ - Generalized component slot hydration to pass default, named, dynamic, and scoped slots through `props.children` and `props.slots`.
6
+ - Improved SSR component slots so dynamic slot names support scoped props and explicit default slot templates are exposed through `props.children`.
7
+ - Added async component hydration delegation so SSR-rendered async children inside `<AsyncBoundary>` can be reused after streaming SSR.
8
+ - Improved async component hydration fallback handling for loader errors, timeouts, and retry recovery.
9
+ - Added Teleport + AsyncBoundary hydration coverage for SSR target reuse, async child hydration, and sibling stability.
10
+ - Added route SSR Teleport collection and RouterView + Teleport hydration coverage.
11
+ - Added Teleport + ErrorBoundary hydration coverage for target-side DOM reuse and cleanup.
12
+ - Added Transition and TransitionGroup async-child hydration coverage for DOM reuse, keyed order, and cleanup.
13
+ - Added nested AsyncBoundary streaming SSR hydration coverage for parent/child async DOM reuse and cleanup.
14
+ - Added nested lazy RouterView SSR hydration coverage with route-level Teleport reuse.
15
+ - Expanded SSR/hydration examples and E2E coverage for lazy route Teleport and nested AsyncBoundary Teleport patterns.
16
+ - Added nested AsyncBoundary error and timeout hydration coverage for inner fallback retry, sibling stability, and cleanup.
17
+ - Added SSR `v-model` form-control state rendering for input, textarea, checkbox, radio, select, and multiple select hydration parity.
18
+ - Improved hydration diagnostics with phase/component/file context and `hydration:warning` devtools events.
19
+ - Updated README and docs to reflect current SSR/hydration, router, diagnostics, examples, and release checklist coverage.
4
20
 
5
21
  ## 1.0.21 - 2026-05-13
6
22
 
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,7 +37,9 @@ 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_hydrationDiagnostic = (message, details = {}) => ({ ...__mikuru_componentInfo, phase: \"hydration\", message, ...details });");
42
+ 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
43
  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
44
  emit(context, 1, "const __mikuru_restoreRegistrar = () => { if (__mikuru_previousRegistrar === undefined) { delete globalThis.__mikuru_currentRegistrar; } else { globalThis.__mikuru_currentRegistrar = __mikuru_previousRegistrar; } };");
42
45
  emit(context, 1, "const __mikuru_recovery = {};");
@@ -134,6 +137,7 @@ function hydrateElement(context, node, elementVar, indent) {
134
137
  hydrateAttrs(context, node, elementVar, indent);
135
138
  hydrateEvents(context, node, elementVar, indent);
136
139
  const contentDirective = getContentDirectiveAttr(node);
140
+ const textareaModel = node.tag === "textarea" && hasElementModel(node);
137
141
  const hydrateChildrenBeforeModel = node.tag === "select" && !contentDirective;
138
142
  if (hydrateChildrenBeforeModel) {
139
143
  hydrateChildren(context, node.children, elementVar, indent);
@@ -141,7 +145,10 @@ function hydrateElement(context, node, elementVar, indent) {
141
145
  hydrateModelAndShow(context, node, elementVar, indent);
142
146
  hydrateContentDirective(context, node, elementVar, indent);
143
147
  hydrateTemplateRef(context, node, elementVar, indent);
144
- if (!contentDirective && !hydrateChildrenBeforeModel) {
148
+ if (textareaModel) {
149
+ emit(context, indent, `if (${elementVar}.childNodes.length > 0) { const __mikuru_textarea_value = ${elementVar}.value; ${elementVar}.textContent = ""; ${elementVar}.value = __mikuru_textarea_value; }`);
150
+ }
151
+ if (!contentDirective && !hydrateChildrenBeforeModel && !textareaModel) {
145
152
  hydrateChildren(context, node.children, elementVar, indent);
146
153
  }
147
154
  }
@@ -446,35 +453,134 @@ function emitRouterViewRouteSlot(context, node, propsVar, indent) {
446
453
  emit(context, indent, `if (typeof props.children === "function") { ${propsVar}.children = props.children; ${propsVar}.slots = { ...(${propsVar}.slots ?? {}), default: props.children }; }`);
447
454
  }
448
455
  function emitHydrationComponentSlots(context, node, propsVar, indent) {
449
- const children = getDefaultHydrationSlotChildren(node);
450
- if (!children || !hasMeaningfulHydrationChildren(children)) {
456
+ const slots = collectHydrationComponentSlots(context, node);
457
+ if (slots.length === 0) {
451
458
  return;
452
459
  }
460
+ const defaultSlot = slots.find((slot) => !slot.nameExpression && slot.name === "default");
461
+ if (defaultSlot) {
462
+ emitHydrationSlotFunction(context, `${propsVar}.children`, defaultSlot, indent);
463
+ }
464
+ emit(context, indent, `${propsVar}.slots = { ...(${propsVar}.slots ?? {}) };`);
465
+ for (const slot of slots) {
466
+ const property = slot.nameExpression ? `[${slot.nameExpression}]` : `[${quote(slot.name)}]`;
467
+ emitHydrationSlotFunction(context, `${propsVar}.slots${property}`, slot, indent);
468
+ }
469
+ }
470
+ function emitHydrationSlotFunction(context, target, slot, indent) {
453
471
  const slotTargetVar = nextName(context, "slotTarget");
454
472
  const slotPropsVar = nextName(context, "slotProps");
455
- emit(context, indent, `${propsVar}.children = (${slotTargetVar}, ${slotPropsVar} = {}) => {`);
456
- hydrateChildren(context, children, slotTargetVar, indent + 1);
473
+ emit(context, indent, `${target} = (${slotTargetVar}, ${slotPropsVar} = {}) => {`);
474
+ emitHydrationSlotScopeBindings(context, slot, slotPropsVar, indent + 1);
475
+ hydrateChildren(context, slot.children, slotTargetVar, indent + 1);
457
476
  emit(context, indent, "};");
458
- emit(context, indent, `${propsVar}.slots = { ...(${propsVar}.slots ?? {}), default: ${propsVar}.children };`);
459
477
  }
460
- function getDefaultHydrationSlotChildren(node) {
478
+ function emitHydrationSlotScopeBindings(context, slot, slotPropsVar, indent) {
479
+ for (const binding of parseHydrationSlotScopeBindings(slot.scope, context, slot.scopeLoc ?? slot.loc)) {
480
+ if (binding.kind === "props") {
481
+ emit(context, indent, `const ${binding.alias} = ${slotPropsVar};`);
482
+ continue;
483
+ }
484
+ if (binding.kind === "rest") {
485
+ emit(context, indent, `const ${binding.alias} = { get value() { const rest = { ...${slotPropsVar} }; ${binding.exclude.map((key) => `delete rest[${quote(key)}];`).join(" ")} return rest; } };`);
486
+ continue;
487
+ }
488
+ const valueExpression = slotScopePathExpression(slotPropsVar, binding.path);
489
+ if (binding.defaultValue) {
490
+ emit(context, indent, `const ${binding.alias} = { get value() { const value = ${valueExpression}; return value === undefined ? (${binding.defaultValue}) : value; } };`);
491
+ continue;
492
+ }
493
+ emit(context, indent, `const ${binding.alias} = { get value() { return ${valueExpression}; } };`);
494
+ }
495
+ }
496
+ function slotScopePathExpression(rootVar, path) {
497
+ return path.reduce((expression, key, index) => {
498
+ const property = /^[A-Za-z_$][\w$]*$/.test(key) ? `.${key}` : `[${quote(key)}]`;
499
+ return `${expression}${index === 0 ? property : `?.${property.slice(1)}`}`;
500
+ }, rootVar);
501
+ }
502
+ function collectHydrationComponentSlots(context, node) {
503
+ const slots = [];
504
+ const usedNames = new Set();
461
505
  const defaultChildren = [];
462
506
  for (const child of node.children) {
463
507
  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)) {
508
+ const slotDirective = getHydrationSlotTemplateDirective(child, context);
509
+ if (slotDirective) {
510
+ if (usedNames.has(slotDirective.name)) {
511
+ throwTemplateError(`Duplicate slot template: ${slotDirective.name}`, context, slotDirective.loc);
512
+ }
513
+ usedNames.add(slotDirective.name);
514
+ slots.push({
515
+ name: slotDirective.name,
516
+ nameExpression: slotDirective.nameExpression,
517
+ children: child.children,
518
+ scope: slotDirective.scope,
519
+ loc: child.loc,
520
+ scopeLoc: slotDirective.scopeLoc
521
+ });
469
522
  continue;
470
523
  }
471
524
  }
472
525
  defaultChildren.push(child);
473
526
  }
474
- return defaultChildren;
527
+ if (hasMeaningfulHydrationChildren(defaultChildren)) {
528
+ if (usedNames.has("default")) {
529
+ throwTemplateError("Duplicate slot template: default", context, node.loc);
530
+ }
531
+ slots.unshift({
532
+ name: "default",
533
+ children: defaultChildren,
534
+ scope: true,
535
+ loc: node.loc
536
+ });
537
+ }
538
+ return slots;
539
+ }
540
+ function getHydrationSlotTemplateDirective(node, context) {
541
+ const slotAttrs = node.attrs.filter((attr) => isSlotTemplateAttr(attr));
542
+ if (slotAttrs.length === 0) {
543
+ return undefined;
544
+ }
545
+ if (slotAttrs.length > 1) {
546
+ throwTemplateError("A slot template can only declare one slot target", context, slotAttrs[1]?.loc);
547
+ }
548
+ const attr = slotAttrs[0];
549
+ const name = getHydrationSlotTemplateName(attr, context);
550
+ return {
551
+ ...name,
552
+ scope: attr.value,
553
+ loc: attr.loc,
554
+ scopeLoc: attr.valueLoc
555
+ };
556
+ }
557
+ function getHydrationSlotTemplateName(attr, context) {
558
+ if (attr.name === "v-slot") {
559
+ return { name: "default" };
560
+ }
561
+ if (attr.name.startsWith("v-slot:")) {
562
+ return parseHydrationSlotTemplateName(attr.name.slice("v-slot:".length) || "default", attr, context);
563
+ }
564
+ if (attr.name.startsWith("#")) {
565
+ return parseHydrationSlotTemplateName(attr.name.slice(1) || "default", attr, context);
566
+ }
567
+ return { name: "default" };
475
568
  }
476
- function isDefaultSlotTemplateAttr(attr) {
477
- return attr.name === "v-slot" || attr.name === "v-slot:default" || attr.name === "#default";
569
+ function parseHydrationSlotTemplateName(rawName, attr, context) {
570
+ const name = rawName.trim();
571
+ const dynamicStart = name.indexOf("[");
572
+ const dynamicEnd = name.lastIndexOf("]");
573
+ if (dynamicStart >= 0 && dynamicEnd > dynamicStart) {
574
+ const expression = name.slice(dynamicStart + 1, dynamicEnd).trim();
575
+ if (!expression) {
576
+ throwTemplateError("Dynamic slot name requires an expression", context, attr.loc);
577
+ }
578
+ return {
579
+ name: `[${expression}]`,
580
+ nameExpression: compileTemplateExpression(expression, attr.name, toExpressionContext(context, attr.loc))
581
+ };
582
+ }
583
+ return { name };
478
584
  }
479
585
  function isSlotTemplateAttr(attr) {
480
586
  return attr.name === "v-slot" || attr.name.startsWith("v-slot:") || attr.name.startsWith("#");
@@ -482,6 +588,102 @@ function isSlotTemplateAttr(attr) {
482
588
  function hasMeaningfulHydrationChildren(children) {
483
589
  return children.some((child) => child.type === "element" || child.parts.some((part) => part.value.trim()));
484
590
  }
591
+ function parseHydrationSlotScopeBindings(scope, context, location) {
592
+ if (scope === true || !scope.trim()) {
593
+ return [];
594
+ }
595
+ const source = scope.trim();
596
+ if (isIdentifier(source)) {
597
+ return [{ kind: "props", alias: source }];
598
+ }
599
+ if (!source.startsWith("{") || !source.endsWith("}")) {
600
+ throwTemplateError("Slot scope must be an identifier or object destructuring pattern", context, location);
601
+ }
602
+ const body = source.slice(1, -1).trim();
603
+ if (!body) {
604
+ return [];
605
+ }
606
+ return parseHydrationSlotScopeObjectPattern(body, context, location, []);
607
+ }
608
+ function parseHydrationSlotScopeObjectPattern(body, context, location, pathPrefix) {
609
+ const bindings = [];
610
+ const excludedTopLevelKeys = [];
611
+ for (const part of splitTopLevel(body, ",")) {
612
+ const sourcePart = part.trim();
613
+ if (!sourcePart) {
614
+ continue;
615
+ }
616
+ if (sourcePart.startsWith("...")) {
617
+ const alias = sourcePart.slice(3).trim();
618
+ if (pathPrefix.length > 0) {
619
+ throwTemplateError("Slot scope rest destructuring is only supported at the top level", context, location);
620
+ }
621
+ if (!isIdentifier(alias)) {
622
+ throwTemplateError("Slot scope rest destructuring must use a simple identifier like ...rest", context, location);
623
+ }
624
+ bindings.push({ kind: "rest", alias, exclude: excludedTopLevelKeys });
625
+ continue;
626
+ }
627
+ const { left, right } = splitHydrationSlotScopeEntry(sourcePart, context, location);
628
+ if (!isIdentifier(left)) {
629
+ throwTemplateError(`Unsupported slot scope key "${left}". Use identifier keys in slot scope destructuring`, context, location);
630
+ }
631
+ if (pathPrefix.length === 0) {
632
+ excludedTopLevelKeys.push(left);
633
+ }
634
+ const path = [...pathPrefix, left];
635
+ if (right === undefined) {
636
+ bindings.push(...hydrationSlotScopeLeafBindings(left, path, context, location));
637
+ continue;
638
+ }
639
+ const value = right.trim();
640
+ if (value.startsWith("{") && value.endsWith("}")) {
641
+ bindings.push(...parseHydrationSlotScopeObjectPattern(value.slice(1, -1), context, location, path));
642
+ continue;
643
+ }
644
+ if (value.startsWith("[") || value.includes("{")) {
645
+ throwTemplateError("Slot scope destructuring supports nested object patterns only; array and mixed patterns are not supported", context, location);
646
+ }
647
+ bindings.push(...hydrationSlotScopeLeafBindings(value, path, context, location));
648
+ }
649
+ return bindings;
650
+ }
651
+ function splitHydrationSlotScopeEntry(source, context, location) {
652
+ const colonIndex = findTopLevelToken(source, ":");
653
+ if (colonIndex >= 0) {
654
+ return {
655
+ left: source.slice(0, colonIndex).trim(),
656
+ right: source.slice(colonIndex + 1).trim()
657
+ };
658
+ }
659
+ const equalsIndex = findTopLevelToken(source, "=");
660
+ if (equalsIndex >= 0) {
661
+ const left = source.slice(0, equalsIndex).trim();
662
+ if (!isIdentifier(left)) {
663
+ throwTemplateError("Slot scope default values can only be assigned to simple identifiers", context, location);
664
+ }
665
+ return { left, right: source };
666
+ }
667
+ return { left: source.trim() };
668
+ }
669
+ function hydrationSlotScopeLeafBindings(source, path, context, location) {
670
+ const equalsIndex = findTopLevelToken(source, "=");
671
+ const localSource = equalsIndex >= 0 ? source.slice(0, equalsIndex).trim() : source.trim();
672
+ const defaultSource = equalsIndex >= 0 ? source.slice(equalsIndex + 1).trim() : undefined;
673
+ if (!isIdentifier(localSource)) {
674
+ throwTemplateError(`Unsupported slot scope binding "${source}". Use identifiers, aliases, defaults, nested objects, or top-level ...rest`, context, location);
675
+ }
676
+ return [
677
+ {
678
+ kind: "property",
679
+ path,
680
+ alias: localSource,
681
+ defaultValue: defaultSource
682
+ ? compileTemplateExpression(defaultSource, "slot scope default", toExpressionContext(context, location))
683
+ : undefined
684
+ }
685
+ ];
686
+ }
485
687
  function hydrateDynamicComponentAtIndex(context, node, parentVar, domIndex, indent) {
486
688
  const isAttr = getDynamicComponentIsAttr(node);
487
689
  if (!isAttr) {
@@ -1438,6 +1640,9 @@ function parseModelDirective(name) {
1438
1640
  const [argument = "", ...modifiers] = name.slice("v-model:".length).split(".");
1439
1641
  return { argument, modifiers: modifiers.filter(Boolean) };
1440
1642
  }
1643
+ function hasElementModel(node) {
1644
+ return node.attrs.some((attr) => Boolean(parseModelDirective(attr.name)));
1645
+ }
1441
1646
  function toComponentEventProp(eventName) {
1442
1647
  return `on${eventName.split(":").map((part) => part ? part[0].toUpperCase() + part.slice(1) : "").join("")}`;
1443
1648
  }
@@ -1510,9 +1715,90 @@ function compileHydrationExpression(context, expression, usage) {
1510
1715
  filename: context.filename
1511
1716
  });
1512
1717
  }
1718
+ function toExpressionContext(context, location) {
1719
+ if (!context.source || !location) {
1720
+ return undefined;
1721
+ }
1722
+ return {
1723
+ source: context.source,
1724
+ offset: location.offset,
1725
+ filename: context.filename
1726
+ };
1727
+ }
1728
+ function throwTemplateError(message, context, location) {
1729
+ if (context.source && location) {
1730
+ throw createCompileError(message, context.source, location.offset, context.filename);
1731
+ }
1732
+ throw new Error(message);
1733
+ }
1513
1734
  function isIdentifier(value) {
1514
1735
  return /^[A-Za-z_$][\w$]*$/.test(value);
1515
1736
  }
1737
+ function splitTopLevel(source, delimiter) {
1738
+ const parts = [];
1739
+ let depth = 0;
1740
+ let quoteChar = "";
1741
+ let start = 0;
1742
+ for (let index = 0; index < source.length; index += 1) {
1743
+ const char = source[index];
1744
+ const previous = source[index - 1];
1745
+ if (quoteChar) {
1746
+ if (char === quoteChar && previous !== "\\") {
1747
+ quoteChar = "";
1748
+ }
1749
+ continue;
1750
+ }
1751
+ if (char === "\"" || char === "'" || char === "`") {
1752
+ quoteChar = char;
1753
+ continue;
1754
+ }
1755
+ if (char === "{" || char === "[" || char === "(") {
1756
+ depth += 1;
1757
+ continue;
1758
+ }
1759
+ if (char === "}" || char === "]" || char === ")") {
1760
+ depth -= 1;
1761
+ continue;
1762
+ }
1763
+ if (depth === 0 && source.startsWith(delimiter, index)) {
1764
+ parts.push(source.slice(start, index));
1765
+ start = index + delimiter.length;
1766
+ index += delimiter.length - 1;
1767
+ }
1768
+ }
1769
+ parts.push(source.slice(start));
1770
+ return parts;
1771
+ }
1772
+ function findTopLevelToken(source, token) {
1773
+ let depth = 0;
1774
+ let quoteChar = "";
1775
+ for (let index = 0; index < source.length; index += 1) {
1776
+ const char = source[index];
1777
+ const previous = source[index - 1];
1778
+ if (quoteChar) {
1779
+ if (char === quoteChar && previous !== "\\") {
1780
+ quoteChar = "";
1781
+ }
1782
+ continue;
1783
+ }
1784
+ if (char === "\"" || char === "'" || char === "`") {
1785
+ quoteChar = char;
1786
+ continue;
1787
+ }
1788
+ if (char === "{" || char === "[" || char === "(") {
1789
+ depth += 1;
1790
+ continue;
1791
+ }
1792
+ if (char === "}" || char === "]" || char === ")") {
1793
+ depth -= 1;
1794
+ continue;
1795
+ }
1796
+ if (depth === 0 && source.startsWith(token, index)) {
1797
+ return index;
1798
+ }
1799
+ }
1800
+ return -1;
1801
+ }
1516
1802
  function withTemplateRefMode(context, mode, callback) {
1517
1803
  const previousMode = context.templateRefMode;
1518
1804
  context.templateRefMode = mode;