runtypex 0.2.2 → 0.2.4
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 +14 -0
- package/dist/cjs/generator/generate-jsdoc.js +13 -7
- package/dist/esm/generator/generate-jsdoc.js +13 -7
- package/docs/build-integrations.md +4 -0
- package/docs/jsdoc-generation.md +58 -7
- package/docs/ko/build-integrations.md +186 -0
- package/docs/ko/jsdoc-generation.md +276 -0
- package/docs/ko/mapper.md +343 -0
- package/docs/ko/mapping-policy.md +192 -0
- package/docs/ko/runtime-validation.md +292 -0
- package/docs/mapper.md +87 -0
- package/docs/runtime-validation.md +71 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,20 @@ isUser({ id: 1, name: "Lux", active: true }); // true
|
|
|
37
37
|
assertUser({ id: "bad" }); // throws
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
## Runtime vs Build-Time Behavior
|
|
41
|
+
|
|
42
|
+
`makeValidate<T>()` and `makeAssert<T>()` need the Vite plugin or TypeScript
|
|
43
|
+
transformer to become real validation functions. `makeMapper<TDto, TDomain>()`
|
|
44
|
+
also benefits from the transformer, but it still has a runtime fallback that can
|
|
45
|
+
interpret the mapper spec directly.
|
|
46
|
+
|
|
47
|
+
| Execution path | `makeValidate<T>()` / `makeAssert<T>()` | `makeMapper<TDto, TDomain>()` | Docs generation |
|
|
48
|
+
| --- | --- | --- | --- |
|
|
49
|
+
| `npm run dev` with the Vite plugin | Works. Validation usually runs. | Works with validation. | Can generate when the dev server starts. |
|
|
50
|
+
| `npm run build` with the Vite plugin | Works. | Works. | Generates docs. |
|
|
51
|
+
| `npm run build` + `NODE_ENV=production` + `removeInProd: true` | Replaced with no-op validation. | Mapping still works; validation is removed. | Unaffected. |
|
|
52
|
+
| No Vite plugin or transformer | Placeholder runtime fallback; does not inspect `T`. | Runtime fallback mapping still works. | Not generated. |
|
|
53
|
+
|
|
40
54
|
## Vite Setup
|
|
41
55
|
|
|
42
56
|
```ts
|
|
@@ -30,14 +30,14 @@ function generateJSDocFromSpec(params) {
|
|
|
30
30
|
_pushJSDocText(lines, _escapeComment(description));
|
|
31
31
|
lines.push(" *");
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
_pushJSDocBulletField(lines, "DTO", _formatCodeSpan(`${dtoName}.${rule.from}`));
|
|
34
34
|
if (rule.dtoDescription) {
|
|
35
|
-
|
|
35
|
+
_pushJSDocBulletField(lines, "DTO description", _escapeComment(rule.dtoDescription));
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
_pushJSDocBulletField(lines, "DTO type", _formatCodeSpan(dtoPathType ? checker.typeToString(dtoPathType) : "unknown"));
|
|
38
38
|
if (rule.db)
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
_pushJSDocBulletField(lines, "Origin", _formatCodeSpan(rule.db));
|
|
40
|
+
_pushJSDocBulletField(lines, "Domain type", _formatCodeSpan(checker.typeToString(domainType)));
|
|
41
41
|
lines.push(" */");
|
|
42
42
|
lines.push(` ${_propertyName(prop.name)}${optional}: ${checker.typeToString(domainType)};`);
|
|
43
43
|
lines.push("");
|
|
@@ -75,8 +75,8 @@ function _getDomainDescription(checker, prop) {
|
|
|
75
75
|
const description = typescript_1.default.displayPartsToString(prop.getDocumentationComment(checker)).trim();
|
|
76
76
|
return description || null;
|
|
77
77
|
}
|
|
78
|
-
function
|
|
79
|
-
_pushJSDocText(lines,
|
|
78
|
+
function _pushJSDocBulletField(lines, label, value) {
|
|
79
|
+
_pushJSDocText(lines, `- ${label}: ${value}`, "", " ");
|
|
80
80
|
}
|
|
81
81
|
function _pushJSDocText(lines, text, firstIndent = "", continuationIndent = firstIndent) {
|
|
82
82
|
for (const line of _wrapJSDocText(text, firstIndent, continuationIndent)) {
|
|
@@ -101,3 +101,9 @@ function _wrapJSDocText(text, firstIndent, continuationIndent) {
|
|
|
101
101
|
lines.push(current.trimEnd());
|
|
102
102
|
return lines;
|
|
103
103
|
}
|
|
104
|
+
function _formatCodeSpan(value) {
|
|
105
|
+
return `\`${_escapeMarkdownCode(_escapeComment(value))}\``;
|
|
106
|
+
}
|
|
107
|
+
function _escapeMarkdownCode(value) {
|
|
108
|
+
return value.replace(/`/g, "\\`");
|
|
109
|
+
}
|
|
@@ -24,14 +24,14 @@ export function generateJSDocFromSpec(params) {
|
|
|
24
24
|
_pushJSDocText(lines, _escapeComment(description));
|
|
25
25
|
lines.push(" *");
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
_pushJSDocBulletField(lines, "DTO", _formatCodeSpan(`${dtoName}.${rule.from}`));
|
|
28
28
|
if (rule.dtoDescription) {
|
|
29
|
-
|
|
29
|
+
_pushJSDocBulletField(lines, "DTO description", _escapeComment(rule.dtoDescription));
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
_pushJSDocBulletField(lines, "DTO type", _formatCodeSpan(dtoPathType ? checker.typeToString(dtoPathType) : "unknown"));
|
|
32
32
|
if (rule.db)
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
_pushJSDocBulletField(lines, "Origin", _formatCodeSpan(rule.db));
|
|
34
|
+
_pushJSDocBulletField(lines, "Domain type", _formatCodeSpan(checker.typeToString(domainType)));
|
|
35
35
|
lines.push(" */");
|
|
36
36
|
lines.push(` ${_propertyName(prop.name)}${optional}: ${checker.typeToString(domainType)};`);
|
|
37
37
|
lines.push("");
|
|
@@ -69,8 +69,8 @@ function _getDomainDescription(checker, prop) {
|
|
|
69
69
|
const description = ts.displayPartsToString(prop.getDocumentationComment(checker)).trim();
|
|
70
70
|
return description || null;
|
|
71
71
|
}
|
|
72
|
-
function
|
|
73
|
-
_pushJSDocText(lines,
|
|
72
|
+
function _pushJSDocBulletField(lines, label, value) {
|
|
73
|
+
_pushJSDocText(lines, `- ${label}: ${value}`, "", " ");
|
|
74
74
|
}
|
|
75
75
|
function _pushJSDocText(lines, text, firstIndent = "", continuationIndent = firstIndent) {
|
|
76
76
|
for (const line of _wrapJSDocText(text, firstIndent, continuationIndent)) {
|
|
@@ -95,3 +95,9 @@ function _wrapJSDocText(text, firstIndent, continuationIndent) {
|
|
|
95
95
|
lines.push(current.trimEnd());
|
|
96
96
|
return lines;
|
|
97
97
|
}
|
|
98
|
+
function _formatCodeSpan(value) {
|
|
99
|
+
return `\`${_escapeMarkdownCode(_escapeComment(value))}\``;
|
|
100
|
+
}
|
|
101
|
+
function _escapeMarkdownCode(value) {
|
|
102
|
+
return value.replace(/`/g, "\\`");
|
|
103
|
+
}
|
|
@@ -53,6 +53,10 @@ export const addressMap = defineMap<
|
|
|
53
53
|
It generates `SearchAddressDomain` by removing the `Source` suffix and writes
|
|
54
54
|
all generated interfaces for the same folder to `runtypex.generated.ts`.
|
|
55
55
|
|
|
56
|
+
Unlike `makeValidate<T>()`, `makeAssert<T>()`, and `makeMapper<TDto, TDomain>()`,
|
|
57
|
+
docs generation does create a file. When `docs` is not configured, no
|
|
58
|
+
`runtypex.generated.ts` file is written.
|
|
59
|
+
|
|
56
60
|
Docs options:
|
|
57
61
|
|
|
58
62
|
| Option | Default | Description |
|
package/docs/jsdoc-generation.md
CHANGED
|
@@ -25,6 +25,57 @@ The plugin finds `defineMap<TDto, TDomainSource>()(...)` calls in included mappe
|
|
|
25
25
|
files, removes the `Source` suffix from `TDomainSource`, and writes generated
|
|
26
26
|
interfaces to `runtypex.generated.ts` next to the mapper file.
|
|
27
27
|
|
|
28
|
+
## Build-Time vs No Build Integration
|
|
29
|
+
|
|
30
|
+
Docs generation runs from the Vite plugin `buildStart` hook. It is not a runtime
|
|
31
|
+
API and it does not change application code.
|
|
32
|
+
|
|
33
|
+
With `docs.include` configured, each matching mapper file is scanned during the
|
|
34
|
+
build:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// src/features/address/address.mapper.ts
|
|
38
|
+
export interface SearchAddressDomainSource {
|
|
39
|
+
/** Address id */
|
|
40
|
+
id: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const addressMap = defineMap<
|
|
44
|
+
SearchAddressDto,
|
|
45
|
+
SearchAddressDomainSource
|
|
46
|
+
>()({
|
|
47
|
+
id: source("RESULT.ID", {
|
|
48
|
+
db: "address.id",
|
|
49
|
+
dtoDescription: "Address identifier from the API response.",
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The plugin writes a generated file next to the mapper file:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// src/features/address/runtypex.generated.ts
|
|
58
|
+
export interface SearchAddressDomain {
|
|
59
|
+
/**
|
|
60
|
+
* Address id
|
|
61
|
+
*
|
|
62
|
+
* - DTO: `SearchAddressDto.RESULT.ID`
|
|
63
|
+
* - DTO description: Address identifier from the API response.
|
|
64
|
+
* - DTO type: `string`
|
|
65
|
+
* - Origin: `address.id`
|
|
66
|
+
* - Domain type: `string`
|
|
67
|
+
*/
|
|
68
|
+
id: string;
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
No application runtime code is emitted for docs generation. The only output is
|
|
73
|
+
the generated `.ts` documentation file.
|
|
74
|
+
|
|
75
|
+
Without `docs` configured, or without running the Vite plugin, no docs file is
|
|
76
|
+
created. Mapper files remain unchanged, and `generateJSDocFromSpec()` is only
|
|
77
|
+
available as a manual build-tool API.
|
|
78
|
+
|
|
28
79
|
## Manual API
|
|
29
80
|
|
|
30
81
|
```ts
|
|
@@ -66,8 +117,8 @@ Field meanings:
|
|
|
66
117
|
| Field | Meaning |
|
|
67
118
|
| --- | --- |
|
|
68
119
|
| Domain property JSDoc | Domain field description. Usually used as the first JSDoc sentence. |
|
|
69
|
-
| `dtoDescription` | Optional explanation shown
|
|
70
|
-
| `db` | Optional
|
|
120
|
+
| `dtoDescription` | Optional explanation shown as the `DTO description` bullet. |
|
|
121
|
+
| `db` | Optional origin field shown as the `Origin` bullet. |
|
|
71
122
|
|
|
72
123
|
For older mapper specs, `description` is still read as a fallback when the
|
|
73
124
|
domain property has no JSDoc. New code should prefer domain property JSDoc so
|
|
@@ -95,11 +146,11 @@ the generated documentation can look like this:
|
|
|
95
146
|
/**
|
|
96
147
|
* User id
|
|
97
148
|
*
|
|
98
|
-
* DTO: UserDto.user_id
|
|
99
|
-
*
|
|
100
|
-
* DTO type: string
|
|
101
|
-
*
|
|
102
|
-
* Domain type: string
|
|
149
|
+
* - DTO: `UserDto.user_id`
|
|
150
|
+
* - DTO description: Identifier returned by the user API.
|
|
151
|
+
* - DTO type: `string`
|
|
152
|
+
* - Origin: `users.user_id`
|
|
153
|
+
* - Domain type: `string`
|
|
103
154
|
*/
|
|
104
155
|
id: string;
|
|
105
156
|
```
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
|
|
2
|
+
# 빌드 연동
|
|
3
|
+
|
|
4
|
+
`runtypex`는 TypeScript 컴파일러 API를 사용해 타입 정보를 분석하고, 이를 바탕으로 검증 함수와 매퍼 코드를 생성합니다.
|
|
5
|
+
|
|
6
|
+
가장 좋은 사용 방식은 **빌드 과정에서 TypeScript 트랜스포머를 실행하는 것**입니다. 이렇게 하면 타입 정보를 온전히 활용할 수 있어 `runtypex`의 기능을 제대로 사용할 수 있습니다.
|
|
7
|
+
|
|
8
|
+
<br/>
|
|
9
|
+
|
|
10
|
+
## Vite에서 사용하기
|
|
11
|
+
|
|
12
|
+
Vite 프로젝트에서는 `runtypex`의 Vite 플러그인을 등록하면 됩니다.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
// vite.config.ts
|
|
16
|
+
import { defineConfig } from "vite";
|
|
17
|
+
import { vitePlugin as runtypex } from "runtypex";
|
|
18
|
+
|
|
19
|
+
export default defineConfig({
|
|
20
|
+
plugins: [runtypex()],
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Vite 플러그인은 TypeScript 파일을 분석하면서 다음 호출을 찾습니다.
|
|
25
|
+
|
|
26
|
+
* `makeValidate<T>()`
|
|
27
|
+
* `makeAssert<T>()`
|
|
28
|
+
* `makeMapper<TDto, TDomain>()`
|
|
29
|
+
|
|
30
|
+
대상 호출을 찾으면 플러그인은 다음 순서로 동작합니다.
|
|
31
|
+
|
|
32
|
+
1. 해당 파일에서 가장 가까운 `tsconfig.json`을 찾습니다.
|
|
33
|
+
2. TypeScript 프로그램을 생성합니다.
|
|
34
|
+
3. `runtypex` 트랜스포머를 실행합니다.
|
|
35
|
+
4. 변환된 코드를 Vite에 반환합니다.
|
|
36
|
+
|
|
37
|
+
즉, 개발자는 위 함수들을 선언적으로 작성하고, 실제 검증 및 매핑 코드는 빌드 과정에서 자동으로 생성됩니다.
|
|
38
|
+
|
|
39
|
+
<br/>
|
|
40
|
+
|
|
41
|
+
## Vite에서 매퍼 문서 생성하기
|
|
42
|
+
|
|
43
|
+
Vite 플러그인은 매퍼 파일을 분석해, 컨벤션 기반으로 타입 문서 파일을 생성할 수도 있습니다.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
export default defineConfig({
|
|
47
|
+
plugins: [
|
|
48
|
+
runtypex({
|
|
49
|
+
docs: {
|
|
50
|
+
include: "src/features/**/*.mapper.ts",
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
위 설정을 추가하면 `runtypex`는 지정된 매퍼 파일을 스캔합니다.
|
|
58
|
+
|
|
59
|
+
예를 들어 다음과 같은 매퍼 선언이 있다고 가정합니다.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
export const addressMap = defineMap<
|
|
63
|
+
SearchAddressDto,
|
|
64
|
+
SearchAddressDomainSource
|
|
65
|
+
>()({
|
|
66
|
+
id: source("RESULT.ID"),
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`runtypex`는 두 번째 타입 인자인 `SearchAddressDomainSource`를 기준으로 생성할 도메인 타입 이름을 결정합니다.
|
|
71
|
+
|
|
72
|
+
기본 설정에서는 `Source` 접미사를 제거하므로, 생성되는 인터페이스 이름은 다음과 같습니다.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
SearchAddressDomain
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
생성된 인터페이스들은 각 매퍼 파일과 같은 폴더의 `runtypex.generated.ts` 파일에 기록됩니다.
|
|
79
|
+
|
|
80
|
+
> `makeValidate<T>()`, `makeAssert<T>()`, `makeMapper<TDto, TDomain>()`은 빌드 중 코드를 변환하지만,
|
|
81
|
+
> `docs` 옵션은 실제 파일인 `runtypex.generated.ts`를 생성합니다.
|
|
82
|
+
|
|
83
|
+
`docs` 옵션을 설정하지 않으면 `runtypex.generated.ts` 파일은 생성되지 않습니다.
|
|
84
|
+
|
|
85
|
+
<br/>
|
|
86
|
+
|
|
87
|
+
## 문서 생성 옵션
|
|
88
|
+
|
|
89
|
+
| 옵션 | 기본값 | 설명 |
|
|
90
|
+
| ------------------- | ----------------------------------- | ----------------------------------------------------------- |
|
|
91
|
+
| `include` | `**/*.mapper.ts`, `**/*.mapper.tsx` | Vite 루트 기준으로 스캔할 매퍼 파일 패턴입니다. |
|
|
92
|
+
| `exclude` | generated file name | 스캔에서 제외할 파일 패턴입니다. |
|
|
93
|
+
| `sourceSuffix` | `Source` | 생성 인터페이스 이름을 만들 때 제거할 도메인 타입 접미사입니다. |
|
|
94
|
+
| `generatedFileName` | `runtypex.generated.ts` | 각 매퍼 파일 옆에 생성할 파일 이름입니다. |
|
|
95
|
+
| `outDir` | `near-source` | 생성 파일 위치입니다. 현재는 소스 파일 근처 생성만 지원합니다. |
|
|
96
|
+
| `policyMode` | `warn` | 매퍼가 문서 생성 컨벤션을 어겼을 때의 처리 방식입니다. `error`로 설정하면 빌드 실패로 처리합니다. |
|
|
97
|
+
|
|
98
|
+
<br/>
|
|
99
|
+
|
|
100
|
+
## ts-loader에서 사용하기
|
|
101
|
+
|
|
102
|
+
webpack에서 `ts-loader`를 사용하는 경우, `getCustomTransformers` 옵션에 `runtypex` 트랜스포머를 등록합니다.
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
// webpack.config.js
|
|
106
|
+
const { tsTransformer } = require("runtypex");
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
module: {
|
|
110
|
+
rules: [
|
|
111
|
+
{
|
|
112
|
+
test: /\.tsx?$/,
|
|
113
|
+
loader: "ts-loader",
|
|
114
|
+
options: {
|
|
115
|
+
getCustomTransformers: (program) => ({
|
|
116
|
+
before: [tsTransformer({ program })],
|
|
117
|
+
}),
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
이 설정은 TypeScript 컴파일 전에 `runtypex` 트랜스포머를 실행합니다.
|
|
126
|
+
|
|
127
|
+
<br/>
|
|
128
|
+
|
|
129
|
+
## 트랜스포머 옵션
|
|
130
|
+
|
|
131
|
+
`tsTransformer`에는 다음 옵션을 전달할 수 있습니다.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
tsTransformer({
|
|
135
|
+
program,
|
|
136
|
+
removeInProd: true,
|
|
137
|
+
validateDto: true,
|
|
138
|
+
validateDomain: true,
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
| 옵션 | 기본값 | 설명 |
|
|
143
|
+
| ---------------- | -------- | -------------------------------------------- |
|
|
144
|
+
| `program` | required | 타입 해석에 사용할 TypeScript 프로그램입니다. 반드시 전달해야 합니다. |
|
|
145
|
+
| `removeInProd` | `false` | 프로덕션 빌드에서 생성된 검증 코드를 no-op 함수로 대체합니다. |
|
|
146
|
+
| `validateDto` | `true` | 생성 매퍼에서 DTO 입력값 검증을 활성화합니다. |
|
|
147
|
+
| `validateDomain` | `true` | 생성 매퍼에서 도메인 출력값 검증을 활성화합니다. |
|
|
148
|
+
|
|
149
|
+
<br/>
|
|
150
|
+
|
|
151
|
+
## 패키지 진입점
|
|
152
|
+
|
|
153
|
+
필요한 기능에 따라 다음 경로에서 가져올 수 있습니다.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { makeValidate } from "runtypex";
|
|
157
|
+
import { makeMapper } from "runtypex/mapper";
|
|
158
|
+
import { generateJSDocFromSpec } from "runtypex/generator";
|
|
159
|
+
import { tsTransformer } from "runtypex/transformer";
|
|
160
|
+
import { vitePlugin } from "runtypex/transformer/vite-plugin";
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
패키지는 ESM과 CommonJS 빌드를 모두 제공합니다.
|
|
164
|
+
|
|
165
|
+
* ESM: `dist/esm`
|
|
166
|
+
* CommonJS: `dist/cjs`
|
|
167
|
+
|
|
168
|
+
<br/>
|
|
169
|
+
|
|
170
|
+
## 로컬 검증
|
|
171
|
+
|
|
172
|
+
빌드와 테스트가 정상적으로 동작하는지 확인하려면 다음 명령을 사용할 수 있습니다.
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
npm run build
|
|
176
|
+
npx jest --runInBand --watchman=false
|
|
177
|
+
npm run test:esm
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
각 명령의 용도는 다음과 같습니다.
|
|
181
|
+
|
|
182
|
+
| 명령 | 용도 |
|
|
183
|
+
| --------------------------------------- | ----------------------------- |
|
|
184
|
+
| `npm run build` | 패키지를 빌드합니다. |
|
|
185
|
+
| `npx jest --runInBand --watchman=false` | Jest 테스트를 단일 프로세스로 실행합니다. |
|
|
186
|
+
| `npm run test:esm` | ESM 환경에서 패키지가 정상 동작하는지 확인합니다. |
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
# JSDoc 생성
|
|
4
|
+
|
|
5
|
+
`runtypex`는 매퍼 메타데이터를 바탕으로 도메인 인터페이스용 JSDoc 문서를 생성할 수 있습니다.
|
|
6
|
+
|
|
7
|
+
생성된 JSDoc은 에디터에서 도메인 필드를 볼 때 다음 정보를 함께 확인할 수 있도록 돕습니다.
|
|
8
|
+
|
|
9
|
+
* 도메인 필드의 설명
|
|
10
|
+
* 해당 필드가 참조하는 DTO 경로
|
|
11
|
+
* DTO 필드 설명
|
|
12
|
+
* DTO 타입
|
|
13
|
+
* 원본 데이터 출처
|
|
14
|
+
* 도메인 타입
|
|
15
|
+
|
|
16
|
+
즉, 매퍼를 통해 만들어진 도메인 필드가 **어떤 DTO 또는 원본 필드에서 왔는지**를 코드 안에서 바로 확인할 수 있습니다.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
<br/>
|
|
20
|
+
|
|
21
|
+
## Vite에서 자동 생성하기
|
|
22
|
+
|
|
23
|
+
Vite 프로젝트에서는 `runtypex` 플러그인의 `docs` 옵션을 설정하면 JSDoc 문서 파일을 자동으로 생성할 수 있습니다.
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { defineConfig } from "vite";
|
|
27
|
+
import { vitePlugin as runtypex } from "runtypex";
|
|
28
|
+
|
|
29
|
+
export default defineConfig({
|
|
30
|
+
plugins: [
|
|
31
|
+
runtypex({
|
|
32
|
+
docs: {
|
|
33
|
+
include: "src/features/**/*.mapper.ts",
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
플러그인은 `docs.include`에 포함된 매퍼 파일을 스캔하고, 다음 형태의 호출을 찾습니다.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
defineMap<TDto, TDomainSource>()(...)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
그런 다음 두 번째 타입 인자인 `TDomainSource`를 기준으로 생성할 인터페이스 이름을 결정합니다.
|
|
47
|
+
|
|
48
|
+
기본 설정에서는 `Source` 접미사를 제거합니다.
|
|
49
|
+
|
|
50
|
+
예를 들어 다음 타입은:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
SearchAddressDomainSource
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
다음 인터페이스 이름으로 생성됩니다.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
SearchAddressDomain
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
생성된 인터페이스는 매퍼 파일과 같은 폴더의 `runtypex.generated.ts` 파일에 기록됩니다.
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
<br/>
|
|
66
|
+
|
|
67
|
+
## 생성 예시
|
|
68
|
+
|
|
69
|
+
다음과 같은 매퍼 파일이 있다고 가정합니다.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// src/features/address/address.mapper.ts
|
|
73
|
+
export interface SearchAddressDomainSource {
|
|
74
|
+
/** Address id */
|
|
75
|
+
id: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const addressMap = defineMap<
|
|
79
|
+
SearchAddressDto,
|
|
80
|
+
SearchAddressDomainSource
|
|
81
|
+
>()({
|
|
82
|
+
id: source("RESULT.ID", {
|
|
83
|
+
db: "address.id",
|
|
84
|
+
dtoDescription: "Address identifier from the API response.",
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`runtypex`는 매퍼 파일 옆에 다음과 같은 생성 파일을 기록합니다.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
// src/features/address/runtypex.generated.ts
|
|
93
|
+
export interface SearchAddressDomain {
|
|
94
|
+
/**
|
|
95
|
+
* Address id
|
|
96
|
+
*
|
|
97
|
+
* - DTO: `SearchAddressDto.RESULT.ID`
|
|
98
|
+
* - DTO description: Address identifier from the API response.
|
|
99
|
+
* - DTO type: `string`
|
|
100
|
+
* - Origin: `address.id`
|
|
101
|
+
* - Domain type: `string`
|
|
102
|
+
*/
|
|
103
|
+
id: string;
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
이 문서는 런타임 코드가 아니라, 타입과 에디터 경험을 위한 `.ts` 문서 파일입니다.
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
<br/>
|
|
111
|
+
|
|
112
|
+
## 문서 생성 시점
|
|
113
|
+
|
|
114
|
+
JSDoc 문서 생성은 Vite 플러그인의 `buildStart` 훅에서 실행됩니다.
|
|
115
|
+
|
|
116
|
+
따라서 다음 조건을 만족할 때만 문서 파일이 생성됩니다.
|
|
117
|
+
|
|
118
|
+
1. Vite 플러그인을 사용합니다.
|
|
119
|
+
2. `docs` 옵션을 설정합니다.
|
|
120
|
+
3. `docs.include`에 해당하는 매퍼 파일이 있습니다.
|
|
121
|
+
4. 해당 파일 안에 `defineMap<TDto, TDomainSource>()(...)` 호출이 있습니다.
|
|
122
|
+
|
|
123
|
+
반대로 다음 경우에는 `runtypex.generated.ts` 파일이 생성되지 않습니다.
|
|
124
|
+
|
|
125
|
+
* `docs` 옵션을 설정하지 않은 경우
|
|
126
|
+
* Vite 플러그인을 실행하지 않은 경우
|
|
127
|
+
* 포함 조건에 맞는 매퍼 파일이 없는 경우
|
|
128
|
+
* 매퍼 파일이 문서 생성 컨벤션을 따르지 않는 경우
|
|
129
|
+
|
|
130
|
+
문서 생성은 애플리케이션 런타임 코드를 변경하지 않습니다. 매퍼 파일도 그대로 유지되며, 출력 결과는 생성된 `.ts` 문서 파일뿐입니다.
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
<br/>
|
|
134
|
+
|
|
135
|
+
## 수동 API 사용하기
|
|
136
|
+
|
|
137
|
+
Vite 플러그인을 사용하지 않고, 자체 빌드 도구에서 JSDoc 생성을 직접 실행할 수도 있습니다.
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { generateJSDocFromSpec } from "runtypex/generator";
|
|
141
|
+
|
|
142
|
+
const source = generateJSDocFromSpec({
|
|
143
|
+
checker,
|
|
144
|
+
dtoType,
|
|
145
|
+
domainType,
|
|
146
|
+
specNode,
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`generateJSDocFromSpec()`은 저수준 API입니다.
|
|
151
|
+
|
|
152
|
+
이 API는 다음 값에 이미 접근할 수 있는 빌드 도구를 위한 기능입니다.
|
|
153
|
+
|
|
154
|
+
* TypeScript 프로그램
|
|
155
|
+
* TypeScript checker
|
|
156
|
+
* DTO 타입
|
|
157
|
+
* 도메인 타입
|
|
158
|
+
* 매퍼 명세 노드
|
|
159
|
+
|
|
160
|
+
일반적인 Vite 프로젝트에서는 직접 호출하기보다 `docs` 옵션을 사용하는 것을 권장합니다.
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
<br/>
|
|
164
|
+
|
|
165
|
+
## 메타데이터 작성 위치
|
|
166
|
+
|
|
167
|
+
도메인 필드의 기본 설명은 도메인 타입에 작성하는 것이 좋습니다.
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
interface User {
|
|
171
|
+
/** User id */
|
|
172
|
+
id: string;
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
매퍼 메타데이터에는 DTO 또는 원본 데이터에 특화된 설명을 작성할 수 있습니다.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
source("user_id", {
|
|
180
|
+
db: "users.user_id",
|
|
181
|
+
dtoDescription: "Identifier returned by the user API.",
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
각 필드는 생성된 JSDoc에서 다음과 같이 사용됩니다.
|
|
186
|
+
|
|
187
|
+
| 필드 | 설명 |
|
|
188
|
+
| --------------------- | ---------------------------------------- |
|
|
189
|
+
| Domain property JSDoc | 도메인 필드 설명입니다. 일반적으로 JSDoc의 첫 문장으로 사용됩니다. |
|
|
190
|
+
| `dtoDescription` | `DTO description` 항목에 표시되는 선택 설명입니다. |
|
|
191
|
+
| `db` | `Origin` 항목에 표시되는 선택 원본 필드입니다. |
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
<br/>
|
|
195
|
+
|
|
196
|
+
## `description` 폴백
|
|
197
|
+
|
|
198
|
+
이전 매퍼 명세와의 호환성을 위해, 도메인 프로퍼티에 JSDoc이 없으면 매퍼 메타데이터의 `description` 값을 폴백으로 사용합니다.
|
|
199
|
+
|
|
200
|
+
다만 새 코드에서는 도메인 프로퍼티 JSDoc을 우선 사용하는 것이 좋습니다.
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
interface User {
|
|
204
|
+
/** User id */
|
|
205
|
+
id: string;
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
이 방식은 도메인 필드의 의미를 타입 정의에 한 번만 작성할 수 있게 해줍니다. 같은 도메인 필드를 여러 DTO 매핑에서 사용할 때도 설명을 반복하지 않아도 됩니다.
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
<br/>
|
|
213
|
+
|
|
214
|
+
## 생성 결과 예시
|
|
215
|
+
|
|
216
|
+
다음 도메인 타입과 매핑이 있다고 가정합니다.
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
interface User {
|
|
220
|
+
/** User id */
|
|
221
|
+
id: string;
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
id: source("user_id", {
|
|
227
|
+
db: "users.user_id",
|
|
228
|
+
dtoDescription: "Identifier returned by the user API.",
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
생성되는 JSDoc은 다음과 같은 형태가 됩니다.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
/**
|
|
236
|
+
* User id
|
|
237
|
+
*
|
|
238
|
+
* - DTO: `UserDto.user_id`
|
|
239
|
+
* - DTO description: Identifier returned by the user API.
|
|
240
|
+
* - DTO type: `string`
|
|
241
|
+
* - Origin: `users.user_id`
|
|
242
|
+
* - Domain type: `string`
|
|
243
|
+
*/
|
|
244
|
+
id: string;
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
<br/>
|
|
249
|
+
|
|
250
|
+
## 정책 옵션 연동
|
|
251
|
+
|
|
252
|
+
JSDoc 생성은 매퍼 정책 옵션과 함께 사용할 수 있습니다.
|
|
253
|
+
|
|
254
|
+
정책 옵션을 설정하면 매퍼 명세가 표준 DTO 경로 명명 규칙을 위반했을 때 경고를 출력하거나, 문서 생성 자체를 실패시킬 수 있습니다.
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
generateJSDocFromSpec({
|
|
258
|
+
checker,
|
|
259
|
+
dtoType,
|
|
260
|
+
domainType,
|
|
261
|
+
specNode,
|
|
262
|
+
options: {
|
|
263
|
+
mappingPolicy: policy,
|
|
264
|
+
policyMode: "error",
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
`policyMode`는 정책 위반을 어떻게 처리할지 결정합니다.
|
|
270
|
+
|
|
271
|
+
| 값 | 동작 |
|
|
272
|
+
| ------- | ------------------------------ |
|
|
273
|
+
| `warn` | 정책 위반을 경고로 처리합니다. |
|
|
274
|
+
| `error` | 정책 위반을 오류로 처리하고 문서 생성을 실패시킵니다. |
|
|
275
|
+
|
|
276
|
+
이 옵션은 생성 문서가 런타임 매퍼 및 빌드 시점 매퍼 생성과 같은 명명 규칙을 따르도록 만들고 싶을 때 유용합니다.
|