@tschk/moonshine-vue 0.0.0-stage → 0.4.4
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 +298 -2
- package/package.json +57 -4
- package/src/app.ts +97 -0
- package/src/compiler-sfc.ts +1 -0
- package/src/context.ts +59 -0
- package/src/error-context.ts +57 -0
- package/src/errors.ts +24 -0
- package/src/events.ts +86 -0
- package/src/framework.ts +1 -0
- package/src/index.ts +42 -0
- package/src/lifecycle.ts +59 -0
- package/src/normalize.ts +111 -0
- package/src/props.ts +471 -0
- package/src/reactive.ts +390 -0
- package/src/render.ts +365 -0
- package/src/shims.d.ts +17 -0
- package/src/slots.ts +82 -0
- package/src/text.ts +56 -0
- package/src/vue.ts +11 -0
package/src/render.ts
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/** Compiler-private optional Vue defaults component scheduling. */
|
|
2
|
+
import { collectDeps, type Trackable } from "@tschk/moonshine/tracking";
|
|
3
|
+
import {
|
|
4
|
+
createComponentScope,
|
|
5
|
+
createRoot,
|
|
6
|
+
createSignal,
|
|
7
|
+
getOwner,
|
|
8
|
+
getRenderScheduler,
|
|
9
|
+
onCleanup,
|
|
10
|
+
runWithOwner,
|
|
11
|
+
untrack,
|
|
12
|
+
type Owner,
|
|
13
|
+
} from "@tschk/moonshine-dom/reactive";
|
|
14
|
+
import {
|
|
15
|
+
renderSchedulerKey,
|
|
16
|
+
type RenderScheduler,
|
|
17
|
+
} from "@tschk/moonshine-dom/render-scheduler";
|
|
18
|
+
import { isSSRRendering } from "@tschk/moonshine-dom/ssr-owner";
|
|
19
|
+
const groupKey = /* @__PURE__ */ Symbol("moonshine.vue.render-group");
|
|
20
|
+
type Order = {
|
|
21
|
+
parent?: Order;
|
|
22
|
+
next: number;
|
|
23
|
+
index: number;
|
|
24
|
+
read?: () => number;
|
|
25
|
+
};
|
|
26
|
+
type Job = {
|
|
27
|
+
owner: Owner;
|
|
28
|
+
order: Order;
|
|
29
|
+
phase: "pre" | "structural" | "render";
|
|
30
|
+
run: () => void;
|
|
31
|
+
pass: number;
|
|
32
|
+
};
|
|
33
|
+
type Group = {
|
|
34
|
+
order?: Order;
|
|
35
|
+
id: number;
|
|
36
|
+
owner: Owner;
|
|
37
|
+
jobs: Set<Job>;
|
|
38
|
+
posts: Set<() => void>;
|
|
39
|
+
blocked: boolean;
|
|
40
|
+
preparing: number;
|
|
41
|
+
pass: number;
|
|
42
|
+
};
|
|
43
|
+
let running: Group | undefined;
|
|
44
|
+
let nextId = 0,
|
|
45
|
+
flushing = false,
|
|
46
|
+
scheduled = false;
|
|
47
|
+
const pending = /* @__PURE__ */ new Set<Group>();
|
|
48
|
+
export function currentGroup(): Group | undefined {
|
|
49
|
+
return getOwner()?.context?.get(groupKey) as Group | undefined;
|
|
50
|
+
}
|
|
51
|
+
const alive = (g: Group) => !g.owner.disposed;
|
|
52
|
+
function enqueue(g: Group) {
|
|
53
|
+
if (!alive(g) || g.blocked) return;
|
|
54
|
+
pending.add(g);
|
|
55
|
+
if (!scheduled && !flushing) {
|
|
56
|
+
scheduled = true;
|
|
57
|
+
queueMicrotask(flush);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function compare(a: Order, b: Order) {
|
|
61
|
+
const path = (n: Order) => {
|
|
62
|
+
const p: Order[] = [];
|
|
63
|
+
for (let v: Order | undefined = n; v; v = v.parent) p.unshift(v);
|
|
64
|
+
return p;
|
|
65
|
+
};
|
|
66
|
+
const x = path(a),
|
|
67
|
+
y = path(b);
|
|
68
|
+
let i = 0;
|
|
69
|
+
while (x[i] && x[i] === y[i]) i++;
|
|
70
|
+
if (!x[i] || !y[i]) return x.length - y.length;
|
|
71
|
+
return untrack(
|
|
72
|
+
() => (x[i]!.read?.() ?? x[i]!.index) - (y[i]!.read?.() ?? y[i]!.index),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
function drain(callbacks: Iterable<() => void>) {
|
|
76
|
+
let failed = false,
|
|
77
|
+
error: unknown;
|
|
78
|
+
for (const cb of callbacks)
|
|
79
|
+
try {
|
|
80
|
+
cb();
|
|
81
|
+
} catch (e) {
|
|
82
|
+
if (!failed) error = e;
|
|
83
|
+
failed = true;
|
|
84
|
+
}
|
|
85
|
+
if (failed) throw error;
|
|
86
|
+
}
|
|
87
|
+
function flush() {
|
|
88
|
+
if (flushing) return;
|
|
89
|
+
scheduled = false;
|
|
90
|
+
flushing = true;
|
|
91
|
+
let failed = false,
|
|
92
|
+
error: unknown;
|
|
93
|
+
const counts = new Map<Group, number>();
|
|
94
|
+
const postCounts = new Map<Group, number>();
|
|
95
|
+
try {
|
|
96
|
+
do {
|
|
97
|
+
while (pending.size) {
|
|
98
|
+
// oxlint-disable-next-line unicorn/no-array-sort -- sort an already fresh snapshot.
|
|
99
|
+
const g = [...pending].sort(
|
|
100
|
+
(a, b) => compare(a.order!, b.order!) || a.id - b.id,
|
|
101
|
+
)[0]!;
|
|
102
|
+
pending.delete(g);
|
|
103
|
+
if (!alive(g) || g.blocked || g.preparing) continue;
|
|
104
|
+
const count = (counts.get(g) ?? 0) + 1;
|
|
105
|
+
counts.set(g, count);
|
|
106
|
+
try {
|
|
107
|
+
if (count > 1000) throw Error("Vue render update limit exceeded");
|
|
108
|
+
running = g;
|
|
109
|
+
g.pass++;
|
|
110
|
+
const ran = new Set<Job>();
|
|
111
|
+
while (true) {
|
|
112
|
+
const jobs = [...g.jobs]
|
|
113
|
+
.filter(
|
|
114
|
+
(j) => !j.owner.disposed && !ran.has(j) && j.pass !== g.pass,
|
|
115
|
+
)
|
|
116
|
+
// oxlint-disable-next-line unicorn/no-array-sort -- sort an already fresh filtered snapshot.
|
|
117
|
+
.sort(
|
|
118
|
+
(a, b) =>
|
|
119
|
+
(a.phase === "pre" ? 0 : a.phase === "structural" ? 1 : 2) -
|
|
120
|
+
(b.phase === "pre"
|
|
121
|
+
? 0
|
|
122
|
+
: b.phase === "structural"
|
|
123
|
+
? 1
|
|
124
|
+
: 2) || compare(a.order, b.order),
|
|
125
|
+
);
|
|
126
|
+
const job = jobs[0];
|
|
127
|
+
if (!job || !alive(g) || g.blocked) break;
|
|
128
|
+
ran.add(job);
|
|
129
|
+
job.pass = g.pass;
|
|
130
|
+
job.run();
|
|
131
|
+
}
|
|
132
|
+
} catch (e) {
|
|
133
|
+
if (!failed) error = e;
|
|
134
|
+
failed = true;
|
|
135
|
+
} finally {
|
|
136
|
+
running = undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Child groups post before their live ancestors; callback registration order within group.
|
|
140
|
+
// oxlint-disable-next-line unicorn/no-array-sort -- sort an already fresh snapshot.
|
|
141
|
+
const posts = [...postGroups].sort((a, b) => {
|
|
142
|
+
const ancestor = (x: Order, y: Order) => {
|
|
143
|
+
for (let v: Order | undefined = y; v; v = v.parent)
|
|
144
|
+
if (v === x) return true;
|
|
145
|
+
return false;
|
|
146
|
+
};
|
|
147
|
+
return ancestor(a.order!, b.order!)
|
|
148
|
+
? 1
|
|
149
|
+
: ancestor(b.order!, a.order!)
|
|
150
|
+
? -1
|
|
151
|
+
: compare(a.order!, b.order!) || a.id - b.id;
|
|
152
|
+
});
|
|
153
|
+
postGroups.clear();
|
|
154
|
+
for (const g of posts) {
|
|
155
|
+
const callbacks = [...g.posts];
|
|
156
|
+
g.posts.clear();
|
|
157
|
+
try {
|
|
158
|
+
const count = (postCounts.get(g) ?? 0) + 1;
|
|
159
|
+
postCounts.set(g, count);
|
|
160
|
+
if (count > 1000) throw Error("Vue post update limit exceeded");
|
|
161
|
+
drain(callbacks);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
if (!failed) error = e;
|
|
164
|
+
failed = true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} while (pending.size || postGroups.size);
|
|
168
|
+
} finally {
|
|
169
|
+
flushing = false;
|
|
170
|
+
if (pending.size || postGroups.size) {
|
|
171
|
+
scheduled = true;
|
|
172
|
+
queueMicrotask(flush);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (failed) throw error;
|
|
176
|
+
}
|
|
177
|
+
const postGroups = /* @__PURE__ */ new Set<Group>();
|
|
178
|
+
export function prepareProps<T>(group: Group | undefined, fn: () => T): T {
|
|
179
|
+
if (!group) return fn();
|
|
180
|
+
group.preparing++;
|
|
181
|
+
let success = false;
|
|
182
|
+
try {
|
|
183
|
+
const value = fn();
|
|
184
|
+
success = true;
|
|
185
|
+
return value;
|
|
186
|
+
} finally {
|
|
187
|
+
group.preparing--;
|
|
188
|
+
if (alive(group)) {
|
|
189
|
+
group.blocked = !success;
|
|
190
|
+
if (success) enqueue(group);
|
|
191
|
+
else pending.delete(group);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function install<T>(
|
|
196
|
+
fn: () => T,
|
|
197
|
+
parent?: Order,
|
|
198
|
+
existing?: Group,
|
|
199
|
+
read?: () => number,
|
|
200
|
+
): T {
|
|
201
|
+
const owner = getOwner();
|
|
202
|
+
if (!owner || owner.disposed)
|
|
203
|
+
throw Error("Vue scheduler requires live owner");
|
|
204
|
+
const group = existing ?? {
|
|
205
|
+
id: nextId++,
|
|
206
|
+
owner,
|
|
207
|
+
jobs: new Set<Job>(),
|
|
208
|
+
posts: new Set<() => void>(),
|
|
209
|
+
blocked: false,
|
|
210
|
+
preparing: 0,
|
|
211
|
+
pass: 0,
|
|
212
|
+
};
|
|
213
|
+
const order: Order = {
|
|
214
|
+
parent,
|
|
215
|
+
next: 0,
|
|
216
|
+
index: parent ? parent.next++ : 0,
|
|
217
|
+
read,
|
|
218
|
+
};
|
|
219
|
+
if (!existing) group.order = order;
|
|
220
|
+
const before = owner.context;
|
|
221
|
+
owner.context = new Map(before);
|
|
222
|
+
owner.context.set(groupKey, group);
|
|
223
|
+
owner.context.set(renderSchedulerKey, carrier(group, order));
|
|
224
|
+
if (!existing)
|
|
225
|
+
onCleanup(() => {
|
|
226
|
+
pending.delete(group);
|
|
227
|
+
postGroups.delete(group);
|
|
228
|
+
group.posts.clear();
|
|
229
|
+
});
|
|
230
|
+
return fn();
|
|
231
|
+
}
|
|
232
|
+
type VueCarrier = RenderScheduler & { [groupKey]: Group };
|
|
233
|
+
function carrier(group: Group, parent: Order): VueCarrier {
|
|
234
|
+
return {
|
|
235
|
+
[groupKey]: group,
|
|
236
|
+
effect(fn, initial) {
|
|
237
|
+
let previous = initial;
|
|
238
|
+
registerJob(group, parent, "render", () => {
|
|
239
|
+
previous = fn(previous);
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
memo: ((fn: any, initial: any, options: any) => {
|
|
243
|
+
const [read, write] = createSignal(initial, options);
|
|
244
|
+
registerJob(group, parent, "structural", () =>
|
|
245
|
+
write(() => fn(untrack(read))),
|
|
246
|
+
);
|
|
247
|
+
return read;
|
|
248
|
+
}) as RenderScheduler["memo"],
|
|
249
|
+
component(fn) {
|
|
250
|
+
return install(fn, parent);
|
|
251
|
+
},
|
|
252
|
+
branch(fn, order) {
|
|
253
|
+
return install(fn, parent, group, order);
|
|
254
|
+
},
|
|
255
|
+
reorder() {
|
|
256
|
+
if (running !== group) enqueue(group);
|
|
257
|
+
},
|
|
258
|
+
pre(fn) {
|
|
259
|
+
registerJob(group, parent, "pre", () => {
|
|
260
|
+
const stop = fn();
|
|
261
|
+
if (typeof stop === "function") onCleanup(stop);
|
|
262
|
+
});
|
|
263
|
+
},
|
|
264
|
+
post(fn) {
|
|
265
|
+
group.posts.add(fn);
|
|
266
|
+
postGroups.add(group);
|
|
267
|
+
if (!scheduled && !flushing) {
|
|
268
|
+
scheduled = true;
|
|
269
|
+
queueMicrotask(flush);
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function registerJob(
|
|
275
|
+
group: Group,
|
|
276
|
+
parent: Order,
|
|
277
|
+
phase: Job["phase"],
|
|
278
|
+
fn: () => void,
|
|
279
|
+
) {
|
|
280
|
+
const owner = createComponentScope(() => getOwner()!);
|
|
281
|
+
const order: Order = { parent, next: 0, index: parent.next++ };
|
|
282
|
+
let stopRun: (() => void) | undefined;
|
|
283
|
+
let subscriptions = new Map<Trackable, () => void>();
|
|
284
|
+
const value: Job = { owner, order, phase, run, pass: group.pass };
|
|
285
|
+
group.jobs.add(value);
|
|
286
|
+
function close() {
|
|
287
|
+
group.jobs.delete(value);
|
|
288
|
+
const old = stopRun;
|
|
289
|
+
stopRun = undefined;
|
|
290
|
+
const callbacks = [...subscriptions.values(), ...(old ? [old] : [])];
|
|
291
|
+
subscriptions.clear();
|
|
292
|
+
drain(callbacks);
|
|
293
|
+
}
|
|
294
|
+
function run() {
|
|
295
|
+
if (owner.disposed || !alive(group)) return;
|
|
296
|
+
const old = stopRun;
|
|
297
|
+
stopRun = undefined;
|
|
298
|
+
old?.();
|
|
299
|
+
if (owner.disposed) return;
|
|
300
|
+
let found: Trackable[] = [];
|
|
301
|
+
const versions = new Map<Trackable, number>();
|
|
302
|
+
runWithOwner(owner, () =>
|
|
303
|
+
createRoot((stop) => {
|
|
304
|
+
const scope = getOwner()!;
|
|
305
|
+
stopRun = stop;
|
|
306
|
+
scope.context = new Map(scope.context);
|
|
307
|
+
scope.context.set(renderSchedulerKey, carrier(group, order));
|
|
308
|
+
createComponentScope(() => {
|
|
309
|
+
found = collectDeps(fn, (dep) => versions.set(dep, dep.version));
|
|
310
|
+
});
|
|
311
|
+
if (!owner.disposed) {
|
|
312
|
+
scope.parent = owner;
|
|
313
|
+
owner.children.add(scope);
|
|
314
|
+
}
|
|
315
|
+
}),
|
|
316
|
+
);
|
|
317
|
+
if (owner.disposed) {
|
|
318
|
+
close();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const next = new Map<Trackable, () => void>();
|
|
322
|
+
try {
|
|
323
|
+
for (const dep of found)
|
|
324
|
+
next.set(
|
|
325
|
+
dep,
|
|
326
|
+
subscriptions.get(dep) ??
|
|
327
|
+
dep.subscribe(() => {
|
|
328
|
+
if (running !== group || value.pass === group.pass)
|
|
329
|
+
enqueue(group);
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
332
|
+
} catch (e) {
|
|
333
|
+
try {
|
|
334
|
+
drain(next.values());
|
|
335
|
+
close();
|
|
336
|
+
} catch {}
|
|
337
|
+
throw e;
|
|
338
|
+
}
|
|
339
|
+
const removed = [...subscriptions]
|
|
340
|
+
.filter(([dep]) => !next.has(dep))
|
|
341
|
+
.map(([, stop]) => stop);
|
|
342
|
+
subscriptions = next;
|
|
343
|
+
drain(removed);
|
|
344
|
+
if (found.some((dep) => versions.get(dep) !== dep.version)) enqueue(group);
|
|
345
|
+
}
|
|
346
|
+
runWithOwner(owner, () => onCleanup(close));
|
|
347
|
+
try {
|
|
348
|
+
run();
|
|
349
|
+
} catch (e) {
|
|
350
|
+
try {
|
|
351
|
+
close();
|
|
352
|
+
} catch {}
|
|
353
|
+
throw e;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
export function withVueScheduler<T>(fn: () => T): T {
|
|
357
|
+
if (isSSRRendering()) return fn();
|
|
358
|
+
const group = currentGroup();
|
|
359
|
+
const current = getRenderScheduler() as VueCarrier | undefined;
|
|
360
|
+
if (group && current?.[groupKey] === group && alive(group)) {
|
|
361
|
+
if (getOwner()?.disposed) throw Error("Vue scheduler requires live owner");
|
|
362
|
+
return fn();
|
|
363
|
+
}
|
|
364
|
+
return install(fn);
|
|
365
|
+
}
|
package/src/shims.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
declare module "vue" {
|
|
2
|
+
export const ref: unknown;
|
|
3
|
+
export const computed: unknown;
|
|
4
|
+
export const watch: unknown;
|
|
5
|
+
export const reactive: unknown;
|
|
6
|
+
export const onMounted: unknown;
|
|
7
|
+
export const onUnmounted: unknown;
|
|
8
|
+
export const defineComponent: unknown;
|
|
9
|
+
export const h: unknown;
|
|
10
|
+
export const createApp: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
declare module "vue/compiler-sfc" {
|
|
14
|
+
export const parse: unknown;
|
|
15
|
+
export const compileScript: unknown;
|
|
16
|
+
export const compileTemplate: unknown;
|
|
17
|
+
}
|
package/src/slots.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Compiler-private static default-slot transport; independent of declared props. */
|
|
2
|
+
import {
|
|
3
|
+
getComponentContext,
|
|
4
|
+
withComponentConsumer,
|
|
5
|
+
} from "@tschk/moonshine-dom/component";
|
|
6
|
+
import {
|
|
7
|
+
createComponentScope,
|
|
8
|
+
runWithOwner,
|
|
9
|
+
untrack,
|
|
10
|
+
} from "@tschk/moonshine-dom/reactive";
|
|
11
|
+
import type { DOMChild } from "@tschk/moonshine-dom";
|
|
12
|
+
import type { SSRChild } from "@tschk/moonshine-dom/ssr";
|
|
13
|
+
const slots = /* @__PURE__ */ Symbol("moonshine.vue.slots");
|
|
14
|
+
type Content = () => unknown;
|
|
15
|
+
/** Each authored outlet gets a distinct instance; its factory identity is static. */
|
|
16
|
+
function createSlotOutlet(
|
|
17
|
+
source: object,
|
|
18
|
+
persistentDOM: boolean,
|
|
19
|
+
): (fallback?: Content) => unknown {
|
|
20
|
+
const consumer = getComponentContext();
|
|
21
|
+
const supplied = Reflect.get(source, slots, source) as Content | undefined;
|
|
22
|
+
if (supplied !== undefined && typeof supplied !== "function")
|
|
23
|
+
throw new TypeError("Slot content must be a factory");
|
|
24
|
+
let initialized = false,
|
|
25
|
+
result: unknown;
|
|
26
|
+
return (fallback) => {
|
|
27
|
+
if (consumer.owner.disposed) return undefined;
|
|
28
|
+
if (!initialized) {
|
|
29
|
+
const invoke = () =>
|
|
30
|
+
withComponentConsumer(consumer, () =>
|
|
31
|
+
untrack(() => (supplied ? supplied() : fallback?.())),
|
|
32
|
+
);
|
|
33
|
+
// Static DOM content outlives transient insert reads; SSR must retain its current attempt.
|
|
34
|
+
result = persistentDOM
|
|
35
|
+
? runWithOwner(consumer.owner, () => createComponentScope(invoke))
|
|
36
|
+
: invoke();
|
|
37
|
+
if (consumer.owner.disposed) return undefined;
|
|
38
|
+
initialized = true;
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createDOMSlotOutlet(
|
|
45
|
+
source: object,
|
|
46
|
+
): (fallback?: () => DOMChild) => DOMChild {
|
|
47
|
+
return createSlotOutlet(source, true) as (
|
|
48
|
+
fallback?: () => DOMChild,
|
|
49
|
+
) => DOMChild;
|
|
50
|
+
}
|
|
51
|
+
export function createServerSlotOutlet(
|
|
52
|
+
source: object,
|
|
53
|
+
): (fallback?: () => SSRChild) => SSRChild {
|
|
54
|
+
return createSlotOutlet(source, false) as (
|
|
55
|
+
fallback?: () => SSRChild,
|
|
56
|
+
) => SSRChild;
|
|
57
|
+
}
|
|
58
|
+
/** Compiler-private wrapper: native h creates its one consumer scope around this function. */
|
|
59
|
+
export function withDefaultSlot<P extends object, R>(
|
|
60
|
+
component: (props: P) => R,
|
|
61
|
+
content: Content,
|
|
62
|
+
): (props: P) => R {
|
|
63
|
+
if (typeof component !== "function" || typeof content !== "function")
|
|
64
|
+
throw new TypeError("Static component and slot factories required");
|
|
65
|
+
return (source) => {
|
|
66
|
+
const target = Object.create(
|
|
67
|
+
Object.getPrototypeOf(source),
|
|
68
|
+
Object.getOwnPropertyDescriptors(source),
|
|
69
|
+
);
|
|
70
|
+
Object.defineProperty(target, slots, {
|
|
71
|
+
value: content,
|
|
72
|
+
enumerable: true,
|
|
73
|
+
configurable: true,
|
|
74
|
+
});
|
|
75
|
+
const view = new Proxy(target, {
|
|
76
|
+
get(_target, key) {
|
|
77
|
+
return key === slots ? content : Reflect.get(source, key, source);
|
|
78
|
+
},
|
|
79
|
+
}) as P;
|
|
80
|
+
return component(view);
|
|
81
|
+
};
|
|
82
|
+
}
|
package/src/text.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { isRef } from "./reactive";
|
|
2
|
+
const baseString = Object.prototype.toString;
|
|
3
|
+
const object = (value: unknown): value is Record<string, unknown> =>
|
|
4
|
+
value !== null && typeof value === "object";
|
|
5
|
+
function symbolText(value: unknown, fallback: string | number = ""): unknown {
|
|
6
|
+
return typeof value === "symbol"
|
|
7
|
+
? "Symbol(" + (value.description ?? fallback) + ")"
|
|
8
|
+
: value;
|
|
9
|
+
}
|
|
10
|
+
function jsonValue(_key: string, value: unknown): unknown {
|
|
11
|
+
if (isRef(value)) return jsonValue("", value.value);
|
|
12
|
+
if (baseString.call(value) === "[object Map]") {
|
|
13
|
+
const map = value as Map<unknown, unknown>,
|
|
14
|
+
label = `Map(${map.size})`,
|
|
15
|
+
entries: Record<string, unknown> = {};
|
|
16
|
+
let index = 0;
|
|
17
|
+
// Gather before coercing keys so iterator failures retain precedence.
|
|
18
|
+
const pairs = [...map.entries()];
|
|
19
|
+
// Concatenation deliberately preserves the default primitive hint for object keys.
|
|
20
|
+
for (const [key, item] of pairs)
|
|
21
|
+
entries[(symbolText(key, index++) as string) + " =>"] = item;
|
|
22
|
+
return { [label]: entries };
|
|
23
|
+
}
|
|
24
|
+
if (baseString.call(value) === "[object Set]") {
|
|
25
|
+
const set = value as Set<unknown>;
|
|
26
|
+
return {
|
|
27
|
+
[`Set(${set.size})`]: [...set.values()].map((item) => symbolText(item)),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === "symbol") return symbolText(value);
|
|
31
|
+
if (
|
|
32
|
+
object(value) &&
|
|
33
|
+
!Array.isArray(value) &&
|
|
34
|
+
baseString.call(value) !== "[object Object]"
|
|
35
|
+
)
|
|
36
|
+
return String(value);
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
/** Opt-in native Vue interpolation conversion; no HTML is produced. */
|
|
40
|
+
export function toDisplayString(value: unknown): string | undefined {
|
|
41
|
+
if (typeof value === "string") return value;
|
|
42
|
+
if (value == null) return "";
|
|
43
|
+
if (
|
|
44
|
+
Array.isArray(value) ||
|
|
45
|
+
(object(value) &&
|
|
46
|
+
(value.toString === baseString || typeof value.toString !== "function"))
|
|
47
|
+
) {
|
|
48
|
+
if (isRef(value)) return toDisplayString(value.value);
|
|
49
|
+
return JSON.stringify(value, jsonValue, 2);
|
|
50
|
+
}
|
|
51
|
+
return String(value);
|
|
52
|
+
}
|
|
53
|
+
/** SSR interpolation stringifies the helper result before the renderer escapes it. */
|
|
54
|
+
export function toDisplayStringSSR(value: unknown): string {
|
|
55
|
+
return String(toDisplayString(value));
|
|
56
|
+
}
|