runtypex 0.1.12 → 0.2.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.
Files changed (74) hide show
  1. package/README.md +112 -69
  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 +32 -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 +69 -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 +71 -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 +32 -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 +63 -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 +71 -0
  61. package/dist/esm/runtime/mapper.js +71 -0
  62. package/dist/esm/transformer/helper.d.ts +2 -0
  63. package/dist/esm/transformer/helper.js +57 -0
  64. package/dist/esm/transformer/index.d.ts +3 -0
  65. package/dist/esm/transformer/index.js +3 -0
  66. package/dist/esm/transformer/ts-transformer.d.ts +8 -4
  67. package/dist/esm/transformer/ts-transformer.js +46 -55
  68. package/dist/esm/transformer/vite-plugin.js +7 -93
  69. package/docs/build-integrations.md +89 -0
  70. package/docs/jsdoc-generation.md +88 -0
  71. package/docs/mapper.md +104 -0
  72. package/docs/mapping-policy.md +78 -0
  73. package/docs/runtime-validation.md +84 -0
  74. package/package.json +76 -36
@@ -0,0 +1,109 @@
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 index_js_1 = require("../core/index.js");
9
+ const emitMapperFromSpec_js_1 = require("../core/emitMapperFromSpec.js");
10
+ /**
11
+ * 🧩 tsTransformer
12
+ * TypeScript custom transformer (BEFORE) factory.
13
+ *
14
+ * 📘 Usage (ts-loader / ttypescript):
15
+ * ```ts
16
+ * getCustomTransformers: (program) => ({
17
+ * before: [ tsTransformer({ program, removeInProd: true }) ]
18
+ * })
19
+ * ```
20
+ *
21
+ * 🧠 Purpose:
22
+ * - Replace makeValidate<T>(), makeAssert<T>() calls
23
+ * with *pre-generated runtime validation code* derived from T.
24
+ *
25
+ * 💡 Effect:
26
+ * ✅ No reflection or runtime type parsing
27
+ * ✅ Validation logic embedded at build-time
28
+ * ✅ Optionally removed in production builds
29
+ */
30
+ function tsTransformer(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
+ if ((name === "makeValidate" || name === "makeAssert") && node.typeArguments?.length) {
39
+ const type = checker.getTypeFromTypeNode(node.typeArguments[0]);
40
+ const isRemovedInProd = removeInProd && prod;
41
+ switch (name) {
42
+ case "makeValidate":
43
+ return _emitMakeValidate(checker, type, isRemovedInProd);
44
+ case "makeAssert":
45
+ return _emitMakeAssert(checker, type, isRemovedInProd);
46
+ }
47
+ }
48
+ // makeMapper<TDto, TDomain>(spec) becomes an inline validating mapper.
49
+ if (name === "makeMapper" && node.typeArguments?.length === 2 && node.arguments[0]) {
50
+ const mapperCallOptions = _readMapperCallOptions(node.arguments[1]);
51
+ const mapper = (0, emitMapperFromSpec_js_1.emitMapperFromSpec)({
52
+ checker,
53
+ dtoType: checker.getTypeFromTypeNode(node.typeArguments[0]),
54
+ domainType: checker.getTypeFromTypeNode(node.typeArguments[1]),
55
+ specNode: node.arguments[0],
56
+ sourceFile: node.getSourceFile(),
57
+ options: {
58
+ validateDto: !(removeInProd && prod) && options.validateDto !== false,
59
+ validateDomain: !(removeInProd && prod) && options.validateDomain !== false,
60
+ mappingPolicy: mapperCallOptions.policy,
61
+ policyMode: mapperCallOptions.policyMode,
62
+ },
63
+ });
64
+ if (mapper)
65
+ return typescript_1.default.factory.createIdentifier(mapper);
66
+ }
67
+ }
68
+ return typescript_1.default.visitEachChild(node, visit, context);
69
+ };
70
+ return (sf) => typescript_1.default.visitNode(sf, visit);
71
+ };
72
+ }
73
+ function _readMapperCallOptions(node) {
74
+ if (!node)
75
+ return {};
76
+ const expr = typescript_1.default.isAsExpression(node) || typescript_1.default.isParenthesizedExpression(node) ? node.expression : node;
77
+ if (!typescript_1.default.isObjectLiteralExpression(expr))
78
+ return {};
79
+ return {
80
+ policy: _readExpressionProperty(expr, "policy"),
81
+ policyMode: _readPolicyMode(expr),
82
+ };
83
+ }
84
+ function _readExpressionProperty(object, name) {
85
+ for (const item of object.properties) {
86
+ if (typescript_1.default.isPropertyAssignment(item) && typescript_1.default.isIdentifier(item.name) && item.name.text === name) {
87
+ return item.initializer;
88
+ }
89
+ if (typescript_1.default.isShorthandPropertyAssignment(item) && item.name.text === name) {
90
+ return item.name;
91
+ }
92
+ }
93
+ return undefined;
94
+ }
95
+ function _readPolicyMode(object) {
96
+ const mode = _readExpressionProperty(object, "policyMode");
97
+ return mode && typescript_1.default.isStringLiteral(mode) && (mode.text === "warn" || mode.text === "error") ? mode.text : undefined;
98
+ }
99
+ function _emitMakeValidate(checker, type, isRemovedInProd) {
100
+ const guard = isRemovedInProd ? "((_)=>true)" : (0, index_js_1.emitGuardFromType)(checker, type);
101
+ return typescript_1.default.factory.createIdentifier(guard);
102
+ }
103
+ function _emitMakeAssert(checker, type, isRemovedInProd) {
104
+ if (isRemovedInProd)
105
+ return typescript_1.default.factory.createIdentifier("((_)=>{})");
106
+ const guard = (0, index_js_1.emitGuardFromType)(checker, type);
107
+ const txt = `(function(){const G=${guard};return(i)=>{if(!G(i))throw new TypeError("[runtypex] Validation failed.");};})()`;
108
+ return typescript_1.default.factory.createIdentifier(txt);
109
+ }
@@ -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,72 @@
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 ts_transformer_js_1 = __importDefault(require("./ts-transformer.js"));
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
+ return {
27
+ name: "vite-plugin-runtypex",
28
+ enforce: "pre",
29
+ transform(code, id) {
30
+ const isTS = id.endsWith(".ts") || id.endsWith(".tsx");
31
+ const isTargetFunction = /make(?:Validate|Assert|Mapper)</.test(code);
32
+ if (!isTS || !isTargetFunction)
33
+ return;
34
+ const { program } = _createProgramFor(id);
35
+ const sf = program.getSourceFile(id);
36
+ if (!sf)
37
+ return;
38
+ const result = typescript_1.default.transform(sf, [(0, ts_transformer_js_1.default)({ program, removeInProd })]);
39
+ const mutated = typescript_1.default.createPrinter().printFile(result.transformed[0]);
40
+ result.dispose();
41
+ return mutated === code ? null : { code: mutated, map: null };
42
+ },
43
+ };
44
+ }
45
+ // ──────────────────────────────────────────────
46
+ // ① createProgram & TypeChecker
47
+ // ──────────────────────────────────────────────
48
+ function _createProgramFor(file) {
49
+ const tsconfig = _findNearestTsconfig(node_path_1.default.dirname(file));
50
+ const cfg = typescript_1.default.readConfigFile(tsconfig, typescript_1.default.sys.readFile);
51
+ if (cfg.error)
52
+ throw new Error(typescript_1.default.flattenDiagnosticMessageText(cfg.error.messageText, "\n"));
53
+ const parsed = typescript_1.default.parseJsonConfigFileContent(cfg.config, typescript_1.default.sys, node_path_1.default.dirname(tsconfig));
54
+ const program = typescript_1.default.createProgram({ rootNames: parsed.fileNames, options: parsed.options });
55
+ return { program };
56
+ }
57
+ function _findNearestTsconfig(start) {
58
+ let dir = start;
59
+ while (true) {
60
+ const candidate = node_path_1.default.join(dir, "tsconfig.json");
61
+ if (typescript_1.default.sys.fileExists(candidate))
62
+ return candidate;
63
+ const parent = node_path_1.default.dirname(dir);
64
+ if (parent === dir)
65
+ break;
66
+ dir = parent;
67
+ }
68
+ const fallback = node_path_1.default.join(process.cwd(), "tsconfig.json");
69
+ if (typescript_1.default.sys.fileExists(fallback))
70
+ return fallback;
71
+ throw new Error("tsconfig.json not found");
72
+ }
@@ -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 array (T[]) and tuple ([A, B]) types.
5
5
  */
