runtypex 0.1.12 β 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -1,24 +1,43 @@
|
|
|
1
1
|
# π‘οΈ runtypex
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
TypeScript νμ
μΌλ‘λΆν° **λ°νμ νμ
κ°λ(runtime type guard)** λ₯Ό μλ μμ±ν©λλ€.
|
|
4
|
+
μ€ν€λ§λ, λ°μ½λ μ΄ν°λ νμ μμ΅λλ€.
|
|
5
|
+
**λΉλ μ νμ
μ 보λ₯Ό λΆμν΄ μ΅μ νλ κ²μ¦ ν¨μλ₯Ό μλ μμ±νλ©°**,
|
|
6
|
+
νμ
λ§μΌλ‘ β **λ°νμ κ²μ¦**μ μνν©λλ€.
|
|
5
7
|
|
|
8
|
+
> Generate **runtime type guards** automatically from your TypeScript types.
|
|
9
|
+
> No schemas. No decorators.
|
|
10
|
+
> **Analyzes types at build time to generate optimized validation functions**,
|
|
11
|
+
> enabling **blazing-fast runtime validation** powered purely by TypeScript.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
<br/><br/>
|
|
15
|
+
|
|
16
|
+
## βοΈ μ¬μ©λ² (Usage)
|
|
17
|
+
```bash
|
|
18
|
+
npm i runtypex
|
|
19
|
+
```
|
|
6
20
|
|
|
7
|
-
## Use
|
|
8
21
|
```ts
|
|
9
22
|
import { makeValidate, makeAssert } from "runtypex";
|
|
10
23
|
|
|
11
|
-
interface User {
|
|
24
|
+
interface User {
|
|
25
|
+
id: number;
|
|
26
|
+
name: string;
|
|
27
|
+
active: boolean;
|
|
28
|
+
}
|
|
12
29
|
|
|
13
30
|
const isUser = makeValidate<User>();
|
|
14
31
|
const assertUser: ReturnType<typeof makeAssert<User>> = makeAssert<User>();
|
|
15
32
|
|
|
16
|
-
isUser({ id: 1, name: "Lux", active: true }); // true
|
|
17
|
-
assertUser({ id: "bad" }); // throws
|
|
18
|
-
toUser({ nope: true }); // β fallback
|
|
33
|
+
isUser({ id: 1, name: "Lux", active: true }); // β
true
|
|
34
|
+
assertUser({ id: "bad" }); // β throws
|
|
19
35
|
```
|
|
20
36
|
|
|
21
|
-
|
|
37
|
+
<br/><br/>
|
|
38
|
+
|
|
39
|
+
### Vite μμ (Vite Example)
|
|
40
|
+
|
|
22
41
|
```ts
|
|
23
42
|
// vite.config.ts
|
|
24
43
|
import { defineConfig } from "vite";
|
|
@@ -29,21 +48,24 @@ export default defineConfig({
|
|
|
29
48
|
});
|
|
30
49
|
```
|
|
31
50
|
|
|
32
|
-
|
|
33
|
-
```ts
|
|
34
|
-
// vite.config.ts
|
|
35
|
-
import { defineConfig } from "vite";
|
|
36
|
-
import { vitePlugin as runtypex } from "runtypex";
|
|
51
|
+
<br/>
|
|
37
52
|
|
|
53
|
+
νλ‘λμ
λΉλ μ λ°νμ κ²μ¦ μ½λλ₯Ό μ κ±°νλ €λ©΄ μ΅μ
`{ removeInProd: true }`λ₯Ό μ λ¬νμΈμ.
|
|
54
|
+
To remove validation code in production builds, pass `{ removeInProd: true }`.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
38
57
|
export default defineConfig({
|
|
39
58
|
plugins: [runtypex({ removeInProd: true })],
|
|
40
59
|
});
|
|
41
60
|
```
|
|
42
61
|
|
|
43
|
-
|
|
62
|
+
<br/>
|
|
63
|
+
|
|
64
|
+
### Webpack (ts-loader) μμ (Webpack Example)
|
|
65
|
+
|
|
44
66
|
```js
|
|
45
67
|
// webpack.config.js
|
|
46
|
-
const { tsTransformer } = require("runtypex
|
|
68
|
+
const { tsTransformer } = require("runtypex");
|
|
47
69
|
|
|
48
70
|
module.exports = {
|
|
49
71
|
module: {
|
|
@@ -62,8 +84,195 @@ module.exports = {
|
|
|
62
84
|
}
|
|
63
85
|
```
|
|
64
86
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
87
|
+
<br/><br/>
|
|
88
|
+
|
|
89
|
+
## β‘ runtypexμ νΉμ§ (Features)
|
|
90
|
+
|
|
91
|
+
| νλͺ© | μ€λͺ
| Description |
|
|
92
|
+
|------|------|-------------|
|
|
93
|
+
| β‘ **λΉ λ¦ (Fast)** | ASTλ‘ μ»΄νμΌλ κ²μ¦ μ½λ, λ°νμ μ€ν€λ§ νμ± μμ | Compiled guards, no runtime schema parsing |
|
|
94
|
+
| π§© **λ¨μν¨ (Simple)** | νμ
λ§ μ μ, μ€ν€λ§ μ€λ³΅ μ μΈ λΆνμ | Define once, no schema duplication |
|
|
95
|
+
| π§± **μ μ°ν¨ (Flexible)** | Vite, Webpack λͺ¨λ μ§μ | Works with both Vite and Webpack |
|
|
96
|
+
| π οΈ **API** | `makeValidate`, `makeAssert` μ 곡 | Clean runtime API |
|
|
97
|
+
|
|
98
|
+
<br/><br/>
|
|
99
|
+
|
|
100
|
+
## π§ͺ λ°λͺ¨ (Demo)
|
|
101
|
+
|
|
102
|
+
π [runtypex-demo (GitHub)](https://github.com/KumJungMin/runtypex-demo)
|
|
103
|
+
TypeScript νμ
μ΄ **λΉλ μ μλμΌλ‘ λ°νμ κ²μ¦ μ½λλ‘ λ³νλλ κ³Όμ **μ νμΈν μ μμ΅λλ€.
|
|
104
|
+
See how TypeScript types are transformed into real runtime guards at build time.
|
|
105
|
+
|
|
106
|
+
<br/><br/>
|
|
107
|
+
|
|
108
|
+
## π§ λ§λ€κ² λ μ΄μ (Background)
|
|
109
|
+
|
|
110
|
+
TypeScriptμ νμ
μμ€ν
λλΆμ μ½λ μμμλ μμ νμ§λ§,
|
|
111
|
+
κ²°μ μ μΈ νκ³λ₯Ό λ§μ£Όνμ΅λλ€.
|
|
112
|
+
|
|
113
|
+
> TypeScriptμ νμ
μ **λ°νμμ μ‘΄μ¬νμ§ μλλ€.**
|
|
114
|
+
> TypeScript types **do not exist at runtime.**
|
|
115
|
+
|
|
116
|
+
<br/>
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
μ¦, λΉλ νμμλ μμ νμ§λ§ μ€μ μ€ν νκ²½(JS)μμλ
|
|
120
|
+
λͺ¨λ νμ
μ λ³΄κ° μ¬λΌμ§λλ€.
|
|
121
|
+
κ²°κ΅ **APIλ μΈλΆ λͺ¨λμμ μλͺ»λ νμ
μ λ°μ΄ν°κ° λ€μ΄μλ κ²μ¦ν λ°©λ²μ΄ μμμ΅λλ€.**
|
|
122
|
+
|
|
123
|
+
> In short, TypeScript ensures type safety at build time,
|
|
124
|
+
> but once the code is compiled, all type information disappears.
|
|
125
|
+
> This means that even if invalid data comes from an API or an external source,
|
|
126
|
+
> thereβs no way to detect it at runtime.
|
|
127
|
+
|
|
128
|
+
<br/>
|
|
129
|
+
|
|
130
|
+
### π‘ κΈ°μ‘΄μ μλ (Existing Approach): Zod λ± μ€ν€λ§ κΈ°λ° κ²μ¦ <br/>(Previous Approach: Schema-Based Validators (e.g., Zod, Yup))
|
|
131
|
+
|
|
132
|
+
Zod, Yup κ°μ **μ€ν€λ§ κΈ°λ° κ²μ¦κΈ°**λ₯Ό λ¨Όμ μλνμ΅λλ€.
|
|
133
|
+
νμ§λ§ λ€μ μΈ κ°μ§ λ¬Έμ μ λΆλͺνμ΅λλ€:
|
|
134
|
+
|
|
135
|
+
> We first tried schema-based validators like **Zod** and **Yup**,
|
|
136
|
+
> but encountered three main problems:
|
|
137
|
+
|
|
138
|
+
| λ¬Έμ | μ€λͺ
| Problem | Description |
|
|
139
|
+
|------|------|----------|-------------|
|
|
140
|
+
| β‘ μ±λ₯ | λ§€λ² μ€ν€λ§λ₯Ό ν΄μνλ©° κ²μ¦ β λ°λ³΅ λΉμ© λ°μ | Performance | Every validation re-parses schema, causing overhead |
|
|
141
|
+
| β οΈ μμ μ± | νμ
μ μμ μ€ν€λ§ λΆμΌμΉ κ°λ₯ | Safety | Schema can desync from TypeScript type definitions |
|
|
142
|
+
| π§βπ» DX | νμ
μ΄ μ€λ³΅ μ μΈλ¨ (`interface` + `z.object`) | DX | Requires writing both `interface` and `z.object` |
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
### π§ μλ‘μ΄ μ κ·Ό (New Approach): ASTλ‘ κ²μ¦ μ½λ μμ±<br/>(A New Approach: Compile-Time Guard Generation via AST)
|
|
147
|
+
|
|
148
|
+
runtypexλ **TypeScript AST(Abstract Syntax Tree)** λ₯Ό λΆμν΄
|
|
149
|
+
**λΉλ μμ μ μλμΌλ‘ κ²μ¦ ν¨μλ₯Ό μμ±ν©λλ€.**
|
|
150
|
+
|
|
151
|
+
> runtypex analyzes the **TypeScript AST (Abstract Syntax Tree)**
|
|
152
|
+
> and automatically generates runtime validation functions **at build time**.
|
|
153
|
+
|
|
154
|
+
μ΄ λ°©μμ λ€μκ³Ό κ°μ μ₯μ μ κ°μ§λλ€ π
|
|
155
|
+
This approach provides several key advantages π
|
|
156
|
+
|
|
157
|
+
- νμ
μ μ ν λ²μΌλ‘ λ°νμ κ²μ¦ μλν
|
|
158
|
+
β **Single source of truth** for type + validation
|
|
159
|
+
- νμ
λΆμΌμΉ λ¬Έμ μ κ±°
|
|
160
|
+
β Eliminates schema desync between TS and runtime
|
|
161
|
+
- λ°νμ μ€λ²ν€λ μ΅μν
|
|
162
|
+
β No dynamic schema parsing during execution
|
|
163
|
+
|
|
164
|
+
<br/><br/>
|
|
165
|
+
|
|
166
|
+
## π κ°λ
μμ½ (Concept Overview)
|
|
167
|
+
|
|
168
|
+
| νλͺ© | λ΄μ© | Concept | Description |
|
|
169
|
+
|------|------|----------|-------------|
|
|
170
|
+
| **λ¬Έμ ** | TypeScript νμ
μ λ°νμμμ μ¬λΌμ§λ€ | Problem | TypeScript types vanish at runtime |
|
|
171
|
+
| **κ²°κ³Ό** | μΈλΆ λ°μ΄ν° λΆμΌμΉ μ μλ¬ λ°μ μ ν¨ | Result | Type mismatches arenβt caught at runtime |
|
|
172
|
+
| **ν΄κ²°** | ASTλ‘ νμ
λΆμ β κ²μ¦ μ½λ μλ μμ± | Solution | Parse TS AST β Generate guard functions |
|
|
173
|
+
|
|
174
|
+
> Simply put: runtypex bridges the gap between TypeScriptβs compile-time safety
|
|
175
|
+
> and JavaScriptβs runtime uncertainty β automatically.
|
|
176
|
+
|
|
177
|
+
<br/>
|
|
178
|
+
|
|
179
|
+
### π§© μμ (Example)
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
// Before: manual type guard
|
|
183
|
+
function isUser(value: unknown): value is User {
|
|
184
|
+
return (
|
|
185
|
+
typeof value === "object" &&
|
|
186
|
+
value !== null &&
|
|
187
|
+
typeof (value as any).id === "number" &&
|
|
188
|
+
typeof (value as any).name === "string"
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// After: runtypex auto-generated
|
|
193
|
+
const isUser = makeValidate<User>();
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
<br/>
|
|
197
|
+
|
|
198
|
+
λΉλ μ `makeValidate<User>()`λ
|
|
199
|
+
ASTλ₯Ό κΈ°λ°μΌλ‘ μλμ κ°μ ν¨μλ‘ **μλ λ³νλ©λλ€.**
|
|
200
|
+
|
|
201
|
+
> At build time, `makeValidate<User>()`
|
|
202
|
+
> is replaced with the following optimized validation code:
|
|
203
|
+
|
|
204
|
+
```js
|
|
205
|
+
const isUser = (v) =>
|
|
206
|
+
typeof v === "object" &&
|
|
207
|
+
v !== null &&
|
|
208
|
+
typeof v.id === "number" &&
|
|
209
|
+
typeof v.name === "string";
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
> β
Zero runtime parsing
|
|
213
|
+
> β
Fully type-synced
|
|
214
|
+
> β
Generated during build β not executed dynamically
|
|
215
|
+
|
|
216
|
+
<br/>
|
|
217
|
+
|
|
218
|
+
### π λ μ½μ΄λ³΄κΈ° (Further Reading)
|
|
219
|
+
|
|
220
|
+
π [TS Γ ν΄λ¦° μν€ν
μ² 2νΈ β νμ
μ€ν¬λ¦½νΈ νκ³μ Mapper, ASTλ‘ νμ
κ²μ¦νκΈ°](https://mong-blog.tistory.com/entry/TS-Γ-ν΄λ¦°-μν€ν
μ²-2νΈ-β-νμ
μ€ν¬λ¦½νΈ-νκ³μ-Mapper-ASTλ‘-νμ
-κ²μ¦νκΈ°)
|
|
221
|
+
> AST κΈ°λ° νμ
κ²μ¦ μλνμ μ리μ λΉλ νμ μ΅μ ν κ³Όμ μ μμΈν λ€λ£Ήλλ€.
|
|
222
|
+
> A deep dive into how AST-based type parsing enables build-time validation generation and optimization.
|
|
223
|
+
|
|
224
|
+
<br/>
|
|
225
|
+
|
|
226
|
+
### β
μμ½ (Summary)
|
|
227
|
+
|
|
228
|
+
| ν΅μ¬ ν¬μΈνΈ | English Summary |
|
|
229
|
+
|--------------|-----------------|
|
|
230
|
+
| TypeScript νμ
μ λ°νμμ μ‘΄μ¬νμ§ μλλ€ | TypeScript types vanish at runtime |
|
|
231
|
+
| μΈλΆ APIλ‘λΆν°μ λ°μ΄ν°λ μ λ’°ν μ μλ€ | External data canβt be trusted blindly |
|
|
232
|
+
| ASTλ‘ νμ
μ λΆμν΄ κ²μ¦ ν¨μλ₯Ό μμ±νλ€ | Parse TS AST β generate guards at build time |
|
|
233
|
+
| λ°νμ μ€λ²ν€λ μμ΄ νμ
μμ μ±μ ν보νλ€ | Provides runtime safety with zero runtime cost |
|
|
234
|
+
| DX, μμ μ±, μ±λ₯ λͺ¨λ κ°νλλ€ | Improves DX, safety, and performance |
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
<br/><br/>
|
|
238
|
+
|
|
239
|
+
## π v0.2.0 μ
λ°μ΄νΈ μμ (Upcoming in v0.2.0)
|
|
240
|
+
|
|
241
|
+
### π μλ‘μ΄ κΈ°λ₯ (New Features)
|
|
242
|
+
|
|
243
|
+
| νλͺ© | μ€λͺ
| Description |
|
|
244
|
+
|------|------|-------------|
|
|
245
|
+
| βοΈ **mode μ΅μ
μΆκ°** | λ°νμ λμ λͺ¨λ μ€μ (`"development"`, `"production"`, `"test"`) | Define runtime behavior mode (`"development"`, `"production"`, `"test"`) |
|
|
246
|
+
| π§Ή **strip μ΅μ
μΆκ°** | λΉλ μ λͺ¨λ `makeAssert`, `makeValidate` νΈμΆμ μ κ±°νμ¬ νμΌ ν¬κΈ° μ΅μν | Removes all runtime validators from build output for minimal bundle size |
|
|
247
|
+
| πͺΆ **logLevel μ΅μ
μΆκ°** | νλ¬κ·ΈμΈ λ‘κΉ
λ 벨 μ€μ (`"silent"`, `"info"`, `"debug"`) | Controls plugin log verbosity (`"silent"`, `"info"`, `"debug"`) |
|
|
248
|
+
| π·οΈ **`/* @runtypex:keep */` μ£Όμ μ§μ** | νΉμ κ²μ¦ ν¨μλ μ κ±°νμ§ μκ³ μ μ§ (`strip` νμ±ν μ μμΈ μ²λ¦¬μ©) | Preserve marked validators even when `strip` is enabled |
|
|
249
|
+
|
|
250
|
+
<br/>
|
|
251
|
+
|
|
252
|
+
### βοΈ μμ (Example)
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
// vite.config.ts
|
|
256
|
+
import { defineConfig } from "vite";
|
|
257
|
+
import { vitePlugin as runtypex } from "runtypex";
|
|
258
|
+
|
|
259
|
+
export default defineConfig({
|
|
260
|
+
plugins: [
|
|
261
|
+
runtypex({
|
|
262
|
+
mode: "development", // λ°νμ κ²μ¦ μ μ§
|
|
263
|
+
strip: false, // κ²μ¦ μ½λ μ κ±° λΉνμ±ν
|
|
264
|
+
logLevel: "info", // μΌλ° λ‘κ·Έ μΆλ ₯
|
|
265
|
+
}),
|
|
266
|
+
],
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
/* @runtypex:keep */
|
|
273
|
+
const assertUser = makeAssert<User>();
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
> `/* @runtypex:keep */` μ£Όμμ λΆμ΄λ©΄ `strip` μ΅μ
μ΄ νμ±νλμ΄λ
|
|
277
|
+
> ν΄λΉ κ²μ¦ ν¨μλ λΉλ κ²°κ³Όλ¬Όμμ μ κ±°λμ§ μμ΅λλ€.
|
|
278
|
+
> Add `/* @runtypex:keep */` above a validator to keep it even when `strip` is enabled.
|
|
@@ -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,5 +1,6 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import { emitGuardFromType } from "../core/index.js";
|
|
3
|
+
import { resolveTypeByName } from "./helper.js";
|
|
3
4
|
/**
|
|
4
5
|
* π§© tsTransformer
|
|
5
6
|
* TypeScript custom transformer (BEFORE) factory.
|
|
@@ -33,7 +34,7 @@ export default function tsTransformer(options) {
|
|
|
33
34
|
if (targetFunctions.includes(name) && node.typeArguments?.length) {
|
|
34
35
|
const typeNode = node.typeArguments[0];
|
|
35
36
|
const typeName = typeNode.getText();
|
|
36
|
-
const type =
|
|
37
|
+
const type = resolveTypeByName(program, node.getSourceFile(), checker, typeName);
|
|
37
38
|
if (!type)
|
|
38
39
|
return node;
|
|
39
40
|
const isRemovedInProd = removeInProd && prod;
|
|
@@ -50,57 +51,6 @@ export default function tsTransformer(options) {
|
|
|
50
51
|
return (sf) => ts.visitNode(sf, visit);
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
|
-
function _resolveTypeByName(program, sf, checker, name) {
|
|
54
|
-
// 1οΈβ£ <string|number|boolean|null|undefined>
|
|
55
|
-
const primitiveNames = ["string", "number", "boolean", "bigint", "symbol", "null", "undefined"];
|
|
56
|
-
if (primitiveNames.includes(name)) {
|
|
57
|
-
const map = {
|
|
58
|
-
string: checker.getStringType(),
|
|
59
|
-
number: checker.getNumberType(),
|
|
60
|
-
boolean: checker.getBooleanType(),
|
|
61
|
-
bigint: checker.getBigIntType(),
|
|
62
|
-
symbol: checker.getESSymbolType(),
|
|
63
|
-
null: checker.getNullType(),
|
|
64
|
-
undefined: checker.getUndefinedType(),
|
|
65
|
-
};
|
|
66
|
-
return map[name];
|
|
67
|
-
}
|
|
68
|
-
// 2οΈβ£ <Type|Interface|Enum> declared in the same file or other files
|
|
69
|
-
for (const file of program.getSourceFiles()) {
|
|
70
|
-
const decl = _findLocalDeclaration(file, name);
|
|
71
|
-
if (!decl)
|
|
72
|
-
continue;
|
|
73
|
-
if (ts.isInterfaceDeclaration(decl) || ts.isClassDeclaration(decl) || ts.isEnumDeclaration(decl)) {
|
|
74
|
-
const symbol = decl.name ? checker.getSymbolAtLocation(decl.name) : null;
|
|
75
|
-
if (symbol)
|
|
76
|
-
return checker.getDeclaredTypeOfSymbol(symbol);
|
|
77
|
-
}
|
|
78
|
-
if (ts.isTypeAliasDeclaration(decl)) {
|
|
79
|
-
return checker.getTypeFromTypeNode(decl.type);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
// 3οΈβ£ <Type|Interface|Enum> imported from other modules
|
|
83
|
-
const symbol = checker
|
|
84
|
-
.getSymbolsInScope(sf, ts.SymbolFlags.Type | ts.SymbolFlags.Alias | ts.SymbolFlags.Interface)
|
|
85
|
-
.find((s) => s.name === name);
|
|
86
|
-
return symbol ? checker.getDeclaredTypeOfSymbol(symbol) : null;
|
|
87
|
-
}
|
|
88
|
-
function _findLocalDeclaration(sf, name) {
|
|
89
|
-
let found;
|
|
90
|
-
(function walk(node) {
|
|
91
|
-
if ((ts.isInterfaceDeclaration(node) ||
|
|
92
|
-
ts.isTypeAliasDeclaration(node) ||
|
|
93
|
-
ts.isEnumDeclaration(node) ||
|
|
94
|
-
ts.isClassDeclaration(node)) &&
|
|
95
|
-
node.name?.text === name) {
|
|
96
|
-
found = node;
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
if (!found)
|
|
100
|
-
node.forEachChild(walk);
|
|
101
|
-
})(sf);
|
|
102
|
-
return found;
|
|
103
|
-
}
|
|
104
54
|
function _emitMakeValidate(checker, type, isRemovedInProd) {
|
|
105
55
|
const guard = isRemovedInProd ? "((_)=>true)" : emitGuardFromType(checker, type);
|
|
106
56
|
return ts.factory.createIdentifier(guard);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { emitGuardFromType } from "../core/index.js";
|
|
4
|
+
import { resolveTypeByName } from "./helper.js";
|
|
4
5
|
/**
|
|
5
6
|
* π§© vitePluginRuntypex
|
|
6
7
|
* A Vite plugin that performs build-time type β runtime validation transformation.
|
|
@@ -74,7 +75,7 @@ function _findNearestTsconfig(start) {
|
|
|
74
75
|
function _emitMakeValidate({ program, checker, sf, typeName, prod, removeInProd, }) {
|
|
75
76
|
if (removeInProd && prod)
|
|
76
77
|
return `((_)=>true)`;
|
|
77
|
-
const type =
|
|
78
|
+
const type = resolveTypeByName(program, sf, checker, typeName.trim());
|
|
78
79
|
if (!type)
|
|
79
80
|
return null;
|
|
80
81
|
return emitGuardFromType(checker, type);
|
|
@@ -82,71 +83,9 @@ function _emitMakeValidate({ program, checker, sf, typeName, prod, removeInProd,
|
|
|
82
83
|
function _emitMakeAssert({ program, checker, sf, typeName, prod, removeInProd, }) {
|
|
83
84
|
if (removeInProd && prod)
|
|
84
85
|
return `((_)=>{})`;
|
|
85
|
-
const type =
|
|
86
|
+
const type = resolveTypeByName(program, sf, checker, typeName.trim());
|
|
86
87
|
if (!type)
|
|
87
88
|
return null;
|
|
88
89
|
const guard = emitGuardFromType(checker, type);
|
|
89
90
|
return `(function(){const G=${guard};return(i)=>{if(!G(i))throw new TypeError("[runtypex] Validation failed.");};})()`;
|
|
90
91
|
}
|
|
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
|
-
}
|