···1010import type { Renderer } from '../renderer';
1111import type { IFileIn } from './types';
12121313-export class File {
1313+export class File<Node extends INode = INode> {
1414 /**
1515 * Exports from this file.
1616 */
···4242 /**
4343 * Syntax nodes contained in this file.
4444 */
4545- private _nodes: Array<INode> = [];
4545+ private _nodes: Array<Node> = [];
4646 /**
4747 * Renderer assigned to this file.
4848 */
···142142 /**
143143 * Syntax nodes contained in this file.
144144 */
145145- get nodes(): ReadonlyArray<INode> {
145145+ get nodes(): ReadonlyArray<Node> {
146146 return [...this._nodes];
147147 }
148148···170170 /**
171171 * Add a syntax node to the file.
172172 */
173173- addNode(node: INode): void {
173173+ addNode(node: Node): void {
174174 this._nodes.push(node);
175175 node.file = this;
176176 }
+10-1
packages/codegen-core/src/index.ts
···2121 NameConflictResolvers,
2222} from './languages/types';
2323export type { AstContext } from './nodes/context';
2424-export type { INode as Node } from './nodes/node';
2424+export type {
2525+ AccessPatternContext,
2626+ AccessPatternOptions,
2727+ INode as Node,
2828+ NodeName,
2929+ NodeNameSanitizer,
3030+ NodeRole,
3131+ NodeScope,
3232+ StructuralRelationship,
3333+} from './nodes/node';
2534export type { IOutput as Output } from './output';
2635export {
2736 simpleNameConflictResolver,
+8-4
packages/codegen-core/src/nodes/context.d.ts
···11-import type { INode } from './node';
11+import type { Symbol } from '../symbols/symbol';
22+import type { AccessPatternOptions, INode } from './node';
2334/**
45 * Context passed to `.toAst()` methods.
56 */
66-export type AstContext = {
77+export interface AstContext {
78 /**
89 * Returns the canonical node for accessing the provided node.
910 */
1010- getAccess<T extends INode>(node: T): T;
1111-};
1111+ getAccess<T = unknown>(
1212+ node: INode | Symbol,
1313+ options?: AccessPatternOptions,
1414+ ): T;
1515+}
+61-4
packages/codegen-core/src/nodes/node.d.ts
···11import type { File } from '../files/file';
22import type { Language } from '../languages/types';
33import type { IAnalysisContext } from '../planner/types';
44+import type { Ref } from '../refs/types';
45import type { Symbol } from '../symbols/symbol';
56import type { AstContext } from './context';
6788+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+}
2727+2828+export type MaybeRef<T> = T | Ref<T>;
2929+3030+export type NodeName = MaybeRef<Symbol | string | number>;
3131+3232+export type NodeNameSanitizer = (name: string) => string;
3333+3434+export type NodeRole =
3535+ | 'accessor'
3636+ | 'container'
3737+ | 'control-flow'
3838+ | 'expression'
3939+ | 'literal';
4040+4141+export type NodeScope = 'type' | 'value';
4242+4343+export type StructuralRelationship = 'container' | 'reference';
4444+745export 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;
852 /** Perform semantic analysis. */
953 analyze(ctx: IAnalysisContext): void;
1054 /** Whether this node is exported from its file. */
···1357 file?: File;
1458 /** The programming language associated with this node */
1559 language: Language;
1616- /** Parent node in the syntax tree. */
1717- parent?: INode;
1818- /** Root node of the syntax tree. */
1919- root?: INode;
6060+ /** The display name of this node. */
6161+ readonly name: Ref<NodeName> & {
6262+ set(value: NodeName): void;
6363+ toString(): string;
6464+ };
6565+ /** Optional function to sanitize the node name. */
6666+ readonly nameSanitizer?: NodeNameSanitizer;
6767+ /** The role of this node within the structure. */
6868+ role?: NodeRole;
6969+ /** Whether this node is a root node in the file. */
7070+ root?: boolean;
7171+ /** The scope of this node. */
7272+ scope?: NodeScope;
7373+ /** Semantic children in the structure hierarchy. */
7474+ structuralChildren?: Map<INode, StructuralRelationship>;
7575+ /** Semantic parents in the structure hierarchy. */
7676+ structuralParents?: Map<INode, StructuralRelationship>;
2077 /** The symbol associated with this node. */
2178 symbol?: Symbol;
2279 /** Convert this node into AST representation. */
+70-9
packages/codegen-core/src/planner/analyzer.ts
···11import { isNodeRef, isSymbolRef } from '../guards';
22-import type { INode } from '../nodes/node';
22+import type { INode, StructuralRelationship } from '../nodes/node';
33import { fromRef, isRef, ref } from '../refs/refs';
44import type { Ref } from '../refs/types';
55import type { Symbol } from '../symbols/symbol';
···88import type { IAnalysisContext, Input } from './types';
991010export class AnalysisContext implements IAnalysisContext {
1111+ /**
1212+ * Stack of parent nodes during analysis.
1313+ *
1414+ * The top of the stack is the current semantic container.
1515+ */
1616+ private _parentStack: Array<INode> = [];
1717+1118 scope: Scope;
1219 scopes: Scope = createScope();
1320 symbol?: Symbol;
14211515- constructor(symbol?: Symbol) {
2222+ constructor(node: INode) {
2323+ this._parentStack.push(node);
1624 this.scope = this.scopes;
1717- this.symbol = symbol;
2525+ this.symbol = node.symbol;
2626+ }
2727+2828+ /**
2929+ * Get the current semantic parent (top of stack).
3030+ */
3131+ get currentParent(): INode | undefined {
3232+ return this._parentStack[this._parentStack.length - 1];
3333+ }
3434+3535+ /**
3636+ * Register a child node under the current parent.
3737+ */
3838+ addChild(
3939+ child: INode,
4040+ relationship: StructuralRelationship = 'container',
4141+ ): void {
4242+ const parent = this.currentParent;
4343+ if (!parent) return;
4444+4545+ if (!parent.structuralChildren) {
4646+ parent.structuralChildren = new Map();
4747+ }
4848+ parent.structuralChildren.set(child, relationship);
4949+5050+ if (!child.structuralParents) {
5151+ child.structuralParents = new Map();
5252+ }
5353+ child.structuralParents.set(parent, relationship);
1854 }
19552056 addDependency(symbol: Ref<Symbol>): void {
···2460 }
25612662 analyze(input: Input): void {
2727- const v = isRef(input) ? input : ref(input);
2828- if (isSymbolRef(v)) {
2929- this.addDependency(v);
3030- } else if (isNodeRef(v)) {
3131- fromRef(v).analyze(this);
6363+ const value = isRef(input) ? input : ref(input);
6464+ if (isSymbolRef(value)) {
6565+ const symbol = fromRef(value);
6666+ // avoid adding self as child
6767+ if (symbol.node && this.currentParent !== symbol.node) {
6868+ this.addChild(symbol.node, 'reference');
6969+ }
7070+ this.addDependency(value);
7171+ } else if (isNodeRef(value)) {
7272+ const node = fromRef(value);
7373+ this.addChild(node, 'container');
7474+ this.pushParent(node);
7575+ node.analyze(this);
7676+ this.popParent();
3277 }
3378 }
3479···5398 return names;
5499 }
55100101101+ /**
102102+ * Pop the current semantic parent.
103103+ * Call this when exiting a container node.
104104+ */
105105+ popParent(): void {
106106+ this._parentStack.pop();
107107+ }
108108+56109 popScope(): void {
57110 this.scope = this.scope.parent ?? this.scope;
58111 }
59112113113+ /**
114114+ * Push a node as the current semantic parent.
115115+ */
116116+ pushParent(node: INode): void {
117117+ this._parentStack.push(node);
118118+ }
119119+60120 pushScope(): void {
61121 const scope = createScope({ parent: this.scope });
62122 this.scope.children.push(scope);
···86146 const cached = this.nodeCache.get(node);
87147 if (cached) return cached;
881488989- const ctx = new AnalysisContext(node.symbol);
149149+ node.root = true;
150150+ const ctx = new AnalysisContext(node);
90151 node.analyze(ctx);
9115292153 this.nodeCache.set(node, ctx);
···55import { defaultExtensions } from '../languages/extensions';
66import { defaultNameConflictResolvers } from '../languages/resolvers';
77import type { Extensions, NameConflictResolvers } from '../languages/types';
88-import type { AstContext } from '../nodes/context';
98import { NodeRegistry } from '../nodes/registry';
109import type { IOutput } from '../output';
1110import { Planner } from '../planner/planner';
···6160 render(meta?: IProjectRenderMeta): ReadonlyArray<IOutput> {
6261 new Planner(this).plan(meta);
6362 const files: Array<IOutput> = [];
6464- const astContext: AstContext = {
6565- getAccess(node) {
6666- return node;
6767- },
6868- };
6963 for (const file of this.files.registered()) {
7064 if (file.finalPath && file.renderer) {
7171- const content = file.renderer.render({
7272- astContext,
7373- file,
7474- meta,
7575- project: this,
7676- });
6565+ const content = file.renderer.render({ file, meta, project: this });
7766 files.push({ content, path: file.finalPath });
7867 }
7968 }
+12-4
packages/codegen-core/src/refs/refs.ts
···11-import type { FromRefs, Ref, Refs } from './types';
11+import type { FromRef, FromRefs, Ref, Refs } from './types';
2233/**
44 * Wraps a single value in a Ref object.
55+ *
66+ * If the value is already a Ref, returns it as-is (idempotent).
57 *
68 * @example
79 * ```ts
810 * const r = ref(123); // { '~ref': 123 }
911 * console.log(r['~ref']); // 123
1212+ *
1313+ * const r2 = ref(r); // { '~ref': 123 } (not double-wrapped)
1014 * ```
1115 */
1212-export const ref = <T>(value: T): Ref<T> => ({ '~ref': value });
1616+export const ref = <T>(value: T): Ref<T> => {
1717+ if (isRef(value)) {
1818+ return value as Ref<T>;
1919+ }
2020+ return { '~ref': value } as Ref<T>;
2121+};
13221423/**
1524 * Converts a plain object to an object of Refs (deep, per property).
···4251 */
4352export const fromRef = <T extends Ref<unknown> | undefined>(
4453 ref: T,
4545-): T extends Ref<infer U> ? U : undefined =>
4646- ref?.['~ref'] as T extends Ref<infer U> ? U : undefined;
5454+): FromRef<T> => ref?.['~ref'] as FromRef<T>;
47554856/**
4957 * Converts an object of Refs back to a plain object (unwraps all refs).
+3-3
packages/codegen-core/src/refs/types.d.ts
···88 * console.log(num['~ref']); // 42
99 * ```
1010 */
1111-export type Ref<T> = { '~ref': T };
1111+export type Ref<T> = T extends { ['~ref']: unknown } ? T : { '~ref': T };
12121313/**
1414 * Maps every property of `T` to a `Ref` of that property.
···3333 * type N = FromRef<{ '~ref': number }>; // number
3434 * ```
3535 */
3636-export type FromRef<T> = T extends Ref<infer V> ? V : T;
3636+export type FromRef<T> = T extends { '~ref': infer U } ? U : T;
37373838/**
3939 * Maps every property of a Ref-wrapped object back to its plain value.
···4646 * ```
4747 */
4848export type FromRefs<T> = {
4949- [K in keyof T]: T[K] extends Ref<infer V> ? V : T[K];
4949+ [K in keyof T]: T[K] extends Ref<infer U> ? U : T[K];
5050};
+4-8
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';
33+import type { INode } from './nodes/node';
44import type { IProject } from './project/types';
5566-export interface RenderContext {
77- /**
88- * The context passed to `.toAst()` methods.
99- */
1010- astContext: AstContext;
66+export interface RenderContext<Node extends INode = INode> {
117 /**
128 * The current file.
139 */
1414- file: File;
1010+ file: File<Node>;
1511 /**
1612 * Arbitrary metadata.
1713 */
···2622 /** Renders the given file. */
2723 render(ctx: RenderContext): string;
2824 /** Returns whether this renderer can render the given file. */
2929- supports(ctx: Omit<RenderContext, 'astContext'>): boolean;
2525+ supports(ctx: RenderContext): boolean;
3026}
+6-34
packages/codegen-core/src/symbols/symbol.ts
···33import type { ISymbolMeta } from '../extensions';
44import type { File } from '../files/file';
55import type { INode } from '../nodes/node';
66-import type {
77- BindingKind,
88- ISymbolIn,
99- SymbolKind,
1010- SymbolNameSanitizer,
1111-} from './types';
66+import type { BindingKind, ISymbolIn, SymbolKind } from './types';
1271313-export class Symbol {
88+export class Symbol<Node extends INode = INode> {
149 /**
1510 * Canonical symbol this stub resolves to, if any.
1611 *
···7974 */
8075 private _name: string;
8176 /**
8282- * Optional function to sanitize the symbol name.
8383- *
8484- * @default undefined
8585- */
8686- private _nameSanitizer?: SymbolNameSanitizer;
8787- /**
8877 * Node that defines this symbol.
8978 */
9090- private _node?: INode;
7979+ private _node?: Node;
91809281 /** Brand used for identifying symbols. */
9382 readonly '~brand' = symbolBrand;
···202191 }
203192204193 /**
205205- * Optional function to sanitize the symbol name.
206206- */
207207- get nameSanitizer(): SymbolNameSanitizer | undefined {
208208- return this.canonical._nameSanitizer;
209209- }
210210-211211- /**
212194 * Read‑only accessor for the defining node.
213195 */
214214- get node(): INode | undefined {
215215- return this.canonical._node;
196196+ get node(): Node | undefined {
197197+ return this.canonical._node as Node | undefined;
216198 }
217199218200 /**
···308290 }
309291310292 /**
311311- * Sets a custom function to sanitize the symbol's name.
312312- *
313313- * @param fn — The name sanitizer function to apply.
314314- */
315315- setNameSanitizer(fn: SymbolNameSanitizer): void {
316316- this.assertCanonical();
317317- this._nameSanitizer = fn;
318318- }
319319-320320- /**
321293 * Binds the node that defines this symbol.
322294 *
323295 * This may only be set once.
324296 */
325325- setNode(node: INode): void {
297297+ setNode(node: Node): void {
326298 this.assertCanonical();
327299 if (this._node && this._node !== node) {
328300 const message = `Symbol ${this.canonical.toString()} is already bound to a different node.`;
-2
packages/codegen-core/src/symbols/types.d.ts
···1414 | 'type'
1515 | 'var';
16161717-export type SymbolNameSanitizer = (name: string) => string;
1818-1917export type ISymbolIn = {
2018 /**
2119 * Array of file names (without extensions) from which this symbol is re-exported.