fork of hey-api/openapi-ts because I need some additional things
1import path from 'node:path';
2
3import type { ExportModule, ImportModule } from '../bindings';
4import { fileBrand } from '../brands';
5import type { Language } from '../languages/types';
6import { log } from '../log';
7import type { INode } from '../nodes/node';
8import type { NameScopes } from '../planner/scope';
9import type { IProject } from '../project/types';
10import type { Renderer } from '../renderer';
11import type { IFileIn } from './types';
12
13export class File<Node extends INode = INode> {
14 /**
15 * Exports from this file.
16 */
17 private _exports: Array<ExportModule> = [];
18 /**
19 * File extension (e.g. `.ts`).
20 */
21 private _extension?: string;
22 /**
23 * Actual emitted file path, including extension and directories.
24 */
25 private _finalPath?: string;
26 /**
27 * Imports to this file.
28 */
29 private _imports: Array<ImportModule> = [];
30 /**
31 * Language of the file.
32 */
33 private _language?: Language;
34 /**
35 * Logical, extension-free path used for planning and routing.
36 */
37 private _logicalFilePath: string;
38 /**
39 * Base name of the file (without extension).
40 */
41 private _name?: string;
42 /**
43 * Syntax nodes contained in this file.
44 */
45 private _nodes: Array<Node> = [];
46 /**
47 * Renderer assigned to this file.
48 */
49 private _renderer?: Renderer;
50
51 /** Brand used for identifying files. */
52 readonly '~brand' = fileBrand;
53 /** All names defined in this file, including local scopes. */
54 allNames: NameScopes = new Map();
55 /** Whether this file is external to the project. */
56 external: boolean;
57 /** Unique identifier for the file. */
58 readonly id: number;
59 /** The project this file belongs to. */
60 readonly project: IProject;
61 /** Names declared at the top level of the file. */
62 topLevelNames: NameScopes = new Map();
63
64 constructor(input: IFileIn, id: number, project: IProject) {
65 this.external = input.external ?? false;
66 this.id = id;
67 if (input.language !== undefined) this._language = input.language;
68 this._logicalFilePath = input.logicalFilePath.split(path.sep).join('/');
69 if (input.name !== undefined) this._name = input.name;
70 this.project = project;
71 }
72
73 /**
74 * Exports from this file.
75 */
76 get exports(): ReadonlyArray<ExportModule> {
77 return [...this._exports];
78 }
79
80 /**
81 * Read-only accessor for the file extension.
82 */
83 get extension(): string | undefined {
84 if (this.external) return;
85 if (this._extension) return this._extension;
86 const language = this.language;
87 const extension = language ? this.project.extensions[language] : undefined;
88 if (extension && extension[0]) return extension[0];
89 return;
90 }
91
92 /**
93 * Read-only accessor for the final emitted path.
94 *
95 * If undefined, the file has not yet been assigned a final path
96 * or is external to the project and should not be emitted.
97 */
98 get finalPath(): string | undefined {
99 if (this._finalPath) return this._finalPath;
100 const dirs = this._logicalFilePath ? this._logicalFilePath.split('/').slice(0, -1) : [];
101 return [...dirs, `${this.name}${this.extension ?? ''}`].join('/');
102 }
103
104 /**
105 * Imports to this file.
106 */
107 get imports(): ReadonlyArray<ImportModule> {
108 return [...this._imports];
109 }
110
111 /**
112 * Language of the file; inferred from nodes or fallback if not set explicitly.
113 */
114 get language(): Language | undefined {
115 if (this._language) return this._language;
116 if (this._nodes[0]) return this._nodes[0].language;
117 return;
118 }
119
120 /**
121 * Logical, extension-free path used for planning and routing.
122 */
123 get logicalFilePath(): string {
124 return this._logicalFilePath;
125 }
126
127 /**
128 * Base name of the file (without extension).
129 *
130 * If no name was set explicitly, it is inferred from the logical file path.
131 */
132 get name(): string {
133 if (this._name) return this._name;
134 const name = this._logicalFilePath.split('/').pop();
135 if (name) return name;
136 const message = `File ${this.toString()} has no name`;
137 log.debug(message, 'file');
138 throw new Error(message);
139 }
140
141 /**
142 * Syntax nodes contained in this file.
143 */
144 get nodes(): ReadonlyArray<Node> {
145 return [...this._nodes];
146 }
147
148 /**
149 * Renderer assigned to this file.
150 */
151 get renderer(): Renderer | undefined {
152 return this._renderer;
153 }
154
155 /**
156 * Add an export group to the file.
157 */
158 addExport(group: ExportModule): void {
159 this._exports.push(group);
160 }
161
162 /**
163 * Add an import group to the file.
164 */
165 addImport(group: ImportModule): void {
166 this._imports.push(group);
167 }
168
169 /**
170 * Add a syntax node to the file.
171 */
172 addNode(node: Node): void {
173 this._nodes.push(node);
174 node.file = this;
175 }
176
177 /**
178 * Sets the file extension.
179 */
180 setExtension(extension: string): void {
181 this._extension = extension;
182 }
183
184 /**
185 * Sets the final emitted path of the file.
186 */
187 setFinalPath(path: string): void {
188 this._finalPath = path;
189 }
190
191 /**
192 * Sets the language of the file.
193 */
194 setLanguage(lang: Language): void {
195 this._language = lang;
196 }
197
198 /**
199 * Sets the name of the file.
200 */
201 setName(name: string): void {
202 this._name = name;
203 }
204
205 /**
206 * Sets the renderer assigned to this file.
207 */
208 setRenderer(renderer: Renderer): void {
209 this._renderer = renderer;
210 }
211
212 /**
213 * Returns a debug‑friendly string representation identifying the file.
214 */
215 toString(): string {
216 return `[File ${this._logicalFilePath}#${this.id}]`;
217 }
218}