runtypex 0.1.12 → 0.2.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 (74) hide show
  1. package/README.md +112 -69
  2. package/dist/cjs/core/emitArrayOrTuple.d.ts +6 -0
  3. package/dist/cjs/core/emitArrayOrTuple.js +40 -0
  4. package/dist/cjs/core/emitLiteralOrEnum.d.ts +4 -0
  5. package/dist/cjs/core/emitLiteralOrEnum.js +47 -0
  6. package/dist/cjs/core/emitMapperFromSpec.d.ts +32 -0
  7. package/dist/cjs/core/emitMapperFromSpec.js +199 -0
  8. package/dist/cjs/core/emitObject.d.ts +6 -0
  9. package/dist/cjs/core/emitObject.js +30 -0
  10. package/dist/cjs/core/emitPrimitive.d.ts +6 -0
  11. package/dist/cjs/core/emitPrimitive.js +29 -0
  12. package/dist/cjs/core/emitUnionOrIntersection.d.ts +6 -0
  13. package/dist/cjs/core/emitUnionOrIntersection.js +15 -0
  14. package/dist/cjs/core/index.d.ts +17 -0
  15. package/dist/cjs/core/index.js +41 -0
  16. package/dist/cjs/core/path.d.ts +9 -0
  17. package/dist/cjs/core/path.js +42 -0
  18. package/dist/cjs/generator/generate-jsdoc.d.ts +13 -0
  19. package/dist/cjs/generator/generate-jsdoc.js +69 -0
  20. package/dist/cjs/generator/index.d.ts +1 -0
  21. package/dist/cjs/generator/index.js +17 -0
  22. package/dist/cjs/index.d.ts +4 -0
  23. package/dist/cjs/index.js +20 -0
  24. package/dist/cjs/mapper/index.d.ts +1 -0
  25. package/dist/cjs/mapper/index.js +17 -0
  26. package/dist/cjs/runtime/index.d.ts +2 -0
  27. package/dist/cjs/runtime/index.js +18 -0
  28. package/dist/cjs/runtime/mapper.d.ts +71 -0
  29. package/dist/cjs/runtime/mapper.js +79 -0
  30. package/dist/cjs/runtime/validate.d.ts +11 -0
  31. package/dist/cjs/runtime/validate.js +18 -0
  32. package/dist/cjs/transformer/helper.d.ts +2 -0
  33. package/dist/cjs/transformer/helper.js +63 -0
  34. package/dist/cjs/transformer/index.d.ts +3 -0
  35. package/dist/cjs/transformer/index.js +12 -0
  36. package/dist/cjs/transformer/ts-transformer.d.ts +29 -0
  37. package/dist/cjs/transformer/ts-transformer.js +109 -0
  38. package/dist/cjs/transformer/vite-plugin.d.ts +18 -0
  39. package/dist/cjs/transformer/vite-plugin.js +72 -0
  40. package/dist/esm/core/emitArrayOrTuple.d.ts +1 -1
  41. package/dist/esm/core/emitLiteralOrEnum.d.ts +1 -1
  42. package/dist/esm/core/emitMapperFromSpec.d.ts +32 -0
  43. package/dist/esm/core/emitMapperFromSpec.js +189 -0
  44. package/dist/esm/core/emitObject.d.ts +1 -1
  45. package/dist/esm/core/emitObject.js +4 -2
  46. package/dist/esm/core/emitPrimitive.d.ts +1 -1
  47. package/dist/esm/core/emitUnionOrIntersection.d.ts +1 -1
  48. package/dist/esm/core/path.d.ts +9 -0
  49. package/dist/esm/core/path.js +36 -0
  50. package/dist/esm/generator/generate-jsdoc.d.ts +13 -0
  51. package/dist/esm/generator/generate-jsdoc.js +63 -0
  52. package/dist/esm/generator/index.d.ts +1 -0
  53. package/dist/esm/generator/index.js +1 -0
  54. package/dist/esm/index.d.ts +4 -3
  55. package/dist/esm/index.js +1 -0
  56. package/dist/esm/mapper/index.d.ts +1 -0
  57. package/dist/esm/mapper/index.js +1 -0
  58. package/dist/esm/runtime/index.d.ts +2 -0
  59. package/dist/esm/runtime/index.js +2 -0
  60. package/dist/esm/runtime/mapper.d.ts +71 -0
  61. package/dist/esm/runtime/mapper.js +71 -0
  62. package/dist/esm/transformer/helper.d.ts +2 -0
  63. package/dist/esm/transformer/helper.js +57 -0
  64. package/dist/esm/transformer/index.d.ts +3 -0
  65. package/dist/esm/transformer/index.js +3 -0
  66. package/dist/esm/transformer/ts-transformer.d.ts +8 -4
  67. package/dist/esm/transformer/ts-transformer.js +46 -55
  68. package/dist/esm/transformer/vite-plugin.js +7 -93
  69. package/docs/build-integrations.md +89 -0
  70. package/docs/jsdoc-generation.md +88 -0
  71. package/docs/mapper.md +104 -0
  72. package/docs/mapping-policy.md +78 -0
  73. package/docs/runtime-validation.md +84 -0
  74. package/package.json +76 -36
@@ -0,0 +1,71 @@
1
+ type Primitive = string | number | boolean | bigint | symbol | null | undefined | Date;
2
+ export type PathOf<T> = T extends Primitive ? never : T extends readonly (infer U)[] ? `${number}` | `${number}.${PathOf<U>}` : {
3
+ [K in Extract<keyof T, string>]: T[K] extends Primitive ? K : T[K] extends readonly (infer U)[] ? K | `${K}.${number}` | `${K}.${number}.${PathOf<U>}` : K | `${K}.${PathOf<T[K]>}`;
4
+ }[Extract<keyof T, string>];
5
+ export type Mapper<TDto, TDomain> = (dto: TDto) => TDomain;
6
+ export type MapperMetadata<TValue = never> = {
7
+ db?: string;
8
+ description?: string;
9
+ dtoDescription?: string;
10
+ default?: TValue;
11
+ };
12
+ export type MapRule<TDto, TValue> = MapperMetadata<TValue> & {
13
+ from: PathOf<TDto>;
14
+ transform?: (value: unknown, dto: TDto) => TValue;
15
+ };
16
+ export type MapSpec<TDto, TDomain> = {
17
+ [K in keyof TDomain]-?: MapRule<TDto, TDomain[K]>;
18
+ };
19
+ export type MappingPolicy<TDto> = Record<string, MapRule<TDto, unknown>>;
20
+ export type MappingPolicyMode = "warn" | "error";
21
+ export type MapperOptions<TDto> = {
22
+ policy?: MappingPolicy<TDto>;
23
+ policyMode?: MappingPolicyMode;
24
+ };
25
+ declare const DTO_TYPE: unique symbol;
26
+ declare const DOMAIN_TYPE: unique symbol;
27
+ export type DefinedMap<TDto, TDomain> = MapSpec<TDto, TDomain> & {
28
+ readonly [DTO_TYPE]?: TDto;
29
+ readonly [DOMAIN_TYPE]?: TDomain;
30
+ };
31
+ export declare function defineMap<TDto, TDomain>(): <const TSpec extends MapSpec<TDto, TDomain>>(spec: TSpec) => TSpec & DefinedMap<TDto, TDomain>;
32
+ /** Declares canonical DTO path -> Domain field names for consistency checks. */
33
+ export declare function defineMappingPolicy<TDto>(): <const TSpec extends MappingPolicy<TDto>>(spec: TSpec) => TSpec;
34
+ /** Shorthand rule for direct DTO path reads. */
35
+ export declare function source<const TPath extends string, TValue = never>(from: TPath, metadata?: MapperMetadata<TValue>): {
36
+ db?: string;
37
+ description?: string;
38
+ dtoDescription?: string;
39
+ default?: TValue | undefined;
40
+ from: TPath;
41
+ };
42
+ /** Shorthand rule for DTO path reads that require a value conversion. */
43
+ export declare function transform<const TPath extends string, TValue>(from: TPath, transform: (value: unknown, dto: unknown) => TValue, metadata?: MapperMetadata<TValue>): {
44
+ db?: string;
45
+ description?: string;
46
+ dtoDescription?: string;
47
+ default?: TValue | undefined;
48
+ from: TPath;
49
+ transform: (value: unknown, dto: unknown) => TValue;
50
+ };
51
+ /** Typed helpers for callbacks that need access to the source DTO shape. */
52
+ export declare function mapperHelpers<TDto>(): {
53
+ source: <const TPath extends PathOf<TDto>, TValue = never>(from: TPath, metadata?: MapperMetadata<TValue>) => {
54
+ db?: string;
55
+ description?: string;
56
+ dtoDescription?: string;
57
+ default?: TValue | undefined;
58
+ from: TPath;
59
+ };
60
+ transform: <const TPath extends PathOf<TDto>, TValue>(from: TPath, transform: (value: unknown, dto: TDto) => TValue, metadata?: MapperMetadata<TValue>) => {
61
+ db?: string;
62
+ description?: string;
63
+ dtoDescription?: string;
64
+ default?: TValue | undefined;
65
+ from: TPath;
66
+ transform: (value: unknown, dto: TDto) => TValue;
67
+ };
68
+ };
69
+ /** Runtime interpreter used as fallback when the transformer is not configured. */
70
+ export declare function makeMapper<TDto, TDomain>(spec: DefinedMap<TDto, TDomain> | MapSpec<TDto, TDomain>, options?: MapperOptions<TDto>): Mapper<TDto, TDomain>;
71
+ export {};
@@ -0,0 +1,71 @@
1
+ import { getByPath } from "../core/path.js";
2
+ export function defineMap() {
3
+ return (spec) => spec;
4
+ }
5
+ /** Declares canonical DTO path -> Domain field names for consistency checks. */
6
+ export function defineMappingPolicy() {
7
+ return (spec) => spec;
8
+ }
9
+ /** Shorthand rule for direct DTO path reads. */
10
+ export function source(from, metadata) {
11
+ return { from, ...metadata };
12
+ }
13
+ /** Shorthand rule for DTO path reads that require a value conversion. */
14
+ export function transform(from, transform, metadata) {
15
+ return { from, transform, ...metadata };
16
+ }
17
+ /** Typed helpers for callbacks that need access to the source DTO shape. */
18
+ export function mapperHelpers() {
19
+ return {
20
+ source: (from, metadata) => source(from, metadata),
21
+ transform: (from, transform, metadata) => ({ from, transform, ...metadata }),
22
+ };
23
+ }
24
+ /** Runtime interpreter used as fallback when the transformer is not configured. */
25
+ export function makeMapper(spec, options) {
26
+ _handlePolicyViolations(_findPolicyViolations(spec, options?.policy), options?.policyMode ?? "warn");
27
+ return ((dto) => {
28
+ const output = {};
29
+ for (const key of Object.keys(spec)) {
30
+ const rule = spec[key];
31
+ const raw = getByPath(dto, String(rule.from));
32
+ const value = raw === undefined && _hasOwn(rule, "default") ? rule.default : raw;
33
+ output[key] = rule.transform ? rule.transform(value, dto) : value;
34
+ }
35
+ return output;
36
+ });
37
+ }
38
+ function _hasOwn(value, key) {
39
+ return Object.prototype.hasOwnProperty.call(value, key);
40
+ }
41
+ function _findPolicyViolations(spec, policy) {
42
+ if (!policy)
43
+ return [];
44
+ const canonicalByPath = new Map();
45
+ const violations = [];
46
+ for (const key of Object.keys(policy)) {
47
+ const from = String(policy[key].from);
48
+ const existing = canonicalByPath.get(from);
49
+ if (existing && existing !== key) {
50
+ violations.push(`DTO path "${from}" is canonically mapped as "${existing}", but this map uses "${key}".`);
51
+ continue;
52
+ }
53
+ canonicalByPath.set(from, key);
54
+ }
55
+ violations.push(...Object.keys(spec).flatMap((key) => {
56
+ const from = String(spec[key].from);
57
+ const expected = canonicalByPath.get(from);
58
+ return expected && expected !== key
59
+ ? [`DTO path "${from}" is canonically mapped as "${expected}", but this map uses "${key}".`]
60
+ : [];
61
+ }));
62
+ return violations;
63
+ }
64
+ function _handlePolicyViolations(violations, mode) {
65
+ if (!violations.length)
66
+ return;
67
+ const message = `[runtypex/mapper] Mapping policy violation:\n${violations.join("\n")}`;
68
+ if (mode === "error")
69
+ throw new Error(message);
70
+ console.warn(message);
71
+ }
@@ -0,0 +1,2 @@
1
+ import ts from "typescript";
2
+ export declare function resolveTypeByName(program: ts.Program, sf: ts.SourceFile, checker: ts.TypeChecker, name: string): ts.Type | null;
@@ -0,0 +1,57 @@
1
+ import ts from "typescript";
2
+ export function resolveTypeByName(program, sf, checker, name) {
3
+ // -1️⃣ Primitive type fallback
4
+ const primitiveNames = ["string", "number", "boolean", "bigint", "symbol", "null", "undefined"];
5
+ if (primitiveNames.includes(name)) {
6
+ const map = {
7
+ string: checker.getStringType(),
8
+ number: checker.getNumberType(),
9
+ boolean: checker.getBooleanType(),
10
+ bigint: checker.getBigIntType(),
11
+ symbol: checker.getESSymbolType(),
12
+ null: checker.getNullType(),
13
+ undefined: checker.getUndefinedType(),
14
+ };
15
+ return map[name];
16
+ }
17
+ // 2️⃣ Scan source files
18
+ for (const file of program.getSourceFiles()) {
19
+ const decl = _findLocalDeclaration(file, name);
20
+ if (!decl)
21
+ continue;
22
+ // ✅ type, interface, enum, class
23
+ if (ts.isInterfaceDeclaration(decl) ||
24
+ ts.isClassDeclaration(decl) ||
25
+ ts.isEnumDeclaration(decl)) {
26
+ if (decl.name) {
27
+ const symbol = checker.getSymbolAtLocation(decl.name);
28
+ if (symbol)
29
+ return checker.getDeclaredTypeOfSymbol(symbol);
30
+ }
31
+ }
32
+ if (ts.isTypeAliasDeclaration(decl)) {
33
+ return checker.getTypeFromTypeNode(decl.type);
34
+ }
35
+ }
36
+ // 3️⃣ Scope-based fallback
37
+ const symbol = checker
38
+ .getSymbolsInScope(sf, ts.SymbolFlags.Type | ts.SymbolFlags.Alias | ts.SymbolFlags.Interface)
39
+ .find((s) => s.name === name);
40
+ return symbol ? checker.getDeclaredTypeOfSymbol(symbol) : null;
41
+ }
42
+ function _findLocalDeclaration(sf, name) {
43
+ let found;
44
+ (function walk(node) {
45
+ if ((ts.isInterfaceDeclaration(node) ||
46
+ ts.isTypeAliasDeclaration(node) ||
47
+ ts.isEnumDeclaration(node) ||
48
+ ts.isClassDeclaration(node)) &&
49
+ node.name?.text === name) {
50
+ found = node;
51
+ return;
52
+ }
53
+ if (!found)
54
+ node.forEachChild(walk);
55
+ })(sf);
56
+ return found;
57
+ }
@@ -0,0 +1,3 @@
1
+ export { default } from "./ts-transformer.js";
2
+ export { default as tsTransformer } from "./ts-transformer.js";
3
+ export { default as vitePlugin } from "./vite-plugin.js";
@@ -0,0 +1,3 @@
1
+ export { default } from "./ts-transformer.js";
2
+ export { default as tsTransformer } from "./ts-transformer.js";
3
+ export { default as vitePlugin } from "./vite-plugin.js";
@@ -1,4 +1,10 @@
1
1
  import ts from "typescript";
2
+ type TransformerOptions = {
3
+ program: ts.Program;
4
+ removeInProd?: boolean;
5
+ validateDto?: boolean;
6
+ validateDomain?: boolean;
7
+ };
2
8
  /**
3
9
  * 🧩 tsTransformer
4
10
  * TypeScript custom transformer (BEFORE) factory.
@@ -19,7 +25,5 @@ import ts from "typescript";
19
25
  * ✅ Validation logic embedded at build-time
20
26
  * ✅ Optionally removed in production builds
21
27
  */
22
- export default function tsTransformer(options: {
23
- program: ts.Program;
24
- removeInProd?: boolean;
25
- }): (context: ts.TransformationContext) => (sf: ts.SourceFile) => ts.Node | undefined;
28
+ export default function tsTransformer(options: TransformerOptions): ts.TransformerFactory<ts.SourceFile>;
29
+ export {};
@@ -1,5 +1,6 @@
1
1
  import ts from "typescript";
2
2
  import { emitGuardFromType } from "../core/index.js";
3
+ import { emitMapperFromSpec } from "../core/emitMapperFromSpec.js";
3
4
  /**
4
5
  * 🧩 tsTransformer
5
6
  * TypeScript custom transformer (BEFORE) factory.
@@ -21,7 +22,6 @@ import { emitGuardFromType } from "../core/index.js";
21
22
  * ✅ Optionally removed in production builds
22
23
  */
23
24
  export default function tsTransformer(options) {
24
- const { program } = options;
25
25
  const checker = options.program.getTypeChecker();
26
26
  const removeInProd = !!options.removeInProd;
27
27
  const prod = process.env.NODE_ENV === "production";
@@ -29,13 +29,8 @@ export default function tsTransformer(options) {
29
29
  const visit = (node) => {
30
30
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
31
31
  const name = node.expression.text;
32
- const targetFunctions = ["makeValidate", "makeAssert"];
33
- if (targetFunctions.includes(name) && node.typeArguments?.length) {
34
- const typeNode = node.typeArguments[0];
35
- const typeName = typeNode.getText();
36
- const type = _resolveTypeByName(program, node.getSourceFile(), checker, typeName);
37
- if (!type)
38
- return node;
32
+ if ((name === "makeValidate" || name === "makeAssert") && node.typeArguments?.length) {
33
+ const type = checker.getTypeFromTypeNode(node.typeArguments[0]);
39
34
  const isRemovedInProd = removeInProd && prod;
40
35
  switch (name) {
41
36
  case "makeValidate":
@@ -44,69 +39,65 @@ export default function tsTransformer(options) {
44
39
  return _emitMakeAssert(checker, type, isRemovedInProd);
45
40
  }
46
41
  }
42
+ // makeMapper<TDto, TDomain>(spec) becomes an inline validating mapper.
43
+ if (name === "makeMapper" && node.typeArguments?.length === 2 && node.arguments[0]) {
44
+ const mapperCallOptions = _readMapperCallOptions(node.arguments[1]);
45
+ const mapper = emitMapperFromSpec({
46
+ checker,
47
+ dtoType: checker.getTypeFromTypeNode(node.typeArguments[0]),
48
+ domainType: checker.getTypeFromTypeNode(node.typeArguments[1]),
49
+ specNode: node.arguments[0],
50
+ sourceFile: node.getSourceFile(),
51
+ options: {
52
+ validateDto: !(removeInProd && prod) && options.validateDto !== false,
53
+ validateDomain: !(removeInProd && prod) && options.validateDomain !== false,
54
+ mappingPolicy: mapperCallOptions.policy,
55
+ policyMode: mapperCallOptions.policyMode,
56
+ },
57
+ });
58
+ if (mapper)
59
+ return ts.factory.createIdentifier(mapper);
60
+ }
47
61
  }
48
62
  return ts.visitEachChild(node, visit, context);
49
63
  };
50
64
  return (sf) => ts.visitNode(sf, visit);
51
65
  };
52
66
  }
53
- function _resolveTypeByName(program, sf, checker, name) {
54
- // 1️⃣ <string|number|boolean|null|undefined>
55
- const primitiveNames = ["string", "number", "boolean", "bigint", "symbol", "null", "undefined"];
56
- if (primitiveNames.includes(name)) {
57
- const map = {
58
- string: checker.getStringType(),
59
- number: checker.getNumberType(),
60
- boolean: checker.getBooleanType(),
61
- bigint: checker.getBigIntType(),
62
- symbol: checker.getESSymbolType(),
63
- null: checker.getNullType(),
64
- undefined: checker.getUndefinedType(),
65
- };
66
- return map[name];
67
- }
68
- // 2️⃣ <Type|Interface|Enum> declared in the same file or other files
69
- for (const file of program.getSourceFiles()) {
70
- const decl = _findLocalDeclaration(file, name);
71
- if (!decl)
72
- continue;
73
- if (ts.isInterfaceDeclaration(decl) || ts.isClassDeclaration(decl) || ts.isEnumDeclaration(decl)) {
74
- const symbol = decl.name ? checker.getSymbolAtLocation(decl.name) : null;
75
- if (symbol)
76
- return checker.getDeclaredTypeOfSymbol(symbol);
67
+ function _readMapperCallOptions(node) {
68
+ if (!node)
69
+ return {};
70
+ const expr = ts.isAsExpression(node) || ts.isParenthesizedExpression(node) ? node.expression : node;
71
+ if (!ts.isObjectLiteralExpression(expr))
72
+ return {};
73
+ return {
74
+ policy: _readExpressionProperty(expr, "policy"),
75
+ policyMode: _readPolicyMode(expr),
76
+ };
77
+ }
78
+ function _readExpressionProperty(object, name) {
79
+ for (const item of object.properties) {
80
+ if (ts.isPropertyAssignment(item) && ts.isIdentifier(item.name) && item.name.text === name) {
81
+ return item.initializer;
77
82
  }
78
- if (ts.isTypeAliasDeclaration(decl)) {
79
- return checker.getTypeFromTypeNode(decl.type);
83
+ if (ts.isShorthandPropertyAssignment(item) && item.name.text === name) {
84
+ return item.name;
80
85
  }
81
86
  }
82
- // 3️⃣ <Type|Interface|Enum> imported from other modules
83
- const symbol = checker
84
- .getSymbolsInScope(sf, ts.SymbolFlags.Type | ts.SymbolFlags.Alias | ts.SymbolFlags.Interface)
85
- .find((s) => s.name === name);
86
- return symbol ? checker.getDeclaredTypeOfSymbol(symbol) : null;
87
+ return undefined;
87
88
  }
88
- function _findLocalDeclaration(sf, name) {
89
- let found;
90
- (function walk(node) {
91
- if ((ts.isInterfaceDeclaration(node) ||
92
- ts.isTypeAliasDeclaration(node) ||
93
- ts.isEnumDeclaration(node) ||
94
- ts.isClassDeclaration(node)) &&
95
- node.name?.text === name) {
96
- found = node;
97
- return;
98
- }
99
- if (!found)
100
- node.forEachChild(walk);
101
- })(sf);
102
- return found;
89
+ function _readPolicyMode(object) {
90
+ const mode = _readExpressionProperty(object, "policyMode");
91
+ return mode && ts.isStringLiteral(mode) && (mode.text === "warn" || mode.text === "error") ? mode.text : undefined;
103
92
  }
104
93
  function _emitMakeValidate(checker, type, isRemovedInProd) {
105
94
  const guard = isRemovedInProd ? "((_)=>true)" : emitGuardFromType(checker, type);
106
95
  return ts.factory.createIdentifier(guard);
107
96
  }
108
97
  function _emitMakeAssert(checker, type, isRemovedInProd) {
109
- const guard = isRemovedInProd ? "((_)=>{})" : emitGuardFromType(checker, type);
98
+ if (isRemovedInProd)
99
+ return ts.factory.createIdentifier("((_)=>{})");
100
+ const guard = emitGuardFromType(checker, type);
110
101
  const txt = `(function(){const G=${guard};return(i)=>{if(!G(i))throw new TypeError("[runtypex] Validation failed.");};})()`;
111
102
  return ts.factory.createIdentifier(txt);
112
103
  }
@@ -1,6 +1,6 @@
1
1
  import ts from "typescript";
2
2
  import path from "node:path";
3
- import { emitGuardFromType } from "../core/index.js";
3
+ import tsTransformer from "./ts-transformer.js";
4
4
  /**
5
5
  * 🧩 vitePluginRuntypex
6
6
  * A Vite plugin that performs build-time type → runtime validation transformation.
@@ -17,24 +17,21 @@ import { emitGuardFromType } from "../core/index.js";
17
17
  */
18
18
  export default function vitePluginRuntypex(options) {
19
19
  const removeInProd = !!options?.removeInProd;
20
- const prod = process.env.NODE_ENV === "production";
21
20
  return {
22
21
  name: "vite-plugin-runtypex",
23
22
  enforce: "pre",
24
23
  transform(code, id) {
25
24
  const isTS = id.endsWith(".ts") || id.endsWith(".tsx");
26
- const isTargetFunction = /make(?:Validate|Assert)</.test(code);
25
+ const isTargetFunction = /make(?:Validate|Assert|Mapper)</.test(code);
27
26
  if (!isTS || !isTargetFunction)
28
27
  return;
29
- const { program, checker } = _createProgramFor(id);
28
+ const { program } = _createProgramFor(id);
30
29
  const sf = program.getSourceFile(id);
31
30
  if (!sf)
32
31
  return;
33
- let mutated = code;
34
- // makeAssert<T>()
35
- mutated = mutated.replace(/makeAssert<\s*([^>]+)\s*>\s*\(\s*\)/g, (_m, typeName) => _emitMakeAssert({ program, checker, sf, typeName, prod, removeInProd }) ?? _m);
36
- // ② makeValidate<T>()
37
- mutated = mutated.replace(/makeValidate<\s*([^>]+)\s*>\s*\(\s*\)/g, (_m, typeName) => _emitMakeValidate({ program, checker, sf, typeName, prod, removeInProd }) ?? _m);
32
+ const result = ts.transform(sf, [tsTransformer({ program, removeInProd })]);
33
+ const mutated = ts.createPrinter().printFile(result.transformed[0]);
34
+ result.dispose();
38
35
  return mutated === code ? null : { code: mutated, map: null };
39
36
  },
40
37
  };
@@ -49,8 +46,7 @@ function _createProgramFor(file) {
49
46
  throw new Error(ts.flattenDiagnosticMessageText(cfg.error.messageText, "\n"));
50
47
  const parsed = ts.parseJsonConfigFileContent(cfg.config, ts.sys, path.dirname(tsconfig));
51
48
  const program = ts.createProgram({ rootNames: parsed.fileNames, options: parsed.options });
52
- const checker = program.getTypeChecker();
53
- return { program, checker };
49
+ return { program };
54
50
  }
55
51
  function _findNearestTsconfig(start) {
56
52
  let dir = start;
@@ -68,85 +64,3 @@ function _findNearestTsconfig(start) {
68
64
  return fallback;
69
65
  throw new Error("tsconfig.json not found");
70
66
  }
71
- // ──────────────────────────────────────────────
72
- // ② Emit Helpers
73
- // ──────────────────────────────────────────────
74
- function _emitMakeValidate({ program, checker, sf, typeName, prod, removeInProd, }) {
75
- if (removeInProd && prod)
76
- return `((_)=>true)`;
77
- const type = _resolveTypeByName(program, sf, checker, typeName.trim());
78
- if (!type)
79
- return null;
80
- return emitGuardFromType(checker, type);
81
- }
82
- function _emitMakeAssert({ program, checker, sf, typeName, prod, removeInProd, }) {
83
- if (removeInProd && prod)
84
- return `((_)=>{})`;
85
- const type = _resolveTypeByName(program, sf, checker, typeName.trim());
86
- if (!type)
87
- return null;
88
- const guard = emitGuardFromType(checker, type);
89
- return `(function(){const G=${guard};return(i)=>{if(!G(i))throw new TypeError("[runtypex] Validation failed.");};})()`;
90
- }
91
- // ──────────────────────────────────────────────
92
- // ③ Type Resolution (support primitive/interface/type/enum)
93
- // ──────────────────────────────────────────────
94
- function _resolveTypeByName(program, sf, checker, name) {
95
- // -1️⃣ Primitive type fallback
96
- const primitiveNames = ["string", "number", "boolean", "bigint", "symbol", "null", "undefined"];
97
- if (primitiveNames.includes(name)) {
98
- const map = {
99
- string: checker.getStringType(),
100
- number: checker.getNumberType(),
101
- boolean: checker.getBooleanType(),
102
- bigint: checker.getBigIntType(),
103
- symbol: checker.getESSymbolType(),
104
- null: checker.getNullType(),
105
- undefined: checker.getUndefinedType(),
106
- };
107
- return map[name];
108
- }
109
- // 2️⃣ Scan source files
110
- for (const file of program.getSourceFiles()) {
111
- const decl = _findLocalDeclaration(file, name);
112
- if (!decl)
113
- continue;
114
- // ✅ type, interface, enum, class
115
- if (ts.isInterfaceDeclaration(decl) ||
116
- ts.isClassDeclaration(decl) ||
117
- ts.isEnumDeclaration(decl)) {
118
- if (decl.name) {
119
- const symbol = checker.getSymbolAtLocation(decl.name);
120
- if (symbol)
121
- return checker.getDeclaredTypeOfSymbol(symbol);
122
- }
123
- }
124
- if (ts.isTypeAliasDeclaration(decl)) {
125
- return checker.getTypeFromTypeNode(decl.type);
126
- }
127
- }
128
- // 3️⃣ Scope-based fallback
129
- const symbol = checker
130
- .getSymbolsInScope(sf, ts.SymbolFlags.Type | ts.SymbolFlags.Alias | ts.SymbolFlags.Interface)
131
- .find((s) => s.name === name);
132
- return symbol ? checker.getDeclaredTypeOfSymbol(symbol) : null;
133
- }
134
- // ──────────────────────────────────────────────
135
- // ④ AST Utility
136
- // ──────────────────────────────────────────────
137
- function _findLocalDeclaration(sf, name) {
138
- let found;
139
- (function walk(node) {
140
- if ((ts.isInterfaceDeclaration(node) ||
141
- ts.isTypeAliasDeclaration(node) ||
142
- ts.isEnumDeclaration(node) ||
143
- ts.isClassDeclaration(node)) &&
144
- node.name?.text === name) {
145
- found = node;
146
- return;
147
- }
148
- if (!found)
149
- node.forEachChild(walk);
150
- })(sf);
151
- return found;
152
- }
@@ -0,0 +1,89 @@
1
+ # Build Integrations
2
+
3
+ `runtypex` relies on the TypeScript compiler API, so its full value comes from
4
+ running the transformer during build.
5
+
6
+ ## Vite
7
+
8
+ ```ts
9
+ // vite.config.ts
10
+ import { defineConfig } from "vite";
11
+ import { vitePlugin as runtypex } from "runtypex";
12
+
13
+ export default defineConfig({
14
+ plugins: [runtypex()],
15
+ });
16
+ ```
17
+
18
+ The Vite plugin scans TypeScript files for:
19
+
20
+ - `makeValidate<T>()`
21
+ - `makeAssert<T>()`
22
+ - `makeMapper<TDto, TDomain>()`
23
+
24
+ When a target call is found, the plugin creates a TypeScript program for the
25
+ nearest `tsconfig.json`, runs the transformer, and returns the transformed code
26
+ to Vite.
27
+
28
+ ## ts-loader
29
+
30
+ ```js
31
+ // webpack.config.js
32
+ const { tsTransformer } = require("runtypex");
33
+
34
+ module.exports = {
35
+ module: {
36
+ rules: [
37
+ {
38
+ test: /\.tsx?$/,
39
+ loader: "ts-loader",
40
+ options: {
41
+ getCustomTransformers: (program) => ({
42
+ before: [tsTransformer({ program })],
43
+ }),
44
+ },
45
+ },
46
+ ],
47
+ },
48
+ };
49
+ ```
50
+
51
+ ## Transformer Options
52
+
53
+ ```ts
54
+ tsTransformer({
55
+ program,
56
+ removeInProd: true,
57
+ validateDto: true,
58
+ validateDomain: true,
59
+ });
60
+ ```
61
+
62
+ | Option | Default | Description |
63
+ | --- | --- | --- |
64
+ | `program` | required | TypeScript program used to resolve types. |
65
+ | `removeInProd` | `false` | Replaces generated validation with no-op functions in production. |
66
+ | `validateDto` | `true` | Enables DTO input validation for generated mappers. |
67
+ | `validateDomain` | `true` | Enables domain output validation for generated mappers. |
68
+
69
+ ## Package Entry Points
70
+
71
+ ```ts
72
+ import { makeValidate } from "runtypex";
73
+ import { makeMapper } from "runtypex/mapper";
74
+ import { generateJSDocFromSpec } from "runtypex/generator";
75
+ import { tsTransformer } from "runtypex/transformer";
76
+ import { vitePlugin } from "runtypex/transformer/vite-plugin";
77
+ ```
78
+
79
+ The package exports ESM and CommonJS builds from `dist/esm` and `dist/cjs`.
80
+
81
+ ## Local Verification
82
+
83
+ Useful verification commands:
84
+
85
+ ```bash
86
+ npm run build
87
+ npx jest --runInBand --watchman=false
88
+ npm run test:esm
89
+ ```