···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,
3130 index.BindingKind,
3231 index.ExportMember,
3332 index.ExportModule,
···3635 index.FileIn,
3736 index.FromRef<any>,
3837 index.FromRefs<any>,
3939- index.IProject,
4038 index.ImportMember,
4139 index.ImportModule,
4040+ index.IProject,
4241 index.Language,
4342 index.NameConflictResolver,
4443 index.NameConflictResolvers,
4544 index.Node,
4545+ index.NodeName,
4646+ index.NodeNameSanitizer,
4747+ index.NodeRelationship,
4848+ index.NodeScope,
4649 index.Output,
4750 index.Project,
4851 index.ProjectRenderMeta,
+9-10
packages/codegen-core/src/bindings.d.ts
···3232export interface ImportMember {
3333 /** Whether this import is type-only. */
3434 isTypeOnly: boolean;
3535- /** Import flavor. */
3636- kind: BindingKind;
3735 /**
3836 * The name this symbol will have locally in this file.
3937 * This is where aliasing is applied:
···4745 sourceName: string;
4846}
49475050-export type ImportModule = Pick<ImportMember, 'isTypeOnly'> & {
5151- /** Source file. */
5252- from: File;
5353- /** List of symbols imported from this module. */
5454- imports: Array<ImportMember>;
5555- /** Namespace import: `import * as name from 'module'`. Mutually exclusive with `imports`. */
5656- namespaceImport?: string;
5757-};
4848+export type ImportModule = Pick<ImportMember, 'isTypeOnly'> &
4949+ Pick<Partial<ImportMember>, 'localName'> & {
5050+ /** Source file. */
5151+ from: File;
5252+ /** List of symbols imported from this module. */
5353+ imports: Array<ImportMember>;
5454+ /** Import flavor. */
5555+ kind: BindingKind;
5656+ };
+1-5
packages/codegen-core/src/index.ts
···2020 Language,
2121 NameConflictResolvers,
2222} from './languages/types';
2323-export type { AstContext } from './nodes/context';
2423export type {
2525- AccessPatternContext,
2626- AccessPatternOptions,
2724 INode as Node,
2825 NodeName,
2926 NodeNameSanitizer,
3030- NodeRole,
2727+ NodeRelationship,
3128 NodeScope,
3232- StructuralRelationship,
3329} from './nodes/node';
3430export type { IOutput as Output } from './output';
3531export {
-15
packages/codegen-core/src/nodes/context.d.ts
···11-import type { Symbol } from '../symbols/symbol';
22-import type { AccessPatternOptions, INode } from './node';
33-44-/**
55- * Context passed to `.toAst()` methods.
66- */
77-export interface AstContext {
88- /**
99- * Returns the canonical node for accessing the provided node.
1010- */
1111- getAccess<T = unknown>(
1212- node: INode | Symbol,
1313- options?: AccessPatternOptions,
1414- ): T;
1515-}
+6-40
packages/codegen-core/src/nodes/node.d.ts
···33import type { IAnalysisContext } from '../planner/types';
44import type { Ref } from '../refs/types';
55import type { Symbol } from '../symbols/symbol';
66-import type { AstContext } from './context';
77-88-export type AccessContext = 'docs' | 'runtime';
99-1010-export interface AccessPatternContext<Node extends INode = INode> {
1111- /** The full chain. */
1212- chain: ReadonlyArray<Node>;
1313- /** Position in the chain (0 = root). */
1414- index: number;
1515- /** Is this the leaf node? */
1616- isLeaf: boolean;
1717- /** Is this the root node? */
1818- isRoot: boolean;
1919- /** Total length of the chain. */
2020- length: number;
2121-}
2222-2323-export interface AccessPatternOptions {
2424- /** The access context. */
2525- context: AccessContext;
2626-}
276287export type MaybeRef<T> = T | Ref<T>;
298···31103211export type NodeNameSanitizer = (name: string) => string;
33123434-export type NodeRole =
3535- | 'accessor'
3636- | 'container'
3737- | 'control-flow'
3838- | 'expression'
3939- | 'literal';
1313+export type NodeRelationship = 'container' | 'reference';
40144115export type NodeScope = 'type' | 'value';
42164343-export type StructuralRelationship = 'container' | 'reference';
4444-4517export interface INode<T = unknown> {
4646- /** Custom access pattern for this node. */
4747- accessPattern?(
4848- node: this,
4949- options: AccessPatternOptions,
5050- ctx: AccessPatternContext<this>,
5151- ): unknown | undefined;
5218 /** Perform semantic analysis. */
5319 analyze(ctx: IAnalysisContext): void;
2020+ /** Create a shallow copy of this node. */
2121+ clone(): this;
5422 /** Whether this node is exported from its file. */
5523 exported?: boolean;
5624 /** The file this node belongs to. */
···6432 };
6533 /** Optional function to sanitize the node name. */
6634 readonly nameSanitizer?: NodeNameSanitizer;
6767- /** The role of this node within the structure. */
6868- role?: NodeRole;
6935 /** Whether this node is a root node in the file. */
7036 root?: boolean;
7137 /** The scope of this node. */
7238 scope?: NodeScope;
7339 /** Semantic children in the structure hierarchy. */
7474- structuralChildren?: Map<INode, StructuralRelationship>;
4040+ structuralChildren?: Map<INode, NodeRelationship>;
7541 /** Semantic parents in the structure hierarchy. */
7676- structuralParents?: Map<INode, StructuralRelationship>;
4242+ structuralParents?: Map<INode, NodeRelationship>;
7743 /** The symbol associated with this node. */
7844 symbol?: Symbol;
7945 /** Convert this node into AST representation. */
8080- toAst(ctx: AstContext): T;
4646+ toAst(): T;
8147 /** Brand used for renderer dispatch. */
8248 readonly '~brand': string;
8349}
+2-5
packages/codegen-core/src/planner/analyzer.ts
···11import { isNodeRef, isSymbolRef } from '../guards';
22-import type { INode, StructuralRelationship } from '../nodes/node';
22+import type { INode, NodeRelationship } from '../nodes/node';
33import { fromRef, isRef, ref } from '../refs/refs';
44import type { Ref } from '../refs/types';
55import type { Symbol } from '../symbols/symbol';
···3535 /**
3636 * Register a child node under the current parent.
3737 */
3838- addChild(
3939- child: INode,
4040- relationship: StructuralRelationship = 'container',
4141- ): void {
3838+ addChild(child: INode, relationship: NodeRelationship = 'container'): void {
4239 const parent = this.currentParent;
4340 if (!parent) return;
4441
···11-import type {
22- AnalysisContext,
33- AstContext,
44- NodeScope,
55-} from '@hey-api/codegen-core';
11+import type { AnalysisContext, NodeScope } from '@hey-api/codegen-core';
62import ts from 'typescript';
7384import type { MaybeTsDsl, TypeTsDsl } from '../base';
···3127 ctx.analyze(this._expr);
3228 }
33293434- override toAst(ctx: AstContext) {
3535- const expr = this.$node(ctx, this._expr);
3030+ override toAst() {
3131+ const expr = this.$node(this._expr);
3632 return ts.factory.createTypeQueryNode(expr as unknown as ts.EntityName);
3733 }
3834}
+3-7
packages/openapi-ts/src/ts-dsl/type/template.ts
···11-import type {
22- AnalysisContext,
33- AstContext,
44- NodeScope,
55-} from '@hey-api/codegen-core';
11+import type { AnalysisContext, NodeScope } from '@hey-api/codegen-core';
62import ts from 'typescript';
7384import type { MaybeTsDsl } from '../base';
···3430 return this;
3531 }
36323737- override toAst(ctx: AstContext) {
3838- const parts = this.$node(ctx, this.parts);
3333+ override toAst() {
3434+ const parts = this.$node(this.parts);
39354036 const normalized: Array<string | ts.TypeNode> = [];
4137 // merge consecutive string parts
+3-7
packages/openapi-ts/src/ts-dsl/type/tuple.ts
···11-import type {
22- AnalysisContext,
33- AstContext,
44- NodeScope,
55-} from '@hey-api/codegen-core';
11+import type { AnalysisContext, NodeScope } from '@hey-api/codegen-core';
62import ts from 'typescript';
7384import type { TypeTsDsl } from '../base';
···3329 return this;
3430 }
35313636- override toAst(ctx: AstContext) {
3232+ override toAst() {
3733 return ts.factory.createTupleTypeNode(
3838- this._elements.map((t) => this.$type(ctx, t)),
3434+ this._elements.map((t) => this.$type(t)),
3935 );
4036 }
4137}
+224-83
packages/openapi-ts/src/ts-dsl/utils/context.ts
···11-import type {
22- AccessPatternOptions,
33- AstContext,
44- NodeScope,
55- Symbol,
66-} from '@hey-api/codegen-core';
11+import type { BindingKind, NodeScope, Symbol } from '@hey-api/codegen-core';
72import { isSymbol } from '@hey-api/codegen-core';
33+import type ts from 'typescript';
8499-import { $ } from '~/ts-dsl';
55+import { $, TypeScriptRenderer } from '~/ts-dsl';
106117import type { TsDsl } from '../base';
88+import type { CallArgs } from '../expr/call';
1291313-const getScope = (node: TsDsl): NodeScope => node.scope ?? 'value';
1010+export type NodeChain = ReadonlyArray<TsDsl>;
14111515-function traverseStructuralParent(node: TsDsl): boolean {
1616- if (node.role === 'literal') {
1717- return false;
1818- }
1919- return true;
1212+export interface AccessOptions {
1313+ /** The access context. */
1414+ context?: 'example';
2015}
21162222-function foldAccessChain<Node extends TsDsl = TsDsl>(
2323- chain: ReadonlyArray<Node>,
2424-): ReadonlyArray<Node> {
2525- const folded: Array<Node> = [];
1717+export type AccessResult = ReturnType<
1818+ typeof $.expr | typeof $.attr | typeof $.call | typeof $.new
1919+>;
2020+2121+export interface ExampleOptions {
2222+ /** Import kind for the root node. */
2323+ importKind?: BindingKind;
2424+ /** Import name for the root node. */
2525+ importName?: string;
2626+ /** Setup to run before calling the example. */
2727+ importSetup?:
2828+ | TsDsl<ts.Expression>
2929+ | ((imp: TsDsl<ts.Expression>) => TsDsl<ts.Expression>);
3030+ /** Module to import from. */
3131+ moduleName: string;
3232+ /** Example request payload. */
3333+ payload?: CallArgs | CallArgs[number];
3434+ /** Variable name for setup node. */
3535+ setupName?: string;
3636+}
26372727- for (const node of chain) {
2828- if (folded.length === 0) {
2929- if (node.role === 'container') {
3030- folded.push(node);
3131- }
3232- } else if (node.role === 'accessor') {
3333- folded.push(node);
3838+function accessChainToNode<T = AccessResult>(accessChain: NodeChain): T {
3939+ let result!: AccessResult;
4040+ accessChain.forEach((node, index) => {
4141+ if (index === 0) {
4242+ // assume correct node
4343+ result = node as typeof result;
4444+ } else {
4545+ result = result.attr(node.name);
3446 }
4747+ });
4848+ return result as T;
4949+}
5050+5151+function getAccessChainForNode(node: TsDsl): NodeChain {
5252+ const structuralChain = [...getStructuralChainForNode(node, new Set())];
5353+ const accessChain = structuralToAccessChain(structuralChain);
5454+ if (accessChain.length === 0) {
5555+ throw new Error(
5656+ `Cannot build access chain for node ${node['~dsl']} (${node.name.toString()})`,
5757+ );
3558 }
5959+ return accessChain.map((node) => node.clone());
6060+}
36613737- return folded;
6262+function getScope(node: TsDsl): NodeScope {
6363+ return node.scope ?? 'value';
3864}
39654040-function getAccessChain<Node extends TsDsl = TsDsl>(
4141- node: Node,
4242-): ReadonlyArray<Node> {
4343- const chain: Array<TsDsl> = [];
4444- const scope: NodeScope = getScope(node);
4545- const visited = new Set<TsDsl>();
6666+function getStructuralChainForNode(
6767+ node: TsDsl,
6868+ visited: Set<TsDsl>,
6969+): NodeChain {
7070+ if (visited.has(node)) return [];
7171+ visited.add(node);
46724747- let current: TsDsl | undefined = node;
4848- while (current) {
4949- if (visited.has(current)) break;
5050- visited.add(current);
7373+ if (node['~dsl'] === 'TemplateTsDsl' || node['~dsl'] === 'FuncTsDsl') {
7474+ return [];
7575+ }
51765252- chain.unshift(current);
7777+ if (node.structuralParents) {
7878+ for (const [parent] of node.structuralParents) {
7979+ if (getScope(parent) !== getScope(node)) continue;
53805454- let foundParent = false;
5555- for (const [parent] of current.structuralParents || []) {
5656- if (getScope(parent) === scope && traverseStructuralParent(parent)) {
5757- current = parent;
5858- foundParent = true;
5959- break;
6060- }
8181+ const chain = getStructuralChainForNode(parent, visited);
8282+ if (chain.length > 0) return [...chain, node];
6183 }
8484+ }
62856363- if (!foundParent) break;
6464- }
8686+ if (!node.root) return [];
8787+8888+ return [node];
8989+}
65906666- // trim any unreachable nodes before root
6767- const rootIndex = chain.findIndex((node) => node.root);
6868- if (rootIndex !== -1) {
6969- chain.splice(0, rootIndex);
7070- }
9191+/**
9292+ * Fold a structural chain to an access chain by removing
9393+ * non-accessor nodes.
9494+ */
9595+function structuralToAccessChain(structuralChain: NodeChain): NodeChain {
9696+ const accessChain: Array<TsDsl> = [];
9797+ structuralChain.forEach((node, index) => {
9898+ // assume first node is always included
9999+ if (index === 0) {
100100+ accessChain.push(node);
101101+ } else if (
102102+ node['~dsl'] === 'FieldTsDsl' ||
103103+ node['~dsl'] === 'GetterTsDsl' ||
104104+ node['~dsl'] === 'MethodTsDsl'
105105+ ) {
106106+ accessChain.push(node);
107107+ }
108108+ });
109109+ return accessChain;
110110+}
711117272- return foldAccessChain(chain) as ReadonlyArray<Node>;
112112+function transformAccessChain(
113113+ accessChain: NodeChain,
114114+ options: AccessOptions = {},
115115+): NodeChain {
116116+ return accessChain.map((node, index) => {
117117+ const accessNode = node.toAccessNode?.(node, options, {
118118+ chain: accessChain,
119119+ index,
120120+ isLeaf: index === accessChain.length - 1,
121121+ isRoot: index === 0,
122122+ length: accessChain.length,
123123+ });
124124+ if (accessNode) return accessNode;
125125+ if (index === 0) {
126126+ if (node['~dsl'] === 'ClassTsDsl') {
127127+ const nextNode = accessChain[index + 1];
128128+ if (nextNode?.['~dsl'] === 'FieldTsDsl') {
129129+ if ((nextNode as ReturnType<typeof $.field>).hasModifier('static')) {
130130+ return $(node.name);
131131+ }
132132+ }
133133+ return $.new(node.name).args();
134134+ }
135135+ return $(node.name);
136136+ }
137137+ return node;
138138+ });
73139}
741407575-export const astContext: AstContext = {
7676- getAccess<T = unknown>(
7777- to: TsDsl | Symbol<TsDsl>,
7878- options?: AccessPatternOptions,
141141+export class TsDslContext {
142142+ /**
143143+ * Build an expression for accessing the node.
144144+ *
145145+ * @param node - The node or symbol to build access for
146146+ * @param options - Access options
147147+ * @returns Expression for accessing the node
148148+ *
149149+ * @example
150150+ * ```ts
151151+ * ctx.access(node); // → Expression for accessing the node
152152+ * ```
153153+ */
154154+ access<T = AccessResult>(
155155+ node: TsDsl | Symbol<TsDsl>,
156156+ options?: AccessOptions,
79157 ): T {
8080- const node = isSymbol(to) ? to.node! : to;
8181- const chain = getAccessChain(node);
8282- if (chain.length === 0) return node as T;
158158+ const n = isSymbol(node) ? node.node : node;
159159+ if (!n) {
160160+ throw new Error(`Symbol ${node.name} is not resolved to a node.`);
161161+ }
162162+ const accessChain = getAccessChainForNode(n);
163163+ const finalChain = transformAccessChain(accessChain, options);
164164+ return accessChainToNode<T>(finalChain);
165165+ }
831668484- let result!: ReturnType<typeof $.expr | typeof $.attr>;
167167+ /**
168168+ * Build an example.
169169+ *
170170+ * @param node - The node to generate an example for
171171+ * @param options - Example options
172172+ * @returns Full example string
173173+ *
174174+ * @example
175175+ * ```ts
176176+ * ctx.example(node, { moduleName: 'my-sdk' }); // → Full example string
177177+ * ```
178178+ */
179179+ example(
180180+ node: TsDsl,
181181+ options: ExampleOptions | undefined,
182182+ astOptions?: Parameters<typeof TypeScriptRenderer.astToString>[0],
183183+ ): string {
184184+ if (astOptions) {
185185+ return TypeScriptRenderer.astToString(astOptions);
186186+ }
851878686- for (let index = 0; index < chain.length; index++) {
8787- const currentNode = chain[index]!;
188188+ if (!options) {
189189+ throw new Error('Example options are required.');
190190+ }
191191+192192+ const accessChain = getAccessChainForNode(node);
193193+ if (options.importName) {
194194+ accessChain[0]!.name.set(options.importName);
195195+ }
196196+ const importNode = $(accessChain[0]!.name.toString()); // must store name before transform
197197+ const finalChain = transformAccessChain(accessChain, {
198198+ context: 'example',
199199+ });
882008989- const transformed = currentNode.accessPattern?.(
9090- currentNode,
9191- {
9292- context: 'runtime',
9393- ...options,
9494- },
9595- {
9696- chain,
9797- index,
9898- isLeaf: index === chain.length - 1,
9999- isRoot: index === 0,
100100- length: chain.length,
101101- },
102102- ) as typeof result | undefined;
201201+ const setupNode = options.importSetup
202202+ ? typeof options.importSetup === 'function'
203203+ ? options.importSetup(importNode)
204204+ : options.importSetup
205205+ : (finalChain[0]! as TsDsl<ts.Expression>);
206206+ const setupName = options.setupName;
207207+ const payload =
208208+ options.payload instanceof Array
209209+ ? options.payload
210210+ : options.payload
211211+ ? [options.payload]
212212+ : [];
103213104104- if (index === 0) {
105105- result = transformed || $(currentNode.name);
106106- } else {
107107- result = result.attr(transformed?.name || currentNode.name);
108108- }
214214+ let nodes: Array<TsDsl> = [];
215215+ if (setupName) {
216216+ nodes = [
217217+ $.const(setupName).assign(setupNode),
218218+ accessChainToNode([$(setupName), ...finalChain.slice(1)]).call(
219219+ ...payload,
220220+ ),
221221+ ];
222222+ } else {
223223+ nodes = [
224224+ accessChainToNode([setupNode, ...finalChain.slice(1)]).call(...payload),
225225+ ];
109226 }
110227111111- return result as T;
112112- },
113113-};
228228+ const localName = importNode.name.toString();
229229+ return TypeScriptRenderer.astToString({
230230+ imports: [
231231+ [
232232+ {
233233+ imports:
234234+ !options.importKind || options.importKind === 'named'
235235+ ? [
236236+ {
237237+ isTypeOnly: false,
238238+ localName,
239239+ sourceName: localName,
240240+ },
241241+ ]
242242+ : [],
243243+ isTypeOnly: false,
244244+ kind: options.importKind ?? 'named',
245245+ localName: options.importKind !== 'named' ? localName : undefined,
246246+ modulePath: options.moduleName,
247247+ },
248248+ ],
249249+ ],
250250+ nodes,
251251+ trailingNewline: false,
252252+ });
253253+ }
254254+}
+4
packages/openapi-ts/src/ts-dsl/utils/factories.ts
···22import type { AttrCtor } from '../expr/attr';
33import type { AwaitCtor } from '../expr/await';
44import type { CallCtor } from '../expr/call';
55+import type { NewCtor } from '../expr/new';
56import type { TypeOfExprCtor } from '../expr/typeof';
67import type { ReturnCtor } from '../stmt/return';
78import type { TypeExprCtor } from '../type/expr';
···44454546 /** Factory for creating function or method call expressions (e.g. `fn(arg)`). */
4647 call: createFactory<CallCtor>('call'),
4848+4949+ /** Factory for creating new expressions (e.g. `new ClassName()`). */
5050+ new: createFactory<NewCtor>('new'),
47514852 /** Factory for creating return statements. */
4953 return: createFactory<ReturnCtor>('return'),
+10-6
packages/openapi-ts/src/ts-dsl/utils/lazy.ts
···11-import type { AnalysisContext, AstContext } from '@hey-api/codegen-core';
11+import type { AnalysisContext } from '@hey-api/codegen-core';
22import type ts from 'typescript';
3344import { TsDsl } from '../base';
55-import { astContext } from './context';
55+import { TsDslContext } from './context';
6677-export type LazyThunk<T extends ts.Node> = (ctx: AstContext) => TsDsl<T>;
77+export type LazyThunk<T extends ts.Node> = (ctx: TsDslContext) => TsDsl<T>;
8899export class LazyTsDsl<T extends ts.Node = ts.Node> extends TsDsl<T> {
1010 readonly '~dsl' = 'LazyTsDsl';
···18181919 override analyze(ctx: AnalysisContext): void {
2020 super.analyze(ctx);
2121- ctx.analyze(this._thunk(astContext));
2121+ ctx.analyze(this.toResult());
2222 }
23232424- override toAst(ctx: AstContext): T {
2525- return this._thunk(ctx).toAst(ctx);
2424+ toResult(): TsDsl<T> {
2525+ return this._thunk(new TsDslContext());
2626+ }
2727+2828+ override toAst(): T {
2929+ return this.toResult().toAst();
2630 }
2731}