runtypex 0.1.13 → 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.
Files changed (72) hide show
  1. package/README.md +112 -278
  2. package/dist/cjs/core/emitArrayOrTuple.d.ts +6 -0
  3. package/dist/cjs/core/emitArrayOrTuple.js +40 -0
  4. package/dist/cjs/core/emitLiteralOrEnum.d.ts +4 -0
  5. package/dist/cjs/core/emitLiteralOrEnum.js +47 -0
  6. package/dist/cjs/core/emitMapperFromSpec.d.ts +33 -0
  7. package/dist/cjs/core/emitMapperFromSpec.js +199 -0
  8. package/dist/cjs/core/emitObject.d.ts +6 -0
  9. package/dist/cjs/core/emitObject.js +30 -0
  10. package/dist/cjs/core/emitPrimitive.d.ts +6 -0
  11. package/dist/cjs/core/emitPrimitive.js +29 -0
  12. package/dist/cjs/core/emitUnionOrIntersection.d.ts +6 -0
  13. package/dist/cjs/core/emitUnionOrIntersection.js +15 -0
  14. package/dist/cjs/core/index.d.ts +17 -0
  15. package/dist/cjs/core/index.js +41 -0
  16. package/dist/cjs/core/path.d.ts +9 -0
  17. package/dist/cjs/core/path.js +42 -0
  18. package/dist/cjs/generator/generate-jsdoc.d.ts +13 -0
  19. package/dist/cjs/generator/generate-jsdoc.js +103 -0
  20. package/dist/cjs/generator/index.d.ts +1 -0
  21. package/dist/cjs/generator/index.js +17 -0
  22. package/dist/cjs/index.d.ts +4 -0
  23. package/dist/cjs/index.js +20 -0
  24. package/dist/cjs/mapper/index.d.ts +1 -0
  25. package/dist/cjs/mapper/index.js +17 -0
  26. package/dist/cjs/runtime/index.d.ts +2 -0
  27. package/dist/cjs/runtime/index.js +18 -0
  28. package/dist/cjs/runtime/mapper.d.ts +72 -0
  29. package/dist/cjs/runtime/mapper.js +79 -0
  30. package/dist/cjs/runtime/validate.d.ts +11 -0
  31. package/dist/cjs/runtime/validate.js +18 -0
  32. package/dist/cjs/transformer/helper.d.ts +2 -0
  33. package/dist/cjs/transformer/helper.js +63 -0
  34. package/dist/cjs/transformer/index.d.ts +3 -0
  35. package/dist/cjs/transformer/index.js +12 -0
  36. package/dist/cjs/transformer/ts-transformer.d.ts +29 -0
  37. package/dist/cjs/transformer/ts-transformer.js +109 -0
  38. package/dist/cjs/transformer/vite-plugin.d.ts +18 -0
  39. package/dist/cjs/transformer/vite-plugin.js +72 -0
  40. package/dist/esm/core/emitArrayOrTuple.d.ts +1 -1
  41. package/dist/esm/core/emitLiteralOrEnum.d.ts +1 -1
  42. package/dist/esm/core/emitMapperFromSpec.d.ts +33 -0
  43. package/dist/esm/core/emitMapperFromSpec.js +189 -0
  44. package/dist/esm/core/emitObject.d.ts +1 -1
  45. package/dist/esm/core/emitObject.js +4 -2
  46. package/dist/esm/core/emitPrimitive.d.ts +1 -1
  47. package/dist/esm/core/emitUnionOrIntersection.d.ts +1 -1
  48. package/dist/esm/core/path.d.ts +9 -0
  49. package/dist/esm/core/path.js +36 -0
  50. package/dist/esm/generator/generate-jsdoc.d.ts +13 -0
  51. package/dist/esm/generator/generate-jsdoc.js +97 -0
  52. package/dist/esm/generator/index.d.ts +1 -0
  53. package/dist/esm/generator/index.js +1 -0
  54. package/dist/esm/index.d.ts +4 -3
  55. package/dist/esm/index.js +1 -0
  56. package/dist/esm/mapper/index.d.ts +1 -0
  57. package/dist/esm/mapper/index.js +1 -0
  58. package/dist/esm/runtime/index.d.ts +2 -0
  59. package/dist/esm/runtime/index.js +2 -0
  60. package/dist/esm/runtime/mapper.d.ts +72 -0
  61. package/dist/esm/runtime/mapper.js +71 -0
  62. package/dist/esm/transformer/index.d.ts +3 -0
  63. package/dist/esm/transformer/index.js +3 -0
  64. package/dist/esm/transformer/ts-transformer.d.ts +8 -4
  65. package/dist/esm/transformer/ts-transformer.js +51 -10
  66. package/dist/esm/transformer/vite-plugin.js +7 -32
  67. package/docs/build-integrations.md +89 -0
  68. package/docs/jsdoc-generation.md +105 -0
  69. package/docs/mapper.md +104 -0
  70. package/docs/mapping-policy.md +78 -0
  71. package/docs/runtime-validation.md +84 -0
  72. package/package.json +76 -36
