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
package/README.md
CHANGED
|
@@ -1,69 +1,112 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
69
|
-
|
|
1
|
+
# runtypex
|
|
2
|
+
|
|
3
|
+
`runtypex` generates runtime validation and mapping code from TypeScript types.
|
|
4
|
+
It keeps TypeScript types as the source of truth, so you do not need to maintain
|
|
5
|
+
a separate schema just to validate data at runtime.
|
|
6
|
+
|
|
7
|
+
## What It Solves
|
|
8
|
+
|
|
9
|
+
TypeScript types disappear after compilation. That means data from APIs,
|
|
10
|
+
databases, files, or external modules can still be invalid at runtime even when
|
|
11
|
+
the consuming code is type-safe at build time.
|
|
12
|
+
|
|
13
|
+
`runtypex` closes that gap by using the TypeScript compiler API to generate
|
|
14
|
+
runtime guards and mappers during build.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm i runtypex
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { makeAssert, makeValidate } from "runtypex";
|
|
26
|
+
|
|
27
|
+
interface User {
|
|
28
|
+
id: number;
|
|
29
|
+
name: string;
|
|
30
|
+
active: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const isUser = makeValidate<User>();
|
|
34
|
+
const assertUser = makeAssert<User>();
|
|
35
|
+
|
|
36
|
+
isUser({ id: 1, name: "Lux", active: true }); // true
|
|
37
|
+
assertUser({ id: "bad" }); // throws
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Vite Setup
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// vite.config.ts
|
|
44
|
+
import { defineConfig } from "vite";
|
|
45
|
+
import { vitePlugin as runtypex } from "runtypex";
|
|
46
|
+
|
|
47
|
+
export default defineConfig({
|
|
48
|
+
plugins: [runtypex()],
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
To replace validators with no-op functions in production builds:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
export default defineConfig({
|
|
56
|
+
plugins: [runtypex({ removeInProd: true })],
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Feature Docs
|
|
61
|
+
|
|
62
|
+
| Feature | Description |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| [Runtime validation](docs/runtime-validation.md) | Generate `makeValidate<T>()` and `makeAssert<T>()` implementations from TypeScript types. |
|
|
65
|
+
| [Mapper](docs/mapper.md) | Convert DTO shapes into domain shapes with typed mapping specs. |
|
|
66
|
+
| [Mapping policy](docs/mapping-policy.md) | Keep DTO path to domain field names consistent across multiple mappers. |
|
|
67
|
+
| [JSDoc generation](docs/jsdoc-generation.md) | Generate field documentation from mapper metadata. |
|
|
68
|
+
| [Build integrations](docs/build-integrations.md) | Configure Vite, ts-loader, ESM exports, and build behavior. |
|
|
69
|
+
|
|
70
|
+
## Mapper Example
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { defineMap, makeMapper, source, transform } from "runtypex/mapper";
|
|
74
|
+
|
|
75
|
+
interface UserDto {
|
|
76
|
+
user_id: string;
|
|
77
|
+
profile: { name: string };
|
|
78
|
+
status: "ACTIVE" | "INACTIVE";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface User {
|
|
82
|
+
id: string;
|
|
83
|
+
displayName: string;
|
|
84
|
+
isActive: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const userMap = defineMap<UserDto, User>()({
|
|
88
|
+
id: source("user_id", {
|
|
89
|
+
db: "users.user_id",
|
|
90
|
+
description: "User id",
|
|
91
|
+
dtoDescription: "User identifier from the user DTO.",
|
|
92
|
+
}),
|
|
93
|
+
displayName: source("profile.name"),
|
|
94
|
+
isActive: transform("status", (value) => value === "ACTIVE"),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const toUser = makeMapper<UserDto, User>(userMap);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Why runtypex?
|
|
101
|
+
|
|
102
|
+
| Goal | How runtypex handles it |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| Avoid schema duplication | Runtime code is generated from TypeScript types. |
|
|
105
|
+
| Validate external data | Generated guards check values after compilation. |
|
|
106
|
+
| Keep DTO and domain mapping explicit | Mapper specs make field movement visible and typed. |
|
|
107
|
+
| Reduce runtime overhead | Build-time generation avoids dynamic schema parsing. |
|
|
108
|
+
|
|
109
|
+
## Demo
|
|
110
|
+
|
|
111
|
+
[runtypex-demo](https://github.com/KumJungMin/runtypex-demo) shows TypeScript
|
|
112
|
+
types being transformed into runtime guards during build.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.emitArrayOrTuple = emitArrayOrTuple;
|
|
4
|
+
/**
|
|
5
|
+
* Handles array (T[]) and tuple ([A, B]) types.
|
|
6
|
+
*/
|
|
7
|
+
function emitArrayOrTuple(ctx, expr, t) {
|
|
8
|
+
if (ctx.checker.isTupleType(t)) {
|
|
9
|
+
return _emitTuple(t, expr, ctx);
|
|
10
|
+
}
|
|
11
|
+
if (ctx.checker.isArrayType(t)) {
|
|
12
|
+
return _emitArray(ctx, expr, t);
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Generate validation for Array<T>
|
|
18
|
+
*/
|
|
19
|
+
function _emitArray(ctx, expr, t) {
|
|
20
|
+
const arrayCheck = `Array.isArray(${expr})`;
|
|
21
|
+
// Try extracting element type
|
|
22
|
+
const element = ctx.checker.getElementTypeOfArrayType?.(t) ||
|
|
23
|
+
t.typeArguments?.[0] ||
|
|
24
|
+
t.getNumberIndexType?.();
|
|
25
|
+
if (!element)
|
|
26
|
+
return arrayCheck;
|
|
27
|
+
const eachCheck = `${expr}.every(e=>${ctx.emit("e", element)})`;
|
|
28
|
+
return `(${arrayCheck}&&${eachCheck})`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Generate validation for Tuple [A, B, ...]
|
|
32
|
+
*/
|
|
33
|
+
function _emitTuple(ref, expr, ctx) {
|
|
34
|
+
const elements = ref.typeArguments ?? ctx.checker.getTypeArguments?.(ref) ?? [];
|
|
35
|
+
const arrayCheck = `Array.isArray(${expr})`;
|
|
36
|
+
const lenCheck = `${expr}.length===${elements.length}`;
|
|
37
|
+
const elementChecks = elements.map((el, i) => ctx.emit(`${expr}[${i}]`, el));
|
|
38
|
+
const parts = [arrayCheck, lenCheck, ...elementChecks];
|
|
39
|
+
return `(${parts.join("&&")})`;
|
|
40
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.emitLiteralOrEnum = emitLiteralOrEnum;
|
|
7
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
+
/** Handles literal types and enum-like types. */
|
|
9
|
+
function emitLiteralOrEnum(_, expr, t) {
|
|
10
|
+
if (t.isLiteral()) {
|
|
11
|
+
const value = t.value;
|
|
12
|
+
const isString = typeof value === "string";
|
|
13
|
+
const newValue = isString ? JSON.stringify(value) : String(value);
|
|
14
|
+
return `${expr}===${newValue}`;
|
|
15
|
+
}
|
|
16
|
+
const isEnum = t.flags & typescript_1.default.TypeFlags.EnumLike;
|
|
17
|
+
if (isEnum) {
|
|
18
|
+
const enumValues = _extractEnumValues(t);
|
|
19
|
+
if (enumValues.length) {
|
|
20
|
+
return `(${enumValues.map(v => `${expr}===${v}`).join("||")})`;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
// Extracts numeric or string values from an Enum declaration.
|
|
26
|
+
function _extractEnumValues(t) {
|
|
27
|
+
const symbol = t.getSymbol();
|
|
28
|
+
if (!symbol)
|
|
29
|
+
return [];
|
|
30
|
+
const values = [];
|
|
31
|
+
const declarations = symbol.getDeclarations() ?? [];
|
|
32
|
+
for (const declaration of declarations) {
|
|
33
|
+
const isEnum = typescript_1.default.isEnumDeclaration(declaration);
|
|
34
|
+
if (!isEnum)
|
|
35
|
+
continue;
|
|
36
|
+
for (const member of declaration.members) {
|
|
37
|
+
const init = member.initializer;
|
|
38
|
+
if (!init)
|
|
39
|
+
continue;
|
|
40
|
+
if (typescript_1.default.isStringLiteral(init) || typescript_1.default.isNumericLiteral(init)) {
|
|
41
|
+
const value = typescript_1.default.isStringLiteral(init) ? JSON.stringify(init.text) : init.text;
|
|
42
|
+
values.push(value);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return values;
|
|
47
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
export type MapperEmitOptions = {
|
|
3
|
+
validateDto?: boolean;
|
|
4
|
+
validateDomain?: boolean;
|
|
5
|
+
mappingPolicy?: ts.Expression;
|
|
6
|
+
policyMode?: "warn" | "error";
|
|
7
|
+
};
|
|
8
|
+
export type MapRuleInfo = {
|
|
9
|
+
key: string;
|
|
10
|
+
from: string;
|
|
11
|
+
db?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
dtoDescription?: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function emitMapperFromSpec(params: {
|
|
16
|
+
checker: ts.TypeChecker;
|
|
17
|
+
dtoType: ts.Type;
|
|
18
|
+
domainType: ts.Type;
|
|
19
|
+
specNode: ts.Expression;
|
|
20
|
+
sourceFile: ts.SourceFile;
|
|
21
|
+
options?: MapperEmitOptions;
|
|
22
|
+
}): string | null;
|
|
23
|
+
export declare function readMapRules(checker: ts.TypeChecker, specNode: ts.Expression): Map<string, MapRuleInfo>;
|
|
24
|
+
export type MapPolicyViolation = {
|
|
25
|
+
from: string;
|
|
26
|
+
expectedKey: string;
|
|
27
|
+
actualKey: string;
|
|
28
|
+
};
|
|
29
|
+
export declare function findMapPolicyViolations(checker: ts.TypeChecker, specNode: ts.Expression, policyNode: ts.Expression | undefined): MapPolicyViolation[];
|
|
30
|
+
export declare function handleMapPolicyViolations(violations: MapPolicyViolation[], mode: "warn" | "error"): void;
|
|
31
|
+
/** Finds the mapping object behind inline, defineMap-wrapped, or identifier specs. */
|
|
32
|
+
export declare function resolveMapSpecObject(checker: ts.TypeChecker, node: ts.Expression): ts.ObjectLiteralExpression | null;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.emitMapperFromSpec = emitMapperFromSpec;
|
|
7
|
+
exports.readMapRules = readMapRules;
|
|
8
|
+
exports.findMapPolicyViolations = findMapPolicyViolations;
|
|
9
|
+
exports.handleMapPolicyViolations = handleMapPolicyViolations;
|
|
10
|
+
exports.resolveMapSpecObject = resolveMapSpecObject;
|
|
11
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
12
|
+
const index_js_1 = require("./index.js");
|
|
13
|
+
const path_js_1 = require("./path.js");
|
|
14
|
+
function emitMapperFromSpec(params) {
|
|
15
|
+
// Resolve the concrete object literal so generated code does not retain DSL calls.
|
|
16
|
+
const specObject = resolveMapSpecObject(params.checker, params.specNode);
|
|
17
|
+
if (!specObject)
|
|
18
|
+
return null;
|
|
19
|
+
handleMapPolicyViolations(findMapPolicyViolations(params.checker, specObject, params.options?.mappingPolicy), params.options?.policyMode ?? "warn");
|
|
20
|
+
const rules = readMapRules(params.checker, specObject);
|
|
21
|
+
const props = params.checker.getPropertiesOfType(params.domainType);
|
|
22
|
+
const fields = [];
|
|
23
|
+
for (const prop of props) {
|
|
24
|
+
const rule = rules.get(prop.name);
|
|
25
|
+
if (!rule)
|
|
26
|
+
return null;
|
|
27
|
+
fields.push(`${JSON.stringify(prop.name)}:R(${JSON.stringify(prop.name)},${(0, path_js_1.emitPathAccess)("input", rule.from)})`);
|
|
28
|
+
}
|
|
29
|
+
const specText = _emitRuntimeSpecText(specObject, params.sourceFile);
|
|
30
|
+
const dtoGuard = params.options?.validateDto === false ? null : (0, index_js_1.emitGuardFromType)(params.checker, params.dtoType);
|
|
31
|
+
const domainGuard = params.options?.validateDomain === false ? null : (0, index_js_1.emitGuardFromType)(params.checker, params.domainType);
|
|
32
|
+
return [
|
|
33
|
+
`(function(){const S=${specText};`,
|
|
34
|
+
dtoGuard ? `const VD=${dtoGuard};` : "",
|
|
35
|
+
domainGuard ? `const VO=${domainGuard};` : "",
|
|
36
|
+
`return(input)=>{`,
|
|
37
|
+
dtoGuard ? `if(!VD(input))throw new TypeError("[runtypex] DTO validation failed.");` : "",
|
|
38
|
+
`const R=(key,raw)=>{const rule=S[key];const value=raw===undefined&&Object.prototype.hasOwnProperty.call(rule,"default")?rule.default:raw;return typeof rule.transform==="function"?rule.transform(value,input):value;};`,
|
|
39
|
+
`const output={${fields.join(",")}};`,
|
|
40
|
+
domainGuard ? `if(!VO(output))throw new TypeError("[runtypex] Domain validation failed.");` : "",
|
|
41
|
+
`return output;};})()`,
|
|
42
|
+
].join("");
|
|
43
|
+
}
|
|
44
|
+
function readMapRules(checker, specNode) {
|
|
45
|
+
const object = resolveMapSpecObject(checker, specNode);
|
|
46
|
+
const rules = new Map();
|
|
47
|
+
if (!object)
|
|
48
|
+
return rules;
|
|
49
|
+
for (const prop of object.properties) {
|
|
50
|
+
if (!typescript_1.default.isPropertyAssignment(prop))
|
|
51
|
+
continue;
|
|
52
|
+
const key = _propertyName(prop.name);
|
|
53
|
+
const rule = _readRule(prop.initializer);
|
|
54
|
+
if (key && rule)
|
|
55
|
+
rules.set(key, { key, ...rule });
|
|
56
|
+
}
|
|
57
|
+
return rules;
|
|
58
|
+
}
|
|
59
|
+
function findMapPolicyViolations(checker, specNode, policyNode) {
|
|
60
|
+
if (!policyNode)
|
|
61
|
+
return [];
|
|
62
|
+
const rules = readMapRules(checker, specNode);
|
|
63
|
+
const policyRules = readMapRules(checker, policyNode);
|
|
64
|
+
const canonicalByPath = new Map();
|
|
65
|
+
const violations = [];
|
|
66
|
+
for (const rule of policyRules.values()) {
|
|
67
|
+
const existing = canonicalByPath.get(rule.from);
|
|
68
|
+
if (existing && existing !== rule.key) {
|
|
69
|
+
violations.push({ from: rule.from, expectedKey: existing, actualKey: rule.key });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
canonicalByPath.set(rule.from, rule.key);
|
|
73
|
+
}
|
|
74
|
+
violations.push(...Array.from(rules.values()).flatMap((rule) => {
|
|
75
|
+
const expected = canonicalByPath.get(rule.from);
|
|
76
|
+
return expected && expected !== rule.key
|
|
77
|
+
? [{ from: rule.from, expectedKey: expected, actualKey: rule.key }]
|
|
78
|
+
: [];
|
|
79
|
+
}));
|
|
80
|
+
return violations;
|
|
81
|
+
}
|
|
82
|
+
function handleMapPolicyViolations(violations, mode) {
|
|
83
|
+
if (!violations.length)
|
|
84
|
+
return;
|
|
85
|
+
const details = violations
|
|
86
|
+
.map((item) => `DTO path "${item.from}" is canonically mapped as "${item.expectedKey}", but this map uses "${item.actualKey}".`)
|
|
87
|
+
.join("\n");
|
|
88
|
+
const message = `[runtypex/mapper] Mapping policy violation:\n${details}`;
|
|
89
|
+
if (mode === "error")
|
|
90
|
+
throw new Error(message);
|
|
91
|
+
console.warn(message);
|
|
92
|
+
}
|
|
93
|
+
/** Finds the mapping object behind inline, defineMap-wrapped, or identifier specs. */
|
|
94
|
+
function resolveMapSpecObject(checker, node) {
|
|
95
|
+
const expr = _skip(node);
|
|
96
|
+
if (typescript_1.default.isObjectLiteralExpression(expr))
|
|
97
|
+
return expr;
|
|
98
|
+
if (typescript_1.default.isCallExpression(expr) && expr.arguments[0]) {
|
|
99
|
+
const arg = _skip(expr.arguments[0]);
|
|
100
|
+
if (typescript_1.default.isObjectLiteralExpression(arg))
|
|
101
|
+
return arg;
|
|
102
|
+
}
|
|
103
|
+
if (typescript_1.default.isCallExpression(expr) && typescript_1.default.isCallExpression(expr.expression)) {
|
|
104
|
+
return resolveMapSpecObject(checker, expr.expression);
|
|
105
|
+
}
|
|
106
|
+
if (typescript_1.default.isIdentifier(expr)) {
|
|
107
|
+
const symbol = checker.getShorthandAssignmentValueSymbol?.(expr) ?? checker.getSymbolAtLocation(expr);
|
|
108
|
+
const declaration = symbol?.valueDeclaration ?? symbol?.declarations?.[0];
|
|
109
|
+
if (declaration && typescript_1.default.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
110
|
+
return resolveMapSpecObject(checker, declaration.initializer);
|
|
111
|
+
}
|
|
112
|
+
const variable = _findVariableDeclaration(expr.getSourceFile(), expr.text, expr.getStart(expr.getSourceFile()));
|
|
113
|
+
if (variable?.initializer)
|
|
114
|
+
return resolveMapSpecObject(checker, variable.initializer);
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function _readRule(node) {
|
|
119
|
+
const expr = _skip(node);
|
|
120
|
+
if (typescript_1.default.isObjectLiteralExpression(expr)) {
|
|
121
|
+
return _readRuleObject(expr);
|
|
122
|
+
}
|
|
123
|
+
if (typescript_1.default.isCallExpression(expr) && expr.arguments[0]) {
|
|
124
|
+
const from = _stringValue(expr.arguments[0]);
|
|
125
|
+
const metadata = expr.arguments.length > 2 ? expr.arguments[2] : expr.arguments[1];
|
|
126
|
+
if (!from)
|
|
127
|
+
return null;
|
|
128
|
+
return { from, ..._readMetadata(metadata) };
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
function _readRuleObject(object) {
|
|
133
|
+
const from = _readStringProperty(object, "from");
|
|
134
|
+
if (!from)
|
|
135
|
+
return null;
|
|
136
|
+
return {
|
|
137
|
+
from,
|
|
138
|
+
db: _readStringProperty(object, "db") ?? undefined,
|
|
139
|
+
description: _readStringProperty(object, "description") ?? undefined,
|
|
140
|
+
dtoDescription: _readStringProperty(object, "dtoDescription") ?? undefined,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function _readMetadata(node) {
|
|
144
|
+
const expr = node ? _skip(node) : null;
|
|
145
|
+
if (!expr || !typescript_1.default.isObjectLiteralExpression(expr))
|
|
146
|
+
return {};
|
|
147
|
+
return {
|
|
148
|
+
db: _readStringProperty(expr, "db") ?? undefined,
|
|
149
|
+
description: _readStringProperty(expr, "description") ?? undefined,
|
|
150
|
+
dtoDescription: _readStringProperty(expr, "dtoDescription") ?? undefined,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function _readStringProperty(object, name) {
|
|
154
|
+
const prop = object.properties.find((item) => typescript_1.default.isPropertyAssignment(item) && _propertyName(item.name) === name);
|
|
155
|
+
return prop ? _stringValue(prop.initializer) : null;
|
|
156
|
+
}
|
|
157
|
+
function _skip(node) {
|
|
158
|
+
let expr = node;
|
|
159
|
+
while (typescript_1.default.isParenthesizedExpression(expr) || typescript_1.default.isAsExpression(expr) || typescript_1.default.isTypeAssertionExpression(expr)) {
|
|
160
|
+
expr = expr.expression;
|
|
161
|
+
}
|
|
162
|
+
return expr;
|
|
163
|
+
}
|
|
164
|
+
function _propertyName(name) {
|
|
165
|
+
if (typescript_1.default.isIdentifier(name) || typescript_1.default.isStringLiteral(name) || typescript_1.default.isNumericLiteral(name))
|
|
166
|
+
return name.text;
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
function _stringValue(node) {
|
|
170
|
+
if (typescript_1.default.isStringLiteral(node) || typescript_1.default.isNoSubstitutionTemplateLiteral(node))
|
|
171
|
+
return node.text;
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
function _findVariableDeclaration(sourceFile, name, before) {
|
|
175
|
+
let found = null;
|
|
176
|
+
const visit = (node) => {
|
|
177
|
+
if (typescript_1.default.isVariableDeclaration(node) &&
|
|
178
|
+
typescript_1.default.isIdentifier(node.name) &&
|
|
179
|
+
node.name.text === name &&
|
|
180
|
+
node.getStart(sourceFile) < before) {
|
|
181
|
+
found = node;
|
|
182
|
+
}
|
|
183
|
+
node.forEachChild(visit);
|
|
184
|
+
};
|
|
185
|
+
visit(sourceFile);
|
|
186
|
+
return found;
|
|
187
|
+
}
|
|
188
|
+
function _emitRuntimeSpecText(specObject, sourceFile) {
|
|
189
|
+
// Remove TypeScript-only syntax from inline transform callbacks before embedding.
|
|
190
|
+
const marker = "__runtypexSpec";
|
|
191
|
+
const output = typescript_1.default.transpileModule(`const ${marker} = ${specObject.getText(sourceFile)};`, {
|
|
192
|
+
compilerOptions: {
|
|
193
|
+
module: typescript_1.default.ModuleKind.ESNext,
|
|
194
|
+
target: typescript_1.default.ScriptTarget.ESNext,
|
|
195
|
+
},
|
|
196
|
+
}).outputText.trim();
|
|
197
|
+
const prefix = `const ${marker} = `;
|
|
198
|
+
return output.startsWith(prefix) ? output.slice(prefix.length).replace(/;$/, "") : specObject.getText(sourceFile);
|
|
199
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.emitObject = emitObject;
|
|
7
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
+
const path_js_1 = require("./path.js");
|
|
9
|
+
/**
|
|
10
|
+
* Handles interfaces, classes, and object-like structures.
|
|
11
|
+
*/
|
|
12
|
+
function emitObject(ctx, expr, t) {
|
|
13
|
+
const isObject = (t.getFlags() & typescript_1.default.TypeFlags.Object) !== 0;
|
|
14
|
+
if (!isObject)
|
|
15
|
+
return null;
|
|
16
|
+
const props = ctx.checker.getPropertiesOfType(t);
|
|
17
|
+
const parts = [`typeof ${expr}==="object"`, `${expr}!==null`];
|
|
18
|
+
for (const prop of props) {
|
|
19
|
+
const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
|
|
20
|
+
if (!declaration)
|
|
21
|
+
continue;
|
|
22
|
+
const propType = ctx.checker.getTypeOfSymbolAtLocation(prop, declaration);
|
|
23
|
+
const isOptional = (prop.getFlags() & typescript_1.default.SymbolFlags.Optional) !== 0;
|
|
24
|
+
const propExpr = (0, path_js_1.emitPropertyAccess)(expr, prop.name);
|
|
25
|
+
const condition = ctx.emit(propExpr, propType);
|
|
26
|
+
const checkExpr = isOptional ? `(${propExpr}===undefined||${condition})` : condition;
|
|
27
|
+
parts.push(checkExpr);
|
|
28
|
+
}
|
|
29
|
+
return `(${parts.join("&&")})`;
|
|
30
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.emitPrimitive = emitPrimitive;
|
|
7
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
+
/**
|
|
9
|
+
* Handles primitive types like number, string, boolean...
|
|
10
|
+
*/
|
|
11
|
+
function emitPrimitive(ctx, expr, t) {
|
|
12
|
+
if ((t.flags & (typescript_1.default.TypeFlags.Any | typescript_1.default.TypeFlags.Unknown)) !== 0)
|
|
13
|
+
return "true";
|
|
14
|
+
if (t.flags & typescript_1.default.TypeFlags.Null)
|
|
15
|
+
return `${expr}===null`;
|
|
16
|
+
if (t.flags & typescript_1.default.TypeFlags.Undefined)
|
|
17
|
+
return `${expr}===undefined`;
|
|
18
|
+
if (t.flags & typescript_1.default.TypeFlags.BooleanLike)
|
|
19
|
+
return `typeof ${expr}==="boolean"`;
|
|
20
|
+
if (t.flags & typescript_1.default.TypeFlags.NumberLike)
|
|
21
|
+
return `typeof ${expr}==="number"`;
|
|
22
|
+
if (t.flags & typescript_1.default.TypeFlags.StringLike)
|
|
23
|
+
return `typeof ${expr}==="string"`;
|
|
24
|
+
if (t.flags & typescript_1.default.TypeFlags.BigIntLike)
|
|
25
|
+
return `typeof ${expr}==="bigint"`;
|
|
26
|
+
if (t.flags & typescript_1.default.TypeFlags.ESSymbolLike)
|
|
27
|
+
return `typeof ${expr}==="symbol"`;
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.emitUnionOrIntersection = emitUnionOrIntersection;
|
|
4
|
+
/**
|
|
5
|
+
* Handles union (A | B) and intersection (A & B) types.
|
|
6
|
+
*/
|
|
7
|
+
function emitUnionOrIntersection(ctx, expr, t) {
|
|
8
|
+
if (t.isUnion()) {
|
|
9
|
+
return `(${t.types.map(tt => ctx.emit(expr, tt)).join("||")})`;
|
|
10
|
+
}
|
|
11
|
+
if (t.isIntersection()) {
|
|
12
|
+
return `(${t.types.map(tt => ctx.emit(expr, tt)).join("&&")})`;
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
/**
|
|
3
|
+
* ✅ emitGuardFromType
|
|
4
|
+
* Converts a TypeScript type to a JavaScript runtime validation function string.
|
|
5
|
+
*/
|
|
6
|
+
export declare function emitGuardFromType(checker: ts.TypeChecker, type: ts.Type): string;
|
|
7
|
+
/**
|
|
8
|
+
* ✅ GenContext
|
|
9
|
+
* Internal helper for converting TypeScript types to JS validation expressions.
|
|
10
|
+
*/
|
|
11
|
+
export declare class GenContext {
|
|
12
|
+
checker: ts.TypeChecker;
|
|
13
|
+
private seen;
|
|
14
|
+
constructor(checker: ts.TypeChecker);
|
|
15
|
+
/** Top-level router — delegates each type to the correct handler. */
|
|
16
|
+
emit(expr: string, t: ts.Type): string;
|
|
17
|
+
}
|