fork of hey-api/openapi-ts because I need some additional things
at feat/skip-token 218 lines 5.5 kB view raw
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}