runtypex 0.2.0 → 0.2.1

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
@@ -79,6 +79,7 @@ interface UserDto {
79
79
  }
80
80
 
81
81
  interface User {
82
+ /** User id */
82
83
  id: string;
83
84
  displayName: string;
84
85
  isActive: boolean;
@@ -87,7 +88,6 @@ interface User {
87
88
  const userMap = defineMap<UserDto, User>()({
88
89
  id: source("user_id", {
89
90
  db: "users.user_id",
90
- description: "User id",
91
91
  dtoDescription: "User identifier from the user DTO.",
92
92
  }),
93
93
  displayName: source("profile.name"),
@@ -9,6 +9,7 @@ export type MapRuleInfo = {
9
9
  key: string;
10
10
  from: string;
11
11
  db?: string;
12
+ /** @deprecated Prefer domain property JSDoc for domain field descriptions. */
12
13
  description?: string;
13
14
  dtoDescription?: string;
14
15
  };
@@ -7,6 +7,7 @@ exports.generateJSDocFromSpec = generateJSDocFromSpec;
7
7
  const typescript_1 = __importDefault(require("typescript"));
8
8
  const path_js_1 = require("../core/path.js");
9
9
  const emitMapperFromSpec_js_1 = require("../core/emitMapperFromSpec.js");
10
+ const JSDOC_CONTENT_WIDTH = 76;
10
11
  function generateJSDocFromSpec(params) {
11
12
  const checker = params.checker;
12
13
  const dtoName = params.dtoType.symbol?.name ?? "Dto";
@@ -22,18 +23,21 @@ function generateJSDocFromSpec(params) {
22
23
  const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
23
24
  const domainType = declaration ? checker.getTypeOfSymbolAtLocation(prop, declaration) : checker.getAnyType();
24
25
  const dtoPathType = _getTypeAtPath(checker, params.dtoType, rule.from);
26
+ const description = _getDomainDescription(checker, prop) ?? rule.description;
25
27
  const optional = (prop.getFlags() & typescript_1.default.SymbolFlags.Optional) !== 0 ? "?" : "";
26
28
  lines.push(" /**");
27
- if (rule.description) {
28
- lines.push(` * ${_escapeComment(rule.description)}`);
29
+ if (description) {
30
+ _pushJSDocText(lines, _escapeComment(description));
29
31
  lines.push(" *");
30
32
  }
31
- const dtoDescription = rule.dtoDescription ? ` ${_escapeComment(rule.dtoDescription)}` : "";
32
- lines.push(` * DTO: ${dtoName}.${rule.from}${dtoDescription}`);
33
- lines.push(` * DTO type: ${dtoPathType ? checker.typeToString(dtoPathType) : "unknown"}`);
33
+ _pushJSDocField(lines, "DTO", `${dtoName}.${rule.from}`);
34
+ if (rule.dtoDescription) {
35
+ _pushJSDocText(lines, _escapeComment(rule.dtoDescription), " ");
36
+ }
37
+ _pushJSDocField(lines, "DTO type", dtoPathType ? checker.typeToString(dtoPathType) : "unknown");
34
38
  if (rule.db)
35
- lines.push(` * DB: ${_escapeComment(rule.db)}`);
36
- lines.push(` * Domain type: ${checker.typeToString(domainType)}`);
39
+ _pushJSDocField(lines, "DB", _escapeComment(rule.db));
40
+ _pushJSDocField(lines, "Domain type", checker.typeToString(domainType));
37
41
  lines.push(" */");
38
42
  lines.push(` ${_propertyName(prop.name)}${optional}: ${checker.typeToString(domainType)};`);
39
43
  lines.push("");
@@ -67,3 +71,33 @@ function _propertyName(name) {
67
71
  function _escapeComment(value) {
68
72
  return value.replace(/\*\//g, "* /");
69
73
  }
74
+ function _getDomainDescription(checker, prop) {
75
+ const description = typescript_1.default.displayPartsToString(prop.getDocumentationComment(checker)).trim();
76
+ return description || null;
77
+ }
78
+ function _pushJSDocField(lines, label, value) {
79
+ _pushJSDocText(lines, `${label}: ${value}`, "", " ".repeat(label.length + 2));
80
+ }
81
+ function _pushJSDocText(lines, text, firstIndent = "", continuationIndent = firstIndent) {
82
+ for (const line of _wrapJSDocText(text, firstIndent, continuationIndent)) {
83
+ lines.push(` * ${line}`);
84
+ }
85
+ }
86
+ function _wrapJSDocText(text, firstIndent, continuationIndent) {
87
+ const words = text.replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
88
+ if (!words.length)
89
+ return [firstIndent.trimEnd()];
90
+ const lines = [];
91
+ let current = firstIndent;
92
+ for (const word of words) {
93
+ const candidate = current.trim().length ? `${current} ${word}` : `${current}${word}`;
94
+ if (candidate.length <= JSDOC_CONTENT_WIDTH || !current.trim().length) {
95
+ current = candidate;
96
+ continue;
97
+ }
98
+ lines.push(current.trimEnd());
99
+ current = `${continuationIndent}${word}`;
100
+ }
101
+ lines.push(current.trimEnd());
102
+ return lines;
103
+ }
@@ -5,6 +5,7 @@ export type PathOf<T> = T extends Primitive ? never : T extends readonly (infer
5
5
  export type Mapper<TDto, TDomain> = (dto: TDto) => TDomain;
6
6
  export type MapperMetadata<TValue = never> = {
7
7
  db?: string;
8
+ /** @deprecated Prefer domain property JSDoc for domain field descriptions. */
8
9
  description?: string;
9
10
  dtoDescription?: string;
10
11
  default?: TValue;
@@ -9,6 +9,7 @@ export type MapRuleInfo = {
9
9
  key: string;
10
10
  from: string;
11
11
  db?: string;
12
+ /** @deprecated Prefer domain property JSDoc for domain field descriptions. */
12
13
  description?: string;
13
14
  dtoDescription?: string;
14
15
  };
@@ -1,6 +1,7 @@
1
1
  import ts from "typescript";
2
2
  import { parsePath } from "../core/path.js";
3
3
  import { findMapPolicyViolations, handleMapPolicyViolations, readMapRules } from "../core/emitMapperFromSpec.js";
4
+ const JSDOC_CONTENT_WIDTH = 76;
4
5
  export function generateJSDocFromSpec(params) {
5
6
  const checker = params.checker;
6
7
  const dtoName = params.dtoType.symbol?.name ?? "Dto";
@@ -16,18 +17,21 @@ export function generateJSDocFromSpec(params) {
16
17
  const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
17
18
  const domainType = declaration ? checker.getTypeOfSymbolAtLocation(prop, declaration) : checker.getAnyType();
18
19
  const dtoPathType = _getTypeAtPath(checker, params.dtoType, rule.from);
20
+ const description = _getDomainDescription(checker, prop) ?? rule.description;
19
21
  const optional = (prop.getFlags() & ts.SymbolFlags.Optional) !== 0 ? "?" : "";
20
22
  lines.push(" /**");
21
- if (rule.description) {
22
- lines.push(` * ${_escapeComment(rule.description)}`);
23
+ if (description) {
24
+ _pushJSDocText(lines, _escapeComment(description));
23
25
  lines.push(" *");
24
26
  }
25
- const dtoDescription = rule.dtoDescription ? ` ${_escapeComment(rule.dtoDescription)}` : "";
26
- lines.push(` * DTO: ${dtoName}.${rule.from}${dtoDescription}`);
27
- lines.push(` * DTO type: ${dtoPathType ? checker.typeToString(dtoPathType) : "unknown"}`);
27
+ _pushJSDocField(lines, "DTO", `${dtoName}.${rule.from}`);
28
+ if (rule.dtoDescription) {
29
+ _pushJSDocText(lines, _escapeComment(rule.dtoDescription), " ");
30
+ }
31
+ _pushJSDocField(lines, "DTO type", dtoPathType ? checker.typeToString(dtoPathType) : "unknown");
28
32
  if (rule.db)
29
- lines.push(` * DB: ${_escapeComment(rule.db)}`);
30
- lines.push(` * Domain type: ${checker.typeToString(domainType)}`);
33
+ _pushJSDocField(lines, "DB", _escapeComment(rule.db));
34
+ _pushJSDocField(lines, "Domain type", checker.typeToString(domainType));
31
35
  lines.push(" */");
32
36
  lines.push(` ${_propertyName(prop.name)}${optional}: ${checker.typeToString(domainType)};`);
33
37
  lines.push("");
@@ -61,3 +65,33 @@ function _propertyName(name) {
61
65
  function _escapeComment(value) {
62
66
  return value.replace(/\*\//g, "* /");
63
67
  }
68
+ function _getDomainDescription(checker, prop) {
69
+ const description = ts.displayPartsToString(prop.getDocumentationComment(checker)).trim();
70
+ return description || null;
71
+ }
72
+ function _pushJSDocField(lines, label, value) {
73
+ _pushJSDocText(lines, `${label}: ${value}`, "", " ".repeat(label.length + 2));
74
+ }
75
+ function _pushJSDocText(lines, text, firstIndent = "", continuationIndent = firstIndent) {
76
+ for (const line of _wrapJSDocText(text, firstIndent, continuationIndent)) {
77
+ lines.push(` * ${line}`);
78
+ }
79
+ }
80
+ function _wrapJSDocText(text, firstIndent, continuationIndent) {
81
+ const words = text.replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
82
+ if (!words.length)
83
+ return [firstIndent.trimEnd()];
84
+ const lines = [];
85
+ let current = firstIndent;
86
+ for (const word of words) {
87
+ const candidate = current.trim().length ? `${current} ${word}` : `${current}${word}`;
88
+ if (candidate.length <= JSDOC_CONTENT_WIDTH || !current.trim().length) {
89
+ current = candidate;
90
+ continue;
91
+ }
92
+ lines.push(current.trimEnd());
93
+ current = `${continuationIndent}${word}`;
94
+ }
95
+ lines.push(current.trimEnd());
96
+ return lines;
97
+ }
@@ -5,6 +5,7 @@ export type PathOf<T> = T extends Primitive ? never : T extends readonly (infer
5
5
  export type Mapper<TDto, TDomain> = (dto: TDto) => TDomain;
6
6
  export type MapperMetadata<TValue = never> = {
7
7
  db?: string;
8
+ /** @deprecated Prefer domain property JSDoc for domain field descriptions. */
8
9
  description?: string;
9
10
  dtoDescription?: string;
10
11
  default?: TValue;
@@ -20,14 +20,22 @@ const source = generateJSDocFromSpec({
20
20
  This API is intended for build tooling that already has access to the TypeScript
21
21
  program, checker, DTO type, domain type, and mapper spec node.
22
22
 
23
- ## Metadata Fields
23
+ ## Metadata Sources
24
24
 
25
- Mapper metadata can include:
25
+ Domain field descriptions should live on the domain type:
26
+
27
+ ```ts
28
+ interface User {
29
+ /** User id */
30
+ id: string;
31
+ }
32
+ ```
33
+
34
+ Mapper metadata can include source-specific details:
26
35
 
27
36
  ```ts
28
37
  source("user_id", {
29
38
  db: "users.user_id",
30
- description: "User id",
31
39
  dtoDescription: "Identifier returned by the user API.",
32
40
  });
33
41
  ```
@@ -36,18 +44,26 @@ Field meanings:
36
44
 
37
45
  | Field | Meaning |
38
46
  | --- | --- |
39
- | `description` | Domain field description. Usually used as the first JSDoc sentence. |
40
- | `dtoDescription` | Optional explanation attached to the DTO path line. |
47
+ | Domain property JSDoc | Domain field description. Usually used as the first JSDoc sentence. |
48
+ | `dtoDescription` | Optional explanation shown below the DTO path line. |
41
49
  | `db` | Optional database table and column reference. |
42
50
 
51
+ For older mapper specs, `description` is still read as a fallback when the
52
+ domain property has no JSDoc. New code should prefer domain property JSDoc so
53
+ the domain description is not duplicated per DTO mapping.
54
+
43
55
  ## Generated Output
44
56
 
45
- Given this domain field:
57
+ Given this domain field and mapping:
46
58
 
47
59
  ```ts
60
+ interface User {
61
+ /** User id */
62
+ id: string;
63
+ }
64
+
48
65
  id: source("user_id", {
49
66
  db: "users.user_id",
50
- description: "User id",
51
67
  dtoDescription: "Identifier returned by the user API.",
52
68
  });
53
69
  ```
@@ -58,7 +74,8 @@ the generated documentation can look like this:
58
74
  /**
59
75
  * User id
60
76
  *
61
- * DTO: UserDto.user_id Identifier returned by the user API.
77
+ * DTO: UserDto.user_id
78
+ * Identifier returned by the user API.
62
79
  * DTO type: string
63
80
  * DB: users.user_id
64
81
  * Domain type: string
package/docs/mapper.md CHANGED
@@ -77,13 +77,13 @@ Mapping rules can include metadata:
77
77
  ```ts
78
78
  id: source("user_id", {
79
79
  db: "users.user_id",
80
- description: "User id",
81
80
  dtoDescription: "Identifier returned by the user API.",
82
81
  });
83
82
  ```
84
83
 
85
84
  Metadata is not required for mapping. It is mainly used by JSDoc generation and
86
- documentation tooling.
85
+ documentation tooling. Keep domain field descriptions on the domain type JSDoc;
86
+ mapper metadata should describe DTO/database-specific details.
87
87
 
88
88
  ## Typed Helpers
89
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runtypex",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Runtime type guards compiled from TypeScript types.",
5
5
  "license": "MIT",
6
6
  "author": "KumJungMin",