@@ -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 =>
97100const 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).
101107const 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+ } ;
0 commit comments