@@ -0,0 +1,189 @@
1
+ import ts from "typescript";
2
+ import { emitGuardFromType } from "./index.js";
3
+ import { emitPathAccess } from "./path.js";
4
+ export function emitMapperFromSpec(params) {
5
+ // Resolve the concrete object literal so generated code does not retain DSL calls.
6
+ const specObject = resolveMapSpecObject(params.checker, params.specNode);
7
+ if (!specObject)
8
+ return null;
9
+ handleMapPolicyViolations(findMapPolicyViolations(params.checker, specObject, params.options?.mappingPolicy), params.options?.policyMode ?? "warn");
10
+ const rules = readMapRules(params.checker, specObject);
11
+ const props = params.checker.getPropertiesOfType(params.domainType);
12
+ const fields = [];
13
+ for (const prop of props) {
14
+ const rule = rules.get(prop.name);
15
+ if (!rule)
16
+ return null;
17
+ fields.push(`${JSON.stringify(prop.name)}:R(${JSON.stringify(prop.name)},${emitPathAccess("input", rule.from)})`);
18
+ }
19
+ const specText = _emitRuntimeSpecText(specObject, params.sourceFile);
20
+ const dtoGuard = params.options?.validateDto === false ? null : emitGuardFromType(params.checker, params.dtoType);
21
+ const domainGuard = params.options?.validateDomain === false ? null : emitGuardFromType(params.checker, params.domainType);
22
+ return [
23
+ `(function(){const S=${specText};`,
24
+ dtoGuard ? `const VD=${dtoGuard};` : "",
25
+ domainGuard ? `const VO=${domainGuard};` : "",
26
+ `return(input)=>{`,
27
+ dtoGuard ? `if(!VD(input))throw new TypeError("[runtypex] DTO validation failed.");` : "",
28
+ `const R=(key,raw)=>{const rule=S[key];const value=raw===undefined&&Object.prototype.hasOwnProperty.call(rule,"default")?rule.default:raw;return typeof rule.transform==="function"?rule.transform(value,input):value;};`,
29
+ `const output={${fields.join(",")}};`,
30
+ domainGuard ? `if(!VO(output))throw new TypeError("[runtypex] Domain validation failed.");` : "",
31
+ `return output;};})()`,
32
+ ].join("");
33
+ }
34
+ export function readMapRules(checker, specNode) {
35
+ const object = resolveMapSpecObject(checker, specNode);
36
+ const rules = new Map();
37
+ if (!object)
38
+ return rules;
39
+ for (const prop of object.properties) {
40
+ if (!ts.isPropertyAssignment(prop))
41
+ continue;
42
+ const key = _propertyName(prop.name);
43
+ const rule = _readRule(prop.initializer);
44
+ if (key && rule)
45
+ rules.set(key, { key, ...rule });
46
+ }
47
+ return rules;
48
+ }
49
+ export function findMapPolicyViolations(checker, specNode, policyNode) {
50
+ if (!policyNode)
51
+ return [];
52
+ const rules = readMapRules(checker, specNode);
53
+ const policyRules = readMapRules(checker, policyNode);
54
+ const canonicalByPath = new Map();
55
+ const violations = [];
56
+ for (const rule of policyRules.values()) {
57
+ const existing = canonicalByPath.get(rule.from);
58
+ if (existing && existing !== rule.key) {
59
+ violations.push({ from: rule.from, expectedKey: existing, actualKey: rule.key });
60
+ continue;
61
+ }
62
+ canonicalByPath.set(rule.from, rule.key);
63
+ }
64
+ violations.push(...Array.from(rules.values()).flatMap((rule) => {
65
+ const expected = canonicalByPath.get(rule.from);
66
+ return expected && expected !== rule.key
67
+ ? [{ from: rule.from, expectedKey: expected, actualKey: rule.key }]
68
+ : [];
69
+ }));
70
+ return violations;
71
+ }
72
+ export function handleMapPolicyViolations(violations, mode) {
73
+ if (!violations.length)
74
+ return;
75
+ const details = violations
76
+ .map((item) => `DTO path "${item.from}" is canonically mapped as "${item.expectedKey}", but this map uses "${item.actualKey}".`)
77
+ .join("\n");
78
+ const message = `[runtypex/mapper] Mapping policy violation:\n${details}`;
79
+ if (mode === "error")
80
+ throw new Error(message);
81
+ console.warn(message);
82
+ }
83
+ /** Finds the mapping object behind inline, defineMap-wrapped, or identifier specs. */
84
+ export function resolveMapSpecObject(checker, node) {
85
+ const expr = _skip(node);
86
+ if (ts.isObjectLiteralExpression(expr))
87
+ return expr;
88
+ if (ts.isCallExpression(expr) && expr.arguments[0]) {
89
+ const arg = _skip(expr.arguments[0]);
90
+ if (ts.isObjectLiteralExpression(arg))
91
+ return arg;
92
+ }
93
+ if (ts.isCallExpression(expr) && ts.isCallExpression(expr.expression)) {
94
+ return resolveMapSpecObject(checker, expr.expression);
95
+ }
96
+ if (ts.isIdentifier(expr)) {
97
+ const symbol = checker.getShorthandAssignmentValueSymbol?.(expr) ?? checker.getSymbolAtLocation(expr);
98
+ const declaration = symbol?.valueDeclaration ?? symbol?.declarations?.[0];
99
+ if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) {
100
+ return resolveMapSpecObject(checker, declaration.initializer);
101
+ }
102
+ const variable = _findVariableDeclaration(expr.getSourceFile(), expr.text, expr.getStart(expr.getSourceFile()));
103
+ if (variable?.initializer)
104
+ return resolveMapSpecObject(checker, variable.initializer);
105
+ }
106
+ return null;
107
+ }
108
+ function _readRule(node) {
109
+ const expr = _skip(node);
110
+ if (ts.isObjectLiteralExpression(expr)) {
111
+ return _readRuleObject(expr);
112
+ }
113
+ if (ts.isCallExpression(expr) && expr.arguments[0]) {
114
+ const from = _stringValue(expr.arguments[0]);
115
+ const metadata = expr.arguments.length > 2 ? expr.arguments[2] : expr.arguments[1];
116
+ if (!from)
117
+ return null;
118
+ return { from, ..._readMetadata(metadata) };
119
+ }
120
+ return null;
121
+ }
122
+ function _readRuleObject(object) {
123
+ const from = _readStringProperty(object, "from");
124
+ if (!from)
125
+ return null;
126
+ return {
127
+ from,
128
+ db: _readStringProperty(object, "db") ?? undefined,
129
+ description: _readStringProperty(object, "description") ?? undefined,
130
+ dtoDescription: _readStringProperty(object, "dtoDescription") ?? undefined,
131
+ };
132
+ }
133
+ function _readMetadata(node) {
134
+ const expr = node ? _skip(node) : null;
135
+ if (!expr || !ts.isObjectLiteralExpression(expr))
136
+ return {};
137
+ return {
138
+ db: _readStringProperty(expr, "db") ?? undefined,
139
+ description: _readStringProperty(expr, "description") ?? undefined,
140
+ dtoDescription: _readStringProperty(expr, "dtoDescription") ?? undefined,
141
+ };
142
+ }
143
+ function _readStringProperty(object, name) {
144
+ const prop = object.properties.find((item) => ts.isPropertyAssignment(item) && _propertyName(item.name) === name);
145
+ return prop ? _stringValue(prop.initializer) : null;
146
+ }
147
+ function _skip(node) {
148
+ let expr = node;
149
+ while (ts.isParenthesizedExpression(expr) || ts.isAsExpression(expr) || ts.isTypeAssertionExpression(expr)) {
150
+ expr = expr.expression;
151
+ }
152
+ return expr;
153
+ }
154
+ function _propertyName(name) {
155
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name))
156
+ return name.text;
157
+ return null;
158
+ }
159
+ function _stringValue(node) {
160
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
161
+ return node.text;
162
+ return null;
163
+ }
164
+ function _findVariableDeclaration(sourceFile, name, before) {
165
+ let found = null;
166
+ const visit = (node) => {
167
+ if (ts.isVariableDeclaration(node) &&
168
+ ts.isIdentifier(node.name) &&
169
+ node.name.text === name &&
170
+ node.getStart(sourceFile) < before) {
171
+ found = node;
172
+ }
173
+ node.forEachChild(visit);
174
+ };
175
+ visit(sourceFile);
176
+ return found;
177
+ }
178
+ function _emitRuntimeSpecText(specObject, sourceFile) {
179
+ // Remove TypeScript-only syntax from inline transform callbacks before embedding.
180
+ const marker = "__runtypexSpec";
181
+ const output = ts.transpileModule(`const ${marker} = ${specObject.getText(sourceFile)};`, {
182
+ compilerOptions: {
183
+ module: ts.ModuleKind.ESNext,
184
+ target: ts.ScriptTarget.ESNext,
185
+ },
186
+ }).outputText.trim();
187
+ const prefix = `const ${marker} = `;
188
+ return output.startsWith(prefix) ? output.slice(prefix.length).replace(/;$/, "") : specObject.getText(sourceFile);
189
+ }
@@ -1,5 +1,5 @@
1
1
  import ts from "typescript";
