runtypex 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- 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/emitObject.d.ts +6 -0
- package/dist/cjs/core/emitObject.js +28 -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/index.d.ts +3 -0
- package/dist/cjs/index.js +13 -0
- package/dist/cjs/runtime/validate.d.ts +10 -0
- package/dist/cjs/runtime/validate.js +18 -0
- package/dist/cjs/transformer/ts-transformer.d.ts +25 -0
- package/dist/cjs/transformer/ts-transformer.js +103 -0
- package/dist/cjs/transformer/vite-plugin.d.ts +18 -0
- package/dist/cjs/transformer/vite-plugin.js +137 -0
- package/dist/esm/core/emitArrayOrTuple.d.ts +6 -0
- package/dist/esm/core/emitArrayOrTuple.js +37 -0
- package/dist/esm/core/emitLiteralOrEnum.d.ts +4 -0
- package/dist/esm/core/emitLiteralOrEnum.js +41 -0
- package/dist/esm/core/emitObject.d.ts +6 -0
- package/dist/esm/core/emitObject.js +22 -0
- package/dist/esm/core/emitPrimitive.d.ts +6 -0
- package/dist/esm/core/emitPrimitive.js +23 -0
- package/dist/esm/core/emitUnionOrIntersection.d.ts +6 -0
- package/dist/esm/core/emitUnionOrIntersection.js +12 -0
- package/dist/esm/core/index.d.ts +17 -0
- package/dist/esm/core/index.js +36 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/runtime/validate.d.ts +10 -0
- package/dist/esm/runtime/validate.js +14 -0
- package/dist/esm/transformer/ts-transformer.d.ts +25 -0
- package/dist/esm/transformer/ts-transformer.js +97 -0
- package/dist/esm/transformer/vite-plugin.d.ts +18 -0
- package/dist/esm/transformer/vite-plugin.js +131 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# 🛡️ runtypex
|
|
2
|
+
|
|
3
|
+
Runtime type guards compiled from your TypeScript types.
|
|
4
|
+
No schemas. No decorators. Just types → blazing-fast runtime checks.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## Use
|
|
8
|
+
```ts
|
|
9
|
+
import { makeValidate, makeAssert } from "runtypex";
|
|
10
|
+
|
|
11
|
+
interface User { id: number; name: string; active: boolean; }
|
|
12
|
+
|
|
13
|
+
const isUser = makeValidate<User>();
|
|
14
|
+
const assertUser: ReturnType<typeof makeAssert<User>> = makeAssert<User>();
|
|
15
|
+
|
|
16
|
+
isUser({ id: 1, name: "Lux", active: true }); // true
|
|
17
|
+
assertUser({ id: "bad" }); // throws
|
|
18
|
+
toUser({ nope: true }); // → fallback
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Vite
|
|
22
|
+
```ts
|
|
23
|
+
// vite.config.ts
|
|
24
|
+
import { defineConfig } from "vite";
|
|
25
|
+
import { vitePlugin as runtypex } from "runtypex";
|
|
26
|
+
|
|
27
|
+
export default defineConfig({
|
|
28
|
+
plugins: [runtypex()],
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
To disable runtime checks in production builds, pass the option `{ removeInProd: true }` when initializing the Vite plugin.
|
|
33
|
+
```ts
|
|
34
|
+
// vite.config.ts
|
|
35
|
+
import { defineConfig } from "vite";
|
|
36
|
+
import { vitePlugin as runtypex } from "runtypex";
|
|
37
|
+
|
|
38
|
+
export default defineConfig({
|
|
39
|
+
plugins: [runtypex({ removeInProd: true })],
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Webpack (ts-loader)
|
|
44
|
+
```js
|
|
45
|
+
// webpack.config.js
|
|
46
|
+
const { tsTransformer } = require("runtypex/dist/ts-transformer.js");
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
module: {
|
|
50
|
+
rules: [
|
|
51
|
+
{
|
|
52
|
+
test: /\.tsx?$/,
|
|
53
|
+
loader: "ts-loader",
|
|
54
|
+
options: {
|
|
55
|
+
getCustomTransformers: (program) => ({
|
|
56
|
+
before: [ tsTransformer({ program }) ]
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Why runtypex?
|
|
66
|
+
- ⚡ **Fast**: compiled checks, no runtime schema walk
|
|
67
|
+
- 🧩 **Simple**: types only, no schema duplication
|
|
68
|
+
- 🧱 **Flexible**: Vite or Webpack
|
|
69
|
+
- 🛠️ **APIs**: `makeValidate`, `makeAssert`
|
|
@@ -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,28 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Handles interfaces, classes, and object-like structures.
|
|
10
|
+
*/
|
|
11
|
+
function emitObject(ctx, expr, t) {
|
|
12
|
+
const isObject = (t.getFlags() & typescript_1.default.TypeFlags.Object) !== 0;
|
|
13
|
+
if (!isObject)
|
|
14
|
+
return null;
|
|
15
|
+
const props = ctx.checker.getPropertiesOfType(t);
|
|
16
|
+
const parts = [`typeof ${expr}==="object"`, `${expr}!==null`];
|
|
17
|
+
for (const prop of props) {
|
|
18
|
+
const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
|
|
19
|
+
if (!declaration)
|
|
20
|
+
continue;
|
|
21
|
+
const propType = ctx.checker.getTypeOfSymbolAtLocation(prop, declaration);
|
|
22
|
+
const isOptional = (prop.getFlags() & typescript_1.default.SymbolFlags.Optional) !== 0;
|
|
23
|
+
const condition = ctx.emit(`${expr}.${prop.name}`, propType);
|
|
24
|
+
const checkExpr = isOptional ? `(${expr}.${prop.name}===undefined||${condition})` : condition;
|
|
25
|
+
parts.push(checkExpr);
|
|
26
|
+
}
|
|
27
|
+
return `(${parts.join("&&")})`;
|
|
28
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GenContext = void 0;
|
|
4
|
+
exports.emitGuardFromType = emitGuardFromType;
|
|
5
|
+
const emitPrimitive_1 = require("./emitPrimitive");
|
|
6
|
+
const emitLiteralOrEnum_1 = require("./emitLiteralOrEnum");
|
|
7
|
+
const emitUnionOrIntersection_1 = require("./emitUnionOrIntersection");
|
|
8
|
+
const emitArrayOrTuple_1 = require("./emitArrayOrTuple");
|
|
9
|
+
const emitObject_1 = require("./emitObject");
|
|
10
|
+
/**
|
|
11
|
+
* ✅ emitGuardFromType
|
|
12
|
+
* Converts a TypeScript type to a JavaScript runtime validation function string.
|
|
13
|
+
*/
|
|
14
|
+
function emitGuardFromType(checker, type) {
|
|
15
|
+
const ctx = new GenContext(checker);
|
|
16
|
+
const condition = ctx.emit("input", type);
|
|
17
|
+
return `(input)=>${condition}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* ✅ GenContext
|
|
21
|
+
* Internal helper for converting TypeScript types to JS validation expressions.
|
|
22
|
+
*/
|
|
23
|
+
class GenContext {
|
|
24
|
+
checker;
|
|
25
|
+
seen = new Map();
|
|
26
|
+
constructor(checker) {
|
|
27
|
+
this.checker = checker;
|
|
28
|
+
}
|
|
29
|
+
/** Top-level router — delegates each type to the correct handler. */
|
|
30
|
+
emit(expr, t) {
|
|
31
|
+
if (this.seen.has(t))
|
|
32
|
+
return this.seen.get(t);
|
|
33
|
+
return ((0, emitPrimitive_1.emitPrimitive)(this, expr, t) ??
|
|
34
|
+
(0, emitLiteralOrEnum_1.emitLiteralOrEnum)(this, expr, t) ??
|
|
35
|
+
(0, emitUnionOrIntersection_1.emitUnionOrIntersection)(this, expr, t) ??
|
|
36
|
+
(0, emitArrayOrTuple_1.emitArrayOrTuple)(this, expr, t) ??
|
|
37
|
+
(0, emitObject_1.emitObject)(this, expr, t) ??
|
|
38
|
+
"true");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.GenContext = GenContext;
|
|
@@ -0,0 +1,13 @@
|
|
|
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.tsTransformer = exports.vitePlugin = exports.makeAssert = exports.makeValidate = void 0;
|
|
7
|
+
var validate_js_1 = require("./runtime/validate.js");
|
|
8
|
+
Object.defineProperty(exports, "makeValidate", { enumerable: true, get: function () { return validate_js_1.makeValidate; } });
|
|
9
|
+
Object.defineProperty(exports, "makeAssert", { enumerable: true, get: function () { return validate_js_1.makeAssert; } });
|
|
10
|
+
var vite_plugin_js_1 = require("./transformer/vite-plugin.js");
|
|
11
|
+
Object.defineProperty(exports, "vitePlugin", { enumerable: true, get: function () { return __importDefault(vite_plugin_js_1).default; } });
|
|
12
|
+
var ts_transformer_js_1 = require("./transformer/ts-transformer.js");
|
|
13
|
+
Object.defineProperty(exports, "tsTransformer", { enumerable: true, get: function () { return __importDefault(ts_transformer_js_1).default; } });
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runtypex — runtime APIs
|
|
3
|
+
* 1) makeValidate<T>() : (v) => v is T
|
|
4
|
+
* 2) makeAssert<T>() : throws on invalid
|
|
5
|
+
*
|
|
6
|
+
* NOTE: These factories are replaced at build-time by the transformer.
|
|
7
|
+
*/
|
|
8
|
+
export type ValidateFn<T> = (value: unknown) => value is T;
|
|
9
|
+
export declare function makeValidate<T>(): ValidateFn<T>;
|
|
10
|
+
export declare function makeAssert<T>(): (value: unknown) => asserts value is T;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.makeValidate = makeValidate;
|
|
4
|
+
exports.makeAssert = makeAssert;
|
|
5
|
+
// Replaced by transformer to generated guard
|
|
6
|
+
function __validate(_value) {
|
|
7
|
+
throw new Error("[runtypex] makeValidate() was not transformed. Add the plugin/transformer.");
|
|
8
|
+
}
|
|
9
|
+
function makeValidate() {
|
|
10
|
+
return (value) => __validate(value);
|
|
11
|
+
}
|
|
12
|
+
function makeAssert() {
|
|
13
|
+
const validate = makeValidate();
|
|
14
|
+
return (value) => {
|
|
15
|
+
if (!validate(value))
|
|
16
|
+
throw new TypeError("[runtypex] Validation failed.");
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
/**
|
|
3
|
+
* 🧩 tsTransformer
|
|
4
|
+
* TypeScript custom transformer (BEFORE) factory.
|
|
5
|
+
*
|
|
6
|
+
* 📘 Usage (ts-loader / ttypescript):
|
|
7
|
+
* ```ts
|
|
8
|
+
* getCustomTransformers: (program) => ({
|
|
9
|
+
* before: [ tsTransformer({ program, removeInProd: true }) ]
|
|
10
|
+
* })
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* 🧠 Purpose:
|
|
14
|
+
* - Replace makeValidate<T>(), makeAssert<T>() calls
|
|
15
|
+
* with *pre-generated runtime validation code* derived from T.
|
|
16
|
+
*
|
|
17
|
+
* 💡 Effect:
|
|
18
|
+
* ✅ No reflection or runtime type parsing
|
|
19
|
+
* ✅ Validation logic embedded at build-time
|
|
20
|
+
* ✅ Optionally removed in production builds
|
|
21
|
+
*/
|
|
22
|
+
export default function tsTransformer(options: {
|
|
23
|
+
program: ts.Program;
|
|
24
|
+
removeInProd?: boolean;
|
|
25
|
+
}): (context: ts.TransformationContext) => (sf: ts.SourceFile) => ts.Node | undefined;
|
|
@@ -0,0 +1,103 @@
|
|
|
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.default = tsTransformer;
|
|
7
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
+
const core_1 = require("../core");
|
|
9
|
+
/**
|
|
10
|
+
* 🧩 tsTransformer
|
|
11
|
+
* TypeScript custom transformer (BEFORE) factory.
|
|
12
|
+
*
|
|
13
|
+
* 📘 Usage (ts-loader / ttypescript):
|
|
14
|
+
* ```ts
|
|
15
|
+
* getCustomTransformers: (program) => ({
|
|
16
|
+
* before: [ tsTransformer({ program, removeInProd: true }) ]
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* 🧠 Purpose:
|
|
21
|
+
* - Replace makeValidate<T>(), makeAssert<T>() calls
|
|
22
|
+
* with *pre-generated runtime validation code* derived from T.
|
|
23
|
+
*
|
|
24
|
+
* 💡 Effect:
|
|
25
|
+
* ✅ No reflection or runtime type parsing
|
|
26
|
+
* ✅ Validation logic embedded at build-time
|
|
27
|
+
* ✅ Optionally removed in production builds
|
|
28
|
+
*/
|
|
29
|
+
function tsTransformer(options) {
|
|
30
|
+
const { program } = options;
|
|
31
|
+
const checker = options.program.getTypeChecker();
|
|
32
|
+
const removeInProd = !!options.removeInProd;
|
|
33
|
+
const prod = process.env.NODE_ENV === "production";
|
|
34
|
+
return (context) => {
|
|
35
|
+
const visit = (node) => {
|
|
36
|
+
if (typescript_1.default.isCallExpression(node) && typescript_1.default.isIdentifier(node.expression)) {
|
|
37
|
+
const name = node.expression.text;
|
|
38
|
+
const targetFunctions = ["makeValidate", "makeAssert"];
|
|
39
|
+
if (targetFunctions.includes(name) && node.typeArguments?.length) {
|
|
40
|
+
const typeNode = node.typeArguments[0];
|
|
41
|
+
const typeName = typeNode.getText();
|
|
42
|
+
const type = _resolveTypeByName(program, node.getSourceFile(), checker, typeName);
|
|
43
|
+
if (!type)
|
|
44
|
+
return node;
|
|
45
|
+
const isRemovedInProd = removeInProd && prod;
|
|
46
|
+
switch (name) {
|
|
47
|
+
case "makeValidate":
|
|
48
|
+
return _emitMakeValidate(checker, type, isRemovedInProd);
|
|
49
|
+
case "makeAssert":
|
|
50
|
+
return _emitMakeAssert(checker, type, isRemovedInProd);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return typescript_1.default.visitEachChild(node, visit, context);
|
|
55
|
+
};
|
|
56
|
+
return (sf) => typescript_1.default.visitNode(sf, visit);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function _resolveTypeByName(program, sf, checker, name) {
|
|
60
|
+
for (const file of program.getSourceFiles()) {
|
|
61
|
+
const decl = _findLocalDeclaration(file, name);
|
|
62
|
+
if (!decl)
|
|
63
|
+
continue;
|
|
64
|
+
if (typescript_1.default.isInterfaceDeclaration(decl) || typescript_1.default.isClassDeclaration(decl) || typescript_1.default.isEnumDeclaration(decl)) {
|
|
65
|
+
// @ts-ignore
|
|
66
|
+
const sym = checker.getSymbolAtLocation(decl.name);
|
|
67
|
+
if (sym)
|
|
68
|
+
return checker.getDeclaredTypeOfSymbol(sym);
|
|
69
|
+
}
|
|
70
|
+
if (typescript_1.default.isTypeAliasDeclaration(decl)) {
|
|
71
|
+
return checker.getTypeFromTypeNode(decl.type);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const sym = checker
|
|
75
|
+
.getSymbolsInScope(sf, typescript_1.default.SymbolFlags.Type | typescript_1.default.SymbolFlags.Alias | typescript_1.default.SymbolFlags.Interface)
|
|
76
|
+
.find((s) => s.name === name);
|
|
77
|
+
return sym ? checker.getDeclaredTypeOfSymbol(sym) : null;
|
|
78
|
+
}
|
|
79
|
+
function _findLocalDeclaration(sf, name) {
|
|
80
|
+
let found;
|
|
81
|
+
(function walk(node) {
|
|
82
|
+
if ((typescript_1.default.isInterfaceDeclaration(node) ||
|
|
83
|
+
typescript_1.default.isTypeAliasDeclaration(node) ||
|
|
84
|
+
typescript_1.default.isEnumDeclaration(node) ||
|
|
85
|
+
typescript_1.default.isClassDeclaration(node)) &&
|
|
86
|
+
node.name?.text === name) {
|
|
87
|
+
found = node;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!found)
|
|
91
|
+
node.forEachChild(walk);
|
|
92
|
+
})(sf);
|
|
93
|
+
return found;
|
|
94
|
+
}
|
|
95
|
+
function _emitMakeValidate(checker, type, isRemovedInProd) {
|
|
96
|
+
const guard = isRemovedInProd ? "((_)=>true)" : (0, core_1.emitGuardFromType)(checker, type);
|
|
97
|
+
return typescript_1.default.factory.createIdentifier(guard);
|
|
98
|
+
}
|
|
99
|
+
function _emitMakeAssert(checker, type, isRemovedInProd) {
|
|
100
|
+
const guard = isRemovedInProd ? "((_)=>{})" : (0, core_1.emitGuardFromType)(checker, type);
|
|
101
|
+
const txt = `(function(){const G=${guard};return(i)=>{if(!G(i))throw new TypeError("[runtypex] Validation failed.");};})()`;
|
|
102
|
+
return typescript_1.default.factory.createIdentifier(txt);
|
|
103
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
/**
|
|
3
|
+
* 🧩 vitePluginRuntypex
|
|
4
|
+
* A Vite plugin that performs build-time type → runtime validation transformation.
|
|
5
|
+
*
|
|
6
|
+
* 📘 Purpose
|
|
7
|
+
* - Replace calls like:
|
|
8
|
+
* makeValidate<T>(), makeAssert<T>()
|
|
9
|
+
* with *inline JavaScript guard functions* derived from TypeScript types.
|
|
10
|
+
*
|
|
11
|
+
* 💡 Features
|
|
12
|
+
* - Works in both dev & build mode
|
|
13
|
+
* - Optional: remove validation code in production (`removeInProd`)
|
|
14
|
+
* - Compatible with Rollup / Webpack (via Vite plugin API)
|
|
15
|
+
*/
|
|
16
|
+
export default function vitePluginRuntypex(options?: {
|
|
17
|
+
removeInProd?: boolean;
|
|
18
|
+
}): Plugin;
|
|
@@ -0,0 +1,137 @@
|
|
|
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.default = vitePluginRuntypex;
|
|
7
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const core_1 = require("../core");
|
|
10
|
+
/**
|
|
11
|
+
* 🧩 vitePluginRuntypex
|
|
12
|
+
* A Vite plugin that performs build-time type → runtime validation transformation.
|
|
13
|
+
*
|
|
14
|
+
* 📘 Purpose
|
|
15
|
+
* - Replace calls like:
|
|
16
|
+
* makeValidate<T>(), makeAssert<T>()
|
|
17
|
+
* with *inline JavaScript guard functions* derived from TypeScript types.
|
|
18
|
+
*
|
|
19
|
+
* 💡 Features
|
|
20
|
+
* - Works in both dev & build mode
|
|
21
|
+
* - Optional: remove validation code in production (`removeInProd`)
|
|
22
|
+
* - Compatible with Rollup / Webpack (via Vite plugin API)
|
|
23
|
+
*/
|
|
24
|
+
function vitePluginRuntypex(options) {
|
|
25
|
+
const removeInProd = !!options?.removeInProd;
|
|
26
|
+
const prod = process.env.NODE_ENV === "production";
|
|
27
|
+
return {
|
|
28
|
+
name: "vite-plugin-runtypex",
|
|
29
|
+
enforce: "pre",
|
|
30
|
+
transform(code, id) {
|
|
31
|
+
const isTS = id.endsWith(".ts") || id.endsWith(".tsx");
|
|
32
|
+
const isTargetFunction = /make(?:Validate|Assert)</.test(code);
|
|
33
|
+
if (!isTS || !isTargetFunction)
|
|
34
|
+
return;
|
|
35
|
+
const { program, checker } = _createProgramFor(id);
|
|
36
|
+
const sf = program.getSourceFile(id);
|
|
37
|
+
if (!sf)
|
|
38
|
+
return;
|
|
39
|
+
let mutated = code;
|
|
40
|
+
// ② makeAssert<T>()
|
|
41
|
+
mutated = mutated.replace(/makeAssert<\s*([^>]+)\s*>\s*\(\s*\)/g, (_m, typeName) => _emitMakeAssert({ program, checker, sf, typeName, prod, removeInProd }) ?? _m);
|
|
42
|
+
// ③ makeValidate<T>()
|
|
43
|
+
mutated = mutated.replace(/makeValidate<\s*([^>]+)\s*>\s*\(\s*\)/g, (_m, typeName) => _emitMakeValidate({ program, checker, sf, typeName, prod, removeInProd }) ?? _m);
|
|
44
|
+
return mutated === code ? null : { code: mutated, map: null };
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// ──────────────────────────────────────────────
|
|
49
|
+
// ① createProgram & TypeChecker
|
|
50
|
+
// ──────────────────────────────────────────────
|
|
51
|
+
function _createProgramFor(file) {
|
|
52
|
+
const tsconfig = _findNearestTsconfig(node_path_1.default.dirname(file));
|
|
53
|
+
const cfg = typescript_1.default.readConfigFile(tsconfig, typescript_1.default.sys.readFile);
|
|
54
|
+
if (cfg.error) {
|
|
55
|
+
throw new Error(typescript_1.default.flattenDiagnosticMessageText(cfg.error.messageText, "\n"));
|
|
56
|
+
}
|
|
57
|
+
const parsed = typescript_1.default.parseJsonConfigFileContent(cfg.config, typescript_1.default.sys, node_path_1.default.dirname(tsconfig));
|
|
58
|
+
const program = typescript_1.default.createProgram({ rootNames: parsed.fileNames, options: parsed.options });
|
|
59
|
+
const checker = program.getTypeChecker();
|
|
60
|
+
return { program, checker };
|
|
61
|
+
}
|
|
62
|
+
function _findNearestTsconfig(start) {
|
|
63
|
+
let dir = start;
|
|
64
|
+
while (true) {
|
|
65
|
+
const candidate = node_path_1.default.join(dir, "tsconfig.json");
|
|
66
|
+
if (typescript_1.default.sys.fileExists(candidate))
|
|
67
|
+
return candidate;
|
|
68
|
+
const parent = node_path_1.default.dirname(dir);
|
|
69
|
+
if (parent === dir)
|
|
70
|
+
break;
|
|
71
|
+
dir = parent;
|
|
72
|
+
}
|
|
73
|
+
const fallback = node_path_1.default.join(process.cwd(), "tsconfig.json");
|
|
74
|
+
if (typescript_1.default.sys.fileExists(fallback))
|
|
75
|
+
return fallback;
|
|
76
|
+
throw new Error("tsconfig.json not found");
|
|
77
|
+
}
|
|
78
|
+
function _emitMakeValidate({ program, checker, sf, typeName, prod, removeInProd, }) {
|
|
79
|
+
if (removeInProd && prod)
|
|
80
|
+
return `((_)=>true)`;
|
|
81
|
+
const type = _resolveTypeByName(program, sf, checker, typeName.trim());
|
|
82
|
+
if (!type)
|
|
83
|
+
return null;
|
|
84
|
+
return (0, core_1.emitGuardFromType)(checker, type);
|
|
85
|
+
}
|
|
86
|
+
function _emitMakeAssert({ program, checker, sf, typeName, prod, removeInProd, }) {
|
|
87
|
+
if (removeInProd && prod)
|
|
88
|
+
return `((_)=>{})`;
|
|
89
|
+
const type = _resolveTypeByName(program, sf, checker, typeName.trim());
|
|
90
|
+
if (!type)
|
|
91
|
+
return null;
|
|
92
|
+
const guard = (0, core_1.emitGuardFromType)(checker, type);
|
|
93
|
+
return `(function(){const G=${guard};return(i)=>{if(!G(i))throw new TypeError("[runtypex] Validation failed.");};})()`;
|
|
94
|
+
}
|
|
95
|
+
// ──────────────────────────────────────────────
|
|
96
|
+
// ③ Type Resolution (support interface/type/enum)
|
|
97
|
+
// ──────────────────────────────────────────────
|
|
98
|
+
function _resolveTypeByName(program, sf, checker, name) {
|
|
99
|
+
// scan all source files for local declaration
|
|
100
|
+
for (const file of program.getSourceFiles()) {
|
|
101
|
+
const decl = _findLocalDeclaration(file, name);
|
|
102
|
+
if (!decl)
|
|
103
|
+
continue;
|
|
104
|
+
// interface / class / enum
|
|
105
|
+
if (typescript_1.default.isInterfaceDeclaration(decl) || typescript_1.default.isClassDeclaration(decl) || typescript_1.default.isEnumDeclaration(decl)) {
|
|
106
|
+
// @ts-ignore
|
|
107
|
+
const symbol = checker.getSymbolAtLocation(decl.name);
|
|
108
|
+
if (symbol)
|
|
109
|
+
return checker.getDeclaredTypeOfSymbol(symbol);
|
|
110
|
+
}
|
|
111
|
+
// type alias
|
|
112
|
+
if (typescript_1.default.isTypeAliasDeclaration(decl)) {
|
|
113
|
+
return checker.getTypeFromTypeNode(decl.type);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Scope-based fallback
|
|
117
|
+
const symbol = checker
|
|
118
|
+
.getSymbolsInScope(sf, typescript_1.default.SymbolFlags.Type | typescript_1.default.SymbolFlags.Alias | typescript_1.default.SymbolFlags.Interface)
|
|
119
|
+
.find((s) => s.name === name);
|
|
120
|
+
return symbol ? checker.getDeclaredTypeOfSymbol(symbol) : null;
|
|
121
|
+
}
|
|
122
|
+
function _findLocalDeclaration(sf, name) {
|
|
123
|
+
let found;
|
|
124
|
+
(function walk(node) {
|
|
125
|
+
if ((typescript_1.default.isInterfaceDeclaration(node) ||
|
|
126
|
+
typescript_1.default.isTypeAliasDeclaration(node) ||
|
|
127
|
+
typescript_1.default.isEnumDeclaration(node) ||
|
|
128
|
+
typescript_1.default.isClassDeclaration(node)) &&
|
|
129
|
+
node.name?.text === name) {
|
|
130
|
+
found = node;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (!found)
|
|
134
|
+
node.forEachChild(walk);
|
|
135
|
+
})(sf);
|
|
136
|
+
return found;
|
|
137
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handles array (T[]) and tuple ([A, B]) types.
|
|
3
|
+
*/
|
|
4
|
+
export function emitArrayOrTuple(ctx, expr, t) {
|
|
5
|
+
if (ctx.checker.isTupleType(t)) {
|
|
6
|
+
return _emitTuple(t, expr, ctx);
|
|
7
|
+
}
|
|
8
|
+
if (ctx.checker.isArrayType(t)) {
|
|
9
|
+
return _emitArray(ctx, expr, t);
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Generate validation for Array<T>
|
|
15
|
+
*/
|
|
16
|
+
function _emitArray(ctx, expr, t) {
|
|
17
|
+
const arrayCheck = `Array.isArray(${expr})`;
|
|
18
|
+
// Try extracting element type
|
|
19
|
+
const element = ctx.checker.getElementTypeOfArrayType?.(t) ||
|
|
20
|
+
t.typeArguments?.[0] ||
|
|
21
|
+
t.getNumberIndexType?.();
|
|
22
|
+
if (!element)
|
|
23
|
+
return arrayCheck;
|
|
24
|
+
const eachCheck = `${expr}.every(e=>${ctx.emit("e", element)})`;
|
|
25
|
+
return `(${arrayCheck}&&${eachCheck})`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Generate validation for Tuple [A, B, ...]
|
|
29
|
+
*/
|
|
30
|
+
function _emitTuple(ref, expr, ctx) {
|
|
31
|
+
const elements = ref.typeArguments ?? ctx.checker.getTypeArguments?.(ref) ?? [];
|
|
32
|
+
const arrayCheck = `Array.isArray(${expr})`;
|
|
33
|
+
const lenCheck = `${expr}.length===${elements.length}`;
|
|
34
|
+
const elementChecks = elements.map((el, i) => ctx.emit(`${expr}[${i}]`, el));
|
|
35
|
+
const parts = [arrayCheck, lenCheck, ...elementChecks];
|
|
36
|
+
return `(${parts.join("&&")})`;
|
|
37
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
/** Handles literal types and enum-like types. */
|
|
3
|
+
export function emitLiteralOrEnum(_, expr, t) {
|
|
4
|
+
if (t.isLiteral()) {
|
|
5
|
+
const value = t.value;
|
|
6
|
+
const isString = typeof value === "string";
|
|
7
|
+
const newValue = isString ? JSON.stringify(value) : String(value);
|
|
8
|
+
return `${expr}===${newValue}`;
|
|
9
|
+
}
|
|
10
|
+
const isEnum = t.flags & ts.TypeFlags.EnumLike;
|
|
11
|
+
if (isEnum) {
|
|
12
|
+
const enumValues = _extractEnumValues(t);
|
|
13
|
+
if (enumValues.length) {
|
|
14
|
+
return `(${enumValues.map(v => `${expr}===${v}`).join("||")})`;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
// Extracts numeric or string values from an Enum declaration.
|
|
20
|
+
function _extractEnumValues(t) {
|
|
21
|
+
const symbol = t.getSymbol();
|
|
22
|
+
if (!symbol)
|
|
23
|
+
return [];
|
|
24
|
+
const values = [];
|
|
25
|
+
const declarations = symbol.getDeclarations() ?? [];
|
|
26
|
+
for (const declaration of declarations) {
|
|
27
|
+
const isEnum = ts.isEnumDeclaration(declaration);
|
|
28
|
+
if (!isEnum)
|
|
29
|
+
continue;
|
|
30
|
+
for (const member of declaration.members) {
|
|
31
|
+
const init = member.initializer;
|
|
32
|
+
if (!init)
|
|
33
|
+
continue;
|
|
34
|
+
if (ts.isStringLiteral(init) || ts.isNumericLiteral(init)) {
|
|
35
|
+
const value = ts.isStringLiteral(init) ? JSON.stringify(init.text) : init.text;
|
|
36
|
+
values.push(value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return values;
|
|
41
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
/**
|
|
3
|
+
* Handles interfaces, classes, and object-like structures.
|
|
4
|
+
*/
|
|
5
|
+
export function emitObject(ctx, expr, t) {
|
|
6
|
+
const isObject = (t.getFlags() & ts.TypeFlags.Object) !== 0;
|
|
7
|
+
if (!isObject)
|
|
8
|
+
return null;
|
|
9
|
+
const props = ctx.checker.getPropertiesOfType(t);
|
|
10
|
+
const parts = [`typeof ${expr}==="object"`, `${expr}!==null`];
|
|
11
|
+
for (const prop of props) {
|
|
12
|
+
const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
|
|
13
|
+
if (!declaration)
|
|
14
|
+
continue;
|
|
15
|
+
const propType = ctx.checker.getTypeOfSymbolAtLocation(prop, declaration);
|
|
16
|
+
const isOptional = (prop.getFlags() & ts.SymbolFlags.Optional) !== 0;
|
|
17
|
+
const condition = ctx.emit(`${expr}.${prop.name}`, propType);
|
|
18
|
+
const checkExpr = isOptional ? `(${expr}.${prop.name}===undefined||${condition})` : condition;
|
|
19
|
+
parts.push(checkExpr);
|
|
20
|
+
}
|
|
21
|
+
return `(${parts.join("&&")})`;
|
|
22
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
/**
|
|
3
|
+
* Handles primitive types like number, string, boolean...
|
|
4
|
+
*/
|
|
5
|
+
export function emitPrimitive(ctx, expr, t) {
|
|
6
|
+
if ((t.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0)
|
|
7
|
+
return "true";
|
|
8
|
+
if (t.flags & ts.TypeFlags.Null)
|
|
9
|
+
return `${expr}===null`;
|
|
10
|
+
if (t.flags & ts.TypeFlags.Undefined)
|
|
11
|
+
return `${expr}===undefined`;
|
|
12
|
+
if (t.flags & ts.TypeFlags.BooleanLike)
|
|
13
|
+
return `typeof ${expr}==="boolean"`;
|
|
14
|
+
if (t.flags & ts.TypeFlags.NumberLike)
|
|
15
|
+
return `typeof ${expr}==="number"`;
|
|
16
|
+
if (t.flags & ts.TypeFlags.StringLike)
|
|
17
|
+
return `typeof ${expr}==="string"`;
|
|
18
|
+
if (t.flags & ts.TypeFlags.BigIntLike)
|
|
19
|
+
return `typeof ${expr}==="bigint"`;
|
|
20
|
+
if (t.flags & ts.TypeFlags.ESSymbolLike)
|
|
21
|
+
return `typeof ${expr}==="symbol"`;
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handles union (A | B) and intersection (A & B) types.
|
|
3
|
+
*/
|
|
4
|
+
export function emitUnionOrIntersection(ctx, expr, t) {
|
|
5
|
+
if (t.isUnion()) {
|
|
6
|
+
return `(${t.types.map(tt => ctx.emit(expr, tt)).join("||")})`;
|
|
7
|
+
}
|
|
8
|
+
if (t.isIntersection()) {
|
|
9
|
+
return `(${t.types.map(tt => ctx.emit(expr, tt)).join("&&")})`;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { emitPrimitive } from "./emitPrimitive";
|
|
2
|
+
import { emitLiteralOrEnum } from "./emitLiteralOrEnum";
|
|
3
|
+
import { emitUnionOrIntersection } from "./emitUnionOrIntersection";
|
|
4
|
+
import { emitArrayOrTuple } from "./emitArrayOrTuple";
|
|
5
|
+
import { emitObject } from "./emitObject";
|
|
6
|
+
/**
|
|
7
|
+
* ✅ emitGuardFromType
|
|
8
|
+
* Converts a TypeScript type to a JavaScript runtime validation function string.
|
|
9
|
+
*/
|
|
10
|
+
export function emitGuardFromType(checker, type) {
|
|
11
|
+
const ctx = new GenContext(checker);
|
|
12
|
+
const condition = ctx.emit("input", type);
|
|
13
|
+
return `(input)=>${condition}`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* ✅ GenContext
|
|
17
|
+
* Internal helper for converting TypeScript types to JS validation expressions.
|
|
18
|
+
*/
|
|
19
|
+
export class GenContext {
|
|
20
|
+
checker;
|
|
21
|
+
seen = new Map();
|
|
22
|
+
constructor(checker) {
|
|
23
|
+
this.checker = checker;
|
|
24
|
+
}
|
|
25
|
+
/** Top-level router — delegates each type to the correct handler. */
|
|
26
|
+
emit(expr, t) {
|
|
27
|
+
if (this.seen.has(t))
|
|
28
|
+
return this.seen.get(t);
|
|
29
|
+
return (emitPrimitive(this, expr, t) ??
|
|
30
|
+
emitLiteralOrEnum(this, expr, t) ??
|
|
31
|
+
emitUnionOrIntersection(this, expr, t) ??
|
|
32
|
+
emitArrayOrTuple(this, expr, t) ??
|
|
33
|
+
emitObject(this, expr, t) ??
|
|
34
|
+
"true");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runtypex — runtime APIs
|
|
3
|
+
* 1) makeValidate<T>() : (v) => v is T
|
|
4
|
+
* 2) makeAssert<T>() : throws on invalid
|
|
5
|
+
*
|
|
6
|
+
* NOTE: These factories are replaced at build-time by the transformer.
|
|
7
|
+
*/
|
|
8
|
+
export type ValidateFn<T> = (value: unknown) => value is T;
|
|
9
|
+
export declare function makeValidate<T>(): ValidateFn<T>;
|
|
10
|
+
export declare function makeAssert<T>(): (value: unknown) => asserts value is T;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Replaced by transformer to generated guard
|
|
2
|
+
function __validate(_value) {
|
|
3
|
+
throw new Error("[runtypex] makeValidate() was not transformed. Add the plugin/transformer.");
|
|
4
|
+
}
|
|
5
|
+
export function makeValidate() {
|
|
6
|
+
return (value) => __validate(value);
|
|
7
|
+
}
|
|
8
|
+
export function makeAssert() {
|
|
9
|
+
const validate = makeValidate();
|
|
10
|
+
return (value) => {
|
|
11
|
+
if (!validate(value))
|
|
12
|
+
throw new TypeError("[runtypex] Validation failed.");
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
/**
|
|
3
|
+
* 🧩 tsTransformer
|
|
4
|
+
* TypeScript custom transformer (BEFORE) factory.
|
|
5
|
+
*
|
|
6
|
+
* 📘 Usage (ts-loader / ttypescript):
|
|
7
|
+
* ```ts
|
|
8
|
+
* getCustomTransformers: (program) => ({
|
|
9
|
+
* before: [ tsTransformer({ program, removeInProd: true }) ]
|
|
10
|
+
* })
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* 🧠 Purpose:
|
|
14
|
+
* - Replace makeValidate<T>(), makeAssert<T>() calls
|
|
15
|
+
* with *pre-generated runtime validation code* derived from T.
|
|
16
|
+
*
|
|
17
|
+
* 💡 Effect:
|
|
18
|
+
* ✅ No reflection or runtime type parsing
|
|
19
|
+
* ✅ Validation logic embedded at build-time
|
|
20
|
+
* ✅ Optionally removed in production builds
|
|
21
|
+
*/
|
|
22
|
+
export default function tsTransformer(options: {
|
|
23
|
+
program: ts.Program;
|
|
24
|
+
removeInProd?: boolean;
|
|
25
|
+
}): (context: ts.TransformationContext) => (sf: ts.SourceFile) => ts.Node | undefined;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { emitGuardFromType } from "../core";
|
|
3
|
+
/**
|
|
4
|
+
* 🧩 tsTransformer
|
|
5
|
+
* TypeScript custom transformer (BEFORE) factory.
|
|
6
|
+
*
|
|
7
|
+
* 📘 Usage (ts-loader / ttypescript):
|
|
8
|
+
* ```ts
|
|
9
|
+
* getCustomTransformers: (program) => ({
|
|
10
|
+
* before: [ tsTransformer({ program, removeInProd: true }) ]
|
|
11
|
+
* })
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* 🧠 Purpose:
|
|
15
|
+
* - Replace makeValidate<T>(), makeAssert<T>() calls
|
|
16
|
+
* with *pre-generated runtime validation code* derived from T.
|
|
17
|
+
*
|
|
18
|
+
* 💡 Effect:
|
|
19
|
+
* ✅ No reflection or runtime type parsing
|
|
20
|
+
* ✅ Validation logic embedded at build-time
|
|
21
|
+
* ✅ Optionally removed in production builds
|
|
22
|
+
*/
|
|
23
|
+
export default function tsTransformer(options) {
|
|
24
|
+
const { program } = options;
|
|
25
|
+
const checker = options.program.getTypeChecker();
|
|
26
|
+
const removeInProd = !!options.removeInProd;
|
|
27
|
+
const prod = process.env.NODE_ENV === "production";
|
|
28
|
+
return (context) => {
|
|
29
|
+
const visit = (node) => {
|
|
30
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
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;
|
|
39
|
+
const isRemovedInProd = removeInProd && prod;
|
|
40
|
+
switch (name) {
|
|
41
|
+
case "makeValidate":
|
|
42
|
+
return _emitMakeValidate(checker, type, isRemovedInProd);
|
|
43
|
+
case "makeAssert":
|
|
44
|
+
return _emitMakeAssert(checker, type, isRemovedInProd);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return ts.visitEachChild(node, visit, context);
|
|
49
|
+
};
|
|
50
|
+
return (sf) => ts.visitNode(sf, visit);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function _resolveTypeByName(program, sf, checker, name) {
|
|
54
|
+
for (const file of program.getSourceFiles()) {
|
|
55
|
+
const decl = _findLocalDeclaration(file, name);
|
|
56
|
+
if (!decl)
|
|
57
|
+
continue;
|
|
58
|
+
if (ts.isInterfaceDeclaration(decl) || ts.isClassDeclaration(decl) || ts.isEnumDeclaration(decl)) {
|
|
59
|
+
// @ts-ignore
|
|
60
|
+
const sym = checker.getSymbolAtLocation(decl.name);
|
|
61
|
+
if (sym)
|
|
62
|
+
return checker.getDeclaredTypeOfSymbol(sym);
|
|
63
|
+
}
|
|
64
|
+
if (ts.isTypeAliasDeclaration(decl)) {
|
|
65
|
+
return checker.getTypeFromTypeNode(decl.type);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const sym = checker
|
|
69
|
+
.getSymbolsInScope(sf, ts.SymbolFlags.Type | ts.SymbolFlags.Alias | ts.SymbolFlags.Interface)
|
|
70
|
+
.find((s) => s.name === name);
|
|
71
|
+
return sym ? checker.getDeclaredTypeOfSymbol(sym) : null;
|
|
72
|
+
}
|
|
73
|
+
function _findLocalDeclaration(sf, name) {
|
|
74
|
+
let found;
|
|
75
|
+
(function walk(node) {
|
|
76
|
+
if ((ts.isInterfaceDeclaration(node) ||
|
|
77
|
+
ts.isTypeAliasDeclaration(node) ||
|
|
78
|
+
ts.isEnumDeclaration(node) ||
|
|
79
|
+
ts.isClassDeclaration(node)) &&
|
|
80
|
+
node.name?.text === name) {
|
|
81
|
+
found = node;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!found)
|
|
85
|
+
node.forEachChild(walk);
|
|
86
|
+
})(sf);
|
|
87
|
+
return found;
|
|
88
|
+
}
|
|
89
|
+
function _emitMakeValidate(checker, type, isRemovedInProd) {
|
|
90
|
+
const guard = isRemovedInProd ? "((_)=>true)" : emitGuardFromType(checker, type);
|
|
91
|
+
return ts.factory.createIdentifier(guard);
|
|
92
|
+
}
|
|
93
|
+
function _emitMakeAssert(checker, type, isRemovedInProd) {
|
|
94
|
+
const guard = isRemovedInProd ? "((_)=>{})" : emitGuardFromType(checker, type);
|
|
95
|
+
const txt = `(function(){const G=${guard};return(i)=>{if(!G(i))throw new TypeError("[runtypex] Validation failed.");};})()`;
|
|
96
|
+
return ts.factory.createIdentifier(txt);
|
|
97
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
/**
|
|
3
|
+
* 🧩 vitePluginRuntypex
|
|
4
|
+
* A Vite plugin that performs build-time type → runtime validation transformation.
|
|
5
|
+
*
|
|
6
|
+
* 📘 Purpose
|
|
7
|
+
* - Replace calls like:
|
|
8
|
+
* makeValidate<T>(), makeAssert<T>()
|
|
9
|
+
* with *inline JavaScript guard functions* derived from TypeScript types.
|
|
10
|
+
*
|
|
11
|
+
* 💡 Features
|
|
12
|
+
* - Works in both dev & build mode
|
|
13
|
+
* - Optional: remove validation code in production (`removeInProd`)
|
|
14
|
+
* - Compatible with Rollup / Webpack (via Vite plugin API)
|
|
15
|
+
*/
|
|
16
|
+
export default function vitePluginRuntypex(options?: {
|
|
17
|
+
removeInProd?: boolean;
|
|
18
|
+
}): Plugin;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { emitGuardFromType } from "../core";
|
|
4
|
+
/**
|
|
5
|
+
* 🧩 vitePluginRuntypex
|
|
6
|
+
* A Vite plugin that performs build-time type → runtime validation transformation.
|
|
7
|
+
*
|
|
8
|
+
* 📘 Purpose
|
|
9
|
+
* - Replace calls like:
|
|
10
|
+
* makeValidate<T>(), makeAssert<T>()
|
|
11
|
+
* with *inline JavaScript guard functions* derived from TypeScript types.
|
|
12
|
+
*
|
|
13
|
+
* 💡 Features
|
|
14
|
+
* - Works in both dev & build mode
|
|
15
|
+
* - Optional: remove validation code in production (`removeInProd`)
|
|
16
|
+
* - Compatible with Rollup / Webpack (via Vite plugin API)
|
|
17
|
+
*/
|
|
18
|
+
export default function vitePluginRuntypex(options) {
|
|
19
|
+
const removeInProd = !!options?.removeInProd;
|
|
20
|
+
const prod = process.env.NODE_ENV === "production";
|
|
21
|
+
return {
|
|
22
|
+
name: "vite-plugin-runtypex",
|
|
23
|
+
enforce: "pre",
|
|
24
|
+
transform(code, id) {
|
|
25
|
+
const isTS = id.endsWith(".ts") || id.endsWith(".tsx");
|
|
26
|
+
const isTargetFunction = /make(?:Validate|Assert)</.test(code);
|
|
27
|
+
if (!isTS || !isTargetFunction)
|
|
28
|
+
return;
|
|
29
|
+
const { program, checker } = _createProgramFor(id);
|
|
30
|
+
const sf = program.getSourceFile(id);
|
|
31
|
+
if (!sf)
|
|
32
|
+
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);
|
|
38
|
+
return mutated === code ? null : { code: mutated, map: null };
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// ──────────────────────────────────────────────
|
|
43
|
+
// ① createProgram & TypeChecker
|
|
44
|
+
// ──────────────────────────────────────────────
|
|
45
|
+
function _createProgramFor(file) {
|
|
46
|
+
const tsconfig = _findNearestTsconfig(path.dirname(file));
|
|
47
|
+
const cfg = ts.readConfigFile(tsconfig, ts.sys.readFile);
|
|
48
|
+
if (cfg.error) {
|
|
49
|
+
throw new Error(ts.flattenDiagnosticMessageText(cfg.error.messageText, "\n"));
|
|
50
|
+
}
|
|
51
|
+
const parsed = ts.parseJsonConfigFileContent(cfg.config, ts.sys, path.dirname(tsconfig));
|
|
52
|
+
const program = ts.createProgram({ rootNames: parsed.fileNames, options: parsed.options });
|
|
53
|
+
const checker = program.getTypeChecker();
|
|
54
|
+
return { program, checker };
|
|
55
|
+
}
|
|
56
|
+
function _findNearestTsconfig(start) {
|
|
57
|
+
let dir = start;
|
|
58
|
+
while (true) {
|
|
59
|
+
const candidate = path.join(dir, "tsconfig.json");
|
|
60
|
+
if (ts.sys.fileExists(candidate))
|
|
61
|
+
return candidate;
|
|
62
|
+
const parent = path.dirname(dir);
|
|
63
|
+
if (parent === dir)
|
|
64
|
+
break;
|
|
65
|
+
dir = parent;
|
|
66
|
+
}
|
|
67
|
+
const fallback = path.join(process.cwd(), "tsconfig.json");
|
|
68
|
+
if (ts.sys.fileExists(fallback))
|
|
69
|
+
return fallback;
|
|
70
|
+
throw new Error("tsconfig.json not found");
|
|
71
|
+
}
|
|
72
|
+
function _emitMakeValidate({ program, checker, sf, typeName, prod, removeInProd, }) {
|
|
73
|
+
if (removeInProd && prod)
|
|
74
|
+
return `((_)=>true)`;
|
|
75
|
+
const type = _resolveTypeByName(program, sf, checker, typeName.trim());
|
|
76
|
+
if (!type)
|
|
77
|
+
return null;
|
|
78
|
+
return emitGuardFromType(checker, type);
|
|
79
|
+
}
|
|
80
|
+
function _emitMakeAssert({ program, checker, sf, typeName, prod, removeInProd, }) {
|
|
81
|
+
if (removeInProd && prod)
|
|
82
|
+
return `((_)=>{})`;
|
|
83
|
+
const type = _resolveTypeByName(program, sf, checker, typeName.trim());
|
|
84
|
+
if (!type)
|
|
85
|
+
return null;
|
|
86
|
+
const guard = emitGuardFromType(checker, type);
|
|
87
|
+
return `(function(){const G=${guard};return(i)=>{if(!G(i))throw new TypeError("[runtypex] Validation failed.");};})()`;
|
|
88
|
+
}
|
|
89
|
+
// ──────────────────────────────────────────────
|
|
90
|
+
// ③ Type Resolution (support interface/type/enum)
|
|
91
|
+
// ──────────────────────────────────────────────
|
|
92
|
+
function _resolveTypeByName(program, sf, checker, name) {
|
|
93
|
+
// scan all source files for local declaration
|
|
94
|
+
for (const file of program.getSourceFiles()) {
|
|
95
|
+
const decl = _findLocalDeclaration(file, name);
|
|
96
|
+
if (!decl)
|
|
97
|
+
continue;
|
|
98
|
+
// interface / class / enum
|
|
99
|
+
if (ts.isInterfaceDeclaration(decl) || ts.isClassDeclaration(decl) || ts.isEnumDeclaration(decl)) {
|
|
100
|
+
// @ts-ignore
|
|
101
|
+
const symbol = checker.getSymbolAtLocation(decl.name);
|
|
102
|
+
if (symbol)
|
|
103
|
+
return checker.getDeclaredTypeOfSymbol(symbol);
|
|
104
|
+
}
|
|
105
|
+
// type alias
|
|
106
|
+
if (ts.isTypeAliasDeclaration(decl)) {
|
|
107
|
+
return checker.getTypeFromTypeNode(decl.type);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Scope-based fallback
|
|
111
|
+
const symbol = checker
|
|
112
|
+
.getSymbolsInScope(sf, ts.SymbolFlags.Type | ts.SymbolFlags.Alias | ts.SymbolFlags.Interface)
|
|
113
|
+
.find((s) => s.name === name);
|
|
114
|
+
return symbol ? checker.getDeclaredTypeOfSymbol(symbol) : null;
|
|
115
|
+
}
|
|
116
|
+
function _findLocalDeclaration(sf, name) {
|
|
117
|
+
let found;
|
|
118
|
+
(function walk(node) {
|
|
119
|
+
if ((ts.isInterfaceDeclaration(node) ||
|
|
120
|
+
ts.isTypeAliasDeclaration(node) ||
|
|
121
|
+
ts.isEnumDeclaration(node) ||
|
|
122
|
+
ts.isClassDeclaration(node)) &&
|
|
123
|
+
node.name?.text === name) {
|
|
124
|
+
found = node;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (!found)
|
|
128
|
+
node.forEachChild(walk);
|
|
129
|
+
})(sf);
|
|
130
|
+
return found;
|
|
131
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "runtypex",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Runtime type guards compiled from TypeScript types. Fast, zero-schema, transformer-based.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Lux",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"main": "./dist/cjs/index.js",
|
|
10
|
+
"module": "./dist/esm/index.js",
|
|
11
|
+
"types": "./dist/esm/index.d.ts",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://example.com/runtypex.git"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/esm/index.d.ts",
|
|
19
|
+
"import": "./dist/esm/index.js",
|
|
20
|
+
"require": "./dist/cjs/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./vite-plugin": {
|
|
23
|
+
"types": "./dist/esm/transformer/vite-plugin.d.ts",
|
|
24
|
+
"import": "./dist/esm/transformer/vite-plugin.js",
|
|
25
|
+
"require": "./dist/cjs/transformer/vite-plugin.js"
|
|
26
|
+
},
|
|
27
|
+
"./transformer": {
|
|
28
|
+
"types": "./dist/esm/transformer/ts-transformer.d.ts",
|
|
29
|
+
"import": "./dist/esm/transformer/ts-transformer.js",
|
|
30
|
+
"require": "./dist/cjs/transformer/ts-transformer.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"clean": "rm -rf dist",
|
|
38
|
+
"build": "tsc && tsc -p tsconfig.cjs.json",
|
|
39
|
+
"test": "jest --passWithNoTests"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"typescript": ">=5.3"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/jest": "^29.5.14",
|
|
46
|
+
"@types/node": "^22.8.0",
|
|
47
|
+
"jest": "^29.7.0",
|
|
48
|
+
"ts-jest": "^29.2.5",
|
|
49
|
+
"typescript": "^5.6.3"
|
|
50
|
+
}
|
|
51
|
+
}
|