Skip to content

Commit 6483340

Browse files
committed
checkpoint
1 parent 5ae1135 commit 6483340

3 files changed

Lines changed: 84 additions & 118 deletions

File tree

src/loader/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ export class LoaderHost {
5858
/** Parsed JSON values, keyed by URL href. */
5959
#cache = new Map<string, unknown>();
6060

61-
/** Stores the loader system implementation. */
6261
constructor(sys: LoaderSys) {
6362
this.sys = sys;
6463
}
@@ -83,7 +82,7 @@ export class LoaderHost {
8382
// caller-supplied base). Using new URL(str, base) handles both absolute
8483
// file paths (e.g. "/Users/…") and relative strings correctly on all
8584
// platforms without requiring Node's pathToFileURL.
86-
const resolverURL = new URL(input.toString(), toBaseURL(options?.base, defaultBase));
85+
const resolverURL = new URL(input, toBaseURL(options?.base, defaultBase));
8786
resolver = this.readJSON<Resolver>(resolverURL);
8887
// Strip the filename so sibling $refs inside the resolver resolve correctly.
8988
resolverBase = new URL(".", resolverURL);

src/loader/merge.ts

Lines changed: 82 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -3,93 +3,96 @@ import { parsePointer } from "./pointer.js";
33

44
// ─── Public API ───────────────────────────────────────────────────────────────
55

6-
/** Merges ordered formats into a single lazily resolved token tree. */
6+
/** Merges ordered DTCG formats into a single lazily-resolved token tree. */
77
export const mergeFormats = (formats: Format[]): Format =>
88
buildNode(formats as RawObject[], formats as RawObject[], undefined, undefined, []) as unknown as Format;
99

1010
// ─── Internal ─────────────────────────────────────────────────────────────────
1111

1212
type RawObject = Record<string, unknown>;
1313

14-
/** Returns `true` when a value is a plain object. */
14+
/** Returns `true` when `v` is a non-array object. */
1515
const isPlainObject = (v: unknown): v is RawObject => v !== null && typeof v === "object" && !Array.isArray(v);
16-
/** Returns `true` when a value is a DTCG alias string. */
16+
17+
/** Returns `true` when `v` is a DTCG alias string (e.g. `"{color.blue}"`). */
1718
const isAlias = (v: unknown): v is string => typeof v === "string" && v.startsWith("{") && v.endsWith("}");
18-
/** Returns `true` when a value is a JSON reference object. */
19+
20+
/** Returns `true` when `v` is a JSON reference object (e.g. `{ "$ref": "#/…" }`). */
1921
const isRef = (v: unknown): v is { $ref: string } => isPlainObject(v) && typeof v.$ref === "string";
20-
/** Converts a path array into a stable cycle-detection key. */
22+
23+
/** Joins a path into a dot-separated key for cycle detection. */
2124
const pathToId = (path: readonly string[]): string => path.join(".") || "#";
2225

23-
/** Collects nested object values for a key across merged formats. */
26+
/** Collects the sub-objects for `key` across `formats`, resetting on non-object override. */
2427
const getSubFormats = (formats: readonly RawObject[], key: string): RawObject[] => {
25-
const subFormats: RawObject[] = [];
28+
const subs: RawObject[] = [];
2629

2730
for (const format of formats) {
28-
const value = format[key];
29-
if (value === undefined) continue;
30-
if (isPlainObject(value)) {
31-
subFormats.push(value);
31+
const v = format[key];
32+
if (v === undefined) continue;
33+
if (isPlainObject(v)) {
34+
subs.push(v);
3235
} else {
33-
subFormats.length = 0;
36+
subs.length = 0;
3437
}
3538
}
3639

37-
return subFormats;
40+
return subs;
3841
};
3942

40-
/** Resolves all format objects that exist at a nested path. */
41-
const getFormatsAtPath = (formats: readonly RawObject[], path: readonly string[]): RawObject[] => {
42-
let currentFormats = formats as RawObject[];
43+
/** Walks `path` through `formats` and returns the group-level sub-objects found there. */
44+
const getFormatsAtPath = (formats: readonly RawObject[], path: readonly string[]): readonly RawObject[] => {
45+
let current: readonly RawObject[] = formats;
4346

4447
for (const segment of path) {
45-
currentFormats = getSubFormats(currentFormats, segment);
46-
if (currentFormats.length === 0) return [];
48+
current = getSubFormats(current, segment);
49+
if (current.length === 0) return [];
4750
}
4851

49-
return currentFormats.some((format) => "$value" in format) ? [] : currentFormats;
52+
return current.some((f) => "$value" in f) ? [] : current;
5053
};
5154

52-
/** Parses either an alias path or JSON Pointer path into segments. */
53-
const parseReferencePath = (reference: string): string[] | undefined => {
54-
if (isAlias(reference)) return reference.slice(1, -1).split(".");
55+
/** Parses an alias string or JSON Pointer into path segments, or `undefined` on failure. */
56+
const parseReferencePath = (ref: string): string[] | undefined => {
57+
if (isAlias(ref)) return ref.slice(1, -1).split(".");
5558
try {
56-
return parsePointer(reference);
59+
return parsePointer(ref);
5760
} catch {
5861
return undefined;
5962
}
6063
};
6164

65+
// Module-level cycle-detection set for $extends. Safe because JS is
66+
// single-threaded; add before recursing, delete in the `finally` block.
6267
const resolvingExtends = new Set<string>();
6368

64-
/** Expands `$extends` references before a node is merged. */
69+
/** Prepends inherited formats from `$extends` targets before each local format. */
6570
const expandFormats = (formats: readonly RawObject[], rootFormats: readonly RawObject[], path: readonly string[]): RawObject[] => {
66-
const pathId = pathToId(path);
67-
if (resolvingExtends.has(pathId)) return [];
71+
const id = pathToId(path);
72+
if (resolvingExtends.has(id)) return [];
6873

69-
resolvingExtends.add(pathId);
74+
resolvingExtends.add(id);
7075
try {
71-
const expandedFormats: RawObject[] = [];
76+
const out: RawObject[] = [];
7277

7378
for (const format of formats) {
74-
const reference = typeof format.$extends === "string" ? format.$extends : undefined;
75-
if (reference) {
76-
const targetPath = parseReferencePath(reference);
79+
const ref = typeof format.$extends === "string" ? format.$extends : undefined;
80+
if (ref) {
81+
const targetPath = parseReferencePath(ref);
7782
if (targetPath) {
78-
const targetFormats = getFormatsAtPath(rootFormats, targetPath);
79-
expandedFormats.push(...expandFormats(targetFormats, rootFormats, targetPath));
83+
out.push(...expandFormats(getFormatsAtPath(rootFormats, targetPath), rootFormats, targetPath));
8084
}
8185
}
82-
83-
expandedFormats.push(format);
86+
out.push(format);
8487
}
8588

86-
return expandedFormats;
89+
return out;
8790
} finally {
88-
resolvingExtends.delete(pathId);
91+
resolvingExtends.delete(id);
8992
}
9093
};
9194

92-
/** Builds a merged lazy node for a specific format path. */
95+
/** Builds a merged lazy node for the formats at `path`. */
9396
const buildNode = (
9497
formats: RawObject[],
9598
rootFormats: RawObject[],
@@ -98,94 +101,77 @@ const buildNode = (
98101
path: string[],
99102
): RawObject => {
100103
const node = Object.create(null) as RawObject;
101-
// At the top level the node itself IS the resolution root; recursive calls
102-
// receive the already-established root so all aliases resolve to the same tree.
103-
const effectiveRoot = root ?? node;
104+
const effectiveRoot = root ?? node; // top-level call: the node IS the root
104105
const effectiveFormats = expandFormats(formats, rootFormats, path);
105106

106-
// Collect every key that appears across all sources.
107+
// Collect every key across all sources.
107108
const keys = new Set<string>();
108109
for (const format of effectiveFormats) {
109110
for (const key in format) keys.add(key);
110111
}
111112

112-
// Surface the inherited $type even when no source at this level declares one,
113-
// so that `tokens.color.blue.$type` correctly returns "color" when `$type` is
114-
// only defined on the parent group.
113+
// Surface inherited $type so descendants see it even when no local source declares one.
115114
if (!keys.has("$type") && inheritedType !== undefined) keys.add("$type");
116115

117-
/** Returns the effective `$type` for the current node. */
116+
// Memoised $type resolver — scans sources once and caches the result.
118117
let nodeTypeResolved = false;
119118
let nodeType: string | undefined;
120119
const getNodeType = (): string | undefined => {
121120
if (!nodeTypeResolved) {
122121
nodeTypeResolved = true;
123-
for (const format of effectiveFormats) {
124-
if (typeof format.$type === "string") nodeType = format.$type;
122+
for (const fmt of effectiveFormats) {
123+
if (typeof fmt.$type === "string") nodeType = fmt.$type;
125124
}
126125
nodeType ??= inheritedType;
127126
}
128127
return nodeType;
129128
};
130129

131-
// Define a self-memoizing getter for every key.
132-
// On first access the getter computes and caches the value as a plain data
133-
// property — subsequent reads are O(1) and `JSON.stringify` works as usual.
130+
// Self-memoising getter for every key. On first access the getter computes
131+
// the value, replaces itself with a plain data property, and never runs again.
134132
for (const key of keys) {
135133
Object.defineProperty(node, key, {
136134
get(): unknown {
137-
// $type resolves immediately from inherited/own declarations.
138135
let value: unknown = key === "$type" ? getNodeType() : undefined;
139136

140137
if (value === undefined && key !== "$type") {
141-
// Single pass: partition sources for this key into sub-objects
142-
// (groups/tokens) and leaf values. A later leaf resets any
143-
// previously accumulated sub-objects (full override).
144-
const subFormats: RawObject[] = [];
145-
let leafValue: unknown;
138+
// Single pass: collect sub-objects and track the last leaf value.
139+
// A later leaf resets accumulated sub-objects (full override).
140+
const subs: RawObject[] = [];
141+
let leaf: unknown;
146142
let hasLeaf = false;
147143

148-
for (const format of effectiveFormats) {
149-
const v = format[key];
144+
for (const fmt of effectiveFormats) {
145+
const v = fmt[key];
150146
if (v === undefined) continue;
151147
if (isPlainObject(v)) {
152-
subFormats.push(v);
148+
subs.push(v);
153149
} else {
154-
subFormats.length = 0;
155-
leafValue = v;
150+
subs.length = 0;
151+
leaf = v;
156152
hasLeaf = true;
157153
}
158154
}
159155

160-
if (subFormats.length > 0) {
161-
// Merge the sub-objects recursively.
162-
// $type inheritance must NOT bleed into $value content — a
163-
// token's type is DTCG metadata on the token node, not on
164-
// the value object itself (e.g. a border's width inherits
165-
// "border" from its parent, which is wrong).
156+
if (subs.length > 0) {
157+
// $type must NOT bleed into $value — it is metadata on
158+
// the token node, not on the value object itself.
166159
const childType = key === "$value" ? undefined : getNodeType();
167-
const sub = buildNode(subFormats, rootFormats, effectiveRoot, childType, [...path, key]);
160+
const sub = buildNode(subs, rootFormats, effectiveRoot, childType, [...path, key]);
168161

169162
if ("$value" in sub) {
170-
// Leaf token: auto-unwrap so callers get the value directly
171-
// (e.g. tokens["focus-ring"].dark → the border object).
172-
value = sub.$value;
163+
value = sub.$value; // auto-unwrap leaf token
173164
} else if (key === "$value") {
174-
// $value is a composite plain object (border, color, …) —
175-
// resolve any alias strings or $ref objects nested inside it.
176-
value = resolveDeep(sub, effectiveRoot);
165+
value = resolveDeep(sub, effectiveRoot); // composite value
177166
} else {
178-
// Group node: expose as a navigable merged sub-tree.
179-
value = sub;
167+
value = sub; // group sub-tree
180168
}
181169
} else if (hasLeaf) {
182-
// Leaf scalar or array: resolve aliases/refs only inside $value.
183-
value = key === "$value" ? resolveDeep(leafValue, effectiveRoot) : leafValue;
170+
value = key === "$value" ? resolveDeep(leaf, effectiveRoot) : leaf;
184171
}
185172
}
186173

187-
// Overwrite this getter with the computed value so it behaves exactly
188-
// like a plain data property from this point on.
174+
// Replace the getter with a plain data property.
189175
Object.defineProperty(this, key, { value, enumerable: true, configurable: true });
190176
return value;
191177
},
@@ -199,31 +185,28 @@ const buildNode = (
199185

200186
// ─── Alias / $ref resolution ──────────────────────────────────────────────────
201187

202-
// Module-level cycle-detection set. Safe because JS is single-threaded:
203-
// add before recursing, delete in the `finally` block so a thrown error
204-
// never leaves stale entries that would permanently block re-resolution.
188+
// Module-level cycle-detection set for aliases. Safe because JS is
189+
// single-threaded; add before recursing, delete in the `finally` block.
205190
const resolvingAliases = new Set<string>();
206191

207-
/** Resolves a DTCG alias string against the merged token tree. */
192+
/** Resolves a DTCG alias string (e.g. `"{color.blue}"`) against the merged tree. */
208193
const resolveAlias = (alias: string, root: RawObject): unknown => {
209-
const path = alias.slice(1, -1); // strip surrounding { }
210-
if (resolvingAliases.has(path)) return undefined; // circular reference guard
194+
const path = alias.slice(1, -1);
195+
if (resolvingAliases.has(path)) return undefined;
211196
resolvingAliases.add(path);
212197
try {
213198
let node: unknown = root;
214199
for (const seg of path.split(".")) {
215200
if (!isPlainObject(node)) return undefined;
216-
node = node[seg]; // triggers the self-memoizing getter for that segment
201+
node = node[seg];
217202
}
218-
// Because token nodes are auto-unwrapped, the node we land on IS the
219-
// resolved value — return it directly.
220203
return node;
221204
} finally {
222205
resolvingAliases.delete(path);
223206
}
224207
};
225208

226-
/** Resolves a JSON Pointer reference against the merged token tree. */
209+
/** Resolves a JSON Pointer `$ref` against the merged tree, skipping unwrapped `$value` segments. */
227210
const resolveRef = (ref: string, root: RawObject): unknown => {
228211
let path: string[];
229212
try {
@@ -234,31 +217,28 @@ const resolveRef = (ref: string, root: RawObject): unknown => {
234217

235218
let node: unknown = root;
236219
for (const seg of path) {
237-
// DTCG $ref paths are often written with "/$value/" as a literal path
238-
// segment. Since the merged tree auto-unwraps $value, that key no longer
239-
// exists at runtime — skip it so the walk lands in the resolved value.
240220
if (isPlainObject(node)) {
241221
if (seg === "$value" && !(seg in node)) continue;
242222
node = node[seg];
243223
} else if (Array.isArray(node)) {
244-
const index = Number(seg);
245-
node = Number.isInteger(index) ? node[index] : undefined;
224+
const idx = Number(seg);
225+
node = Number.isInteger(idx) ? node[idx] : undefined;
246226
} else {
247227
return undefined;
248228
}
249229
}
250230
return node;
251231
};
252232

253-
/** Recursively resolves aliases and `$ref` objects inside a `$value`. */
233+
/** Recursively resolves aliases and `$ref` objects anywhere inside a value. */
254234
const resolveDeep = (value: unknown, root: RawObject): unknown => {
255235
if (isAlias(value)) return resolveAlias(value, root);
256236
if (isRef(value)) return resolveRef(value.$ref, root);
257-
if (Array.isArray(value)) return value.map((item) => resolveDeep(item, root));
237+
if (Array.isArray(value)) return value.map((v) => resolveDeep(v, root));
258238
if (isPlainObject(value)) {
259-
const resolved: RawObject = Object.create(null);
260-
for (const k in value) resolved[k] = resolveDeep(value[k], root);
261-
return resolved;
239+
const out: RawObject = Object.create(null);
240+
for (const k in value) out[k] = resolveDeep(value[k], root);
241+
return out;
262242
}
263-
return value; // number, boolean, null, string literal — pass through unchanged
243+
return value;
264244
};

src/loader/node.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,7 @@ import { LoaderHost, type LoaderSys, type LoadOptions, type LoadResult } from ".
55

66
// ─── Node.js LoaderSys ────────────────────────────────────────────────────────
77

8-
/**
9-
* {@link LoaderSys} implementation for Node.js. Reads files synchronously
10-
* from the local filesystem using `fs.readFileSync`.
11-
*
12-
* This is NOT browser-safe — import it only from Node.js entry points.
13-
* For browser environments, provide your own {@link LoaderSys} that reads
14-
* from a virtual filesystem, network, or other source.
15-
*
16-
* @example Browser replacement
17-
* const browserSys: LoaderSys = {
18-
* readFile: (url) => fileMap.get(url.href) ?? (() => { throw new Error(`Not found: ${url}`) })(),
19-
* currentDirectory: () => new URL("./", location.href),
20-
* };
21-
*/
8+
/** Node.js {@link LoaderSys} — reads files synchronously via `fs.readFileSync`. */
229
export const nodeSys: LoaderSys = {
2310
/** Reads a UTF-8 file from disk. */
2411
readFile: (url) => readFileSync(url, "utf8"),

0 commit comments

Comments
 (0)