@vobs/runtime 1.7.8 → 1.8.1
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 +1 -1
- package/dist/async-boundary.cjs +14 -4
- package/dist/async-boundary.cjs.map +1 -1
- package/dist/async-boundary.js +17 -7
- package/dist/async-boundary.js.map +1 -1
- package/dist/bind.cjs +43 -3
- package/dist/bind.cjs.map +1 -1
- package/dist/bind.js +48 -8
- package/dist/bind.js.map +1 -1
- package/dist/boundary.cjs +159 -84
- package/dist/boundary.cjs.map +1 -1
- package/dist/boundary.js +162 -87
- package/dist/boundary.js.map +1 -1
- package/dist/client-only.cjs +323 -0
- package/dist/client-only.cjs.map +1 -0
- package/dist/client-only.d.cts +42 -0
- package/dist/client-only.d.ts +42 -0
- package/dist/client-only.js +299 -0
- package/dist/client-only.js.map +1 -0
- package/dist/debug.cjs.map +1 -1
- package/dist/debug.d.cts +12 -2
- package/dist/debug.d.ts +12 -2
- package/dist/debug.js.map +1 -1
- package/dist/dom-events.cjs +46 -0
- package/dist/dom-events.cjs.map +1 -0
- package/dist/dom-events.d.cts +23 -0
- package/dist/dom-events.d.ts +23 -0
- package/dist/dom-events.js +21 -0
- package/dist/dom-events.js.map +1 -0
- package/dist/dom-props.cjs +154 -0
- package/dist/dom-props.cjs.map +1 -0
- package/dist/dom-props.d.cts +21 -0
- package/dist/dom-props.d.ts +21 -0
- package/dist/dom-props.js +128 -0
- package/dist/dom-props.js.map +1 -0
- package/dist/dynamic.cjs +68 -14
- package/dist/dynamic.cjs.map +1 -1
- package/dist/dynamic.js +73 -19
- package/dist/dynamic.js.map +1 -1
- package/dist/error-boundary.cjs +159 -84
- package/dist/error-boundary.cjs.map +1 -1
- package/dist/error-boundary.js +162 -87
- package/dist/error-boundary.js.map +1 -1
- package/dist/error.cjs +52 -20
- package/dist/error.cjs.map +1 -1
- package/dist/error.d.cts +8 -0
- package/dist/error.d.ts +8 -0
- package/dist/error.js +52 -20
- package/dist/error.js.map +1 -1
- package/dist/fragment.cjs.map +1 -1
- package/dist/fragment.js +1 -1
- package/dist/fragment.js.map +1 -1
- package/dist/hmr.cjs +137 -1
- package/dist/hmr.cjs.map +1 -1
- package/dist/hmr.js +135 -1
- package/dist/hmr.js.map +1 -1
- package/dist/index.cjs +713 -236
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +715 -245
- package/dist/index.js.map +1 -1
- package/dist/ops.cjs +561 -107
- package/dist/ops.cjs.map +1 -1
- package/dist/ops.d.cts +49 -4
- package/dist/ops.d.ts +49 -4
- package/dist/ops.js +557 -108
- package/dist/ops.js.map +1 -1
- package/dist/profiler.cjs +14 -4
- package/dist/profiler.cjs.map +1 -1
- package/dist/profiler.js +17 -7
- package/dist/profiler.js.map +1 -1
- package/dist/ref.cjs +160 -5
- package/dist/ref.cjs.map +1 -1
- package/dist/ref.js +160 -5
- package/dist/ref.js.map +1 -1
- package/dist/renderer.cjs.map +1 -1
- package/dist/renderer.d.cts +2 -0
- package/dist/renderer.d.ts +2 -0
- package/dist/show.cjs +196 -0
- package/dist/show.cjs.map +1 -0
- package/dist/show.d.cts +54 -0
- package/dist/show.d.ts +54 -0
- package/dist/show.js +171 -0
- package/dist/show.js.map +1 -0
- package/dist/svg.cjs +102 -0
- package/dist/svg.cjs.map +1 -0
- package/dist/svg.d.cts +21 -0
- package/dist/svg.d.ts +21 -0
- package/dist/svg.js +76 -0
- package/dist/svg.js.map +1 -0
- package/package.json +20 -2
- package/src/bind.ts +30 -2
- package/src/boundaries.test.ts +54 -1
- package/src/boundary.ts +24 -8
- package/src/clear-remove.test.ts +85 -0
- package/src/client-only.test.ts +140 -0
- package/src/client-only.ts +65 -0
- package/src/component-empty-return.test.ts +123 -0
- package/src/debug.ts +12 -1
- package/src/dom-events.ts +32 -0
- package/src/dom-props-consistency.test.ts +149 -0
- package/src/dom-props.test.ts +70 -0
- package/src/dom-props.ts +107 -0
- package/src/dynamic.ts +91 -14
- package/src/error.test.ts +76 -1
- package/src/error.ts +87 -22
- package/src/event-cleanup-growth.test.ts +128 -0
- package/src/events.test.ts +16 -3
- package/src/hmr.ts +8 -2
- package/src/index.ts +13 -1
- package/src/invariants.test.ts +252 -0
- package/src/list-fast-path.test.ts +83 -0
- package/src/list-reactive-item.test.ts +77 -0
- package/src/ops.ts +419 -60
- package/src/props.test.ts +61 -1
- package/src/ref-cleanup.test.ts +144 -0
- package/src/ref.ts +57 -8
- package/src/remove-listener-identity.test.ts +97 -0
- package/src/renderer.ts +3 -1
- package/src/select-hidden-subscription.test.ts +104 -0
- package/src/show-classlist.test.ts +205 -0
- package/src/show.ts +107 -0
- package/src/spread-stable-shape.test.ts +109 -0
- package/src/spread.test.ts +86 -0
- package/src/svg.test.ts +28 -1
- package/src/svg.ts +34 -0
package/dist/error.js
CHANGED
|
@@ -25,15 +25,63 @@ function createVobsError(options) {
|
|
|
25
25
|
}
|
|
26
26
|
__name(createVobsError, "createVobsError");
|
|
27
27
|
function isVobsError(value) {
|
|
28
|
-
return value instanceof VobsError || Boolean(value && typeof value === "object" && typeof value.code === "string" && typeof value.message === "string"
|
|
28
|
+
return value instanceof VobsError || Boolean(value && typeof value === "object" && typeof value.code === "string" && typeof value.message === "string");
|
|
29
29
|
}
|
|
30
30
|
__name(isVobsError, "isVobsError");
|
|
31
|
+
function isForeignVobsError(value) {
|
|
32
|
+
return value instanceof Error && value.name === "VobsError" && typeof value.code === "string";
|
|
33
|
+
}
|
|
34
|
+
__name(isForeignVobsError, "isForeignVobsError");
|
|
35
|
+
function describeUnknown(value) {
|
|
36
|
+
if (value === null || value === void 0) return String(value);
|
|
37
|
+
if (typeof value === "string") return value;
|
|
38
|
+
if (typeof value === "object") {
|
|
39
|
+
const message = value.message;
|
|
40
|
+
if (typeof message === "string" && message !== "") return message;
|
|
41
|
+
try {
|
|
42
|
+
return JSON.stringify(value) ?? String(value);
|
|
43
|
+
} catch {
|
|
44
|
+
return String(value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return String(value);
|
|
48
|
+
}
|
|
49
|
+
__name(describeUnknown, "describeUnknown");
|
|
50
|
+
function adoptVobsError(value, defaults) {
|
|
51
|
+
return new VobsError({
|
|
52
|
+
code: value.code,
|
|
53
|
+
message: value.message,
|
|
54
|
+
severity: value.severity ?? defaults.severity,
|
|
55
|
+
layer: value.layer ?? defaults.layer,
|
|
56
|
+
cause: value.cause ?? (value instanceof Error ? value : void 0),
|
|
57
|
+
fix: value.fix ?? defaults.fix,
|
|
58
|
+
location: value.location,
|
|
59
|
+
trace: value.trace,
|
|
60
|
+
example: value.example,
|
|
61
|
+
docs: value.docs,
|
|
62
|
+
codeFrame: value.codeFrame
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
__name(adoptVobsError, "adoptVobsError");
|
|
31
66
|
function normalizeVobsError(value, defaults = {}) {
|
|
32
67
|
if (value instanceof VobsError) return value;
|
|
68
|
+
if (isForeignVobsError(value)) return adoptVobsError(value, defaults);
|
|
33
69
|
if (value instanceof Error) {
|
|
34
70
|
const metadata = value;
|
|
35
71
|
const code = defaults.code ?? (typeof metadata.vobsCode === "string" ? metadata.vobsCode : void 0);
|
|
36
|
-
if (code)
|
|
72
|
+
if (code) {
|
|
73
|
+
defineErrorMetadata(value, "code", code);
|
|
74
|
+
if (value.code !== code) {
|
|
75
|
+
return new VobsError({
|
|
76
|
+
code,
|
|
77
|
+
message: value.message,
|
|
78
|
+
severity: defaults.severity ?? "error",
|
|
79
|
+
layer: defaults.layer ?? "runtime",
|
|
80
|
+
cause: value,
|
|
81
|
+
fix: defaults.fix ?? (typeof metadata.vobsHint === "string" ? metadata.vobsHint : void 0)
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
37
85
|
defineErrorMetadata(value, "severity", defaults.severity ?? "error");
|
|
38
86
|
defineErrorMetadata(value, "layer", defaults.layer ?? "runtime");
|
|
39
87
|
const fix = defaults.fix ?? (typeof metadata.vobsHint === "string" ? metadata.vobsHint : void 0);
|
|
@@ -44,26 +92,10 @@ function normalizeVobsError(value, defaults = {}) {
|
|
|
44
92
|
}
|
|
45
93
|
return value;
|
|
46
94
|
}
|
|
47
|
-
if (isVobsError(value))
|
|
48
|
-
const candidate = value;
|
|
49
|
-
return new VobsError({
|
|
50
|
-
code: candidate.code,
|
|
51
|
-
message: candidate.message,
|
|
52
|
-
severity: candidate.severity ?? defaults.severity,
|
|
53
|
-
layer: candidate.layer ?? defaults.layer,
|
|
54
|
-
cause: candidate.cause,
|
|
55
|
-
fix: candidate.fix ?? defaults.fix,
|
|
56
|
-
location: candidate.location,
|
|
57
|
-
trace: candidate.trace,
|
|
58
|
-
example: candidate.example,
|
|
59
|
-
docs: candidate.docs,
|
|
60
|
-
codeFrame: candidate.codeFrame
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
const message = String(value);
|
|
95
|
+
if (isVobsError(value)) return adoptVobsError(value, defaults);
|
|
64
96
|
return new VobsError({
|
|
65
97
|
code: defaults.code ?? "VOBS_UNKNOWN",
|
|
66
|
-
message,
|
|
98
|
+
message: describeUnknown(value),
|
|
67
99
|
severity: defaults.severity ?? "error",
|
|
68
100
|
layer: defaults.layer ?? "runtime",
|
|
69
101
|
cause: void 0,
|
package/dist/error.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/error.ts"],"sourcesContent":["/** Shared error protocol used by the runtime, compiler and DevTools. */\nexport type VobsErrorSeverity = 'error' | 'warning' | 'info'\nexport type VobsErrorLayer =\n | 'compiler'\n | 'runtime'\n | 'constraint'\n | 'permission'\n | 'ssr'\n | 'http'\n | 'resource'\n\nexport interface VobsErrorLocation {\n readonly file: string\n readonly line: number\n readonly column: number\n}\n\nexport interface VobsErrorOptions {\n readonly code: string\n readonly message: string\n readonly severity?: VobsErrorSeverity\n readonly layer?: VobsErrorLayer\n readonly cause?: unknown\n readonly fix?: string\n readonly location?: VobsErrorLocation\n readonly trace?: readonly string[]\n readonly example?: string\n readonly docs?: string\n readonly codeFrame?: string\n}\n\nexport interface VobsErrorDefaults {\n readonly code?: string\n readonly severity?: VobsErrorSeverity\n readonly layer?: VobsErrorLayer\n readonly fix?: string\n}\n\n/** A framework error with stable machine-readable metadata. */\nexport class VobsError extends Error {\n readonly code: string\n readonly severity: VobsErrorSeverity\n readonly layer: VobsErrorLayer\n readonly cause?: unknown\n readonly fix?: string\n readonly location?: VobsErrorLocation\n readonly trace?: readonly string[]\n readonly example?: string\n readonly docs?: string\n readonly codeFrame?: string\n\n constructor(options: VobsErrorOptions) {\n super(options.message)\n this.name = 'VobsError'\n this.code = options.code\n this.severity = options.severity ?? 'error'\n this.layer = options.layer ?? 'runtime'\n this.cause = options.cause\n this.fix = options.fix\n this.location = options.location\n this.trace = options.trace\n this.example = options.example\n this.docs = options.docs\n this.codeFrame = options.codeFrame\n }\n}\n\nexport function createVobsError(options: VobsErrorOptions): VobsError {\n return new VobsError(options)\n}\n\nexport function isVobsError(value: unknown): value is VobsError {\n return value instanceof VobsError\n || Boolean(value && typeof value === 'object'\n && typeof (value as { code?: unknown }).code === 'string'\n && typeof (value as { message?: unknown }).message === 'string'\n && typeof (value as { layer?: unknown }).layer === 'string')\n}\n\n/** Convert thrown strings and third-party errors without losing their message. */\nexport function normalizeVobsError(value: unknown, defaults: VobsErrorDefaults = {}): VobsError {\n if (value instanceof VobsError) return value\n if (value instanceof Error) {\n // Preserve the original Error identity so boundaries and DevTools can\n // correlate event/effect phases without creating duplicate diagnostics.\n const metadata = value as Error & {\n readonly vobsCode?: unknown\n readonly vobsHint?: unknown\n readonly vobsSource?: unknown\n }\n const code = defaults.code ?? (typeof metadata.vobsCode === 'string' ? metadata.vobsCode : undefined)\n if (code) defineErrorMetadata(value, 'code', code)\n defineErrorMetadata(value, 'severity', defaults.severity ?? 'error')\n defineErrorMetadata(value, 'layer', defaults.layer ?? 'runtime')\n const fix = defaults.fix ?? (typeof metadata.vobsHint === 'string' ? metadata.vobsHint : undefined)\n if (fix) defineErrorMetadata(value, 'fix', fix)\n const source = metadata.vobsSource\n if (source && typeof source === 'object'\n && typeof (source as { file?: unknown }).file === 'string'\n && typeof (source as { line?: unknown }).line === 'number'\n && typeof (source as { column?: unknown }).column === 'number') {\n defineErrorMetadata(value, 'location', source)\n }\n return value as VobsError\n }\n if (isVobsError(value)) {\n const candidate = value as VobsErrorOptions & { severity?: VobsErrorSeverity; layer?: VobsErrorLayer }\n return new VobsError({\n code: candidate.code,\n message: candidate.message,\n severity: candidate.severity ?? defaults.severity,\n layer: candidate.layer ?? defaults.layer,\n cause: candidate.cause,\n fix: candidate.fix ?? defaults.fix,\n location: candidate.location,\n trace: candidate.trace,\n example: candidate.example,\n docs: candidate.docs,\n codeFrame: candidate.codeFrame\n })\n }\n const message = String(value)\n return new VobsError({\n code: defaults.code ?? 'VOBS_UNKNOWN',\n message,\n severity: defaults.severity ?? 'error',\n layer: defaults.layer ?? 'runtime',\n cause: undefined,\n fix: defaults.fix\n })\n}\n\nfunction defineErrorMetadata(target: Error, key: string, value: unknown): void {\n if (key in target) return\n try {\n Object.defineProperty(target, key, { configurable: true, enumerable: false, value, writable: true })\n } catch {\n // Frozen third-party errors still retain their original message and stack.\n }\n}\n\nexport interface FormatVobsErrorOptions {\n readonly environment?: 'development' | 'production'\n readonly includeStack?: boolean\n}\n\n/** Format a concise, actionable message for terminals and dev overlays. */\nexport function formatVobsError(\n value: unknown,\n options: FormatVobsErrorOptions = {}\n): string {\n const error = normalizeVobsError(value)\n const code = error.code || 'VOBS_UNKNOWN'\n const severity = error.severity || 'error'\n if (options.environment === 'production') return `[Vobs ${code}] ${error.message}`\n\n const lines = [`[Vobs ${capitalize(severity)}] ${error.message}`, `Code: ${code}`]\n if (error.location) lines.push(`Location: ${error.location.file}:${error.location.line}:${error.location.column}`)\n if (error.codeFrame) lines.push('', error.codeFrame)\n if (error.cause !== undefined) lines.push(`Cause: ${formatCause(error.cause)}`)\n if (error.trace?.length) lines.push('', `Trace: ${error.trace.join(' → ')}`)\n if (error.fix) lines.push('', `Fix: ${error.fix}`)\n if (error.example) lines.push('', `Example:\\n${error.example}`)\n if (error.docs) lines.push(`Docs: ${error.docs}`)\n if (options.includeStack && error.stack) lines.push('', error.stack)\n return lines.join('\\n')\n}\n\nfunction formatCause(value: unknown): string {\n return value instanceof Error ? `${value.name}: ${value.message}` : String(value)\n}\n\nfunction capitalize(value: string): string {\n return value.slice(0, 1).toUpperCase() + value.slice(1)\n}\n"],"mappings":";;;;AAuCO,IAAM,aAAN,MAAM,mBAAkB,MAAM;AAAA,EAYnC,YAAY,SAA2B;AACrC,UAAM,QAAQ,OAAO;AACrB,SAAK,OAAO;AACZ,SAAK,OAAO,QAAQ;AACpB,SAAK,WAAW,QAAQ,YAAY;AACpC,SAAK,QAAQ,QAAQ,SAAS;AAC9B,SAAK,QAAQ,QAAQ;AACrB,SAAK,MAAM,QAAQ;AACnB,SAAK,WAAW,QAAQ;AACxB,SAAK,QAAQ,QAAQ;AACrB,SAAK,UAAU,QAAQ;AACvB,SAAK,OAAO,QAAQ;AACpB,SAAK,YAAY,QAAQ;AAAA,EAC3B;AACF;AA1BqC;AAA9B,IAAM,YAAN;AA4BA,SAAS,gBAAgB,SAAsC;AACpE,SAAO,IAAI,UAAU,OAAO;AAC9B;AAFgB;AAIT,SAAS,YAAY,OAAoC;AAC9D,SAAO,iBAAiB,aACnB,QAAQ,SAAS,OAAO,UAAU,YAChC,OAAQ,MAA6B,SAAS,YAC9C,OAAQ,MAAgC,YAAY,YACpD,OAAQ,MAA8B,UAAU,QAAQ;AACjE;AANgB;AAST,SAAS,mBAAmB,OAAgB,WAA8B,CAAC,GAAc;AAC9F,MAAI,iBAAiB,UAAW,QAAO;AACvC,MAAI,iBAAiB,OAAO;AAG1B,UAAM,WAAW;AAKjB,UAAM,OAAO,SAAS,SAAS,OAAO,SAAS,aAAa,WAAW,SAAS,WAAW;AAC3F,QAAI,KAAM,qBAAoB,OAAO,QAAQ,IAAI;AACjD,wBAAoB,OAAO,YAAY,SAAS,YAAY,OAAO;AACnE,wBAAoB,OAAO,SAAS,SAAS,SAAS,SAAS;AAC/D,UAAM,MAAM,SAAS,QAAQ,OAAO,SAAS,aAAa,WAAW,SAAS,WAAW;AACzF,QAAI,IAAK,qBAAoB,OAAO,OAAO,GAAG;AAC9C,UAAM,SAAS,SAAS;AACxB,QAAI,UAAU,OAAO,WAAW,YAC3B,OAAQ,OAA8B,SAAS,YAC/C,OAAQ,OAA8B,SAAS,YAC/C,OAAQ,OAAgC,WAAW,UAAU;AAChE,0BAAoB,OAAO,YAAY,MAAM;AAAA,IAC/C;AACA,WAAO;AAAA,EACT;AACA,MAAI,YAAY,KAAK,GAAG;AACtB,UAAM,YAAY;AAClB,WAAO,IAAI,UAAU;AAAA,MACnB,MAAM,UAAU;AAAA,MAChB,SAAS,UAAU;AAAA,MACnB,UAAU,UAAU,YAAY,SAAS;AAAA,MACzC,OAAO,UAAU,SAAS,SAAS;AAAA,MACnC,OAAO,UAAU;AAAA,MACjB,KAAK,UAAU,OAAO,SAAS;AAAA,MAC/B,UAAU,UAAU;AAAA,MACpB,OAAO,UAAU;AAAA,MACjB,SAAS,UAAU;AAAA,MACnB,MAAM,UAAU;AAAA,MAChB,WAAW,UAAU;AAAA,IACvB,CAAC;AAAA,EACH;AACA,QAAM,UAAU,OAAO,KAAK;AAC5B,SAAO,IAAI,UAAU;AAAA,IACnB,MAAM,SAAS,QAAQ;AAAA,IACvB;AAAA,IACA,UAAU,SAAS,YAAY;AAAA,IAC/B,OAAO,SAAS,SAAS;AAAA,IACzB,OAAO;AAAA,IACP,KAAK,SAAS;AAAA,EAChB,CAAC;AACH;AAlDgB;AAoDhB,SAAS,oBAAoB,QAAe,KAAa,OAAsB;AAC7E,MAAI,OAAO,OAAQ;AACnB,MAAI;AACF,WAAO,eAAe,QAAQ,KAAK,EAAE,cAAc,MAAM,YAAY,OAAO,OAAO,UAAU,KAAK,CAAC;AAAA,EACrG,QAAQ;AAAA,EAER;AACF;AAPS;AAeF,SAAS,gBACd,OACA,UAAkC,CAAC,GAC3B;AACR,QAAM,QAAQ,mBAAmB,KAAK;AACtC,QAAM,OAAO,MAAM,QAAQ;AAC3B,QAAM,WAAW,MAAM,YAAY;AACnC,MAAI,QAAQ,gBAAgB,aAAc,QAAO,SAAS,IAAI,KAAK,MAAM,OAAO;AAEhF,QAAM,QAAQ,CAAC,SAAS,WAAW,QAAQ,CAAC,KAAK,MAAM,OAAO,IAAI,SAAS,IAAI,EAAE;AACjF,MAAI,MAAM,SAAU,OAAM,KAAK,aAAa,MAAM,SAAS,IAAI,IAAI,MAAM,SAAS,IAAI,IAAI,MAAM,SAAS,MAAM,EAAE;AACjH,MAAI,MAAM,UAAW,OAAM,KAAK,IAAI,MAAM,SAAS;AACnD,MAAI,MAAM,UAAU,OAAW,OAAM,KAAK,UAAU,YAAY,MAAM,KAAK,CAAC,EAAE;AAC9E,MAAI,MAAM,OAAO,OAAQ,OAAM,KAAK,IAAI,UAAU,MAAM,MAAM,KAAK,UAAK,CAAC,EAAE;AAC3E,MAAI,MAAM,IAAK,OAAM,KAAK,IAAI,QAAQ,MAAM,GAAG,EAAE;AACjD,MAAI,MAAM,QAAS,OAAM,KAAK,IAAI;AAAA,EAAa,MAAM,OAAO,EAAE;AAC9D,MAAI,MAAM,KAAM,OAAM,KAAK,SAAS,MAAM,IAAI,EAAE;AAChD,MAAI,QAAQ,gBAAgB,MAAM,MAAO,OAAM,KAAK,IAAI,MAAM,KAAK;AACnE,SAAO,MAAM,KAAK,IAAI;AACxB;AAnBgB;AAqBhB,SAAS,YAAY,OAAwB;AAC3C,SAAO,iBAAiB,QAAQ,GAAG,MAAM,IAAI,KAAK,MAAM,OAAO,KAAK,OAAO,KAAK;AAClF;AAFS;AAIT,SAAS,WAAW,OAAuB;AACzC,SAAO,MAAM,MAAM,GAAG,CAAC,EAAE,YAAY,IAAI,MAAM,MAAM,CAAC;AACxD;AAFS;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/error.ts"],"sourcesContent":["/** Shared error protocol used by the runtime, compiler and DevTools. */\nexport type VobsErrorSeverity = 'error' | 'warning' | 'info'\nexport type VobsErrorLayer =\n | 'compiler'\n | 'runtime'\n | 'constraint'\n | 'permission'\n | 'ssr'\n | 'http'\n | 'resource'\n\nexport interface VobsErrorLocation {\n readonly file: string\n readonly line: number\n readonly column: number\n}\n\nexport interface VobsErrorOptions {\n readonly code: string\n readonly message: string\n readonly severity?: VobsErrorSeverity\n readonly layer?: VobsErrorLayer\n readonly cause?: unknown\n readonly fix?: string\n readonly location?: VobsErrorLocation\n readonly trace?: readonly string[]\n readonly example?: string\n readonly docs?: string\n readonly codeFrame?: string\n}\n\nexport interface VobsErrorDefaults {\n readonly code?: string\n readonly severity?: VobsErrorSeverity\n readonly layer?: VobsErrorLayer\n readonly fix?: string\n}\n\n/** A framework error with stable machine-readable metadata. */\nexport class VobsError extends Error {\n readonly code: string\n readonly severity: VobsErrorSeverity\n readonly layer: VobsErrorLayer\n readonly cause?: unknown\n readonly fix?: string\n readonly location?: VobsErrorLocation\n readonly trace?: readonly string[]\n readonly example?: string\n readonly docs?: string\n readonly codeFrame?: string\n\n constructor(options: VobsErrorOptions) {\n super(options.message)\n this.name = 'VobsError'\n this.code = options.code\n this.severity = options.severity ?? 'error'\n this.layer = options.layer ?? 'runtime'\n this.cause = options.cause\n this.fix = options.fix\n this.location = options.location\n this.trace = options.trace\n this.example = options.example\n this.docs = options.docs\n this.codeFrame = options.codeFrame\n }\n}\n\nexport function createVobsError(options: VobsErrorOptions): VobsError {\n return new VobsError(options)\n}\n\n/**\n * 结构化识别(不依赖 `instanceof`)。\n *\n * **不能要求 `layer` 也齐全** —— 原来要求 code + message + layer 三者都是字符串,\n * 于是调用方递一个 `{ code, message }` 就会被判为「不是 VobsError」,掉进最后的\n * `String(value)` 兜底,输出 `[object Object]`:诊断系统自己把诊断丢了。\n * layer 缺失在下面构造时会补上默认值,没有理由在识别阶段就否掉。\n */\nexport function isVobsError(value: unknown): value is VobsError {\n return value instanceof VobsError\n || Boolean(value && typeof value === 'object'\n && typeof (value as { code?: unknown }).code === 'string'\n && typeof (value as { message?: unknown }).message === 'string')\n}\n\n/**\n * 别的打包副本造的 VobsError。\n *\n * tsup `splitting: false` 让 error.ts 同时进 `dist/index.js` 与 `dist/error.js`,\n * 两份类互不 `instanceof`。字段齐全,只是身份不同 —— 识别出来复制成本地实例。\n */\nfunction isForeignVobsError(value: unknown): value is Error & VobsErrorOptions {\n return value instanceof Error\n && value.name === 'VobsError'\n && typeof (value as { code?: unknown }).code === 'string'\n}\n\n/** 兜底取消息:至少要能读出对象上的 message,别再输出 `[object Object]`。 */\nfunction describeUnknown(value: unknown): string {\n if (value === null || value === undefined) return String(value)\n if (typeof value === 'string') return value\n if (typeof value === 'object') {\n const message = (value as { message?: unknown }).message\n if (typeof message === 'string' && message !== '') return message\n try {\n return JSON.stringify(value) ?? String(value)\n } catch {\n return String(value)\n }\n }\n return String(value)\n}\n\n/** 把任意来源的「类 VobsError」字段复制成本地实例。 */\nfunction adoptVobsError(\n value: VobsErrorOptions & { cause?: unknown },\n defaults: VobsErrorDefaults\n): VobsError {\n return new VobsError({\n code: value.code,\n message: value.message,\n severity: value.severity ?? defaults.severity,\n layer: value.layer ?? defaults.layer,\n cause: value.cause ?? (value instanceof Error ? value : undefined),\n fix: value.fix ?? defaults.fix,\n location: value.location,\n trace: value.trace,\n example: value.example,\n docs: value.docs,\n codeFrame: value.codeFrame\n })\n}\n\n/** Convert thrown strings and third-party errors without losing their message. */\nexport function normalizeVobsError(value: unknown, defaults: VobsErrorDefaults = {}): VobsError {\n // 本地实例:原样返回,保持同一性与已有 stack\n if (value instanceof VobsError) return value\n\n // 别的副本造的:复制成本地实例,下游 instanceof 才成立\n if (isForeignVobsError(value)) return adoptVobsError(value, defaults)\n\n if (value instanceof Error) {\n // Preserve the original Error identity so boundaries and DevTools can\n // correlate event/effect phases without creating duplicate diagnostics.\n const metadata = value as Error & {\n readonly vobsCode?: unknown\n readonly vobsHint?: unknown\n readonly vobsSource?: unknown\n }\n const code = defaults.code ?? (typeof metadata.vobsCode === 'string' ? metadata.vobsCode : undefined)\n if (code) {\n defineErrorMetadata(value, 'code', code)\n /*\n * 冻结 / 不可扩展的第三方 Error 上挂不上元数据。原来这里静默吞掉,\n * 于是 normalize 的结果 code 是 undefined(打印成 VOBS_UNKNOWN)。\n * 挂不上就复制成本地实例,别把 code 丢了。\n */\n if ((value as { code?: unknown }).code !== code) {\n return new VobsError({\n code,\n message: value.message,\n severity: defaults.severity ?? 'error',\n layer: defaults.layer ?? 'runtime',\n cause: value,\n fix: defaults.fix ?? (typeof metadata.vobsHint === 'string' ? metadata.vobsHint : undefined)\n })\n }\n }\n defineErrorMetadata(value, 'severity', defaults.severity ?? 'error')\n defineErrorMetadata(value, 'layer', defaults.layer ?? 'runtime')\n const fix = defaults.fix ?? (typeof metadata.vobsHint === 'string' ? metadata.vobsHint : undefined)\n if (fix) defineErrorMetadata(value, 'fix', fix)\n const source = metadata.vobsSource\n if (source && typeof source === 'object'\n && typeof (source as { file?: unknown }).file === 'string'\n && typeof (source as { line?: unknown }).line === 'number'\n && typeof (source as { column?: unknown }).column === 'number') {\n defineErrorMetadata(value, 'location', source)\n }\n return value as VobsError\n }\n\n // 结构化对象(可能缺 layer,甚至缺 severity):补齐后构造成本地 VobsError\n if (isVobsError(value)) return adoptVobsError(value as VobsErrorOptions, defaults)\n\n return new VobsError({\n code: defaults.code ?? 'VOBS_UNKNOWN',\n message: describeUnknown(value),\n severity: defaults.severity ?? 'error',\n layer: defaults.layer ?? 'runtime',\n cause: undefined,\n fix: defaults.fix\n })\n}\n\nfunction defineErrorMetadata(target: Error, key: string, value: unknown): void {\n if (key in target) return\n try {\n Object.defineProperty(target, key, { configurable: true, enumerable: false, value, writable: true })\n } catch {\n // 冻结 / 不可扩展的第三方 Error:元数据挂不上。调用方会用「写完再读回」判断\n // 是否成功,失败时改成复制成本地实例,而不是静默丢掉 code。\n }\n}\n\nexport interface FormatVobsErrorOptions {\n readonly environment?: 'development' | 'production'\n readonly includeStack?: boolean\n}\n\n/** Format a concise, actionable message for terminals and dev overlays. */\nexport function formatVobsError(\n value: unknown,\n options: FormatVobsErrorOptions = {}\n): string {\n const error = normalizeVobsError(value)\n const code = error.code || 'VOBS_UNKNOWN'\n const severity = error.severity || 'error'\n if (options.environment === 'production') return `[Vobs ${code}] ${error.message}`\n\n const lines = [`[Vobs ${capitalize(severity)}] ${error.message}`, `Code: ${code}`]\n if (error.location) lines.push(`Location: ${error.location.file}:${error.location.line}:${error.location.column}`)\n if (error.codeFrame) lines.push('', error.codeFrame)\n if (error.cause !== undefined) lines.push(`Cause: ${formatCause(error.cause)}`)\n if (error.trace?.length) lines.push('', `Trace: ${error.trace.join(' → ')}`)\n if (error.fix) lines.push('', `Fix: ${error.fix}`)\n if (error.example) lines.push('', `Example:\\n${error.example}`)\n if (error.docs) lines.push(`Docs: ${error.docs}`)\n if (options.includeStack && error.stack) lines.push('', error.stack)\n return lines.join('\\n')\n}\n\nfunction formatCause(value: unknown): string {\n return value instanceof Error ? `${value.name}: ${value.message}` : String(value)\n}\n\nfunction capitalize(value: string): string {\n return value.slice(0, 1).toUpperCase() + value.slice(1)\n}\n"],"mappings":";;;;AAuCO,IAAM,aAAN,MAAM,mBAAkB,MAAM;AAAA,EAYnC,YAAY,SAA2B;AACrC,UAAM,QAAQ,OAAO;AACrB,SAAK,OAAO;AACZ,SAAK,OAAO,QAAQ;AACpB,SAAK,WAAW,QAAQ,YAAY;AACpC,SAAK,QAAQ,QAAQ,SAAS;AAC9B,SAAK,QAAQ,QAAQ;AACrB,SAAK,MAAM,QAAQ;AACnB,SAAK,WAAW,QAAQ;AACxB,SAAK,QAAQ,QAAQ;AACrB,SAAK,UAAU,QAAQ;AACvB,SAAK,OAAO,QAAQ;AACpB,SAAK,YAAY,QAAQ;AAAA,EAC3B;AACF;AA1BqC;AAA9B,IAAM,YAAN;AA4BA,SAAS,gBAAgB,SAAsC;AACpE,SAAO,IAAI,UAAU,OAAO;AAC9B;AAFgB;AAYT,SAAS,YAAY,OAAoC;AAC9D,SAAO,iBAAiB,aACnB,QAAQ,SAAS,OAAO,UAAU,YAChC,OAAQ,MAA6B,SAAS,YAC9C,OAAQ,MAAgC,YAAY,QAAQ;AACrE;AALgB;AAahB,SAAS,mBAAmB,OAAmD;AAC7E,SAAO,iBAAiB,SACnB,MAAM,SAAS,eACf,OAAQ,MAA6B,SAAS;AACrD;AAJS;AAOT,SAAS,gBAAgB,OAAwB;AAC/C,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO,OAAO,KAAK;AAC9D,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,UAAW,MAAgC;AACjD,QAAI,OAAO,YAAY,YAAY,YAAY,GAAI,QAAO;AAC1D,QAAI;AACF,aAAO,KAAK,UAAU,KAAK,KAAK,OAAO,KAAK;AAAA,IAC9C,QAAQ;AACN,aAAO,OAAO,KAAK;AAAA,IACrB;AAAA,EACF;AACA,SAAO,OAAO,KAAK;AACrB;AAbS;AAgBT,SAAS,eACP,OACA,UACW;AACX,SAAO,IAAI,UAAU;AAAA,IACnB,MAAM,MAAM;AAAA,IACZ,SAAS,MAAM;AAAA,IACf,UAAU,MAAM,YAAY,SAAS;AAAA,IACrC,OAAO,MAAM,SAAS,SAAS;AAAA,IAC/B,OAAO,MAAM,UAAU,iBAAiB,QAAQ,QAAQ;AAAA,IACxD,KAAK,MAAM,OAAO,SAAS;AAAA,IAC3B,UAAU,MAAM;AAAA,IAChB,OAAO,MAAM;AAAA,IACb,SAAS,MAAM;AAAA,IACf,MAAM,MAAM;AAAA,IACZ,WAAW,MAAM;AAAA,EACnB,CAAC;AACH;AAjBS;AAoBF,SAAS,mBAAmB,OAAgB,WAA8B,CAAC,GAAc;AAE9F,MAAI,iBAAiB,UAAW,QAAO;AAGvC,MAAI,mBAAmB,KAAK,EAAG,QAAO,eAAe,OAAO,QAAQ;AAEpE,MAAI,iBAAiB,OAAO;AAG1B,UAAM,WAAW;AAKjB,UAAM,OAAO,SAAS,SAAS,OAAO,SAAS,aAAa,WAAW,SAAS,WAAW;AAC3F,QAAI,MAAM;AACR,0BAAoB,OAAO,QAAQ,IAAI;AAMvC,UAAK,MAA6B,SAAS,MAAM;AAC/C,eAAO,IAAI,UAAU;AAAA,UACnB;AAAA,UACA,SAAS,MAAM;AAAA,UACf,UAAU,SAAS,YAAY;AAAA,UAC/B,OAAO,SAAS,SAAS;AAAA,UACzB,OAAO;AAAA,UACP,KAAK,SAAS,QAAQ,OAAO,SAAS,aAAa,WAAW,SAAS,WAAW;AAAA,QACpF,CAAC;AAAA,MACH;AAAA,IACF;AACA,wBAAoB,OAAO,YAAY,SAAS,YAAY,OAAO;AACnE,wBAAoB,OAAO,SAAS,SAAS,SAAS,SAAS;AAC/D,UAAM,MAAM,SAAS,QAAQ,OAAO,SAAS,aAAa,WAAW,SAAS,WAAW;AACzF,QAAI,IAAK,qBAAoB,OAAO,OAAO,GAAG;AAC9C,UAAM,SAAS,SAAS;AACxB,QAAI,UAAU,OAAO,WAAW,YAC3B,OAAQ,OAA8B,SAAS,YAC/C,OAAQ,OAA8B,SAAS,YAC/C,OAAQ,OAAgC,WAAW,UAAU;AAChE,0BAAoB,OAAO,YAAY,MAAM;AAAA,IAC/C;AACA,WAAO;AAAA,EACT;AAGA,MAAI,YAAY,KAAK,EAAG,QAAO,eAAe,OAA2B,QAAQ;AAEjF,SAAO,IAAI,UAAU;AAAA,IACnB,MAAM,SAAS,QAAQ;AAAA,IACvB,SAAS,gBAAgB,KAAK;AAAA,IAC9B,UAAU,SAAS,YAAY;AAAA,IAC/B,OAAO,SAAS,SAAS;AAAA,IACzB,OAAO;AAAA,IACP,KAAK,SAAS;AAAA,EAChB,CAAC;AACH;AA3DgB;AA6DhB,SAAS,oBAAoB,QAAe,KAAa,OAAsB;AAC7E,MAAI,OAAO,OAAQ;AACnB,MAAI;AACF,WAAO,eAAe,QAAQ,KAAK,EAAE,cAAc,MAAM,YAAY,OAAO,OAAO,UAAU,KAAK,CAAC;AAAA,EACrG,QAAQ;AAAA,EAGR;AACF;AARS;AAgBF,SAAS,gBACd,OACA,UAAkC,CAAC,GAC3B;AACR,QAAM,QAAQ,mBAAmB,KAAK;AACtC,QAAM,OAAO,MAAM,QAAQ;AAC3B,QAAM,WAAW,MAAM,YAAY;AACnC,MAAI,QAAQ,gBAAgB,aAAc,QAAO,SAAS,IAAI,KAAK,MAAM,OAAO;AAEhF,QAAM,QAAQ,CAAC,SAAS,WAAW,QAAQ,CAAC,KAAK,MAAM,OAAO,IAAI,SAAS,IAAI,EAAE;AACjF,MAAI,MAAM,SAAU,OAAM,KAAK,aAAa,MAAM,SAAS,IAAI,IAAI,MAAM,SAAS,IAAI,IAAI,MAAM,SAAS,MAAM,EAAE;AACjH,MAAI,MAAM,UAAW,OAAM,KAAK,IAAI,MAAM,SAAS;AACnD,MAAI,MAAM,UAAU,OAAW,OAAM,KAAK,UAAU,YAAY,MAAM,KAAK,CAAC,EAAE;AAC9E,MAAI,MAAM,OAAO,OAAQ,OAAM,KAAK,IAAI,UAAU,MAAM,MAAM,KAAK,UAAK,CAAC,EAAE;AAC3E,MAAI,MAAM,IAAK,OAAM,KAAK,IAAI,QAAQ,MAAM,GAAG,EAAE;AACjD,MAAI,MAAM,QAAS,OAAM,KAAK,IAAI;AAAA,EAAa,MAAM,OAAO,EAAE;AAC9D,MAAI,MAAM,KAAM,OAAM,KAAK,SAAS,MAAM,IAAI,EAAE;AAChD,MAAI,QAAQ,gBAAgB,MAAM,MAAO,OAAM,KAAK,IAAI,MAAM,KAAK;AACnE,SAAO,MAAM,KAAK,IAAI;AACxB;AAnBgB;AAqBhB,SAAS,YAAY,OAAwB;AAC3C,SAAO,iBAAiB,QAAQ,GAAG,MAAM,IAAI,KAAK,MAAM,OAAO,KAAK,OAAO,KAAK;AAClF;AAFS;AAIT,SAAS,WAAW,OAAuB;AACzC,SAAO,MAAM,MAAM,GAAG,CAAC,EAAE,YAAY,IAAI,MAAM,MAAM,CAAC;AACxD;AAFS;","names":[]}
|
package/dist/fragment.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/fragment.ts","../src/ops.ts","../src/hmr.ts","../src/ref.ts"],"sourcesContent":["import { getCurrentOwner } from '@vobs/reactivity'\nimport { createComment, getRenderer } from './ops'\n\nexport interface VobsFragment {\n readonly kind: 'vobs-fragment'\n readonly start: Node\n readonly end: Node\n readonly mount: (parent: Node, anchor: Node | null) => void\n readonly unmount: (parent: Node) => void\n}\n\nexport type VobsNode = Node | VobsFragment\nexport type FragmentFactory = (parent: Node, anchor: Node) => void\n\nexport function createFragment(factory: FragmentFactory): VobsFragment {\n const start = createComment('vobs:fragment:start')\n const end = createComment('vobs:fragment:end')\n let parent: Node | null = null\n let initialized = false\n const owner = getCurrentOwner()\n\n const fragment: VobsFragment = {\n kind: 'vobs-fragment',\n start,\n end,\n mount(nextParent, anchor): void {\n if (parent && parent !== nextParent) {\n throw new Error('Vobs Fragment: 不能跨父节点移动 Fragment')\n }\n\n if (initialized) {\n moveRange(nextParent, start, end, anchor)\n return\n }\n\n const renderer = getRenderer()\n renderer.insertBefore(nextParent, start, anchor)\n renderer.insertBefore(nextParent, end, anchor)\n parent = nextParent\n initialized = true\n if (owner) owner.run(() => factory(nextParent, end))\n else factory(nextParent, end)\n },\n unmount(nextParent): void {\n if (!initialized || parent !== nextParent) {\n throw new Error('Vobs Fragment: Fragment 不属于指定父节点')\n }\n const renderer = getRenderer()\n let current = renderer.nextSibling(start)\n while (current && current !== end) {\n const next = renderer.nextSibling(current)\n renderer.removeChild(nextParent, current)\n current = next\n }\n renderer.removeChild(nextParent, start)\n renderer.removeChild(nextParent, end)\n parent = null\n initialized = false\n }\n }\n return fragment\n}\n\nexport function isVobsFragment(value: unknown): value is VobsFragment {\n return Boolean(value) && typeof value === 'object' && (value as VobsFragment).kind === 'vobs-fragment'\n}\n\nfunction moveRange(parent: Node, start: Node, end: Node, anchor: Node | null): void {\n const renderer = getRenderer()\n const nodes: Node[] = [start]\n let current = renderer.nextSibling(start)\n while (current) {\n nodes.push(current)\n if (current === end) break\n current = renderer.nextSibling(current)\n }\n if (nodes[nodes.length - 1] !== end) {\n throw new Error('Vobs Fragment: 找不到结束锚点')\n }\n for (const node of nodes) renderer.insertBefore(parent, node, anchor)\n}\n","// 编译产物调用的基础操作\n\nimport { createOwner, getCurrentOwner, setOwnerDebugName, untrack, type Owner } from '@vobs/reactivity'\nimport { isVobsFragment, type VobsNode } from './fragment'\nimport { describeDebugNode, getRuntimeDebugHooks, invokeRuntimeDebug, readDebugValue } from './debug'\nimport {\n associateHmrInstance,\n markHmrInstanceMounted,\n registerHmrInstance,\n type HmrInstance\n} from './hmr'\nimport type { VobsRenderer } from './renderer'\nimport { setRef } from './ref'\n\ntype RuntimeRenderer = VobsRenderer<Node, Text, Element, Comment>\n\nlet currentRenderer: RuntimeRenderer | null = null\nconst nodeOwners = new WeakMap<object, Owner>()\ninterface EventBinding {\n readonly handler: EventListener\n readonly owner: Owner | null\n readonly original: EventListener\n}\nconst eventBindings = new WeakMap<object, Map<string, EventBinding>>()\n\nexport function setRenderer<\n NodeType,\n TextNode extends NodeType,\n ElementNode extends NodeType,\n CommentNode extends NodeType\n>(renderer: VobsRenderer<NodeType, TextNode, ElementNode, CommentNode>): void {\n // 编译产物仍使用 DOM 节点声明;实际宿主类型由应用提供的渲染器决定。\n currentRenderer = renderer as unknown as RuntimeRenderer\n}\n\nexport function getRenderer(): RuntimeRenderer {\n if (!currentRenderer) {\n throw new Error('渲染器未初始化')\n }\n return currentRenderer\n}\n\nexport function createText(content: string): Text {\n return getRenderer().createText(content)\n}\n\n/** SVG 命名空间(与 HTML 解析器 foreign-content 规则一致) */\nexport const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'\n\n/**\n * SVG 标签全集(= TypeScript SVGElementTagNameMap 的键,运行时分发依据)。\n * 与 React 的运行时清单同思路:只含纯 SVG 标签,刻意排除与 HTML 同名的\n * a/script/style/title(它们在 JSX 类型里归 HTMLElement,按 HTML 创建)。\n */\nconst SVG_TAGS = new Set([\n 'animate', 'animateMotion', 'animateTransform', 'circle', 'clipPath', 'defs', 'desc',\n 'ellipse', 'feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite',\n 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight',\n 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR',\n 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset',\n 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence',\n 'filter', 'foreignObject', 'g', 'image', 'line', 'linearGradient', 'marker', 'mask',\n 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialGradient',\n 'rect', 'set', 'stop', 'svg', 'switch', 'symbol', 'text', 'textPath', 'tspan',\n 'use', 'view'\n])\n\nexport function createElement(tag: string): Element {\n // SVG 标签走命名空间创建:document.createElement('rect') 产物是 HTMLUnknownElement,\n // 整棵 SVG 子树都不会渲染(无报错)。渲染器未实现 createSvgElement 时按 createElement\n // 兜底(旧自定义渲染器保持原行为)。\n if (SVG_TAGS.has(tag)) {\n const renderer = getRenderer()\n if (renderer.createSvgElement) return renderer.createSvgElement(tag)\n }\n return getRenderer().createElement(tag)\n}\n\nexport function createComment(content: string): Comment {\n return getRenderer().createComment(content)\n}\n\nexport function insertBefore(\n parent: Node,\n child: VobsNode,\n anchor: VobsNode | null\n): void {\n if (isVobsFragment(child)) {\n child.mount(parent, isVobsFragment(anchor) ? anchor.start : anchor)\n markHmrInstanceMounted(child, parent)\n return\n }\n getRenderer().insertBefore(parent, child, isVobsFragment(anchor) ? anchor.start : anchor)\n markHmrInstanceMounted(child, parent)\n const nodeName = (child as Element).nodeName\n if (nodeName === 'OPTION' || nodeName === 'OPTGROUP') syncSelectValue(parent)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'insert',\n target: describeDebugNode(child),\n parent: describeDebugNode(parent)\n })\n }\n}\n\n/**\n * Select initial-value auto resync: `bindProperty(node, 'value', …)` registers the value\n * reader for `<select>` elements; whenever an `<option>` is inserted into the select\n * (directly or through an `<optgroup>`), the current bound value is re-applied. This\n * removes the last reason for apps to keep `ref + queueMicrotask` value-sync workarounds\n * for options that arrive after the value binding (e.g. asynchronously loaded lists).\n */\nconst selectValueReaders = new WeakMap<object, () => unknown>()\n\nexport function registerSelectValueBinding(select: Element, read: () => unknown): void {\n selectValueReaders.set(select, read)\n}\n\nfunction syncSelectValue(parent: Node): void {\n let current: Node | null = parent\n while (current !== null) {\n if ((current as Element).nodeName === 'SELECT') {\n const read = selectValueReaders.get(current)\n if (read !== undefined) setProperty(current as Element, 'value', read())\n return\n }\n current = current.parentNode\n }\n}\n\nexport function removeChild(\n parent: Node,\n child: VobsNode\n): void {\n disposeNodeOwner(child)\n if (isVobsFragment(child)) {\n child.unmount(parent)\n return\n }\n getRenderer().removeChild(parent, child)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'remove',\n target: describeDebugNode(child),\n parent: describeDebugNode(parent)\n })\n }\n}\n\nexport function setTextContent(\n node: Text,\n content: string\n): void {\n const previousValue = getRuntimeDebugHooks()\n ? readDebugValue(() => node.textContent)\n : undefined\n getRenderer().setTextContent(node, content)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'text',\n target: describeDebugNode(node),\n previousValue,\n nextValue: content\n })\n }\n}\n\nexport function setProperty(\n node: Element,\n key: string,\n value: unknown\n): void {\n const previousValue = getRuntimeDebugHooks()\n ? readDebugValue(() => Reflect.get(node, key))\n : undefined\n getRenderer().setProperty(node, key, value)\n if (key === 'value' && (node as { tagName?: unknown }).tagName === 'SELECT') {\n scheduleSelectValueSync(node, value)\n }\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'property',\n target: describeDebugNode(node),\n key,\n previousValue,\n nextValue: value\n })\n }\n}\n\nconst pendingSelectValues = new WeakMap<Element, unknown>()\nconst selectSyncScheduled = new WeakSet<Element>()\n\n/**\n * `<select value>` 在 option 子节点存在前赋值不生效(HTML 规范:select 的 value\n * 由已存在的 option 决定)。编译产物先设置属性、后插入子节点,静态写法必然丢初始值\n * (消费方此前只能用 ref + queueMicrotask 规避)。\n * 这里对 select 的 value 赋值统一延迟到微任务重放一次:静态子节点在同一同步任务内\n * 插入完毕,重放即命中。动态(insertList/insertDynamic)插入的 option 晚于该微任务时,\n * 由 value 的绑定 effect 在后续信号更新中正常覆盖。\n */\nfunction scheduleSelectValueSync(node: Element, value: unknown): void {\n pendingSelectValues.set(node, value)\n if (selectSyncScheduled.has(node)) return\n selectSyncScheduled.add(node)\n queueMicrotask(() => {\n selectSyncScheduled.delete(node)\n if (!pendingSelectValues.has(node)) return\n const pending = pendingSelectValues.get(node)\n pendingSelectValues.delete(node)\n getRenderer().setProperty(node, 'value', pending)\n })\n}\n\nexport function setAttribute(\n node: Element,\n key: string,\n value: string\n): void {\n const previousValue = getRuntimeDebugHooks()\n ? readDebugValue(() => typeof node.getAttribute === 'function' ? node.getAttribute(key) : undefined)\n : undefined\n getRenderer().setAttribute(node, key, value)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'attribute',\n target: describeDebugNode(node),\n key,\n previousValue,\n nextValue: value\n })\n }\n}\n\nexport function spreadProps(node: Element, props: Record<string, unknown>): void {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'key' || value === null || value === undefined) continue\n if (key === 'ref') {\n setRef(node, value)\n continue\n }\n if (key.startsWith('on') && typeof value === 'function') addEventListener(node, key.slice(2).toLowerCase(), value as EventListener)\n // property 键的 false 有语义(如 disabled={false} 必须清除),不能跳过;\n // attribute 键的 false 表示“不设置”,与 HTML 语义一致。\n else if (isPropertyKey(key)) setProperty(node, key, value)\n else if (value === false) continue\n else setAttribute(node, domAttributeName(key), key === 'style' && isStyleObject(value) ? formatStyle(value) : String(value))\n }\n}\n\n/** Apply compile-time host properties in one renderer pass. */\nexport function setStaticProps(node: Element, props: Record<string, unknown>): void {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'key' || key === 'ref' || key.startsWith('on')) continue\n if (value === null || value === undefined) continue\n if (isPropertyKey(key)) setProperty(node, key, value)\n else if (value === false) continue\n else setAttribute(node, domAttributeName(key), key === 'style' && isStyleObject(value) ? formatStyle(value) : String(value))\n }\n}\n\nfunction isPropertyKey(key: string): boolean {\n return key === 'value' || key === 'checked' || key === 'selected' || key === 'disabled'\n || key === 'multiple' || key === 'readOnly' || key === 'required'\n || key === 'autofocus' || key === 'hidden' || key === 'tabIndex'\n || key === 'colSpan' || key === 'rowSpan'\n}\n\n/** camelCase JSX 属性名 → HTML attribute 名(与 compiler 的 domAttributeName 保持一致) */\nfunction domAttributeName(key: string): string {\n switch (key) {\n case 'className': return 'class'\n case 'htmlFor': return 'for'\n case 'autoComplete': return 'autocomplete'\n case 'spellCheck': return 'spellcheck'\n default: return key\n }\n}\n\nfunction isStyleObject(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction formatStyle(value: Record<string, unknown>): string {\n return Object.entries(value).filter(([, entry]) => entry !== null && entry !== undefined && entry !== false)\n .map(([key, entry]) => `${key.replace(/[A-Z]/gu, match => `-${match.toLowerCase()}`)}:${String(entry)}`).join(';')\n}\n\nexport function addEventListener(\n node: Element,\n event: string,\n handler: EventListener\n): void {\n const renderer = getRenderer()\n const owner = getCurrentOwner()\n let bindings = eventBindings.get(node)\n if (!bindings) {\n bindings = new Map()\n eventBindings.set(node, bindings)\n }\n const previous = bindings.get(event)\n if (previous && previous.original === handler && previous.owner === owner) return\n if (previous) renderer.removeEventListener(node, event, previous.handler)\n const listener = owner ? (reason: Event) => {\n // Owner 已销毁说明节点所属子树已被卸载/替换,事件来自游离 DOM,直接忽略。\n // 否则 owner.run 会抛\"已销毁的 Owner\",在事件流里制造无意义的错误噪音。\n if (owner.disposed) return\n try {\n owner.run(() => handler(reason))\n } catch (error) {\n const handled = owner.handleError(error)\n invokeRuntimeDebug('error', {\n error,\n owner,\n phase: 'event',\n handled,\n recovery: handled ? 'handled' : 'propagated'\n })\n if (!handled) throw error\n }\n } : handler\n const binding: EventBinding = { handler: listener, owner, original: handler }\n bindings.set(event, binding)\n renderer.addEventListener(node, event, listener)\n owner?.onDispose(() => {\n if (bindings?.get(event) !== binding) return\n bindings.delete(event)\n renderer.removeEventListener(node, event, listener)\n })\n}\n\nexport function removeEventListener(\n node: Element,\n event: string,\n handler: EventListener\n): void {\n const renderer = getRenderer()\n const binding = eventBindings.get(node)?.get(event)\n renderer.removeEventListener(node, event, binding?.handler ?? handler)\n eventBindings.get(node)?.delete(event)\n}\n\nexport function clear(container: Node): void {\n getRenderer().clear(container)\n}\n\ntype VobsComponent = (...args: any[]) => VobsNode\n\ntype ComponentProps<Component extends VobsComponent> = Component extends (\n props: infer Props\n) => VobsNode\n ? NonNullable<Props> extends object ? NonNullable<Props> : Record<string, never>\n : Record<string, never>\n\nexport function createComponent<Component extends VobsComponent>(\n component: Component,\n props: ComponentProps<Component>,\n source?: VobsSourceLocation\n): VobsNode {\n const owner = createOwner()\n const componentName = (component as typeof component & { displayName?: string }).displayName\n || component.name\n || 'anonymous'\n setOwnerDebugName(owner, source\n ? `${componentName} (${source.file}:${source.line}:${source.column})`\n : componentName)\n owner.onError(reason => {\n attachSourceLocation(reason, source)\n attachComponentContext(reason, componentName, owner.id)\n throw reason\n })\n // HMR 注册先于渲染作用域标记:注册清理必须跨热更新保活,不能被\n // disposeSince 当作上一轮渲染的清理释放掉。\n const hmrKey = (component as typeof component & { hmrKey?: string }).hmrKey\n let instance: HmrInstance | null = null\n if (hmrKey) {\n const separator = hmrKey.lastIndexOf(':')\n const moduleId = separator < 0 ? hmrKey : hmrKey.slice(0, separator)\n instance = { node: null as unknown as VobsNode, parent: null, refresh: () => refreshInstance() }\n const cleanup = registerHmrInstance(moduleId, instance)\n owner.onDispose(cleanup)\n }\n // 渲染作用域:组件 Owner 只承载 HMR 注册与错误处理,每轮渲染注册的 effect、\n // onDispose(含 portal 清理)与嵌套组件 Owner 都归属该轮作用域。HMR refresh\n // 重渲染前释放上一轮作用域,旧实例的 effect 不再订阅信号、portal 节点不再\n // 残留在 body 中每轮热更新叠加;组件 Owner 与 HMR 注册保活。\n const renderScope = owner.mark()\n let node: VobsNode\n try {\n // 组件渲染必须 untrack:组件是 run-once 的,其渲染发生在某次 effect 求值\n // (insertDynamic/insertBoundary 的渲染工厂、路由挂载)内时,若不切断追踪,\n // 组件体内读取的信号会被收集为祖先 effect 的依赖——一次无关编辑就会触发\n // 整棵子树销毁重建(输入框被换掉、焦点丢失、事件监听随旧树一起被清理)。\n // 结构性响应只属于条件工厂与绑定 effect,组件本体渲染一次即止。\n node = owner.run(() => untrack(() => component(props)))\n } catch (error) {\n owner.dispose()\n attachSourceLocation(error, source)\n attachComponentContext(error, componentName, owner.id)\n throw error\n }\n associateNodeOwner(node, owner)\n if (instance) {\n instance.node = node\n associateHmrInstance(node, instance)\n }\n function refreshInstance(): void {\n // 组件已随旧渲染树一起卸载(如父级在同一次热更新中先完成刷新),\n // 无法也无需再刷新。\n if (owner.disposed) return\n const previous = instance!.node\n owner.disposeSince(renderScope)\n const next = owner.run(() => untrack(() => component(props)))\n const parent = instance!.parent\n if (parent) {\n // 先插入新树再卸载旧树:替换锚点始终取自仍在文档中的旧树,\n // fragment 与普通节点两种形态任意组合都能正确定位插入点。\n const anchor = isVobsFragment(previous) ? previous.start : previous\n if (isVobsFragment(next)) next.mount(parent, anchor)\n else getRenderer().insertBefore(parent, next, anchor)\n if (isVobsFragment(previous)) previous.unmount(parent)\n else getRenderer().removeChild(parent, previous)\n }\n nodeOwners.delete(previous as object)\n nodeOwners.set(next as object, owner)\n associateHmrInstance(next, instance!)\n instance!.node = next\n }\n return node\n}\n\nexport interface VobsSourceLocation {\n readonly file: string\n readonly line: number\n readonly column: number\n}\n\nexport interface VobsLocatedError extends Error {\n readonly vobsSource?: VobsSourceLocation\n readonly vobsComponent?: string\n readonly vobsOwnerId?: string\n}\n\nfunction attachSourceLocation(reason: unknown, source: VobsSourceLocation | undefined): void {\n if (!source || (!reason || (typeof reason !== 'object' && typeof reason !== 'function'))) return\n const error = reason as VobsLocatedError\n if (error.vobsSource) return\n try {\n Object.defineProperty(error, 'vobsSource', {\n configurable: true,\n enumerable: false,\n value: source,\n writable: false\n })\n } catch {\n // Frozen third-party errors still propagate with their original details.\n }\n}\n\nfunction attachComponentContext(reason: unknown, component: string, ownerId: string): void {\n if (!reason || (typeof reason !== 'object' && typeof reason !== 'function')) return\n const error = reason as VobsLocatedError\n try {\n if (!error.vobsComponent) Object.defineProperty(error, 'vobsComponent', { configurable: true, enumerable: false, value: component, writable: false })\n if (!error.vobsOwnerId) Object.defineProperty(error, 'vobsOwnerId', { configurable: true, enumerable: false, value: ownerId, writable: false })\n } catch {\n // Frozen third-party errors still propagate with their original details.\n }\n}\n\nexport function createBlock(factory: () => VobsNode | null | undefined | false): VobsNode | null {\n const owner = createOwner()\n setOwnerDebugName(owner, 'dynamic')\n let node: VobsNode | null | undefined | false\n try {\n node = owner.run(factory)\n } catch (error) {\n owner.dispose()\n throw error\n }\n if (!node) {\n owner.dispose()\n return null\n }\n associateNodeOwner(node, owner)\n return node\n}\n\nexport function associateNodeOwner(node: VobsNode, owner: Owner): void {\n nodeOwners.set(node, owner)\n}\n\nexport function disposeNodeOwner(node: VobsNode): void {\n const owner = nodeOwners.get(node)\n if (!owner) return\n nodeOwners.delete(node)\n owner.dispose()\n}\n","import type { VobsNode } from './fragment'\n\nexport type HmrComponent<Props extends object = Record<string, unknown>> =\n (props: Props) => VobsNode\n\nexport interface HmrStateStore {\n get<T>(key: string, initial: T | (() => T)): T\n set<T>(key: string, value: T): void\n has(key: string): boolean\n delete(key: string): void\n clear(): void\n}\n\nexport interface HmrInstance {\n node: VobsNode\n parent: Node | null\n refresh(): void\n}\n\ninterface HmrModuleState {\n readonly components: Map<string, HmrComponent>\n readonly state: Map<string, unknown>\n readonly instances: Set<HmrInstance>\n}\n\ninterface HmrGlobal {\n modules: Map<string, HmrModuleState>\n /** 编译器生成的 hmrStateRef 注册表:键为 `${moduleId}#${声明名}`,值跨模块重执行保活。 */\n states: Map<string, unknown>\n}\n\nconst globalTarget = globalThis as typeof globalThis & { __VOBS_HMR__?: HmrGlobal }\nconst hmrGlobal = globalTarget.__VOBS_HMR__ ?? { modules: new Map<string, HmrModuleState>(), states: new Map<string, unknown>() }\nglobalTarget.__VOBS_HMR__ = hmrGlobal\n\nexport function resolveComponent<Props extends object>(\n component: HmrComponent<Props>,\n moduleId: string,\n exportName: string\n): HmrComponent<Props> {\n const module = getModule(moduleId)\n const existing = module.components.get(exportName)\n if (existing) return existing as HmrComponent<Props>\n\n const proxy = ((props: Props) => {\n const current = (proxy as HmrComponent<Props> & { current: HmrComponent<Props> }).current\n return current(props)\n }) as HmrComponent<Props> & { current: HmrComponent<Props> }\n proxy.current = component\n Object.defineProperties(proxy, {\n displayName: { configurable: true, value: component.name || exportName },\n hmrKey: { configurable: false, value: `${moduleId}:${exportName}` }\n })\n module.components.set(exportName, proxy as HmrComponent)\n return proxy\n}\n\nexport function updateHmrModule(moduleId: string, nextModule: Record<string, unknown>): void {\n const module = hmrGlobal.modules.get(moduleId)\n if (!module) return\n let changed = false\n for (const [name, proxy] of module.components) {\n const next = nextModule[name]\n if (typeof next !== 'function') continue\n const hmrProxy = proxy as HmrComponent & { current: HmrComponent; displayName?: string }\n hmrProxy.current = next as HmrComponent\n Object.defineProperty(hmrProxy, 'displayName', { configurable: true, value: next.name || name })\n changed = true\n }\n if (!changed) return\n // 快照后正序遍历:实例在自身渲染开始前注册,祖先必然先于后代入列——\n // 祖先先刷新会 dispose 上一轮渲染作用域,其树内旧后代实例的 refresh\n // 命中已销毁守卫被跳过,避免同一组件在一次热更新中重渲染两次。\n // 同时 refresh 重渲染以新代码创建的全新实例不在快照中,天然不会再刷新。\n const instances = [...module.instances]\n for (const instance of instances) {\n try {\n instance.refresh()\n } catch {\n // HMR failures remain application errors on the next normal render.\n }\n }\n}\n\nexport function disposeHmrModule(_moduleId: string): void {\n // State and component proxies intentionally survive module disposal.\n}\n\nexport function createHmrStateStore(moduleId: string): HmrStateStore {\n const state = getModule(moduleId).state\n return {\n get<T>(key: string, initial: T | (() => T)): T {\n if (!state.has(key)) state.set(key, typeof initial === 'function' ? (initial as () => T)() : initial)\n return state.get(key) as T\n },\n set<T>(key: string, value: T): void {\n state.set(key, value)\n },\n has: key => state.has(key),\n delete: key => { state.delete(key) },\n clear: () => { state.clear() }\n }\n}\n\n/**\n * 编译器为模块顶层 state() 声明生成的取值入口。键为 `${moduleId}#${声明名}`,\n * 与手动 store 键空间隔离。模块热更新重执行时复用既有信号实例:旧导入方持有的\n * 实例与新模块实例共享同一份状态,消除\"两份模块、两份状态\"导致的编辑不生效/页面半边失灵。\n */\nexport function hmrStateRef<T>(key: string, create: () => T): T {\n const states = hmrGlobal.states\n if (states.has(key)) return states.get(key) as T\n const value = create()\n states.set(key, value)\n return value\n}\n\nexport function registerHmrInstance(moduleId: string, instance: HmrInstance): () => void {\n const instances = getModule(moduleId).instances\n instances.add(instance)\n return () => instances.delete(instance)\n}\n\nexport function markHmrInstanceMounted(node: VobsNode, parent: Node): void {\n const instance = hmrInstances.get(node as object)\n if (instance) instance.parent = parent\n}\n\nfunction getModule(moduleId: string): HmrModuleState {\n let module = hmrGlobal.modules.get(moduleId)\n if (!module) {\n module = { components: new Map(), state: new Map(), instances: new Set() }\n hmrGlobal.modules.set(moduleId, module)\n }\n return module\n}\n\nconst hmrInstances = new WeakMap<object, HmrInstance>()\n\nexport function associateHmrInstance(node: VobsNode, instance: HmrInstance): void {\n hmrInstances.set(node as object, instance)\n}\n","import { getCurrentOwner } from '@vobs/reactivity'\n\n/** A mutable reference populated when a host node is mounted. */\nexport interface Ref<T extends object = Node> {\n current: T | null\n}\n\nexport type RefTarget<T extends object = Node> = Ref<T> | ((value: T | null) => void)\n\nexport function ref<T extends object = Node>(initialValue: T | null = null): Ref<T> {\n return { current: initialValue }\n}\n\n/** Bind a host node to an object or callback ref and clear it with its Owner. */\nexport function setRef<T extends object>(node: T, target: unknown): void {\n if (!isRefTarget<T>(target)) return\n // SSR's serializable nodes are never exposed as live refs. Custom renderers\n // may use arbitrary host objects, so only exclude the known SSR shape.\n if (isSSRNode(node)) return\n const owner = getCurrentOwner()\n assignRef(target, node)\n owner?.onDispose(() => {\n // Do not clear a ref that has since been reassigned to another node.\n if (isObjectRef(target) && target.current !== node) return\n assignRef(target, null)\n })\n}\n\nfunction isSSRNode(value: object): boolean {\n const type = (value as { type?: unknown }).type\n return (type === 'element' || type === 'text' || type === 'comment')\n && !('nodeType' in value)\n}\n\nfunction isObjectRef<T extends object>(value: unknown): value is Ref<T> {\n return Boolean(value && typeof value === 'object' && 'current' in value)\n}\n\nfunction isRefTarget<T extends object>(value: unknown): value is RefTarget<T> {\n return isObjectRef<T>(value) || typeof value === 'function'\n}\n\nfunction assignRef<T extends object>(target: RefTarget<T>, value: T | null): void {\n try {\n if (typeof target === 'function') target(value)\n else target.current = value\n } catch {\n // Ref callbacks are user code; never make mounting fail because of them.\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAAA,qBAAgC;;;ACEhC,IAAAC,qBAAqF;;;AC6BrF,IAAM,eAAe;AACrB,IAAM,YAAY,aAAa,gBAAgB,EAAE,SAAS,oBAAI,IAA4B,GAAG,QAAQ,oBAAI,IAAqB,EAAE;AAChI,aAAa,eAAe;;;ACjC5B,wBAAgC;;;AFgBhC,IAAI,kBAA0C;AAmBvC,SAAS,cAA+B;AAC7C,MAAI,CAAC,iBAAiB;AACpB,UAAM,IAAI,MAAM,4CAAS;AAAA,EAC3B;AACA,SAAO;AACT;AALgB;AA2CT,SAAS,cAAc,SAA0B;AACtD,SAAO,YAAY,EAAE,cAAc,OAAO;AAC5C;AAFgB;;;ADhET,SAAS,eAAe,SAAwC;AACrE,QAAM,QAAQ,cAAc,qBAAqB;AACjD,QAAM,MAAM,cAAc,mBAAmB;AAC7C,MAAI,SAAsB;AAC1B,MAAI,cAAc;AAClB,QAAM,YAAQ,oCAAgB;AAE9B,QAAM,WAAyB;AAAA,IAC7B,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,MAAM,YAAY,QAAc;AAC9B,UAAI,UAAU,WAAW,YAAY;AACnC,cAAM,IAAI,MAAM,0EAAkC;AAAA,MACpD;AAEA,UAAI,aAAa;AACf,kBAAU,YAAY,OAAO,KAAK,MAAM;AACxC;AAAA,MACF;AAEA,YAAM,WAAW,YAAY;AAC7B,eAAS,aAAa,YAAY,OAAO,MAAM;AAC/C,eAAS,aAAa,YAAY,KAAK,MAAM;AAC7C,eAAS;AACT,oBAAc;AACd,UAAI,MAAO,OAAM,IAAI,MAAM,QAAQ,YAAY,GAAG,CAAC;AAAA,UAC9C,SAAQ,YAAY,GAAG;AAAA,IAC9B;AAAA,IACA,QAAQ,YAAkB;AACxB,UAAI,CAAC,eAAe,WAAW,YAAY;AACzC,cAAM,IAAI,MAAM,0EAAkC;AAAA,MACpD;AACA,YAAM,WAAW,YAAY;AAC7B,UAAI,UAAU,SAAS,YAAY,KAAK;AACxC,aAAO,WAAW,YAAY,KAAK;AACjC,cAAM,OAAO,SAAS,YAAY,OAAO;AACzC,iBAAS,YAAY,YAAY,OAAO;AACxC,kBAAU;AAAA,MACZ;AACA,eAAS,YAAY,YAAY,KAAK;AACtC,eAAS,YAAY,YAAY,GAAG;AACpC,eAAS;AACT,oBAAc;AAAA,IAChB;AAAA,EACF;AACA,SAAO;AACT;AA/CgB;AAiDT,SAAS,eAAe,OAAuC;AACpE,SAAO,QAAQ,KAAK,KAAK,OAAO,UAAU,YAAa,MAAuB,SAAS;AACzF;AAFgB;AAIhB,SAAS,UAAU,QAAc,OAAa,KAAW,QAA2B;AAClF,QAAM,WAAW,YAAY;AAC7B,QAAM,QAAgB,CAAC,KAAK;AAC5B,MAAI,UAAU,SAAS,YAAY,KAAK;AACxC,SAAO,SAAS;AACd,UAAM,KAAK,OAAO;AAClB,QAAI,YAAY,IAAK;AACrB,cAAU,SAAS,YAAY,OAAO;AAAA,EACxC;AACA,MAAI,MAAM,MAAM,SAAS,CAAC,MAAM,KAAK;AACnC,UAAM,IAAI,MAAM,2DAAwB;AAAA,EAC1C;AACA,aAAW,QAAQ,MAAO,UAAS,aAAa,QAAQ,MAAM,MAAM;AACtE;AAbS;","names":["import_reactivity","import_reactivity"]}
|
|
1
|
+
{"version":3,"sources":["../src/fragment.ts","../src/ops.ts","../src/hmr.ts","../src/ref.ts"],"sourcesContent":["import { getCurrentOwner } from '@vobs/reactivity'\nimport { createComment, getRenderer } from './ops'\n\nexport interface VobsFragment {\n readonly kind: 'vobs-fragment'\n readonly start: Node\n readonly end: Node\n readonly mount: (parent: Node, anchor: Node | null) => void\n readonly unmount: (parent: Node) => void\n}\n\nexport type VobsNode = Node | VobsFragment\nexport type FragmentFactory = (parent: Node, anchor: Node) => void\n\nexport function createFragment(factory: FragmentFactory): VobsFragment {\n const start = createComment('vobs:fragment:start')\n const end = createComment('vobs:fragment:end')\n let parent: Node | null = null\n let initialized = false\n const owner = getCurrentOwner()\n\n const fragment: VobsFragment = {\n kind: 'vobs-fragment',\n start,\n end,\n mount(nextParent, anchor): void {\n if (parent && parent !== nextParent) {\n throw new Error('Vobs Fragment: 不能跨父节点移动 Fragment')\n }\n\n if (initialized) {\n moveRange(nextParent, start, end, anchor)\n return\n }\n\n const renderer = getRenderer()\n renderer.insertBefore(nextParent, start, anchor)\n renderer.insertBefore(nextParent, end, anchor)\n parent = nextParent\n initialized = true\n if (owner) owner.run(() => factory(nextParent, end))\n else factory(nextParent, end)\n },\n unmount(nextParent): void {\n if (!initialized || parent !== nextParent) {\n throw new Error('Vobs Fragment: Fragment 不属于指定父节点')\n }\n const renderer = getRenderer()\n let current = renderer.nextSibling(start)\n while (current && current !== end) {\n const next = renderer.nextSibling(current)\n renderer.removeChild(nextParent, current)\n current = next\n }\n renderer.removeChild(nextParent, start)\n renderer.removeChild(nextParent, end)\n parent = null\n initialized = false\n }\n }\n return fragment\n}\n\nexport function isVobsFragment(value: unknown): value is VobsFragment {\n return Boolean(value) && typeof value === 'object' && (value as VobsFragment).kind === 'vobs-fragment'\n}\n\nfunction moveRange(parent: Node, start: Node, end: Node, anchor: Node | null): void {\n const renderer = getRenderer()\n const nodes: Node[] = [start]\n let current = renderer.nextSibling(start)\n while (current) {\n nodes.push(current)\n if (current === end) break\n current = renderer.nextSibling(current)\n }\n if (nodes[nodes.length - 1] !== end) {\n throw new Error('Vobs Fragment: 找不到结束锚点')\n }\n for (const node of nodes) renderer.insertBefore(parent, node, anchor)\n}\n","// 编译产物调用的基础操作\n\nimport { createOwner, effect, getCurrentOwner, setOwnerDebugName, untrack, type Owner } from '@vobs/reactivity'\nimport { isVobsFragment, type VobsNode } from './fragment'\nimport { domAttributeName, isPropertyName } from './dom-props'\nimport { resolveEventName } from './dom-events'\nimport { isSvgTag } from './svg'\nimport { formatVobsError } from './error'\nimport { describeDebugNode, getRuntimeDebugHooks, invokeRuntimeDebug, readDebugValue } from './debug'\nimport {\n associateHmrInstance,\n markHmrInstanceMounted,\n registerHmrInstance,\n type HmrInstance\n} from './hmr'\nimport type { VobsRenderer } from './renderer'\nimport { setRef } from './ref'\n\ntype RuntimeRenderer = VobsRenderer<Node, Text, Element, Comment>\n\nlet currentRenderer: RuntimeRenderer | null = null\nconst nodeOwners = new WeakMap<object, Owner>()\ninterface EventBinding {\n readonly handler: EventListener\n readonly owner: Owner | null\n readonly original: EventListener\n}\nconst eventBindings = new WeakMap<object, Map<string, EventBinding>>()\n\n/*\n * 事件清理槽。目的:让**每个 (Owner, node, event) 只在 `Owner.cleanups` 里占一个槽**,\n * 重绑 handler 时只换槽里的间接引用,数组不增长(理由见 addEventListener)。\n *\n * 用 WeakMap<Owner, Map<key, handle>> 而不是往 Owner 上挂属性:\n * Owner 是 class 实例,挂新属性会把它推进字典模式(reactivity/src/owner.ts:30-50 记过这笔账)。\n * key 由 per-node 数字 id 与事件名拼成,避免每次重绑比较长字符串前缀。\n */\nconst eventCleanupSlots = new WeakMap<Owner, Map<string, EventBinding>>()\nconst eventCleanupNodeIds = new WeakMap<object, number>()\nlet nextEventCleanupNodeId = 0\n\nfunction eventCleanupKey(node: object, event: string): string {\n let id = eventCleanupNodeIds.get(node)\n if (id === undefined) {\n id = ++nextEventCleanupNodeId\n eventCleanupNodeIds.set(node, id)\n }\n return `${id}:${event}`\n}\n\nexport function setRenderer<\n NodeType,\n TextNode extends NodeType,\n ElementNode extends NodeType,\n CommentNode extends NodeType\n>(\n renderer: VobsRenderer<NodeType, TextNode, ElementNode, CommentNode> | undefined\n): VobsRenderer<NodeType, TextNode, ElementNode, CommentNode> | undefined {\n // 返回**上一份**渲染器(没有则是 undefined),调用方据此成对\"安装 / 还原\"。\n // 全局单例原来只装不还:一次 renderToString 之后就永久停在 SSR 渲染器上(见 vobs/src/app.ts)。\n const previous = currentRenderer as unknown as\n VobsRenderer<NodeType, TextNode, ElementNode, CommentNode> | undefined\n // 编译产物仍使用 DOM 节点声明;实际宿主类型由应用提供的渲染器决定。\n currentRenderer = (renderer as unknown as RuntimeRenderer) ?? null\n return previous\n}\n\nexport function getRenderer(): RuntimeRenderer {\n if (!currentRenderer) {\n throw new Error('渲染器未初始化')\n }\n return currentRenderer\n}\n\nexport function createText(content: string): Text {\n return getRenderer().createText(content)\n}\n\n/** SVG 命名空间与标签清单见 ./svg —— 编译期判定提升用的是同一份依据。 */\nexport { SVG_NAMESPACE } from './svg'\n\nexport function createElement(tag: string): Element {\n // SVG 标签走命名空间创建:document.createElement('rect') 产物是 HTMLUnknownElement,\n // 整棵 SVG 子树都不会渲染(无报错)。渲染器未实现 createSvgElement 时按 createElement\n // 兜底(旧自定义渲染器保持原行为)。\n if (isSvgTag(tag)) {\n const renderer = getRenderer()\n if (renderer.createSvgElement) return renderer.createSvgElement(tag)\n }\n return getRenderer().createElement(tag)\n}\n\n/**\n * 在 SVG 命名空间里建元素,**不看标签名**。\n *\n * 给编译器用在 `<svg>` 祖先下那些与 HTML 同名的标签(a / title / style / script)——\n * 它们不在 SVG_TAGS 里,`createElement` 会按 HTML 建。只按名字判定命名空间是行不通的:\n * `<svg><a href=\"…\">` 会变成 HTML 锚点,而且不报错。\n *\n * 渲染器没实现 createSvgElement 时按 createElement 兜底(自定义渲染器保持原行为)。\n */\nexport function createSvgElement(tag: string): Element {\n const renderer = getRenderer()\n if (renderer.createSvgElement) return renderer.createSvgElement(tag)\n return renderer.createElement(tag)\n}\n\nexport function createComment(content: string): Comment {\n return getRenderer().createComment(content)\n}\n\nexport function insertBefore(\n parent: Node,\n child: VobsNode,\n anchor: VobsNode | null\n): void {\n if (isVobsFragment(child)) {\n child.mount(parent, isVobsFragment(anchor) ? anchor.start : anchor)\n markHmrInstanceMounted(child, parent)\n return\n }\n getRenderer().insertBefore(parent, child, isVobsFragment(anchor) ? anchor.start : anchor)\n markHmrInstanceMounted(child, parent)\n const nodeName = (child as Element).nodeName\n if (nodeName === 'OPTION' || nodeName === 'OPTGROUP') syncSelectValue(parent)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'insert',\n target: describeDebugNode(child),\n parent: describeDebugNode(parent)\n })\n }\n}\n\n/**\n * Select initial-value auto resync: `bindProperty(node, 'value', …)` registers the value\n * reader for `<select>` elements; whenever an `<option>` is inserted into the select\n * (directly or through an `<optgroup>`), the current bound value is re-applied. This\n * removes the last reason for apps to keep `ref + queueMicrotask` value-sync workarounds\n * for options that arrive after the value binding (e.g. asynchronously loaded lists).\n */\nconst selectValueReaders = new WeakMap<object, () => unknown>()\n\nexport function registerSelectValueBinding(select: Element, read: () => unknown): void {\n selectValueReaders.set(select, read)\n}\n\nfunction syncSelectValue(parent: Node): void {\n let current: Node | null = parent\n while (current !== null) {\n if ((current as Element).nodeName === 'SELECT') {\n const read = selectValueReaders.get(current)\n /*\n * 读取必须 `untrack` —— 这里是在**插入 option 的过程中同步**重放当前值,\n * 而插入往往发生在某个 effect 运行时(`insertList` / `insertDynamic` 都由 effect 驱动)。\n * 若那次读取被算进外层 effect 的依赖,该 effect 以后会被这个信号唤醒:\n * 一个**隐藏订阅** —— 依赖关系图上多一条没人打算建立的边,\n * 表现为\"插入选项时恰好重跑了某个不相关的 effect\",很难追。\n *\n * 这条绑定的订阅由 `bindProperty` 自己的 effect 负责(bind.ts),\n * 这里的重放只是把值**再写一次**,不需要也不应该建立依赖。\n */\n if (read !== undefined) setProperty(current as Element, 'value', untrack(read))\n return\n }\n current = current.parentNode\n }\n}\n\nexport function removeChild(\n parent: Node,\n child: VobsNode\n): void {\n /*\n * **先摘 DOM,再释放 Owner。**\n *\n * 原来是反过来的:`disposeNodeOwner(child)` 抛错(用户 cleanup 里抛)就永远走不到\n * 摘除那一步 —— DOM 里留着旧节点、它的 effect 却已经没了,屏幕上出现两棵树且没有提示。\n * 顺序反过来之后,即使清理抛错,DOM 也已一致;错误照旧抛给调用方。\n */\n if (isVobsFragment(child)) {\n child.unmount(parent)\n disposeNodeOwner(child)\n return\n }\n getRenderer().removeChild(parent, child)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'remove',\n target: describeDebugNode(child),\n parent: describeDebugNode(parent)\n })\n }\n disposeNodeOwner(child)\n}\n\nexport function setTextContent(\n node: Text,\n content: string\n): void {\n const previousValue = getRuntimeDebugHooks()\n ? readDebugValue(() => node.textContent)\n : undefined\n getRenderer().setTextContent(node, content)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'text',\n target: describeDebugNode(node),\n previousValue,\n nextValue: content\n })\n }\n}\n\nexport function setProperty(\n node: Element,\n key: string,\n value: unknown\n): void {\n const previousValue = getRuntimeDebugHooks()\n ? readDebugValue(() => Reflect.get(node, key))\n : undefined\n getRenderer().setProperty(node, PROPERTY_IDL_NAMES[key] ?? key, value)\n if (key === 'value' && (node as { tagName?: unknown }).tagName === 'SELECT') {\n scheduleSelectValueSync(node, value)\n }\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'property',\n target: describeDebugNode(node),\n key,\n previousValue,\n nextValue: value\n })\n }\n}\n\nconst pendingSelectValues = new WeakMap<Element, unknown>()\nconst selectSyncScheduled = new WeakSet<Element>()\n\n/**\n * `<select value>` 在 option 子节点存在前赋值不生效(HTML 规范:select 的 value\n * 由已存在的 option 决定)。编译产物先设置属性、后插入子节点,静态写法必然丢初始值\n * (消费方此前只能用 ref + queueMicrotask 规避)。\n * 这里对 select 的 value 赋值统一延迟到微任务重放一次:静态子节点在同一同步任务内\n * 插入完毕,重放即命中。动态(insertList/insertDynamic)插入的 option 晚于该微任务时,\n * 由 value 的绑定 effect 在后续信号更新中正常覆盖。\n */\nfunction scheduleSelectValueSync(node: Element, value: unknown): void {\n pendingSelectValues.set(node, value)\n if (selectSyncScheduled.has(node)) return\n selectSyncScheduled.add(node)\n /*\n * 在这一刻就把渲染器抓下来,**不要**在微任务里再 getRenderer():\n * 1) 应用销毁会把全局渲染器还原(没装过就是\"没有\"),那时微任务里的 getRenderer() 会抛\n * \"渲染器未初始化\" —— 抛在微任务里没人接,直接变成进程级 unhandled error(实测\n * `packages/ui/src/bind.test.ts` 就因为这条让 vitest 报 1 个 unhandled error,退出码非 0);\n * 2) 语义上也更对:节点是哪个渲染器造的,就用哪个渲染器写回(期间全局可能已经换人)。\n */\n const renderer = getRenderer()\n queueMicrotask(() => {\n selectSyncScheduled.delete(node)\n if (!pendingSelectValues.has(node)) return\n const pending = pendingSelectValues.get(node)\n pendingSelectValues.delete(node)\n renderer.setProperty(node, 'value', pending)\n })\n}\n\nexport function setAttribute(\n node: Element,\n key: string,\n value: string\n): void {\n const previousValue = getRuntimeDebugHooks()\n ? readDebugValue(() => typeof node.getAttribute === 'function' ? node.getAttribute(key) : undefined)\n : undefined\n getRenderer().setAttribute(node, key, value)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'attribute',\n target: describeDebugNode(node),\n key,\n previousValue,\n nextValue: value\n })\n }\n}\n\nexport function spreadProps(node: Element, props: Record<string, unknown>): void {\n applySpreadProps(node, null, props)\n}\n\n/** 删除一个 attribute。渲染器没实现时退回空串(属性仍在,但内容清空)—— 老自定义渲染器的降级行为。 */\nexport function removeAttribute(node: Element, key: string): void {\n const renderer = getRenderer()\n if (renderer.removeAttribute) {\n renderer.removeAttribute(node, key)\n return\n }\n renderer.setAttribute(node, key, '')\n}\n\n/** 布尔型 property 的「清除」值是 false,其余是空串(与 React 的移除语义一致)。 */\nconst BOOLEAN_PROPERTIES = new Set([\n 'checked', 'selected', 'disabled', 'multiple', 'readOnly', 'required',\n 'hidden', 'open', 'indeterminate', 'defaultChecked', 'muted', 'autoFocus'\n])\n\n/**\n * JSX 属性名 → **IDL 属性名**(`el[name] = value` 里的那个 name)。\n *\n * 多数名字两边相同,但少数不同;写错名字的后果是**静默的**:\n * `Reflect.set(el, 'autoFocus', …)` 只是给元素挂了一个不反射的 expando,\n * 既不产生 `autofocus` 属性、也不改变 IDL 属性 —— 于是 `autoFocus={false}` 毫无作用。\n * 实测(jsdom,与真实 DOM 同语义):\n * IDL 名 `autofocus`(全小写)→ has=true/idl=true,写 false 则 has=false/idl=false ← 反射正确\n * JSX 名 `autoFocus`(驼峰) → 只挂 expando,属性与 IDL 都不变\n * 这也解释了为什么 `disabled` / `checked` / `readOnly` / `tabIndex` 一直是对的:\n * 它们的 IDL 名恰好与 JSX 名相同。\n */\nconst PROPERTY_IDL_NAMES: Readonly<Record<string, string>> = {\n autoFocus: 'autofocus'\n}\n\n/**\n * 把一份 props 应用(或**增量**应用到)节点上。\n *\n * `previous` 为 null 表示首次应用(`spreadProps` 的老路径);否则只处理真正变化的键,\n * 并**移除**新对象里已经消失的键 —— 这正是原来缺的那一半:`{...props}` 只在创建时应用一次,\n * 之后对象里删掉的键会永远留在 DOM 上,而且改了的值也不会生效。\n */\nfunction applySpreadProps(\n node: Element,\n previous: Record<string, unknown> | null,\n next: Record<string, unknown>\n): void {\n for (const [key, value] of Object.entries(next)) {\n if (key === 'key') continue\n if (previous !== null && Object.is(previous[key], value)) continue\n\n if (key.startsWith('on')) {\n // 事件:先摘旧监听(值变了、或新值不再是函数),再挂新的。\n // 事件名必须过别名表:`onDoubleClick` 的真实事件名是 `dblclick`,\n // 直接小写会得到不存在的 `\"doubleclick\"`(回调永不触发且静默)。\n const eventName = resolveEventName(key) ?? key.slice(2).toLowerCase()\n const previousHandler = previous?.[key]\n if (typeof previousHandler === 'function') {\n removeEventListener(node, eventName, previousHandler as EventListener)\n }\n if (typeof value === 'function') {\n addEventListener(node, eventName, value as EventListener)\n }\n continue\n }\n if (key === 'ref') {\n setRef(node, value)\n continue\n }\n // null/undefined 表示\"不提供值\",**保持现状**(props.test.ts 把这条钉成了契约)。\n // 注意它有一个已知后果:键仍在 `next` 里时移除循环够不着(`key in next`),\n // 所以\"值从中变成 null\"不会清掉已有属性 —— 要清就得把键从对象里删掉。\n if (value === null || value === undefined) continue\n // property 键的 false 有语义(如 disabled={false} 必须清除),不能跳过;\n // attribute 键的 false 表示\"不设置\",与 HTML 语义一致。\n else if (isClassListKey(key)) applyClassList(node, value)\n else if (isPropertyName(key)) setProperty(node, key, value)\n else if (value === false) continue\n else setAttribute(node, domAttributeName(key), key === 'style' && isStyleObject(value) ? formatStyle(value) : String(value))\n }\n\n if (previous === null) return\n for (const key of Object.keys(previous)) {\n if (key in next || key === 'key') continue\n if (key.startsWith('on')) {\n const eventName = resolveEventName(key) ?? key.slice(2).toLowerCase()\n const previousHandler = previous[key]\n if (typeof previousHandler === 'function') {\n removeEventListener(node, eventName, previousHandler as EventListener)\n }\n continue\n }\n if (key === 'ref') continue\n if (isPropertyName(key)) setProperty(node, key, BOOLEAN_PROPERTIES.has(key) ? false : '')\n else removeAttribute(node, domAttributeName(key))\n }\n}\n\n/**\n * 把 `{...props}` 绑定成**响应式**的:对象变了就增量应用,消失的键会被移除。\n *\n * 编译器对带展开的 JSX 属性发射这个而不是 `spreadProps` —— 后者只在创建时应用一次,\n * 于是「改了 props 不生效」「删掉的键留在 DOM 上」两个问题都**不报错**。\n *\n * ⚠️ `previous` 必须是**值的快照**,不能是 `next` 的引用。\n * 这里存引用时,下一轮 `applySpreadProps` 里的 `Object.is(previous[key], value)`\n * 读的是同一份值 → 恒等 → **整个 diff 短路**,移除循环也因 `key in next` 全跳过。\n * 而编译器为 `<div {...props}>` 发射的正是**身份稳定 + getter**的形状\n * (`bindSpreadProps(_el0, () => props)`,值经 getter 反应式求值),\n * 于是真实产物下\"改了 props 什么都不更新\"。\n *\n * 快照用浅拷贝即可,且**必须在应用之前取**:拷贝会把 getter 求值一次、转成数据属性,\n * 而 `applySpreadProps` 又会把 `next` 的每个键读一遍 —— 两者都在 effect 的追踪窗口内,\n * 所以信号依赖不会因为多这一遍而丢失。\n */\n/**\n * `classList` 每次贡献的类名(按元素记账,便于下次替换而不是叠加)。\n *\n * 为什么需要记账:`classList` 与 `class` 写的是**同一个** HTML 属性。\n * 若不记住上次由 `classList` 贡献了哪些,切换时要么越加越多、要么把作者的 `class` 抹掉。\n */\nconst classListContributions = new WeakMap<object, string[]>()\n\n/** `classList` 值 → 类名数组。支持对象 / 数组 / 字符串三种写法。 */\nexport function parseClassList(value: unknown): string[] {\n const names: string[] = []\n const push = (candidate: unknown): void => {\n if (typeof candidate === 'string') {\n for (const name of candidate.split(/\\s+/u)) if (name !== '') names.push(name)\n }\n }\n if (Array.isArray(value)) {\n for (const entry of value) {\n if (entry !== null && typeof entry === 'object') {\n for (const [name, on] of Object.entries(entry)) if (on) push(name)\n } else push(entry)\n }\n } else if (value !== null && typeof value === 'object') {\n for (const [name, on] of Object.entries(value)) if (on) push(name)\n } else push(value)\n return names\n}\n\n/**\n * 应用 `classList`:与元素上已有的 `class` **合并**,并可反复切换。\n *\n * 存在的理由(外部踩坑文档 C 条):条件分支里的输入控件会丢焦点 ——\n * `insertDynamic` 按引用比较(`next === current`),写条件表达式就会重建子树。\n * 文档给出的对策是「固定渲染 + 响应式 class 显隐」,但此前只能手写字符串模板:\n *\n * ```tsx\n * <div class={open.value ? 'panel is-open' : 'panel'}> // 易漏、易写错\n * <div class=\"panel\" classList={{ 'is-open': open.value }}> // 现在:\n * ```\n *\n * `classList` 只负责它自己贡献的部分,作者写在 `class` 里的类名不受影响。\n */\nexport function applyClassList(node: Element, value: unknown): void {\n const next = parseClassList(value)\n const previous = classListContributions.get(node) ?? []\n classListContributions.set(node, next)\n const base = (node.getAttribute('class') ?? '')\n .split(/\\s+/u)\n .filter(name => name !== '' && !previous.includes(name))\n const merged = [...new Set([...base, ...next])]\n setAttribute(node, 'class', merged.join(' '))\n}\n\nfunction isClassListKey(key: string): boolean {\n return key === 'classList'\n}\n\nexport function bindSpreadProps(node: Element, source: () => Record<string, unknown>): void {\n let previous: Record<string, unknown> | null = null\n effect(() => {\n const next = source() ?? {}\n const snapshot = { ...next }\n applySpreadProps(node, previous, next)\n previous = snapshot\n })\n}\n\n/** Apply compile-time host properties in one renderer pass. */\nexport function setStaticProps(node: Element, props: Record<string, unknown>): void {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'key' || key === 'ref' || key.startsWith('on')) continue\n if (value === null || value === undefined) continue\n if (isClassListKey(key)) applyClassList(node, value)\n else if (isPropertyName(key)) setProperty(node, key, value)\n else if (value === false) continue\n else setAttribute(node, domAttributeName(key), key === 'style' && isStyleObject(value) ? formatStyle(value) : String(value))\n }\n}\n\n\nfunction isStyleObject(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction formatStyle(value: Record<string, unknown>): string {\n return Object.entries(value).filter(([, entry]) => entry !== null && entry !== undefined && entry !== false)\n .map(([key, entry]) => `${key.replace(/[A-Z]/gu, match => `-${match.toLowerCase()}`)}:${String(entry)}`).join(';')\n}\n\nexport function addEventListener(\n node: Element,\n event: string,\n handler: EventListener\n): void {\n const renderer = getRenderer()\n const owner = getCurrentOwner()\n let bindings = eventBindings.get(node)\n if (!bindings) {\n bindings = new Map()\n eventBindings.set(node, bindings)\n }\n const previous = bindings.get(event)\n if (previous && previous.original === handler && previous.owner === owner) return\n if (previous) {\n // 旧监听先摘掉;Owner 清理槽由下面的 slots 原地续用,不在清理表里增删\n renderer.removeEventListener(node, event, previous.handler)\n }\n const listener = owner ? (reason: Event) => {\n // Owner 已销毁说明节点所属子树已被卸载/替换,事件来自游离 DOM,直接忽略。\n // 否则 owner.run 会抛\"已销毁的 Owner\",在事件流里制造无意义的错误噪音。\n if (owner.disposed) return\n try {\n owner.run(() => handler(reason))\n } catch (error) {\n const handled = owner.handleError(error)\n invokeRuntimeDebug('error', {\n error,\n owner,\n phase: 'event',\n handled,\n recovery: handled ? 'handled' : 'propagated'\n })\n /*\n * 被 owner 的 onError 处理掉之后,错误就只剩 debug 钩子这一个出口 ——\n * 没装 DevTools 时控制台一个字都没有:事件处理器抛错、界面没反应、无从查起。\n * 没被处理的情况下面会 rethrow(浏览器自己会报),所以只在「被吞下」时补控制台。\n */\n if (handled && getRuntimeDebugHooks()?.error === undefined) {\n console.error(formatVobsError(error, { includeStack: true }))\n }\n if (!handled) throw error\n }\n } : handler\n /*\n * 这个 (node,event) 在 Owner 清理表里只占**一个**槽,替换 handler 时**原地**换掉它做的事。\n *\n * 此前是在**每次** addEventListener 里无条件 `owner.onDispose(...)`,而 `Owner.cleanups`\n * 是只 push 的数组(reactivity/src/owner.ts:57,84)→ 换一次 handler 就多一条永不执行的\n * 清理项,旧的 handler 闭包也被一并扣住。实测(.artifacts/reports/runtime.supplement.md\n * 缺点 4 与既有报告缺点 1,两轮独立复现):500 次替换 → `cleanups.length === 500`;\n * 一轮 `{...props}` 换 onClick → 202。组件的 effect 每次重跑都会重绑事件,\n * 所以这是热路径上的**无界增长**(长寿命页面里等价于内存泄漏)。\n *\n * `binding` 是 `const`,所以槽里必须包一层可变的 `current` —— 换绑时改的是 `current`,\n * 数组长度不变、旧闭包被释放。\n */\n const binding: EventBinding = { handler: listener, owner, original: handler }\n bindings.set(event, binding)\n renderer.addEventListener(node, event, listener)\n if (!owner) return\n /*\n * **每个 (node,event) 在 Owner 清理表里只占一个槽。**\n *\n * 此前是在**每次** addEventListener 里无条件 `owner.onDispose(...)`,而 `Owner.cleanups`\n * 是只 push 的数组(reactivity/src/owner.ts:57,84)→ 换一次 handler 就多一条永不执行的\n * 清理项,旧的 handler 闭包也被一并扣住。实测(.artifacts/reports/runtime.supplement.md\n * 缺点 4 与既有报告缺点 1,两轮独立复现):500 次替换 → `cleanups.length === 500`;\n * 一轮 `{...props}` 换 onClick → 202。组件 effect 每次重跑都会重绑事件,\n * 所以这是热路径上的**无界增长**(长寿命页面里等价于内存泄漏)。\n *\n * 做法:清理槽里存的是**间接引用**(owner → (node,event) → 当前 handle),\n * 重绑时只换间接引用指向的东西,槽本身不增不减;最后一个引用被清掉时槽自然失效。\n * 键用 per-owner 的 `eventCleanupKeys` 映射成数字,避免长字符串前缀比较。\n */\n const key = eventCleanupKey(node, event)\n let slots = eventCleanupSlots.get(owner)\n if (!slots) {\n slots = new Map()\n eventCleanupSlots.set(owner, slots)\n }\n // 槽存的永远是\"当前该清哪一条绑定\",所以重绑/摘除后再绑都只是覆盖它\n const hasSlot = slots.has(key)\n slots.set(key, binding)\n if (hasSlot) return\n owner.onDispose(() => {\n const current = eventCleanupSlots.get(owner)?.get(key)\n if (!current) return\n eventCleanupSlots.get(owner)?.delete(key)\n const map = eventBindings.get(node)\n if (map?.get(event) === current) map.delete(event)\n renderer.removeEventListener(node, event, current.handler)\n })\n}\n\nexport function removeEventListener(\n node: Element,\n event: string,\n handler: EventListener\n): void {\n const renderer = getRenderer()\n const map = eventBindings.get(node)\n const binding = map?.get(event)\n /*\n * **必须比 handler 身份**(DOM 的 `removeEventListener` 语义就是如此)。\n *\n * 原来这里忽略传入的 `handler`:只要 `(node,event)` 上有绑定就一律摘掉,\n * 并把绑定记录删掉。于是\"用另一个 handler 调 remove\"会把**别人的**绑定误摘 ——\n * 调用方以为只是取消自己那次(本来应当是 no-op),实际把当前生效的监听弄没了。\n *\n * 现在的行为:\n * - 有绑定且身份一致 → 摘掉(正常路径)\n * - 有绑定但身份不同 → **no-op**(不去动别人的绑定)\n * - 绑定记录里没有(例如由别处直接经 renderer 绑的)→ 按传入的 handler 兜底摘一次,\n * 保持与渲染器 API 直连时的可用性\n */\n if (binding) {\n if (binding.handler !== handler) return\n map?.delete(event)\n }\n renderer.removeEventListener(node, event, binding?.handler ?? handler)\n}\n\n/**\n * 清空容器。\n *\n * 原来只调渲染器的 clear(DOM 实现就是 `textContent = ''`)—— **被清掉子树的 Owner\n * 一个都不释放**:组件里的 effect 继续订阅信号(幽灵更新)、监听不解绑、onDispose 不跑。\n * 现在先把子树里能找到的 Owner 释放掉,再清 DOM。\n *\n * 覆盖范围如实说明:`nodeOwners` 只登记**组件输出节点**(`createComponent` 时\n * `associateNodeOwner`)。所以在组件边界上清理是完整的(组件 Owner 的销毁会级联它的\n * effect 与子 Owner);而在组件外直接 `bindText` 之类建立的 effect 挂在调用方当时的\n * Owner 上、不在节点表里,这里**清不到** —— 那部分归它的 Owner 管,通常由\n * `app.destroy()` 负责。要彻底解决需要节点表能反向枚举,代价更大,先不做。\n *\n * 清理抛错不影响 DOM 被清空(先释放、后清 DOM 的顺序也保证了这一点)。\n */\nexport function clear(container: Node): void {\n let firstError: unknown\n const walk = (node: Node): void => {\n try {\n disposeNodeOwner(node as unknown as VobsNode)\n } catch (error) {\n firstError ??= error\n }\n for (const child of Array.from(node.childNodes)) walk(child)\n }\n for (const child of Array.from(container.childNodes)) walk(child)\n\n getRenderer().clear(container)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'clear',\n target: describeDebugNode(container as unknown as VobsNode),\n parent: describeDebugNode(container as unknown as VobsNode)\n })\n }\n if (firstError) throw firstError\n}\n\ntype VobsComponent = (...args: any[]) => VobsNode\n\ntype ComponentProps<Component extends VobsComponent> = Component extends (\n props: infer Props\n) => VobsNode\n ? NonNullable<Props> extends object ? NonNullable<Props> : Record<string, never>\n : Record<string, never>\n\nexport function createComponent<Component extends VobsComponent>(\n component: Component,\n props: ComponentProps<Component>,\n source?: VobsSourceLocation\n): VobsNode {\n const owner = createOwner()\n const componentName = (component as typeof component & { displayName?: string }).displayName\n || component.name\n || 'anonymous'\n setOwnerDebugName(owner, source\n ? `${componentName} (${source.file}:${source.line}:${source.column})`\n : componentName)\n owner.onError(reason => {\n attachSourceLocation(reason, source)\n attachComponentContext(reason, componentName, owner.id)\n throw reason\n })\n // HMR 注册先于渲染作用域标记:注册清理必须跨热更新保活,不能被\n // disposeSince 当作上一轮渲染的清理释放掉。\n const hmrKey = (component as typeof component & { hmrKey?: string }).hmrKey\n let instance: HmrInstance | null = null\n if (hmrKey) {\n const separator = hmrKey.lastIndexOf(':')\n const moduleId = separator < 0 ? hmrKey : hmrKey.slice(0, separator)\n instance = { node: null as unknown as VobsNode, parent: null, refresh: () => refreshInstance() }\n const cleanup = registerHmrInstance(moduleId, instance)\n owner.onDispose(cleanup)\n }\n // 渲染作用域:组件 Owner 只承载 HMR 注册与错误处理,每轮渲染注册的 effect、\n // onDispose(含 portal 清理)与嵌套组件 Owner 都归属该轮作用域。HMR refresh\n // 重渲染前释放上一轮作用域,旧实例的 effect 不再订阅信号、portal 节点不再\n // 残留在 body 中每轮热更新叠加;组件 Owner 与 HMR 注册保活。\n const renderScope = owner.mark()\n /*\n * 局部类型放宽到运行时真实值域:组件返回 `null` / `undefined` / `false`\n * 都是合法 JSX 写法(`cond ? <X/> : null`、`cond && <X/>`),\n * 只是 `VobsComponent` 的声明把它写窄了。下面统一归一化成空注释节点。\n */\n /*\n * 用**独立变量**承接渲染结果再归一化。\n * 直接写 `let node: VobsNode | null | undefined | false` 再比较 `node === false` 时,\n * TS 的控制流分析会依据 `component(props)` 的声明类型把 `node` 收窄回 `VobsNode`,\n * 于是 `false` 分支被判成\"类型不可达\"而报错 —— 但运行时它确实会出现。\n */\n let rendered: unknown\n try {\n // 组件渲染必须 untrack:组件是 run-once 的,其渲染发生在某次 effect 求值\n // (insertDynamic/insertBoundary 的渲染工厂、路由挂载)内时,若不切断追踪,\n // 组件体内读取的信号会被收集为祖先 effect 的依赖——一次无关编辑就会触发\n // 整棵子树销毁重建(输入框被换掉、焦点丢失、事件监听随旧树一起被清理)。\n // 结构性响应只属于条件工厂与绑定 effect,组件本体渲染一次即止。\n rendered = owner.run(() => untrack(() => component(props)))\n } catch (error) {\n owner.dispose()\n attachSourceLocation(error, source)\n attachComponentContext(error, componentName, owner.id)\n throw error\n }\n /*\n * 组件返回 `null` / `undefined` / `false` 时用**注释节点**代替。\n *\n * 这是生产事故(Labelune 2026-09-30 发版黑屏)的根因:`createComponent` 把渲染结果\n * 直接当作 `nodeOwners` 这个 **WeakMap 的 key**,而 `null` / `undefined` 不是合法 key ——\n * `nodeOwners.set(null, owner)` 抛 `Invalid value used as weak map key`。\n * 后果特别贵:App 挂载即崩、**没有任何 JS 崩溃日志**、splash 兜底也失效,\n * 只能靠人肉纪律「组件永远返回真实节点」避免。\n *\n * 「组件顶层条件 return null」是合法的 JSX 写法,框架不该崩。\n * 注释节点是**渲染为空的既有原语**(fragment 与 dynamic 的标记都用它),\n * 视觉结果与返回 null 完全一致,但满足 `VobsNode` 契约 —— 于是挂载、HMR 刷新、\n * 节点替换、水合这些下游路径都不需要各自特判一个\"空\"分支。\n */\n const node: VobsNode = isRenderableNode(rendered) ? rendered : createComment('vobs:empty')\n associateNodeOwner(node, owner)\n if (instance) {\n instance.node = node\n associateHmrInstance(node, instance)\n }\n function refreshInstance(): void {\n // 组件已随旧渲染树一起卸载(如父级在同一次热更新中先完成刷新),\n // 无法也无需再刷新。\n if (owner.disposed) return\n const previous = instance!.node\n owner.disposeSince(renderScope)\n const next = owner.run(() => untrack(() => component(props)))\n const parent = instance!.parent\n if (parent) {\n // 先插入新树再卸载旧树:替换锚点始终取自仍在文档中的旧树,\n // fragment 与普通节点两种形态任意组合都能正确定位插入点。\n const anchor = isVobsFragment(previous) ? previous.start : previous\n if (isVobsFragment(next)) next.mount(parent, anchor)\n else getRenderer().insertBefore(parent, next, anchor)\n if (isVobsFragment(previous)) previous.unmount(parent)\n else getRenderer().removeChild(parent, previous)\n }\n nodeOwners.delete(previous as object)\n nodeOwners.set(next as object, owner)\n associateHmrInstance(next, instance!)\n instance!.node = next\n }\n return node\n}\n\nexport interface VobsSourceLocation {\n readonly file: string\n readonly line: number\n readonly column: number\n}\n\nexport interface VobsLocatedError extends Error {\n readonly vobsSource?: VobsSourceLocation\n readonly vobsComponent?: string\n readonly vobsOwnerId?: string\n}\n\nfunction attachSourceLocation(reason: unknown, source: VobsSourceLocation | undefined): void {\n if (!source || (!reason || (typeof reason !== 'object' && typeof reason !== 'function'))) return\n const error = reason as VobsLocatedError\n if (error.vobsSource) return\n try {\n Object.defineProperty(error, 'vobsSource', {\n configurable: true,\n enumerable: false,\n value: source,\n writable: false\n })\n } catch {\n // Frozen third-party errors still propagate with their original details.\n }\n}\n\nfunction attachComponentContext(reason: unknown, component: string, ownerId: string): void {\n if (!reason || (typeof reason !== 'object' && typeof reason !== 'function')) return\n const error = reason as VobsLocatedError\n try {\n if (!error.vobsComponent) Object.defineProperty(error, 'vobsComponent', { configurable: true, enumerable: false, value: component, writable: false })\n if (!error.vobsOwnerId) Object.defineProperty(error, 'vobsOwnerId', { configurable: true, enumerable: false, value: ownerId, writable: false })\n } catch {\n // Frozen third-party errors still propagate with their original details.\n }\n}\n\nexport function createBlock(factory: () => VobsNode | null | undefined | false): VobsNode | null {\n const owner = createOwner()\n setOwnerDebugName(owner, 'dynamic')\n let node: VobsNode | null | undefined | false\n try {\n node = owner.run(factory)\n } catch (error) {\n owner.dispose()\n throw error\n }\n if (!node) {\n owner.dispose()\n return null\n }\n associateNodeOwner(node, owner)\n return node\n}\n\n/**\n * 组件渲染结果是否是**可用的节点值**。\n *\n * `VobsNode` 的真实值域是一个对象(`Node` / fragment / 自定义渲染器的宿主对象),\n * 而合法 JSX 写法可能返回 `null` / `undefined` / `false`(`cond ? <X/> : null`、`cond && <X/>`)。\n * 用一个守卫而不是直接比较,是因为 `component()` 的声明类型是 `VobsNode`,\n * TS 的控制流会把 `=== false` 之类的比较判成\"类型不可达\"。\n */\nfunction isRenderableNode(value: unknown): value is VobsNode {\n return (typeof value === 'object' && value !== null) || typeof value === 'function'\n}\n\nexport function associateNodeOwner(node: VobsNode, owner: Owner): void {\n /*\n * 防御:`node` 在类型上必然是 `VobsNode`,但运行时可能收到 `null`/`undefined`\n * (组件返回空、第三方渲染器返回非节点)。直接当 WeakMap key 会抛\n * `Invalid value used as weak map key` —— 那是**框架内部错误**,信息量为零,\n * 且会让 App 挂载静默崩溃。这条断言把它变成一条能指认原因的报错。\n */\n if (node === null || node === undefined || (typeof node !== 'object' && typeof node !== 'function')) {\n throw new Error(\n `Vobs: associateNodeOwner 收到非节点值(${String(node)})。`\n + '组件必须返回 VobsNode;返回 null/undefined 由 createComponent 转成空注释节点。'\n )\n }\n nodeOwners.set(node, owner)\n}\n\nexport function disposeNodeOwner(node: VobsNode): void {\n const owner = nodeOwners.get(node)\n if (!owner) return\n nodeOwners.delete(node)\n owner.dispose()\n}\n","import type { VobsNode } from './fragment'\nimport { formatVobsError } from './error'\n\nexport type HmrComponent<Props extends object = Record<string, unknown>> =\n (props: Props) => VobsNode\n\nexport interface HmrStateStore {\n get<T>(key: string, initial: T | (() => T)): T\n set<T>(key: string, value: T): void\n has(key: string): boolean\n delete(key: string): void\n clear(): void\n}\n\nexport interface HmrInstance {\n node: VobsNode\n parent: Node | null\n refresh(): void\n}\n\ninterface HmrModuleState {\n readonly components: Map<string, HmrComponent>\n readonly state: Map<string, unknown>\n readonly instances: Set<HmrInstance>\n}\n\ninterface HmrGlobal {\n modules: Map<string, HmrModuleState>\n /** 编译器生成的 hmrStateRef 注册表:键为 `${moduleId}#${声明名}`,值跨模块重执行保活。 */\n states: Map<string, unknown>\n}\n\nconst globalTarget = globalThis as typeof globalThis & { __VOBS_HMR__?: HmrGlobal }\nconst hmrGlobal = globalTarget.__VOBS_HMR__ ?? { modules: new Map<string, HmrModuleState>(), states: new Map<string, unknown>() }\nglobalTarget.__VOBS_HMR__ = hmrGlobal\n\nexport function resolveComponent<Props extends object>(\n component: HmrComponent<Props>,\n moduleId: string,\n exportName: string\n): HmrComponent<Props> {\n const module = getModule(moduleId)\n const existing = module.components.get(exportName)\n if (existing) return existing as HmrComponent<Props>\n\n const proxy = ((props: Props) => {\n const current = (proxy as HmrComponent<Props> & { current: HmrComponent<Props> }).current\n return current(props)\n }) as HmrComponent<Props> & { current: HmrComponent<Props> }\n proxy.current = component\n Object.defineProperties(proxy, {\n displayName: { configurable: true, value: component.name || exportName },\n hmrKey: { configurable: false, value: `${moduleId}:${exportName}` }\n })\n module.components.set(exportName, proxy as HmrComponent)\n return proxy\n}\n\nexport function updateHmrModule(moduleId: string, nextModule: Record<string, unknown>): void {\n const module = hmrGlobal.modules.get(moduleId)\n if (!module) return\n let changed = false\n for (const [name, proxy] of module.components) {\n const next = nextModule[name]\n if (typeof next !== 'function') continue\n const hmrProxy = proxy as HmrComponent & { current: HmrComponent; displayName?: string }\n hmrProxy.current = next as HmrComponent\n Object.defineProperty(hmrProxy, 'displayName', { configurable: true, value: next.name || name })\n changed = true\n }\n if (!changed) return\n // 快照后正序遍历:实例在自身渲染开始前注册,祖先必然先于后代入列——\n // 祖先先刷新会 dispose 上一轮渲染作用域,其树内旧后代实例的 refresh\n // 命中已销毁守卫被跳过,避免同一组件在一次热更新中重渲染两次。\n // 同时 refresh 重渲染以新代码创建的全新实例不在快照中,天然不会再刷新。\n const instances = [...module.instances]\n for (const instance of instances) {\n try {\n instance.refresh()\n } catch (error) {\n /*\n * 静默吞掉会让开发者以为热更新成功了,而屏幕上是旧的 —— 最容易被当成\n * 「改了没生效」查半天。刷新失败本身不冒泡(下一次正常渲染会再抛),\n * 但必须留下记录。\n */\n console.error(formatVobsError(error, { includeStack: true }))\n }\n }\n}\n\nexport function disposeHmrModule(_moduleId: string): void {\n // State and component proxies intentionally survive module disposal.\n}\n\nexport function createHmrStateStore(moduleId: string): HmrStateStore {\n const state = getModule(moduleId).state\n return {\n get<T>(key: string, initial: T | (() => T)): T {\n if (!state.has(key)) state.set(key, typeof initial === 'function' ? (initial as () => T)() : initial)\n return state.get(key) as T\n },\n set<T>(key: string, value: T): void {\n state.set(key, value)\n },\n has: key => state.has(key),\n delete: key => { state.delete(key) },\n clear: () => { state.clear() }\n }\n}\n\n/**\n * 编译器为模块顶层 state() 声明生成的取值入口。键为 `${moduleId}#${声明名}`,\n * 与手动 store 键空间隔离。模块热更新重执行时复用既有信号实例:旧导入方持有的\n * 实例与新模块实例共享同一份状态,消除\"两份模块、两份状态\"导致的编辑不生效/页面半边失灵。\n */\nexport function hmrStateRef<T>(key: string, create: () => T): T {\n const states = hmrGlobal.states\n if (states.has(key)) return states.get(key) as T\n const value = create()\n states.set(key, value)\n return value\n}\n\nexport function registerHmrInstance(moduleId: string, instance: HmrInstance): () => void {\n const instances = getModule(moduleId).instances\n instances.add(instance)\n return () => instances.delete(instance)\n}\n\nexport function markHmrInstanceMounted(node: VobsNode, parent: Node): void {\n const instance = hmrInstances.get(node as object)\n if (instance) instance.parent = parent\n}\n\nfunction getModule(moduleId: string): HmrModuleState {\n let module = hmrGlobal.modules.get(moduleId)\n if (!module) {\n module = { components: new Map(), state: new Map(), instances: new Set() }\n hmrGlobal.modules.set(moduleId, module)\n }\n return module\n}\n\nconst hmrInstances = new WeakMap<object, HmrInstance>()\n\nexport function associateHmrInstance(node: VobsNode, instance: HmrInstance): void {\n hmrInstances.set(node as object, instance)\n}\n","import { getCurrentOwner, type Owner } from '@vobs/reactivity'\nimport { formatVobsError } from './error'\n\n/** A mutable reference populated when a host node is mounted. */\nexport interface Ref<T extends object = Node> {\n current: T | null\n}\n\nexport type RefTarget<T extends object = Node> = Ref<T> | ((value: T | null) => void)\n\nexport function ref<T extends object = Node>(initialValue: T | null = null): Ref<T> {\n return { current: initialValue }\n}\n\n/*\n * 每个 (Owner, ref 目标) 只保留**一条**清理注册。\n *\n * 为什么需要:`ref` 会被先后绑到不同节点(同一个 ref 对象、节点被替换;\n * 或条件分支里换了宿主元素)。若每次都 `onDispose` 追加一条,销毁时按注册**逆序**执行,\n * 后注册的那条先把 `target.current` 清成 `null`,于是先注册那条的守卫\n * (`isObjectRef(target) && target.current !== node`)**恰好通过**,再清一次 ——\n * 最终读到 `null` 而不是当前节点。\n *\n * 用 WeakMap<Owner, WeakMap<target, cleanup>> 而不是往 Owner 上挂属性:\n * Owner 是 class 实例,挂新属性会把它推进字典模式(reactivity/src/owner.ts 记过这笔代价)。\n * target 可弱引用(对象),函数型 ref 也可弱引用,所以不阻止回收。\n */\nconst refCleanups = new WeakMap<Owner, WeakMap<object, () => void>>()\n\nfunction rememberRefCleanup(owner: Owner, target: object, cleanup: () => void): void {\n let byTarget = refCleanups.get(owner)\n if (!byTarget) {\n byTarget = new WeakMap()\n refCleanups.set(owner, byTarget)\n }\n const previous = byTarget.get(target)\n if (previous) owner.removeCleanup(previous)\n byTarget.set(target, cleanup)\n}\n\n/** Bind a host node to an object or callback ref and clear it with its Owner. */\nexport function setRef<T extends object>(node: T, target: unknown): void {\n if (!isRefTarget<T>(target)) return\n // SSR's serializable nodes are never exposed as live refs. Custom renderers\n // may use arbitrary host objects, so only exclude the known SSR shape.\n if (isSSRNode(node)) return\n const owner = getCurrentOwner()\n assignRef(target, node)\n if (!owner) return\n const cleanup = (): void => {\n /*\n * 清除 ref 时**必须自己吞掉异常**,不能让 user 的 ref 回调把 dispose 级联打断。\n *\n * `assignRef` 内部已经 try/catch + `console.error`(回调是用户代码,不该让挂载失败),\n * 但那条保护只覆盖\"赋值\"这一步 —— 而这里还有一个**可能抛错的前置判断**。\n * 按 React 语义,函数型 ref 在卸载时**本来就该收到 `null`**;若回调没判空\n * (如 `node => insertList(node, …)`),它会抛,而异常从 onDispose 冒出去会\n * **中断 Owner 的整个清理循环**(实测:/runtime 页面的 `<ul ref={attachList}>`)。\n * 这与 owner.ts 已修的\"子 Owner 抛错不能中断级联\"是同一类缺陷。\n */\n try {\n // Do not clear a ref that has since been reassigned to another node.\n if (isObjectRef(target) && target.current !== node) return\n assignRef(target, null)\n } catch (error) {\n console.error(formatVobsError(error, { includeStack: true }))\n }\n }\n rememberRefCleanup(owner, target as object, cleanup)\n owner.onDispose(cleanup)\n}\n\nfunction isSSRNode(value: object): boolean {\n const type = (value as { type?: unknown }).type\n return (type === 'element' || type === 'text' || type === 'comment')\n && !('nodeType' in value)\n}\n\nfunction isObjectRef<T extends object>(value: unknown): value is Ref<T> {\n return Boolean(value && typeof value === 'object' && 'current' in value)\n}\n\nfunction isRefTarget<T extends object>(value: unknown): value is RefTarget<T> {\n return isObjectRef<T>(value) || typeof value === 'function'\n}\n\nfunction assignRef<T extends object>(target: RefTarget<T>, value: T | null): void {\n try {\n if (typeof target === 'function') target(value)\n else target.current = value\n } catch (error) {\n /*\n * ref 回调是用户代码,挂载不该因为它失败 —— 但也不能**完全无声**:\n * 回调抛错时 ref 拿不到节点,界面看起来正常,用户完全不知道为什么。\n * 保持吞掉(不冒泡),但打一条控制台记录。\n */\n console.error(formatVobsError(error, { includeStack: true }))\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAAA,qBAAgC;;;ACEhC,IAAAC,qBAA6F;;;AC8B7F,IAAM,eAAe;AACrB,IAAM,YAAY,aAAa,gBAAgB,EAAE,SAAS,oBAAI,IAA4B,GAAG,QAAQ,oBAAI,IAAqB,EAAE;AAChI,aAAa,eAAe;;;AClC5B,wBAA4C;;;AFoB5C,IAAI,kBAA0C;AA+CvC,SAAS,cAA+B;AAC7C,MAAI,CAAC,iBAAiB;AACpB,UAAM,IAAI,MAAM,4CAAS;AAAA,EAC3B;AACA,SAAO;AACT;AALgB;AAwCT,SAAS,cAAc,SAA0B;AACtD,SAAO,YAAY,EAAE,cAAc,OAAO;AAC5C;AAFgB;;;AD7FT,SAAS,eAAe,SAAwC;AACrE,QAAM,QAAQ,cAAc,qBAAqB;AACjD,QAAM,MAAM,cAAc,mBAAmB;AAC7C,MAAI,SAAsB;AAC1B,MAAI,cAAc;AAClB,QAAM,YAAQ,oCAAgB;AAE9B,QAAM,WAAyB;AAAA,IAC7B,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,MAAM,YAAY,QAAc;AAC9B,UAAI,UAAU,WAAW,YAAY;AACnC,cAAM,IAAI,MAAM,0EAAkC;AAAA,MACpD;AAEA,UAAI,aAAa;AACf,kBAAU,YAAY,OAAO,KAAK,MAAM;AACxC;AAAA,MACF;AAEA,YAAM,WAAW,YAAY;AAC7B,eAAS,aAAa,YAAY,OAAO,MAAM;AAC/C,eAAS,aAAa,YAAY,KAAK,MAAM;AAC7C,eAAS;AACT,oBAAc;AACd,UAAI,MAAO,OAAM,IAAI,MAAM,QAAQ,YAAY,GAAG,CAAC;AAAA,UAC9C,SAAQ,YAAY,GAAG;AAAA,IAC9B;AAAA,IACA,QAAQ,YAAkB;AACxB,UAAI,CAAC,eAAe,WAAW,YAAY;AACzC,cAAM,IAAI,MAAM,0EAAkC;AAAA,MACpD;AACA,YAAM,WAAW,YAAY;AAC7B,UAAI,UAAU,SAAS,YAAY,KAAK;AACxC,aAAO,WAAW,YAAY,KAAK;AACjC,cAAM,OAAO,SAAS,YAAY,OAAO;AACzC,iBAAS,YAAY,YAAY,OAAO;AACxC,kBAAU;AAAA,MACZ;AACA,eAAS,YAAY,YAAY,KAAK;AACtC,eAAS,YAAY,YAAY,GAAG;AACpC,eAAS;AACT,oBAAc;AAAA,IAChB;AAAA,EACF;AACA,SAAO;AACT;AA/CgB;AAiDT,SAAS,eAAe,OAAuC;AACpE,SAAO,QAAQ,KAAK,KAAK,OAAO,UAAU,YAAa,MAAuB,SAAS;AACzF;AAFgB;AAIhB,SAAS,UAAU,QAAc,OAAa,KAAW,QAA2B;AAClF,QAAM,WAAW,YAAY;AAC7B,QAAM,QAAgB,CAAC,KAAK;AAC5B,MAAI,UAAU,SAAS,YAAY,KAAK;AACxC,SAAO,SAAS;AACd,UAAM,KAAK,OAAO;AAClB,QAAI,YAAY,IAAK;AACrB,cAAU,SAAS,YAAY,OAAO;AAAA,EACxC;AACA,MAAI,MAAM,MAAM,SAAS,CAAC,MAAM,KAAK;AACnC,UAAM,IAAI,MAAM,2DAAwB;AAAA,EAC1C;AACA,aAAW,QAAQ,MAAO,UAAS,aAAa,QAAQ,MAAM,MAAM;AACtE;AAbS;","names":["import_reactivity","import_reactivity"]}
|
package/dist/fragment.js
CHANGED
|
@@ -5,7 +5,7 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
|
|
|
5
5
|
import { getCurrentOwner as getCurrentOwner3 } from "@vobs/reactivity";
|
|
6
6
|
|
|
7
7
|
// packages/runtime/src/ops.ts
|
|
8
|
-
import { createOwner, getCurrentOwner as getCurrentOwner2, setOwnerDebugName, untrack } from "@vobs/reactivity";
|
|
8
|
+
import { createOwner, effect, getCurrentOwner as getCurrentOwner2, setOwnerDebugName, untrack } from "@vobs/reactivity";
|
|
9
9
|
|
|
10
10
|
// packages/runtime/src/hmr.ts
|
|
11
11
|
var globalTarget = globalThis;
|
package/dist/fragment.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/fragment.ts","../src/ops.ts","../src/hmr.ts","../src/ref.ts"],"sourcesContent":["import { getCurrentOwner } from '@vobs/reactivity'\nimport { createComment, getRenderer } from './ops'\n\nexport interface VobsFragment {\n readonly kind: 'vobs-fragment'\n readonly start: Node\n readonly end: Node\n readonly mount: (parent: Node, anchor: Node | null) => void\n readonly unmount: (parent: Node) => void\n}\n\nexport type VobsNode = Node | VobsFragment\nexport type FragmentFactory = (parent: Node, anchor: Node) => void\n\nexport function createFragment(factory: FragmentFactory): VobsFragment {\n const start = createComment('vobs:fragment:start')\n const end = createComment('vobs:fragment:end')\n let parent: Node | null = null\n let initialized = false\n const owner = getCurrentOwner()\n\n const fragment: VobsFragment = {\n kind: 'vobs-fragment',\n start,\n end,\n mount(nextParent, anchor): void {\n if (parent && parent !== nextParent) {\n throw new Error('Vobs Fragment: 不能跨父节点移动 Fragment')\n }\n\n if (initialized) {\n moveRange(nextParent, start, end, anchor)\n return\n }\n\n const renderer = getRenderer()\n renderer.insertBefore(nextParent, start, anchor)\n renderer.insertBefore(nextParent, end, anchor)\n parent = nextParent\n initialized = true\n if (owner) owner.run(() => factory(nextParent, end))\n else factory(nextParent, end)\n },\n unmount(nextParent): void {\n if (!initialized || parent !== nextParent) {\n throw new Error('Vobs Fragment: Fragment 不属于指定父节点')\n }\n const renderer = getRenderer()\n let current = renderer.nextSibling(start)\n while (current && current !== end) {\n const next = renderer.nextSibling(current)\n renderer.removeChild(nextParent, current)\n current = next\n }\n renderer.removeChild(nextParent, start)\n renderer.removeChild(nextParent, end)\n parent = null\n initialized = false\n }\n }\n return fragment\n}\n\nexport function isVobsFragment(value: unknown): value is VobsFragment {\n return Boolean(value) && typeof value === 'object' && (value as VobsFragment).kind === 'vobs-fragment'\n}\n\nfunction moveRange(parent: Node, start: Node, end: Node, anchor: Node | null): void {\n const renderer = getRenderer()\n const nodes: Node[] = [start]\n let current = renderer.nextSibling(start)\n while (current) {\n nodes.push(current)\n if (current === end) break\n current = renderer.nextSibling(current)\n }\n if (nodes[nodes.length - 1] !== end) {\n throw new Error('Vobs Fragment: 找不到结束锚点')\n }\n for (const node of nodes) renderer.insertBefore(parent, node, anchor)\n}\n","// 编译产物调用的基础操作\n\nimport { createOwner, getCurrentOwner, setOwnerDebugName, untrack, type Owner } from '@vobs/reactivity'\nimport { isVobsFragment, type VobsNode } from './fragment'\nimport { describeDebugNode, getRuntimeDebugHooks, invokeRuntimeDebug, readDebugValue } from './debug'\nimport {\n associateHmrInstance,\n markHmrInstanceMounted,\n registerHmrInstance,\n type HmrInstance\n} from './hmr'\nimport type { VobsRenderer } from './renderer'\nimport { setRef } from './ref'\n\ntype RuntimeRenderer = VobsRenderer<Node, Text, Element, Comment>\n\nlet currentRenderer: RuntimeRenderer | null = null\nconst nodeOwners = new WeakMap<object, Owner>()\ninterface EventBinding {\n readonly handler: EventListener\n readonly owner: Owner | null\n readonly original: EventListener\n}\nconst eventBindings = new WeakMap<object, Map<string, EventBinding>>()\n\nexport function setRenderer<\n NodeType,\n TextNode extends NodeType,\n ElementNode extends NodeType,\n CommentNode extends NodeType\n>(renderer: VobsRenderer<NodeType, TextNode, ElementNode, CommentNode>): void {\n // 编译产物仍使用 DOM 节点声明;实际宿主类型由应用提供的渲染器决定。\n currentRenderer = renderer as unknown as RuntimeRenderer\n}\n\nexport function getRenderer(): RuntimeRenderer {\n if (!currentRenderer) {\n throw new Error('渲染器未初始化')\n }\n return currentRenderer\n}\n\nexport function createText(content: string): Text {\n return getRenderer().createText(content)\n}\n\n/** SVG 命名空间(与 HTML 解析器 foreign-content 规则一致) */\nexport const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'\n\n/**\n * SVG 标签全集(= TypeScript SVGElementTagNameMap 的键,运行时分发依据)。\n * 与 React 的运行时清单同思路:只含纯 SVG 标签,刻意排除与 HTML 同名的\n * a/script/style/title(它们在 JSX 类型里归 HTMLElement,按 HTML 创建)。\n */\nconst SVG_TAGS = new Set([\n 'animate', 'animateMotion', 'animateTransform', 'circle', 'clipPath', 'defs', 'desc',\n 'ellipse', 'feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite',\n 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight',\n 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR',\n 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset',\n 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence',\n 'filter', 'foreignObject', 'g', 'image', 'line', 'linearGradient', 'marker', 'mask',\n 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialGradient',\n 'rect', 'set', 'stop', 'svg', 'switch', 'symbol', 'text', 'textPath', 'tspan',\n 'use', 'view'\n])\n\nexport function createElement(tag: string): Element {\n // SVG 标签走命名空间创建:document.createElement('rect') 产物是 HTMLUnknownElement,\n // 整棵 SVG 子树都不会渲染(无报错)。渲染器未实现 createSvgElement 时按 createElement\n // 兜底(旧自定义渲染器保持原行为)。\n if (SVG_TAGS.has(tag)) {\n const renderer = getRenderer()\n if (renderer.createSvgElement) return renderer.createSvgElement(tag)\n }\n return getRenderer().createElement(tag)\n}\n\nexport function createComment(content: string): Comment {\n return getRenderer().createComment(content)\n}\n\nexport function insertBefore(\n parent: Node,\n child: VobsNode,\n anchor: VobsNode | null\n): void {\n if (isVobsFragment(child)) {\n child.mount(parent, isVobsFragment(anchor) ? anchor.start : anchor)\n markHmrInstanceMounted(child, parent)\n return\n }\n getRenderer().insertBefore(parent, child, isVobsFragment(anchor) ? anchor.start : anchor)\n markHmrInstanceMounted(child, parent)\n const nodeName = (child as Element).nodeName\n if (nodeName === 'OPTION' || nodeName === 'OPTGROUP') syncSelectValue(parent)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'insert',\n target: describeDebugNode(child),\n parent: describeDebugNode(parent)\n })\n }\n}\n\n/**\n * Select initial-value auto resync: `bindProperty(node, 'value', …)` registers the value\n * reader for `<select>` elements; whenever an `<option>` is inserted into the select\n * (directly or through an `<optgroup>`), the current bound value is re-applied. This\n * removes the last reason for apps to keep `ref + queueMicrotask` value-sync workarounds\n * for options that arrive after the value binding (e.g. asynchronously loaded lists).\n */\nconst selectValueReaders = new WeakMap<object, () => unknown>()\n\nexport function registerSelectValueBinding(select: Element, read: () => unknown): void {\n selectValueReaders.set(select, read)\n}\n\nfunction syncSelectValue(parent: Node): void {\n let current: Node | null = parent\n while (current !== null) {\n if ((current as Element).nodeName === 'SELECT') {\n const read = selectValueReaders.get(current)\n if (read !== undefined) setProperty(current as Element, 'value', read())\n return\n }\n current = current.parentNode\n }\n}\n\nexport function removeChild(\n parent: Node,\n child: VobsNode\n): void {\n disposeNodeOwner(child)\n if (isVobsFragment(child)) {\n child.unmount(parent)\n return\n }\n getRenderer().removeChild(parent, child)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'remove',\n target: describeDebugNode(child),\n parent: describeDebugNode(parent)\n })\n }\n}\n\nexport function setTextContent(\n node: Text,\n content: string\n): void {\n const previousValue = getRuntimeDebugHooks()\n ? readDebugValue(() => node.textContent)\n : undefined\n getRenderer().setTextContent(node, content)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'text',\n target: describeDebugNode(node),\n previousValue,\n nextValue: content\n })\n }\n}\n\nexport function setProperty(\n node: Element,\n key: string,\n value: unknown\n): void {\n const previousValue = getRuntimeDebugHooks()\n ? readDebugValue(() => Reflect.get(node, key))\n : undefined\n getRenderer().setProperty(node, key, value)\n if (key === 'value' && (node as { tagName?: unknown }).tagName === 'SELECT') {\n scheduleSelectValueSync(node, value)\n }\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'property',\n target: describeDebugNode(node),\n key,\n previousValue,\n nextValue: value\n })\n }\n}\n\nconst pendingSelectValues = new WeakMap<Element, unknown>()\nconst selectSyncScheduled = new WeakSet<Element>()\n\n/**\n * `<select value>` 在 option 子节点存在前赋值不生效(HTML 规范:select 的 value\n * 由已存在的 option 决定)。编译产物先设置属性、后插入子节点,静态写法必然丢初始值\n * (消费方此前只能用 ref + queueMicrotask 规避)。\n * 这里对 select 的 value 赋值统一延迟到微任务重放一次:静态子节点在同一同步任务内\n * 插入完毕,重放即命中。动态(insertList/insertDynamic)插入的 option 晚于该微任务时,\n * 由 value 的绑定 effect 在后续信号更新中正常覆盖。\n */\nfunction scheduleSelectValueSync(node: Element, value: unknown): void {\n pendingSelectValues.set(node, value)\n if (selectSyncScheduled.has(node)) return\n selectSyncScheduled.add(node)\n queueMicrotask(() => {\n selectSyncScheduled.delete(node)\n if (!pendingSelectValues.has(node)) return\n const pending = pendingSelectValues.get(node)\n pendingSelectValues.delete(node)\n getRenderer().setProperty(node, 'value', pending)\n })\n}\n\nexport function setAttribute(\n node: Element,\n key: string,\n value: string\n): void {\n const previousValue = getRuntimeDebugHooks()\n ? readDebugValue(() => typeof node.getAttribute === 'function' ? node.getAttribute(key) : undefined)\n : undefined\n getRenderer().setAttribute(node, key, value)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'attribute',\n target: describeDebugNode(node),\n key,\n previousValue,\n nextValue: value\n })\n }\n}\n\nexport function spreadProps(node: Element, props: Record<string, unknown>): void {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'key' || value === null || value === undefined) continue\n if (key === 'ref') {\n setRef(node, value)\n continue\n }\n if (key.startsWith('on') && typeof value === 'function') addEventListener(node, key.slice(2).toLowerCase(), value as EventListener)\n // property 键的 false 有语义(如 disabled={false} 必须清除),不能跳过;\n // attribute 键的 false 表示“不设置”,与 HTML 语义一致。\n else if (isPropertyKey(key)) setProperty(node, key, value)\n else if (value === false) continue\n else setAttribute(node, domAttributeName(key), key === 'style' && isStyleObject(value) ? formatStyle(value) : String(value))\n }\n}\n\n/** Apply compile-time host properties in one renderer pass. */\nexport function setStaticProps(node: Element, props: Record<string, unknown>): void {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'key' || key === 'ref' || key.startsWith('on')) continue\n if (value === null || value === undefined) continue\n if (isPropertyKey(key)) setProperty(node, key, value)\n else if (value === false) continue\n else setAttribute(node, domAttributeName(key), key === 'style' && isStyleObject(value) ? formatStyle(value) : String(value))\n }\n}\n\nfunction isPropertyKey(key: string): boolean {\n return key === 'value' || key === 'checked' || key === 'selected' || key === 'disabled'\n || key === 'multiple' || key === 'readOnly' || key === 'required'\n || key === 'autofocus' || key === 'hidden' || key === 'tabIndex'\n || key === 'colSpan' || key === 'rowSpan'\n}\n\n/** camelCase JSX 属性名 → HTML attribute 名(与 compiler 的 domAttributeName 保持一致) */\nfunction domAttributeName(key: string): string {\n switch (key) {\n case 'className': return 'class'\n case 'htmlFor': return 'for'\n case 'autoComplete': return 'autocomplete'\n case 'spellCheck': return 'spellcheck'\n default: return key\n }\n}\n\nfunction isStyleObject(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction formatStyle(value: Record<string, unknown>): string {\n return Object.entries(value).filter(([, entry]) => entry !== null && entry !== undefined && entry !== false)\n .map(([key, entry]) => `${key.replace(/[A-Z]/gu, match => `-${match.toLowerCase()}`)}:${String(entry)}`).join(';')\n}\n\nexport function addEventListener(\n node: Element,\n event: string,\n handler: EventListener\n): void {\n const renderer = getRenderer()\n const owner = getCurrentOwner()\n let bindings = eventBindings.get(node)\n if (!bindings) {\n bindings = new Map()\n eventBindings.set(node, bindings)\n }\n const previous = bindings.get(event)\n if (previous && previous.original === handler && previous.owner === owner) return\n if (previous) renderer.removeEventListener(node, event, previous.handler)\n const listener = owner ? (reason: Event) => {\n // Owner 已销毁说明节点所属子树已被卸载/替换,事件来自游离 DOM,直接忽略。\n // 否则 owner.run 会抛\"已销毁的 Owner\",在事件流里制造无意义的错误噪音。\n if (owner.disposed) return\n try {\n owner.run(() => handler(reason))\n } catch (error) {\n const handled = owner.handleError(error)\n invokeRuntimeDebug('error', {\n error,\n owner,\n phase: 'event',\n handled,\n recovery: handled ? 'handled' : 'propagated'\n })\n if (!handled) throw error\n }\n } : handler\n const binding: EventBinding = { handler: listener, owner, original: handler }\n bindings.set(event, binding)\n renderer.addEventListener(node, event, listener)\n owner?.onDispose(() => {\n if (bindings?.get(event) !== binding) return\n bindings.delete(event)\n renderer.removeEventListener(node, event, listener)\n })\n}\n\nexport function removeEventListener(\n node: Element,\n event: string,\n handler: EventListener\n): void {\n const renderer = getRenderer()\n const binding = eventBindings.get(node)?.get(event)\n renderer.removeEventListener(node, event, binding?.handler ?? handler)\n eventBindings.get(node)?.delete(event)\n}\n\nexport function clear(container: Node): void {\n getRenderer().clear(container)\n}\n\ntype VobsComponent = (...args: any[]) => VobsNode\n\ntype ComponentProps<Component extends VobsComponent> = Component extends (\n props: infer Props\n) => VobsNode\n ? NonNullable<Props> extends object ? NonNullable<Props> : Record<string, never>\n : Record<string, never>\n\nexport function createComponent<Component extends VobsComponent>(\n component: Component,\n props: ComponentProps<Component>,\n source?: VobsSourceLocation\n): VobsNode {\n const owner = createOwner()\n const componentName = (component as typeof component & { displayName?: string }).displayName\n || component.name\n || 'anonymous'\n setOwnerDebugName(owner, source\n ? `${componentName} (${source.file}:${source.line}:${source.column})`\n : componentName)\n owner.onError(reason => {\n attachSourceLocation(reason, source)\n attachComponentContext(reason, componentName, owner.id)\n throw reason\n })\n // HMR 注册先于渲染作用域标记:注册清理必须跨热更新保活,不能被\n // disposeSince 当作上一轮渲染的清理释放掉。\n const hmrKey = (component as typeof component & { hmrKey?: string }).hmrKey\n let instance: HmrInstance | null = null\n if (hmrKey) {\n const separator = hmrKey.lastIndexOf(':')\n const moduleId = separator < 0 ? hmrKey : hmrKey.slice(0, separator)\n instance = { node: null as unknown as VobsNode, parent: null, refresh: () => refreshInstance() }\n const cleanup = registerHmrInstance(moduleId, instance)\n owner.onDispose(cleanup)\n }\n // 渲染作用域:组件 Owner 只承载 HMR 注册与错误处理,每轮渲染注册的 effect、\n // onDispose(含 portal 清理)与嵌套组件 Owner 都归属该轮作用域。HMR refresh\n // 重渲染前释放上一轮作用域,旧实例的 effect 不再订阅信号、portal 节点不再\n // 残留在 body 中每轮热更新叠加;组件 Owner 与 HMR 注册保活。\n const renderScope = owner.mark()\n let node: VobsNode\n try {\n // 组件渲染必须 untrack:组件是 run-once 的,其渲染发生在某次 effect 求值\n // (insertDynamic/insertBoundary 的渲染工厂、路由挂载)内时,若不切断追踪,\n // 组件体内读取的信号会被收集为祖先 effect 的依赖——一次无关编辑就会触发\n // 整棵子树销毁重建(输入框被换掉、焦点丢失、事件监听随旧树一起被清理)。\n // 结构性响应只属于条件工厂与绑定 effect,组件本体渲染一次即止。\n node = owner.run(() => untrack(() => component(props)))\n } catch (error) {\n owner.dispose()\n attachSourceLocation(error, source)\n attachComponentContext(error, componentName, owner.id)\n throw error\n }\n associateNodeOwner(node, owner)\n if (instance) {\n instance.node = node\n associateHmrInstance(node, instance)\n }\n function refreshInstance(): void {\n // 组件已随旧渲染树一起卸载(如父级在同一次热更新中先完成刷新),\n // 无法也无需再刷新。\n if (owner.disposed) return\n const previous = instance!.node\n owner.disposeSince(renderScope)\n const next = owner.run(() => untrack(() => component(props)))\n const parent = instance!.parent\n if (parent) {\n // 先插入新树再卸载旧树:替换锚点始终取自仍在文档中的旧树,\n // fragment 与普通节点两种形态任意组合都能正确定位插入点。\n const anchor = isVobsFragment(previous) ? previous.start : previous\n if (isVobsFragment(next)) next.mount(parent, anchor)\n else getRenderer().insertBefore(parent, next, anchor)\n if (isVobsFragment(previous)) previous.unmount(parent)\n else getRenderer().removeChild(parent, previous)\n }\n nodeOwners.delete(previous as object)\n nodeOwners.set(next as object, owner)\n associateHmrInstance(next, instance!)\n instance!.node = next\n }\n return node\n}\n\nexport interface VobsSourceLocation {\n readonly file: string\n readonly line: number\n readonly column: number\n}\n\nexport interface VobsLocatedError extends Error {\n readonly vobsSource?: VobsSourceLocation\n readonly vobsComponent?: string\n readonly vobsOwnerId?: string\n}\n\nfunction attachSourceLocation(reason: unknown, source: VobsSourceLocation | undefined): void {\n if (!source || (!reason || (typeof reason !== 'object' && typeof reason !== 'function'))) return\n const error = reason as VobsLocatedError\n if (error.vobsSource) return\n try {\n Object.defineProperty(error, 'vobsSource', {\n configurable: true,\n enumerable: false,\n value: source,\n writable: false\n })\n } catch {\n // Frozen third-party errors still propagate with their original details.\n }\n}\n\nfunction attachComponentContext(reason: unknown, component: string, ownerId: string): void {\n if (!reason || (typeof reason !== 'object' && typeof reason !== 'function')) return\n const error = reason as VobsLocatedError\n try {\n if (!error.vobsComponent) Object.defineProperty(error, 'vobsComponent', { configurable: true, enumerable: false, value: component, writable: false })\n if (!error.vobsOwnerId) Object.defineProperty(error, 'vobsOwnerId', { configurable: true, enumerable: false, value: ownerId, writable: false })\n } catch {\n // Frozen third-party errors still propagate with their original details.\n }\n}\n\nexport function createBlock(factory: () => VobsNode | null | undefined | false): VobsNode | null {\n const owner = createOwner()\n setOwnerDebugName(owner, 'dynamic')\n let node: VobsNode | null | undefined | false\n try {\n node = owner.run(factory)\n } catch (error) {\n owner.dispose()\n throw error\n }\n if (!node) {\n owner.dispose()\n return null\n }\n associateNodeOwner(node, owner)\n return node\n}\n\nexport function associateNodeOwner(node: VobsNode, owner: Owner): void {\n nodeOwners.set(node, owner)\n}\n\nexport function disposeNodeOwner(node: VobsNode): void {\n const owner = nodeOwners.get(node)\n if (!owner) return\n nodeOwners.delete(node)\n owner.dispose()\n}\n","import type { VobsNode } from './fragment'\n\nexport type HmrComponent<Props extends object = Record<string, unknown>> =\n (props: Props) => VobsNode\n\nexport interface HmrStateStore {\n get<T>(key: string, initial: T | (() => T)): T\n set<T>(key: string, value: T): void\n has(key: string): boolean\n delete(key: string): void\n clear(): void\n}\n\nexport interface HmrInstance {\n node: VobsNode\n parent: Node | null\n refresh(): void\n}\n\ninterface HmrModuleState {\n readonly components: Map<string, HmrComponent>\n readonly state: Map<string, unknown>\n readonly instances: Set<HmrInstance>\n}\n\ninterface HmrGlobal {\n modules: Map<string, HmrModuleState>\n /** 编译器生成的 hmrStateRef 注册表:键为 `${moduleId}#${声明名}`,值跨模块重执行保活。 */\n states: Map<string, unknown>\n}\n\nconst globalTarget = globalThis as typeof globalThis & { __VOBS_HMR__?: HmrGlobal }\nconst hmrGlobal = globalTarget.__VOBS_HMR__ ?? { modules: new Map<string, HmrModuleState>(), states: new Map<string, unknown>() }\nglobalTarget.__VOBS_HMR__ = hmrGlobal\n\nexport function resolveComponent<Props extends object>(\n component: HmrComponent<Props>,\n moduleId: string,\n exportName: string\n): HmrComponent<Props> {\n const module = getModule(moduleId)\n const existing = module.components.get(exportName)\n if (existing) return existing as HmrComponent<Props>\n\n const proxy = ((props: Props) => {\n const current = (proxy as HmrComponent<Props> & { current: HmrComponent<Props> }).current\n return current(props)\n }) as HmrComponent<Props> & { current: HmrComponent<Props> }\n proxy.current = component\n Object.defineProperties(proxy, {\n displayName: { configurable: true, value: component.name || exportName },\n hmrKey: { configurable: false, value: `${moduleId}:${exportName}` }\n })\n module.components.set(exportName, proxy as HmrComponent)\n return proxy\n}\n\nexport function updateHmrModule(moduleId: string, nextModule: Record<string, unknown>): void {\n const module = hmrGlobal.modules.get(moduleId)\n if (!module) return\n let changed = false\n for (const [name, proxy] of module.components) {\n const next = nextModule[name]\n if (typeof next !== 'function') continue\n const hmrProxy = proxy as HmrComponent & { current: HmrComponent; displayName?: string }\n hmrProxy.current = next as HmrComponent\n Object.defineProperty(hmrProxy, 'displayName', { configurable: true, value: next.name || name })\n changed = true\n }\n if (!changed) return\n // 快照后正序遍历:实例在自身渲染开始前注册,祖先必然先于后代入列——\n // 祖先先刷新会 dispose 上一轮渲染作用域,其树内旧后代实例的 refresh\n // 命中已销毁守卫被跳过,避免同一组件在一次热更新中重渲染两次。\n // 同时 refresh 重渲染以新代码创建的全新实例不在快照中,天然不会再刷新。\n const instances = [...module.instances]\n for (const instance of instances) {\n try {\n instance.refresh()\n } catch {\n // HMR failures remain application errors on the next normal render.\n }\n }\n}\n\nexport function disposeHmrModule(_moduleId: string): void {\n // State and component proxies intentionally survive module disposal.\n}\n\nexport function createHmrStateStore(moduleId: string): HmrStateStore {\n const state = getModule(moduleId).state\n return {\n get<T>(key: string, initial: T | (() => T)): T {\n if (!state.has(key)) state.set(key, typeof initial === 'function' ? (initial as () => T)() : initial)\n return state.get(key) as T\n },\n set<T>(key: string, value: T): void {\n state.set(key, value)\n },\n has: key => state.has(key),\n delete: key => { state.delete(key) },\n clear: () => { state.clear() }\n }\n}\n\n/**\n * 编译器为模块顶层 state() 声明生成的取值入口。键为 `${moduleId}#${声明名}`,\n * 与手动 store 键空间隔离。模块热更新重执行时复用既有信号实例:旧导入方持有的\n * 实例与新模块实例共享同一份状态,消除\"两份模块、两份状态\"导致的编辑不生效/页面半边失灵。\n */\nexport function hmrStateRef<T>(key: string, create: () => T): T {\n const states = hmrGlobal.states\n if (states.has(key)) return states.get(key) as T\n const value = create()\n states.set(key, value)\n return value\n}\n\nexport function registerHmrInstance(moduleId: string, instance: HmrInstance): () => void {\n const instances = getModule(moduleId).instances\n instances.add(instance)\n return () => instances.delete(instance)\n}\n\nexport function markHmrInstanceMounted(node: VobsNode, parent: Node): void {\n const instance = hmrInstances.get(node as object)\n if (instance) instance.parent = parent\n}\n\nfunction getModule(moduleId: string): HmrModuleState {\n let module = hmrGlobal.modules.get(moduleId)\n if (!module) {\n module = { components: new Map(), state: new Map(), instances: new Set() }\n hmrGlobal.modules.set(moduleId, module)\n }\n return module\n}\n\nconst hmrInstances = new WeakMap<object, HmrInstance>()\n\nexport function associateHmrInstance(node: VobsNode, instance: HmrInstance): void {\n hmrInstances.set(node as object, instance)\n}\n","import { getCurrentOwner } from '@vobs/reactivity'\n\n/** A mutable reference populated when a host node is mounted. */\nexport interface Ref<T extends object = Node> {\n current: T | null\n}\n\nexport type RefTarget<T extends object = Node> = Ref<T> | ((value: T | null) => void)\n\nexport function ref<T extends object = Node>(initialValue: T | null = null): Ref<T> {\n return { current: initialValue }\n}\n\n/** Bind a host node to an object or callback ref and clear it with its Owner. */\nexport function setRef<T extends object>(node: T, target: unknown): void {\n if (!isRefTarget<T>(target)) return\n // SSR's serializable nodes are never exposed as live refs. Custom renderers\n // may use arbitrary host objects, so only exclude the known SSR shape.\n if (isSSRNode(node)) return\n const owner = getCurrentOwner()\n assignRef(target, node)\n owner?.onDispose(() => {\n // Do not clear a ref that has since been reassigned to another node.\n if (isObjectRef(target) && target.current !== node) return\n assignRef(target, null)\n })\n}\n\nfunction isSSRNode(value: object): boolean {\n const type = (value as { type?: unknown }).type\n return (type === 'element' || type === 'text' || type === 'comment')\n && !('nodeType' in value)\n}\n\nfunction isObjectRef<T extends object>(value: unknown): value is Ref<T> {\n return Boolean(value && typeof value === 'object' && 'current' in value)\n}\n\nfunction isRefTarget<T extends object>(value: unknown): value is RefTarget<T> {\n return isObjectRef<T>(value) || typeof value === 'function'\n}\n\nfunction assignRef<T extends object>(target: RefTarget<T>, value: T | null): void {\n try {\n if (typeof target === 'function') target(value)\n else target.current = value\n } catch {\n // Ref callbacks are user code; never make mounting fail because of them.\n }\n}\n"],"mappings":";;;;AAAA,SAAS,mBAAAA,wBAAuB;;;ACEhC,SAAS,aAAa,mBAAAC,kBAAiB,mBAAmB,eAA2B;;;AC6BrF,IAAM,eAAe;AACrB,IAAM,YAAY,aAAa,gBAAgB,EAAE,SAAS,oBAAI,IAA4B,GAAG,QAAQ,oBAAI,IAAqB,EAAE;AAChI,aAAa,eAAe;;;ACjC5B,SAAS,uBAAuB;;;AFgBhC,IAAI,kBAA0C;AAmBvC,SAAS,cAA+B;AAC7C,MAAI,CAAC,iBAAiB;AACpB,UAAM,IAAI,MAAM,4CAAS;AAAA,EAC3B;AACA,SAAO;AACT;AALgB;AA2CT,SAAS,cAAc,SAA0B;AACtD,SAAO,YAAY,EAAE,cAAc,OAAO;AAC5C;AAFgB;;;ADhET,SAAS,eAAe,SAAwC;AACrE,QAAM,QAAQ,cAAc,qBAAqB;AACjD,QAAM,MAAM,cAAc,mBAAmB;AAC7C,MAAI,SAAsB;AAC1B,MAAI,cAAc;AAClB,QAAM,QAAQC,iBAAgB;AAE9B,QAAM,WAAyB;AAAA,IAC7B,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,MAAM,YAAY,QAAc;AAC9B,UAAI,UAAU,WAAW,YAAY;AACnC,cAAM,IAAI,MAAM,0EAAkC;AAAA,MACpD;AAEA,UAAI,aAAa;AACf,kBAAU,YAAY,OAAO,KAAK,MAAM;AACxC;AAAA,MACF;AAEA,YAAM,WAAW,YAAY;AAC7B,eAAS,aAAa,YAAY,OAAO,MAAM;AAC/C,eAAS,aAAa,YAAY,KAAK,MAAM;AAC7C,eAAS;AACT,oBAAc;AACd,UAAI,MAAO,OAAM,IAAI,MAAM,QAAQ,YAAY,GAAG,CAAC;AAAA,UAC9C,SAAQ,YAAY,GAAG;AAAA,IAC9B;AAAA,IACA,QAAQ,YAAkB;AACxB,UAAI,CAAC,eAAe,WAAW,YAAY;AACzC,cAAM,IAAI,MAAM,0EAAkC;AAAA,MACpD;AACA,YAAM,WAAW,YAAY;AAC7B,UAAI,UAAU,SAAS,YAAY,KAAK;AACxC,aAAO,WAAW,YAAY,KAAK;AACjC,cAAM,OAAO,SAAS,YAAY,OAAO;AACzC,iBAAS,YAAY,YAAY,OAAO;AACxC,kBAAU;AAAA,MACZ;AACA,eAAS,YAAY,YAAY,KAAK;AACtC,eAAS,YAAY,YAAY,GAAG;AACpC,eAAS;AACT,oBAAc;AAAA,IAChB;AAAA,EACF;AACA,SAAO;AACT;AA/CgB;AAiDT,SAAS,eAAe,OAAuC;AACpE,SAAO,QAAQ,KAAK,KAAK,OAAO,UAAU,YAAa,MAAuB,SAAS;AACzF;AAFgB;AAIhB,SAAS,UAAU,QAAc,OAAa,KAAW,QAA2B;AAClF,QAAM,WAAW,YAAY;AAC7B,QAAM,QAAgB,CAAC,KAAK;AAC5B,MAAI,UAAU,SAAS,YAAY,KAAK;AACxC,SAAO,SAAS;AACd,UAAM,KAAK,OAAO;AAClB,QAAI,YAAY,IAAK;AACrB,cAAU,SAAS,YAAY,OAAO;AAAA,EACxC;AACA,MAAI,MAAM,MAAM,SAAS,CAAC,MAAM,KAAK;AACnC,UAAM,IAAI,MAAM,2DAAwB;AAAA,EAC1C;AACA,aAAW,QAAQ,MAAO,UAAS,aAAa,QAAQ,MAAM,MAAM;AACtE;AAbS;","names":["getCurrentOwner","getCurrentOwner","getCurrentOwner"]}
|
|
1
|
+
{"version":3,"sources":["../src/fragment.ts","../src/ops.ts","../src/hmr.ts","../src/ref.ts"],"sourcesContent":["import { getCurrentOwner } from '@vobs/reactivity'\nimport { createComment, getRenderer } from './ops'\n\nexport interface VobsFragment {\n readonly kind: 'vobs-fragment'\n readonly start: Node\n readonly end: Node\n readonly mount: (parent: Node, anchor: Node | null) => void\n readonly unmount: (parent: Node) => void\n}\n\nexport type VobsNode = Node | VobsFragment\nexport type FragmentFactory = (parent: Node, anchor: Node) => void\n\nexport function createFragment(factory: FragmentFactory): VobsFragment {\n const start = createComment('vobs:fragment:start')\n const end = createComment('vobs:fragment:end')\n let parent: Node | null = null\n let initialized = false\n const owner = getCurrentOwner()\n\n const fragment: VobsFragment = {\n kind: 'vobs-fragment',\n start,\n end,\n mount(nextParent, anchor): void {\n if (parent && parent !== nextParent) {\n throw new Error('Vobs Fragment: 不能跨父节点移动 Fragment')\n }\n\n if (initialized) {\n moveRange(nextParent, start, end, anchor)\n return\n }\n\n const renderer = getRenderer()\n renderer.insertBefore(nextParent, start, anchor)\n renderer.insertBefore(nextParent, end, anchor)\n parent = nextParent\n initialized = true\n if (owner) owner.run(() => factory(nextParent, end))\n else factory(nextParent, end)\n },\n unmount(nextParent): void {\n if (!initialized || parent !== nextParent) {\n throw new Error('Vobs Fragment: Fragment 不属于指定父节点')\n }\n const renderer = getRenderer()\n let current = renderer.nextSibling(start)\n while (current && current !== end) {\n const next = renderer.nextSibling(current)\n renderer.removeChild(nextParent, current)\n current = next\n }\n renderer.removeChild(nextParent, start)\n renderer.removeChild(nextParent, end)\n parent = null\n initialized = false\n }\n }\n return fragment\n}\n\nexport function isVobsFragment(value: unknown): value is VobsFragment {\n return Boolean(value) && typeof value === 'object' && (value as VobsFragment).kind === 'vobs-fragment'\n}\n\nfunction moveRange(parent: Node, start: Node, end: Node, anchor: Node | null): void {\n const renderer = getRenderer()\n const nodes: Node[] = [start]\n let current = renderer.nextSibling(start)\n while (current) {\n nodes.push(current)\n if (current === end) break\n current = renderer.nextSibling(current)\n }\n if (nodes[nodes.length - 1] !== end) {\n throw new Error('Vobs Fragment: 找不到结束锚点')\n }\n for (const node of nodes) renderer.insertBefore(parent, node, anchor)\n}\n","// 编译产物调用的基础操作\n\nimport { createOwner, effect, getCurrentOwner, setOwnerDebugName, untrack, type Owner } from '@vobs/reactivity'\nimport { isVobsFragment, type VobsNode } from './fragment'\nimport { domAttributeName, isPropertyName } from './dom-props'\nimport { resolveEventName } from './dom-events'\nimport { isSvgTag } from './svg'\nimport { formatVobsError } from './error'\nimport { describeDebugNode, getRuntimeDebugHooks, invokeRuntimeDebug, readDebugValue } from './debug'\nimport {\n associateHmrInstance,\n markHmrInstanceMounted,\n registerHmrInstance,\n type HmrInstance\n} from './hmr'\nimport type { VobsRenderer } from './renderer'\nimport { setRef } from './ref'\n\ntype RuntimeRenderer = VobsRenderer<Node, Text, Element, Comment>\n\nlet currentRenderer: RuntimeRenderer | null = null\nconst nodeOwners = new WeakMap<object, Owner>()\ninterface EventBinding {\n readonly handler: EventListener\n readonly owner: Owner | null\n readonly original: EventListener\n}\nconst eventBindings = new WeakMap<object, Map<string, EventBinding>>()\n\n/*\n * 事件清理槽。目的:让**每个 (Owner, node, event) 只在 `Owner.cleanups` 里占一个槽**,\n * 重绑 handler 时只换槽里的间接引用,数组不增长(理由见 addEventListener)。\n *\n * 用 WeakMap<Owner, Map<key, handle>> 而不是往 Owner 上挂属性:\n * Owner 是 class 实例,挂新属性会把它推进字典模式(reactivity/src/owner.ts:30-50 记过这笔账)。\n * key 由 per-node 数字 id 与事件名拼成,避免每次重绑比较长字符串前缀。\n */\nconst eventCleanupSlots = new WeakMap<Owner, Map<string, EventBinding>>()\nconst eventCleanupNodeIds = new WeakMap<object, number>()\nlet nextEventCleanupNodeId = 0\n\nfunction eventCleanupKey(node: object, event: string): string {\n let id = eventCleanupNodeIds.get(node)\n if (id === undefined) {\n id = ++nextEventCleanupNodeId\n eventCleanupNodeIds.set(node, id)\n }\n return `${id}:${event}`\n}\n\nexport function setRenderer<\n NodeType,\n TextNode extends NodeType,\n ElementNode extends NodeType,\n CommentNode extends NodeType\n>(\n renderer: VobsRenderer<NodeType, TextNode, ElementNode, CommentNode> | undefined\n): VobsRenderer<NodeType, TextNode, ElementNode, CommentNode> | undefined {\n // 返回**上一份**渲染器(没有则是 undefined),调用方据此成对\"安装 / 还原\"。\n // 全局单例原来只装不还:一次 renderToString 之后就永久停在 SSR 渲染器上(见 vobs/src/app.ts)。\n const previous = currentRenderer as unknown as\n VobsRenderer<NodeType, TextNode, ElementNode, CommentNode> | undefined\n // 编译产物仍使用 DOM 节点声明;实际宿主类型由应用提供的渲染器决定。\n currentRenderer = (renderer as unknown as RuntimeRenderer) ?? null\n return previous\n}\n\nexport function getRenderer(): RuntimeRenderer {\n if (!currentRenderer) {\n throw new Error('渲染器未初始化')\n }\n return currentRenderer\n}\n\nexport function createText(content: string): Text {\n return getRenderer().createText(content)\n}\n\n/** SVG 命名空间与标签清单见 ./svg —— 编译期判定提升用的是同一份依据。 */\nexport { SVG_NAMESPACE } from './svg'\n\nexport function createElement(tag: string): Element {\n // SVG 标签走命名空间创建:document.createElement('rect') 产物是 HTMLUnknownElement,\n // 整棵 SVG 子树都不会渲染(无报错)。渲染器未实现 createSvgElement 时按 createElement\n // 兜底(旧自定义渲染器保持原行为)。\n if (isSvgTag(tag)) {\n const renderer = getRenderer()\n if (renderer.createSvgElement) return renderer.createSvgElement(tag)\n }\n return getRenderer().createElement(tag)\n}\n\n/**\n * 在 SVG 命名空间里建元素,**不看标签名**。\n *\n * 给编译器用在 `<svg>` 祖先下那些与 HTML 同名的标签(a / title / style / script)——\n * 它们不在 SVG_TAGS 里,`createElement` 会按 HTML 建。只按名字判定命名空间是行不通的:\n * `<svg><a href=\"…\">` 会变成 HTML 锚点,而且不报错。\n *\n * 渲染器没实现 createSvgElement 时按 createElement 兜底(自定义渲染器保持原行为)。\n */\nexport function createSvgElement(tag: string): Element {\n const renderer = getRenderer()\n if (renderer.createSvgElement) return renderer.createSvgElement(tag)\n return renderer.createElement(tag)\n}\n\nexport function createComment(content: string): Comment {\n return getRenderer().createComment(content)\n}\n\nexport function insertBefore(\n parent: Node,\n child: VobsNode,\n anchor: VobsNode | null\n): void {\n if (isVobsFragment(child)) {\n child.mount(parent, isVobsFragment(anchor) ? anchor.start : anchor)\n markHmrInstanceMounted(child, parent)\n return\n }\n getRenderer().insertBefore(parent, child, isVobsFragment(anchor) ? anchor.start : anchor)\n markHmrInstanceMounted(child, parent)\n const nodeName = (child as Element).nodeName\n if (nodeName === 'OPTION' || nodeName === 'OPTGROUP') syncSelectValue(parent)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'insert',\n target: describeDebugNode(child),\n parent: describeDebugNode(parent)\n })\n }\n}\n\n/**\n * Select initial-value auto resync: `bindProperty(node, 'value', …)` registers the value\n * reader for `<select>` elements; whenever an `<option>` is inserted into the select\n * (directly or through an `<optgroup>`), the current bound value is re-applied. This\n * removes the last reason for apps to keep `ref + queueMicrotask` value-sync workarounds\n * for options that arrive after the value binding (e.g. asynchronously loaded lists).\n */\nconst selectValueReaders = new WeakMap<object, () => unknown>()\n\nexport function registerSelectValueBinding(select: Element, read: () => unknown): void {\n selectValueReaders.set(select, read)\n}\n\nfunction syncSelectValue(parent: Node): void {\n let current: Node | null = parent\n while (current !== null) {\n if ((current as Element).nodeName === 'SELECT') {\n const read = selectValueReaders.get(current)\n /*\n * 读取必须 `untrack` —— 这里是在**插入 option 的过程中同步**重放当前值,\n * 而插入往往发生在某个 effect 运行时(`insertList` / `insertDynamic` 都由 effect 驱动)。\n * 若那次读取被算进外层 effect 的依赖,该 effect 以后会被这个信号唤醒:\n * 一个**隐藏订阅** —— 依赖关系图上多一条没人打算建立的边,\n * 表现为\"插入选项时恰好重跑了某个不相关的 effect\",很难追。\n *\n * 这条绑定的订阅由 `bindProperty` 自己的 effect 负责(bind.ts),\n * 这里的重放只是把值**再写一次**,不需要也不应该建立依赖。\n */\n if (read !== undefined) setProperty(current as Element, 'value', untrack(read))\n return\n }\n current = current.parentNode\n }\n}\n\nexport function removeChild(\n parent: Node,\n child: VobsNode\n): void {\n /*\n * **先摘 DOM,再释放 Owner。**\n *\n * 原来是反过来的:`disposeNodeOwner(child)` 抛错(用户 cleanup 里抛)就永远走不到\n * 摘除那一步 —— DOM 里留着旧节点、它的 effect 却已经没了,屏幕上出现两棵树且没有提示。\n * 顺序反过来之后,即使清理抛错,DOM 也已一致;错误照旧抛给调用方。\n */\n if (isVobsFragment(child)) {\n child.unmount(parent)\n disposeNodeOwner(child)\n return\n }\n getRenderer().removeChild(parent, child)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'remove',\n target: describeDebugNode(child),\n parent: describeDebugNode(parent)\n })\n }\n disposeNodeOwner(child)\n}\n\nexport function setTextContent(\n node: Text,\n content: string\n): void {\n const previousValue = getRuntimeDebugHooks()\n ? readDebugValue(() => node.textContent)\n : undefined\n getRenderer().setTextContent(node, content)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'text',\n target: describeDebugNode(node),\n previousValue,\n nextValue: content\n })\n }\n}\n\nexport function setProperty(\n node: Element,\n key: string,\n value: unknown\n): void {\n const previousValue = getRuntimeDebugHooks()\n ? readDebugValue(() => Reflect.get(node, key))\n : undefined\n getRenderer().setProperty(node, PROPERTY_IDL_NAMES[key] ?? key, value)\n if (key === 'value' && (node as { tagName?: unknown }).tagName === 'SELECT') {\n scheduleSelectValueSync(node, value)\n }\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'property',\n target: describeDebugNode(node),\n key,\n previousValue,\n nextValue: value\n })\n }\n}\n\nconst pendingSelectValues = new WeakMap<Element, unknown>()\nconst selectSyncScheduled = new WeakSet<Element>()\n\n/**\n * `<select value>` 在 option 子节点存在前赋值不生效(HTML 规范:select 的 value\n * 由已存在的 option 决定)。编译产物先设置属性、后插入子节点,静态写法必然丢初始值\n * (消费方此前只能用 ref + queueMicrotask 规避)。\n * 这里对 select 的 value 赋值统一延迟到微任务重放一次:静态子节点在同一同步任务内\n * 插入完毕,重放即命中。动态(insertList/insertDynamic)插入的 option 晚于该微任务时,\n * 由 value 的绑定 effect 在后续信号更新中正常覆盖。\n */\nfunction scheduleSelectValueSync(node: Element, value: unknown): void {\n pendingSelectValues.set(node, value)\n if (selectSyncScheduled.has(node)) return\n selectSyncScheduled.add(node)\n /*\n * 在这一刻就把渲染器抓下来,**不要**在微任务里再 getRenderer():\n * 1) 应用销毁会把全局渲染器还原(没装过就是\"没有\"),那时微任务里的 getRenderer() 会抛\n * \"渲染器未初始化\" —— 抛在微任务里没人接,直接变成进程级 unhandled error(实测\n * `packages/ui/src/bind.test.ts` 就因为这条让 vitest 报 1 个 unhandled error,退出码非 0);\n * 2) 语义上也更对:节点是哪个渲染器造的,就用哪个渲染器写回(期间全局可能已经换人)。\n */\n const renderer = getRenderer()\n queueMicrotask(() => {\n selectSyncScheduled.delete(node)\n if (!pendingSelectValues.has(node)) return\n const pending = pendingSelectValues.get(node)\n pendingSelectValues.delete(node)\n renderer.setProperty(node, 'value', pending)\n })\n}\n\nexport function setAttribute(\n node: Element,\n key: string,\n value: string\n): void {\n const previousValue = getRuntimeDebugHooks()\n ? readDebugValue(() => typeof node.getAttribute === 'function' ? node.getAttribute(key) : undefined)\n : undefined\n getRenderer().setAttribute(node, key, value)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'attribute',\n target: describeDebugNode(node),\n key,\n previousValue,\n nextValue: value\n })\n }\n}\n\nexport function spreadProps(node: Element, props: Record<string, unknown>): void {\n applySpreadProps(node, null, props)\n}\n\n/** 删除一个 attribute。渲染器没实现时退回空串(属性仍在,但内容清空)—— 老自定义渲染器的降级行为。 */\nexport function removeAttribute(node: Element, key: string): void {\n const renderer = getRenderer()\n if (renderer.removeAttribute) {\n renderer.removeAttribute(node, key)\n return\n }\n renderer.setAttribute(node, key, '')\n}\n\n/** 布尔型 property 的「清除」值是 false,其余是空串(与 React 的移除语义一致)。 */\nconst BOOLEAN_PROPERTIES = new Set([\n 'checked', 'selected', 'disabled', 'multiple', 'readOnly', 'required',\n 'hidden', 'open', 'indeterminate', 'defaultChecked', 'muted', 'autoFocus'\n])\n\n/**\n * JSX 属性名 → **IDL 属性名**(`el[name] = value` 里的那个 name)。\n *\n * 多数名字两边相同,但少数不同;写错名字的后果是**静默的**:\n * `Reflect.set(el, 'autoFocus', …)` 只是给元素挂了一个不反射的 expando,\n * 既不产生 `autofocus` 属性、也不改变 IDL 属性 —— 于是 `autoFocus={false}` 毫无作用。\n * 实测(jsdom,与真实 DOM 同语义):\n * IDL 名 `autofocus`(全小写)→ has=true/idl=true,写 false 则 has=false/idl=false ← 反射正确\n * JSX 名 `autoFocus`(驼峰) → 只挂 expando,属性与 IDL 都不变\n * 这也解释了为什么 `disabled` / `checked` / `readOnly` / `tabIndex` 一直是对的:\n * 它们的 IDL 名恰好与 JSX 名相同。\n */\nconst PROPERTY_IDL_NAMES: Readonly<Record<string, string>> = {\n autoFocus: 'autofocus'\n}\n\n/**\n * 把一份 props 应用(或**增量**应用到)节点上。\n *\n * `previous` 为 null 表示首次应用(`spreadProps` 的老路径);否则只处理真正变化的键,\n * 并**移除**新对象里已经消失的键 —— 这正是原来缺的那一半:`{...props}` 只在创建时应用一次,\n * 之后对象里删掉的键会永远留在 DOM 上,而且改了的值也不会生效。\n */\nfunction applySpreadProps(\n node: Element,\n previous: Record<string, unknown> | null,\n next: Record<string, unknown>\n): void {\n for (const [key, value] of Object.entries(next)) {\n if (key === 'key') continue\n if (previous !== null && Object.is(previous[key], value)) continue\n\n if (key.startsWith('on')) {\n // 事件:先摘旧监听(值变了、或新值不再是函数),再挂新的。\n // 事件名必须过别名表:`onDoubleClick` 的真实事件名是 `dblclick`,\n // 直接小写会得到不存在的 `\"doubleclick\"`(回调永不触发且静默)。\n const eventName = resolveEventName(key) ?? key.slice(2).toLowerCase()\n const previousHandler = previous?.[key]\n if (typeof previousHandler === 'function') {\n removeEventListener(node, eventName, previousHandler as EventListener)\n }\n if (typeof value === 'function') {\n addEventListener(node, eventName, value as EventListener)\n }\n continue\n }\n if (key === 'ref') {\n setRef(node, value)\n continue\n }\n // null/undefined 表示\"不提供值\",**保持现状**(props.test.ts 把这条钉成了契约)。\n // 注意它有一个已知后果:键仍在 `next` 里时移除循环够不着(`key in next`),\n // 所以\"值从中变成 null\"不会清掉已有属性 —— 要清就得把键从对象里删掉。\n if (value === null || value === undefined) continue\n // property 键的 false 有语义(如 disabled={false} 必须清除),不能跳过;\n // attribute 键的 false 表示\"不设置\",与 HTML 语义一致。\n else if (isClassListKey(key)) applyClassList(node, value)\n else if (isPropertyName(key)) setProperty(node, key, value)\n else if (value === false) continue\n else setAttribute(node, domAttributeName(key), key === 'style' && isStyleObject(value) ? formatStyle(value) : String(value))\n }\n\n if (previous === null) return\n for (const key of Object.keys(previous)) {\n if (key in next || key === 'key') continue\n if (key.startsWith('on')) {\n const eventName = resolveEventName(key) ?? key.slice(2).toLowerCase()\n const previousHandler = previous[key]\n if (typeof previousHandler === 'function') {\n removeEventListener(node, eventName, previousHandler as EventListener)\n }\n continue\n }\n if (key === 'ref') continue\n if (isPropertyName(key)) setProperty(node, key, BOOLEAN_PROPERTIES.has(key) ? false : '')\n else removeAttribute(node, domAttributeName(key))\n }\n}\n\n/**\n * 把 `{...props}` 绑定成**响应式**的:对象变了就增量应用,消失的键会被移除。\n *\n * 编译器对带展开的 JSX 属性发射这个而不是 `spreadProps` —— 后者只在创建时应用一次,\n * 于是「改了 props 不生效」「删掉的键留在 DOM 上」两个问题都**不报错**。\n *\n * ⚠️ `previous` 必须是**值的快照**,不能是 `next` 的引用。\n * 这里存引用时,下一轮 `applySpreadProps` 里的 `Object.is(previous[key], value)`\n * 读的是同一份值 → 恒等 → **整个 diff 短路**,移除循环也因 `key in next` 全跳过。\n * 而编译器为 `<div {...props}>` 发射的正是**身份稳定 + getter**的形状\n * (`bindSpreadProps(_el0, () => props)`,值经 getter 反应式求值),\n * 于是真实产物下\"改了 props 什么都不更新\"。\n *\n * 快照用浅拷贝即可,且**必须在应用之前取**:拷贝会把 getter 求值一次、转成数据属性,\n * 而 `applySpreadProps` 又会把 `next` 的每个键读一遍 —— 两者都在 effect 的追踪窗口内,\n * 所以信号依赖不会因为多这一遍而丢失。\n */\n/**\n * `classList` 每次贡献的类名(按元素记账,便于下次替换而不是叠加)。\n *\n * 为什么需要记账:`classList` 与 `class` 写的是**同一个** HTML 属性。\n * 若不记住上次由 `classList` 贡献了哪些,切换时要么越加越多、要么把作者的 `class` 抹掉。\n */\nconst classListContributions = new WeakMap<object, string[]>()\n\n/** `classList` 值 → 类名数组。支持对象 / 数组 / 字符串三种写法。 */\nexport function parseClassList(value: unknown): string[] {\n const names: string[] = []\n const push = (candidate: unknown): void => {\n if (typeof candidate === 'string') {\n for (const name of candidate.split(/\\s+/u)) if (name !== '') names.push(name)\n }\n }\n if (Array.isArray(value)) {\n for (const entry of value) {\n if (entry !== null && typeof entry === 'object') {\n for (const [name, on] of Object.entries(entry)) if (on) push(name)\n } else push(entry)\n }\n } else if (value !== null && typeof value === 'object') {\n for (const [name, on] of Object.entries(value)) if (on) push(name)\n } else push(value)\n return names\n}\n\n/**\n * 应用 `classList`:与元素上已有的 `class` **合并**,并可反复切换。\n *\n * 存在的理由(外部踩坑文档 C 条):条件分支里的输入控件会丢焦点 ——\n * `insertDynamic` 按引用比较(`next === current`),写条件表达式就会重建子树。\n * 文档给出的对策是「固定渲染 + 响应式 class 显隐」,但此前只能手写字符串模板:\n *\n * ```tsx\n * <div class={open.value ? 'panel is-open' : 'panel'}> // 易漏、易写错\n * <div class=\"panel\" classList={{ 'is-open': open.value }}> // 现在:\n * ```\n *\n * `classList` 只负责它自己贡献的部分,作者写在 `class` 里的类名不受影响。\n */\nexport function applyClassList(node: Element, value: unknown): void {\n const next = parseClassList(value)\n const previous = classListContributions.get(node) ?? []\n classListContributions.set(node, next)\n const base = (node.getAttribute('class') ?? '')\n .split(/\\s+/u)\n .filter(name => name !== '' && !previous.includes(name))\n const merged = [...new Set([...base, ...next])]\n setAttribute(node, 'class', merged.join(' '))\n}\n\nfunction isClassListKey(key: string): boolean {\n return key === 'classList'\n}\n\nexport function bindSpreadProps(node: Element, source: () => Record<string, unknown>): void {\n let previous: Record<string, unknown> | null = null\n effect(() => {\n const next = source() ?? {}\n const snapshot = { ...next }\n applySpreadProps(node, previous, next)\n previous = snapshot\n })\n}\n\n/** Apply compile-time host properties in one renderer pass. */\nexport function setStaticProps(node: Element, props: Record<string, unknown>): void {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'key' || key === 'ref' || key.startsWith('on')) continue\n if (value === null || value === undefined) continue\n if (isClassListKey(key)) applyClassList(node, value)\n else if (isPropertyName(key)) setProperty(node, key, value)\n else if (value === false) continue\n else setAttribute(node, domAttributeName(key), key === 'style' && isStyleObject(value) ? formatStyle(value) : String(value))\n }\n}\n\n\nfunction isStyleObject(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction formatStyle(value: Record<string, unknown>): string {\n return Object.entries(value).filter(([, entry]) => entry !== null && entry !== undefined && entry !== false)\n .map(([key, entry]) => `${key.replace(/[A-Z]/gu, match => `-${match.toLowerCase()}`)}:${String(entry)}`).join(';')\n}\n\nexport function addEventListener(\n node: Element,\n event: string,\n handler: EventListener\n): void {\n const renderer = getRenderer()\n const owner = getCurrentOwner()\n let bindings = eventBindings.get(node)\n if (!bindings) {\n bindings = new Map()\n eventBindings.set(node, bindings)\n }\n const previous = bindings.get(event)\n if (previous && previous.original === handler && previous.owner === owner) return\n if (previous) {\n // 旧监听先摘掉;Owner 清理槽由下面的 slots 原地续用,不在清理表里增删\n renderer.removeEventListener(node, event, previous.handler)\n }\n const listener = owner ? (reason: Event) => {\n // Owner 已销毁说明节点所属子树已被卸载/替换,事件来自游离 DOM,直接忽略。\n // 否则 owner.run 会抛\"已销毁的 Owner\",在事件流里制造无意义的错误噪音。\n if (owner.disposed) return\n try {\n owner.run(() => handler(reason))\n } catch (error) {\n const handled = owner.handleError(error)\n invokeRuntimeDebug('error', {\n error,\n owner,\n phase: 'event',\n handled,\n recovery: handled ? 'handled' : 'propagated'\n })\n /*\n * 被 owner 的 onError 处理掉之后,错误就只剩 debug 钩子这一个出口 ——\n * 没装 DevTools 时控制台一个字都没有:事件处理器抛错、界面没反应、无从查起。\n * 没被处理的情况下面会 rethrow(浏览器自己会报),所以只在「被吞下」时补控制台。\n */\n if (handled && getRuntimeDebugHooks()?.error === undefined) {\n console.error(formatVobsError(error, { includeStack: true }))\n }\n if (!handled) throw error\n }\n } : handler\n /*\n * 这个 (node,event) 在 Owner 清理表里只占**一个**槽,替换 handler 时**原地**换掉它做的事。\n *\n * 此前是在**每次** addEventListener 里无条件 `owner.onDispose(...)`,而 `Owner.cleanups`\n * 是只 push 的数组(reactivity/src/owner.ts:57,84)→ 换一次 handler 就多一条永不执行的\n * 清理项,旧的 handler 闭包也被一并扣住。实测(.artifacts/reports/runtime.supplement.md\n * 缺点 4 与既有报告缺点 1,两轮独立复现):500 次替换 → `cleanups.length === 500`;\n * 一轮 `{...props}` 换 onClick → 202。组件的 effect 每次重跑都会重绑事件,\n * 所以这是热路径上的**无界增长**(长寿命页面里等价于内存泄漏)。\n *\n * `binding` 是 `const`,所以槽里必须包一层可变的 `current` —— 换绑时改的是 `current`,\n * 数组长度不变、旧闭包被释放。\n */\n const binding: EventBinding = { handler: listener, owner, original: handler }\n bindings.set(event, binding)\n renderer.addEventListener(node, event, listener)\n if (!owner) return\n /*\n * **每个 (node,event) 在 Owner 清理表里只占一个槽。**\n *\n * 此前是在**每次** addEventListener 里无条件 `owner.onDispose(...)`,而 `Owner.cleanups`\n * 是只 push 的数组(reactivity/src/owner.ts:57,84)→ 换一次 handler 就多一条永不执行的\n * 清理项,旧的 handler 闭包也被一并扣住。实测(.artifacts/reports/runtime.supplement.md\n * 缺点 4 与既有报告缺点 1,两轮独立复现):500 次替换 → `cleanups.length === 500`;\n * 一轮 `{...props}` 换 onClick → 202。组件 effect 每次重跑都会重绑事件,\n * 所以这是热路径上的**无界增长**(长寿命页面里等价于内存泄漏)。\n *\n * 做法:清理槽里存的是**间接引用**(owner → (node,event) → 当前 handle),\n * 重绑时只换间接引用指向的东西,槽本身不增不减;最后一个引用被清掉时槽自然失效。\n * 键用 per-owner 的 `eventCleanupKeys` 映射成数字,避免长字符串前缀比较。\n */\n const key = eventCleanupKey(node, event)\n let slots = eventCleanupSlots.get(owner)\n if (!slots) {\n slots = new Map()\n eventCleanupSlots.set(owner, slots)\n }\n // 槽存的永远是\"当前该清哪一条绑定\",所以重绑/摘除后再绑都只是覆盖它\n const hasSlot = slots.has(key)\n slots.set(key, binding)\n if (hasSlot) return\n owner.onDispose(() => {\n const current = eventCleanupSlots.get(owner)?.get(key)\n if (!current) return\n eventCleanupSlots.get(owner)?.delete(key)\n const map = eventBindings.get(node)\n if (map?.get(event) === current) map.delete(event)\n renderer.removeEventListener(node, event, current.handler)\n })\n}\n\nexport function removeEventListener(\n node: Element,\n event: string,\n handler: EventListener\n): void {\n const renderer = getRenderer()\n const map = eventBindings.get(node)\n const binding = map?.get(event)\n /*\n * **必须比 handler 身份**(DOM 的 `removeEventListener` 语义就是如此)。\n *\n * 原来这里忽略传入的 `handler`:只要 `(node,event)` 上有绑定就一律摘掉,\n * 并把绑定记录删掉。于是\"用另一个 handler 调 remove\"会把**别人的**绑定误摘 ——\n * 调用方以为只是取消自己那次(本来应当是 no-op),实际把当前生效的监听弄没了。\n *\n * 现在的行为:\n * - 有绑定且身份一致 → 摘掉(正常路径)\n * - 有绑定但身份不同 → **no-op**(不去动别人的绑定)\n * - 绑定记录里没有(例如由别处直接经 renderer 绑的)→ 按传入的 handler 兜底摘一次,\n * 保持与渲染器 API 直连时的可用性\n */\n if (binding) {\n if (binding.handler !== handler) return\n map?.delete(event)\n }\n renderer.removeEventListener(node, event, binding?.handler ?? handler)\n}\n\n/**\n * 清空容器。\n *\n * 原来只调渲染器的 clear(DOM 实现就是 `textContent = ''`)—— **被清掉子树的 Owner\n * 一个都不释放**:组件里的 effect 继续订阅信号(幽灵更新)、监听不解绑、onDispose 不跑。\n * 现在先把子树里能找到的 Owner 释放掉,再清 DOM。\n *\n * 覆盖范围如实说明:`nodeOwners` 只登记**组件输出节点**(`createComponent` 时\n * `associateNodeOwner`)。所以在组件边界上清理是完整的(组件 Owner 的销毁会级联它的\n * effect 与子 Owner);而在组件外直接 `bindText` 之类建立的 effect 挂在调用方当时的\n * Owner 上、不在节点表里,这里**清不到** —— 那部分归它的 Owner 管,通常由\n * `app.destroy()` 负责。要彻底解决需要节点表能反向枚举,代价更大,先不做。\n *\n * 清理抛错不影响 DOM 被清空(先释放、后清 DOM 的顺序也保证了这一点)。\n */\nexport function clear(container: Node): void {\n let firstError: unknown\n const walk = (node: Node): void => {\n try {\n disposeNodeOwner(node as unknown as VobsNode)\n } catch (error) {\n firstError ??= error\n }\n for (const child of Array.from(node.childNodes)) walk(child)\n }\n for (const child of Array.from(container.childNodes)) walk(child)\n\n getRenderer().clear(container)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'clear',\n target: describeDebugNode(container as unknown as VobsNode),\n parent: describeDebugNode(container as unknown as VobsNode)\n })\n }\n if (firstError) throw firstError\n}\n\ntype VobsComponent = (...args: any[]) => VobsNode\n\ntype ComponentProps<Component extends VobsComponent> = Component extends (\n props: infer Props\n) => VobsNode\n ? NonNullable<Props> extends object ? NonNullable<Props> : Record<string, never>\n : Record<string, never>\n\nexport function createComponent<Component extends VobsComponent>(\n component: Component,\n props: ComponentProps<Component>,\n source?: VobsSourceLocation\n): VobsNode {\n const owner = createOwner()\n const componentName = (component as typeof component & { displayName?: string }).displayName\n || component.name\n || 'anonymous'\n setOwnerDebugName(owner, source\n ? `${componentName} (${source.file}:${source.line}:${source.column})`\n : componentName)\n owner.onError(reason => {\n attachSourceLocation(reason, source)\n attachComponentContext(reason, componentName, owner.id)\n throw reason\n })\n // HMR 注册先于渲染作用域标记:注册清理必须跨热更新保活,不能被\n // disposeSince 当作上一轮渲染的清理释放掉。\n const hmrKey = (component as typeof component & { hmrKey?: string }).hmrKey\n let instance: HmrInstance | null = null\n if (hmrKey) {\n const separator = hmrKey.lastIndexOf(':')\n const moduleId = separator < 0 ? hmrKey : hmrKey.slice(0, separator)\n instance = { node: null as unknown as VobsNode, parent: null, refresh: () => refreshInstance() }\n const cleanup = registerHmrInstance(moduleId, instance)\n owner.onDispose(cleanup)\n }\n // 渲染作用域:组件 Owner 只承载 HMR 注册与错误处理,每轮渲染注册的 effect、\n // onDispose(含 portal 清理)与嵌套组件 Owner 都归属该轮作用域。HMR refresh\n // 重渲染前释放上一轮作用域,旧实例的 effect 不再订阅信号、portal 节点不再\n // 残留在 body 中每轮热更新叠加;组件 Owner 与 HMR 注册保活。\n const renderScope = owner.mark()\n /*\n * 局部类型放宽到运行时真实值域:组件返回 `null` / `undefined` / `false`\n * 都是合法 JSX 写法(`cond ? <X/> : null`、`cond && <X/>`),\n * 只是 `VobsComponent` 的声明把它写窄了。下面统一归一化成空注释节点。\n */\n /*\n * 用**独立变量**承接渲染结果再归一化。\n * 直接写 `let node: VobsNode | null | undefined | false` 再比较 `node === false` 时,\n * TS 的控制流分析会依据 `component(props)` 的声明类型把 `node` 收窄回 `VobsNode`,\n * 于是 `false` 分支被判成\"类型不可达\"而报错 —— 但运行时它确实会出现。\n */\n let rendered: unknown\n try {\n // 组件渲染必须 untrack:组件是 run-once 的,其渲染发生在某次 effect 求值\n // (insertDynamic/insertBoundary 的渲染工厂、路由挂载)内时,若不切断追踪,\n // 组件体内读取的信号会被收集为祖先 effect 的依赖——一次无关编辑就会触发\n // 整棵子树销毁重建(输入框被换掉、焦点丢失、事件监听随旧树一起被清理)。\n // 结构性响应只属于条件工厂与绑定 effect,组件本体渲染一次即止。\n rendered = owner.run(() => untrack(() => component(props)))\n } catch (error) {\n owner.dispose()\n attachSourceLocation(error, source)\n attachComponentContext(error, componentName, owner.id)\n throw error\n }\n /*\n * 组件返回 `null` / `undefined` / `false` 时用**注释节点**代替。\n *\n * 这是生产事故(Labelune 2026-09-30 发版黑屏)的根因:`createComponent` 把渲染结果\n * 直接当作 `nodeOwners` 这个 **WeakMap 的 key**,而 `null` / `undefined` 不是合法 key ——\n * `nodeOwners.set(null, owner)` 抛 `Invalid value used as weak map key`。\n * 后果特别贵:App 挂载即崩、**没有任何 JS 崩溃日志**、splash 兜底也失效,\n * 只能靠人肉纪律「组件永远返回真实节点」避免。\n *\n * 「组件顶层条件 return null」是合法的 JSX 写法,框架不该崩。\n * 注释节点是**渲染为空的既有原语**(fragment 与 dynamic 的标记都用它),\n * 视觉结果与返回 null 完全一致,但满足 `VobsNode` 契约 —— 于是挂载、HMR 刷新、\n * 节点替换、水合这些下游路径都不需要各自特判一个\"空\"分支。\n */\n const node: VobsNode = isRenderableNode(rendered) ? rendered : createComment('vobs:empty')\n associateNodeOwner(node, owner)\n if (instance) {\n instance.node = node\n associateHmrInstance(node, instance)\n }\n function refreshInstance(): void {\n // 组件已随旧渲染树一起卸载(如父级在同一次热更新中先完成刷新),\n // 无法也无需再刷新。\n if (owner.disposed) return\n const previous = instance!.node\n owner.disposeSince(renderScope)\n const next = owner.run(() => untrack(() => component(props)))\n const parent = instance!.parent\n if (parent) {\n // 先插入新树再卸载旧树:替换锚点始终取自仍在文档中的旧树,\n // fragment 与普通节点两种形态任意组合都能正确定位插入点。\n const anchor = isVobsFragment(previous) ? previous.start : previous\n if (isVobsFragment(next)) next.mount(parent, anchor)\n else getRenderer().insertBefore(parent, next, anchor)\n if (isVobsFragment(previous)) previous.unmount(parent)\n else getRenderer().removeChild(parent, previous)\n }\n nodeOwners.delete(previous as object)\n nodeOwners.set(next as object, owner)\n associateHmrInstance(next, instance!)\n instance!.node = next\n }\n return node\n}\n\nexport interface VobsSourceLocation {\n readonly file: string\n readonly line: number\n readonly column: number\n}\n\nexport interface VobsLocatedError extends Error {\n readonly vobsSource?: VobsSourceLocation\n readonly vobsComponent?: string\n readonly vobsOwnerId?: string\n}\n\nfunction attachSourceLocation(reason: unknown, source: VobsSourceLocation | undefined): void {\n if (!source || (!reason || (typeof reason !== 'object' && typeof reason !== 'function'))) return\n const error = reason as VobsLocatedError\n if (error.vobsSource) return\n try {\n Object.defineProperty(error, 'vobsSource', {\n configurable: true,\n enumerable: false,\n value: source,\n writable: false\n })\n } catch {\n // Frozen third-party errors still propagate with their original details.\n }\n}\n\nfunction attachComponentContext(reason: unknown, component: string, ownerId: string): void {\n if (!reason || (typeof reason !== 'object' && typeof reason !== 'function')) return\n const error = reason as VobsLocatedError\n try {\n if (!error.vobsComponent) Object.defineProperty(error, 'vobsComponent', { configurable: true, enumerable: false, value: component, writable: false })\n if (!error.vobsOwnerId) Object.defineProperty(error, 'vobsOwnerId', { configurable: true, enumerable: false, value: ownerId, writable: false })\n } catch {\n // Frozen third-party errors still propagate with their original details.\n }\n}\n\nexport function createBlock(factory: () => VobsNode | null | undefined | false): VobsNode | null {\n const owner = createOwner()\n setOwnerDebugName(owner, 'dynamic')\n let node: VobsNode | null | undefined | false\n try {\n node = owner.run(factory)\n } catch (error) {\n owner.dispose()\n throw error\n }\n if (!node) {\n owner.dispose()\n return null\n }\n associateNodeOwner(node, owner)\n return node\n}\n\n/**\n * 组件渲染结果是否是**可用的节点值**。\n *\n * `VobsNode` 的真实值域是一个对象(`Node` / fragment / 自定义渲染器的宿主对象),\n * 而合法 JSX 写法可能返回 `null` / `undefined` / `false`(`cond ? <X/> : null`、`cond && <X/>`)。\n * 用一个守卫而不是直接比较,是因为 `component()` 的声明类型是 `VobsNode`,\n * TS 的控制流会把 `=== false` 之类的比较判成\"类型不可达\"。\n */\nfunction isRenderableNode(value: unknown): value is VobsNode {\n return (typeof value === 'object' && value !== null) || typeof value === 'function'\n}\n\nexport function associateNodeOwner(node: VobsNode, owner: Owner): void {\n /*\n * 防御:`node` 在类型上必然是 `VobsNode`,但运行时可能收到 `null`/`undefined`\n * (组件返回空、第三方渲染器返回非节点)。直接当 WeakMap key 会抛\n * `Invalid value used as weak map key` —— 那是**框架内部错误**,信息量为零,\n * 且会让 App 挂载静默崩溃。这条断言把它变成一条能指认原因的报错。\n */\n if (node === null || node === undefined || (typeof node !== 'object' && typeof node !== 'function')) {\n throw new Error(\n `Vobs: associateNodeOwner 收到非节点值(${String(node)})。`\n + '组件必须返回 VobsNode;返回 null/undefined 由 createComponent 转成空注释节点。'\n )\n }\n nodeOwners.set(node, owner)\n}\n\nexport function disposeNodeOwner(node: VobsNode): void {\n const owner = nodeOwners.get(node)\n if (!owner) return\n nodeOwners.delete(node)\n owner.dispose()\n}\n","import type { VobsNode } from './fragment'\nimport { formatVobsError } from './error'\n\nexport type HmrComponent<Props extends object = Record<string, unknown>> =\n (props: Props) => VobsNode\n\nexport interface HmrStateStore {\n get<T>(key: string, initial: T | (() => T)): T\n set<T>(key: string, value: T): void\n has(key: string): boolean\n delete(key: string): void\n clear(): void\n}\n\nexport interface HmrInstance {\n node: VobsNode\n parent: Node | null\n refresh(): void\n}\n\ninterface HmrModuleState {\n readonly components: Map<string, HmrComponent>\n readonly state: Map<string, unknown>\n readonly instances: Set<HmrInstance>\n}\n\ninterface HmrGlobal {\n modules: Map<string, HmrModuleState>\n /** 编译器生成的 hmrStateRef 注册表:键为 `${moduleId}#${声明名}`,值跨模块重执行保活。 */\n states: Map<string, unknown>\n}\n\nconst globalTarget = globalThis as typeof globalThis & { __VOBS_HMR__?: HmrGlobal }\nconst hmrGlobal = globalTarget.__VOBS_HMR__ ?? { modules: new Map<string, HmrModuleState>(), states: new Map<string, unknown>() }\nglobalTarget.__VOBS_HMR__ = hmrGlobal\n\nexport function resolveComponent<Props extends object>(\n component: HmrComponent<Props>,\n moduleId: string,\n exportName: string\n): HmrComponent<Props> {\n const module = getModule(moduleId)\n const existing = module.components.get(exportName)\n if (existing) return existing as HmrComponent<Props>\n\n const proxy = ((props: Props) => {\n const current = (proxy as HmrComponent<Props> & { current: HmrComponent<Props> }).current\n return current(props)\n }) as HmrComponent<Props> & { current: HmrComponent<Props> }\n proxy.current = component\n Object.defineProperties(proxy, {\n displayName: { configurable: true, value: component.name || exportName },\n hmrKey: { configurable: false, value: `${moduleId}:${exportName}` }\n })\n module.components.set(exportName, proxy as HmrComponent)\n return proxy\n}\n\nexport function updateHmrModule(moduleId: string, nextModule: Record<string, unknown>): void {\n const module = hmrGlobal.modules.get(moduleId)\n if (!module) return\n let changed = false\n for (const [name, proxy] of module.components) {\n const next = nextModule[name]\n if (typeof next !== 'function') continue\n const hmrProxy = proxy as HmrComponent & { current: HmrComponent; displayName?: string }\n hmrProxy.current = next as HmrComponent\n Object.defineProperty(hmrProxy, 'displayName', { configurable: true, value: next.name || name })\n changed = true\n }\n if (!changed) return\n // 快照后正序遍历:实例在自身渲染开始前注册,祖先必然先于后代入列——\n // 祖先先刷新会 dispose 上一轮渲染作用域,其树内旧后代实例的 refresh\n // 命中已销毁守卫被跳过,避免同一组件在一次热更新中重渲染两次。\n // 同时 refresh 重渲染以新代码创建的全新实例不在快照中,天然不会再刷新。\n const instances = [...module.instances]\n for (const instance of instances) {\n try {\n instance.refresh()\n } catch (error) {\n /*\n * 静默吞掉会让开发者以为热更新成功了,而屏幕上是旧的 —— 最容易被当成\n * 「改了没生效」查半天。刷新失败本身不冒泡(下一次正常渲染会再抛),\n * 但必须留下记录。\n */\n console.error(formatVobsError(error, { includeStack: true }))\n }\n }\n}\n\nexport function disposeHmrModule(_moduleId: string): void {\n // State and component proxies intentionally survive module disposal.\n}\n\nexport function createHmrStateStore(moduleId: string): HmrStateStore {\n const state = getModule(moduleId).state\n return {\n get<T>(key: string, initial: T | (() => T)): T {\n if (!state.has(key)) state.set(key, typeof initial === 'function' ? (initial as () => T)() : initial)\n return state.get(key) as T\n },\n set<T>(key: string, value: T): void {\n state.set(key, value)\n },\n has: key => state.has(key),\n delete: key => { state.delete(key) },\n clear: () => { state.clear() }\n }\n}\n\n/**\n * 编译器为模块顶层 state() 声明生成的取值入口。键为 `${moduleId}#${声明名}`,\n * 与手动 store 键空间隔离。模块热更新重执行时复用既有信号实例:旧导入方持有的\n * 实例与新模块实例共享同一份状态,消除\"两份模块、两份状态\"导致的编辑不生效/页面半边失灵。\n */\nexport function hmrStateRef<T>(key: string, create: () => T): T {\n const states = hmrGlobal.states\n if (states.has(key)) return states.get(key) as T\n const value = create()\n states.set(key, value)\n return value\n}\n\nexport function registerHmrInstance(moduleId: string, instance: HmrInstance): () => void {\n const instances = getModule(moduleId).instances\n instances.add(instance)\n return () => instances.delete(instance)\n}\n\nexport function markHmrInstanceMounted(node: VobsNode, parent: Node): void {\n const instance = hmrInstances.get(node as object)\n if (instance) instance.parent = parent\n}\n\nfunction getModule(moduleId: string): HmrModuleState {\n let module = hmrGlobal.modules.get(moduleId)\n if (!module) {\n module = { components: new Map(), state: new Map(), instances: new Set() }\n hmrGlobal.modules.set(moduleId, module)\n }\n return module\n}\n\nconst hmrInstances = new WeakMap<object, HmrInstance>()\n\nexport function associateHmrInstance(node: VobsNode, instance: HmrInstance): void {\n hmrInstances.set(node as object, instance)\n}\n","import { getCurrentOwner, type Owner } from '@vobs/reactivity'\nimport { formatVobsError } from './error'\n\n/** A mutable reference populated when a host node is mounted. */\nexport interface Ref<T extends object = Node> {\n current: T | null\n}\n\nexport type RefTarget<T extends object = Node> = Ref<T> | ((value: T | null) => void)\n\nexport function ref<T extends object = Node>(initialValue: T | null = null): Ref<T> {\n return { current: initialValue }\n}\n\n/*\n * 每个 (Owner, ref 目标) 只保留**一条**清理注册。\n *\n * 为什么需要:`ref` 会被先后绑到不同节点(同一个 ref 对象、节点被替换;\n * 或条件分支里换了宿主元素)。若每次都 `onDispose` 追加一条,销毁时按注册**逆序**执行,\n * 后注册的那条先把 `target.current` 清成 `null`,于是先注册那条的守卫\n * (`isObjectRef(target) && target.current !== node`)**恰好通过**,再清一次 ——\n * 最终读到 `null` 而不是当前节点。\n *\n * 用 WeakMap<Owner, WeakMap<target, cleanup>> 而不是往 Owner 上挂属性:\n * Owner 是 class 实例,挂新属性会把它推进字典模式(reactivity/src/owner.ts 记过这笔代价)。\n * target 可弱引用(对象),函数型 ref 也可弱引用,所以不阻止回收。\n */\nconst refCleanups = new WeakMap<Owner, WeakMap<object, () => void>>()\n\nfunction rememberRefCleanup(owner: Owner, target: object, cleanup: () => void): void {\n let byTarget = refCleanups.get(owner)\n if (!byTarget) {\n byTarget = new WeakMap()\n refCleanups.set(owner, byTarget)\n }\n const previous = byTarget.get(target)\n if (previous) owner.removeCleanup(previous)\n byTarget.set(target, cleanup)\n}\n\n/** Bind a host node to an object or callback ref and clear it with its Owner. */\nexport function setRef<T extends object>(node: T, target: unknown): void {\n if (!isRefTarget<T>(target)) return\n // SSR's serializable nodes are never exposed as live refs. Custom renderers\n // may use arbitrary host objects, so only exclude the known SSR shape.\n if (isSSRNode(node)) return\n const owner = getCurrentOwner()\n assignRef(target, node)\n if (!owner) return\n const cleanup = (): void => {\n /*\n * 清除 ref 时**必须自己吞掉异常**,不能让 user 的 ref 回调把 dispose 级联打断。\n *\n * `assignRef` 内部已经 try/catch + `console.error`(回调是用户代码,不该让挂载失败),\n * 但那条保护只覆盖\"赋值\"这一步 —— 而这里还有一个**可能抛错的前置判断**。\n * 按 React 语义,函数型 ref 在卸载时**本来就该收到 `null`**;若回调没判空\n * (如 `node => insertList(node, …)`),它会抛,而异常从 onDispose 冒出去会\n * **中断 Owner 的整个清理循环**(实测:/runtime 页面的 `<ul ref={attachList}>`)。\n * 这与 owner.ts 已修的\"子 Owner 抛错不能中断级联\"是同一类缺陷。\n */\n try {\n // Do not clear a ref that has since been reassigned to another node.\n if (isObjectRef(target) && target.current !== node) return\n assignRef(target, null)\n } catch (error) {\n console.error(formatVobsError(error, { includeStack: true }))\n }\n }\n rememberRefCleanup(owner, target as object, cleanup)\n owner.onDispose(cleanup)\n}\n\nfunction isSSRNode(value: object): boolean {\n const type = (value as { type?: unknown }).type\n return (type === 'element' || type === 'text' || type === 'comment')\n && !('nodeType' in value)\n}\n\nfunction isObjectRef<T extends object>(value: unknown): value is Ref<T> {\n return Boolean(value && typeof value === 'object' && 'current' in value)\n}\n\nfunction isRefTarget<T extends object>(value: unknown): value is RefTarget<T> {\n return isObjectRef<T>(value) || typeof value === 'function'\n}\n\nfunction assignRef<T extends object>(target: RefTarget<T>, value: T | null): void {\n try {\n if (typeof target === 'function') target(value)\n else target.current = value\n } catch (error) {\n /*\n * ref 回调是用户代码,挂载不该因为它失败 —— 但也不能**完全无声**:\n * 回调抛错时 ref 拿不到节点,界面看起来正常,用户完全不知道为什么。\n * 保持吞掉(不冒泡),但打一条控制台记录。\n */\n console.error(formatVobsError(error, { includeStack: true }))\n }\n}\n"],"mappings":";;;;AAAA,SAAS,mBAAAA,wBAAuB;;;ACEhC,SAAS,aAAa,QAAQ,mBAAAC,kBAAiB,mBAAmB,eAA2B;;;AC8B7F,IAAM,eAAe;AACrB,IAAM,YAAY,aAAa,gBAAgB,EAAE,SAAS,oBAAI,IAA4B,GAAG,QAAQ,oBAAI,IAAqB,EAAE;AAChI,aAAa,eAAe;;;AClC5B,SAAS,uBAAmC;;;AFoB5C,IAAI,kBAA0C;AA+CvC,SAAS,cAA+B;AAC7C,MAAI,CAAC,iBAAiB;AACpB,UAAM,IAAI,MAAM,4CAAS;AAAA,EAC3B;AACA,SAAO;AACT;AALgB;AAwCT,SAAS,cAAc,SAA0B;AACtD,SAAO,YAAY,EAAE,cAAc,OAAO;AAC5C;AAFgB;;;AD7FT,SAAS,eAAe,SAAwC;AACrE,QAAM,QAAQ,cAAc,qBAAqB;AACjD,QAAM,MAAM,cAAc,mBAAmB;AAC7C,MAAI,SAAsB;AAC1B,MAAI,cAAc;AAClB,QAAM,QAAQC,iBAAgB;AAE9B,QAAM,WAAyB;AAAA,IAC7B,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,MAAM,YAAY,QAAc;AAC9B,UAAI,UAAU,WAAW,YAAY;AACnC,cAAM,IAAI,MAAM,0EAAkC;AAAA,MACpD;AAEA,UAAI,aAAa;AACf,kBAAU,YAAY,OAAO,KAAK,MAAM;AACxC;AAAA,MACF;AAEA,YAAM,WAAW,YAAY;AAC7B,eAAS,aAAa,YAAY,OAAO,MAAM;AAC/C,eAAS,aAAa,YAAY,KAAK,MAAM;AAC7C,eAAS;AACT,oBAAc;AACd,UAAI,MAAO,OAAM,IAAI,MAAM,QAAQ,YAAY,GAAG,CAAC;AAAA,UAC9C,SAAQ,YAAY,GAAG;AAAA,IAC9B;AAAA,IACA,QAAQ,YAAkB;AACxB,UAAI,CAAC,eAAe,WAAW,YAAY;AACzC,cAAM,IAAI,MAAM,0EAAkC;AAAA,MACpD;AACA,YAAM,WAAW,YAAY;AAC7B,UAAI,UAAU,SAAS,YAAY,KAAK;AACxC,aAAO,WAAW,YAAY,KAAK;AACjC,cAAM,OAAO,SAAS,YAAY,OAAO;AACzC,iBAAS,YAAY,YAAY,OAAO;AACxC,kBAAU;AAAA,MACZ;AACA,eAAS,YAAY,YAAY,KAAK;AACtC,eAAS,YAAY,YAAY,GAAG;AACpC,eAAS;AACT,oBAAc;AAAA,IAChB;AAAA,EACF;AACA,SAAO;AACT;AA/CgB;AAiDT,SAAS,eAAe,OAAuC;AACpE,SAAO,QAAQ,KAAK,KAAK,OAAO,UAAU,YAAa,MAAuB,SAAS;AACzF;AAFgB;AAIhB,SAAS,UAAU,QAAc,OAAa,KAAW,QAA2B;AAClF,QAAM,WAAW,YAAY;AAC7B,QAAM,QAAgB,CAAC,KAAK;AAC5B,MAAI,UAAU,SAAS,YAAY,KAAK;AACxC,SAAO,SAAS;AACd,UAAM,KAAK,OAAO;AAClB,QAAI,YAAY,IAAK;AACrB,cAAU,SAAS,YAAY,OAAO;AAAA,EACxC;AACA,MAAI,MAAM,MAAM,SAAS,CAAC,MAAM,KAAK;AACnC,UAAM,IAAI,MAAM,2DAAwB;AAAA,EAC1C;AACA,aAAW,QAAQ,MAAO,UAAS,aAAa,QAAQ,MAAM,MAAM;AACtE;AAbS;","names":["getCurrentOwner","getCurrentOwner","getCurrentOwner"]}
|