@@ -1,4 +1,4 @@
1
1
  import ts from "typescript";
2
- import type { GenContext } from "./index";
2
+ import type { GenContext } from "./index.js";
3
3
  /** Handles literal types and enum-like types. */
4
4
  export declare function emitLiteralOrEnum(_: GenContext, expr: string, t: ts.Type): string | null;
@@ -0,0 +1,32 @@
1
+ import ts from "typescript";
2
+ export type MapperEmitOptions = {
3
+ validateDto?: boolean;
4
+ validateDomain?: boolean;
5
+ mappingPolicy?: ts.Expression;
6
+ policyMode?: "warn" | "error";
7
+ };
8
+ export type MapRuleInfo = {
9
+ key: string;
10
+ from: string;
11
+ db?: string;
12
+ description?: string;
13
+ dtoDescription?: string;
14
+ };
15
+ export declare function emitMapperFromSpec(params: {
16
+ checker: ts.TypeChecker;
17
+ dtoType: ts.Type;
18
+ domainType: ts.Type;
19
+ specNode: ts.Expression;
20
+ sourceFile: ts.SourceFile;
21
+ options?: MapperEmitOptions;
22
+ }): string | null;
23
+ export declare function readMapRules(checker: ts.TypeChecker, specNode: ts.Expression): Map<string, MapRuleInfo>;
24
+ export type MapPolicyViolation = {
25
+ from: string;
26
+ expectedKey: string;
27
+ actualKey: string;
28
+ };
29
+ export declare function findMapPolicyViolations(checker: ts.TypeChecker, specNode: ts.Expression, policyNode: ts.Expression | undefined): MapPolicyViolation[];
30
+ export declare function handleMapPolicyViolations(violations: MapPolicyViolation[], mode: "warn" | "error"): void;
31
+ /** Finds the mapping object behind inline, defineMap-wrapped, or identifier specs. */
32
+ export declare function resolveMapSpecObject(checker: ts.TypeChecker, node: ts.Expression): ts.ObjectLiteralExpression | null;
@@ -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,63 @@
1
+ import ts from "typescript";
2
+ import { parsePath } from "../core/path.js";
3
+ import { findMapPolicyViolations, handleMapPolicyViolations, readMapRules } from "../core/emitMapperFromSpec.js";
4
+ export function generateJSDocFromSpec(params) {
5
+ const checker = params.checker;
6
+ const dtoName = params.dtoType.symbol?.name ?? "Dto";
7
+ const name = params.options?.name ?? params.domainType.symbol?.name ?? "GeneratedDomain";
8
+ const rules = readMapRules(checker, params.specNode);
9
+ const lines = [`export interface ${name} {`];
10
+ handleMapPolicyViolations(findMapPolicyViolations(checker, params.specNode, params.options?.mappingPolicy), params.options?.policyMode ?? "warn");
11
+ // Each domain property gets source metadata that editors can show on hover.
12
+ for (const prop of checker.getPropertiesOfType(params.domainType)) {
13
+ const rule = rules.get(prop.name);
14
+ if (!rule)
15
+ throw new Error(`[runtypex/generator] ${name}.${prop.name} is not mapped.`);
16
+ const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
17
+ const domainType = declaration ? checker.getTypeOfSymbolAtLocation(prop, declaration) : checker.getAnyType();
18
+ const dtoPathType = _getTypeAtPath(checker, params.dtoType, rule.from);
19
+ const optional = (prop.getFlags() & ts.SymbolFlags.Optional) !== 0 ? "?" : "";
20
+ lines.push(" /**");
21
+ if (rule.description) {
22
+ lines.push(` * ${_escapeComment(rule.description)}`);
23
+ lines.push(" *");
24
+ }
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"}`);
28
+ if (rule.db)
29
+ lines.push(` * DB: ${_escapeComment(rule.db)}`);
30
+ lines.push(` * Domain type: ${checker.typeToString(domainType)}`);
31
+ lines.push(" */");
32
+ lines.push(` ${_propertyName(prop.name)}${optional}: ${checker.typeToString(domainType)};`);
33
+ lines.push("");
34
+ }
35
+ lines.push("}");
36
+ return lines.join("\n");
37
+ }
38
+ /** Resolves the DTO type reached by a mapping path such as profile.name or items.0.id. */
39
+ function _getTypeAtPath(checker, root, path) {
40
+ let current = root;
41
+ for (const segment of parsePath(path)) {
42
+ if (!current)
43
+ return null;
44
+ if (typeof segment === "number") {
45
+ current =
46
+ checker.getElementTypeOfArrayType?.(current) ??
47
+ current.typeArguments?.[segment] ??
48
+ current.getNumberIndexType?.() ??
49
+ null;
50
+ continue;
51
+ }
52
+ const prop = checker.getPropertyOfType(current, segment);
53
+ const declaration = prop?.valueDeclaration ?? prop?.declarations?.[0];
54
+ current = prop && declaration ? checker.getTypeOfSymbolAtLocation(prop, declaration) : null;
55
+ }
56
+ return current;
57
+ }
58
+ function _propertyName(name) {
59
+ return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
60
+ }
61
+ function _escapeComment(value) {
62
+ return value.replace(/\*\//g, "* /");
63
+ }
@@ -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";