Suite of AT Protocol TypeScript libraries built on web standards
21
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 170 lines 5.1 kB view raw
1import { mkdir, rm, stat, writeFile } from "node:fs/promises"; 2import { dirname, join, resolve } from "node:path"; 3import { IndentationText, Project } from "ts-morph"; 4import type { LexiconDocument, LexiconIndexer } from "../document/mod.ts"; 5import { buildFilter, type BuildFilterOptions } from "./filter.ts"; 6import { FilteredIndexer } from "./filtered-indexer.ts"; 7import { LexDefBuilder, type LexDefBuilderOptions } from "./def-builder.ts"; 8import { formatGeneratedText } from "./formatter.ts"; 9import { 10 LexiconDirectoryIndexer, 11 type LexiconDirectoryIndexerOptions, 12} from "./directory-indexer.ts"; 13import { isSafeIdentifier } from "./ts-lang.ts"; 14 15export type LexBuilderOptions = LexDefBuilderOptions & { 16 importExt?: string; 17 fileExt?: string; 18}; 19 20export type LexBuilderLoadOptions = 21 & LexiconDirectoryIndexerOptions 22 & BuildFilterOptions; 23 24export type LexBuilderSaveOptions = { 25 out: string; 26 clear?: boolean; 27 override?: boolean; 28 format?: boolean; 29}; 30 31export class LexBuilder { 32 readonly #imported = new Set<string>(); 33 readonly #project = new Project({ 34 useInMemoryFileSystem: true, 35 manipulationSettings: { indentationText: IndentationText.TwoSpaces }, 36 }); 37 38 constructor(private readonly options: LexBuilderOptions = {}) {} 39 40 get fileExt(): string { 41 return this.options.fileExt ?? ".ts"; 42 } 43 44 get importExt(): string { 45 return this.options.importExt ?? ".ts"; 46 } 47 48 async load(options: LexBuilderLoadOptions): Promise<void> { 49 await using indexer = new FilteredIndexer( 50 new LexiconDirectoryIndexer(options), 51 buildFilter(options), 52 ); 53 54 for await (const doc of indexer) { 55 if (!this.#imported.has(doc.id)) { 56 this.#imported.add(doc.id); 57 } else { 58 throw new Error(`Duplicate lexicon document id: ${doc.id}`); 59 } 60 61 await this.createDefsFile(doc, indexer); 62 await this.createExportTree(doc); 63 } 64 } 65 66 async save(options: LexBuilderSaveOptions): Promise<void> { 67 const files = this.#project.getSourceFiles(); 68 const destination = resolve(options.out); 69 70 if (options.clear) { 71 await rm(destination, { recursive: true, force: true }); 72 } else if (!options.override) { 73 await Promise.all( 74 files.map((f) => 75 assertNotFileExists( 76 resolveOutputFilePath(destination, f.getFilePath()), 77 ) 78 ), 79 ); 80 } 81 82 await Promise.all( 83 Array.from(files, async (file) => { 84 const filePath = resolveOutputFilePath(destination, file.getFilePath()); 85 const content = options.format === false 86 ? file.getFullText() 87 : await formatGeneratedText(filePath, file.getFullText()); 88 await mkdir(dirname(filePath), { recursive: true }); 89 await rm(filePath, { recursive: true, force: true }); 90 await writeFile(filePath, content, "utf8"); 91 }), 92 ); 93 } 94 95 private createFile(path: string) { 96 return this.#project.createSourceFile(path); 97 } 98 99 private getFile(path: string) { 100 return this.#project.getSourceFile(path) ?? this.createFile(path); 101 } 102 103 private createExportTree(doc: LexiconDocument): void { 104 const namespaces = doc.id.split("."); 105 106 for (let i = 0; i < namespaces.length - 1; i++) { 107 const currentNs = namespaces[i]; 108 const childNs = namespaces[i + 1]; 109 110 const path = join("/", ...namespaces.slice(0, i + 1)); 111 const file = this.getFile(`${path}${this.fileExt}`); 112 113 const childModuleSpecifier = `./${currentNs}/${childNs}${this.importExt}`; 114 const dec = file.getExportDeclaration(childModuleSpecifier); 115 if (!dec) { 116 file.addExportDeclaration({ 117 moduleSpecifier: childModuleSpecifier, 118 namespaceExport: isSafeIdentifier(childNs) 119 ? childNs 120 : JSON.stringify(childNs), 121 }); 122 } 123 } 124 125 const path = join("/", ...namespaces); 126 const file = this.getFile(`${path}${this.fileExt}`); 127 128 file.addExportDeclaration({ 129 moduleSpecifier: `./${namespaces.at(-1)}.defs${this.importExt}`, 130 }); 131 132 file.addExportDeclaration({ 133 moduleSpecifier: `./${namespaces.at(-1)}.defs${this.importExt}`, 134 namespaceExport: "$defs", 135 }); 136 } 137 138 private async createDefsFile( 139 doc: LexiconDocument, 140 indexer: LexiconIndexer, 141 ): Promise<void> { 142 const path = join("/", ...doc.id.split(".")); 143 const file = this.createFile(`${path}.defs${this.fileExt}`); 144 145 const fileBuilder = new LexDefBuilder( 146 { ...this.options, importExt: this.importExt }, 147 file, 148 doc, 149 indexer, 150 ); 151 await fileBuilder.build(); 152 } 153} 154 155async function assertNotFileExists(file: string): Promise<void> { 156 try { 157 await stat(file); 158 throw new Error(`File already exists: ${file}`); 159 } catch (err) { 160 if (err instanceof Error && "code" in err && err.code === "ENOENT") return; 161 throw err; 162 } 163} 164 165function resolveOutputFilePath(destination: string, filePath: string): string { 166 const relativePath = filePath 167 .replaceAll("\\", "/") 168 .replace(/^(?:[A-Za-z]:)?\/+/, ""); 169 return join(destination, ...relativePath.split("/").filter(Boolean)); 170}