@elliots/typical 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +82 -0
  2. package/bin/ttsc +12 -0
  3. package/bin/ttsx +3 -0
  4. package/dist/src/cli.d.ts +2 -0
  5. package/dist/src/cli.js +89 -0
  6. package/dist/src/cli.js.map +1 -0
  7. package/dist/src/config.d.ts +7 -0
  8. package/dist/src/config.js +26 -0
  9. package/dist/src/config.js.map +1 -0
  10. package/dist/src/esm-loader-register.d.ts +1 -0
  11. package/dist/src/esm-loader-register.js +3 -0
  12. package/dist/src/esm-loader-register.js.map +1 -0
  13. package/dist/src/esm-loader.d.ts +4 -0
  14. package/dist/src/esm-loader.js +25 -0
  15. package/dist/src/esm-loader.js.map +1 -0
  16. package/dist/src/file-filter.d.ts +13 -0
  17. package/dist/src/file-filter.js +38 -0
  18. package/dist/src/file-filter.js.map +1 -0
  19. package/dist/src/index.d.ts +2 -0
  20. package/dist/src/index.js +3 -0
  21. package/dist/src/index.js.map +1 -0
  22. package/dist/src/setup.d.ts +2 -0
  23. package/dist/src/setup.js +14 -0
  24. package/dist/src/setup.js.map +1 -0
  25. package/dist/src/transformer.d.ts +29 -0
  26. package/dist/src/transformer.js +472 -0
  27. package/dist/src/transformer.js.map +1 -0
  28. package/dist/src/tsc-plugin.d.ts +3 -0
  29. package/dist/src/tsc-plugin.js +9 -0
  30. package/dist/src/tsc-plugin.js.map +1 -0
  31. package/package.json +71 -0
  32. package/src/cli.ts +111 -0
  33. package/src/config.ts +35 -0
  34. package/src/esm-loader-register.ts +2 -0
  35. package/src/esm-loader.ts +26 -0
  36. package/src/file-filter.ts +44 -0
  37. package/src/index.ts +2 -0
  38. package/src/patch-fs.cjs +25 -0
  39. package/src/patch-tsconfig.cjs +52 -0
  40. package/src/setup.ts +29 -0
  41. package/src/transformer.ts +831 -0
  42. package/src/tsc-plugin.ts +12 -0
