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 +23 -1
- package/README.md +2 -0
- package/dist/compiler/generateHydration.js +312 -24
- package/dist/compiler/generateHydration.js.map +1 -1
- package/dist/compiler/generateSsr.js +188 -8
- package/dist/compiler/generateSsr.js.map +1 -1
- package/dist/runtime/asyncComponent.d.ts +1 -0
- package/dist/runtime/asyncComponent.js +95 -0
- package/dist/runtime/asyncComponent.js.map +1 -1
- package/dist/server.d.ts +5 -1
- package/dist/server.js +18 -6
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
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
|
|
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 (
|
|
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
|
|
450
|
-
if (
|
|
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, `${
|
|
456
|
-
|
|
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
|
|
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
|
|
465
|
-
if (
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
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
|
|
477
|
-
|
|
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;
|