···2727// Type-level test: will fail to compile if any type export is missing or renamed
2828export type _TypeExports = [
2929 index.AnalysisContext,
3030+ index.AstContext,
3031 index.BindingKind,
3132 index.ExportMember,
3233 index.ExportModule,
···2020 Language,
2121 NameConflictResolvers,
2222} from './languages/types';
2323+export type { AstContext } from './nodes/context';
2324export type { INode as Node } from './nodes/node';
2425export type { IOutput as Output } from './output';
2526export {
+11
packages/codegen-core/src/nodes/context.d.ts
···11+import type { INode } from './node';
22+33+/**
44+ * Context passed to `.toAst()` methods.
55+ */
66+export type AstContext = {
77+ /**
88+ * Returns the canonical node for accessing the provided node.
99+ */
1010+ getAccess<T extends INode>(node: T): T;
1111+};
+2-1
packages/codegen-core/src/nodes/node.d.ts
···22import type { Language } from '../languages/types';
33import type { IAnalysisContext } from '../planner/types';
44import type { Symbol } from '../symbols/symbol';
55+import type { AstContext } from './context';
5667export interface INode<T = unknown> {
78 /** Perform semantic analysis. */
···1920 /** The symbol associated with this node. */
2021 symbol?: Symbol;
2122 /** Convert this node into AST representation. */
2222- toAst(): T;
2323+ toAst(ctx: AstContext): T;
2324 /** Brand used for renderer dispatch. */
2425 readonly '~brand': string;
2526}
···55import { defaultExtensions } from '../languages/extensions';
66import { defaultNameConflictResolvers } from '../languages/resolvers';
77import type { Extensions, NameConflictResolvers } from '../languages/types';
88+import type { AstContext } from '../nodes/context';
89import { NodeRegistry } from '../nodes/registry';
910import type { IOutput } from '../output';
1011import { Planner } from '../planner/planner';
···6061 render(meta?: IProjectRenderMeta): ReadonlyArray<IOutput> {
6162 new Planner(this).plan(meta);
6263 const files: Array<IOutput> = [];
6464+ const astContext: AstContext = {
6565+ getAccess(node) {
6666+ return node;
6767+ },
6868+ };
6369 for (const file of this.files.registered()) {
6470 if (file.finalPath && file.renderer) {
6565- const content = file.renderer.render({ file, meta, project: this });
7171+ const content = file.renderer.render({
7272+ astContext,
7373+ file,
7474+ meta,
7575+ project: this,
7676+ });
6677 files.push({ content, path: file.finalPath });
6778 }
6879 }
+6-1
packages/codegen-core/src/renderer.d.ts
···11import type { IProjectRenderMeta } from './extensions';
22import type { File } from './files/file';
33+import type { AstContext } from './nodes/context';
34import type { IProject } from './project/types';
4556export interface RenderContext {
77+ /**
88+ * The context passed to `.toAst()` methods.
99+ */
1010+ astContext: AstContext;
611 /**
712 * The current file.
813 */
···2126 /** Renders the given file. */
2227 render(ctx: RenderContext): string;
2328 /** Returns whether this renderer can render the given file. */
2424- supports(ctx: RenderContext): boolean;
2929+ supports(ctx: Omit<RenderContext, 'astContext'>): boolean;
2530}
+16-8
packages/openapi-ts/src/ts-dsl/base.ts
···11// TODO: symbol should be protected, but needs to be public to satisfy types
22import type {
33 AnalysisContext,
44+ AstContext,
45 File,
56 FromRef,
67 Language,
···2728 parent?: Node;
2829 root?: Node;
2930 symbol?: Symbol;
3030- toAst(): T {
3131+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3232+ toAst(_: AstContext): T {
3133 return undefined as unknown as T;
3234 }
3335 readonly '~brand' = nodeBrand;
···118120 ) as T extends string ? ts.Identifier : T;
119121 }
120122121121- protected $node<I>(value: I): NodeOfMaybe<I> {
123123+ protected $node<I>(ctx: AstContext, value: I): NodeOfMaybe<I> {
122124 if (value === undefined) {
123125 return undefined as NodeOfMaybe<I>;
124126 }
···133135 if (value instanceof Array) {
134136 return value.map((item) => {
135137 if (isRef(item)) item = fromRef(item);
136136- return this.unwrap(item);
138138+ return this.unwrap(item, ctx);
137139 }) as NodeOfMaybe<I>;
138140 }
139139- return this.unwrap(value as any) as NodeOfMaybe<I>;
141141+ return this.unwrap(value as any, ctx) as NodeOfMaybe<I>;
140142 }
141143142144 protected $type<I>(
145145+ ctx: AstContext,
143146 value: I,
144147 args?: ReadonlyArray<ts.TypeNode>,
145148 ): TypeOfMaybe<I> {
···169172 ) as TypeOfMaybe<I>;
170173 }
171174 if (value instanceof Array) {
172172- return value.map((item) => this.$type(item, args)) as TypeOfMaybe<I>;
175175+ return value.map((item) => this.$type(ctx, item, args)) as TypeOfMaybe<I>;
173176 }
174174- return this.unwrap(value as any) as TypeOfMaybe<I>;
177177+ return this.unwrap(value as any, ctx) as TypeOfMaybe<I>;
175178 }
176179177180 /** Unwraps nested nodes into raw TypeScript AST. */
178178- private unwrap<I>(value: I): I extends TsDsl<infer N> ? N : I {
179179- return (isNode(value) ? value.toAst() : value) as I extends TsDsl<infer N>
181181+ private unwrap<I>(
182182+ value: I,
183183+ ctx: AstContext,
184184+ ): I extends TsDsl<infer N> ? N : I {
185185+ return (isNode(value) ? value.toAst(ctx) : value) as I extends TsDsl<
186186+ infer N
187187+ >
180188 ? N
181189 : I;
182190 }
+15-11
packages/openapi-ts/src/ts-dsl/decl/class.ts
···11-import type { AnalysisContext, Ref, Symbol } from '@hey-api/codegen-core';
11+import type {
22+ AnalysisContext,
33+ AstContext,
44+ Ref,
55+ Symbol,
66+} from '@hey-api/codegen-core';
27import { isSymbol, ref } from '@hey-api/codegen-core';
38import ts from 'typescript';
49···97102 return this;
98103 }
99104100100- override toAst() {
101101- const body = this.$node(this.body) as ReadonlyArray<ts.ClassElement>;
105105+ override toAst(ctx: AstContext) {
106106+ const body = this.$node(ctx, this.body) as ReadonlyArray<ts.ClassElement>;
102107 const node = ts.factory.createClassDeclaration(
103103- [...this.$decorators(), ...this.modifiers],
104104- // @ts-expect-error need to improve types
105105- this.$node(this.name),
106106- this.$generics(),
107107- this._heritage(),
108108+ [...this.$decorators(ctx), ...this.modifiers],
109109+ this.$node(ctx, this.name) as ts.Identifier,
110110+ this.$generics(ctx),
111111+ this._heritage(ctx),
108112 body,
109113 );
110110- return this.$docs(node);
114114+ return this.$docs(ctx, node);
111115 }
112116113117 /** Builds heritage clauses (extends). */
114114- private _heritage(): ReadonlyArray<ts.HeritageClause> {
115115- const node = this.$node(this.baseClass);
118118+ private _heritage(ctx: AstContext): ReadonlyArray<ts.HeritageClause> {
119119+ const node = this.$node(ctx, this.baseClass);
116120 if (!node) return [];
117121 return [
118122 ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [