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.
- package/README.md +112 -69
- package/dist/cjs/core/emitArrayOrTuple.d.ts +6 -0
- package/dist/cjs/core/emitArrayOrTuple.js +40 -0
- package/dist/cjs/core/emitLiteralOrEnum.d.ts +4 -0
- package/dist/cjs/core/emitLiteralOrEnum.js +47 -0
- package/dist/cjs/core/emitMapperFromSpec.d.ts +32 -0
- package/dist/cjs/core/emitMapperFromSpec.js +199 -0
- package/dist/cjs/core/emitObject.d.ts +6 -0
- package/dist/cjs/core/emitObject.js +30 -0
- package/dist/cjs/core/emitPrimitive.d.ts +6 -0
- package/dist/cjs/core/emitPrimitive.js +29 -0
- package/dist/cjs/core/emitUnionOrIntersection.d.ts +6 -0
- package/dist/cjs/core/emitUnionOrIntersection.js +15 -0
- package/dist/cjs/core/index.d.ts +17 -0
- package/dist/cjs/core/index.js +41 -0
- package/dist/cjs/core/path.d.ts +9 -0
- package/dist/cjs/core/path.js +42 -0
- package/dist/cjs/generator/generate-jsdoc.d.ts +13 -0
- package/dist/cjs/generator/generate-jsdoc.js +69 -0
- package/dist/cjs/generator/index.d.ts +1 -0
- package/dist/cjs/generator/index.js +17 -0
- package/dist/cjs/index.d.ts +4 -0
- package/dist/cjs/index.js +20 -0
- package/dist/cjs/mapper/index.d.ts +1 -0
- package/dist/cjs/mapper/index.js +17 -0
- package/dist/cjs/runtime/index.d.ts +2 -0
- package/dist/cjs/runtime/index.js +18 -0
- package/dist/cjs/runtime/mapper.d.ts +71 -0
- package/dist/cjs/runtime/mapper.js +79 -0
- package/dist/cjs/runtime/validate.d.ts +11 -0
- package/dist/cjs/runtime/validate.js +18 -0
- package/dist/cjs/transformer/helper.d.ts +2 -0
- package/dist/cjs/transformer/helper.js +63 -0
- package/dist/cjs/transformer/index.d.ts +3 -0
- package/dist/cjs/transformer/index.js +12 -0
- package/dist/cjs/transformer/ts-transformer.d.ts +29 -0
- package/dist/cjs/transformer/ts-transformer.js +109 -0
- package/dist/cjs/transformer/vite-plugin.d.ts +18 -0
- package/dist/cjs/transformer/vite-plugin.js +72 -0
- package/dist/esm/core/emitArrayOrTuple.d.ts +1 -1
- package/dist/esm/core/emitLiteralOrEnum.d.ts +1 -1
- package/dist/esm/core/emitMapperFromSpec.d.ts +32 -0
- package/dist/esm/core/emitMapperFromSpec.js +189 -0
- package/dist/esm/core/emitObject.d.ts +1 -1
- package/dist/esm/core/emitObject.js +4 -2
- package/dist/esm/core/emitPrimitive.d.ts +1 -1
- package/dist/esm/core/emitUnionOrIntersection.d.ts +1 -1
- package/dist/esm/core/path.d.ts +9 -0
- package/dist/esm/core/path.js +36 -0
- package/dist/esm/generator/generate-jsdoc.d.ts +13 -0
- package/dist/esm/generator/generate-jsdoc.js +63 -0
- package/dist/esm/generator/index.d.ts +1 -0
- package/dist/esm/generator/index.js +1 -0
- package/dist/esm/index.d.ts +4 -3
- package/dist/esm/index.js +1 -0
- package/dist/esm/mapper/index.d.ts +1 -0
- package/dist/esm/mapper/index.js +1 -0
- package/dist/esm/runtime/index.d.ts +2 -0
- package/dist/esm/runtime/index.js +2 -0
- package/dist/esm/runtime/mapper.d.ts +71 -0
- package/dist/esm/runtime/mapper.js +71 -0
- package/dist/esm/transformer/helper.d.ts +2 -0
- package/dist/esm/transformer/helper.js +57 -0
- package/dist/esm/transformer/index.d.ts +3 -0
- package/dist/esm/transformer/index.js +3 -0
- package/dist/esm/transformer/ts-transformer.d.ts +8 -4
- package/dist/esm/transformer/ts-transformer.js +46 -55
- package/dist/esm/transformer/vite-plugin.js +7 -93
- package/docs/build-integrations.md +89 -0
- package/docs/jsdoc-generation.md +88 -0
- package/docs/mapper.md +104 -0
- package/docs/mapping-policy.md +78 -0
- package/docs/runtime-validation.md +84 -0
- 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,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
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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.
|
|
79
|
-
return
|
|
83
|
+
if (ts.isShorthandPropertyAssignment(item) && item.name.text === name) {
|
|
84
|
+
return item.name;
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
|
-
|
|
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
|
|
89
|
-
|
|
90
|
-
(
|
|
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
|
-
|
|
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
|
|
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
|
|
28
|
+
const { program } = _createProgramFor(id);
|
|
30
29
|
const sf = program.getSourceFile(id);
|
|
31
30
|
if (!sf)
|
|
32
31
|
return;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
+
```
|