2
- import type { GenContext } from "./index";
2
+ import type { GenContext } from "./index.js";
3
3
  /**
4
4
  * Handles interfaces, classes, and object-like structures.
5
5
  */
@@ -1,4 +1,5 @@
1
1
  import ts from "typescript";
2
+ import { emitPropertyAccess } from "./path.js";
2
3
  /**
3
4
  * Handles interfaces, classes, and object-like structures.
4
5
  */
@@ -14,8 +15,9 @@ export function emitObject(ctx, expr, t) {
14
15
  continue;
15
16
  const propType = ctx.checker.getTypeOfSymbolAtLocation(prop, declaration);
16
17
  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;
18
+ const propExpr = emitPropertyAccess(expr, prop.name);
19
+ const condition = ctx.emit(propExpr, propType);
20
+ const checkExpr = isOptional ? `(${propExpr}===undefined||${condition})` : condition;
19
21
  parts.push(checkExpr);
20
22
  }
21
23
  return `(${parts.join("&&")})`;
@@ -1,5 +1,5 @@
1
1
  import ts from "typescript";
2
- import type { GenContext } from "./index";
2
+ import type { GenContext } from "./index.js";
3
3
  /**
4
4
  * Handles primitive types like number, string, boolean...
5
5
  */
@@ -1,5 +1,5 @@
1
1
  import ts from "typescript";
