|
1 | | -import type { Format } from "../types/format.js" |
| 1 | +import type { Format } from "../types/format.js"; |
2 | 2 |
|
3 | 3 | /** |
4 | | - * Deep-merges an ordered iterable of DTCG {@link Format} objects into a single |
5 | | - * combined token tree. Sources are applied left-to-right; later entries override |
6 | | - * earlier ones at leaf (non-object) positions, while nested groups are recursively |
7 | | - * merged so siblings from different sources are preserved. |
| 4 | + * Lazily merges an ordered array of DTCG {@link Format} objects into a single |
| 5 | + * combined token tree via a {@link Proxy}. Sources are applied left-to-right; |
| 6 | + * later entries override earlier ones at leaf (non-object) positions, while |
| 7 | + * nested groups are recursively merged so siblings from different sources are |
| 8 | + * preserved. No allocation occurs until a property is actually accessed. |
8 | 9 | * |
9 | 10 | * @example |
10 | 11 | * mergeFormats([ |
11 | 12 | * { color: { red: { $type: "color", $value: "…" } } }, |
12 | 13 | * { color: { blue: { $type: "color", $value: "…" } } }, |
13 | 14 | * ]) |
14 | | - * // → { color: { red: { … }, blue: { … } } } |
| 15 | + * // → proxy that lazily produces { color: { red: { … }, blue: { … } } } |
15 | 16 | */ |
16 | | -export const mergeFormats = (formats: Iterable<Format>): Format => { |
17 | | - const merged: Record<string, unknown> = {} |
| 17 | +export const mergeFormats = (formats: Format[]): Format => { |
| 18 | + const handler: ProxyHandler<object> = { |
| 19 | + get(_target, key) { |
| 20 | + if (typeof key !== "string") return undefined; |
18 | 21 |
|
19 | | - for (const format of formats) { |
20 | | - deepMergeInto(merged, format as Record<string, unknown>) |
21 | | - } |
| 22 | + const subFormats: Format[] = []; |
| 23 | + let leafValue: unknown; |
| 24 | + let hasLeaf = false; |
22 | 25 |
|
23 | | - return merged as Format |
24 | | -} |
| 26 | + for (const format of formats) { |
| 27 | + const val = (format as Record<string, unknown>)[key]; |
| 28 | + if (val === undefined) continue; |
25 | 29 |
|
26 | | -// ─── Internal ───────────────────────────────────────────────────────────────── |
| 30 | + if (isPlainObject(val)) { |
| 31 | + subFormats.push(val as Format); |
| 32 | + } else { |
| 33 | + // A later leaf (primitive or array) wins; discard accumulated sub-objects. |
| 34 | + subFormats.length = 0; |
| 35 | + leafValue = val; |
| 36 | + hasLeaf = true; |
| 37 | + } |
| 38 | + } |
| 39 | + |
| 40 | + if (subFormats.length > 0) return mergeFormats(subFormats); |
| 41 | + if (hasLeaf) return leafValue; |
| 42 | + return undefined; |
| 43 | + }, |
| 44 | + |
| 45 | + has(_target, key) { |
| 46 | + if (typeof key !== "string") return false; |
| 47 | + return formats.some((f) => key in (f as object)); |
| 48 | + }, |
| 49 | + |
| 50 | + ownKeys(_target) { |
| 51 | + const keys = new Set<string>(); |
| 52 | + for (const format of formats) { |
| 53 | + for (const key in format) keys.add(key); |
| 54 | + } |
| 55 | + return [...keys]; |
| 56 | + }, |
| 57 | + |
| 58 | + getOwnPropertyDescriptor(_target, key) { |
| 59 | + if (typeof key !== "string") return undefined; |
| 60 | + const exists = formats.some((f) => Object.hasOwn(f as object, key)); |
| 61 | + if (!exists) return undefined; |
| 62 | + return { configurable: true, enumerable: true, writable: false, value: undefined }; |
| 63 | + }, |
| 64 | + }; |
| 65 | + |
| 66 | + // Each call gets a fresh subclass so its prototype slot is independent. |
| 67 | + const MergedFormat = class extends NullProxy {}; |
| 68 | + return MergedFormat.from(handler) as unknown as Format; |
| 69 | +}; |
| 70 | + |
| 71 | +// ─── NullProxy ──────────────────────────────────────────────────────────────── |
27 | 72 |
|
28 | 73 | /** |
29 | | - * Recursively merges `source` into `target` in-place. |
30 | | - * Plain objects are merged deeply; all other values (arrays, primitives) replace. |
| 74 | + * A base class whose instances have no default own properties and whose |
| 75 | + * prototype chain routes all property access through a caller-supplied |
| 76 | + * {@link ProxyHandler}. Callers should subclass and call `.from(handler)` on |
| 77 | + * the subclass so each merged view gets an isolated prototype slot. |
31 | 78 | */ |
32 | | -const deepMergeInto = ( |
33 | | - target: Record<string, unknown>, |
34 | | - source: Record<string, unknown>, |
35 | | -): void => { |
36 | | - for (const [key, value] of Object.entries(source)) { |
37 | | - if (isPlainObject(value) && isPlainObject(target[key])) { |
38 | | - deepMergeInto(target[key] as Record<string, unknown>, value) |
39 | | - } else { |
40 | | - target[key] = value |
41 | | - } |
| 79 | + |
| 80 | +class NullProxy { |
| 81 | + static { |
| 82 | + // @ts-expect-error to fully nullify the prototype |
| 83 | + delete NullProxy.prototype.constructor; |
| 84 | + } |
| 85 | + |
| 86 | + static from(handler: ProxyHandler<object>): NullProxy { |
| 87 | + const proxy = new Proxy(Object.create(null) as object, handler); |
| 88 | + Object.setPrototypeOf(this.prototype as object, proxy); |
| 89 | + return Reflect.construct(this, []) as NullProxy; |
42 | 90 | } |
43 | 91 | } |
44 | 92 |
|
45 | | -const isPlainObject = (value: unknown): value is Record<string, unknown> => |
46 | | - value !== null && typeof value === "object" && !Array.isArray(value) |
| 93 | +// ─── Internal ───────────────────────────────────────────────────────────────── |
47 | 94 |
|
| 95 | +const isPlainObject = (value: unknown): value is Record<string, unknown> => |
| 96 | + value !== null && typeof value === "object" && !Array.isArray(value); |
0 commit comments