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

Configure Feed

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

config modeled after atcute

+844 -156
+48 -9
lex-gen/cmd/gen-api.ts
··· 7 7 } from "../util.ts"; 8 8 import { genClientApi } from "../codegen/client.ts"; 9 9 import { formatGeneratedFiles } from "../codegen/util.ts"; 10 + import { loadLexiconConfig } from "../config.ts"; 11 + import { cleanupPullDirectory, pullLexicons } from "../pull.ts"; 12 + import process from "node:process"; 10 13 11 14 const command = new Command() 12 15 .description("Generate a TS client API") 13 16 .option("--js", "use .js extension for imports instead of .ts") 14 - .option("-o, --outdir <outdir>", "dir path to write to", { required: true }) 15 - .option("-i, --input <input...>", "paths of lexicon files to include", { 16 - required: true, 17 - }) 17 + .option("-o, --outdir <outdir>", "dir path to write to") 18 + .option("-i, --input <input...>", "paths of lexicon files to include") 19 + .option("--config <config>", "path to config file") 18 20 .action( 19 - async ({ outdir, input, js }) => { 20 - const lexicons = readAllLexicons(input); 21 + async ({ outdir, input, js, config: configPath }) => { 22 + const config = await loadLexiconConfig(configPath); 23 + const finalOutdir = outdir ?? config?.outdir; 24 + const finalInput = input ?? config?.files; 25 + 26 + if (!finalOutdir) { 27 + console.error("outdir is required (provide via -o/--outdir or config)"); 28 + if (typeof Deno !== "undefined") { 29 + Deno.exit(1); 30 + } else { 31 + process.exit(1); 32 + } 33 + } 34 + 35 + if (!finalInput || finalInput.length === 0) { 36 + console.error( 37 + "input is required (provide via -i/--input or config.files)", 38 + ); 39 + if (typeof Deno !== "undefined") { 40 + Deno.exit(1); 41 + } else { 42 + process.exit(1); 43 + } 44 + } 45 + 46 + if (config?.pull) { 47 + await pullLexicons(config.pull); 48 + } 49 + 50 + const useJs = js ?? false; 51 + const importSuffix = config?.modules?.importSuffix; 52 + const mappings = config?.mappings; 53 + const lexicons = readAllLexicons(finalInput); 21 54 const api = await genClientApi(lexicons, { 22 - useJsExtension: js, 55 + useJsExtension: useJs, 56 + importSuffix: importSuffix, 57 + mappings: mappings, 23 58 }); 24 - const diff = genFileDiff(outdir, api); 59 + const diff = genFileDiff(finalOutdir, api); 25 60 console.log("This will write the following files:"); 26 61 printFileDiff(diff); 27 62 applyFileDiff(diff); 28 63 if (typeof Deno !== "undefined") { 29 - await formatGeneratedFiles(outdir); 64 + await formatGeneratedFiles(finalOutdir); 30 65 } 31 66 console.log("API generated."); 67 + 68 + if (config?.pull) { 69 + cleanupPullDirectory(config.pull); 70 + } 32 71 }, 33 72 ); 34 73
+44 -7
lex-gen/cmd/gen-md.ts
··· 1 1 import { Command } from "@cliffy/command"; 2 2 import { readAllLexicons } from "../util.ts"; 3 3 import * as mdGen from "../mdgen/index.ts"; 4 + import { loadLexiconConfig } from "../config.ts"; 5 + import { cleanupPullDirectory, pullLexicons } from "../pull.ts"; 4 6 import process from "node:process"; 5 7 6 8 const isDeno = typeof Deno !== "undefined"; 7 9 8 10 const command = new Command() 9 11 .description("Generate markdown documentation") 10 - .option("-o, --output <outfile>", "Output file path", { required: true }) 11 - .option("-i, --input <infile>", "Input file path", { required: true }) 12 + .option("-o, --output <outfile>", "Output file path") 13 + .option("-i, --input <infile>", "Input file path") 14 + .option("--config <config>", "path to config file") 12 15 .action( 13 - async ({ output, input }) => { 14 - if (!output.endsWith(".md")) { 16 + async ({ output, input, config: configPath }) => { 17 + const config = await loadLexiconConfig(configPath); 18 + const finalOutput = output ?? 19 + (config?.outdir ? `${config.outdir}/docs.md` : undefined); 20 + const finalInput = input ?? config?.files?.[0]; 21 + 22 + if (!finalOutput) { 23 + console.error("output is required (provide via -o/--output or config)"); 24 + if (isDeno) { 25 + Deno.exit(1); 26 + } else { 27 + process.exit(1); 28 + } 29 + } 30 + 31 + if (!finalInput) { 32 + console.error( 33 + "input is required (provide via -i/--input or config.files)", 34 + ); 35 + if (isDeno) { 36 + Deno.exit(1); 37 + } else { 38 + process.exit(1); 39 + } 40 + } 41 + 42 + if (!finalOutput.endsWith(".md")) { 15 43 console.error( 16 - "Must supply the path to a .md file as the first parameter", 44 + "Must supply the path to a .md file", 17 45 ); 18 46 if (isDeno) { 19 47 Deno.exit(1); ··· 21 49 process.exit(1); 22 50 } 23 51 } 24 - const lexicons = readAllLexicons(input); 25 - await mdGen.process(output, lexicons); 52 + 53 + if (config?.pull) { 54 + await pullLexicons(config.pull); 55 + } 56 + 57 + const lexicons = readAllLexicons(finalInput); 58 + await mdGen.process(finalOutput, lexicons); 59 + 60 + if (config?.pull) { 61 + cleanupPullDirectory(config.pull); 62 + } 26 63 }, 27 64 ); 28 65
+50 -9
lex-gen/cmd/gen-server.ts
··· 7 7 } from "../util.ts"; 8 8 import { formatGeneratedFiles } from "../codegen/util.ts"; 9 9 import { genServerApi } from "../codegen/server.ts"; 10 + import { loadLexiconConfig } from "../config.ts"; 11 + import { cleanupPullDirectory, pullLexicons } from "../pull.ts"; 12 + import process from "node:process"; 13 + 14 + const isDeno = typeof Deno !== "undefined"; 10 15 11 16 const command = new Command() 12 17 .description("Generate a TS server API") 13 18 .option("--js", "use .js extension for imports instead of .ts") 14 - .option("-o, --outdir <outdir>", "dir path to write to", { required: true }) 15 - .option("-i, --input <input...>", "paths of lexicon files to include", { 16 - required: true, 17 - }) 19 + .option("-o, --outdir <outdir>", "dir path to write to") 20 + .option("-i, --input <input...>", "paths of lexicon files to include") 21 + .option("--config <config>", "path to config file") 18 22 .action( 19 - async ({ outdir, input, js }) => { 23 + async ({ outdir, input, js, config: configPath }) => { 24 + const config = await loadLexiconConfig(configPath); 25 + const finalOutdir = outdir ?? config?.outdir; 26 + const finalInput = input ?? config?.files; 27 + 28 + if (!finalOutdir) { 29 + console.error("outdir is required (provide via -o/--outdir or config)"); 30 + if (isDeno) { 31 + Deno.exit(1); 32 + } else { 33 + process.exit(1); 34 + } 35 + } 36 + 37 + if (!finalInput || finalInput.length === 0) { 38 + console.error( 39 + "input is required (provide via -i/--input or config.files)", 40 + ); 41 + if (isDeno) { 42 + Deno.exit(1); 43 + } else { 44 + process.exit(1); 45 + } 46 + } 47 + 48 + if (config?.pull) { 49 + await pullLexicons(config.pull); 50 + } 51 + 52 + const useJs = js ?? false; 53 + const importSuffix = config?.modules?.importSuffix; 54 + const mappings = config?.mappings; 20 55 console.log("Generating API..."); 21 - const lexicons = readAllLexicons(input); 56 + const lexicons = readAllLexicons(finalInput); 22 57 const api = await genServerApi(lexicons, { 23 - useJsExtension: js, 58 + useJsExtension: useJs, 59 + importSuffix: importSuffix, 60 + mappings: mappings, 24 61 }); 25 62 console.log("API generated."); 26 - const diff = genFileDiff(outdir, api); 63 + const diff = genFileDiff(finalOutdir, api); 27 64 console.log("This will write the following files:"); 28 65 printFileDiff(diff); 29 66 applyFileDiff(diff); 30 67 if (typeof Deno !== "undefined") { 31 - await formatGeneratedFiles(outdir); 68 + await formatGeneratedFiles(finalOutdir); 32 69 } 33 70 console.log("API generated."); 71 + 72 + if (config?.pull) { 73 + cleanupPullDirectory(config.pull); 74 + } 34 75 }, 35 76 ); 36 77
+31 -5
lex-gen/cmd/gen-ts-obj.ts
··· 1 1 import { Command } from "@cliffy/command"; 2 2 import { genTsObj, readAllLexicons } from "../util.ts"; 3 + import { loadLexiconConfig } from "../config.ts"; 4 + import { cleanupPullDirectory, pullLexicons } from "../pull.ts"; 5 + import process from "node:process"; 6 + 7 + const isDeno = typeof Deno !== "undefined"; 3 8 4 9 const command = new Command() 5 10 .description("Generate a TS file that exports an array of lexicons") 6 - .option("-i, --input <lexicons>", "paths of the lexicon files to include", { 7 - required: true, 8 - }) 9 - .action(({ input }) => { 10 - const lexicons = readAllLexicons(input); 11 + .option("-i, --input <lexicons>", "paths of the lexicon files to include") 12 + .option("--config <config>", "path to config file") 13 + .action(async ({ input, config: configPath }) => { 14 + const config = await loadLexiconConfig(configPath); 15 + const finalInput = input ?? config?.files; 16 + 17 + if (!finalInput || finalInput.length === 0) { 18 + console.error( 19 + "input is required (provide via -i/--input or config.files)", 20 + ); 21 + if (isDeno) { 22 + Deno.exit(1); 23 + } else { 24 + process.exit(1); 25 + } 26 + } 27 + 28 + if (config?.pull) { 29 + await pullLexicons(config.pull); 30 + } 31 + 32 + const lexicons = readAllLexicons(finalInput); 11 33 console.log(genTsObj(lexicons)); 34 + 35 + if (config?.pull) { 36 + cleanupPullDirectory(config.pull); 37 + } 12 38 }); 13 39 14 40 export default command;
+14 -12
lex-gen/codegen/client.ts
··· 48 48 const nsidTree = lexiconsToDefTree(lexiconDocs); 49 49 const nsidTokens = schemasToNsidTokens(lexiconDocs); 50 50 for (const lexiconDoc of lexiconDocs) { 51 - api.files.push(await lexiconTs(project, lexicons, lexiconDoc)); 51 + api.files.push(await lexiconTs(project, lexicons, lexiconDoc, options)); 52 52 } 53 53 api.files.push(await utilTs(project)); 54 54 api.files.push(await lexiconsTs(project, lexiconDocs, options)); ··· 66 66 options?: CodeGenOptions, 67 67 ) => 68 68 gen(project, "/index.ts", (file) => { 69 - const extension = options?.useJsExtension ? ".js" : ".ts"; 69 + const importExtension = options?.importSuffix ?? 70 + (options?.useJsExtension ? ".js" : ".ts"); 70 71 //= import { XrpcClient, type FetchHandler, type FetchHandlerOptions } from '@atp/xrpc' 71 72 const xrpcImport = file.addImportDeclaration({ 72 73 moduleSpecifier: "@atp/xrpc", ··· 78 79 ]); 79 80 //= import {schemas} from './lexicons.ts' 80 81 file 81 - .addImportDeclaration({ moduleSpecifier: `./lexicons${extension}` }) 82 + .addImportDeclaration({ moduleSpecifier: `./lexicons${importExtension}` }) 82 83 .addNamedImports([{ name: "schemas" }]); 83 84 84 85 // Check if any lexicon docs use cid-link types ··· 110 111 111 112 //= import { type OmitKey, type Un$Typed } from './util.ts' 112 113 file 113 - .addImportDeclaration({ moduleSpecifier: `./util${extension}` }) 114 + .addImportDeclaration({ moduleSpecifier: `./util${importExtension}` }) 114 115 .addNamedImports([ 115 116 { name: "OmitKey", isTypeOnly: true }, 116 117 { name: "Un$Typed", isTypeOnly: true }, ··· 120 121 for (const lexicon of lexiconDocs) { 121 122 const moduleSpecifier = `./types/${ 122 123 lexicon.id.split(".").join("/") 123 - }${extension}`; 124 + }${importExtension}`; 124 125 file 125 126 .addImportDeclaration({ moduleSpecifier }) 126 127 .setNamespaceImport(toTitleCase(lexicon.id)); ··· 476 477 project: Project, 477 478 lexicons: Lexicons, 478 479 lexiconDoc: LexiconDoc, 480 + options?: CodeGenOptions, 479 481 ) => 480 482 gen( 481 483 project, ··· 499 501 500 502 genCommonImports(file, lexiconDoc.id, lexiconDoc); 501 503 502 - const imports: Set<string> = new Set(); 504 + const imports: Map<string, Set<string>> = new Map(); 503 505 for (const defId in lexiconDoc.defs) { 504 506 const def = lexiconDoc.defs[defId]; 505 507 const lexUri = `${lexiconDoc.id}#${defId}`; 506 508 if (defId === "main") { 507 509 if (def.type === "query" || def.type === "procedure") { 508 510 genXrpcParams(file, lexicons, lexUri, false); 509 - genXrpcInput(file, imports, lexicons, lexUri, false); 510 - genXrpcOutput(file, imports, lexicons, lexUri); 511 + genXrpcInput(file, imports, lexicons, lexUri, false, options); 512 + genXrpcOutput(file, imports, lexicons, lexUri, false, options); 511 513 genClientXrpcCommon(file, lexicons, lexUri); 512 514 } else if (def.type === "subscription") { 513 515 continue; 514 516 } else if (def.type === "record") { 515 - genRecord(file, imports, lexicons, lexUri); 517 + genRecord(file, imports, lexicons, lexUri, options); 516 518 } else { 517 - genUserType(file, imports, lexicons, lexUri); 519 + genUserType(file, imports, lexicons, lexUri, options); 518 520 } 519 521 } else { 520 - genUserType(file, imports, lexicons, lexUri); 522 + genUserType(file, imports, lexicons, lexUri, options); 521 523 } 522 524 } 523 - genImports(file, imports, lexiconDoc.id); 525 + genImports(file, imports, lexiconDoc.id, options); 524 526 return Promise.resolve(); 525 527 }, 526 528 );
+4 -3
lex-gen/codegen/common.ts
··· 21 21 ) => 22 22 gen(project, "/util.ts", (file) => { 23 23 file.replaceWithText(` 24 - import { type ValidationResult } from '@atp/lexicon' 24 + import type { ValidationResult } from '@atp/lexicon' 25 25 26 26 export type OmitKey<T, K extends keyof T> = { 27 27 [K2 in keyof T as K2 extends K ? never : K2]: T[K2] ··· 144 144 options?: CodeGenOptions, 145 145 ) => 146 146 gen(project, "/lexicons.ts", (file) => { 147 - const extension = options?.useJsExtension ? ".js" : ".ts"; 147 + const importExtension = options?.importSuffix ?? 148 + (options?.useJsExtension ? ".js" : ".ts"); 148 149 const nsidToEnum = (nsid: string): string => { 149 150 return nsid 150 151 .split(".") ··· 166 167 167 168 //= import { is$typed, maybe$typed, type $Typed } from "./util${extension}" 168 169 file 169 - .addImportDeclaration({ moduleSpecifier: `./util${extension}` }) 170 + .addImportDeclaration({ moduleSpecifier: `./util${importExtension}` }) 170 171 .addNamedImports([ 171 172 { name: "is$typed" }, 172 173 { name: "maybe$typed" },
+202 -88
lex-gen/codegen/lex-gen.ts
··· 18 18 toTitleCase, 19 19 } from "./util.ts"; 20 20 import type { LexiconDoc, LexUserType } from "@atp/lexicon"; 21 + import type { ImportMapping } from "../types.ts"; 21 22 22 23 interface Commentable { 23 24 addJsDoc: ({ description }: { description: string }) => JSDoc; ··· 38 39 lexiconDoc: LexiconDoc, 39 40 options?: CodeGenOptions, 40 41 ) { 41 - const extension = options?.useJsExtension ? ".js" : ".ts"; 42 + const importExtension = options?.importSuffix ?? 43 + (options?.useJsExtension ? ".js" : ".ts"); 42 44 const needsBlobRef = Object.values(lexiconDoc.defs).some((def: LexUserType) => 43 45 def.type === "blob" || 44 46 (def.type === "object" && ··· 92 94 const needsTypedValidation = Object.values(lexiconDoc.defs).some(( 93 95 def: LexUserType, 94 96 ) => def.type === "record" || def.type === "object"); 97 + 98 + const needsId = Object.values(lexiconDoc.defs).some(( 99 + def: LexUserType, 100 + ) => def.type === "token") || needsTypedValidation; 95 101 96 102 const needsUnionType = Object.values(lexiconDoc.defs).some( 97 103 (def: LexUserType) => { ··· 181 187 }, 182 188 ); 183 189 184 - const needsIdConstant = Object.values(lexiconDoc.defs).some(( 185 - def: LexUserType, 186 - ) => 187 - (def.type === "string" && 188 - (def.enum?.length || def.const || def.knownValues?.length)) || 189 - def.type === "record" || 190 - def.type === "object" 191 - ); 192 - 193 190 //= import {BlobRef} from '@atp/lexicon' 194 191 if (needsBlobRef) { 195 - file 196 - .addImportDeclaration({ 197 - moduleSpecifier: "@atp/lexicon", 198 - }) 199 - .addNamedImports([{ name: "BlobRef" }]); 192 + file.addImportDeclaration({ 193 + isTypeOnly: true, 194 + moduleSpecifier: "@atp/lexicon", 195 + namedImports: [{ name: "BlobRef" }], 196 + }); 200 197 } 201 198 202 199 //= import {CID} from 'multiformats/cid' 203 200 if (needsCID) { 204 - file 205 - .addImportDeclaration({ 206 - moduleSpecifier: "multiformats/cid", 207 - }) 208 - .addNamedImports([{ name: "CID" }]); 201 + file.addImportDeclaration({ 202 + isTypeOnly: true, 203 + moduleSpecifier: "multiformats/cid", 204 + namedImports: [{ name: "CID" }], 205 + }); 209 206 } 210 207 208 + const utilPath = `${ 209 + baseNsid 210 + .split(".") 211 + .map((_str) => "..") 212 + .join("/") 213 + }/util${importExtension}`; 214 + 211 215 if (needsTypedValidation) { 212 216 //= import { validate as _validate } from '../../lexicons.ts' 213 217 file ··· 217 221 .split(".") 218 222 .map((_str) => "..") 219 223 .join("/") 220 - }/lexicons${extension}`, 224 + }/lexicons${importExtension}`, 221 225 }) 222 226 .addNamedImports([{ name: "validate", alias: "_validate" }]); 223 227 224 - //= import { is$typed as _is$typed } from '../[...]/util.ts' 225 - file 226 - .addImportDeclaration({ 227 - moduleSpecifier: `${ 228 - baseNsid 229 - .split(".") 230 - .map((_str) => "..") 231 - .join("/") 232 - }/util${extension}`, 233 - }) 234 - .addNamedImports([ 235 - { name: "is$typed", alias: "_is$typed" }, 236 - ]); 237 - 238 228 // tsc adds protection against circular imports, which hurts bundle size. 239 229 // Since we know that lexicon.ts and util.ts do not depend on the file being 240 230 // generated, we can safely bypass this protection. ··· 250 240 }); 251 241 } 252 242 253 - if (needsIdConstant) { 243 + const utilImports: Array< 244 + { name: string; alias?: string; isTypeOnly?: boolean } 245 + > = []; 246 + if (needsTypedValidation) { 247 + utilImports.push({ name: "is$typed", alias: "_is$typed" }); 248 + } 249 + if (needsUnionType) { 250 + utilImports.push({ name: "$Typed", isTypeOnly: true }); 251 + } 252 + 253 + if (utilImports.length > 0) { 254 + const allTypeOnly = utilImports.every((imp) => imp.isTypeOnly); 255 + if (allTypeOnly) { 256 + file.addImportDeclaration({ 257 + isTypeOnly: true, 258 + moduleSpecifier: utilPath, 259 + namedImports: utilImports.map((imp) => ({ 260 + name: imp.name, 261 + alias: imp.alias, 262 + })), 263 + }); 264 + } else { 265 + file 266 + .addImportDeclaration({ 267 + moduleSpecifier: utilPath, 268 + }) 269 + .addNamedImports(utilImports); 270 + } 271 + } 272 + 273 + if (needsId) { 254 274 //= const id = "{baseNsid}" 255 275 file.addVariableStatement({ 256 276 isExported: false, // Do not export to allow tree-shaking ··· 258 278 declarations: [{ name: "id", initializer: JSON.stringify(baseNsid) }], 259 279 }); 260 280 } 261 - 262 - if (needsUnionType) { 263 - //= import { type $Typed } from '../[...]/util.ts' 264 - file 265 - .addImportDeclaration({ 266 - moduleSpecifier: `${ 267 - baseNsid 268 - .split(".") 269 - .map((_str) => "..") 270 - .join("/") 271 - }/util${extension}`, 272 - }) 273 - .addNamedImports([ 274 - { name: "$Typed", isTypeOnly: true }, 275 - ]); 276 - } 277 281 } 278 282 279 283 export function genImports( 280 284 file: SourceFile, 281 - imports: Set<string>, 285 + imports: Map<string, Set<string>>, 282 286 baseNsid: string, 283 287 options?: CodeGenOptions, 284 288 ) { 285 289 const startPath = "/" + baseNsid.split(".").slice(0, -1).join("/"); 286 - const extension = options?.useJsExtension ? ".js" : ".ts"; 290 + const importExtension = options?.importSuffix ?? 291 + (options?.useJsExtension ? ".js" : ".ts"); 292 + const mappings = options?.mappings; 287 293 288 - for (const nsid of imports) { 289 - const targetPath = "/" + nsid.split(".").join("/") + extension; 290 - let resolvedPath = getRelativePath(startPath, targetPath); 291 - if (!resolvedPath.startsWith(".")) { 292 - resolvedPath = `./${resolvedPath}`; 294 + for (const [nsid, types] of imports) { 295 + const mapping = resolveExternalImport(nsid, mappings); 296 + if (mapping) { 297 + if (typeof mapping.imports === "string") { 298 + file.addImportDeclaration({ 299 + isTypeOnly: true, 300 + moduleSpecifier: mapping.imports, 301 + namespaceImport: toTitleCase(nsid), 302 + }); 303 + } else { 304 + const result = mapping.imports(nsid); 305 + if (result.type === "namespace") { 306 + file.addImportDeclaration({ 307 + isTypeOnly: true, 308 + moduleSpecifier: result.from, 309 + namespaceImport: toTitleCase(nsid), 310 + }); 311 + } else { 312 + const namedImports = Array.from(types).map((typeName) => ({ 313 + name: toTitleCase(typeName), 314 + isTypeOnly: true, 315 + })); 316 + file.addImportDeclaration({ 317 + isTypeOnly: true, 318 + moduleSpecifier: result.from, 319 + namedImports, 320 + }); 321 + } 322 + } 323 + } else { 324 + const targetPath = "/" + nsid.split(".").join("/") + importExtension; 325 + let resolvedPath = getRelativePath(startPath, targetPath); 326 + if (!resolvedPath.startsWith(".")) { 327 + resolvedPath = `./${resolvedPath}`; 328 + } 329 + file.addImportDeclaration({ 330 + isTypeOnly: true, 331 + moduleSpecifier: resolvedPath, 332 + namespaceImport: toTitleCase(nsid), 333 + }); 293 334 } 294 - file.addImportDeclaration({ 295 - isTypeOnly: true, 296 - moduleSpecifier: resolvedPath, 297 - namespaceImport: toTitleCase(nsid), 298 - }); 299 335 } 300 336 } 301 337 302 338 export function genUserType( 303 339 file: SourceFile, 304 - imports: Set<string>, 340 + imports: Map<string, Set<string>>, 305 341 lexicons: Lexicons, 306 342 lexUri: string, 343 + options?: CodeGenOptions, 307 344 ) { 308 345 const def = lexicons.getDefOrThrow(lexUri); 309 346 switch (def.type) { 310 347 case "array": 311 - genArray(file, imports, lexUri, def); 348 + genArray(file, imports, lexUri, def, options); 312 349 break; 313 350 case "token": 314 351 genToken(file, lexUri, def); ··· 317 354 const ifaceName: string = toTitleCase(getHash(lexUri)); 318 355 genObject(file, imports, lexUri, def, ifaceName, { 319 356 typeProperty: true, 320 - }); 357 + }, options); 321 358 genObjHelpers(file, lexUri, ifaceName, { 322 359 requireTypeProperty: false, 323 360 }); ··· 343 380 344 381 function genObject( 345 382 file: SourceFile, 346 - imports: Set<string>, 383 + imports: Map<string, Set<string>>, 347 384 lexUri: string, 348 385 def: LexObject, 349 386 ifaceName: string, ··· 356 393 allowUnknownProperties?: boolean; 357 394 typeProperty?: boolean | "required"; 358 395 } = {}, 396 + options?: CodeGenOptions, 359 397 ) { 360 398 const iface = file.addInterface({ 361 399 name: ifaceName, ··· 391 429 if (propDef.type === "ref" || propDef.type === "union") { 392 430 //= propName: External|External 393 431 const types = propDef.type === "union" 394 - ? propDef.refs.map((ref) => refToUnionType(ref, lexUri, imports)) 395 - : [refToType(propDef.ref, stripScheme(stripHash(lexUri)), imports)]; 432 + ? propDef.refs.map((ref) => 433 + refToUnionType(ref, lexUri, imports, options?.mappings) 434 + ) 435 + : [ 436 + refToType( 437 + propDef.ref, 438 + stripScheme(stripHash(lexUri)), 439 + imports, 440 + options?.mappings, 441 + ), 442 + ]; 396 443 if (propDef.type === "union" && !propDef.closed) { 397 444 types.push("{ $type: string }"); 398 445 } ··· 413 460 propDef.items.ref, 414 461 stripScheme(stripHash(lexUri)), 415 462 imports, 463 + options?.mappings, 416 464 ), 417 465 { 418 466 nullable: propNullable, ··· 422 470 }); 423 471 } else if (propDef.items.type === "union") { 424 472 const types = propDef.items.refs.map((ref) => 425 - refToUnionType(ref, lexUri, imports) 473 + refToUnionType(ref, lexUri, imports, options?.mappings) 426 474 ); 427 475 if (!propDef.items.closed) { 428 476 types.push("{ $type: string }"); ··· 490 538 491 539 export function genArray( 492 540 file: SourceFile, 493 - imports: Set<string>, 541 + imports: Map<string, Set<string>>, 494 542 lexUri: string, 495 543 def: LexArray, 544 + options?: CodeGenOptions, 496 545 ) { 497 546 if (def.items.type === "ref") { 498 547 file.addTypeAlias({ ··· 502 551 def.items.ref, 503 552 stripScheme(stripHash(lexUri)), 504 553 imports, 554 + options?.mappings, 505 555 ) 506 556 }[]`, 507 557 isExported: true, 508 558 }); 509 559 } else if (def.items.type === "union") { 510 560 const types = def.items.refs.map((ref) => 511 - refToUnionType(ref, lexUri, imports) 561 + refToUnionType(ref, lexUri, imports, options?.mappings) 512 562 ); 513 563 if (!def.items.closed) { 514 564 types.push("{ $type: string }"); ··· 612 662 613 663 export function genXrpcInput( 614 664 file: SourceFile, 615 - imports: Set<string>, 665 + imports: Map<string, Set<string>>, 616 666 lexicons: Lexicons, 617 667 lexUri: string, 618 668 defaultsArePresent = true, 669 + options?: CodeGenOptions, 619 670 ) { 620 671 const def = lexicons.getDefOrThrow(lexUri, ["query", "procedure"]); 621 672 ··· 625 676 626 677 const types = def.input.schema.type === "union" 627 678 ? def.input.schema.refs.map((ref) => 628 - refToUnionType(ref, lexUri, imports) 679 + refToUnionType(ref, lexUri, imports, options?.mappings) 629 680 ) 630 681 : [ 631 682 refToType( 632 683 def.input.schema.ref, 633 684 stripScheme(stripHash(lexUri)), 634 685 imports, 686 + options?.mappings, 635 687 ), 636 688 ]; 637 689 ··· 647 699 //= export interface InputSchema {...} 648 700 genObject(file, imports, lexUri, def.input.schema, `InputSchema`, { 649 701 defaultsArePresent, 650 - }); 702 + }, options); 651 703 } 652 704 } else if (def.type === "procedure" && def.input?.encoding) { 653 705 //= export type InputSchema = string | Uint8Array | Blob ··· 668 720 669 721 export function genXrpcOutput( 670 722 file: SourceFile, 671 - imports: Set<string>, 723 + imports: Map<string, Set<string>>, 672 724 lexicons: Lexicons, 673 725 lexUri: string, 674 726 defaultsArePresent = true, 727 + options?: CodeGenOptions, 675 728 ) { 676 729 const def = lexicons.getDefOrThrow(lexUri, [ 677 730 "query", ··· 686 739 if (schema.type === "ref" || schema.type === "union") { 687 740 //= export type OutputSchema = ... 688 741 const types = schema.type === "union" 689 - ? schema.refs.map((ref) => refToUnionType(ref, lexUri, imports)) 690 - : [refToType(schema.ref, stripScheme(stripHash(lexUri)), imports)]; 742 + ? schema.refs.map((ref) => 743 + refToUnionType(ref, lexUri, imports, options?.mappings) 744 + ) 745 + : [ 746 + refToType( 747 + schema.ref, 748 + stripScheme(stripHash(lexUri)), 749 + imports, 750 + options?.mappings, 751 + ), 752 + ]; 691 753 if (schema.type === "union" && !schema.closed) { 692 754 types.push("{ $type: string }"); 693 755 } ··· 711 773 //= export interface OutputSchema {...} 712 774 genObject(file, imports, lexUri, schema, `OutputSchema`, { 713 775 defaultsArePresent, 714 - }); 776 + }, options); 715 777 } 716 778 } 717 779 } ··· 719 781 720 782 export function genRecord( 721 783 file: SourceFile, 722 - imports: Set<string>, 784 + imports: Map<string, Set<string>>, 723 785 lexicons: Lexicons, 724 786 lexUri: string, 787 + options?: CodeGenOptions, 725 788 ) { 726 789 const def = lexicons.getDefOrThrow(lexUri, ["record"]); 727 790 ··· 730 793 defaultsArePresent: true, 731 794 allowUnknownProperties: true, 732 795 typeProperty: "required", 733 - }); 796 + }, options); 734 797 735 798 //= export function isRecord(v: unknown): v is Record {...} 736 799 genObjHelpers(file, lexUri, "Record", { 737 800 requireTypeProperty: true, 738 801 }); 802 + 803 + const hash = getHash(lexUri); 804 + if (hash === "main") { 805 + //= export type Main = Record 806 + file.addTypeAlias({ 807 + name: "Main", 808 + type: "Record", 809 + isExported: true, 810 + }); 811 + } 739 812 } 740 813 741 814 function genObjHelpers( ··· 810 883 function refToUnionType( 811 884 ref: string, 812 885 lexUri: string, 813 - imports: Set<string>, 886 + imports: Map<string, Set<string>>, 887 + mappings?: ImportMapping[], 814 888 ): string { 815 889 const baseNsid = stripScheme(stripHash(lexUri)); 816 - return `$Typed<${refToType(ref, baseNsid, imports)}>`; 890 + return `$Typed<${refToType(ref, baseNsid, imports, mappings)}>`; 891 + } 892 + 893 + function resolveExternalImport( 894 + nsid: string, 895 + mappings?: ImportMapping[], 896 + ): ImportMapping | undefined { 897 + if (!mappings) return undefined; 898 + return mappings.find((mapping) => { 899 + return mapping.nsid.some((pattern) => { 900 + if (pattern.endsWith(".*")) { 901 + return nsid.startsWith(pattern.slice(0, -1)); 902 + } 903 + return nsid === pattern; 904 + }); 905 + }); 817 906 } 818 907 819 908 function refToType( 820 909 ref: string, 821 910 baseNsid: string, 822 - imports: Set<string>, 911 + imports: Map<string, Set<string>>, 912 + mappings?: ImportMapping[], 823 913 ): string { 824 - // TODO: import external types! 825 914 let [refBase, refHash] = ref.split("#"); 826 915 refBase = stripScheme(refBase); 827 916 if (!refHash) refHash = "main"; ··· 831 920 return toTitleCase(refHash); 832 921 } 833 922 834 - // external 835 - imports.add(refBase); 923 + // external - check if there's a mapping 924 + const mapping = resolveExternalImport(refBase, mappings); 925 + if (mapping) { 926 + if (!imports.has(refBase)) { 927 + imports.set(refBase, new Set()); 928 + } 929 + const types = imports.get(refBase)!; 930 + types.add(refHash); 931 + 932 + if (typeof mapping.imports === "string") { 933 + // String mapping means namespace import 934 + return `${toTitleCase(refBase)}.${toTitleCase(refHash)}`; 935 + } else { 936 + const result = mapping.imports(refBase); 937 + if (result.type === "namespace") { 938 + return `${toTitleCase(refBase)}.${toTitleCase(refHash)}`; 939 + } else { 940 + // Named import - return just the type name 941 + return toTitleCase(refHash); 942 + } 943 + } 944 + } 945 + 946 + // external - no mapping, use relative import 947 + if (!imports.has(refBase)) { 948 + imports.set(refBase, new Set()); 949 + } 836 950 return `${toTitleCase(refBase)}.${toTitleCase(refHash)}`; 837 951 } 838 952
+20 -20
lex-gen/codegen/server.ts
··· 58 58 options?: CodeGenOptions, 59 59 ) => 60 60 gen(project, "/index.ts", (file) => { 61 - const extension = options?.useJsExtension ? ".js" : ".ts"; 61 + const importExtension = options?.importSuffix ?? 62 + (options?.useJsExtension ? ".js" : ".ts"); 62 63 63 64 // Check if there are any subscription types 64 65 const hasSubscriptions = lexiconDocs.some((doc) => ··· 69 70 const namedImports = [ 70 71 { name: "Auth", isTypeOnly: true }, 71 72 { name: "Options", alias: "XrpcOptions", isTypeOnly: true }, 72 - { name: "Server", alias: "XrpcServer" }, 73 + { name: "Server", alias: "XrpcServer", isTypeOnly: true }, 73 74 { name: "MethodConfigOrHandler", isTypeOnly: true }, 74 75 { name: "createServer", alias: "createXrpcServer" }, 75 76 ]; ··· 88 89 //= import {schemas} from './lexicons.ts' 89 90 file 90 91 .addImportDeclaration({ 91 - moduleSpecifier: "./lexicons.ts", 92 + moduleSpecifier: `./lexicons${importExtension}`, 92 93 }) 93 94 .addNamedImport({ 94 95 name: "schemas", ··· 103 104 ) { 104 105 continue; 105 106 } 106 - file 107 - .addImportDeclaration({ 108 - moduleSpecifier: `./types/${ 109 - lexiconDoc.id.split(".").join("/") 110 - }${extension}`, 111 - }) 112 - .setNamespaceImport(toTitleCase(lexiconDoc.id)); 107 + file.addImportDeclaration({ 108 + isTypeOnly: true, 109 + moduleSpecifier: `./types/${ 110 + lexiconDoc.id.split(".").join("/") 111 + }${importExtension}`, 112 + namespaceImport: toTitleCase(lexiconDoc.id), 113 + }); 113 114 } 114 115 115 116 // generate token enums ··· 286 287 ) => 287 288 gen( 288 289 project, 289 - `/types/${lexiconDoc.id.split(".").join("/")}${ 290 - options?.useJsExtension ? ".js" : ".ts" 291 - }`, 290 + `/types/${lexiconDoc.id.split(".").join("/")}.ts`, 292 291 (file) => { 293 292 const main = lexiconDoc.defs.main; 294 293 if (main?.type === "query" || main?.type === "procedure") { ··· 304 303 305 304 genCommonImports(file, lexiconDoc.id, lexiconDoc); 306 305 307 - const imports: Set<string> = new Set(); 306 + const imports: Map<string, Set<string>> = new Map(); 308 307 for (const defId in lexiconDoc.defs) { 309 308 const def = lexiconDoc.defs[defId]; 310 309 const lexUri = `${lexiconDoc.id}#${defId}`; 311 310 if (defId === "main") { 312 311 if (def.type === "query" || def.type === "procedure") { 313 312 genXrpcParams(file, lexicons, lexUri); 314 - genXrpcInput(file, imports, lexicons, lexUri); 315 - genXrpcOutput(file, imports, lexicons, lexUri, false); 313 + genXrpcInput(file, imports, lexicons, lexUri, false, options); 314 + genXrpcOutput(file, imports, lexicons, lexUri, false, options); 316 315 genServerXrpcMethod(file, lexicons, lexUri); 317 316 } else if (def.type === "subscription") { 318 317 genXrpcParams(file, lexicons, lexUri); 319 - genXrpcOutput(file, imports, lexicons, lexUri, false); 318 + genXrpcOutput(file, imports, lexicons, lexUri, false, options); 320 319 genServerXrpcStreaming(file, lexicons, lexUri); 321 320 } else if (def.type === "record") { 322 - genRecord(file, imports, lexicons, lexUri); 321 + genRecord(file, imports, lexicons, lexUri, options); 323 322 } else { 324 - genUserType(file, imports, lexicons, lexUri); 323 + genUserType(file, imports, lexicons, lexUri, options); 325 324 } 326 325 } else { 327 - genUserType(file, imports, lexicons, lexUri); 326 + genUserType(file, imports, lexicons, lexUri, options); 328 327 } 329 328 } 330 329 genImports(file, imports, lexiconDoc.id, options); ··· 439 438 const def = lexicons.getDefOrThrow(lexUri, ["subscription"]); 440 439 441 440 file.addImportDeclaration({ 441 + isTypeOnly: true, 442 442 moduleSpecifier: "@atp/xrpc-server", 443 443 namedImports: [{ name: "ErrorFrame" }], 444 444 });
+3
lex-gen/codegen/util.ts
··· 1 1 import type { LexiconDoc, LexUserType } from "@atp/lexicon"; 2 2 import { NSID } from "@atp/syntax"; 3 + import type { ImportMapping } from "../types.ts"; 3 4 4 5 export interface CodeGenOptions { 5 6 useJsExtension?: boolean; 7 + importSuffix?: string; 8 + mappings?: ImportMapping[]; 6 9 } 7 10 8 11 export interface DefTreeNodeUserType {
+151
lex-gen/config.ts
··· 1 + import { NSID } from "@atp/syntax"; 2 + import type { LexiconConfig } from "./types.ts"; 3 + 4 + function isValidLexiconPattern(pattern: string): boolean { 5 + if (pattern.endsWith(".*")) { 6 + try { 7 + NSID.parse(`${pattern.slice(0, -2)}.x`); 8 + return true; 9 + } catch { 10 + return false; 11 + } 12 + } 13 + return NSID.isValid(pattern); 14 + } 15 + 16 + function validateConfig(config: LexiconConfig): void { 17 + if (!config.outdir || config.outdir.length === 0) { 18 + throw new Error("outdir must not be empty"); 19 + } 20 + 21 + if (!config.files || config.files.length === 0) { 22 + throw new Error("files must include at least one glob pattern"); 23 + } 24 + 25 + for (const file of config.files) { 26 + if (!file || file.length === 0) { 27 + throw new Error("files must not contain empty strings"); 28 + } 29 + } 30 + 31 + if (config.mappings) { 32 + for (const mapping of config.mappings) { 33 + if (!mapping.nsid || mapping.nsid.length === 0) { 34 + throw new Error("mappings.nsid requires at least one pattern"); 35 + } 36 + 37 + for (const pattern of mapping.nsid) { 38 + if (!isValidLexiconPattern(pattern)) { 39 + throw new Error( 40 + `invalid NSID pattern: ${pattern} (must be valid NSID or end with .*)`, 41 + ); 42 + } 43 + } 44 + 45 + if (typeof mapping.imports === "string") { 46 + if (mapping.imports.length === 0) { 47 + throw new Error("mappings.imports must not be empty"); 48 + } 49 + } else if (typeof mapping.imports !== "function") { 50 + throw new Error("mappings.imports must be a string or function"); 51 + } 52 + } 53 + } 54 + 55 + if (config.modules?.importSuffix !== undefined) { 56 + if (config.modules.importSuffix.length === 0) { 57 + throw new Error("modules.importSuffix must not be empty"); 58 + } 59 + } 60 + 61 + if (config.pull) { 62 + if (!config.pull.outdir || config.pull.outdir.length === 0) { 63 + throw new Error("pull.outdir must not be empty"); 64 + } 65 + 66 + if (!config.pull.sources || config.pull.sources.length === 0) { 67 + throw new Error("pull.sources must include at least one source"); 68 + } 69 + 70 + for (const source of config.pull.sources) { 71 + if (source.type === "git") { 72 + if (!source.remote || source.remote.length === 0) { 73 + throw new Error("pull.sources[].remote must not be empty"); 74 + } 75 + 76 + if (source.ref !== undefined && source.ref.length === 0) { 77 + throw new Error("pull.sources[].ref must not be empty"); 78 + } 79 + 80 + if (!source.pattern || source.pattern.length === 0) { 81 + throw new Error( 82 + "pull.sources[].pattern must include at least one glob pattern", 83 + ); 84 + } 85 + 86 + for (const pattern of source.pattern) { 87 + if (!pattern || pattern.length === 0) { 88 + throw new Error( 89 + "pull.sources[].pattern must not contain empty strings", 90 + ); 91 + } 92 + } 93 + } 94 + } 95 + } 96 + } 97 + 98 + export function defineLexiconConfig(config: LexiconConfig): LexiconConfig { 99 + validateConfig(config); 100 + return config; 101 + } 102 + 103 + export async function loadLexiconConfig( 104 + configPath?: string, 105 + ): Promise<LexiconConfig | null> { 106 + if (!configPath) { 107 + const possiblePaths = [ 108 + "./lexicon.config.ts", 109 + "./lexicon.config.js", 110 + "./lexicon.config.json", 111 + ]; 112 + for (const path of possiblePaths) { 113 + try { 114 + if (typeof Deno !== "undefined") { 115 + const stat = Deno.statSync(path); 116 + if (stat.isFile) { 117 + configPath = path; 118 + break; 119 + } 120 + } 121 + } catch { 122 + continue; 123 + } 124 + } 125 + } 126 + 127 + if (!configPath) { 128 + return null; 129 + } 130 + 131 + try { 132 + if (configPath.endsWith(".json")) { 133 + const content = Deno.readTextFileSync(configPath); 134 + const parsed = JSON.parse(content); 135 + return defineLexiconConfig(parsed); 136 + } else { 137 + const module = await import( 138 + new URL(configPath, `file://${Deno.cwd()}/`).href 139 + ); 140 + const config = module.default ?? module.config; 141 + if (typeof config === "function") { 142 + return defineLexiconConfig(config()); 143 + } else { 144 + return defineLexiconConfig(config); 145 + } 146 + } 147 + } catch (error) { 148 + console.warn(`Failed to load config from ${configPath}:`, error); 149 + return null; 150 + } 151 + }
+11
lex-gen/mod.ts
··· 33 33 */ 34 34 import { Command } from "@cliffy/command"; 35 35 import { genApi, genMd, genServer, genTsObj } from "./cmd/index.ts"; 36 + import { defineLexiconConfig, loadLexiconConfig } from "./config.ts"; 36 37 import process from "node:process"; 38 + 39 + export { defineLexiconConfig, loadLexiconConfig }; 40 + export type { 41 + GitSourceConfig, 42 + ImportMapping, 43 + LexiconConfig, 44 + ModulesConfig, 45 + PullConfig, 46 + SourceConfig, 47 + } from "./types.ts"; 37 48 38 49 const isDeno = typeof Deno !== "undefined"; 39 50
+163
lex-gen/pull.ts
··· 1 + import { join } from "@std/path"; 2 + import { existsSync } from "@std/fs"; 3 + import { removeSync } from "@std/fs/unstable-remove"; 4 + import { mkdirSync } from "@std/fs/unstable-mkdir"; 5 + import { readFileSync } from "@std/fs/unstable-read-file"; 6 + import { writeFileSync } from "@std/fs/unstable-write-file"; 7 + import { readDirSync } from "@std/fs/unstable-read-dir"; 8 + import { statSync } from "@std/fs/unstable-stat"; 9 + import { globToRegExp } from "@std/path"; 10 + import process from "node:process"; 11 + import type { PullConfig } from "./types.ts"; 12 + 13 + function copyMatchingFiles( 14 + sourceDir: string, 15 + targetBase: string, 16 + relativePath: string, 17 + regex: RegExp, 18 + ): void { 19 + try { 20 + if (!existsSync(sourceDir)) return; 21 + const entries = Array.from(readDirSync(sourceDir)); 22 + for (const entry of entries) { 23 + const sourcePath = join(sourceDir, entry.name); 24 + const relPath = relativePath 25 + ? join(relativePath, entry.name) 26 + : entry.name; 27 + const testPath = relPath.startsWith("/") ? relPath : `/${relPath}`; 28 + 29 + if (statSync(sourcePath).isDirectory) { 30 + copyMatchingFiles(sourcePath, targetBase, relPath, regex); 31 + } else if (entry.name.endsWith(".json")) { 32 + if (regex.test(testPath) || regex.test(relPath)) { 33 + const targetPath = join(targetBase, relPath); 34 + mkdirSync(join(targetPath, ".."), { recursive: true }); 35 + const content = readFileSync(sourcePath); 36 + writeFileSync(targetPath, content); 37 + } 38 + } 39 + } 40 + } catch { 41 + // skip 42 + } 43 + } 44 + 45 + export async function pullLexicons(config: PullConfig): Promise<void> { 46 + const cwd = typeof Deno !== "undefined" ? Deno.cwd() : process.cwd(); 47 + const pullDir = join(cwd, config.outdir); 48 + 49 + if (config.clean && existsSync(pullDir)) { 50 + console.log(`Cleaning ${pullDir}...`); 51 + removeSync(pullDir); 52 + } 53 + 54 + mkdirSync(pullDir, { recursive: true }); 55 + 56 + for (const source of config.sources) { 57 + if (source.type === "git") { 58 + await pullFromGit(source, pullDir); 59 + } 60 + } 61 + } 62 + 63 + export function cleanupPullDirectory(config: PullConfig): void { 64 + if (!config.clean) { 65 + return; 66 + } 67 + 68 + const cwd = typeof Deno !== "undefined" ? Deno.cwd() : process.cwd(); 69 + const pullDir = join(cwd, config.outdir); 70 + 71 + if (existsSync(pullDir)) { 72 + try { 73 + removeSync(pullDir, { recursive: true }); 74 + } catch { 75 + // ignore cleanup errors 76 + } 77 + } 78 + } 79 + 80 + async function pullFromGit( 81 + source: { remote: string; ref?: string; pattern: string[] }, 82 + targetDir: string, 83 + ): Promise<void> { 84 + const cwd = typeof Deno !== "undefined" ? Deno.cwd() : process.cwd(); 85 + const tempDir = join(cwd, ".lex-gen-temp", crypto.randomUUID()); 86 + 87 + try { 88 + console.log(`Cloning ${source.remote}...`); 89 + const cloneArgs = [ 90 + "clone", 91 + "--depth", 92 + "1", 93 + "--filter=blob:none", 94 + "--sparse", 95 + ]; 96 + 97 + if (source.ref) { 98 + cloneArgs.push(`--branch=${source.ref}`); 99 + } 100 + 101 + cloneArgs.push(source.remote, tempDir); 102 + 103 + const cloneCmd = new Deno.Command("git", { 104 + args: cloneArgs, 105 + cwd, 106 + }); 107 + 108 + const cloneResult = await cloneCmd.output(); 109 + if (!cloneResult.success) { 110 + const error = new TextDecoder().decode(cloneResult.stderr); 111 + throw new Error(`Failed to clone repository: ${error}`); 112 + } 113 + 114 + const sparseCheckoutCmd = new Deno.Command("git", { 115 + args: ["sparse-checkout", "set", "--no-cone", ...source.pattern], 116 + cwd: tempDir, 117 + }); 118 + 119 + const sparseResult = await sparseCheckoutCmd.output(); 120 + if (!sparseResult.success) { 121 + const error = new TextDecoder().decode(sparseResult.stderr); 122 + throw new Error(`Failed to set sparse checkout: ${error}`); 123 + } 124 + 125 + const checkoutCmd = new Deno.Command("git", { 126 + args: ["checkout"], 127 + cwd: tempDir, 128 + }); 129 + 130 + const checkoutResult = await checkoutCmd.output(); 131 + if (!checkoutResult.success) { 132 + const error = new TextDecoder().decode(checkoutResult.stderr); 133 + throw new Error(`Failed to checkout files: ${error}`); 134 + } 135 + 136 + for (const pattern of source.pattern) { 137 + const normalizedPattern = pattern.startsWith("./") 138 + ? pattern.slice(2) 139 + : pattern; 140 + const regex = globToRegExp(normalizedPattern, { 141 + extended: true, 142 + globstar: true, 143 + }); 144 + 145 + copyMatchingFiles(tempDir, targetDir, "", regex); 146 + } 147 + } finally { 148 + if (existsSync(tempDir)) { 149 + removeSync(tempDir, { recursive: true }); 150 + } 151 + const tempParent = join(cwd, ".lex-gen-temp"); 152 + if (existsSync(tempParent)) { 153 + try { 154 + const entries = Array.from(readDirSync(tempParent)); 155 + if (entries.length === 0) { 156 + removeSync(tempParent); 157 + } 158 + } catch { 159 + // ignore 160 + } 161 + } 162 + } 163 + }
+34
lex-gen/types.ts
··· 12 12 path: string; 13 13 content?: string; 14 14 } 15 + 16 + export interface GitSourceConfig { 17 + type: "git"; 18 + remote: string; 19 + ref?: string; 20 + pattern: string[]; 21 + } 22 + 23 + export type SourceConfig = GitSourceConfig; 24 + 25 + export interface PullConfig { 26 + outdir: string; 27 + clean?: boolean; 28 + sources: SourceConfig[]; 29 + } 30 + 31 + export interface ImportMapping { 32 + nsid: string[]; 33 + imports: 34 + | string 35 + | ((nsid: string) => { type: "named" | "namespace"; from: string }); 36 + } 37 + 38 + export interface ModulesConfig { 39 + importSuffix?: string; 40 + } 41 + 42 + export interface LexiconConfig { 43 + outdir: string; 44 + files: string[]; 45 + mappings?: ImportMapping[]; 46 + modules?: ModulesConfig; 47 + pull?: PullConfig; 48 + }
+69 -3
lex-gen/util.ts
··· 3 3 import { mkdirSync } from "@std/fs/unstable-mkdir"; 4 4 import { writeFileSync } from "@std/fs/unstable-write-file"; 5 5 import { existsSync } from "@std/fs"; 6 - import { join } from "@std/path"; 6 + import { globToRegExp, join } from "@std/path"; 7 7 import { removeSync } from "@std/fs/unstable-remove"; 8 8 import { readDirSync } from "@std/fs/unstable-read-dir"; 9 9 import { colors } from "@cliffy/ansi/colors"; 10 10 import { ZodError } from "zod"; 11 11 import { type LexiconDoc, parseLexiconDoc } from "@atp/lexicon"; 12 12 import type { FileDiff, GeneratedAPI } from "./types.ts"; 13 + import process from "node:process"; 13 14 14 15 type RecursiveZodError = { 15 16 _errors?: string[]; 16 17 [k: string]: RecursiveZodError | string[] | undefined; 17 18 }; 18 19 20 + export function expandGlobPatterns(patterns: string[]): string[] { 21 + const files: string[] = []; 22 + const cwd = typeof Deno !== "undefined" ? Deno.cwd() : process.cwd(); 23 + 24 + function walkDir( 25 + dir: string, 26 + relativePath: string, 27 + regex: RegExp, 28 + files: string[], 29 + ): void { 30 + try { 31 + if (!existsSync(dir)) return; 32 + const entries = Array.from(readDirSync(dir)); 33 + for (const entry of entries) { 34 + const fullPath = join(dir, entry.name); 35 + const relPath = relativePath 36 + ? join(relativePath, entry.name) 37 + : entry.name; 38 + if (statSync(fullPath).isDirectory) { 39 + walkDir(fullPath, relPath, regex, files); 40 + } else if (entry.name.endsWith(".json")) { 41 + const testPath = relPath.startsWith("/") ? relPath : `/${relPath}`; 42 + if (regex.test(testPath) || regex.test(relPath)) { 43 + files.push(fullPath); 44 + } 45 + } 46 + } 47 + } catch { 48 + // skip 49 + } 50 + } 51 + 52 + for (const pattern of patterns) { 53 + const normalizedPattern = pattern.startsWith("./") 54 + ? pattern.slice(2) 55 + : pattern; 56 + const regex = globToRegExp(normalizedPattern, { 57 + extended: true, 58 + globstar: true, 59 + }); 60 + const basePath = normalizedPattern.split("*")[0] || 61 + normalizedPattern.split("?")[0] || ""; 62 + const searchDir = basePath.includes("/") 63 + ? join( 64 + cwd, 65 + basePath.substring(0, basePath.lastIndexOf("/") || basePath.length), 66 + ) 67 + : cwd; 68 + 69 + walkDir(searchDir, "", regex, files); 70 + } 71 + 72 + return Array.from(new Set(files)); 73 + } 74 + 19 75 export function readAllLexicons(paths: string[] | string): LexiconDoc[] { 20 76 const docs: LexiconDoc[] = []; 21 - for (const path of Array.isArray(paths) ? paths : [paths]) { 77 + const pathArray = Array.isArray(paths) ? paths : [paths]; 78 + const expandedPaths: string[] = []; 79 + 80 + for (const path of pathArray) { 81 + if (path.includes("*") || path.includes("?")) { 82 + expandedPaths.push(...expandGlobPatterns([path])); 83 + } else { 84 + expandedPaths.push(path); 85 + } 86 + } 87 + 88 + for (const path of expandedPaths) { 22 89 if (statSync(path).isDirectory) { 23 - // If it's a directory, recursively read all .json files in it 24 90 const entries = Array.from(readDirSync(path)); 25 91 const subPaths = entries.map((entry) => join(path, entry.name)); 26 92 docs.push(...readAllLexicons(subPaths));