2
- import type { GenContext } from "./index";
2
+ import type { GenContext } from "./index.js";
3
3
  /**
4
4
  * Handles union (A | B) and intersection (A & B) types.
5
5
  */
@@ -0,0 +1,9 @@
1
+ export type PathSegment = string | number;
2
+ /** Splits a mapper path into object keys and numeric array indexes. */
3
+ export declare function parsePath(path: string): PathSegment[];
4
+ /** Runtime fallback reader used when mapper calls are not transformed. */
5
+ export declare function getByPath(value: unknown, path: string): unknown;
6
+ /** Emits bracket-only access for user-authored DTO paths. */
7
+ export declare function emitPathAccess(root: string, path: string): string;
8
+ /** Emits compact dot access when safe, with bracket fallback for quoted keys. */
9
+ export declare function emitPropertyAccess(root: string, property: string | number): string;
@@ -0,0 +1,36 @@
1
+ /** Splits a mapper path into object keys and numeric array indexes. */
2
+ export function parsePath(path) {
3
+ if (!path)
4
+ return [];
5
+ return path.split(".").map((segment) => {
6
+ if (/^(0|[1-9]\d*)$/.test(segment))
7
+ return Number(segment);
8
+ return segment;
9
+ });
10
+ }
11
+ /** Runtime fallback reader used when mapper calls are not transformed. */
12
+ export function getByPath(value, path) {
13
+ let current = value;
14
+ for (const segment of parsePath(path)) {
15
+ if (current == null)
16
+ return undefined;
17
+ current = current[segment];
18
+ }
19
+ return current;
20
+ }
21
+ /** Emits bracket-only access for user-authored DTO paths. */
22
+ export function emitPathAccess(root, path) {
23
+ return parsePath(path).reduce((expr, segment) => {
24
+ if (typeof segment === "number")
25
+ return `${expr}[${segment}]`;
26
+ return `${expr}[${JSON.stringify(segment)}]`;
27
+ }, root);
28
+ }
29
+ /** Emits compact dot access when safe, with bracket fallback for quoted keys. */
30
+ export function emitPropertyAccess(root, property) {
31
+ if (typeof property === "number")
32
+ return `${root}[${property}]`;
33
+ if (/^[A-Za-z_$][\w$]*$/.test(property))
34
+ return `${root}.${property}`;
35
+ return `${root}[${JSON.stringify(property)}]`;
36
+ }
@@ -0,0 +1,13 @@
1
+ import ts from "typescript";
2
+ export type GenerateJSDocOptions = {
3
+ name?: string;
4
+ mappingPolicy?: ts.Expression;
5
+ policyMode?: "warn" | "error";
6
+ };
7
+ export declare function generateJSDocFromSpec(params: {
8
+ checker: ts.TypeChecker;
9
+ dtoType: ts.Type;
10
+ domainType: ts.Type;
11
+ specNode: ts.Expression;
12
+ options?: GenerateJSDocOptions;
13
+ }): string;
@@ -0,0 +1,97 @@
1
+ import ts from "typescript";
2
+ import { parsePath } from "../core/path.js";
3
+ import { findMapPolicyViolations, handleMapPolicyViolations, readMapRules } from "../core/emitMapperFromSpec.js";
4
+ const JSDOC_CONTENT_WIDTH = 76;
5
+ export function generateJSDocFromSpec(params) {
6
+ const checker = params.checker;
7
+ const dtoName = params.dtoType.symbol?.name ?? "Dto";
8
+ const name = params.options?.name ?? params.domainType.symbol?.name ?? "GeneratedDomain";
9
+ const rules = readMapRules(checker, params.specNode);
10
+ const lines = [`export interface ${name} {`];
11
+ handleMapPolicyViolations(findMapPolicyViolations(checker, params.specNode, params.options?.mappingPolicy), params.options?.policyMode ?? "warn");
12
+ // Each domain property gets source metadata that editors can show on hover.
13
+ for (const prop of checker.getPropertiesOfType(params.domainType)) {
14
+ const rule = rules.get(prop.name);
15
+ if (!rule)
16
+ throw new Error(`[runtypex/generator] ${name}.${prop.name} is not mapped.`);
17
+ const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
18
+ const domainType = declaration ? checker.getTypeOfSymbolAtLocation(prop, declaration) : checker.getAnyType();
19
+ const dtoPathType = _getTypeAtPath(checker, params.dtoType, rule.from);
20
+ const description = _getDomainDescription(checker, prop) ?? rule.description;
21
+ const optional = (prop.getFlags() & ts.SymbolFlags.Optional) !== 0 ? "?" : "";
22
+ lines.push(" /**");
23
+ if (description) {
24
+ _pushJSDocText(lines, _escapeComment(description));
25
+ lines.push(" *");
26
+ }
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");
32
+ if (rule.db)
33
+ _pushJSDocField(lines, "DB", _escapeComment(rule.db));
34
+ _pushJSDocField(lines, "Domain type", checker.typeToString(domainType));
35
+ lines.push(" */");
36
+ lines.push(` ${_propertyName(prop.name)}${optional}: ${checker.typeToString(domainType)};`);
37
+ lines.push("");
38
+ }
39
+ lines.push("}");
40
+ return lines.join("\n");
41
+ }
42
+ /** Resolves the DTO type reached by a mapping path such as profile.name or items.0.id. */
43
+ function _getTypeAtPath(checker, root, path) {
44
+ let current = root;
45
+ for (const segment of parsePath(path)) {
46
+ if (!current)
47
+ return null;
48
+ if (typeof segment === "number") {
49
+ current =
50
+ checker.getElementTypeOfArrayType?.(current) ??
51
+ current.typeArguments?.[segment] ??
52
+ current.getNumberIndexType?.() ??
53
+ null;
54
+ continue;
55
+ }
56
+ const prop = checker.getPropertyOfType(current, segment);
57
+ const declaration = prop?.valueDeclaration ?? prop?.declarations?.[0];
58
+ current = prop && declaration ? checker.getTypeOfSymbolAtLocation(prop, declaration) : null;
59
+ }
60
+ return current;
61
+ }
62
+ function _propertyName(name) {
63
+ return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
64
+ }
65
+ function _escapeComment(value) {
66
+ return value.replace(/\*\//g, "* /");
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
+ }
@@ -0,0 +1 @@
1
+ export * from "./generate-jsdoc.js";
@@ -0,0 +1 @@
1
+ export * from "./generate-jsdoc.js";
@@ -1,3 +1,4 @@
1
- export { makeValidate, makeAssert, type ValidateFn, type AssertFn } from "./runtime/validate";
2
- export { default as vitePlugin } from "./transformer/vite-plugin";
3
- export { default as tsTransformer } from "./transformer/ts-transformer";
1
+ export { makeValidate, makeAssert, type ValidateFn, type AssertFn } from "./runtime/validate.js";
2
+ export { defineMap, defineMappingPolicy, mapperHelpers, makeMapper, source, transform, type DefinedMap, type Mapper, type MapperOptions, type MapperMetadata, type MapRule, type MapSpec, type MappingPolicy, type MappingPolicyMode, type PathOf, } from "./runtime/mapper.js";
3
+ export { default as vitePlugin } from "./transformer/vite-plugin.js";
4
+ export { default as tsTransformer } from "./transformer/ts-transformer.js";
package/dist/esm/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { makeValidate, makeAssert } from "./runtime/validate.js";
2
+ export { defineMap, defineMappingPolicy, mapperHelpers, makeMapper, source, transform, } from "./runtime/mapper.js";
2
3
  export { default as vitePlugin } from "./transformer/vite-plugin.js";
3
4
  export { default as tsTransformer } from "./transformer/ts-transformer.js";
@@ -0,0 +1 @@
1
+ export * from "../runtime/mapper.js";
@@ -0,0 +1 @@
1
+ export * from "../runtime/mapper.js";
@@ -0,0 +1,2 @@
1
+ export * from "./validate.js";
2
+ export * from "./mapper.js";
@@ -0,0 +1,2 @@
1
+ export * from "./validate.js";
2
+ export * from "./mapper.js";
@@ -0,0 +1,72 @@
1
+ type Primitive = string | number | boolean | bigint | symbol | null | undefined | Date;
2
+ export type PathOf<T> = T extends Primitive ? never : T extends readonly (infer U)[] ? `${number}` | `${number}.${PathOf<U>}` : {
3
+ [K in Extract<keyof T, string>]: T[K] extends Primitive ? K : T[K] extends readonly (infer U)[] ? K | `${K}.${number}` | `${K}.${number}.${PathOf<U>}` : K | `${K}.${PathOf<T[K]>}`;
4
+ }[Extract<keyof T, string>];
5
+ export type Mapper<TDto, TDomain> = (dto: TDto) => TDomain;
6
+ export type MapperMetadata<TValue = never> = {
7
+ db?: string;
8
+ /** @deprecated Prefer domain property JSDoc for domain field descriptions. */
9
+ description?: string;
10
+ dtoDescription?: string;
11
+ default?: TValue;
12
+ };
13
+ export type MapRule<TDto, TValue> = MapperMetadata<TValue> & {
14
+ from: PathOf<TDto>;
15
+ transform?: (value: unknown, dto: TDto) => TValue;
16
+ };
17
+ export type MapSpec<TDto, TDomain> = {
18
+ [K in keyof TDomain]-?: MapRule<TDto, TDomain[K]>;
19
+ };
20
+ export type MappingPolicy<TDto> = Record<string, MapRule<TDto, unknown>>;
21
+ export type MappingPolicyMode = "warn" | "error";
22
+ export type MapperOptions<TDto> = {
23
+ policy?: MappingPolicy<TDto>;
24
+ policyMode?: MappingPolicyMode;
25
+ };
26
+ declare const DTO_TYPE: unique symbol;
27
+ declare const DOMAIN_TYPE: unique symbol;
28
+ export type DefinedMap<TDto, TDomain> = MapSpec<TDto, TDomain> & {
29
+ readonly [DTO_TYPE]?: TDto;
30
+ readonly [DOMAIN_TYPE]?: TDomain;
31
+ };
32
+ export declare function defineMap<TDto, TDomain>(): <const TSpec extends MapSpec<TDto, TDomain>>(spec: TSpec) => TSpec & DefinedMap<TDto, TDomain>;
33
+ /** Declares canonical DTO path -> Domain field names for consistency checks. */
34
+ export declare function defineMappingPolicy<TDto>(): <const TSpec extends MappingPolicy<TDto>>(spec: TSpec) => TSpec;
35
+ /** Shorthand rule for direct DTO path reads. */
36
+ export declare function source<const TPath extends string, TValue = never>(from: TPath, metadata?: MapperMetadata<TValue>): {
37
+ db?: string;
38
+ description?: string;
39
+ dtoDescription?: string;
40
+ default?: TValue | undefined;
41
+ from: TPath;
42
+ };
43
+ /** Shorthand rule for DTO path reads that require a value conversion. */
44
+ export declare function transform<const TPath extends string, TValue>(from: TPath, transform: (value: unknown, dto: unknown) => TValue, metadata?: MapperMetadata<TValue>): {
45
+ db?: string;
46
+ description?: string;
47
+ dtoDescription?: string;
48
+ default?: TValue | undefined;
49
+ from: TPath;
50
+ transform: (value: unknown, dto: unknown) => TValue;
51
+ };
52
+ /** Typed helpers for callbacks that need access to the source DTO shape. */
53
+ export declare function mapperHelpers<TDto>(): {
54
+ source: <const TPath extends PathOf<TDto>, TValue = never>(from: TPath, metadata?: MapperMetadata<TValue>) => {
55
+ db?: string;
56
+ description?: string;
57
+ dtoDescription?: string;
58
+ default?: TValue | undefined;
59
+ from: TPath;
60
+ };
61
+ transform: <const TPath extends PathOf<TDto>, TValue>(from: TPath, transform: (value: unknown, dto: TDto) => TValue, metadata?: MapperMetadata<TValue>) => {
62
+ db?: string;
63
+ description?: string;
64
+ dtoDescription?: string;
65
+ default?: TValue | undefined;
66
+ from: TPath;
67
+ transform: (value: unknown, dto: TDto) => TValue;
68
+ };
69
+ };
70
+ /** Runtime interpreter used as fallback when the transformer is not configured. */
71
+ export declare function makeMapper<TDto, TDomain>(spec: DefinedMap<TDto, TDomain> | MapSpec<TDto, TDomain>, options?: MapperOptions<TDto>): Mapper<TDto, TDomain>;
72
+ export {};
@@ -0,0 +1,71 @@
1
+ import { getByPath } from "../core/path.js";
2
+ export function defineMap() {
3
+ return (spec) => spec;
4
+ }
5
+ /** Declares canonical DTO path -> Domain field names for consistency checks. */
6
+ export function defineMappingPolicy() {
7
+ return (spec) => spec;
8
+ }
9
+ /** Shorthand rule for direct DTO path reads. */
10
+ export function source(from, metadata) {
11
+ return { from, ...metadata };
12
+ }
13
+ /** Shorthand rule for DTO path reads that require a value conversion. */
14
+ export function transform(from, transform, metadata) {
15
+ return { from, transform, ...metadata };
16
+ }
17
+ /** Typed helpers for callbacks that need access to the source DTO shape. */
18
+ export function mapperHelpers() {
19
+ return {
20
+ source: (from, metadata) => source(from, metadata),
21
+ transform: (from, transform, metadata) => ({ from, transform, ...metadata }),
22
+ };
23
+ }
24
+ /** Runtime interpreter used as fallback when the transformer is not configured. */
25
+ export function makeMapper(spec, options) {
26
+ _handlePolicyViolations(_findPolicyViolations(spec, options?.policy), options?.policyMode ?? "warn");
27
+ return ((dto) => {
28
+ const output = {};
29
+ for (const key of Object.keys(spec)) {
30
+ const rule = spec[key];
31
+ const raw = getByPath(dto, String(rule.from));
32
+ const value = raw === undefined && _hasOwn(rule, "default") ? rule.default : raw;
33
+ output[key] = rule.transform ? rule.transform(value, dto) : value;
34
+ }
35
+ return output;
36
+ });
37
+ }
38
+ function _hasOwn(value, key) {
39
+ return Object.prototype.hasOwnProperty.call(value, key);
40
+ }
41
+ function _findPolicyViolations(spec, policy) {
42
+ if (!policy)
43
+ return [];
44
+ const canonicalByPath = new Map();
45
+ const violations = [];
46
+ for (const key of Object.keys(policy)) {
47
+ const from = String(policy[key].from);
48
+ const existing = canonicalByPath.get(from);
49
+ if (existing && existing !== key) {
50
+ violations.push(`DTO path "${from}" is canonically mapped as "${existing}", but this map uses "${key}".`);
51
+ continue;
52
+ }
53
+ canonicalByPath.set(from, key);
54
+ }
55
+ violations.push(...Object.keys(spec).flatMap((key) => {
56
+ const from = String(spec[key].from);
57
+ const expected = canonicalByPath.get(from);
58
+ return expected && expected !== key
59
+ ? [`DTO path "${from}" is canonically mapped as "${expected}", but this map uses "${key}".`]
60
+ : [];
61
+ }));
62
+ return violations;
63
+ }
64
+ function _handlePolicyViolations(violations, mode) {
65
+ if (!violations.length)
66
+ return;
67
+ const message = `[runtypex/mapper] Mapping policy violation:\n${violations.join("\n")}`;
68
+ if (mode === "error")
69
+ throw new Error(message);
70
+ console.warn(message);
71
+ }
@@ -0,0 +1,3 @@
1
+ export { default } from "./ts-transformer.js";
2
+ export { default as tsTransformer } from "./ts-transformer.js";
3
+ export { default as vitePlugin } from "./vite-plugin.js";
@@ -0,0 +1,3 @@
1
+ export { default } from "./ts-transformer.js";
2
+ export { default as tsTransformer } from "./ts-transformer.js";
3
+ export { default as vitePlugin } from "./vite-plugin.js";
@@ -1,4 +1,10 @@
1
1
  import ts from "typescript";
2
+ type TransformerOptions = {
3
+ program: ts.Program;
4
+ removeInProd?: boolean;
5
+ validateDto?: boolean;
6
+ validateDomain?: boolean;
7
+ };
2
8
  /**
3
9
  * 🧩 tsTransformer
4
10
  * TypeScript custom transformer (BEFORE) factory.
@@ -19,7 +25,5 @@ import ts from "typescript";
19
25
  * ✅ Validation logic embedded at build-time
20
26
  * ✅ Optionally removed in production builds
21
27
  */
22
- export default function tsTransformer(options: {
23
- program: ts.Program;
24
- removeInProd?: boolean;
25
- }): (context: ts.TransformationContext) => (sf: ts.SourceFile) => ts.Node | undefined;
28
+ export default function tsTransformer(options: TransformerOptions): ts.TransformerFactory<ts.SourceFile>;
29
+ export {};