@@ -0,0 +1,831 @@
1
+ import ts from "typescript";
2
+ import { loadConfig, TypicalConfig } from "./config.js";
3
+ import { shouldTransformFile } from "./file-filter.js";
4
+
5
+ import { transform as typiaTransform } from "typia/lib/transform.js";
6
+ import { setupTsProgram } from "./setup.js";
7
+
8
+ export interface TransformContext {
9
+ ts: typeof ts;
10
+ factory: ts.NodeFactory;
11
+ context: ts.TransformationContext;
12
+ }
13
+
14
+ export class TypicalTransformer {
15
+ public config: TypicalConfig;
16
+ private program: ts.Program;
17
+ private ts: typeof ts;
18
+ private typeValidators = new Map<
19
+ string,
20
+ { name: string; typeNode: ts.TypeNode }
21
+ >(); // type -> { validator variable name, type node }
22
+ private typeStringifiers = new Map<
23
+ string,
24
+ { name: string; typeNode: ts.TypeNode }
25
+ >(); // type -> { stringifier variable name, type node }
26
+ private typeParsers = new Map<
27
+ string,
28
+ { name: string; typeNode: ts.TypeNode }
29
+ >(); // type -> { parser variable name, type node }
30
+
31
+ constructor(
32
+ config?: TypicalConfig,
33
+ program?: ts.Program,
34
+ tsInstance?: typeof ts
35
+ ) {
36
+ this.config = config ?? loadConfig();
37
+ this.ts = tsInstance ?? ts;
38
+ this.program = program ?? setupTsProgram(this.ts);
39
+ }
40
+
41
+ public createSourceFile(fileName: string, content: string): ts.SourceFile {
42
+ return this.ts.createSourceFile(
43
+ fileName,
44
+ content,
45
+ this.ts.ScriptTarget.ES2020,
46
+ true
47
+ );
48
+ }
49
+
50
+ public transform(
51
+ sourceFile: ts.SourceFile | string,
52
+ mode: "basic" | "typia" | "js"
53
+ ): string {
54
+ if (typeof sourceFile === "string") {
55
+ const file = this.program.getSourceFile(sourceFile);
56
+ if (!file) {
57
+ throw new Error(`Source file not found in program: ${sourceFile}`);
58
+ }
59
+ sourceFile = file;
60
+ }
61
+
62
+ const transformer = this.getTransformer(mode !== "basic");
63
+ const result = this.ts.transform(sourceFile, [transformer]);
64
+ const printer = this.ts.createPrinter();
65
+ const transformedCode = printer.printFile(result.transformed[0]);
66
+ result.dispose();
67
+
68
+ if (mode === "typia" || mode === 'basic') {
69
+ return transformedCode;
70
+ }
71
+
72
+ const compileResult = ts.transpileModule(transformedCode, {
73
+ compilerOptions: this.program.getCompilerOptions(),
74
+ });
75
+
76
+ return compileResult.outputText;
77
+ }
78
+
79
+ public getTransformer(
80
+ withTypia: boolean
81
+ ): ts.TransformerFactory<ts.SourceFile> {
82
+ return (context: ts.TransformationContext) => {
83
+ const factory = context.factory;
84
+ const typeChecker = this.program.getTypeChecker();
85
+ const transformContext: TransformContext = {
86
+ ts: this.ts,
87
+ factory,
88
+ context,
89
+ };
90
+
91
+ return (sourceFile: ts.SourceFile) => {
92
+
93
+ if (process.env.DEBUG) {
94
+ console.log("TYPICAL: processing ", sourceFile.fileName);
95
+ }
96
+ // First apply our transformation
97
+ let transformedSourceFile = this.transformSourceFile(
98
+ sourceFile,
99
+ transformContext,
100
+ typeChecker
101
+ );
102
+
103
+ if (!withTypia) {
104
+ return transformedSourceFile;
105
+ }
106
+
107
+ // Then apply typia if we added typia calls
108
+ const printer = this.ts.createPrinter();
109
+ const transformedCode = printer.printFile(transformedSourceFile);
110
+
111
+ if (transformedCode.includes("typia.")) {
112
+ try {
113
+ // Apply typia transformation to files with typia calls
114
+
115
+ // Create a new source file with the transformed code, preserving original filename
116
+ const newSourceFile = this.ts.createSourceFile(
117
+ sourceFile.fileName, // Use original filename to maintain source map references
118
+ transformedCode,
119
+ sourceFile.languageVersion,
120
+ true
121
+ );
122
+
123
+ // Create a new program with the transformed source file so typia's type checker works
124
+ const compilerOptions = this.program.getCompilerOptions();
125
+ const originalSourceFiles = new Map<string, ts.SourceFile>();
126
+ for (const sf of this.program.getSourceFiles()) {
127
+ originalSourceFiles.set(sf.fileName, sf);
128
+ }
129
+ // Replace the original source file with the transformed one
130
+ originalSourceFiles.set(sourceFile.fileName, newSourceFile);
131
+
132
+ const customHost: ts.CompilerHost = {
133
+ getSourceFile: (fileName, languageVersion) => {
134
+ if (originalSourceFiles.has(fileName)) {
135
+ return originalSourceFiles.get(fileName);
136
+ }
137
+ return this.ts.createSourceFile(
138
+ fileName,
139
+ this.ts.sys.readFile(fileName) || "",
140
+ languageVersion,
141
+ true
142
+ );
143
+ },
144
+ getDefaultLibFileName: () => this.ts.getDefaultLibFilePath(compilerOptions),
145
+ writeFile: () => {},
146
+ getCurrentDirectory: () => this.ts.sys.getCurrentDirectory(),
147
+ getCanonicalFileName: (fileName) =>
148
+ this.ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),
149
+ useCaseSensitiveFileNames: () => this.ts.sys.useCaseSensitiveFileNames,
150
+ getNewLine: () => this.ts.sys.newLine,
151
+ fileExists: (fileName) => originalSourceFiles.has(fileName) || this.ts.sys.fileExists(fileName),
152
+ readFile: (fileName) => this.ts.sys.readFile(fileName),
153
+ };
154
+
155
+ const newProgram = this.ts.createProgram(
156
+ Array.from(originalSourceFiles.keys()),
157
+ compilerOptions,
158
+ customHost
159
+ );
160
+
161
+ // Create typia transformer with the NEW program that has the transformed source
162
+ const typiaTransformer = typiaTransform(
163
+ newProgram,
164
+ {},
165
+ {
166
+ addDiagnostic(diag: ts.Diagnostic) {
167
+ console.warn("Typia diagnostic:", diag);
168
+ return 0;
169
+ },
170
+ }
171
+ );
172
+
173
+ // Apply the transformer with source map preservation
174
+ const transformationResult = this.ts.transform(
175
+ newSourceFile,
176
+ [typiaTransformer],
177
+ { ...compilerOptions, sourceMap: true }
178
+ );
179
+
180
+ if (transformationResult.transformed.length > 0) {
181
+ const finalTransformed = transformationResult.transformed[0];
182
+ transformedSourceFile = finalTransformed;
183
+
184
+ // Typia transformation completed successfully
185
+ }
186
+
187
+ transformationResult.dispose();
188
+ } catch (error) {
189
+ console.warn("Failed to apply typia transformer:", sourceFile.fileName, error);
190
+ }
191
+ }
192
+
193
+ return transformedSourceFile;
194
+ };
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Transform a single source file with TypeScript AST
200
+ */
201
+ private transformSourceFile(
202
+ sourceFile: ts.SourceFile,
203
+ ctx: TransformContext,
204
+ typeChecker: ts.TypeChecker
205
+ ): ts.SourceFile {
206
+ const { ts } = ctx;
207
+
208
+ // Check if this file should be transformed
209
+ if (!this.shouldTransformFile(sourceFile.fileName)) {
210
+ return sourceFile; // Return unchanged for excluded files
211
+ }
212
+
213
+ // Check if this file has already been transformed by us
214
+ const sourceText = sourceFile.getFullText();
215
+ if (sourceText.includes('__typical_assert_') || sourceText.includes('__typical_stringify_') || sourceText.includes('__typical_parse_')) {
216
+ throw new Error(`File ${sourceFile.fileName} has already been transformed by Typical! Double transformation detected.`);
217
+ }
218
+
219
+ // Reset caches for each file
220
+ this.typeValidators.clear();
221
+ this.typeStringifiers.clear();
222
+ this.typeParsers.clear();
223
+
224
+ let needsTypiaImport = false;
225
+
226
+ const visit = (node: ts.Node): ts.Node => {
227
+ // Transform JSON calls first (before they get wrapped in functions)
228
+ if (
229
+ ts.isCallExpression(node) &&
230
+ ts.isPropertyAccessExpression(node.expression)
231
+ ) {
232
+ const propertyAccess = node.expression;
233
+ if (
234
+ ts.isIdentifier(propertyAccess.expression) &&
235
+ propertyAccess.expression.text === "JSON"
236
+ ) {
237
+ needsTypiaImport = true;
238
+
239
+ if (propertyAccess.name.text === "stringify") {
240
+ // For stringify, we need to infer the type from the argument
241
+ if (this.config.reusableValidators) {
242
+ // For JSON.stringify, try to infer the type from the argument
243
+ let typeText = "unknown";
244
+ let typeNodeForCache: ts.TypeNode | undefined;
245
+
246
+ if (node.arguments.length > 0) {
247
+ const arg = node.arguments[0];
248
+
249
+ // Check if it's a type assertion
250
+ if (ts.isAsExpression(arg)) {
251
+ // For type assertions, use the asserted type directly
252
+ const assertedType = arg.type;
253
+ const objectType =
254
+ typeChecker.getTypeFromTypeNode(assertedType);
255
+
256
+ const typeNode = assertedType;
257
+
258
+ if (typeNode) {
259
+ const typeString = typeChecker.typeToString(objectType);
260
+ typeText = `Asserted_${typeString.replace(
261
+ /[^a-zA-Z0-9_]/g,
262
+ "_"
263
+ )}`;
264
+ typeNodeForCache = typeNode;
265
+ } else {
266
+ typeText = "unknown";
267
+ typeNodeForCache = ctx.factory.createKeywordTypeNode(
268
+ ctx.ts.SyntaxKind.UnknownKeyword
269
+ );
270
+ }
271
+ } else if (ts.isObjectLiteralExpression(arg)) {
272
+ // For object literals, use the type checker to get the actual type
273
+ const objectType = typeChecker.getTypeAtLocation(arg);
274
+
275
+ const typeNode = typeChecker.typeToTypeNode(
276
+ objectType,
277
+ arg,
278
+ ts.NodeBuilderFlags.InTypeAlias
279
+ );
280
+
281
+ if (typeNode) {
282
+ const propNames = arg.properties
283
+ .map((prop) => {
284
+ if (ts.isShorthandPropertyAssignment(prop)) {
285
+ return prop.name.text;
286
+ } else if (
287
+ ts.isPropertyAssignment(prop) &&
288
+ ts.isIdentifier(prop.name)
289
+ ) {
290
+ return prop.name.text;
291
+ }
292
+ return "unknown";
293
+ })
294
+ .sort()
295
+ .join("_");
296
+
297
+ typeText = `ObjectLiteral_${propNames}`;
298
+ typeNodeForCache = typeNode;
299
+ } else {
300
+ // typeText = "unknown";
301
+ // typeNodeForCache = ctx.factory.createKeywordTypeNode(
302
+ // ctx.ts.SyntaxKind.UnknownKeyword
303
+ // );
304
+ throw new Error('unknown type node for object literal: ' + arg.getText());
305
+ }
306
+ } else {
307
+ // For other expressions, try to get the type from the type checker
308
+ const argType = typeChecker.getTypeAtLocation(arg);
309
+
310
+ const typeNode = typeChecker.typeToTypeNode(
311
+ argType,
312
+ arg,
313
+ ts.NodeBuilderFlags.InTypeAlias
314
+ );
315
+ if (typeNode) {
316
+ const typeString = typeChecker.typeToString(argType);
317
+ typeText = `Expression_${typeString.replace(
318
+ /[^a-zA-Z0-9_]/g,
319
+ "_"
320
+ )}`;
321
+ typeNodeForCache = typeNode;
322
+ } else {
323
+ typeText = "unknown";
324
+ typeNodeForCache = ctx.factory.createKeywordTypeNode(
325
+ ctx.ts.SyntaxKind.UnknownKeyword
326
+ )
327
+ }
328
+ }
329
+ }
330
+
331
+ const stringifierName = this.getOrCreateStringifier(
332
+ typeText,
333
+ typeNodeForCache!
334
+ );
335
+
336
+ const newCall = ctx.factory.createCallExpression(
337
+ ctx.factory.createIdentifier(stringifierName),
338
+ undefined,
339
+ node.arguments
340
+ );
341
+
342
+ return newCall;
343
+ } else {
344
+ // Use inline typia.json.stringify
345
+ return ctx.factory.updateCallExpression(
346
+ node,
347
+ ctx.factory.createPropertyAccessExpression(
348
+ ctx.factory.createPropertyAccessExpression(
349
+ ctx.factory.createIdentifier("typia"),
350
+ "json"
351
+ ),
352
+ "stringify"
353
+ ),
354
+ node.typeArguments,
355
+ node.arguments
356
+ );
357
+ }
358
+ } else if (propertyAccess.name.text === "parse") {
359
+ // For JSON.parse, we need to infer the expected type from context
360
+ // Check if this is part of a variable declaration or type assertion
361
+ let targetType: ts.TypeNode | undefined;
362
+
363
+ // Look for type annotations in parent nodes
364
+ let parent = node.parent;
365
+ while (parent) {
366
+ if (ts.isVariableDeclaration(parent) && parent.type) {
367
+ targetType = parent.type;
368
+ break;
369
+ } else if (ts.isAsExpression(parent)) {
370
+ targetType = parent.type;
371
+ break;
372
+ } else if (ts.isReturnStatement(parent)) {
373
+ // Look for function return type
374
+ let funcParent = parent.parent;
375
+ while (funcParent) {
376
+ if (
377
+ (ts.isFunctionDeclaration(funcParent) ||
378
+ ts.isArrowFunction(funcParent) ||
379
+ ts.isMethodDeclaration(funcParent)) &&
380
+ funcParent.type
381
+ ) {
382
+ targetType = funcParent.type;
383
+ break;
384
+ }
385
+ funcParent = funcParent.parent;
386
+ }
387
+ break;
388
+ }
389
+ parent = parent.parent;
390
+ }
391
+
392
+ if (this.config.reusableValidators && targetType) {
393
+ // Use reusable parser - use typeToString
394
+ const targetTypeObj = typeChecker.getTypeFromTypeNode(targetType);
395
+ const typeText = typeChecker.typeToString(targetTypeObj);
396
+ const parserName = this.getOrCreateParser(typeText, targetType);
397
+
398
+ const newCall = ctx.factory.createCallExpression(
399
+ ctx.factory.createIdentifier(parserName),
400
+ undefined,
401
+ node.arguments
402
+ );
403
+
404
+ return newCall;
405
+ } else {
406
+ // Use inline typia.json.assertParse
407
+ const typeArguments = targetType
408
+ ? [targetType]
409
+ : node.typeArguments;
410
+
411
+ return ctx.factory.updateCallExpression(
412
+ node,
413
+ ctx.factory.createPropertyAccessExpression(
414
+ ctx.factory.createPropertyAccessExpression(
415
+ ctx.factory.createIdentifier("typia"),
416
+ "json"
417
+ ),
418
+ "assertParse"
419
+ ),
420
+ typeArguments,
421
+ node.arguments
422
+ );
423
+ }
424
+ }
425
+ }
426
+ }
427
+
428
+ // Transform function declarations
429
+ if (ts.isFunctionDeclaration(node)) {
430
+ needsTypiaImport = true;
431
+ return transformFunction(node);
432
+ }
433
+
434
+ // Transform arrow functions
435
+ if (ts.isArrowFunction(node)) {
436
+ needsTypiaImport = true;
437
+ return transformFunction(node);
438
+ }
439
+
440
+ // Transform method declarations
441
+ if (ts.isMethodDeclaration(node)) {
442
+ needsTypiaImport = true;
443
+ return transformFunction(node);
444
+ }
445
+
446
+ return ctx.ts.visitEachChild(node, visit, ctx.context);
447
+ };
448
+
449
+ const transformFunction = (
450
+ func: ts.FunctionDeclaration | ts.ArrowFunction | ts.MethodDeclaration
451
+ ): ts.Node => {
452
+ const body = func.body;
453
+ if (!body || !ts.isBlock(body)) return func;
454
+
455
+ // Add parameter validation
456
+ const validationStatements: ts.Statement[] = [];
457
+
458
+ func.parameters.forEach((param) => {
459
+ if (param.type) {
460
+ const paramName = ts.isIdentifier(param.name)
461
+ ? param.name.text
462
+ : "param";
463
+ const paramIdentifier = ctx.factory.createIdentifier(paramName);
464
+
465
+ if (this.config.reusableValidators) {
466
+ // Use reusable validators - use typeToString
467
+ const paramType = typeChecker.getTypeFromTypeNode(param.type);
468
+ const typeText = typeChecker.typeToString(paramType);
469
+ const validatorName = this.getOrCreateValidator(
470
+ typeText,
471
+ param.type
472
+ );
473
+
474
+ const validatorCall = ctx.factory.createCallExpression(
475
+ ctx.factory.createIdentifier(validatorName),
476
+ undefined,
477
+ [paramIdentifier]
478
+ );
479
+ const assertCall =
480
+ ctx.factory.createExpressionStatement(validatorCall);
481
+
482
+ validationStatements.push(assertCall);
483
+ } else {
484
+ // Use inline typia.assert calls
485
+ const typiaIdentifier = ctx.factory.createIdentifier("typia");
486
+ const assertIdentifier = ctx.factory.createIdentifier("assert");
487
+ const propertyAccess = ctx.factory.createPropertyAccessExpression(
488
+ typiaIdentifier,
489
+ assertIdentifier
490
+ );
491
+ const callExpression = ctx.factory.createCallExpression(
492
+ propertyAccess,
493
+ [param.type],
494
+ [paramIdentifier]
495
+ );
496
+ const assertCall =
497
+ ctx.factory.createExpressionStatement(callExpression);
498
+
499
+ validationStatements.push(assertCall);
500
+ }
501
+ }
502
+ });
503
+
504
+ // First visit all child nodes (including JSON calls) before adding validation
505
+ const visitedBody = ctx.ts.visitNode(body, visit) as ts.Block;
506
+
507
+ // Transform return statements - use explicit type or infer from type checker
508
+ let transformedStatements = visitedBody.statements;
509
+ let returnType = func.type;
510
+
511
+ // If no explicit return type, try to infer it from the type checker
512
+ let returnTypeForString: ts.Type | undefined;
513
+ if (!returnType) {
514
+ const signature = typeChecker.getSignatureFromDeclaration(func);
515
+ if (signature) {
516
+ const inferredReturnType = typeChecker.getReturnTypeOfSignature(signature);
517
+ returnType = typeChecker.typeToTypeNode(
518
+ inferredReturnType,
519
+ func,
520
+ ts.NodeBuilderFlags.InTypeAlias
521
+ );
522
+ returnTypeForString = inferredReturnType;
523
+ }
524
+ } else {
525
+ // For explicit return types, get the Type from the TypeNode
526
+ returnTypeForString = typeChecker.getTypeFromTypeNode(returnType);
527
+ }
528
+
529
+ if (returnType && returnTypeForString) {
530
+ const returnTransformer = (node: ts.Node): ts.Node => {
531
+ if (ts.isReturnStatement(node) && node.expression) {
532
+ if (this.config.reusableValidators) {
533
+ // Use reusable validators - always use typeToString
534
+ const returnTypeText = typeChecker.typeToString(returnTypeForString!);
535
+ const validatorName = this.getOrCreateValidator(
536
+ returnTypeText,
537
+ returnType
538
+ );
539
+
540
+ const validatorCall = ctx.factory.createCallExpression(
541
+ ctx.factory.createIdentifier(validatorName),
542
+ undefined,
543
+ [node.expression]
544
+ );
545
+
546
+ return ctx.factory.updateReturnStatement(node, validatorCall);
547
+ } else {
548
+ // Use inline typia.assert calls
549
+ const typiaIdentifier = ctx.factory.createIdentifier("typia");
550
+ const assertIdentifier = ctx.factory.createIdentifier("assert");
551
+ const propertyAccess = ctx.factory.createPropertyAccessExpression(
552
+ typiaIdentifier,
553
+ assertIdentifier
554
+ );
555
+ const callExpression = ctx.factory.createCallExpression(
556
+ propertyAccess,
557
+ [returnType],
558
+ [node.expression]
559
+ );
560
+
561
+ return ctx.factory.updateReturnStatement(node, callExpression);
562
+ }
563
+ }
564
+ return ctx.ts.visitEachChild(node, returnTransformer, ctx.context);
565
+ };
566
+
567
+ transformedStatements = ctx.ts.visitNodes(
568
+ visitedBody.statements,
569
+ returnTransformer
570
+ ) as ts.NodeArray<ts.Statement>;
571
+ }
572
+
573
+ // Insert validation statements at the beginning
574
+ const newStatements = ctx.factory.createNodeArray([
575
+ ...validationStatements,
576
+ ...transformedStatements,
577
+ ]);
578
+ const newBody = ctx.factory.updateBlock(visitedBody, newStatements);
579
+
580
+ if (ts.isFunctionDeclaration(func)) {
581
+ return ctx.factory.updateFunctionDeclaration(
582
+ func,
583
+ func.modifiers,
584
+ func.asteriskToken,
585
+ func.name,
586
+ func.typeParameters,
587
+ func.parameters,
588
+ func.type,
589
+ newBody
590
+ );
591
+ } else if (ts.isArrowFunction(func)) {
592
+ return ctx.factory.updateArrowFunction(
593
+ func,
594
+ func.modifiers,
595
+ func.typeParameters,
596
+ func.parameters,
597
+ func.type,
598
+ func.equalsGreaterThanToken,
599
+ newBody
600
+ );
601
+ } else if (ts.isMethodDeclaration(func)) {
602
+ return ctx.factory.updateMethodDeclaration(
603
+ func,
604
+ func.modifiers,
605
+ func.asteriskToken,
606
+ func.name,
607
+ func.questionToken,
608
+ func.typeParameters,
609
+ func.parameters,
610
+ func.type,
611
+ newBody
612
+ );
613
+ }
614
+
615
+ return func;
616
+ };
617
+
618
+ let transformedSourceFile = ctx.ts.visitNode(
619
+ sourceFile,
620
+ visit
621
+ ) as ts.SourceFile;
622
+
623
+ // Add typia import and validator statements if needed
624
+ if (needsTypiaImport) {
625
+ transformedSourceFile = this.addTypiaImport(transformedSourceFile, ctx);
626
+
627
+ // Add validator statements after imports (only if using reusable validators)
628
+ if (this.config.reusableValidators) {
629
+ const validatorStmts = this.createValidatorStatements(ctx);
630
+
631
+ if (validatorStmts.length > 0) {
632
+ const importStatements = transformedSourceFile.statements.filter(
633
+ ctx.ts.isImportDeclaration
634
+ );
635
+ const otherStatements = transformedSourceFile.statements.filter(
636
+ (stmt) => !ctx.ts.isImportDeclaration(stmt)
637
+ );
638
+
639
+ const newStatements = ctx.factory.createNodeArray([
640
+ ...importStatements,
641
+ ...validatorStmts,
642
+ ...otherStatements,
643
+ ]);
644
+
645
+ transformedSourceFile = ctx.factory.updateSourceFile(
646
+ transformedSourceFile,
647
+ newStatements
648
+ );
649
+ }
650
+ }
651
+ }
652
+
653
+ return transformedSourceFile;
654
+ }
655
+
656
+ public shouldTransformFile(fileName: string): boolean {
657
+ return shouldTransformFile(fileName, this.config);
658
+ }
659
+
660
+ private addTypiaImport(
661
+ sourceFile: ts.SourceFile,
662
+ ctx: TransformContext
663
+ ): ts.SourceFile {
664
+ const { factory } = ctx;
665
+
666
+ const existingImports = sourceFile.statements.filter(
667
+ ctx.ts.isImportDeclaration
668
+ );
669
+ const hasTypiaImport = existingImports.some(
670
+ (imp) =>
671
+ imp.moduleSpecifier &&
672
+ ctx.ts.isStringLiteral(imp.moduleSpecifier) &&
673
+ imp.moduleSpecifier.text === "typia"
674
+ );
675
+
676
+ if (!hasTypiaImport) {
677
+ const typiaImport = factory.createImportDeclaration(
678
+ undefined,
679
+ factory.createImportClause(
680
+ false,
681
+ factory.createIdentifier("typia"),
682
+ undefined
683
+ ),
684
+ factory.createStringLiteral("typia")
685
+ );
686
+
687
+ const newSourceFile = factory.updateSourceFile(
688
+ sourceFile,
689
+ factory.createNodeArray([typiaImport, ...sourceFile.statements])
690
+ );
691
+
692
+ return newSourceFile;
693
+ }
694
+
695
+ return sourceFile;
696
+ }
697
+
698
+ private getOrCreateValidator(
699
+ typeText: string,
700
+ typeNode: ts.TypeNode
701
+ ): string {
702
+ if (this.typeValidators.has(typeText)) {
703
+ return this.typeValidators.get(typeText)!.name;
704
+ }
705
+
706
+ const validatorName = `__typical_assert_${this.typeValidators.size}`;
707
+ this.typeValidators.set(typeText, { name: validatorName, typeNode });
708
+ return validatorName;
709
+ }
710
+
711
+ private getOrCreateStringifier(
712
+ typeText: string,
713
+ typeNode: ts.TypeNode
714
+ ): string {
715
+ if (this.typeStringifiers.has(typeText)) {
716
+ return this.typeStringifiers.get(typeText)!.name;
717
+ }
718
+
719
+ const stringifierName = `__typical_stringify_${this.typeStringifiers.size}`;
720
+ this.typeStringifiers.set(typeText, { name: stringifierName, typeNode });
721
+ return stringifierName;
722
+ }
723
+
724
+ private getOrCreateParser(typeText: string, typeNode: ts.TypeNode): string {
725
+ if (this.typeParsers.has(typeText)) {
726
+ return this.typeParsers.get(typeText)!.name;
727
+ }
728
+
729
+ const parserName = `__typical_parse_${this.typeParsers.size}`;
730
+ this.typeParsers.set(typeText, { name: parserName, typeNode });
731
+ return parserName;
732
+ }
733
+
734
+ private createValidatorStatements(ctx: TransformContext): ts.Statement[] {
735
+ const { factory } = ctx;
736
+ const statements: ts.Statement[] = [];
737
+
738
+ // Create assert validators
739
+ for (const [, { name: validatorName, typeNode }] of this.typeValidators) {
740
+ const createAssertCall = factory.createCallExpression(
741
+ factory.createPropertyAccessExpression(
742
+ factory.createIdentifier("typia"),
743
+ "createAssert"
744
+ ),
745
+ [typeNode],
746
+ []
747
+ );
748
+
749
+ const validatorDeclaration = factory.createVariableStatement(
750
+ undefined,
751
+ factory.createVariableDeclarationList(
752
+ [
753
+ factory.createVariableDeclaration(
754
+ validatorName,
755
+ undefined,
756
+ undefined,
757
+ createAssertCall
758
+ ),
759
+ ],
760
+ ctx.ts.NodeFlags.Const
761
+ )
762
+ );
763
+ statements.push(validatorDeclaration);
764
+ }
765
+
766
+ // Create stringifiers
767
+ for (const [, { name: stringifierName, typeNode }] of this
768
+ .typeStringifiers) {
769
+ const createStringifyCall = factory.createCallExpression(
770
+ factory.createPropertyAccessExpression(
771
+ factory.createPropertyAccessExpression(
772
+ factory.createIdentifier("typia"),
773
+ "json"
774
+ ),
775
+ "createStringify"
776
+ ),
777
+ [typeNode],
778
+ []
779
+ );
780
+
781
+ const stringifierDeclaration = factory.createVariableStatement(
782
+ undefined,
783
+ factory.createVariableDeclarationList(
784
+ [
785
+ factory.createVariableDeclaration(
786
+ stringifierName,
787
+ undefined,
788
+ undefined,
789
+ createStringifyCall
790
+ ),
791
+ ],
792
+ ctx.ts.NodeFlags.Const
793
+ )
794
+ );
795
+ statements.push(stringifierDeclaration);
796
+ }
797
+
798
+ // Create parsers
799
+ for (const [, { name: parserName, typeNode }] of this.typeParsers) {
800
+ const createParseCall = factory.createCallExpression(
801
+ factory.createPropertyAccessExpression(
802
+ factory.createPropertyAccessExpression(
803
+ factory.createIdentifier("typia"),
804
+ "json"
805
+ ),
806
+ "createAssertParse"
807
+ ),
808
+ [typeNode],
809
+ []
810
+ );
811
+
812
+ const parserDeclaration = factory.createVariableStatement(
813
+ undefined,
814
+ factory.createVariableDeclarationList(
815
+ [
816
+ factory.createVariableDeclaration(
817
+ parserName,
818
+ undefined,
819
+ undefined,
820
+ createParseCall
821
+ ),
822
+ ],
823
+ ctx.ts.NodeFlags.Const
824
+ )
825
+ );
826
+ statements.push(parserDeclaration);
827
+ }
828
+
829
+ return statements;
830
+ }
831
+ }