@elliots/typical 0.1.0 → 0.1.1

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.
package/README.md CHANGED
@@ -23,7 +23,7 @@ Why not.
23
23
  - ✅ Configurable include/exclude patterns
24
24
  - ✅ Optionally reuse validation logic for identical types to optimize performance (enabled by default)
25
25
  - ✅ TSC plugin
26
- - ✅ ESM loader for runtime transformation with `node --import typical/esm` (or `node --loader typical/esm-loader` for older Node versions)
26
+ - ✅ ESM loader for runtime transformation with `node --import @elliots/typical/esm` (or `node --loader @elliots/typical/esm-loader` for older Node versions)
27
27
  - ✅ tsx wrapper (ttsx) for easy use like `npx ttsx script.ts`
28
28
 
29
29
  ## Installation
package/bin/ttsx CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/bin/sh
2
2
 
3
- NODE_OPTIONS="--import typical/esm" exec tsx $@
3
+ NODE_OPTIONS="--import @elliots/typical/esm" exec tsx $@
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elliots/typical",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Runtime safe TypeScript transformer using typia",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -17,8 +17,7 @@
17
17
  },
18
18
  "files": [
19
19
  "dist/src",
20
- "bin",
21
- "src"
20
+ "bin"
22
21
  ],
23
22
  "scripts": {
24
23
  "build": "tsc",
package/src/cli.ts DELETED
@@ -1,111 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { Command } from 'commander';
4
- import * as fs from 'fs';
5
- import * as path from 'path';
6
- import { TypicalTransformer } from './transformer.js';
7
- import * as ts from 'typescript';
8
- import { loadConfig } from './config.js';
9
- import { shouldIncludeFile } from './file-filter.js';
10
-
11
- const program = new Command();
12
-
13
- program
14
- .name('typical')
15
- .description('Runtime safe TypeScript transformer using typia')
16
- .version('0.1.0');
17
-
18
- program
19
- .command('transform')
20
- .description('Transform a TypeScript file with runtime validation')
21
- .argument('<file>', 'TypeScript file to transform')
22
- .option('-o, --output <file>', 'Output file')
23
- .option('-c, --config <file>', 'Config file path', 'typical.json')
24
- .option('-m, --mode <mode>', 'Transformation mode: basic, typia, js', 'basic')
25
- .action(async (file: string, options: { output?: string; config?: string; mode?: 'basic' | 'typia' | 'js' }) => {
26
- try {
27
- const config = loadConfig(options.config);
28
- const transformer = new TypicalTransformer(config);
29
-
30
- if (!fs.existsSync(file)) {
31
- console.error(`File not found: ${file}`);
32
- process.exit(1);
33
- }
34
-
35
- console.log(`Transforming ${file}...`);
36
- const transformedCode = transformer.transform(path.resolve(file), options.mode ?? 'basic');
37
-
38
- const outputFilename = options.output ? path.resolve(options.output) : options.mode === 'js' ? file + '.js' : file + '.transformed.ts';
39
-
40
- const outputFile = options.output ? path.resolve(options.output) : file + '.transformed.ts';
41
- fs.writeFileSync(outputFile, transformedCode);
42
-
43
- console.log(`Transformed code written to ${outputFile}`);
44
- } catch (error) {
45
- console.error('Transformation failed:', error);
46
- process.exit(1);
47
- }
48
- });
49
-
50
- // program
51
- // .command('build')
52
- // .description('Transform all TypeScript files in the project')
53
- // .option('-c, --config <file>', 'Config file path')
54
- // .option('--dry-run', 'Show what would be transformed without making changes')
55
- // .action(async (options: { config?: string, dryRun?: boolean }) => {
56
- // try {
57
- // const transformer = new TypicalTransformer();
58
-
59
- // const { glob } = await import('glob');
60
-
61
- // const config = loadConfig(options.config);
62
-
63
- // if (!config.include || config.include.length === 0) {
64
- // console.error('No include patterns specified in config');
65
- // process.exit(1);
66
- // }
67
-
68
- // const files: string[] = [];
69
-
70
- // for (const pattern of config.include) {
71
- // const matched = await glob(pattern, {
72
- // ignore: config.exclude,
73
- // absolute: true
74
- // });
75
- // files.push(...matched);
76
- // }
77
-
78
- // console.log(`Found ${files.length} files to transform`);
79
-
80
- // if (options.dryRun) {
81
- // files.forEach(file => console.log(`Would transform: ${file}`));
82
- // return;
83
- // }
84
-
85
- // let transformed = 0;
86
-
87
- // for (const file of files) {
88
- // // Double-check with our shared filtering logic
89
- // if (!shouldIncludeFile(file, config)) {
90
- // console.log(`Skipping ${file} (excluded by filters)`);
91
- // continue;
92
- // }
93
-
94
- // try {
95
- // console.log(`Transforming ${file}...`);
96
- // const transformedCode = transformer.transformFile(file, ts);
97
- // fs.writeFileSync(file, transformedCode);
98
- // transformed++;
99
- // } catch (error) {
100
- // console.error(`Failed to transform ${file}:`, error);
101
- // }
102
- // }
103
-
104
- // console.log(`Successfully transformed ${transformed}/${files.length} files`);
105
- // } catch (error) {
106
- // console.error('Build failed:', error);
107
- // process.exit(1);
108
- // }
109
- // });
110
-
111
- program.parse();
package/src/config.ts DELETED
@@ -1,35 +0,0 @@
1
- export interface TypicalConfig {
2
- include?: string[];
3
- exclude?: string[];
4
- reusableValidators?: boolean;
5
- }
6
-
7
- export const defaultConfig: TypicalConfig = {
8
- include: ["**/*.ts", "**/*.tsx"],
9
- exclude: ["node_modules/**", "**/*.d.ts", "dist/**", "build/**"],
10
- reusableValidators: true,
11
- };
12
-
13
- import fs from 'fs';
14
- import path from 'path';
15
-
16
- export function loadConfig(configPath?: string): TypicalConfig {
17
- const configFile = configPath || path.join(process.cwd(), 'typical.json');
18
-
19
- if (fs.existsSync(configFile)) {
20
- try {
21
- const configContent = fs.readFileSync(configFile, 'utf8');
22
- const userConfig: Partial<TypicalConfig> = JSON.parse(configContent);
23
-
24
- return {
25
- ...defaultConfig,
26
- ...userConfig,
27
- };
28
- } catch (error) {
29
- console.warn(`Failed to parse config file ${configFile}:`, error);
30
- return defaultConfig;
31
- }
32
- }
33
-
34
- return defaultConfig;
35
- }
@@ -1,2 +0,0 @@
1
- import { register } from 'node:module';
2
- register('./esm-loader.js', { parentURL: import.meta.url });
package/src/esm-loader.ts DELETED
@@ -1,26 +0,0 @@
1
- import { fileURLToPath } from "url";
2
- import { TypicalTransformer } from "./transformer.js";
3
-
4
- const transformer = new TypicalTransformer();
5
-
6
- /**
7
- * Load hook - transforms TypeScript files on the fly
8
- */
9
- export async function load(url: string, context: any, nextLoad: any) {
10
- if (!url.endsWith(".ts")) {
11
- return nextLoad(url, context);
12
- }
13
- const filePath = fileURLToPath(url);
14
-
15
- try {
16
- const transformedCode = transformer.transform(filePath, 'js');
17
- return {
18
- format: "module",
19
- source: transformedCode,
20
- shortCircuit: true,
21
- };
22
- } catch (error) {
23
- console.error(`Error transforming ${filePath}:`, error);
24
- throw error;
25
- }
26
- }
@@ -1,44 +0,0 @@
1
- import * as path from 'path';
2
- import { minimatch } from 'minimatch';
3
- import { TypicalConfig } from './config.js';
4
-
5
- /**
6
- * Determines if a file should be transformed based on include/exclude patterns
7
- */
8
- export function shouldTransformFile(fileName: string, config: TypicalConfig): boolean {
9
- const relativePath = path.relative(process.cwd(), fileName);
10
-
11
- // Check include patterns
12
- const isIncluded = config.include?.some(pattern => {
13
- return minimatch(relativePath, pattern);
14
- }) ?? true;
15
-
16
- if (!isIncluded) return false;
17
-
18
- // Check exclude patterns
19
- const isExcluded = config.exclude?.some(pattern => {
20
- return minimatch(relativePath, pattern);
21
- }) ?? false;
22
-
23
- return !isExcluded;
24
- }
25
-
26
- /**
27
- * Checks if a file is a TypeScript file that can be transformed
28
- */
29
- export function isTransformableTypeScriptFile(fileName: string): boolean {
30
- // Only transform TypeScript files
31
- if (!/\.(ts|tsx)$/.test(fileName)) return false;
32
-
33
- // Skip declaration files
34
- if (fileName.endsWith('.d.ts')) return false;
35
-
36
- return true;
37
- }
38
-
39
- /**
40
- * Combined check for both file type and include/exclude patterns
41
- */
42
- export function shouldIncludeFile(fileName: string, config: TypicalConfig): boolean {
43
- return isTransformableTypeScriptFile(fileName) && shouldTransformFile(fileName, config);
44
- }
package/src/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export { TypicalTransformer } from './transformer.js';
2
- export { loadConfig, defaultConfig, TypicalConfig } from './config.js';
package/src/patch-fs.cjs DELETED
@@ -1,25 +0,0 @@
1
- // logs all readFile and readFileSync calls (for debugging)
2
-
3
- const fs = require("fs");
4
- const fsp = require("fs/promises");
5
-
6
- // monkeypatch promises/readFile
7
- const origFspReadFile = fsp.readFile;
8
- fsp.readFile = async function (path, ...args) {
9
- console.log("fsp.readFile", path);
10
- return origFspReadFile.call(this, path, ...args);
11
- };
12
-
13
- // monkeypatch readFile
14
- const origFsReadFile = fs.readFile;
15
- fs.readFile = async function (path, ...args) {
16
- console.log("fs.readFile", path);
17
- return origFsReadFile.call(this, path, ...args);
18
- };
19
-
20
- // monkeypatch readFileSync
21
- const origFsReadFileSync = fs.readFileSync;
22
- fs.readFileSync = function (path, ...args) {
23
- console.log("fs.readFileSync", path);
24
- return origFsReadFileSync.call(this, path, ...args);
25
- };
@@ -1,52 +0,0 @@
1
- // monkeypatch fs.readFileSync to automatically add typical/tsc-plugin to tsconfig.json (if not present)
2
-
3
- const fs = require("fs");
4
- const stripJsonComments = require('strip-json-comments').default;
5
-
6
- const origFsReadFileSync = fs.readFileSync;
7
-
8
- fs.readFileSync = function (path, ...args) {
9
- const result = origFsReadFileSync.call(this, path, ...args);
10
-
11
- if (typeof path === "string" && path.endsWith("/tsconfig.json")) {
12
- try {
13
-
14
- const json = stripJsonComments(result.toString(), { trailingCommas: true });
15
-
16
- const config = JSON.parse(json);
17
-
18
- if (!config.compilerOptions) {
19
- config.compilerOptions = {};
20
- }
21
-
22
- if (!config.compilerOptions.plugins) {
23
- config.compilerOptions.plugins = [];
24
- }
25
-
26
- const hasTypical = config.compilerOptions.plugins.some(
27
- (plugin) => plugin.transform === "typical/tsc-plugin"
28
- );
29
-
30
- if (!hasTypical) {
31
- if (fs.existsSync("./dist/src/tsc-plugin.js")) {
32
- console.log("DEV MODE: Adding ./dist/src/tsc-plugin.js to tsconfig.json");
33
- config.compilerOptions.plugins.push({
34
- transform: "./dist/src/tsc-plugin.js",
35
- });
36
- } else {
37
- config.compilerOptions.plugins.push({
38
- transform: "typical/tsc-plugin",
39
- });
40
- }
41
- }
42
-
43
- // console.log("patched config", JSON.stringify(config, null, 2));
44
-
45
- return JSON.stringify(config, null, 2);
46
- } catch (e) {
47
- console.error("ERROR patching tsconfig.json to add typical/tsc-plugin", e);
48
- throw e;
49
- }
50
- }
51
- return result;
52
- };
package/src/setup.ts DELETED
@@ -1,29 +0,0 @@
1
- import type ts from "typescript";
2
-
3
- export function setupTsProgram(tsInstance: typeof ts): ts.Program {
4
- // Find tsconfig.json
5
- const tsConfigPath = tsInstance.findConfigFile(
6
- process.cwd(),
7
- tsInstance.sys.fileExists,
8
- "tsconfig.json"
9
- );
10
- if (!tsConfigPath) {
11
- throw new Error("Could not find tsconfig.json");
12
- }
13
-
14
- // Load and parse tsconfig.json
15
- const configFile = tsInstance.readConfigFile(tsConfigPath, tsInstance.sys.readFile);
16
- const parsedConfig = tsInstance.parseJsonConfigFileContent(
17
- configFile.config,
18
- tsInstance.sys,
19
- process.cwd()
20
- );
21
-
22
- // Create the TypeScript program with all project files
23
- const tsProgram = tsInstance.createProgram(
24
- parsedConfig.fileNames,
25
- parsedConfig.options
26
- );
27
-
28
- return tsProgram;
29
- }
@@ -1,831 +0,0 @@
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
- }
package/src/tsc-plugin.ts DELETED
@@ -1,12 +0,0 @@
1
- import type ts from 'typescript';
2
- import type { TransformerExtras, PluginConfig } from 'ts-patch';
3
- import { TypicalTransformer } from './transformer.js';
4
- import { loadConfig } from './config.js';
5
-
6
- export default function (program: ts.Program, pluginConfig: PluginConfig, { ts: tsInstance }: TransformerExtras) {
7
- const config = loadConfig();
8
- const transformer = new TypicalTransformer(config, program, tsInstance);
9
-
10
- // Create the typical transformer with typia integration
11
- return transformer.getTransformer(true);
12
- }