zodrift 0.1.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.
- package/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +50 -0
- package/dist/commands/check.d.ts +1 -0
- package/dist/commands/check.js +52 -0
- package/dist/commands/codegen.d.ts +1 -0
- package/dist/commands/codegen.js +63 -0
- package/dist/commands/fix.d.ts +1 -0
- package/dist/commands/fix.js +113 -0
- package/dist/commands/forms.d.ts +1 -0
- package/dist/commands/forms.js +41 -0
- package/dist/commands/openapi.d.ts +1 -0
- package/dist/commands/openapi.js +49 -0
- package/dist/core/checker.d.ts +2 -0
- package/dist/core/checker.js +39 -0
- package/dist/core/compare.d.ts +2 -0
- package/dist/core/compare.js +190 -0
- package/dist/core/emit.d.ts +10 -0
- package/dist/core/emit.js +229 -0
- package/dist/core/file-discovery.d.ts +5 -0
- package/dist/core/file-discovery.js +37 -0
- package/dist/core/pairing.d.ts +6 -0
- package/dist/core/pairing.js +27 -0
- package/dist/core/ts-parser.d.ts +5 -0
- package/dist/core/ts-parser.js +239 -0
- package/dist/core/type-utils.d.ts +16 -0
- package/dist/core/type-utils.js +139 -0
- package/dist/core/types.d.ts +133 -0
- package/dist/core/types.js +1 -0
- package/dist/core/zod-parser.d.ts +5 -0
- package/dist/core/zod-parser.js +321 -0
- package/dist/reporters/index.d.ts +2 -0
- package/dist/reporters/index.js +14 -0
- package/dist/reporters/json.d.ts +2 -0
- package/dist/reporters/json.js +18 -0
- package/dist/reporters/pretty.d.ts +2 -0
- package/dist/reporters/pretty.js +35 -0
- package/dist/reporters/sarif.d.ts +2 -0
- package/dist/reporters/sarif.js +85 -0
- package/dist/utils/args.d.ts +9 -0
- package/dist/utils/args.js +55 -0
- package/package.json +44 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import { cloneTypeNode, locationFromTs, makePrimitive, stripUndefinedFromUnion, unwrapOptional } from "./type-utils.js";
|
|
5
|
+
function isExported(node) {
|
|
6
|
+
const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
|
|
7
|
+
return !!modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
8
|
+
}
|
|
9
|
+
function getLocationFromNode(sourceFile, node) {
|
|
10
|
+
const pos = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
11
|
+
return locationFromTs(path.resolve(sourceFile.fileName), pos.line, pos.character);
|
|
12
|
+
}
|
|
13
|
+
function normalizePropertyType(node) {
|
|
14
|
+
const unwrapped = unwrapOptional(node);
|
|
15
|
+
let optional = unwrapped.optional;
|
|
16
|
+
let next = unwrapped.node;
|
|
17
|
+
if (next.kind === "union") {
|
|
18
|
+
const stripped = stripUndefinedFromUnion(next);
|
|
19
|
+
optional = optional || stripped.optional;
|
|
20
|
+
next = stripped.node;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
optional,
|
|
24
|
+
node: next,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function parseLiteralExpression(node) {
|
|
28
|
+
if (ts.isStringLiteral(node)) {
|
|
29
|
+
return { kind: "literal", value: node.text };
|
|
30
|
+
}
|
|
31
|
+
if (ts.isNumericLiteral(node)) {
|
|
32
|
+
return { kind: "literal", value: Number(node.text) };
|
|
33
|
+
}
|
|
34
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) {
|
|
35
|
+
return { kind: "literal", value: true };
|
|
36
|
+
}
|
|
37
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) {
|
|
38
|
+
return { kind: "literal", value: false };
|
|
39
|
+
}
|
|
40
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) {
|
|
41
|
+
return { kind: "literal", value: null };
|
|
42
|
+
}
|
|
43
|
+
return makePrimitive("unknown");
|
|
44
|
+
}
|
|
45
|
+
function wrapUnion(nodes) {
|
|
46
|
+
if (nodes.length === 1) {
|
|
47
|
+
return nodes[0];
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
kind: "union",
|
|
51
|
+
members: nodes,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function parseShapeObject(node, sourceFile, variableMap, seen) {
|
|
55
|
+
const properties = {};
|
|
56
|
+
for (const prop of node.properties) {
|
|
57
|
+
if (!ts.isPropertyAssignment(prop) && !ts.isShorthandPropertyAssignment(prop)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
let propName = "";
|
|
61
|
+
let initializer = null;
|
|
62
|
+
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
63
|
+
propName = prop.name.text;
|
|
64
|
+
initializer = prop.name;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
if (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) || ts.isNumericLiteral(prop.name)) {
|
|
68
|
+
propName = prop.name.text;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
propName = prop.name.getText(sourceFile);
|
|
72
|
+
}
|
|
73
|
+
initializer = prop.initializer;
|
|
74
|
+
}
|
|
75
|
+
if (!initializer) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const parsed = parseZodExpression(initializer, sourceFile, variableMap, seen);
|
|
79
|
+
const normalized = normalizePropertyType(parsed);
|
|
80
|
+
properties[propName] = {
|
|
81
|
+
name: propName,
|
|
82
|
+
optional: normalized.optional,
|
|
83
|
+
type: normalized.node,
|
|
84
|
+
location: getLocationFromNode(sourceFile, prop.name),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
kind: "object",
|
|
89
|
+
properties,
|
|
90
|
+
location: getLocationFromNode(sourceFile, node),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function parseZodFactory(method, args, sourceFile, variableMap, seen) {
|
|
94
|
+
switch (method) {
|
|
95
|
+
case "string":
|
|
96
|
+
return { kind: "primitive", name: "string" };
|
|
97
|
+
case "number":
|
|
98
|
+
return { kind: "primitive", name: "number" };
|
|
99
|
+
case "boolean":
|
|
100
|
+
return { kind: "primitive", name: "boolean" };
|
|
101
|
+
case "any":
|
|
102
|
+
return { kind: "primitive", name: "any" };
|
|
103
|
+
case "unknown":
|
|
104
|
+
return { kind: "primitive", name: "unknown" };
|
|
105
|
+
case "null":
|
|
106
|
+
return { kind: "primitive", name: "null" };
|
|
107
|
+
case "undefined":
|
|
108
|
+
return { kind: "primitive", name: "undefined" };
|
|
109
|
+
case "literal":
|
|
110
|
+
return args[0] ? parseLiteralExpression(args[0]) : makePrimitive("unknown");
|
|
111
|
+
case "object": {
|
|
112
|
+
if (args[0] && ts.isObjectLiteralExpression(args[0])) {
|
|
113
|
+
return parseShapeObject(args[0], sourceFile, variableMap, seen);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
kind: "object",
|
|
117
|
+
properties: {},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
case "array":
|
|
121
|
+
return {
|
|
122
|
+
kind: "array",
|
|
123
|
+
element: args[0]
|
|
124
|
+
? parseZodExpression(args[0], sourceFile, variableMap, seen)
|
|
125
|
+
: makePrimitive("unknown"),
|
|
126
|
+
};
|
|
127
|
+
case "union": {
|
|
128
|
+
const firstArg = args[0];
|
|
129
|
+
if (!firstArg || !ts.isArrayLiteralExpression(firstArg)) {
|
|
130
|
+
return makePrimitive("unknown");
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
kind: "union",
|
|
134
|
+
members: firstArg.elements.map((element) => parseZodExpression(element, sourceFile, variableMap, seen)),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
case "tuple": {
|
|
138
|
+
const firstArg = args[0];
|
|
139
|
+
if (!firstArg || !ts.isArrayLiteralExpression(firstArg)) {
|
|
140
|
+
return makePrimitive("unknown");
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
kind: "tuple",
|
|
144
|
+
items: firstArg.elements.map((element) => parseZodExpression(element, sourceFile, variableMap, seen)),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
case "enum": {
|
|
148
|
+
const firstArg = args[0];
|
|
149
|
+
if (!firstArg || !ts.isArrayLiteralExpression(firstArg)) {
|
|
150
|
+
return makePrimitive("unknown");
|
|
151
|
+
}
|
|
152
|
+
const members = firstArg.elements
|
|
153
|
+
.filter((element) => ts.isStringLiteral(element) || ts.isNumericLiteral(element))
|
|
154
|
+
.map((element) => ts.isStringLiteral(element)
|
|
155
|
+
? { kind: "literal", value: element.text }
|
|
156
|
+
: { kind: "literal", value: Number(element.text) });
|
|
157
|
+
return members.length > 0 ? wrapUnion(members) : makePrimitive("unknown");
|
|
158
|
+
}
|
|
159
|
+
case "optional": {
|
|
160
|
+
const inner = args[0]
|
|
161
|
+
? parseZodExpression(args[0], sourceFile, variableMap, seen)
|
|
162
|
+
: makePrimitive("unknown");
|
|
163
|
+
return {
|
|
164
|
+
kind: "optional",
|
|
165
|
+
inner,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
case "nullable": {
|
|
169
|
+
const inner = args[0]
|
|
170
|
+
? parseZodExpression(args[0], sourceFile, variableMap, seen)
|
|
171
|
+
: makePrimitive("unknown");
|
|
172
|
+
return wrapUnion([inner, { kind: "primitive", name: "null" }]);
|
|
173
|
+
}
|
|
174
|
+
default:
|
|
175
|
+
return makePrimitive("unknown");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function applyMethod(base, method, args, sourceFile, variableMap, seen) {
|
|
179
|
+
switch (method) {
|
|
180
|
+
case "optional":
|
|
181
|
+
return {
|
|
182
|
+
kind: "optional",
|
|
183
|
+
inner: base,
|
|
184
|
+
};
|
|
185
|
+
case "nullable":
|
|
186
|
+
return wrapUnion([base, { kind: "primitive", name: "null" }]);
|
|
187
|
+
case "nullish":
|
|
188
|
+
return {
|
|
189
|
+
kind: "optional",
|
|
190
|
+
inner: wrapUnion([base, { kind: "primitive", name: "null" }]),
|
|
191
|
+
};
|
|
192
|
+
case "array":
|
|
193
|
+
return {
|
|
194
|
+
kind: "array",
|
|
195
|
+
element: base,
|
|
196
|
+
};
|
|
197
|
+
case "or": {
|
|
198
|
+
const right = args[0]
|
|
199
|
+
? parseZodExpression(args[0], sourceFile, variableMap, seen)
|
|
200
|
+
: makePrimitive("unknown");
|
|
201
|
+
if (base.kind === "union") {
|
|
202
|
+
return {
|
|
203
|
+
kind: "union",
|
|
204
|
+
members: [...base.members, right],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
kind: "union",
|
|
209
|
+
members: [base, right],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
case "and": {
|
|
213
|
+
const right = args[0]
|
|
214
|
+
? parseZodExpression(args[0], sourceFile, variableMap, seen)
|
|
215
|
+
: makePrimitive("unknown");
|
|
216
|
+
return {
|
|
217
|
+
kind: "union",
|
|
218
|
+
members: [base, right],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
default:
|
|
222
|
+
// Refinement/metadata methods do not alter the structural type shape.
|
|
223
|
+
return base;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function parseZodExpression(expression, sourceFile, variableMap, seen) {
|
|
227
|
+
if (ts.isIdentifier(expression)) {
|
|
228
|
+
const referenced = variableMap.get(expression.text);
|
|
229
|
+
if (!referenced || !referenced.initializer || seen.has(expression.text)) {
|
|
230
|
+
return {
|
|
231
|
+
kind: "reference",
|
|
232
|
+
name: expression.text,
|
|
233
|
+
location: getLocationFromNode(sourceFile, expression),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
seen.add(expression.text);
|
|
237
|
+
const parsed = parseZodExpression(referenced.initializer, sourceFile, variableMap, seen);
|
|
238
|
+
seen.delete(expression.text);
|
|
239
|
+
return cloneTypeNode(parsed);
|
|
240
|
+
}
|
|
241
|
+
if (ts.isCallExpression(expression)) {
|
|
242
|
+
const callee = expression.expression;
|
|
243
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
244
|
+
const method = callee.name.text;
|
|
245
|
+
if (ts.isIdentifier(callee.expression) && callee.expression.text === "z") {
|
|
246
|
+
return {
|
|
247
|
+
...parseZodFactory(method, expression.arguments, sourceFile, variableMap, seen),
|
|
248
|
+
location: getLocationFromNode(sourceFile, expression),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const base = parseZodExpression(callee.expression, sourceFile, variableMap, seen);
|
|
252
|
+
return {
|
|
253
|
+
...applyMethod(base, method, expression.arguments, sourceFile, variableMap, seen),
|
|
254
|
+
location: getLocationFromNode(sourceFile, expression),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
if (ts.isIdentifier(callee) && callee.text === "z") {
|
|
258
|
+
return makePrimitive("unknown");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
kind: "primitive",
|
|
263
|
+
name: "unknown",
|
|
264
|
+
location: getLocationFromNode(sourceFile, expression),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function collectVariables(sourceFile) {
|
|
268
|
+
const variableMap = new Map();
|
|
269
|
+
for (const statement of sourceFile.statements) {
|
|
270
|
+
if (!ts.isVariableStatement(statement)) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
274
|
+
if (ts.isIdentifier(declaration.name)) {
|
|
275
|
+
variableMap.set(declaration.name.text, declaration);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return variableMap;
|
|
280
|
+
}
|
|
281
|
+
export function parseSchemaDeclarations(filePaths) {
|
|
282
|
+
const declarations = [];
|
|
283
|
+
const errors = [];
|
|
284
|
+
for (const filePath of filePaths) {
|
|
285
|
+
try {
|
|
286
|
+
const absPath = path.resolve(filePath);
|
|
287
|
+
const content = fs.readFileSync(absPath, "utf8");
|
|
288
|
+
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.ES2022, true, ts.ScriptKind.TS);
|
|
289
|
+
const variableMap = collectVariables(sourceFile);
|
|
290
|
+
for (const statement of sourceFile.statements) {
|
|
291
|
+
if (!ts.isVariableStatement(statement) || !isExported(statement)) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
295
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const name = declaration.name.text;
|
|
299
|
+
if (!name.endsWith("Schema")) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const parsed = parseZodExpression(declaration.initializer, sourceFile, variableMap, new Set());
|
|
303
|
+
declarations.push({
|
|
304
|
+
name,
|
|
305
|
+
node: parsed,
|
|
306
|
+
location: getLocationFromNode(sourceFile, declaration.name),
|
|
307
|
+
sourceFilePath: absPath,
|
|
308
|
+
objectNodeText: declaration.initializer.getText(sourceFile),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
errors.push(`Failed to parse schemas in ${filePath}: ${String(error)}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
declarations,
|
|
319
|
+
errors,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { renderJson } from "./json.js";
|
|
2
|
+
import { renderPretty } from "./pretty.js";
|
|
3
|
+
import { renderSarif } from "./sarif.js";
|
|
4
|
+
export function renderOutput(format, payload) {
|
|
5
|
+
switch (format) {
|
|
6
|
+
case "json":
|
|
7
|
+
return renderJson(payload);
|
|
8
|
+
case "sarif":
|
|
9
|
+
return renderSarif(payload);
|
|
10
|
+
case "pretty":
|
|
11
|
+
default:
|
|
12
|
+
return renderPretty(payload);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function renderJson(payload) {
|
|
2
|
+
return JSON.stringify({
|
|
3
|
+
summary: {
|
|
4
|
+
checkedPairs: payload.result.checkedPairs,
|
|
5
|
+
totalIssues: payload.result.totalIssues,
|
|
6
|
+
unmatchedTypes: payload.result.unmatchedTypes,
|
|
7
|
+
unmatchedSchemas: payload.result.unmatchedSchemas,
|
|
8
|
+
errors: payload.result.errors,
|
|
9
|
+
},
|
|
10
|
+
pairs: payload.result.pairs.map((pair) => ({
|
|
11
|
+
typeName: pair.typeName,
|
|
12
|
+
schemaName: pair.schemaName,
|
|
13
|
+
typeFile: pair.typeDecl.sourceFilePath,
|
|
14
|
+
schemaFile: pair.schemaDecl.sourceFilePath,
|
|
15
|
+
issues: pair.issues,
|
|
16
|
+
})),
|
|
17
|
+
}, null, 2);
|
|
18
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function renderPretty(payload) {
|
|
2
|
+
const lines = [];
|
|
3
|
+
if (payload.result.errors.length > 0) {
|
|
4
|
+
lines.push("Errors:");
|
|
5
|
+
for (const error of payload.result.errors) {
|
|
6
|
+
lines.push(` - ${error}`);
|
|
7
|
+
}
|
|
8
|
+
lines.push("");
|
|
9
|
+
}
|
|
10
|
+
for (const pair of payload.result.pairs) {
|
|
11
|
+
if (pair.issues.length === 0) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
lines.push(`✗ ${pair.typeName} ↔ ${pair.schemaName}`);
|
|
15
|
+
for (const issue of pair.issues) {
|
|
16
|
+
lines.push(` - ${issue.message}`);
|
|
17
|
+
}
|
|
18
|
+
lines.push("");
|
|
19
|
+
}
|
|
20
|
+
if (payload.result.totalIssues === 0 && payload.result.errors.length === 0) {
|
|
21
|
+
lines.push("✓ No drift found");
|
|
22
|
+
}
|
|
23
|
+
if (payload.result.unmatchedTypes.length > 0) {
|
|
24
|
+
lines.push(`Unmatched exported types: ${payload.result.unmatchedTypes
|
|
25
|
+
.slice(0, 20)
|
|
26
|
+
.join(", ")}`);
|
|
27
|
+
}
|
|
28
|
+
if (payload.result.unmatchedSchemas.length > 0) {
|
|
29
|
+
lines.push(`Unmatched exported schemas: ${payload.result.unmatchedSchemas
|
|
30
|
+
.slice(0, 20)
|
|
31
|
+
.join(", ")}`);
|
|
32
|
+
}
|
|
33
|
+
lines.push(`Checked pairs: ${payload.result.checkedPairs} | Issues: ${payload.result.totalIssues}`);
|
|
34
|
+
return lines.join("\n").trim();
|
|
35
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
function levelForIssue(kind) {
|
|
3
|
+
switch (kind) {
|
|
4
|
+
case "type_mismatch":
|
|
5
|
+
case "optional_mismatch":
|
|
6
|
+
return "error";
|
|
7
|
+
default:
|
|
8
|
+
return "warning";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function ruleName(kind) {
|
|
12
|
+
switch (kind) {
|
|
13
|
+
case "missing_in_schema":
|
|
14
|
+
return "missing-field";
|
|
15
|
+
case "extra_in_schema":
|
|
16
|
+
return "extra-field";
|
|
17
|
+
case "optional_mismatch":
|
|
18
|
+
return "optional-mismatch";
|
|
19
|
+
case "type_mismatch":
|
|
20
|
+
return "type-mismatch";
|
|
21
|
+
default:
|
|
22
|
+
return "drift";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function renderSarif(payload) {
|
|
26
|
+
const rules = [
|
|
27
|
+
{
|
|
28
|
+
id: "missing-field",
|
|
29
|
+
shortDescription: { text: "Field exists in TS type but missing in Zod schema" },
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "extra-field",
|
|
33
|
+
shortDescription: { text: "Field exists in Zod schema but not in TS type" },
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "optional-mismatch",
|
|
37
|
+
shortDescription: { text: "Optional/required mismatch between TS and Zod" },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "type-mismatch",
|
|
41
|
+
shortDescription: { text: "Type mismatch between TS and Zod" },
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
const results = payload.result.pairs.flatMap((pair) => pair.issues.map((issue) => {
|
|
45
|
+
const location = issue.schemaLocation ?? issue.typeLocation;
|
|
46
|
+
return {
|
|
47
|
+
ruleId: ruleName(issue.kind),
|
|
48
|
+
level: levelForIssue(issue.kind),
|
|
49
|
+
message: {
|
|
50
|
+
text: `${pair.typeName} ↔ ${pair.schemaName}: ${issue.message}`,
|
|
51
|
+
},
|
|
52
|
+
locations: location
|
|
53
|
+
? [
|
|
54
|
+
{
|
|
55
|
+
physicalLocation: {
|
|
56
|
+
artifactLocation: {
|
|
57
|
+
uri: path.relative(payload.cwd, location.filePath) || location.filePath,
|
|
58
|
+
},
|
|
59
|
+
region: {
|
|
60
|
+
startLine: location.line,
|
|
61
|
+
startColumn: location.column,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
]
|
|
66
|
+
: undefined,
|
|
67
|
+
};
|
|
68
|
+
}));
|
|
69
|
+
return JSON.stringify({
|
|
70
|
+
version: "2.1.0",
|
|
71
|
+
$schema: "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0.json",
|
|
72
|
+
runs: [
|
|
73
|
+
{
|
|
74
|
+
tool: {
|
|
75
|
+
driver: {
|
|
76
|
+
name: "zodrift",
|
|
77
|
+
informationUri: "https://github.com/greyllmmoder/zodrift",
|
|
78
|
+
rules,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
results,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
}, null, 2);
|
|
85
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ParsedArgs {
|
|
2
|
+
command: string | null;
|
|
3
|
+
flags: Map<string, string | boolean>;
|
|
4
|
+
positionals: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function parseArgs(argv: string[]): ParsedArgs;
|
|
7
|
+
export declare function flagString(flags: Map<string, string | boolean>, key: string, fallback: string): string;
|
|
8
|
+
export declare function flagBoolean(flags: Map<string, string | boolean>, key: string, fallback?: boolean): boolean;
|
|
9
|
+
export declare function flagNumber(flags: Map<string, string | boolean>, key: string): number | undefined;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export function parseArgs(argv) {
|
|
2
|
+
const [commandOrNull, ...rest] = argv;
|
|
3
|
+
const flags = new Map();
|
|
4
|
+
const positionals = [];
|
|
5
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
6
|
+
const token = rest[i];
|
|
7
|
+
if (!token.startsWith("--")) {
|
|
8
|
+
positionals.push(token);
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
const withoutPrefix = token.slice(2);
|
|
12
|
+
if (!withoutPrefix) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (withoutPrefix.includes("=")) {
|
|
16
|
+
const [key, ...valueParts] = withoutPrefix.split("=");
|
|
17
|
+
flags.set(key, valueParts.join("="));
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const next = rest[i + 1];
|
|
21
|
+
if (next && !next.startsWith("--")) {
|
|
22
|
+
flags.set(withoutPrefix, next);
|
|
23
|
+
i += 1;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
flags.set(withoutPrefix, true);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
command: commandOrNull ?? null,
|
|
30
|
+
flags,
|
|
31
|
+
positionals,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function flagString(flags, key, fallback) {
|
|
35
|
+
const value = flags.get(key);
|
|
36
|
+
return typeof value === "string" ? value : fallback;
|
|
37
|
+
}
|
|
38
|
+
export function flagBoolean(flags, key, fallback = false) {
|
|
39
|
+
const value = flags.get(key);
|
|
40
|
+
if (typeof value === "boolean") {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === "string") {
|
|
44
|
+
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
45
|
+
}
|
|
46
|
+
return fallback;
|
|
47
|
+
}
|
|
48
|
+
export function flagNumber(flags, key) {
|
|
49
|
+
const value = flags.get(key);
|
|
50
|
+
if (typeof value !== "string") {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const parsed = Number(value);
|
|
54
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
55
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "zodrift",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Catch drift between your TypeScript types and Zod schemas.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"zodrift": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"dev": "tsx src/cli.ts",
|
|
20
|
+
"build": "rm -rf dist && tsc -p tsconfig.json",
|
|
21
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
22
|
+
"test": "tsx --test src/**/*.test.ts",
|
|
23
|
+
"check": "npm run dev -- check",
|
|
24
|
+
"prepublishOnly": "npm run typecheck && npm run build && npm test"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"zod",
|
|
28
|
+
"typescript",
|
|
29
|
+
"schema",
|
|
30
|
+
"drift",
|
|
31
|
+
"cli"
|
|
32
|
+
],
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^22.15.0",
|
|
35
|
+
"tsx": "^4.20.0",
|
|
36
|
+
"typescript": "^5.8.3"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"zod": "^3.25.76"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
}
|
|
44
|
+
}
|