fork of hey-api/openapi-ts because I need some additional things
at feat/skip-token 311 lines 9.6 kB view raw
1import type { RenderContext, Renderer } from '@hey-api/codegen-core'; 2import type { MaybeArray, MaybeFunc } from '@hey-api/types'; 3 4import type { PyDsl } from '../../py-dsl'; 5import { py } from '../../ts-python'; 6import type { ModuleExport, ModuleImport, SortGroup, SortKey, SortModule } from './render-utils'; 7import { astToString, moduleSortKey } from './render-utils'; 8 9type Exports = ReadonlyArray<ReadonlyArray<ModuleExport>>; 10type ExportsOptions = { 11 preferExportAll?: boolean; 12}; 13type Header = MaybeArray<string> | null | undefined; 14type Imports = ReadonlyArray<ReadonlyArray<ModuleImport>>; 15 16function headerToLines(header: Header): ReadonlyArray<string> { 17 if (!header) return []; 18 const lines: Array<string> = []; 19 if (typeof header === 'string') { 20 lines.push(...header.split(/\r?\n/)); 21 return lines; 22 } 23 for (const line of header) { 24 lines.push(...line.split(/\r?\n/)); 25 } 26 return lines; 27} 28 29export class PythonRenderer implements Renderer { 30 /** 31 * Function to generate a file header. 32 * 33 * @private 34 */ 35 private _header?: MaybeFunc<(ctx: RenderContext<PyDsl>) => Header>; 36 /** 37 * Whether `export * from 'module'` should be used when possible instead of named exports. 38 * 39 * @private 40 */ 41 private _preferExportAll: boolean; 42 /** 43 * Controls whether imports/exports include a file extension (e.g., '.ts' or '.js'). 44 * 45 * @private 46 */ 47 private _preferFileExtension: string; 48 /** 49 * Optional function to transform module specifiers. 50 * 51 * @private 52 */ 53 private _resolveModuleName?: (moduleName: string) => string | undefined; 54 55 constructor( 56 args: { 57 header?: MaybeFunc<(ctx: RenderContext<PyDsl>) => Header>; 58 preferExportAll?: boolean; 59 preferFileExtension?: string; 60 resolveModuleName?: (moduleName: string) => string | undefined; 61 } = {}, 62 ) { 63 this._header = args.header; 64 this._preferExportAll = args.preferExportAll ?? false; 65 this._preferFileExtension = args.preferFileExtension ?? ''; 66 this._resolveModuleName = args.resolveModuleName; 67 } 68 69 render(ctx: RenderContext<PyDsl>): string { 70 const header = typeof this._header === 'function' ? this._header(ctx) : this._header; 71 return PythonRenderer.astToString({ 72 // exports: this.getExports(ctx), 73 exportsOptions: { 74 preferExportAll: this._preferExportAll, 75 }, 76 header, 77 imports: this.getImports(ctx), 78 nodes: ctx.file.nodes, 79 }); 80 } 81 82 supports(ctx: RenderContext): boolean { 83 return ctx.file.language === 'python'; 84 } 85 86 static astToString(args: { 87 exports?: Exports; 88 exportsOptions?: ExportsOptions; 89 header?: Header; 90 imports?: Imports; 91 nodes?: ReadonlyArray<PyDsl>; 92 /** 93 * Whether to include a trailing newline at the end of the file. 94 * 95 * @default true 96 */ 97 trailingNewline?: boolean; 98 }): string { 99 let text = ''; 100 for (const header of headerToLines(args.header)) { 101 text += `${header}\n`; 102 } 103 104 let imports = ''; 105 for (const group of args.imports ?? []) { 106 if (imports) imports += '\n'; 107 for (const imp of group) { 108 imports += `${astToString(PythonRenderer.toImportAst(imp))}\n`; 109 } 110 } 111 text = `${text}${text && imports ? '\n' : ''}${imports}`; 112 113 let nodes = ''; 114 for (const node of args.nodes ?? []) { 115 if (nodes) nodes += '\n'; 116 nodes += `${astToString(node.toAst())}\n`; 117 } 118 text = `${text}${text && nodes ? '\n' : ''}${nodes}`; 119 120 const exports = ''; 121 // let exports = ''; 122 // for (const group of args.exports ?? []) { 123 // if ((!exports && nodes) || exports) exports += '\n'; 124 // for (const exp of group) { 125 // exports += `${astToString(PythonRenderer.toExportAst(exp, args.exportsOptions))}\n`; 126 // } 127 // } 128 text = `${text}${text && exports ? '\n' : ''}${exports}`; 129 130 if (args.trailingNewline === false && text.endsWith('\n')) { 131 text = text.slice(0, -1); 132 } 133 134 return text; 135 } 136 137 // static toExportAst(group: ModuleExport, options?: ExportsOptions): ts.ExportDeclaration { 138 // const specifiers = group.exports.map((exp) => { 139 // const specifier = ts.factory.createExportSpecifier( 140 // exp.isTypeOnly, 141 // exp.sourceName !== exp.exportedName ? $.id(exp.sourceName).toAst() : undefined, 142 // $.id(exp.exportedName).toAst(), 143 // ); 144 // return specifier; 145 // }); 146 // const exportClause = group.namespaceExport 147 // ? ts.factory.createNamespaceExport($.id(group.namespaceExport).toAst()) 148 // : (!group.canExportAll || !options?.preferExportAll) && specifiers.length 149 // ? ts.factory.createNamedExports(specifiers) 150 // : undefined; 151 // return ts.factory.createExportDeclaration( 152 // undefined, 153 // group.isTypeOnly, 154 // exportClause, 155 // $.literal(group.modulePath).toAst(), 156 // ); 157 // } 158 159 static toImportAst(group: ModuleImport): py.ImportStatement { 160 const names: Array<{ 161 alias?: string; 162 name: string; 163 }> = group.imports.map((imp) => ({ 164 alias: imp.localName !== imp.sourceName ? imp.localName : undefined, 165 name: imp.sourceName, 166 })); 167 return py.factory.createImportStatement(group.modulePath, names, group.imports.length > 0); 168 } 169 170 // private getExports(ctx: RenderContext): Exports { 171 // type ModuleEntry = { 172 // group: ModuleExport; 173 // sortKey: SortKey; 174 // }; 175 176 // const groups = new Map<SortGroup, Map<SortModule, ModuleEntry>>(); 177 178 // for (const exp of ctx.file.exports) { 179 // const sortKey = moduleSortKey({ 180 // file: ctx.file, 181 // fromFile: exp.from, 182 // preferFileExtension: this._preferFileExtension, 183 // root: ctx.project.root, 184 // }); 185 // const modulePath = this._resolveModuleName?.(sortKey[2]) ?? sortKey[2]; 186 // const [groupIndex] = sortKey; 187 188 // if (!groups.has(groupIndex)) groups.set(groupIndex, new Map()); 189 // const moduleMap = groups.get(groupIndex)!; 190 191 // if (!moduleMap.has(modulePath)) { 192 // moduleMap.set(modulePath, { 193 // group: { 194 // canExportAll: exp.canExportAll, 195 // exports: exp.exports, 196 // isTypeOnly: exp.isTypeOnly, 197 // modulePath, 198 // namespaceExport: exp.namespaceExport, 199 // }, 200 // sortKey, 201 // }); 202 // } 203 // } 204 205 // const exports: Array<Array<ModuleExport>> = Array.from(groups.entries()) 206 // .sort((a, b) => a[0] - b[0]) 207 // .map(([, moduleMap]) => { 208 // const entries = Array.from(moduleMap.values()); 209 210 // entries.sort((a, b) => { 211 // const d = a.sortKey[1] - b.sortKey[1]; 212 // return d !== 0 ? d : a.group.modulePath.localeCompare(b.group.modulePath); 213 // }); 214 215 // return entries.map((e) => { 216 // const group = e.group; 217 // if (group.namespaceExport) { 218 // group.exports = []; 219 // } else { 220 // const isTypeOnly = !group.exports.find((exp) => !exp.isTypeOnly); 221 // if (isTypeOnly) { 222 // group.isTypeOnly = true; 223 // for (const exp of group.exports) { 224 // exp.isTypeOnly = false; 225 // } 226 // } 227 // group.exports.sort((a, b) => a.exportedName.localeCompare(b.exportedName)); 228 // } 229 // return group; 230 // }); 231 // }); 232 233 // return exports; 234 // } 235 236 private getImports(ctx: RenderContext): Imports { 237 type ModuleEntry = { 238 group: ModuleImport; 239 sortKey: SortKey; 240 }; 241 242 const groups = new Map<SortGroup, Map<SortModule, ModuleEntry>>(); 243 244 for (const imp of ctx.file.imports) { 245 const sortKey = moduleSortKey({ 246 file: ctx.file, 247 fromFile: imp.from, 248 preferFileExtension: this._preferFileExtension, 249 root: ctx.project.root, 250 }); 251 const modulePath = this._resolveModuleName?.(sortKey[2]) ?? sortKey[2]; 252 const [groupIndex] = sortKey; 253 254 if (!groups.has(groupIndex)) groups.set(groupIndex, new Map()); 255 const moduleMap = groups.get(groupIndex)!; 256 257 if (!moduleMap.has(modulePath)) { 258 moduleMap.set(modulePath, { 259 group: { 260 imports: [], 261 isTypeOnly: false, 262 kind: imp.kind, 263 modulePath, 264 }, 265 sortKey, 266 }); 267 } 268 269 const entry = moduleMap.get(modulePath)!; 270 const group = entry.group; 271 272 if (imp.kind !== 'named') { 273 group.isTypeOnly = imp.isTypeOnly; 274 group.kind = imp.kind; 275 group.localName = imp.localName; 276 } else { 277 group.imports.push(...imp.imports); 278 } 279 } 280 281 const imports: Array<Array<ModuleImport>> = Array.from(groups.entries()) 282 .sort((a, b) => a[0] - b[0]) 283 .map(([, moduleMap]) => { 284 const entries = Array.from(moduleMap.values()); 285 286 entries.sort((a, b) => { 287 const d = a.sortKey[1] - b.sortKey[1]; 288 return d !== 0 ? d : a.group.modulePath.localeCompare(b.group.modulePath); 289 }); 290 291 return entries.map((e) => { 292 const group = e.group; 293 if (group.kind === 'namespace') { 294 group.imports = []; 295 } else { 296 const isTypeOnly = !group.imports.find((imp) => !imp.isTypeOnly); 297 if (isTypeOnly) { 298 group.isTypeOnly = true; 299 for (const imp of group.imports) { 300 imp.isTypeOnly = false; 301 } 302 } 303 group.imports.sort((a, b) => a.localName.localeCompare(b.localName)); 304 } 305 return group; 306 }); 307 }); 308 309 return imports; 310 } 311}