@@ -11,6 +11,10 @@ import type { HeyApiTransformersPlugin } from './types';
1111
1212const dataVariableName = 'data' ;
1313
14+ // Track symbols that are currently being built so recursive references
15+ // can emit calls to transformers that will be implemented later.
16+ const buildingSymbols = new Set < number > ( ) ;
17+
1418const ensureStatements = (
1519 nodes : Array < ts . Expression | ts . Statement > ,
1620) : Array < ts . Statement > =>
@@ -65,12 +69,14 @@ const processSchemaType = ({
6569 resourceId : schema . $ref ,
6670 } ;
6771
68- if ( ! plugin . getSymbol ( query ) ) {
69- // TODO: remove
70- // create each schema response transformer only once
72+ let symbol = plugin . getSymbol ( query ) ;
7173
72- // Register symbol early to prevent infinite recursion with self-referential schemas
73- const symbol = plugin . registerSymbol ( {
74+ if ( ! symbol ) {
75+ // Register a placeholder symbol immediately and set its value to null
76+ // as a stop token to prevent infinite recursion for self-referential
77+ // schemas. We also mark this symbol as "building" so that nested
78+ // references to it can emit calls that will be implemented later.
79+ symbol = plugin . registerSymbol ( {
7480 meta : query ,
7581 name : buildName ( {
7682 config : {
@@ -80,35 +86,53 @@ const processSchemaType = ({
8086 name : refToName ( schema . $ref ) ,
8187 } ) ,
8288 } ) ;
89+ plugin . setSymbolValue ( symbol , null ) ;
90+ }
8391
84- const refSchema = plugin . context . resolveIrRef < IR . SchemaObject > (
85- schema . $ref ,
86- ) ;
87- const nodes = schemaResponseTransformerNodes ( {
88- plugin,
89- schema : refSchema ,
90- } ) ;
91- if ( nodes . length ) {
92- const node = tsc . constVariable ( {
93- expression : tsc . arrowFunction ( {
94- async : false ,
95- multiLine : true ,
96- parameters : [
97- {
98- name : dataVariableName ,
99- // TODO: parser - add types, generate types without transforms
100- type : tsc . keywordTypeNode ( { keyword : 'any' } ) ,
101- } ,
102- ] ,
103- statements : ensureStatements ( nodes ) ,
104- } ) ,
105- name : symbol . placeholder ,
92+ // Only compute the implementation if the symbol isn't already being built.
93+ // This prevents infinite recursion on self-referential schemas. We still
94+ // allow emitting a call when the symbol is currently being built so
95+ // parent nodes can reference the transformer that will be emitted later.
96+ const existingValue = plugin . gen . symbols . getValue ( symbol . id ) ;
97+ if ( ! existingValue && ! buildingSymbols . has ( symbol . id ) ) {
98+ buildingSymbols . add ( symbol . id ) ;
99+ try {
100+ const refSchema = plugin . context . resolveIrRef < IR . SchemaObject > (
101+ schema . $ref ,
102+ ) ;
103+ const nodes = schemaResponseTransformerNodes ( {
104+ plugin,
105+ schema : refSchema ,
106106 } ) ;
107- plugin . setSymbolValue ( symbol , node ) ;
107+
108+ if ( nodes . length ) {
109+ const node = tsc . constVariable ( {
110+ expression : tsc . arrowFunction ( {
111+ async : false ,
112+ multiLine : true ,
113+ parameters : [
114+ {
115+ name : dataVariableName ,
116+ // TODO: parser - add types, generate types without transforms
117+ type : tsc . keywordTypeNode ( { keyword : 'any' } ) ,
118+ } ,
119+ ] ,
120+ statements : ensureStatements ( nodes ) ,
121+ } ) ,
122+ name : symbol . placeholder ,
123+ } ) ;
124+ plugin . setSymbolValue ( symbol , node ) ;
125+ }
126+ } finally {
127+ buildingSymbols . delete ( symbol . id ) ;
108128 }
109129 }
110130
111- if ( plugin . isSymbolRegistered ( query ) ) {
131+ // Only emit a call if the symbol has a value (implementation) OR the
132+ // symbol is currently being built (recursive reference) — in the
133+ // latter case we allow emitting a call that will be implemented later.
134+ const currentValue = plugin . gen . symbols . getValue ( symbol . id ) ;
135+ if ( currentValue || buildingSymbols . has ( symbol . id ) ) {
112136 const ref = plugin . referenceSymbol ( query ) ;
113137 const callExpression = tsc . callExpression ( {
114138 functionName : ref . placeholder ,
0 commit comments