Skip to content

Commit ee010b3

Browse files
committed
checkpoint
1 parent ecd3c5c commit ee010b3

2 files changed

Lines changed: 76 additions & 36 deletions

File tree

src/loader/merge.ts

Lines changed: 67 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -39,45 +39,48 @@ export const mergeFormats = (formats: Format[], root?: RawObject, inheritedType?
3939
return ownType ?? inheritedType;
4040
};
4141

42-
// ── Define a lazy getter for every key ────────────────────────────────────
42+
// ── Define a self-memoizing getter for every key ─────────────────────────
43+
// On first access the getter computes the value, overwrites itself with a
44+
// plain data property, and is never called again. After a single traversal
45+
// the object is indistinguishable from a regular {}.
4346
for (const key of keys) {
4447
Object.defineProperty(merged, key, {
4548
get(): unknown {
4649
// $type: own value, or fall back to the inherited type.
47-
if (key === "$type") return getNodeType();
48-
49-
const subFormats: RawObject[] = [];
50-
let leafValue: unknown;
51-
let hasLeaf = false;
52-
53-
for (const format of formats) {
54-
const val = (format as RawObject)[key];
55-
if (val === undefined) continue;
56-
57-
if (isPlainObject(val)) {
58-
subFormats.push(val);
59-
} else {
60-
// A later leaf (primitive or array) wins; reset sub-objects.
61-
subFormats.length = 0;
62-
leafValue = val;
63-
hasLeaf = true;
50+
let value: unknown = key === "$type" ? getNodeType() : undefined;
51+
52+
if (value === undefined && key !== "$type") {
53+
const subFormats: RawObject[] = [];
54+
let leafValue: unknown;
55+
let hasLeaf = false;
56+
57+
for (const format of formats) {
58+
const val = (format as RawObject)[key];
59+
if (val === undefined) continue;
60+
61+
if (isPlainObject(val)) {
62+
subFormats.push(val);
63+
} else {
64+
// A later leaf (primitive or array) wins; reset sub-objects.
65+
subFormats.length = 0;
66+
leafValue = val;
67+
hasLeaf = true;
68+
}
6469
}
65-
}
66-
67-
if (subFormats.length > 0) {
68-
// Pass along the current node's effective type so children can inherit it.
69-
return mergeFormats(subFormats as Format[], effectiveRoot, getNodeType());
70-
}
7170

72-
if (hasLeaf) {
73-
// Resolve DTCG alias strings in $value lazily against the merged root.
74-
if (key === "$value" && isAlias(leafValue)) {
75-
return resolveAlias(leafValue, effectiveRoot);
71+
if (subFormats.length > 0) {
72+
// Pass along the current node's effective type so children can inherit it.
73+
value = mergeFormats(subFormats as Format[], effectiveRoot, getNodeType());
74+
} else if (hasLeaf) {
75+
// Deeply resolve aliases and $ref objects inside $value.
76+
value = key === "$value" ? resolveDeep(leafValue, effectiveRoot) : leafValue;
7677
}
77-
return leafValue;
7878
}
7979

80-
return undefined;
80+
// Replace this getter with the computed value so the object becomes
81+
// a plain data property after first access.
82+
Object.defineProperty(this, key, { value, enumerable: true, configurable: true });
83+
return value;
8184
},
8285
enumerable: true,
8386
configurable: true,
@@ -97,6 +100,9 @@ const isPlainObject = (v: unknown): v is RawObject =>
97100
const isAlias = (v: unknown): v is string =>
98101
typeof v === "string" && v.startsWith("{") && v.endsWith("}");
99102

103+
const isRef = (v: unknown): v is { $ref: string } =>
104+
isPlainObject(v) && typeof (v as RawObject).$ref === "string";
105+
100106
// Module-level cycle-detection set (safe because JS is single-threaded).
101107
const resolvingAliases = new Set<string>();
102108

@@ -122,3 +128,34 @@ const resolveAlias = (alias: string, root: RawObject): unknown => {
122128
resolvingAliases.delete(path);
123129
}
124130
};
131+
132+
/**
133+
* Resolves a JSON Reference string (e.g. `"#/base/alpha/dark/$value/components"`)
134+
* against the merged root by walking the `/`-separated path literally.
135+
* Returns `undefined` for any missing segment.
136+
*/
137+
const resolveRef = (ref: string, root: RawObject): unknown => {
138+
if (!ref.startsWith("#/")) return undefined;
139+
let node: unknown = root;
140+
for (const seg of ref.slice(2).split("/")) {
141+
if (!isPlainObject(node)) return undefined;
142+
node = (node as RawObject)[seg];
143+
}
144+
return node;
145+
};
146+
147+
/**
148+
* Recursively resolves DTCG alias strings and `$ref` objects anywhere they
149+
* appear inside a `$value` — including inside arrays and nested objects.
150+
*/
151+
const resolveDeep = (value: unknown, root: RawObject): unknown => {
152+
if (isAlias(value)) return resolveAlias(value, root);
153+
if (isRef(value)) return resolveRef(value.$ref, root);
154+
if (Array.isArray(value)) return value.map(item => resolveDeep(item, root));
155+
if (isPlainObject(value)) {
156+
const out: RawObject = Object.create(null);
157+
for (const k in value) out[k] = resolveDeep(value[k], root);
158+
return out;
159+
}
160+
return value;
161+
};

src/scratch.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ const resolverURL = new URL("../src/test/example/design-tokens.resolver.json", i
44

55
const { tokens } = load(resolverURL);
66

7-
// Direct access — no createTree wrapper needed
8-
console.dir(tokens["z-index"], { depth: null });
7+
// $ref objects and alias strings inside composite $value should be fully resolved
8+
const shadow = (tokens as any)["box-shadow"].panel.$value;
9+
console.log(JSON.stringify(shadow, null, 2));
910

10-
// Alias resolution: color.primary.light.$value should resolve to a concrete color value
11-
// console.log((tokens as any).color?.primary?.light?.$value);
11+
// Spot-check: components should be an array, not a { $ref: "…" } object
12+
const components = shadow[0].color.components;
13+
console.log("components resolved:", Array.isArray(components), components);
1214

13-
// $type inheritance: a token without its own $type should show the group's type
14-
// console.log((tokens as any)["border-radius"]?.full?.$type); // → "dimension"
15+
// Alias resolution: "{base.zero}" inside the shadow value should be a dimension object
16+
const offsetY = shadow[0].offsetY;
17+
console.log("offsetY resolved:", offsetY);

0 commit comments

Comments
 (0)