@@ -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 = (
1519nodes :Array < ts . Expression | ts . Statement > ,
1620) :Array < ts . Statement > =>
@@ -65,12 +69,14 @@ const processSchemaType = ({
6569resourceId :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 ( {
7480meta :query ,
7581name :buildName ( {
7682config :{
@@ -80,35 +86,53 @@ const processSchemaType = ({
8086name :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 ) ) {
112136const ref = plugin . referenceSymbol ( query ) ;
113137const callExpression = tsc . callExpression ( {
114138functionName :ref . placeholder ,