@schemashift/core 0.9.0 → 0.11.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/dist/index.cjs +1796 -95
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +456 -2
- package/dist/index.d.ts +456 -2
- package/dist/index.js +1773 -103
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,19 +17,32 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
21
31
|
var index_exports = {};
|
|
22
32
|
__export(index_exports, {
|
|
33
|
+
ApprovalManager: () => ApprovalManager,
|
|
23
34
|
BehavioralWarningAnalyzer: () => BehavioralWarningAnalyzer,
|
|
24
35
|
BundleEstimator: () => BundleEstimator,
|
|
25
36
|
CompatibilityAnalyzer: () => CompatibilityAnalyzer,
|
|
26
37
|
ComplexityEstimator: () => ComplexityEstimator,
|
|
27
38
|
DetailedAnalyzer: () => DetailedAnalyzer,
|
|
39
|
+
DriftDetector: () => DriftDetector,
|
|
28
40
|
EcosystemAnalyzer: () => EcosystemAnalyzer,
|
|
29
41
|
FormResolverMigrator: () => FormResolverMigrator,
|
|
42
|
+
GOVERNANCE_TEMPLATES: () => GOVERNANCE_TEMPLATES,
|
|
30
43
|
GovernanceEngine: () => GovernanceEngine,
|
|
44
|
+
GovernanceFixer: () => GovernanceFixer,
|
|
45
|
+
GraphExporter: () => GraphExporter,
|
|
31
46
|
IncrementalTracker: () => IncrementalTracker,
|
|
32
47
|
MigrationAuditLog: () => MigrationAuditLog,
|
|
33
48
|
MigrationChain: () => MigrationChain,
|
|
@@ -37,22 +52,38 @@ __export(index_exports, {
|
|
|
37
52
|
PluginLoader: () => PluginLoader,
|
|
38
53
|
SchemaAnalyzer: () => SchemaAnalyzer,
|
|
39
54
|
SchemaDependencyResolver: () => SchemaDependencyResolver,
|
|
55
|
+
StandardSchemaAdvisor: () => StandardSchemaAdvisor,
|
|
40
56
|
TestScaffolder: () => TestScaffolder,
|
|
41
57
|
TransformEngine: () => TransformEngine,
|
|
42
58
|
TypeDedupDetector: () => TypeDedupDetector,
|
|
59
|
+
WebhookNotifier: () => WebhookNotifier,
|
|
43
60
|
buildCallChain: () => buildCallChain,
|
|
44
61
|
computeParallelBatches: () => computeParallelBatches,
|
|
62
|
+
conditionalValidation: () => conditionalValidation,
|
|
63
|
+
dependentFields: () => dependentFields,
|
|
45
64
|
detectFormLibraries: () => detectFormLibraries,
|
|
46
65
|
detectSchemaLibrary: () => detectSchemaLibrary,
|
|
47
66
|
detectStandardSchema: () => detectStandardSchema,
|
|
67
|
+
getAllMigrationTemplates: () => getAllMigrationTemplates,
|
|
68
|
+
getGovernanceTemplate: () => getGovernanceTemplate,
|
|
69
|
+
getGovernanceTemplateNames: () => getGovernanceTemplateNames,
|
|
70
|
+
getGovernanceTemplatesByCategory: () => getGovernanceTemplatesByCategory,
|
|
71
|
+
getMigrationTemplate: () => getMigrationTemplate,
|
|
72
|
+
getMigrationTemplateNames: () => getMigrationTemplateNames,
|
|
73
|
+
getMigrationTemplatesByCategory: () => getMigrationTemplatesByCategory,
|
|
48
74
|
isInsideComment: () => isInsideComment,
|
|
49
75
|
isInsideStringLiteral: () => isInsideStringLiteral,
|
|
50
76
|
loadConfig: () => loadConfig,
|
|
77
|
+
mutuallyExclusive: () => mutuallyExclusive,
|
|
51
78
|
parseCallChain: () => parseCallChain,
|
|
79
|
+
requireIf: () => requireIf,
|
|
80
|
+
requireOneOf: () => requireOneOf,
|
|
52
81
|
shouldSuppressWarning: () => shouldSuppressWarning,
|
|
53
82
|
startsWithBase: () => startsWithBase,
|
|
83
|
+
suggestCrossFieldPattern: () => suggestCrossFieldPattern,
|
|
54
84
|
transformMethodChain: () => transformMethodChain,
|
|
55
|
-
validateConfig: () => validateConfig
|
|
85
|
+
validateConfig: () => validateConfig,
|
|
86
|
+
validateMigrationTemplate: () => validateMigrationTemplate
|
|
56
87
|
});
|
|
57
88
|
module.exports = __toCommonJS(index_exports);
|
|
58
89
|
|
|
@@ -68,6 +99,9 @@ var LIBRARY_PATTERNS = {
|
|
|
68
99
|
joi: [/^joi$/, /^@hapi\/joi$/],
|
|
69
100
|
"io-ts": [/^io-ts$/, /^io-ts\//],
|
|
70
101
|
valibot: [/^valibot$/],
|
|
102
|
+
arktype: [/^arktype$/],
|
|
103
|
+
superstruct: [/^superstruct$/],
|
|
104
|
+
effect: [/^@effect\/schema$/],
|
|
71
105
|
v4: [],
|
|
72
106
|
// Target version, not detectable from imports
|
|
73
107
|
unknown: []
|
|
@@ -188,6 +222,110 @@ var SchemaAnalyzer = class {
|
|
|
188
222
|
}
|
|
189
223
|
};
|
|
190
224
|
|
|
225
|
+
// src/approval.ts
|
|
226
|
+
var import_node_fs = require("fs");
|
|
227
|
+
var import_node_path = require("path");
|
|
228
|
+
var ApprovalManager = class {
|
|
229
|
+
pendingDir;
|
|
230
|
+
constructor(projectPath) {
|
|
231
|
+
this.pendingDir = (0, import_node_path.join)(projectPath, ".schemashift", "pending");
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Create a new migration request for review.
|
|
235
|
+
*/
|
|
236
|
+
createRequest(from, to, files, requestedBy, metadata) {
|
|
237
|
+
const id = `mig-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
238
|
+
const request = {
|
|
239
|
+
id,
|
|
240
|
+
from,
|
|
241
|
+
to,
|
|
242
|
+
files,
|
|
243
|
+
requestedBy,
|
|
244
|
+
requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
245
|
+
status: "pending",
|
|
246
|
+
metadata
|
|
247
|
+
};
|
|
248
|
+
this.ensureDir();
|
|
249
|
+
const filePath = (0, import_node_path.join)(this.pendingDir, `${id}.json`);
|
|
250
|
+
(0, import_node_fs.writeFileSync)(filePath, JSON.stringify(request, null, 2), "utf-8");
|
|
251
|
+
return request;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Review (approve or reject) a pending migration request.
|
|
255
|
+
*/
|
|
256
|
+
review(decision) {
|
|
257
|
+
const request = this.getRequest(decision.requestId);
|
|
258
|
+
if (!request) {
|
|
259
|
+
throw new Error(`Migration request ${decision.requestId} not found`);
|
|
260
|
+
}
|
|
261
|
+
if (request.status !== "pending") {
|
|
262
|
+
throw new Error(`Migration request ${decision.requestId} is already ${request.status}`);
|
|
263
|
+
}
|
|
264
|
+
request.status = decision.status;
|
|
265
|
+
request.reviewedBy = decision.reviewedBy;
|
|
266
|
+
request.reviewedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
267
|
+
request.reason = decision.reason;
|
|
268
|
+
const filePath = (0, import_node_path.join)(this.pendingDir, `${decision.requestId}.json`);
|
|
269
|
+
(0, import_node_fs.writeFileSync)(filePath, JSON.stringify(request, null, 2), "utf-8");
|
|
270
|
+
return request;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Get a specific migration request by ID.
|
|
274
|
+
*/
|
|
275
|
+
getRequest(id) {
|
|
276
|
+
const filePath = (0, import_node_path.join)(this.pendingDir, `${id}.json`);
|
|
277
|
+
if (!(0, import_node_fs.existsSync)(filePath)) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
const content = (0, import_node_fs.readFileSync)(filePath, "utf-8");
|
|
281
|
+
return JSON.parse(content);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* List all migration requests, optionally filtered by status.
|
|
285
|
+
*/
|
|
286
|
+
listRequests(status) {
|
|
287
|
+
if (!(0, import_node_fs.existsSync)(this.pendingDir)) {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
const files = (0, import_node_fs.readdirSync)(this.pendingDir).filter((f) => f.endsWith(".json"));
|
|
291
|
+
const requests = [];
|
|
292
|
+
for (const file of files) {
|
|
293
|
+
const content = (0, import_node_fs.readFileSync)((0, import_node_path.join)(this.pendingDir, file), "utf-8");
|
|
294
|
+
const request = JSON.parse(content);
|
|
295
|
+
if (!status || request.status === status) {
|
|
296
|
+
requests.push(request);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return requests.sort(
|
|
300
|
+
(a, b) => new Date(b.requestedAt).getTime() - new Date(a.requestedAt).getTime()
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Get summary counts of all requests.
|
|
305
|
+
*/
|
|
306
|
+
getSummary() {
|
|
307
|
+
const all = this.listRequests();
|
|
308
|
+
return {
|
|
309
|
+
pending: all.filter((r) => r.status === "pending").length,
|
|
310
|
+
approved: all.filter((r) => r.status === "approved").length,
|
|
311
|
+
rejected: all.filter((r) => r.status === "rejected").length,
|
|
312
|
+
total: all.length
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Check if a migration has been approved.
|
|
317
|
+
*/
|
|
318
|
+
isApproved(requestId) {
|
|
319
|
+
const request = this.getRequest(requestId);
|
|
320
|
+
return request?.status === "approved";
|
|
321
|
+
}
|
|
322
|
+
ensureDir() {
|
|
323
|
+
if (!(0, import_node_fs.existsSync)(this.pendingDir)) {
|
|
324
|
+
(0, import_node_fs.mkdirSync)(this.pendingDir, { recursive: true });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
191
329
|
// src/ast-utils.ts
|
|
192
330
|
var import_ts_morph2 = require("ts-morph");
|
|
193
331
|
function parseCallChain(node) {
|
|
@@ -324,8 +462,8 @@ function transformMethodChain(chain, newBase, factoryMapper, methodMapper) {
|
|
|
324
462
|
|
|
325
463
|
// src/audit-log.ts
|
|
326
464
|
var import_node_crypto = require("crypto");
|
|
327
|
-
var
|
|
328
|
-
var
|
|
465
|
+
var import_node_fs2 = require("fs");
|
|
466
|
+
var import_node_path2 = require("path");
|
|
329
467
|
var AUDIT_DIR = ".schemashift";
|
|
330
468
|
var AUDIT_FILE = "audit-log.json";
|
|
331
469
|
var AUDIT_VERSION = 1;
|
|
@@ -333,8 +471,8 @@ var MigrationAuditLog = class {
|
|
|
333
471
|
logDir;
|
|
334
472
|
logPath;
|
|
335
473
|
constructor(projectPath) {
|
|
336
|
-
this.logDir = (0,
|
|
337
|
-
this.logPath = (0,
|
|
474
|
+
this.logDir = (0, import_node_path2.join)(projectPath, AUDIT_DIR);
|
|
475
|
+
this.logPath = (0, import_node_path2.join)(this.logDir, AUDIT_FILE);
|
|
338
476
|
}
|
|
339
477
|
/**
|
|
340
478
|
* Append a new entry to the audit log.
|
|
@@ -362,18 +500,19 @@ var MigrationAuditLog = class {
|
|
|
362
500
|
errorCount: params.errorCount,
|
|
363
501
|
riskScore: params.riskScore,
|
|
364
502
|
duration: params.duration,
|
|
365
|
-
user: this.getCurrentUser()
|
|
503
|
+
user: this.getCurrentUser(),
|
|
504
|
+
metadata: params.metadata || this.collectMetadata()
|
|
366
505
|
};
|
|
367
506
|
}
|
|
368
507
|
/**
|
|
369
508
|
* Read the current audit log.
|
|
370
509
|
*/
|
|
371
510
|
read() {
|
|
372
|
-
if (!(0,
|
|
511
|
+
if (!(0, import_node_fs2.existsSync)(this.logPath)) {
|
|
373
512
|
return { version: AUDIT_VERSION, entries: [] };
|
|
374
513
|
}
|
|
375
514
|
try {
|
|
376
|
-
const content = (0,
|
|
515
|
+
const content = (0, import_node_fs2.readFileSync)(this.logPath, "utf-8");
|
|
377
516
|
if (!content.trim()) {
|
|
378
517
|
return { version: AUDIT_VERSION, entries: [] };
|
|
379
518
|
}
|
|
@@ -404,17 +543,72 @@ var MigrationAuditLog = class {
|
|
|
404
543
|
migrationPaths
|
|
405
544
|
};
|
|
406
545
|
}
|
|
546
|
+
/**
|
|
547
|
+
* Export audit log as JSON string.
|
|
548
|
+
*/
|
|
549
|
+
exportJson() {
|
|
550
|
+
const log = this.read();
|
|
551
|
+
return JSON.stringify(log, null, 2);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Export audit log as CSV string.
|
|
555
|
+
*/
|
|
556
|
+
exportCsv() {
|
|
557
|
+
const log = this.read();
|
|
558
|
+
const headers = [
|
|
559
|
+
"timestamp",
|
|
560
|
+
"migrationId",
|
|
561
|
+
"filePath",
|
|
562
|
+
"action",
|
|
563
|
+
"from",
|
|
564
|
+
"to",
|
|
565
|
+
"success",
|
|
566
|
+
"warningCount",
|
|
567
|
+
"errorCount",
|
|
568
|
+
"riskScore",
|
|
569
|
+
"user",
|
|
570
|
+
"duration"
|
|
571
|
+
];
|
|
572
|
+
const rows = log.entries.map(
|
|
573
|
+
(e) => headers.map((h) => {
|
|
574
|
+
const val = e[h];
|
|
575
|
+
if (val === void 0 || val === null) return "";
|
|
576
|
+
return String(val).includes(",") ? `"${String(val)}"` : String(val);
|
|
577
|
+
}).join(",")
|
|
578
|
+
);
|
|
579
|
+
return [headers.join(","), ...rows].join("\n");
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Get entries filtered by date range.
|
|
583
|
+
*/
|
|
584
|
+
getByDateRange(start, end) {
|
|
585
|
+
const log = this.read();
|
|
586
|
+
return log.entries.filter((e) => {
|
|
587
|
+
const ts = new Date(e.timestamp);
|
|
588
|
+
return ts >= start && ts <= end;
|
|
589
|
+
});
|
|
590
|
+
}
|
|
407
591
|
/**
|
|
408
592
|
* Clear the audit log.
|
|
409
593
|
*/
|
|
410
594
|
clear() {
|
|
411
595
|
this.write({ version: AUDIT_VERSION, entries: [] });
|
|
412
596
|
}
|
|
597
|
+
collectMetadata() {
|
|
598
|
+
return {
|
|
599
|
+
hostname: process.env.HOSTNAME || void 0,
|
|
600
|
+
nodeVersion: process.version,
|
|
601
|
+
ciJobId: process.env.CI_JOB_ID || process.env.GITHUB_RUN_ID || void 0,
|
|
602
|
+
ciProvider: process.env.GITHUB_ACTIONS ? "github" : process.env.GITLAB_CI ? "gitlab" : process.env.CIRCLECI ? "circleci" : process.env.JENKINS_URL ? "jenkins" : void 0,
|
|
603
|
+
gitBranch: process.env.GITHUB_REF_NAME || process.env.CI_COMMIT_BRANCH || void 0,
|
|
604
|
+
gitCommit: process.env.GITHUB_SHA || process.env.CI_COMMIT_SHA || void 0
|
|
605
|
+
};
|
|
606
|
+
}
|
|
413
607
|
write(log) {
|
|
414
|
-
if (!(0,
|
|
415
|
-
(0,
|
|
608
|
+
if (!(0, import_node_fs2.existsSync)(this.logDir)) {
|
|
609
|
+
(0, import_node_fs2.mkdirSync)(this.logDir, { recursive: true });
|
|
416
610
|
}
|
|
417
|
-
(0,
|
|
611
|
+
(0, import_node_fs2.writeFileSync)(this.logPath, JSON.stringify(log, null, 2));
|
|
418
612
|
}
|
|
419
613
|
hashContent(content) {
|
|
420
614
|
return (0, import_node_crypto.createHash)("sha256").update(content).digest("hex").substring(0, 16);
|
|
@@ -847,12 +1041,12 @@ var MigrationChain = class {
|
|
|
847
1041
|
};
|
|
848
1042
|
|
|
849
1043
|
// src/compatibility.ts
|
|
850
|
-
var
|
|
851
|
-
var
|
|
1044
|
+
var import_node_fs4 = require("fs");
|
|
1045
|
+
var import_node_path4 = require("path");
|
|
852
1046
|
|
|
853
1047
|
// src/ecosystem.ts
|
|
854
|
-
var
|
|
855
|
-
var
|
|
1048
|
+
var import_node_fs3 = require("fs");
|
|
1049
|
+
var import_node_path3 = require("path");
|
|
856
1050
|
var ECOSYSTEM_RULES = [
|
|
857
1051
|
// ORM integrations
|
|
858
1052
|
{
|
|
@@ -1028,6 +1222,191 @@ var ECOSYSTEM_RULES = [
|
|
|
1028
1222
|
severity: "error"
|
|
1029
1223
|
})
|
|
1030
1224
|
},
|
|
1225
|
+
// Zod-based HTTP/API clients
|
|
1226
|
+
{
|
|
1227
|
+
package: "zodios",
|
|
1228
|
+
category: "api",
|
|
1229
|
+
migrations: ["zod-v3->v4"],
|
|
1230
|
+
check: () => ({
|
|
1231
|
+
issue: "Zodios uses Zod schemas for API contract definitions. Zod v4 type changes may break contracts.",
|
|
1232
|
+
suggestion: "Upgrade Zodios to a Zod v4-compatible version and verify all API contracts.",
|
|
1233
|
+
severity: "warning",
|
|
1234
|
+
upgradeCommand: "npm install @zodios/core@latest"
|
|
1235
|
+
})
|
|
1236
|
+
},
|
|
1237
|
+
{
|
|
1238
|
+
package: "@zodios/core",
|
|
1239
|
+
category: "api",
|
|
1240
|
+
migrations: ["zod-v3->v4"],
|
|
1241
|
+
check: () => ({
|
|
1242
|
+
issue: "@zodios/core uses Zod schemas for API contract definitions. Zod v4 type changes may break contracts.",
|
|
1243
|
+
suggestion: "Upgrade @zodios/core to a Zod v4-compatible version and verify all API contracts.",
|
|
1244
|
+
severity: "warning",
|
|
1245
|
+
upgradeCommand: "npm install @zodios/core@latest"
|
|
1246
|
+
})
|
|
1247
|
+
},
|
|
1248
|
+
{
|
|
1249
|
+
package: "@ts-rest/core",
|
|
1250
|
+
category: "api",
|
|
1251
|
+
migrations: ["zod-v3->v4"],
|
|
1252
|
+
check: () => ({
|
|
1253
|
+
issue: "@ts-rest/core uses Zod for contract definitions. Zod v4 type incompatibilities may break runtime validation.",
|
|
1254
|
+
suggestion: "Upgrade @ts-rest/core to a version with Zod v4 support.",
|
|
1255
|
+
severity: "warning",
|
|
1256
|
+
upgradeCommand: "npm install @ts-rest/core@latest"
|
|
1257
|
+
})
|
|
1258
|
+
},
|
|
1259
|
+
{
|
|
1260
|
+
package: "trpc-openapi",
|
|
1261
|
+
category: "openapi",
|
|
1262
|
+
migrations: ["zod-v3->v4"],
|
|
1263
|
+
check: () => ({
|
|
1264
|
+
issue: "trpc-openapi needs a v4-compatible version for Zod v4.",
|
|
1265
|
+
suggestion: "Check for a Zod v4-compatible version of trpc-openapi before upgrading.",
|
|
1266
|
+
severity: "warning",
|
|
1267
|
+
upgradeCommand: "npm install trpc-openapi@latest"
|
|
1268
|
+
})
|
|
1269
|
+
},
|
|
1270
|
+
// Form data and URL state libraries
|
|
1271
|
+
{
|
|
1272
|
+
package: "zod-form-data",
|
|
1273
|
+
category: "form",
|
|
1274
|
+
migrations: ["zod-v3->v4"],
|
|
1275
|
+
check: () => ({
|
|
1276
|
+
issue: "zod-form-data relies on Zod v3 internals (_def) which moved to _zod.def in v4.",
|
|
1277
|
+
suggestion: "Upgrade zod-form-data to a Zod v4-compatible version.",
|
|
1278
|
+
severity: "error",
|
|
1279
|
+
upgradeCommand: "npm install zod-form-data@latest"
|
|
1280
|
+
})
|
|
1281
|
+
},
|
|
1282
|
+
{
|
|
1283
|
+
package: "@conform-to/zod",
|
|
1284
|
+
category: "form",
|
|
1285
|
+
migrations: ["zod-v3->v4"],
|
|
1286
|
+
check: () => ({
|
|
1287
|
+
issue: "@conform-to/zod may have Zod v4 compatibility issues.",
|
|
1288
|
+
suggestion: "Upgrade @conform-to/zod to the latest version with Zod v4 support.",
|
|
1289
|
+
severity: "warning",
|
|
1290
|
+
upgradeCommand: "npm install @conform-to/zod@latest"
|
|
1291
|
+
})
|
|
1292
|
+
},
|
|
1293
|
+
{
|
|
1294
|
+
package: "nuqs",
|
|
1295
|
+
category: "validation-util",
|
|
1296
|
+
migrations: ["zod-v3->v4"],
|
|
1297
|
+
check: () => ({
|
|
1298
|
+
issue: "nuqs uses Zod for URL state parsing. Zod v4 changes may affect URL parameter validation.",
|
|
1299
|
+
suggestion: "Upgrade nuqs to a version with Zod v4 support.",
|
|
1300
|
+
severity: "warning",
|
|
1301
|
+
upgradeCommand: "npm install nuqs@latest"
|
|
1302
|
+
})
|
|
1303
|
+
},
|
|
1304
|
+
// Server action / routing integrations
|
|
1305
|
+
{
|
|
1306
|
+
package: "next-safe-action",
|
|
1307
|
+
category: "api",
|
|
1308
|
+
migrations: ["zod-v3->v4"],
|
|
1309
|
+
check: () => ({
|
|
1310
|
+
issue: "next-safe-action uses Zod for input validation. Zod v4 type changes may break action definitions.",
|
|
1311
|
+
suggestion: "Upgrade next-safe-action to the latest version with Zod v4 support.",
|
|
1312
|
+
severity: "warning",
|
|
1313
|
+
upgradeCommand: "npm install next-safe-action@latest"
|
|
1314
|
+
})
|
|
1315
|
+
},
|
|
1316
|
+
{
|
|
1317
|
+
package: "@tanstack/router",
|
|
1318
|
+
category: "api",
|
|
1319
|
+
migrations: ["zod-v3->v4"],
|
|
1320
|
+
check: () => ({
|
|
1321
|
+
issue: "@tanstack/router uses Zod for route parameter validation. Zod v4 changes may affect type inference.",
|
|
1322
|
+
suggestion: "Upgrade @tanstack/router to a version with Zod v4 support.",
|
|
1323
|
+
severity: "warning",
|
|
1324
|
+
upgradeCommand: "npm install @tanstack/router@latest"
|
|
1325
|
+
})
|
|
1326
|
+
},
|
|
1327
|
+
{
|
|
1328
|
+
package: "@tanstack/react-query",
|
|
1329
|
+
category: "api",
|
|
1330
|
+
migrations: ["zod-v3->v4"],
|
|
1331
|
+
check: () => ({
|
|
1332
|
+
issue: "@tanstack/react-query may use Zod for query key/param validation via integrations.",
|
|
1333
|
+
suggestion: "Verify any Zod-based query validation still works after the Zod v4 upgrade.",
|
|
1334
|
+
severity: "info"
|
|
1335
|
+
})
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
package: "fastify-type-provider-zod",
|
|
1339
|
+
category: "api",
|
|
1340
|
+
migrations: ["zod-v3->v4"],
|
|
1341
|
+
check: () => ({
|
|
1342
|
+
issue: "fastify-type-provider-zod needs a Zod v4-compatible version.",
|
|
1343
|
+
suggestion: "Upgrade fastify-type-provider-zod to a version supporting Zod v4.",
|
|
1344
|
+
severity: "warning",
|
|
1345
|
+
upgradeCommand: "npm install fastify-type-provider-zod@latest"
|
|
1346
|
+
})
|
|
1347
|
+
},
|
|
1348
|
+
{
|
|
1349
|
+
package: "zod-i18n-map",
|
|
1350
|
+
category: "validation-util",
|
|
1351
|
+
migrations: ["zod-v3->v4"],
|
|
1352
|
+
check: () => ({
|
|
1353
|
+
issue: 'zod-i18n-map uses Zod v3 error map format. Error messages changed in v4 (e.g., "Required" is now descriptive).',
|
|
1354
|
+
suggestion: "Check for a Zod v4-compatible version of zod-i18n-map or update custom error maps.",
|
|
1355
|
+
severity: "warning",
|
|
1356
|
+
upgradeCommand: "npm install zod-i18n-map@latest"
|
|
1357
|
+
})
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
package: "openapi-zod-client",
|
|
1361
|
+
category: "openapi",
|
|
1362
|
+
migrations: ["zod-v3->v4"],
|
|
1363
|
+
check: () => ({
|
|
1364
|
+
issue: "openapi-zod-client generates Zod v3 schemas from OpenAPI specs. Generated code may need regeneration.",
|
|
1365
|
+
suggestion: "Upgrade openapi-zod-client and regenerate schemas for Zod v4 compatibility.",
|
|
1366
|
+
severity: "warning",
|
|
1367
|
+
upgradeCommand: "npm install openapi-zod-client@latest"
|
|
1368
|
+
})
|
|
1369
|
+
},
|
|
1370
|
+
// Schema library detection for cross-library migrations
|
|
1371
|
+
{
|
|
1372
|
+
package: "@effect/schema",
|
|
1373
|
+
category: "validation-util",
|
|
1374
|
+
migrations: ["io-ts->zod"],
|
|
1375
|
+
check: () => ({
|
|
1376
|
+
issue: "@effect/schema detected \u2014 this is the successor to io-ts/fp-ts. Consider migrating to Effect Schema instead of Zod if you prefer FP patterns.",
|
|
1377
|
+
suggestion: "If using fp-ts patterns heavily, consider Effect Schema as the migration target instead of Zod.",
|
|
1378
|
+
severity: "info"
|
|
1379
|
+
})
|
|
1380
|
+
},
|
|
1381
|
+
{
|
|
1382
|
+
package: "arktype",
|
|
1383
|
+
category: "validation-util",
|
|
1384
|
+
migrations: ["zod->valibot", "zod-v3->v4"],
|
|
1385
|
+
check: (_version, migration) => {
|
|
1386
|
+
if (migration === "zod->valibot") {
|
|
1387
|
+
return {
|
|
1388
|
+
issue: "ArkType detected alongside Zod. Consider ArkType as a migration target \u2014 it offers 100x faster validation and Standard Schema support.",
|
|
1389
|
+
suggestion: "Consider migrating to ArkType for performance-critical paths, or keep Zod for ecosystem compatibility.",
|
|
1390
|
+
severity: "info"
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
return {
|
|
1394
|
+
issue: "ArkType detected alongside Zod. ArkType supports Standard Schema, making it interoperable with Zod v4.",
|
|
1395
|
+
suggestion: "No action needed \u2014 ArkType and Zod v4 can coexist via Standard Schema.",
|
|
1396
|
+
severity: "info"
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
},
|
|
1400
|
+
{
|
|
1401
|
+
package: "superstruct",
|
|
1402
|
+
category: "validation-util",
|
|
1403
|
+
migrations: ["yup->zod", "joi->zod"],
|
|
1404
|
+
check: () => ({
|
|
1405
|
+
issue: "Superstruct detected in the project. Consider migrating Superstruct schemas to Zod as well for a unified validation approach.",
|
|
1406
|
+
suggestion: "Use SchemaShift to migrate Superstruct schemas alongside Yup/Joi schemas.",
|
|
1407
|
+
severity: "info"
|
|
1408
|
+
})
|
|
1409
|
+
},
|
|
1031
1410
|
// Additional validation utilities
|
|
1032
1411
|
{
|
|
1033
1412
|
package: "zod-to-json-schema",
|
|
@@ -1065,13 +1444,13 @@ var EcosystemAnalyzer = class {
|
|
|
1065
1444
|
const dependencies = [];
|
|
1066
1445
|
const warnings = [];
|
|
1067
1446
|
const blockers = [];
|
|
1068
|
-
const pkgPath = (0,
|
|
1069
|
-
if (!(0,
|
|
1447
|
+
const pkgPath = (0, import_node_path3.join)(projectPath, "package.json");
|
|
1448
|
+
if (!(0, import_node_fs3.existsSync)(pkgPath)) {
|
|
1070
1449
|
return { dependencies, warnings, blockers };
|
|
1071
1450
|
}
|
|
1072
1451
|
let allDeps = {};
|
|
1073
1452
|
try {
|
|
1074
|
-
const pkg = JSON.parse((0,
|
|
1453
|
+
const pkg = JSON.parse((0, import_node_fs3.readFileSync)(pkgPath, "utf-8"));
|
|
1075
1454
|
allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1076
1455
|
} catch {
|
|
1077
1456
|
return { dependencies, warnings, blockers };
|
|
@@ -1192,10 +1571,10 @@ var CompatibilityAnalyzer = class {
|
|
|
1192
1571
|
ecosystemAnalyzer = new EcosystemAnalyzer();
|
|
1193
1572
|
detectVersions(projectPath) {
|
|
1194
1573
|
const versions = [];
|
|
1195
|
-
const pkgPath = (0,
|
|
1196
|
-
if (!(0,
|
|
1574
|
+
const pkgPath = (0, import_node_path4.join)(projectPath, "package.json");
|
|
1575
|
+
if (!(0, import_node_fs4.existsSync)(pkgPath)) return versions;
|
|
1197
1576
|
try {
|
|
1198
|
-
const pkg = JSON.parse((0,
|
|
1577
|
+
const pkg = JSON.parse((0, import_node_fs4.readFileSync)(pkgPath, "utf-8"));
|
|
1199
1578
|
const knownLibs = ["zod", "yup", "joi", "io-ts", "valibot"];
|
|
1200
1579
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1201
1580
|
for (const lib of knownLibs) {
|
|
@@ -1416,9 +1795,115 @@ async function loadConfig(configPath) {
|
|
|
1416
1795
|
};
|
|
1417
1796
|
}
|
|
1418
1797
|
|
|
1798
|
+
// src/cross-field-patterns.ts
|
|
1799
|
+
function requireIf(conditionField, requiredField) {
|
|
1800
|
+
return {
|
|
1801
|
+
name: `requireIf(${conditionField}, ${requiredField})`,
|
|
1802
|
+
description: `${requiredField} is required when ${conditionField} is truthy`,
|
|
1803
|
+
zodCode: [
|
|
1804
|
+
".superRefine((data, ctx) => {",
|
|
1805
|
+
` if (data.${conditionField} && !data.${requiredField}) {`,
|
|
1806
|
+
" ctx.addIssue({",
|
|
1807
|
+
" code: z.ZodIssueCode.custom,",
|
|
1808
|
+
` message: '${requiredField} is required when ${conditionField} is set',`,
|
|
1809
|
+
` path: ['${requiredField}'],`,
|
|
1810
|
+
" });",
|
|
1811
|
+
" }",
|
|
1812
|
+
"})"
|
|
1813
|
+
].join("\n")
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
function requireOneOf(fields) {
|
|
1817
|
+
const fieldList = fields.map((f) => `'${f}'`).join(", ");
|
|
1818
|
+
const conditions = fields.map((f) => `data.${f}`).join(" || ");
|
|
1819
|
+
return {
|
|
1820
|
+
name: `requireOneOf(${fields.join(", ")})`,
|
|
1821
|
+
description: `At least one of [${fields.join(", ")}] must be provided`,
|
|
1822
|
+
zodCode: [
|
|
1823
|
+
".superRefine((data, ctx) => {",
|
|
1824
|
+
` if (!(${conditions})) {`,
|
|
1825
|
+
" ctx.addIssue({",
|
|
1826
|
+
" code: z.ZodIssueCode.custom,",
|
|
1827
|
+
` message: 'At least one of [${fields.join(", ")}] is required',`,
|
|
1828
|
+
` path: [${fieldList}],`,
|
|
1829
|
+
" });",
|
|
1830
|
+
" }",
|
|
1831
|
+
"})"
|
|
1832
|
+
].join("\n")
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
function mutuallyExclusive(fields) {
|
|
1836
|
+
const checks = fields.map((f) => `(data.${f} ? 1 : 0)`).join(" + ");
|
|
1837
|
+
return {
|
|
1838
|
+
name: `mutuallyExclusive(${fields.join(", ")})`,
|
|
1839
|
+
description: `Only one of [${fields.join(", ")}] can be set at a time`,
|
|
1840
|
+
zodCode: [
|
|
1841
|
+
".superRefine((data, ctx) => {",
|
|
1842
|
+
` const count = ${checks};`,
|
|
1843
|
+
" if (count > 1) {",
|
|
1844
|
+
" ctx.addIssue({",
|
|
1845
|
+
" code: z.ZodIssueCode.custom,",
|
|
1846
|
+
` message: 'Only one of [${fields.join(", ")}] can be set at a time',`,
|
|
1847
|
+
" });",
|
|
1848
|
+
" }",
|
|
1849
|
+
"})"
|
|
1850
|
+
].join("\n")
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
function dependentFields(primaryField, dependents) {
|
|
1854
|
+
const checks = dependents.map(
|
|
1855
|
+
(f) => ` if (!data.${f}) {
|
|
1856
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: '${f} is required when ${primaryField} is set', path: ['${f}'] });
|
|
1857
|
+
}`
|
|
1858
|
+
).join("\n");
|
|
1859
|
+
return {
|
|
1860
|
+
name: `dependentFields(${primaryField} -> ${dependents.join(", ")})`,
|
|
1861
|
+
description: `When ${primaryField} is set, [${dependents.join(", ")}] are required`,
|
|
1862
|
+
zodCode: [
|
|
1863
|
+
".superRefine((data, ctx) => {",
|
|
1864
|
+
` if (data.${primaryField}) {`,
|
|
1865
|
+
checks,
|
|
1866
|
+
" }",
|
|
1867
|
+
"})"
|
|
1868
|
+
].join("\n")
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
function conditionalValidation(conditionField, conditionValue, targetField, validationMessage) {
|
|
1872
|
+
return {
|
|
1873
|
+
name: `conditionalValidation(${conditionField}=${conditionValue} -> ${targetField})`,
|
|
1874
|
+
description: `Validate ${targetField} when ${conditionField} equals ${conditionValue}`,
|
|
1875
|
+
zodCode: [
|
|
1876
|
+
".superRefine((data, ctx) => {",
|
|
1877
|
+
` if (data.${conditionField} === ${conditionValue} && !data.${targetField}) {`,
|
|
1878
|
+
" ctx.addIssue({",
|
|
1879
|
+
" code: z.ZodIssueCode.custom,",
|
|
1880
|
+
` message: '${validationMessage}',`,
|
|
1881
|
+
` path: ['${targetField}'],`,
|
|
1882
|
+
" });",
|
|
1883
|
+
" }",
|
|
1884
|
+
"})"
|
|
1885
|
+
].join("\n")
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
function suggestCrossFieldPattern(whenCode) {
|
|
1889
|
+
const booleanMatch = whenCode.match(/\.when\(['"](\w+)['"]\s*,\s*\{[^}]*is:\s*true/);
|
|
1890
|
+
if (booleanMatch?.[1]) {
|
|
1891
|
+
const field = booleanMatch[1];
|
|
1892
|
+
return requireIf(field, "targetField");
|
|
1893
|
+
}
|
|
1894
|
+
const multiFieldMatch = whenCode.match(/\.when\(\[([^\]]+)\]/);
|
|
1895
|
+
if (multiFieldMatch?.[1]) {
|
|
1896
|
+
const fields = multiFieldMatch[1].split(",").map((f) => f.trim().replace(/['"]/g, "")).filter(Boolean);
|
|
1897
|
+
if (fields.length > 1) {
|
|
1898
|
+
return dependentFields(fields[0] ?? "primary", fields.slice(1));
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
return null;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1419
1904
|
// src/dependency-graph.ts
|
|
1420
|
-
var
|
|
1421
|
-
var
|
|
1905
|
+
var import_node_fs5 = require("fs");
|
|
1906
|
+
var import_node_path5 = require("path");
|
|
1422
1907
|
var SchemaDependencyResolver = class {
|
|
1423
1908
|
resolve(project, filePaths) {
|
|
1424
1909
|
const fileSet = new Set(filePaths);
|
|
@@ -1545,38 +2030,38 @@ function computeParallelBatches(packages, suggestedOrder) {
|
|
|
1545
2030
|
}
|
|
1546
2031
|
var MonorepoResolver = class {
|
|
1547
2032
|
detect(projectPath) {
|
|
1548
|
-
const pkgPath = (0,
|
|
1549
|
-
if ((0,
|
|
2033
|
+
const pkgPath = (0, import_node_path5.join)(projectPath, "package.json");
|
|
2034
|
+
if ((0, import_node_fs5.existsSync)(pkgPath)) {
|
|
1550
2035
|
try {
|
|
1551
|
-
const pkg = JSON.parse((0,
|
|
2036
|
+
const pkg = JSON.parse((0, import_node_fs5.readFileSync)(pkgPath, "utf-8"));
|
|
1552
2037
|
if (pkg.workspaces) return true;
|
|
1553
2038
|
} catch {
|
|
1554
2039
|
}
|
|
1555
2040
|
}
|
|
1556
|
-
if ((0,
|
|
2041
|
+
if ((0, import_node_fs5.existsSync)((0, import_node_path5.join)(projectPath, "pnpm-workspace.yaml"))) return true;
|
|
1557
2042
|
return false;
|
|
1558
2043
|
}
|
|
1559
2044
|
/**
|
|
1560
2045
|
* Detect which workspace manager is being used.
|
|
1561
2046
|
*/
|
|
1562
2047
|
detectManager(projectPath) {
|
|
1563
|
-
if ((0,
|
|
1564
|
-
const pkgPath = (0,
|
|
1565
|
-
if ((0,
|
|
2048
|
+
if ((0, import_node_fs5.existsSync)((0, import_node_path5.join)(projectPath, "pnpm-workspace.yaml"))) return "pnpm";
|
|
2049
|
+
const pkgPath = (0, import_node_path5.join)(projectPath, "package.json");
|
|
2050
|
+
if ((0, import_node_fs5.existsSync)(pkgPath)) {
|
|
1566
2051
|
try {
|
|
1567
|
-
const pkg = JSON.parse((0,
|
|
2052
|
+
const pkg = JSON.parse((0, import_node_fs5.readFileSync)(pkgPath, "utf-8"));
|
|
1568
2053
|
if (pkg.packageManager?.startsWith("yarn")) return "yarn";
|
|
1569
2054
|
if (pkg.packageManager?.startsWith("pnpm")) return "pnpm";
|
|
1570
2055
|
} catch {
|
|
1571
2056
|
}
|
|
1572
2057
|
}
|
|
1573
|
-
if ((0,
|
|
1574
|
-
if ((0,
|
|
2058
|
+
if ((0, import_node_fs5.existsSync)((0, import_node_path5.join)(projectPath, "pnpm-lock.yaml"))) return "pnpm";
|
|
2059
|
+
if ((0, import_node_fs5.existsSync)((0, import_node_path5.join)(projectPath, "yarn.lock"))) return "yarn";
|
|
1575
2060
|
return "npm";
|
|
1576
2061
|
}
|
|
1577
2062
|
analyze(projectPath) {
|
|
1578
|
-
const pkgPath = (0,
|
|
1579
|
-
if (!(0,
|
|
2063
|
+
const pkgPath = (0, import_node_path5.join)(projectPath, "package.json");
|
|
2064
|
+
if (!(0, import_node_fs5.existsSync)(pkgPath)) {
|
|
1580
2065
|
return { isMonorepo: false, packages: [], suggestedOrder: [] };
|
|
1581
2066
|
}
|
|
1582
2067
|
let workspaceGlobs;
|
|
@@ -1591,10 +2076,10 @@ var MonorepoResolver = class {
|
|
|
1591
2076
|
const packages = [];
|
|
1592
2077
|
const resolvedDirs = this.resolveWorkspaceDirs(projectPath, workspaceGlobs);
|
|
1593
2078
|
for (const dir of resolvedDirs) {
|
|
1594
|
-
const wsPkgPath = (0,
|
|
1595
|
-
if (!(0,
|
|
2079
|
+
const wsPkgPath = (0, import_node_path5.join)(dir, "package.json");
|
|
2080
|
+
if (!(0, import_node_fs5.existsSync)(wsPkgPath)) continue;
|
|
1596
2081
|
try {
|
|
1597
|
-
const wsPkg = JSON.parse((0,
|
|
2082
|
+
const wsPkg = JSON.parse((0, import_node_fs5.readFileSync)(wsPkgPath, "utf-8"));
|
|
1598
2083
|
if (!wsPkg.name) continue;
|
|
1599
2084
|
const allDeps = { ...wsPkg.dependencies, ...wsPkg.devDependencies };
|
|
1600
2085
|
const depNames = Object.keys(allDeps);
|
|
@@ -1638,14 +2123,14 @@ var MonorepoResolver = class {
|
|
|
1638
2123
|
* Supports: npm/yarn workspaces (package.json), pnpm-workspace.yaml
|
|
1639
2124
|
*/
|
|
1640
2125
|
resolveWorkspaceGlobs(projectPath) {
|
|
1641
|
-
const pnpmPath = (0,
|
|
1642
|
-
if ((0,
|
|
2126
|
+
const pnpmPath = (0, import_node_path5.join)(projectPath, "pnpm-workspace.yaml");
|
|
2127
|
+
if ((0, import_node_fs5.existsSync)(pnpmPath)) {
|
|
1643
2128
|
return this.parsePnpmWorkspace(pnpmPath);
|
|
1644
2129
|
}
|
|
1645
|
-
const pkgPath = (0,
|
|
1646
|
-
if ((0,
|
|
2130
|
+
const pkgPath = (0, import_node_path5.join)(projectPath, "package.json");
|
|
2131
|
+
if ((0, import_node_fs5.existsSync)(pkgPath)) {
|
|
1647
2132
|
try {
|
|
1648
|
-
const pkg = JSON.parse((0,
|
|
2133
|
+
const pkg = JSON.parse((0, import_node_fs5.readFileSync)(pkgPath, "utf-8"));
|
|
1649
2134
|
if (pkg.workspaces) {
|
|
1650
2135
|
return Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages;
|
|
1651
2136
|
}
|
|
@@ -1664,7 +2149,7 @@ var MonorepoResolver = class {
|
|
|
1664
2149
|
* ```
|
|
1665
2150
|
*/
|
|
1666
2151
|
parsePnpmWorkspace(filePath) {
|
|
1667
|
-
const content = (0,
|
|
2152
|
+
const content = (0, import_node_fs5.readFileSync)(filePath, "utf-8");
|
|
1668
2153
|
const globs = [];
|
|
1669
2154
|
let inPackages = false;
|
|
1670
2155
|
for (const line of content.split("\n")) {
|
|
@@ -1689,14 +2174,14 @@ var MonorepoResolver = class {
|
|
|
1689
2174
|
const dirs = [];
|
|
1690
2175
|
for (const glob of globs) {
|
|
1691
2176
|
const clean = glob.replace(/\/?\*$/, "");
|
|
1692
|
-
const base = (0,
|
|
1693
|
-
if (!(0,
|
|
2177
|
+
const base = (0, import_node_path5.resolve)(projectPath, clean);
|
|
2178
|
+
if (!(0, import_node_fs5.existsSync)(base)) continue;
|
|
1694
2179
|
if (glob.endsWith("*")) {
|
|
1695
2180
|
try {
|
|
1696
|
-
const entries = (0,
|
|
2181
|
+
const entries = (0, import_node_fs5.readdirSync)(base, { withFileTypes: true });
|
|
1697
2182
|
for (const entry of entries) {
|
|
1698
2183
|
if (entry.isDirectory()) {
|
|
1699
|
-
dirs.push((0,
|
|
2184
|
+
dirs.push((0, import_node_path5.join)(base, entry.name));
|
|
1700
2185
|
}
|
|
1701
2186
|
}
|
|
1702
2187
|
} catch {
|
|
@@ -1710,8 +2195,8 @@ var MonorepoResolver = class {
|
|
|
1710
2195
|
};
|
|
1711
2196
|
|
|
1712
2197
|
// src/detailed-analyzer.ts
|
|
1713
|
-
var
|
|
1714
|
-
var
|
|
2198
|
+
var import_node_fs6 = require("fs");
|
|
2199
|
+
var import_node_path6 = require("path");
|
|
1715
2200
|
var COMPLEXITY_CHAIN_WEIGHT = 2;
|
|
1716
2201
|
var COMPLEXITY_DEPTH_WEIGHT = 3;
|
|
1717
2202
|
var COMPLEXITY_VALIDATION_WEIGHT = 1;
|
|
@@ -1776,10 +2261,10 @@ var DetailedAnalyzer = class {
|
|
|
1776
2261
|
}
|
|
1777
2262
|
detectLibraryVersions(projectPath) {
|
|
1778
2263
|
const versions = [];
|
|
1779
|
-
const pkgPath = (0,
|
|
1780
|
-
if (!(0,
|
|
2264
|
+
const pkgPath = (0, import_node_path6.join)(projectPath, "package.json");
|
|
2265
|
+
if (!(0, import_node_fs6.existsSync)(pkgPath)) return versions;
|
|
1781
2266
|
try {
|
|
1782
|
-
const pkg = JSON.parse((0,
|
|
2267
|
+
const pkg = JSON.parse((0, import_node_fs6.readFileSync)(pkgPath, "utf-8"));
|
|
1783
2268
|
const knownLibs = ["zod", "yup", "joi", "io-ts", "valibot"];
|
|
1784
2269
|
const allDeps = {
|
|
1785
2270
|
...pkg.dependencies,
|
|
@@ -1952,24 +2437,183 @@ var DetailedAnalyzer = class {
|
|
|
1952
2437
|
}
|
|
1953
2438
|
};
|
|
1954
2439
|
|
|
1955
|
-
// src/
|
|
1956
|
-
var
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
2440
|
+
// src/drift-detector.ts
|
|
2441
|
+
var import_node_crypto2 = require("crypto");
|
|
2442
|
+
var import_node_fs7 = require("fs");
|
|
2443
|
+
var import_node_path7 = require("path");
|
|
2444
|
+
var SNAPSHOT_DIR = ".schemashift";
|
|
2445
|
+
var SNAPSHOT_FILE = "schema-snapshot.json";
|
|
2446
|
+
var SNAPSHOT_VERSION = 1;
|
|
2447
|
+
var DriftDetector = class {
|
|
2448
|
+
snapshotDir;
|
|
2449
|
+
snapshotPath;
|
|
2450
|
+
constructor(projectPath) {
|
|
2451
|
+
this.snapshotDir = (0, import_node_path7.join)(projectPath, SNAPSHOT_DIR);
|
|
2452
|
+
this.snapshotPath = (0, import_node_path7.join)(this.snapshotDir, SNAPSHOT_FILE);
|
|
2453
|
+
}
|
|
2454
|
+
/**
|
|
2455
|
+
* Take a snapshot of the current schema state
|
|
2456
|
+
*/
|
|
2457
|
+
snapshot(files, projectPath) {
|
|
2458
|
+
const schemas = [];
|
|
2459
|
+
for (const filePath of files) {
|
|
2460
|
+
if (!(0, import_node_fs7.existsSync)(filePath)) continue;
|
|
2461
|
+
const content = (0, import_node_fs7.readFileSync)(filePath, "utf-8");
|
|
2462
|
+
const library = this.detectLibraryFromContent(content);
|
|
2463
|
+
if (library === "unknown") continue;
|
|
2464
|
+
const schemaNames = this.extractSchemaNames(content);
|
|
2465
|
+
schemas.push({
|
|
2466
|
+
filePath: (0, import_node_path7.relative)(projectPath, filePath),
|
|
2467
|
+
library,
|
|
2468
|
+
contentHash: this.hashContent(content),
|
|
2469
|
+
schemaCount: schemaNames.length,
|
|
2470
|
+
schemaNames
|
|
2471
|
+
});
|
|
1963
2472
|
}
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
2473
|
+
const snapshot = {
|
|
2474
|
+
version: SNAPSHOT_VERSION,
|
|
2475
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2476
|
+
projectPath,
|
|
2477
|
+
schemas
|
|
2478
|
+
};
|
|
2479
|
+
return snapshot;
|
|
2480
|
+
}
|
|
2481
|
+
/**
|
|
2482
|
+
* Save a snapshot to disk
|
|
2483
|
+
*/
|
|
2484
|
+
saveSnapshot(snapshot) {
|
|
2485
|
+
if (!(0, import_node_fs7.existsSync)(this.snapshotDir)) {
|
|
2486
|
+
(0, import_node_fs7.mkdirSync)(this.snapshotDir, { recursive: true });
|
|
1971
2487
|
}
|
|
1972
|
-
|
|
2488
|
+
(0, import_node_fs7.writeFileSync)(this.snapshotPath, JSON.stringify(snapshot, null, 2));
|
|
2489
|
+
}
|
|
2490
|
+
/**
|
|
2491
|
+
* Load saved snapshot from disk
|
|
2492
|
+
*/
|
|
2493
|
+
loadSnapshot() {
|
|
2494
|
+
if (!(0, import_node_fs7.existsSync)(this.snapshotPath)) {
|
|
2495
|
+
return null;
|
|
2496
|
+
}
|
|
2497
|
+
try {
|
|
2498
|
+
const content = (0, import_node_fs7.readFileSync)(this.snapshotPath, "utf-8");
|
|
2499
|
+
return JSON.parse(content);
|
|
2500
|
+
} catch {
|
|
2501
|
+
return null;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
/**
|
|
2505
|
+
* Compare current state against saved snapshot
|
|
2506
|
+
*/
|
|
2507
|
+
detect(currentFiles, projectPath) {
|
|
2508
|
+
const saved = this.loadSnapshot();
|
|
2509
|
+
if (!saved) {
|
|
2510
|
+
return {
|
|
2511
|
+
hasDrift: false,
|
|
2512
|
+
added: [],
|
|
2513
|
+
removed: [],
|
|
2514
|
+
modified: [],
|
|
2515
|
+
unchanged: 0,
|
|
2516
|
+
totalFiles: 0,
|
|
2517
|
+
snapshotTimestamp: ""
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
const current = this.snapshot(currentFiles, projectPath);
|
|
2521
|
+
return this.compareSnapshots(saved, current);
|
|
2522
|
+
}
|
|
2523
|
+
/**
|
|
2524
|
+
* Compare two snapshots and return drift results
|
|
2525
|
+
*/
|
|
2526
|
+
compareSnapshots(baseline, current) {
|
|
2527
|
+
const baselineMap = new Map(baseline.schemas.map((s) => [s.filePath, s]));
|
|
2528
|
+
const currentMap = new Map(current.schemas.map((s) => [s.filePath, s]));
|
|
2529
|
+
const added = [];
|
|
2530
|
+
const removed = [];
|
|
2531
|
+
const modified = [];
|
|
2532
|
+
let unchanged = 0;
|
|
2533
|
+
for (const [path, currentFile] of currentMap) {
|
|
2534
|
+
const baselineFile = baselineMap.get(path);
|
|
2535
|
+
if (!baselineFile) {
|
|
2536
|
+
added.push(currentFile);
|
|
2537
|
+
} else if (currentFile.contentHash !== baselineFile.contentHash) {
|
|
2538
|
+
const addedSchemas = currentFile.schemaNames.filter(
|
|
2539
|
+
(n) => !baselineFile.schemaNames.includes(n)
|
|
2540
|
+
);
|
|
2541
|
+
const removedSchemas = baselineFile.schemaNames.filter(
|
|
2542
|
+
(n) => !currentFile.schemaNames.includes(n)
|
|
2543
|
+
);
|
|
2544
|
+
modified.push({
|
|
2545
|
+
filePath: path,
|
|
2546
|
+
library: currentFile.library,
|
|
2547
|
+
previousHash: baselineFile.contentHash,
|
|
2548
|
+
currentHash: currentFile.contentHash,
|
|
2549
|
+
previousSchemaCount: baselineFile.schemaCount,
|
|
2550
|
+
currentSchemaCount: currentFile.schemaCount,
|
|
2551
|
+
addedSchemas,
|
|
2552
|
+
removedSchemas
|
|
2553
|
+
});
|
|
2554
|
+
} else {
|
|
2555
|
+
unchanged++;
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
for (const [path, baselineFile] of baselineMap) {
|
|
2559
|
+
if (!currentMap.has(path)) {
|
|
2560
|
+
removed.push(baselineFile);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
return {
|
|
2564
|
+
hasDrift: added.length > 0 || removed.length > 0 || modified.length > 0,
|
|
2565
|
+
added,
|
|
2566
|
+
removed,
|
|
2567
|
+
modified,
|
|
2568
|
+
unchanged,
|
|
2569
|
+
totalFiles: currentMap.size,
|
|
2570
|
+
snapshotTimestamp: baseline.timestamp
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
extractSchemaNames(content) {
|
|
2574
|
+
const names = [];
|
|
2575
|
+
const pattern = /(?:const|let|var)\s+(\w+)\s*=\s*(?:z\.|yup\.|Joi\.|t\.|v\.|type\(|object\(|string\(|S\.)/g;
|
|
2576
|
+
for (const match of content.matchAll(pattern)) {
|
|
2577
|
+
if (match[1]) names.push(match[1]);
|
|
2578
|
+
}
|
|
2579
|
+
return names;
|
|
2580
|
+
}
|
|
2581
|
+
detectLibraryFromContent(content) {
|
|
2582
|
+
if (/from\s*['"]zod['"]/.test(content) || /\bz\./.test(content)) return "zod";
|
|
2583
|
+
if (/from\s*['"]yup['"]/.test(content) || /\byup\./.test(content)) return "yup";
|
|
2584
|
+
if (/from\s*['"]joi['"]/.test(content) || /\bJoi\./.test(content)) return "joi";
|
|
2585
|
+
if (/from\s*['"]io-ts['"]/.test(content) || /\bt\./.test(content) && /from\s*['"]io-ts/.test(content))
|
|
2586
|
+
return "io-ts";
|
|
2587
|
+
if (/from\s*['"]valibot['"]/.test(content) || /\bv\./.test(content) && /from\s*['"]valibot/.test(content))
|
|
2588
|
+
return "valibot";
|
|
2589
|
+
if (/from\s*['"]arktype['"]/.test(content)) return "arktype";
|
|
2590
|
+
if (/from\s*['"]superstruct['"]/.test(content)) return "superstruct";
|
|
2591
|
+
if (/from\s*['"]@effect\/schema['"]/.test(content)) return "effect";
|
|
2592
|
+
return "unknown";
|
|
2593
|
+
}
|
|
2594
|
+
hashContent(content) {
|
|
2595
|
+
return (0, import_node_crypto2.createHash)("sha256").update(content).digest("hex").substring(0, 16);
|
|
2596
|
+
}
|
|
2597
|
+
};
|
|
2598
|
+
|
|
2599
|
+
// src/form-resolver-migrator.ts
|
|
2600
|
+
var RESOLVER_MAPPINGS = {
|
|
2601
|
+
"yup->zod": [
|
|
2602
|
+
{
|
|
2603
|
+
fromImport: "@hookform/resolvers/yup",
|
|
2604
|
+
toImport: "@hookform/resolvers/zod",
|
|
2605
|
+
fromResolver: "yupResolver",
|
|
2606
|
+
toResolver: "zodResolver"
|
|
2607
|
+
}
|
|
2608
|
+
],
|
|
2609
|
+
"joi->zod": [
|
|
2610
|
+
{
|
|
2611
|
+
fromImport: "@hookform/resolvers/joi",
|
|
2612
|
+
toImport: "@hookform/resolvers/zod",
|
|
2613
|
+
fromResolver: "joiResolver",
|
|
2614
|
+
toResolver: "zodResolver"
|
|
2615
|
+
}
|
|
2616
|
+
],
|
|
1973
2617
|
"zod->valibot": [
|
|
1974
2618
|
{
|
|
1975
2619
|
fromImport: "@hookform/resolvers/zod",
|
|
@@ -2313,17 +2957,676 @@ var GovernanceEngine = class {
|
|
|
2313
2957
|
}
|
|
2314
2958
|
};
|
|
2315
2959
|
|
|
2960
|
+
// src/governance-fixer.ts
|
|
2961
|
+
var GovernanceFixer = class {
|
|
2962
|
+
defaultMaxLength = 1e4;
|
|
2963
|
+
/**
|
|
2964
|
+
* Set the default max length appended by the require-max-length fix.
|
|
2965
|
+
*/
|
|
2966
|
+
setDefaultMaxLength(length) {
|
|
2967
|
+
this.defaultMaxLength = length;
|
|
2968
|
+
}
|
|
2969
|
+
/**
|
|
2970
|
+
* Check if a violation is auto-fixable.
|
|
2971
|
+
*/
|
|
2972
|
+
canFix(violation) {
|
|
2973
|
+
return [
|
|
2974
|
+
"no-any-schemas",
|
|
2975
|
+
"require-descriptions",
|
|
2976
|
+
"require-max-length",
|
|
2977
|
+
"naming-convention",
|
|
2978
|
+
"no-any",
|
|
2979
|
+
"require-description",
|
|
2980
|
+
"required-validations",
|
|
2981
|
+
"require-safeParse"
|
|
2982
|
+
].includes(violation.rule);
|
|
2983
|
+
}
|
|
2984
|
+
/**
|
|
2985
|
+
* Fix a single violation in a source file.
|
|
2986
|
+
* Returns the fixed code for the entire file.
|
|
2987
|
+
*/
|
|
2988
|
+
fix(violation, sourceCode) {
|
|
2989
|
+
switch (violation.rule) {
|
|
2990
|
+
case "no-any-schemas":
|
|
2991
|
+
case "no-any":
|
|
2992
|
+
return this.fixNoAny(violation, sourceCode);
|
|
2993
|
+
case "require-descriptions":
|
|
2994
|
+
case "require-description":
|
|
2995
|
+
return this.fixRequireDescription(violation, sourceCode);
|
|
2996
|
+
case "require-max-length":
|
|
2997
|
+
case "required-validations":
|
|
2998
|
+
return this.fixRequireMaxLength(violation, sourceCode);
|
|
2999
|
+
case "naming-convention":
|
|
3000
|
+
return this.fixNamingConvention(violation, sourceCode);
|
|
3001
|
+
case "require-safeParse":
|
|
3002
|
+
return this.fixRequireSafeParse(violation, sourceCode);
|
|
3003
|
+
default:
|
|
3004
|
+
return {
|
|
3005
|
+
success: false,
|
|
3006
|
+
explanation: `No auto-fix available for rule: ${violation.rule}`,
|
|
3007
|
+
rule: violation.rule,
|
|
3008
|
+
lineNumber: violation.lineNumber
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
/**
|
|
3013
|
+
* Fix all fixable violations in a source file.
|
|
3014
|
+
* Applies fixes from bottom to top to preserve line numbers.
|
|
3015
|
+
*/
|
|
3016
|
+
fixAll(violations, sourceCode) {
|
|
3017
|
+
const fixable = violations.filter((v) => this.canFix(v));
|
|
3018
|
+
const results = [];
|
|
3019
|
+
let currentCode = sourceCode;
|
|
3020
|
+
let fixed = 0;
|
|
3021
|
+
const sorted = [...fixable].sort((a, b) => b.lineNumber - a.lineNumber);
|
|
3022
|
+
for (const violation of sorted) {
|
|
3023
|
+
const result = this.fix(violation, currentCode);
|
|
3024
|
+
results.push(result);
|
|
3025
|
+
if (result.success && result.fixedCode) {
|
|
3026
|
+
currentCode = result.fixedCode;
|
|
3027
|
+
fixed++;
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
return {
|
|
3031
|
+
totalViolations: violations.length,
|
|
3032
|
+
fixed,
|
|
3033
|
+
skipped: violations.length - fixed,
|
|
3034
|
+
results
|
|
3035
|
+
};
|
|
3036
|
+
}
|
|
3037
|
+
fixNoAny(violation, sourceCode) {
|
|
3038
|
+
const lines = sourceCode.split("\n");
|
|
3039
|
+
const lineIndex = violation.lineNumber - 1;
|
|
3040
|
+
const line = lines[lineIndex];
|
|
3041
|
+
if (!line) {
|
|
3042
|
+
return {
|
|
3043
|
+
success: false,
|
|
3044
|
+
explanation: `Line ${violation.lineNumber} not found`,
|
|
3045
|
+
rule: violation.rule,
|
|
3046
|
+
lineNumber: violation.lineNumber
|
|
3047
|
+
};
|
|
3048
|
+
}
|
|
3049
|
+
let fixedLine = line;
|
|
3050
|
+
let explanation = "";
|
|
3051
|
+
if (/\bz\.any\(\)/.test(line)) {
|
|
3052
|
+
fixedLine = line.replace(/\bz\.any\(\)/, "z.unknown()");
|
|
3053
|
+
explanation = "Replaced z.any() with z.unknown() for type safety";
|
|
3054
|
+
} else if (/\byup\.mixed\(\)/.test(line)) {
|
|
3055
|
+
fixedLine = line.replace(/\byup\.mixed\(\)/, "yup.mixed().required()");
|
|
3056
|
+
explanation = "Added .required() constraint to yup.mixed()";
|
|
3057
|
+
} else if (/\bt\.any\b/.test(line)) {
|
|
3058
|
+
fixedLine = line.replace(/\bt\.any\b/, "t.unknown");
|
|
3059
|
+
explanation = "Replaced t.any with t.unknown for type safety";
|
|
3060
|
+
} else if (/\bv\.any\(\)/.test(line)) {
|
|
3061
|
+
fixedLine = line.replace(/\bv\.any\(\)/, "v.unknown()");
|
|
3062
|
+
explanation = "Replaced v.any() with v.unknown() for type safety";
|
|
3063
|
+
} else {
|
|
3064
|
+
return {
|
|
3065
|
+
success: false,
|
|
3066
|
+
explanation: "Could not identify any-type pattern to fix",
|
|
3067
|
+
rule: violation.rule,
|
|
3068
|
+
lineNumber: violation.lineNumber
|
|
3069
|
+
};
|
|
3070
|
+
}
|
|
3071
|
+
lines[lineIndex] = fixedLine;
|
|
3072
|
+
return {
|
|
3073
|
+
success: true,
|
|
3074
|
+
fixedCode: lines.join("\n"),
|
|
3075
|
+
explanation,
|
|
3076
|
+
rule: violation.rule,
|
|
3077
|
+
lineNumber: violation.lineNumber
|
|
3078
|
+
};
|
|
3079
|
+
}
|
|
3080
|
+
fixRequireDescription(violation, sourceCode) {
|
|
3081
|
+
const lines = sourceCode.split("\n");
|
|
3082
|
+
const lineIndex = violation.lineNumber - 1;
|
|
3083
|
+
const line = lines[lineIndex];
|
|
3084
|
+
if (!line) {
|
|
3085
|
+
return {
|
|
3086
|
+
success: false,
|
|
3087
|
+
explanation: `Line ${violation.lineNumber} not found`,
|
|
3088
|
+
rule: violation.rule,
|
|
3089
|
+
lineNumber: violation.lineNumber
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
let endLineIndex = lineIndex;
|
|
3093
|
+
for (let i = lineIndex; i < lines.length && i < lineIndex + 20; i++) {
|
|
3094
|
+
if (lines[i]?.includes(";")) {
|
|
3095
|
+
endLineIndex = i;
|
|
3096
|
+
break;
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
const endLine = lines[endLineIndex] ?? "";
|
|
3100
|
+
const schemaName = violation.schemaName || "schema";
|
|
3101
|
+
const description = `${schemaName} schema`;
|
|
3102
|
+
const semicolonIndex = endLine.lastIndexOf(";");
|
|
3103
|
+
if (semicolonIndex >= 0) {
|
|
3104
|
+
lines[endLineIndex] = `${endLine.slice(0, semicolonIndex)}.describe('${description}')${endLine.slice(semicolonIndex)}`;
|
|
3105
|
+
} else {
|
|
3106
|
+
lines[endLineIndex] = `${endLine}.describe('${description}')`;
|
|
3107
|
+
}
|
|
3108
|
+
return {
|
|
3109
|
+
success: true,
|
|
3110
|
+
fixedCode: lines.join("\n"),
|
|
3111
|
+
explanation: `Added .describe('${description}') to ${schemaName}`,
|
|
3112
|
+
rule: violation.rule,
|
|
3113
|
+
lineNumber: violation.lineNumber
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
fixRequireMaxLength(violation, sourceCode) {
|
|
3117
|
+
const lines = sourceCode.split("\n");
|
|
3118
|
+
const lineIndex = violation.lineNumber - 1;
|
|
3119
|
+
const line = lines[lineIndex];
|
|
3120
|
+
if (!line) {
|
|
3121
|
+
return {
|
|
3122
|
+
success: false,
|
|
3123
|
+
explanation: `Line ${violation.lineNumber} not found`,
|
|
3124
|
+
rule: violation.rule,
|
|
3125
|
+
lineNumber: violation.lineNumber
|
|
3126
|
+
};
|
|
3127
|
+
}
|
|
3128
|
+
if (/z\.string\(\)/.test(line)) {
|
|
3129
|
+
lines[lineIndex] = line.replace(/z\.string\(\)/, `z.string().max(${this.defaultMaxLength})`);
|
|
3130
|
+
return {
|
|
3131
|
+
success: true,
|
|
3132
|
+
fixedCode: lines.join("\n"),
|
|
3133
|
+
explanation: `Added .max(${this.defaultMaxLength}) to string schema`,
|
|
3134
|
+
rule: violation.rule,
|
|
3135
|
+
lineNumber: violation.lineNumber
|
|
3136
|
+
};
|
|
3137
|
+
}
|
|
3138
|
+
return {
|
|
3139
|
+
success: false,
|
|
3140
|
+
explanation: "Could not find z.string() pattern to fix on this line",
|
|
3141
|
+
rule: violation.rule,
|
|
3142
|
+
lineNumber: violation.lineNumber
|
|
3143
|
+
};
|
|
3144
|
+
}
|
|
3145
|
+
fixNamingConvention(violation, sourceCode) {
|
|
3146
|
+
const schemaName = violation.schemaName;
|
|
3147
|
+
if (!schemaName) {
|
|
3148
|
+
return {
|
|
3149
|
+
success: false,
|
|
3150
|
+
explanation: "No schema name available for renaming",
|
|
3151
|
+
rule: violation.rule,
|
|
3152
|
+
lineNumber: violation.lineNumber
|
|
3153
|
+
};
|
|
3154
|
+
}
|
|
3155
|
+
const newName = schemaName.endsWith("Schema") ? schemaName : `${schemaName}Schema`;
|
|
3156
|
+
if (newName === schemaName) {
|
|
3157
|
+
return {
|
|
3158
|
+
success: false,
|
|
3159
|
+
explanation: "Schema already matches naming convention",
|
|
3160
|
+
rule: violation.rule,
|
|
3161
|
+
lineNumber: violation.lineNumber
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
const fixedCode = sourceCode.replace(new RegExp(`\\b${schemaName}\\b`, "g"), newName);
|
|
3165
|
+
return {
|
|
3166
|
+
success: true,
|
|
3167
|
+
fixedCode,
|
|
3168
|
+
explanation: `Renamed "${schemaName}" to "${newName}"`,
|
|
3169
|
+
rule: violation.rule,
|
|
3170
|
+
lineNumber: violation.lineNumber
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
3173
|
+
fixRequireSafeParse(violation, sourceCode) {
|
|
3174
|
+
const lines = sourceCode.split("\n");
|
|
3175
|
+
const lineIndex = violation.lineNumber - 1;
|
|
3176
|
+
const line = lines[lineIndex];
|
|
3177
|
+
if (!line) {
|
|
3178
|
+
return {
|
|
3179
|
+
success: false,
|
|
3180
|
+
explanation: `Line ${violation.lineNumber} not found`,
|
|
3181
|
+
rule: violation.rule,
|
|
3182
|
+
lineNumber: violation.lineNumber
|
|
3183
|
+
};
|
|
3184
|
+
}
|
|
3185
|
+
if (line.includes(".parse(") && !line.includes(".safeParse(")) {
|
|
3186
|
+
lines[lineIndex] = line.replace(".parse(", ".safeParse(");
|
|
3187
|
+
return {
|
|
3188
|
+
success: true,
|
|
3189
|
+
fixedCode: lines.join("\n"),
|
|
3190
|
+
explanation: "Replaced .parse() with .safeParse() for safer error handling",
|
|
3191
|
+
rule: violation.rule,
|
|
3192
|
+
lineNumber: violation.lineNumber
|
|
3193
|
+
};
|
|
3194
|
+
}
|
|
3195
|
+
return {
|
|
3196
|
+
success: false,
|
|
3197
|
+
explanation: "Could not find .parse() pattern to fix",
|
|
3198
|
+
rule: violation.rule,
|
|
3199
|
+
lineNumber: violation.lineNumber
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
};
|
|
3203
|
+
|
|
3204
|
+
// src/governance-templates.ts
|
|
3205
|
+
var GOVERNANCE_TEMPLATES = [
|
|
3206
|
+
{
|
|
3207
|
+
name: "no-any-schemas",
|
|
3208
|
+
description: "Disallow z.any(), yup.mixed() without constraints, and similar unrestricted types",
|
|
3209
|
+
category: "security",
|
|
3210
|
+
rule: (sourceFile, _config) => {
|
|
3211
|
+
const violations = [];
|
|
3212
|
+
const text = sourceFile.getFullText();
|
|
3213
|
+
const filePath = sourceFile.getFilePath();
|
|
3214
|
+
const lines = text.split("\n");
|
|
3215
|
+
const anyPatterns = [
|
|
3216
|
+
/\bz\.any\(\)/,
|
|
3217
|
+
/\byup\.mixed\(\)/,
|
|
3218
|
+
/\bt\.any\b/,
|
|
3219
|
+
/\bv\.any\(\)/,
|
|
3220
|
+
/\bunknown\(\)/
|
|
3221
|
+
];
|
|
3222
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3223
|
+
const line = lines[i] ?? "";
|
|
3224
|
+
for (const pattern of anyPatterns) {
|
|
3225
|
+
if (pattern.test(line)) {
|
|
3226
|
+
violations.push({
|
|
3227
|
+
rule: "no-any-schemas",
|
|
3228
|
+
message: "Unrestricted type (any/mixed/unknown) found. Use a specific type with constraints.",
|
|
3229
|
+
filePath,
|
|
3230
|
+
lineNumber: i + 1,
|
|
3231
|
+
schemaName: "",
|
|
3232
|
+
severity: "error",
|
|
3233
|
+
fixable: false
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
return violations;
|
|
3239
|
+
}
|
|
3240
|
+
},
|
|
3241
|
+
{
|
|
3242
|
+
name: "require-descriptions",
|
|
3243
|
+
description: "All exported schemas must have .describe() for documentation",
|
|
3244
|
+
category: "quality",
|
|
3245
|
+
rule: (sourceFile, _config) => {
|
|
3246
|
+
const violations = [];
|
|
3247
|
+
const text = sourceFile.getFullText();
|
|
3248
|
+
const filePath = sourceFile.getFilePath();
|
|
3249
|
+
const lines = text.split("\n");
|
|
3250
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3251
|
+
const line = lines[i] ?? "";
|
|
3252
|
+
if (/export\s+(const|let)\s+\w+.*=\s*(z\.|yup\.)/.test(line)) {
|
|
3253
|
+
let fullStatement = line;
|
|
3254
|
+
let j = i + 1;
|
|
3255
|
+
while (j < lines.length && !lines[j]?.includes(";") && j < i + 10) {
|
|
3256
|
+
fullStatement += lines[j] ?? "";
|
|
3257
|
+
j++;
|
|
3258
|
+
}
|
|
3259
|
+
if (j < lines.length) fullStatement += lines[j] ?? "";
|
|
3260
|
+
if (!fullStatement.includes(".describe(")) {
|
|
3261
|
+
const nameMatch = line.match(/(?:const|let)\s+(\w+)/);
|
|
3262
|
+
violations.push({
|
|
3263
|
+
rule: "require-descriptions",
|
|
3264
|
+
message: `Exported schema ${nameMatch?.[1] || "unknown"} should include .describe() for documentation.`,
|
|
3265
|
+
filePath,
|
|
3266
|
+
lineNumber: i + 1,
|
|
3267
|
+
schemaName: nameMatch?.[1] || "",
|
|
3268
|
+
severity: "warning",
|
|
3269
|
+
fixable: true
|
|
3270
|
+
});
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
return violations;
|
|
3275
|
+
}
|
|
3276
|
+
},
|
|
3277
|
+
{
|
|
3278
|
+
name: "max-nesting-depth",
|
|
3279
|
+
description: "Limit schema nesting depth to prevent TypeScript performance issues",
|
|
3280
|
+
category: "performance",
|
|
3281
|
+
rule: (sourceFile, config) => {
|
|
3282
|
+
const violations = [];
|
|
3283
|
+
const text = sourceFile.getFullText();
|
|
3284
|
+
const filePath = sourceFile.getFilePath();
|
|
3285
|
+
const maxDepth = config.threshold || 5;
|
|
3286
|
+
const lines = text.split("\n");
|
|
3287
|
+
let currentDepth = 0;
|
|
3288
|
+
let maxFoundDepth = 0;
|
|
3289
|
+
let deepestLine = 0;
|
|
3290
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3291
|
+
const line = lines[i] ?? "";
|
|
3292
|
+
for (const char of line) {
|
|
3293
|
+
if (char === "(" || char === "{" || char === "[") {
|
|
3294
|
+
currentDepth++;
|
|
3295
|
+
if (currentDepth > maxFoundDepth) {
|
|
3296
|
+
maxFoundDepth = currentDepth;
|
|
3297
|
+
deepestLine = i + 1;
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
if (char === ")" || char === "}" || char === "]") {
|
|
3301
|
+
currentDepth = Math.max(0, currentDepth - 1);
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
if (maxFoundDepth > maxDepth) {
|
|
3306
|
+
violations.push({
|
|
3307
|
+
rule: "max-nesting-depth",
|
|
3308
|
+
message: `Schema nesting depth ${maxFoundDepth} exceeds maximum of ${maxDepth}. Consider breaking into smaller schemas.`,
|
|
3309
|
+
filePath,
|
|
3310
|
+
lineNumber: deepestLine,
|
|
3311
|
+
schemaName: "",
|
|
3312
|
+
severity: "warning",
|
|
3313
|
+
fixable: false
|
|
3314
|
+
});
|
|
3315
|
+
}
|
|
3316
|
+
return violations;
|
|
3317
|
+
}
|
|
3318
|
+
},
|
|
3319
|
+
{
|
|
3320
|
+
name: "no-deprecated-methods",
|
|
3321
|
+
description: "Flag usage of deprecated schema methods",
|
|
3322
|
+
category: "quality",
|
|
3323
|
+
rule: (sourceFile, _config) => {
|
|
3324
|
+
const violations = [];
|
|
3325
|
+
const text = sourceFile.getFullText();
|
|
3326
|
+
const filePath = sourceFile.getFilePath();
|
|
3327
|
+
const lines = text.split("\n");
|
|
3328
|
+
const deprecatedPatterns = [
|
|
3329
|
+
{
|
|
3330
|
+
pattern: /\.deepPartial\(\)/,
|
|
3331
|
+
message: ".deepPartial() is removed in Zod v4. Use recursive .partial() instead."
|
|
3332
|
+
},
|
|
3333
|
+
{
|
|
3334
|
+
pattern: /\.strip\(\)/,
|
|
3335
|
+
message: ".strip() is deprecated. Use z.strictObject() or explicit stripping."
|
|
3336
|
+
},
|
|
3337
|
+
{
|
|
3338
|
+
pattern: /z\.promise\(/,
|
|
3339
|
+
message: "z.promise() is deprecated in Zod v4. Use native Promise types."
|
|
3340
|
+
},
|
|
3341
|
+
{
|
|
3342
|
+
pattern: /z\.ostring\(\)/,
|
|
3343
|
+
message: "z.ostring() is removed in Zod v4. Use z.string().optional()."
|
|
3344
|
+
},
|
|
3345
|
+
{
|
|
3346
|
+
pattern: /z\.onumber\(\)/,
|
|
3347
|
+
message: "z.onumber() is removed in Zod v4. Use z.number().optional()."
|
|
3348
|
+
},
|
|
3349
|
+
{
|
|
3350
|
+
pattern: /z\.oboolean\(\)/,
|
|
3351
|
+
message: "z.oboolean() is removed in Zod v4. Use z.boolean().optional()."
|
|
3352
|
+
},
|
|
3353
|
+
{
|
|
3354
|
+
pattern: /z\.preprocess\(/,
|
|
3355
|
+
message: "z.preprocess() is removed in Zod v4. Use z.coerce.* instead."
|
|
3356
|
+
}
|
|
3357
|
+
];
|
|
3358
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3359
|
+
const line = lines[i] ?? "";
|
|
3360
|
+
for (const { pattern, message } of deprecatedPatterns) {
|
|
3361
|
+
if (pattern.test(line)) {
|
|
3362
|
+
violations.push({
|
|
3363
|
+
rule: "no-deprecated-methods",
|
|
3364
|
+
message,
|
|
3365
|
+
filePath,
|
|
3366
|
+
lineNumber: i + 1,
|
|
3367
|
+
schemaName: "",
|
|
3368
|
+
severity: "warning",
|
|
3369
|
+
fixable: false
|
|
3370
|
+
});
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
return violations;
|
|
3375
|
+
}
|
|
3376
|
+
},
|
|
3377
|
+
{
|
|
3378
|
+
name: "naming-convention",
|
|
3379
|
+
description: "Enforce schema naming pattern (e.g., must end with Schema)",
|
|
3380
|
+
category: "quality",
|
|
3381
|
+
rule: (sourceFile, config) => {
|
|
3382
|
+
const violations = [];
|
|
3383
|
+
const text = sourceFile.getFullText();
|
|
3384
|
+
const filePath = sourceFile.getFilePath();
|
|
3385
|
+
const lines = text.split("\n");
|
|
3386
|
+
const pattern = new RegExp(config.pattern || ".*Schema$");
|
|
3387
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3388
|
+
const line = lines[i] ?? "";
|
|
3389
|
+
const match = line.match(
|
|
3390
|
+
/(?:const|let)\s+(\w+)\s*=\s*(?:z\.|yup\.|Joi\.|t\.|v\.|type\(|object\(|string\()/
|
|
3391
|
+
);
|
|
3392
|
+
if (match?.[1] && !pattern.test(match[1])) {
|
|
3393
|
+
violations.push({
|
|
3394
|
+
rule: "naming-convention",
|
|
3395
|
+
message: `Schema "${match[1]}" does not match naming pattern ${pattern.source}.`,
|
|
3396
|
+
filePath,
|
|
3397
|
+
lineNumber: i + 1,
|
|
3398
|
+
schemaName: match[1],
|
|
3399
|
+
severity: "warning",
|
|
3400
|
+
fixable: false
|
|
3401
|
+
});
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
return violations;
|
|
3405
|
+
}
|
|
3406
|
+
},
|
|
3407
|
+
{
|
|
3408
|
+
name: "require-max-length",
|
|
3409
|
+
description: "String schemas must have .max() to prevent DoS via unbounded input",
|
|
3410
|
+
category: "security",
|
|
3411
|
+
rule: (sourceFile, _config) => {
|
|
3412
|
+
const violations = [];
|
|
3413
|
+
const text = sourceFile.getFullText();
|
|
3414
|
+
const filePath = sourceFile.getFilePath();
|
|
3415
|
+
const lines = text.split("\n");
|
|
3416
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3417
|
+
const line = lines[i] ?? "";
|
|
3418
|
+
if (/z\.string\(\)/.test(line) && !line.includes(".max(") && !line.includes(".length(")) {
|
|
3419
|
+
let fullChain = line;
|
|
3420
|
+
let j = i + 1;
|
|
3421
|
+
while (j < lines.length && j < i + 5 && /^\s*\./.test(lines[j] ?? "")) {
|
|
3422
|
+
fullChain += lines[j] ?? "";
|
|
3423
|
+
j++;
|
|
3424
|
+
}
|
|
3425
|
+
if (!fullChain.includes(".max(") && !fullChain.includes(".length(")) {
|
|
3426
|
+
violations.push({
|
|
3427
|
+
rule: "require-max-length",
|
|
3428
|
+
message: "String schema should have .max() to prevent unbounded input (DoS protection).",
|
|
3429
|
+
filePath,
|
|
3430
|
+
lineNumber: i + 1,
|
|
3431
|
+
schemaName: "",
|
|
3432
|
+
severity: "warning",
|
|
3433
|
+
fixable: true
|
|
3434
|
+
});
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
return violations;
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
];
|
|
3442
|
+
function getGovernanceTemplate(name) {
|
|
3443
|
+
return GOVERNANCE_TEMPLATES.find((t) => t.name === name);
|
|
3444
|
+
}
|
|
3445
|
+
function getGovernanceTemplatesByCategory(category) {
|
|
3446
|
+
return GOVERNANCE_TEMPLATES.filter((t) => t.category === category);
|
|
3447
|
+
}
|
|
3448
|
+
function getGovernanceTemplateNames() {
|
|
3449
|
+
return GOVERNANCE_TEMPLATES.map((t) => t.name);
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
// src/graph-exporter.ts
|
|
3453
|
+
var LIBRARY_COLORS = {
|
|
3454
|
+
zod: "#3068B7",
|
|
3455
|
+
yup: "#32CD32",
|
|
3456
|
+
joi: "#FF6347",
|
|
3457
|
+
"io-ts": "#9370DB",
|
|
3458
|
+
valibot: "#FF8C00",
|
|
3459
|
+
arktype: "#20B2AA",
|
|
3460
|
+
superstruct: "#DAA520",
|
|
3461
|
+
effect: "#6A5ACD"
|
|
3462
|
+
};
|
|
3463
|
+
var LIBRARY_MERMAID_STYLES = {
|
|
3464
|
+
zod: "fill:#3068B7,color:#fff",
|
|
3465
|
+
yup: "fill:#32CD32,color:#000",
|
|
3466
|
+
joi: "fill:#FF6347,color:#fff",
|
|
3467
|
+
"io-ts": "fill:#9370DB,color:#fff",
|
|
3468
|
+
valibot: "fill:#FF8C00,color:#000",
|
|
3469
|
+
arktype: "fill:#20B2AA,color:#fff",
|
|
3470
|
+
superstruct: "fill:#DAA520,color:#000",
|
|
3471
|
+
effect: "fill:#6A5ACD,color:#fff"
|
|
3472
|
+
};
|
|
3473
|
+
var GraphExporter = class {
|
|
3474
|
+
/**
|
|
3475
|
+
* Export dependency graph as DOT format for Graphviz.
|
|
3476
|
+
*/
|
|
3477
|
+
exportDot(graph, options = {}) {
|
|
3478
|
+
const lines = [];
|
|
3479
|
+
lines.push("digraph SchemaShiftDependencies {");
|
|
3480
|
+
lines.push(" rankdir=LR;");
|
|
3481
|
+
lines.push(' node [shape=box, style=filled, fontname="monospace"];');
|
|
3482
|
+
lines.push(' edge [color="#666666"];');
|
|
3483
|
+
lines.push("");
|
|
3484
|
+
const circularFiles = /* @__PURE__ */ new Set();
|
|
3485
|
+
if (options.highlightCircular && graph.circularWarnings.length > 0) {
|
|
3486
|
+
for (const warning of graph.circularWarnings) {
|
|
3487
|
+
const match = warning.match(/Circular dependency: (.+)/);
|
|
3488
|
+
if (match?.[1]) {
|
|
3489
|
+
for (const part of match[1].split(" -> ")) {
|
|
3490
|
+
for (const file of graph.sortedFiles) {
|
|
3491
|
+
if (file.endsWith(part.trim()) || this.shortenPath(file) === part.trim()) {
|
|
3492
|
+
circularFiles.add(file);
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
for (const filePath of graph.sortedFiles) {
|
|
3500
|
+
const meta = options.nodeMetadata?.get(filePath);
|
|
3501
|
+
const library = meta?.library;
|
|
3502
|
+
if (options.filterLibrary && library !== options.filterLibrary) continue;
|
|
3503
|
+
const shortPath = this.shortenPath(filePath);
|
|
3504
|
+
const nodeId = this.toNodeId(filePath);
|
|
3505
|
+
const attrs = [];
|
|
3506
|
+
attrs.push(`label="${shortPath}"`);
|
|
3507
|
+
if (circularFiles.has(filePath)) {
|
|
3508
|
+
attrs.push('color="#FF0000"');
|
|
3509
|
+
attrs.push("penwidth=2");
|
|
3510
|
+
}
|
|
3511
|
+
if (options.colorByLibrary && library && LIBRARY_COLORS[library]) {
|
|
3512
|
+
attrs.push(`fillcolor="${LIBRARY_COLORS[library]}"`);
|
|
3513
|
+
attrs.push('fontcolor="white"');
|
|
3514
|
+
} else {
|
|
3515
|
+
attrs.push('fillcolor="#E8E8E8"');
|
|
3516
|
+
}
|
|
3517
|
+
if (meta?.schemaCount) {
|
|
3518
|
+
attrs.push(`tooltip="${meta.schemaCount} schema(s)"`);
|
|
3519
|
+
}
|
|
3520
|
+
lines.push(` ${nodeId} [${attrs.join(", ")}];`);
|
|
3521
|
+
}
|
|
3522
|
+
lines.push("");
|
|
3523
|
+
const filterSet = options.filterLibrary ? new Set(
|
|
3524
|
+
graph.sortedFiles.filter((f) => {
|
|
3525
|
+
const meta = options.nodeMetadata?.get(f);
|
|
3526
|
+
return meta?.library === options.filterLibrary;
|
|
3527
|
+
})
|
|
3528
|
+
) : void 0;
|
|
3529
|
+
for (const [file, deps] of graph.dependencies) {
|
|
3530
|
+
if (filterSet && !filterSet.has(file)) continue;
|
|
3531
|
+
const fromId = this.toNodeId(file);
|
|
3532
|
+
for (const dep of deps) {
|
|
3533
|
+
if (filterSet && !filterSet.has(dep)) continue;
|
|
3534
|
+
const toId = this.toNodeId(dep);
|
|
3535
|
+
const edgeAttrs = [];
|
|
3536
|
+
if (options.highlightCircular && circularFiles.has(file) && circularFiles.has(dep)) {
|
|
3537
|
+
edgeAttrs.push('color="#FF0000"');
|
|
3538
|
+
edgeAttrs.push("penwidth=2");
|
|
3539
|
+
}
|
|
3540
|
+
lines.push(
|
|
3541
|
+
` ${fromId} -> ${toId}${edgeAttrs.length > 0 ? ` [${edgeAttrs.join(", ")}]` : ""};`
|
|
3542
|
+
);
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
lines.push("}");
|
|
3546
|
+
return lines.join("\n");
|
|
3547
|
+
}
|
|
3548
|
+
/**
|
|
3549
|
+
* Export dependency graph as Mermaid diagram syntax.
|
|
3550
|
+
*/
|
|
3551
|
+
exportMermaid(graph, options = {}) {
|
|
3552
|
+
const lines = [];
|
|
3553
|
+
lines.push("graph LR");
|
|
3554
|
+
const styledNodes = /* @__PURE__ */ new Map();
|
|
3555
|
+
for (const [file, deps] of graph.dependencies) {
|
|
3556
|
+
const meta = options.nodeMetadata?.get(file);
|
|
3557
|
+
if (options.filterLibrary && meta?.library !== options.filterLibrary) continue;
|
|
3558
|
+
const fromId = this.toMermaidId(file);
|
|
3559
|
+
const fromLabel = this.shortenPath(file);
|
|
3560
|
+
if (meta?.library) {
|
|
3561
|
+
styledNodes.set(fromId, meta.library);
|
|
3562
|
+
}
|
|
3563
|
+
if (deps.length === 0) {
|
|
3564
|
+
lines.push(` ${fromId}["${fromLabel}"]`);
|
|
3565
|
+
}
|
|
3566
|
+
for (const dep of deps) {
|
|
3567
|
+
const depMeta = options.nodeMetadata?.get(dep);
|
|
3568
|
+
if (options.filterLibrary && depMeta?.library !== options.filterLibrary) continue;
|
|
3569
|
+
const toId = this.toMermaidId(dep);
|
|
3570
|
+
const toLabel = this.shortenPath(dep);
|
|
3571
|
+
if (depMeta?.library) {
|
|
3572
|
+
styledNodes.set(toId, depMeta.library);
|
|
3573
|
+
}
|
|
3574
|
+
lines.push(` ${fromId}["${fromLabel}"] --> ${toId}["${toLabel}"]`);
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
for (const file of graph.sortedFiles) {
|
|
3578
|
+
const meta = options.nodeMetadata?.get(file);
|
|
3579
|
+
if (options.filterLibrary && meta?.library !== options.filterLibrary) continue;
|
|
3580
|
+
const id = this.toMermaidId(file);
|
|
3581
|
+
if (!lines.some((l) => l.includes(id))) {
|
|
3582
|
+
lines.push(` ${id}["${this.shortenPath(file)}"]`);
|
|
3583
|
+
if (meta?.library) {
|
|
3584
|
+
styledNodes.set(id, meta.library);
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
if (options.colorByLibrary && styledNodes.size > 0) {
|
|
3589
|
+
lines.push("");
|
|
3590
|
+
const libraryGroups = /* @__PURE__ */ new Map();
|
|
3591
|
+
for (const [nodeId, library] of styledNodes) {
|
|
3592
|
+
const group = libraryGroups.get(library) ?? [];
|
|
3593
|
+
group.push(nodeId);
|
|
3594
|
+
libraryGroups.set(library, group);
|
|
3595
|
+
}
|
|
3596
|
+
for (const [library, nodeIds] of libraryGroups) {
|
|
3597
|
+
const style = LIBRARY_MERMAID_STYLES[library];
|
|
3598
|
+
if (style) {
|
|
3599
|
+
for (const nodeId of nodeIds) {
|
|
3600
|
+
lines.push(` style ${nodeId} ${style}`);
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
return lines.join("\n");
|
|
3606
|
+
}
|
|
3607
|
+
shortenPath(filePath) {
|
|
3608
|
+
const parts = filePath.split("/");
|
|
3609
|
+
return parts.slice(-2).join("/");
|
|
3610
|
+
}
|
|
3611
|
+
toNodeId(filePath) {
|
|
3612
|
+
return filePath.replace(/[^a-zA-Z0-9]/g, "_").replace(/^_+/, "").replace(/_+$/, "");
|
|
3613
|
+
}
|
|
3614
|
+
toMermaidId(filePath) {
|
|
3615
|
+
return filePath.replace(/[^a-zA-Z0-9]/g, "_").replace(/^_+/, "n_").replace(/_+$/, "");
|
|
3616
|
+
}
|
|
3617
|
+
};
|
|
3618
|
+
|
|
2316
3619
|
// src/incremental.ts
|
|
2317
|
-
var
|
|
2318
|
-
var
|
|
3620
|
+
var import_node_fs8 = require("fs");
|
|
3621
|
+
var import_node_path8 = require("path");
|
|
2319
3622
|
var STATE_DIR = ".schemashift";
|
|
2320
3623
|
var STATE_FILE = "incremental.json";
|
|
2321
3624
|
var IncrementalTracker = class {
|
|
2322
3625
|
stateDir;
|
|
2323
3626
|
statePath;
|
|
2324
3627
|
constructor(projectPath) {
|
|
2325
|
-
this.stateDir = (0,
|
|
2326
|
-
this.statePath = (0,
|
|
3628
|
+
this.stateDir = (0, import_node_path8.join)(projectPath, STATE_DIR);
|
|
3629
|
+
this.statePath = (0, import_node_path8.join)(this.stateDir, STATE_FILE);
|
|
2327
3630
|
}
|
|
2328
3631
|
start(files, from, to) {
|
|
2329
3632
|
const state = {
|
|
@@ -2358,9 +3661,9 @@ var IncrementalTracker = class {
|
|
|
2358
3661
|
this.saveState(state);
|
|
2359
3662
|
}
|
|
2360
3663
|
getState() {
|
|
2361
|
-
if (!(0,
|
|
3664
|
+
if (!(0, import_node_fs8.existsSync)(this.statePath)) return null;
|
|
2362
3665
|
try {
|
|
2363
|
-
return JSON.parse((0,
|
|
3666
|
+
return JSON.parse((0, import_node_fs8.readFileSync)(this.statePath, "utf-8"));
|
|
2364
3667
|
} catch {
|
|
2365
3668
|
return null;
|
|
2366
3669
|
}
|
|
@@ -2387,21 +3690,299 @@ var IncrementalTracker = class {
|
|
|
2387
3690
|
};
|
|
2388
3691
|
}
|
|
2389
3692
|
clear() {
|
|
2390
|
-
if ((0,
|
|
2391
|
-
(0,
|
|
3693
|
+
if ((0, import_node_fs8.existsSync)(this.statePath)) {
|
|
3694
|
+
(0, import_node_fs8.unlinkSync)(this.statePath);
|
|
2392
3695
|
}
|
|
2393
3696
|
}
|
|
2394
3697
|
saveState(state) {
|
|
2395
|
-
if (!(0,
|
|
2396
|
-
(0,
|
|
3698
|
+
if (!(0, import_node_fs8.existsSync)(this.stateDir)) {
|
|
3699
|
+
(0, import_node_fs8.mkdirSync)(this.stateDir, { recursive: true });
|
|
3700
|
+
}
|
|
3701
|
+
(0, import_node_fs8.writeFileSync)(this.statePath, JSON.stringify(state, null, 2));
|
|
3702
|
+
}
|
|
3703
|
+
};
|
|
3704
|
+
|
|
3705
|
+
// src/migration-templates.ts
|
|
3706
|
+
var BUILT_IN_TEMPLATES = [
|
|
3707
|
+
{
|
|
3708
|
+
name: "react-hook-form-yup-to-zod",
|
|
3709
|
+
description: "Migrate React Hook Form project from Yup to Zod validation",
|
|
3710
|
+
category: "form-migration",
|
|
3711
|
+
migrationSteps: [{ from: "yup", to: "zod", description: "Convert Yup schemas to Zod schemas" }],
|
|
3712
|
+
preChecks: [
|
|
3713
|
+
{ description: "Ensure @hookform/resolvers is installed" },
|
|
3714
|
+
{ description: "Check for .when() conditional validations that need manual review" }
|
|
3715
|
+
],
|
|
3716
|
+
postSteps: [
|
|
3717
|
+
{
|
|
3718
|
+
description: "Update resolver imports: yupResolver \u2192 zodResolver",
|
|
3719
|
+
command: void 0
|
|
3720
|
+
},
|
|
3721
|
+
{
|
|
3722
|
+
description: "Run tests to verify form validation behavior",
|
|
3723
|
+
command: "npm test"
|
|
3724
|
+
},
|
|
3725
|
+
{
|
|
3726
|
+
description: "Remove Yup dependency if no longer used",
|
|
3727
|
+
command: "npm uninstall yup"
|
|
3728
|
+
}
|
|
3729
|
+
],
|
|
3730
|
+
packageChanges: [
|
|
3731
|
+
{ action: "install", package: "zod", version: "^3.24.0" },
|
|
3732
|
+
{ action: "upgrade", package: "@hookform/resolvers", version: "latest" }
|
|
3733
|
+
],
|
|
3734
|
+
recommendedFlags: ["--cross-file", "--scaffold-tests", "--verbose"],
|
|
3735
|
+
estimatedEffort: "moderate"
|
|
3736
|
+
},
|
|
3737
|
+
{
|
|
3738
|
+
name: "trpc-zod-v3-to-v4",
|
|
3739
|
+
description: "Upgrade tRPC project from Zod v3 to Zod v4",
|
|
3740
|
+
category: "framework-upgrade",
|
|
3741
|
+
migrationSteps: [
|
|
3742
|
+
{ from: "zod-v3", to: "v4", description: "Upgrade Zod v3 schemas to v4 syntax" }
|
|
3743
|
+
],
|
|
3744
|
+
preChecks: [
|
|
3745
|
+
{ description: "Check tRPC version \u2014 v11+ required for Zod v4 compatibility" },
|
|
3746
|
+
{ description: "Check zod-validation-error version \u2014 v5.0.0+ required" },
|
|
3747
|
+
{ description: "Run existing test suite to establish baseline", command: "npm test" }
|
|
3748
|
+
],
|
|
3749
|
+
postSteps: [
|
|
3750
|
+
{
|
|
3751
|
+
description: "Update tRPC to v11 if not already",
|
|
3752
|
+
command: "npm install @trpc/server@latest @trpc/client@latest"
|
|
3753
|
+
},
|
|
3754
|
+
{
|
|
3755
|
+
description: "Update zod-validation-error if used",
|
|
3756
|
+
command: "npm install zod-validation-error@^5.0.0"
|
|
3757
|
+
},
|
|
3758
|
+
{ description: "Review TODO(schemashift) comments for manual fixes" },
|
|
3759
|
+
{ description: "Run tests to verify tRPC router behavior", command: "npm test" }
|
|
3760
|
+
],
|
|
3761
|
+
packageChanges: [
|
|
3762
|
+
{ action: "upgrade", package: "zod", version: "^3.25.0" },
|
|
3763
|
+
{ action: "upgrade", package: "@trpc/server", version: "^11.0.0" }
|
|
3764
|
+
],
|
|
3765
|
+
recommendedFlags: ["--compat-check", "--scaffold-tests", "--verbose"],
|
|
3766
|
+
estimatedEffort: "high"
|
|
3767
|
+
},
|
|
3768
|
+
{
|
|
3769
|
+
name: "express-joi-to-zod",
|
|
3770
|
+
description: "Migrate Express.js API validators from Joi to Zod",
|
|
3771
|
+
category: "library-switch",
|
|
3772
|
+
migrationSteps: [{ from: "joi", to: "zod", description: "Convert Joi schemas to Zod schemas" }],
|
|
3773
|
+
preChecks: [
|
|
3774
|
+
{ description: "Identify middleware using Joi validation" },
|
|
3775
|
+
{ description: "Check for Joi.extend() custom validators that need manual migration" }
|
|
3776
|
+
],
|
|
3777
|
+
postSteps: [
|
|
3778
|
+
{ description: "Update Express middleware to use Zod schemas" },
|
|
3779
|
+
{ description: "Replace celebrate/express-validation with custom Zod middleware" },
|
|
3780
|
+
{ description: "Run API integration tests", command: "npm test" },
|
|
3781
|
+
{ description: "Remove Joi dependency", command: "npm uninstall joi" }
|
|
3782
|
+
],
|
|
3783
|
+
packageChanges: [
|
|
3784
|
+
{ action: "install", package: "zod", version: "^3.24.0" },
|
|
3785
|
+
{ action: "remove", package: "celebrate" }
|
|
3786
|
+
],
|
|
3787
|
+
recommendedFlags: ["--cross-file", "--verbose"],
|
|
3788
|
+
estimatedEffort: "moderate"
|
|
3789
|
+
},
|
|
3790
|
+
{
|
|
3791
|
+
name: "nextjs-form-migration",
|
|
3792
|
+
description: "Migrate Next.js form validation from Yup/Formik to Zod/React Hook Form",
|
|
3793
|
+
category: "form-migration",
|
|
3794
|
+
migrationSteps: [{ from: "yup", to: "zod", description: "Convert Yup schemas to Zod schemas" }],
|
|
3795
|
+
preChecks: [
|
|
3796
|
+
{ description: "Identify all Formik form components" },
|
|
3797
|
+
{ description: "Check for server-side validation using Yup" },
|
|
3798
|
+
{ description: "Run existing tests to establish baseline", command: "npm test" }
|
|
3799
|
+
],
|
|
3800
|
+
postSteps: [
|
|
3801
|
+
{ description: "Replace Formik with React Hook Form + zodResolver" },
|
|
3802
|
+
{ description: "Update server actions to use Zod for validation" },
|
|
3803
|
+
{
|
|
3804
|
+
description: "Install next-safe-action if using server actions",
|
|
3805
|
+
command: "npm install next-safe-action"
|
|
3806
|
+
},
|
|
3807
|
+
{ description: "Run full test suite", command: "npm test" }
|
|
3808
|
+
],
|
|
3809
|
+
packageChanges: [
|
|
3810
|
+
{ action: "install", package: "zod", version: "^3.24.0" },
|
|
3811
|
+
{ action: "install", package: "react-hook-form", version: "^7.0.0" },
|
|
3812
|
+
{ action: "install", package: "@hookform/resolvers", version: "latest" }
|
|
3813
|
+
],
|
|
3814
|
+
recommendedFlags: ["--cross-file", "--scaffold-tests"],
|
|
3815
|
+
estimatedEffort: "high"
|
|
3816
|
+
},
|
|
3817
|
+
{
|
|
3818
|
+
name: "monorepo-staged-migration",
|
|
3819
|
+
description: "Phased monorepo migration with incremental tracking",
|
|
3820
|
+
category: "monorepo",
|
|
3821
|
+
migrationSteps: [
|
|
3822
|
+
{ from: "yup", to: "zod", description: "Convert shared packages first, then applications" }
|
|
3823
|
+
],
|
|
3824
|
+
preChecks: [
|
|
3825
|
+
{ description: "Analyze monorepo workspace structure" },
|
|
3826
|
+
{ description: "Identify shared schema packages used by multiple apps" },
|
|
3827
|
+
{ description: "Ensure all packages build successfully", command: "npm run build" }
|
|
3828
|
+
],
|
|
3829
|
+
postSteps: [
|
|
3830
|
+
{ description: "Run incremental migration starting with leaf packages" },
|
|
3831
|
+
{ description: "Build all packages after each batch", command: "npm run build" },
|
|
3832
|
+
{ description: "Run full test suite", command: "npm test" },
|
|
3833
|
+
{ description: "Review cross-package type compatibility" }
|
|
3834
|
+
],
|
|
3835
|
+
packageChanges: [],
|
|
3836
|
+
recommendedFlags: ["--cross-file", "--incremental", "--compat-check", "--audit"],
|
|
3837
|
+
estimatedEffort: "high"
|
|
3838
|
+
}
|
|
3839
|
+
];
|
|
3840
|
+
function getMigrationTemplate(name) {
|
|
3841
|
+
return BUILT_IN_TEMPLATES.find((t) => t.name === name);
|
|
3842
|
+
}
|
|
3843
|
+
function getMigrationTemplateNames() {
|
|
3844
|
+
return BUILT_IN_TEMPLATES.map((t) => t.name);
|
|
3845
|
+
}
|
|
3846
|
+
function getMigrationTemplatesByCategory(category) {
|
|
3847
|
+
return BUILT_IN_TEMPLATES.filter((t) => t.category === category);
|
|
3848
|
+
}
|
|
3849
|
+
function getAllMigrationTemplates() {
|
|
3850
|
+
return [...BUILT_IN_TEMPLATES];
|
|
3851
|
+
}
|
|
3852
|
+
function validateMigrationTemplate(template) {
|
|
3853
|
+
const errors = [];
|
|
3854
|
+
if (!template.name || template.name.trim().length === 0) {
|
|
3855
|
+
errors.push("Template name is required");
|
|
3856
|
+
}
|
|
3857
|
+
if (!template.description || template.description.trim().length === 0) {
|
|
3858
|
+
errors.push("Template description is required");
|
|
3859
|
+
}
|
|
3860
|
+
if (!template.migrationSteps || template.migrationSteps.length === 0) {
|
|
3861
|
+
errors.push("At least one migration step is required");
|
|
3862
|
+
}
|
|
3863
|
+
for (const step of template.migrationSteps ?? []) {
|
|
3864
|
+
if (!step.from || !step.to) {
|
|
3865
|
+
errors.push(`Migration step must have from and to: ${JSON.stringify(step)}`);
|
|
2397
3866
|
}
|
|
2398
|
-
|
|
3867
|
+
}
|
|
3868
|
+
return { valid: errors.length === 0, errors };
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
// src/notifications.ts
|
|
3872
|
+
async function computeSignature(payload, secret) {
|
|
3873
|
+
const { createHmac } = await import("crypto");
|
|
3874
|
+
return createHmac("sha256", secret).update(payload).digest("hex");
|
|
3875
|
+
}
|
|
3876
|
+
var WebhookNotifier = class {
|
|
3877
|
+
webhooks;
|
|
3878
|
+
constructor(webhooks) {
|
|
3879
|
+
this.webhooks = webhooks;
|
|
3880
|
+
}
|
|
3881
|
+
/**
|
|
3882
|
+
* Create a migration event with current timestamp.
|
|
3883
|
+
*/
|
|
3884
|
+
createEvent(type, details, project) {
|
|
3885
|
+
return {
|
|
3886
|
+
type,
|
|
3887
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3888
|
+
project,
|
|
3889
|
+
details
|
|
3890
|
+
};
|
|
3891
|
+
}
|
|
3892
|
+
/**
|
|
3893
|
+
* Send an event to all matching webhooks.
|
|
3894
|
+
*/
|
|
3895
|
+
async send(event) {
|
|
3896
|
+
const results = [];
|
|
3897
|
+
for (const webhook of this.webhooks) {
|
|
3898
|
+
if (webhook.events && !webhook.events.includes(event.type)) {
|
|
3899
|
+
continue;
|
|
3900
|
+
}
|
|
3901
|
+
const result = await this.sendToWebhook(webhook, event);
|
|
3902
|
+
results.push(result);
|
|
3903
|
+
}
|
|
3904
|
+
return results;
|
|
3905
|
+
}
|
|
3906
|
+
/**
|
|
3907
|
+
* Send event to a single webhook endpoint.
|
|
3908
|
+
*/
|
|
3909
|
+
async sendToWebhook(webhook, event) {
|
|
3910
|
+
const payload = JSON.stringify(event);
|
|
3911
|
+
const headers = {
|
|
3912
|
+
"Content-Type": "application/json",
|
|
3913
|
+
"User-Agent": "SchemaShift-Webhook/1.0",
|
|
3914
|
+
...webhook.headers
|
|
3915
|
+
};
|
|
3916
|
+
if (webhook.secret) {
|
|
3917
|
+
const signature = await computeSignature(payload, webhook.secret);
|
|
3918
|
+
headers["X-SchemaShift-Signature"] = `sha256=${signature}`;
|
|
3919
|
+
}
|
|
3920
|
+
try {
|
|
3921
|
+
const response = await fetch(webhook.url, {
|
|
3922
|
+
method: "POST",
|
|
3923
|
+
headers,
|
|
3924
|
+
body: payload
|
|
3925
|
+
});
|
|
3926
|
+
return {
|
|
3927
|
+
success: response.ok,
|
|
3928
|
+
statusCode: response.status,
|
|
3929
|
+
error: response.ok ? void 0 : `HTTP ${response.status}: ${response.statusText}`
|
|
3930
|
+
};
|
|
3931
|
+
} catch (err) {
|
|
3932
|
+
return {
|
|
3933
|
+
success: false,
|
|
3934
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3935
|
+
};
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
/**
|
|
3939
|
+
* Convenience: send a migration_started event.
|
|
3940
|
+
*/
|
|
3941
|
+
async notifyMigrationStarted(from, to, fileCount, project) {
|
|
3942
|
+
const event = this.createEvent("migration_started", { from, to, fileCount }, project);
|
|
3943
|
+
return this.send(event);
|
|
3944
|
+
}
|
|
3945
|
+
/**
|
|
3946
|
+
* Convenience: send a migration_completed event.
|
|
3947
|
+
*/
|
|
3948
|
+
async notifyMigrationCompleted(from, to, fileCount, warningCount, project) {
|
|
3949
|
+
const event = this.createEvent(
|
|
3950
|
+
"migration_completed",
|
|
3951
|
+
{ from, to, fileCount, warningCount },
|
|
3952
|
+
project
|
|
3953
|
+
);
|
|
3954
|
+
return this.send(event);
|
|
3955
|
+
}
|
|
3956
|
+
/**
|
|
3957
|
+
* Convenience: send a migration_failed event.
|
|
3958
|
+
*/
|
|
3959
|
+
async notifyMigrationFailed(from, to, error, project) {
|
|
3960
|
+
const event = this.createEvent("migration_failed", { from, to, error }, project);
|
|
3961
|
+
return this.send(event);
|
|
3962
|
+
}
|
|
3963
|
+
/**
|
|
3964
|
+
* Convenience: send a governance_violation event.
|
|
3965
|
+
*/
|
|
3966
|
+
async notifyGovernanceViolation(violationCount, rules, project) {
|
|
3967
|
+
const event = this.createEvent("governance_violation", { violationCount, rules }, project);
|
|
3968
|
+
return this.send(event);
|
|
3969
|
+
}
|
|
3970
|
+
/**
|
|
3971
|
+
* Convenience: send a drift_detected event.
|
|
3972
|
+
*/
|
|
3973
|
+
async notifyDriftDetected(modifiedFiles, addedFiles, removedFiles, project) {
|
|
3974
|
+
const event = this.createEvent(
|
|
3975
|
+
"drift_detected",
|
|
3976
|
+
{ modifiedFiles, addedFiles, removedFiles },
|
|
3977
|
+
project
|
|
3978
|
+
);
|
|
3979
|
+
return this.send(event);
|
|
2399
3980
|
}
|
|
2400
3981
|
};
|
|
2401
3982
|
|
|
2402
3983
|
// src/package-updater.ts
|
|
2403
|
-
var
|
|
2404
|
-
var
|
|
3984
|
+
var import_node_fs9 = require("fs");
|
|
3985
|
+
var import_node_path9 = require("path");
|
|
2405
3986
|
var TARGET_VERSIONS = {
|
|
2406
3987
|
"yup->zod": { zod: "^3.24.0" },
|
|
2407
3988
|
"joi->zod": { zod: "^3.24.0" },
|
|
@@ -2422,14 +4003,14 @@ var PackageUpdater = class {
|
|
|
2422
4003
|
const add = {};
|
|
2423
4004
|
const remove = [];
|
|
2424
4005
|
const warnings = [];
|
|
2425
|
-
const pkgPath = (0,
|
|
2426
|
-
if (!(0,
|
|
4006
|
+
const pkgPath = (0, import_node_path9.join)(projectPath, "package.json");
|
|
4007
|
+
if (!(0, import_node_fs9.existsSync)(pkgPath)) {
|
|
2427
4008
|
warnings.push("No package.json found. Cannot plan dependency updates.");
|
|
2428
4009
|
return { add, remove, warnings };
|
|
2429
4010
|
}
|
|
2430
4011
|
let pkg;
|
|
2431
4012
|
try {
|
|
2432
|
-
pkg = JSON.parse((0,
|
|
4013
|
+
pkg = JSON.parse((0, import_node_fs9.readFileSync)(pkgPath, "utf-8"));
|
|
2433
4014
|
} catch {
|
|
2434
4015
|
warnings.push("Could not parse package.json.");
|
|
2435
4016
|
return { add, remove, warnings };
|
|
@@ -2459,9 +4040,9 @@ var PackageUpdater = class {
|
|
|
2459
4040
|
return { add, remove, warnings };
|
|
2460
4041
|
}
|
|
2461
4042
|
apply(projectPath, plan) {
|
|
2462
|
-
const pkgPath = (0,
|
|
2463
|
-
if (!(0,
|
|
2464
|
-
const pkgText = (0,
|
|
4043
|
+
const pkgPath = (0, import_node_path9.join)(projectPath, "package.json");
|
|
4044
|
+
if (!(0, import_node_fs9.existsSync)(pkgPath)) return;
|
|
4045
|
+
const pkgText = (0, import_node_fs9.readFileSync)(pkgPath, "utf-8");
|
|
2465
4046
|
const pkg = JSON.parse(pkgText);
|
|
2466
4047
|
if (!pkg.dependencies) pkg.dependencies = {};
|
|
2467
4048
|
for (const [name, version] of Object.entries(plan.add)) {
|
|
@@ -2471,7 +4052,7 @@ var PackageUpdater = class {
|
|
|
2471
4052
|
pkg.dependencies[name] = version;
|
|
2472
4053
|
}
|
|
2473
4054
|
}
|
|
2474
|
-
(0,
|
|
4055
|
+
(0, import_node_fs9.writeFileSync)(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
2475
4056
|
`);
|
|
2476
4057
|
}
|
|
2477
4058
|
};
|
|
@@ -2643,8 +4224,8 @@ var PluginLoader = class {
|
|
|
2643
4224
|
};
|
|
2644
4225
|
|
|
2645
4226
|
// src/standard-schema.ts
|
|
2646
|
-
var
|
|
2647
|
-
var
|
|
4227
|
+
var import_node_fs10 = require("fs");
|
|
4228
|
+
var import_node_path10 = require("path");
|
|
2648
4229
|
var STANDARD_SCHEMA_LIBRARIES = {
|
|
2649
4230
|
zod: { minMajor: 3, minMinor: 23 },
|
|
2650
4231
|
// Zod v3.23+ and v4+
|
|
@@ -2673,13 +4254,13 @@ function isVersionCompatible(version, minMajor, minMinor) {
|
|
|
2673
4254
|
return false;
|
|
2674
4255
|
}
|
|
2675
4256
|
function detectStandardSchema(projectPath) {
|
|
2676
|
-
const pkgPath = (0,
|
|
2677
|
-
if (!(0,
|
|
4257
|
+
const pkgPath = (0, import_node_path10.join)(projectPath, "package.json");
|
|
4258
|
+
if (!(0, import_node_fs10.existsSync)(pkgPath)) {
|
|
2678
4259
|
return { detected: false, compatibleLibraries: [], recommendation: "", interopTools: [] };
|
|
2679
4260
|
}
|
|
2680
4261
|
let allDeps = {};
|
|
2681
4262
|
try {
|
|
2682
|
-
const pkg = JSON.parse((0,
|
|
4263
|
+
const pkg = JSON.parse((0, import_node_fs10.readFileSync)(pkgPath, "utf-8"));
|
|
2683
4264
|
allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2684
4265
|
} catch {
|
|
2685
4266
|
return { detected: false, compatibleLibraries: [], recommendation: "", interopTools: [] };
|
|
@@ -2718,6 +4299,105 @@ function detectStandardSchema(projectPath) {
|
|
|
2718
4299
|
return { detected, compatibleLibraries, recommendation, adoptionPath, interopTools };
|
|
2719
4300
|
}
|
|
2720
4301
|
|
|
4302
|
+
// src/standard-schema-advisor.ts
|
|
4303
|
+
var STANDARD_SCHEMA_LIBS = /* @__PURE__ */ new Set(["zod", "valibot", "arktype"]);
|
|
4304
|
+
var StandardSchemaAdvisor = class {
|
|
4305
|
+
/**
|
|
4306
|
+
* Check if a schema library supports Standard Schema.
|
|
4307
|
+
*/
|
|
4308
|
+
supportsStandardSchema(library) {
|
|
4309
|
+
return STANDARD_SCHEMA_LIBS.has(library);
|
|
4310
|
+
}
|
|
4311
|
+
/**
|
|
4312
|
+
* Generate advisory for a given migration path.
|
|
4313
|
+
*/
|
|
4314
|
+
advise(from, to) {
|
|
4315
|
+
const fromSupports = this.supportsStandardSchema(from);
|
|
4316
|
+
const toSupports = this.supportsStandardSchema(to);
|
|
4317
|
+
if (!fromSupports && !toSupports) {
|
|
4318
|
+
return {
|
|
4319
|
+
shouldConsiderAdapter: false,
|
|
4320
|
+
reason: `Neither ${from} nor ${to} supports Standard Schema. Full migration is recommended.`,
|
|
4321
|
+
migrationAdvantages: [
|
|
4322
|
+
"Complete type safety with target library",
|
|
4323
|
+
"Access to target library ecosystem",
|
|
4324
|
+
"No runtime adapter overhead"
|
|
4325
|
+
],
|
|
4326
|
+
adapterAdvantages: [],
|
|
4327
|
+
recommendation: "migrate"
|
|
4328
|
+
};
|
|
4329
|
+
}
|
|
4330
|
+
if (fromSupports && toSupports) {
|
|
4331
|
+
return {
|
|
4332
|
+
shouldConsiderAdapter: true,
|
|
4333
|
+
reason: `Both ${from} and ${to} support Standard Schema 1.0. You may be able to use adapters for ecosystem tools (tRPC, TanStack Form, etc.) instead of migrating all schemas.`,
|
|
4334
|
+
adapterExample: this.generateAdapterExample(from, to),
|
|
4335
|
+
migrationAdvantages: [
|
|
4336
|
+
"Full target library API and ergonomics",
|
|
4337
|
+
"Consistent codebase (single library)",
|
|
4338
|
+
"Better IDE support for one library",
|
|
4339
|
+
"Smaller bundle (avoid loading two libraries)"
|
|
4340
|
+
],
|
|
4341
|
+
adapterAdvantages: [
|
|
4342
|
+
"No code changes needed for existing schemas",
|
|
4343
|
+
"Gradual migration possible",
|
|
4344
|
+
"Ecosystem tools work with both libraries via Standard Schema",
|
|
4345
|
+
"Lower risk \u2014 existing validation behavior preserved"
|
|
4346
|
+
],
|
|
4347
|
+
recommendation: "either"
|
|
4348
|
+
};
|
|
4349
|
+
}
|
|
4350
|
+
if (toSupports && !fromSupports) {
|
|
4351
|
+
return {
|
|
4352
|
+
shouldConsiderAdapter: false,
|
|
4353
|
+
reason: `${from} does not support Standard Schema, but ${to} does. Migrating to ${to} gives you Standard Schema interoperability.`,
|
|
4354
|
+
migrationAdvantages: [
|
|
4355
|
+
"Standard Schema interoperability with ecosystem tools",
|
|
4356
|
+
"Future-proof validation layer",
|
|
4357
|
+
`Access to ${to} API and type inference`
|
|
4358
|
+
],
|
|
4359
|
+
adapterAdvantages: [],
|
|
4360
|
+
recommendation: "migrate"
|
|
4361
|
+
};
|
|
4362
|
+
}
|
|
4363
|
+
return {
|
|
4364
|
+
shouldConsiderAdapter: false,
|
|
4365
|
+
reason: `${from} supports Standard Schema but ${to} does not. Consider if you need the specific features of ${to} that justify losing Standard Schema interoperability.`,
|
|
4366
|
+
migrationAdvantages: [`Access to ${to}-specific features`],
|
|
4367
|
+
adapterAdvantages: [`Keeping ${from} preserves Standard Schema interoperability`],
|
|
4368
|
+
recommendation: "migrate"
|
|
4369
|
+
};
|
|
4370
|
+
}
|
|
4371
|
+
/**
|
|
4372
|
+
* Analyze a project and provide advisory based on detected libraries.
|
|
4373
|
+
*/
|
|
4374
|
+
adviseFromProject(projectPath, from, to) {
|
|
4375
|
+
const projectInfo = detectStandardSchema(projectPath);
|
|
4376
|
+
const advisory = this.advise(from, to);
|
|
4377
|
+
return { ...advisory, projectInfo };
|
|
4378
|
+
}
|
|
4379
|
+
generateAdapterExample(from, to) {
|
|
4380
|
+
return [
|
|
4381
|
+
`// Instead of migrating all ${from} schemas to ${to},`,
|
|
4382
|
+
`// you can use Standard Schema adapters for ecosystem tools:`,
|
|
4383
|
+
`//`,
|
|
4384
|
+
`// Example with tRPC (v11+):`,
|
|
4385
|
+
`// tRPC accepts any Standard Schema-compatible schema.`,
|
|
4386
|
+
`// Both ${from} and ${to} schemas work without conversion:`,
|
|
4387
|
+
`//`,
|
|
4388
|
+
`// import { ${from}Schema } from './existing-${from}-schemas';`,
|
|
4389
|
+
`// import { ${to}Schema } from './new-${to}-schemas';`,
|
|
4390
|
+
`//`,
|
|
4391
|
+
`// const router = t.router({`,
|
|
4392
|
+
`// // Works with ${from} schema (Standard Schema compatible)`,
|
|
4393
|
+
`// getUser: t.procedure.input(${from}Schema).query(...)`,
|
|
4394
|
+
`// // Also works with ${to} schema`,
|
|
4395
|
+
`// createUser: t.procedure.input(${to}Schema).mutation(...)`,
|
|
4396
|
+
`// });`
|
|
4397
|
+
].join("\n");
|
|
4398
|
+
}
|
|
4399
|
+
};
|
|
4400
|
+
|
|
2721
4401
|
// src/test-scaffolder.ts
|
|
2722
4402
|
var TestScaffolder = class {
|
|
2723
4403
|
scaffold(sourceFiles, from, to) {
|
|
@@ -3012,14 +4692,19 @@ var TypeDedupDetector = class {
|
|
|
3012
4692
|
};
|
|
3013
4693
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3014
4694
|
0 && (module.exports = {
|
|
4695
|
+
ApprovalManager,
|
|
3015
4696
|
BehavioralWarningAnalyzer,
|
|
3016
4697
|
BundleEstimator,
|
|
3017
4698
|
CompatibilityAnalyzer,
|
|
3018
4699
|
ComplexityEstimator,
|
|
3019
4700
|
DetailedAnalyzer,
|
|
4701
|
+
DriftDetector,
|
|
3020
4702
|
EcosystemAnalyzer,
|
|
3021
4703
|
FormResolverMigrator,
|
|
4704
|
+
GOVERNANCE_TEMPLATES,
|
|
3022
4705
|
GovernanceEngine,
|
|
4706
|
+
GovernanceFixer,
|
|
4707
|
+
GraphExporter,
|
|
3023
4708
|
IncrementalTracker,
|
|
3024
4709
|
MigrationAuditLog,
|
|
3025
4710
|
MigrationChain,
|
|
@@ -3029,21 +4714,37 @@ var TypeDedupDetector = class {
|
|
|
3029
4714
|
PluginLoader,
|
|
3030
4715
|
SchemaAnalyzer,
|
|
3031
4716
|
SchemaDependencyResolver,
|
|
4717
|
+
StandardSchemaAdvisor,
|
|
3032
4718
|
TestScaffolder,
|
|
3033
4719
|
TransformEngine,
|
|
3034
4720
|
TypeDedupDetector,
|
|
4721
|
+
WebhookNotifier,
|
|
3035
4722
|
buildCallChain,
|
|
3036
4723
|
computeParallelBatches,
|
|
4724
|
+
conditionalValidation,
|
|
4725
|
+
dependentFields,
|
|
3037
4726
|
detectFormLibraries,
|
|
3038
4727
|
detectSchemaLibrary,
|
|
3039
4728
|
detectStandardSchema,
|
|
4729
|
+
getAllMigrationTemplates,
|
|
4730
|
+
getGovernanceTemplate,
|
|
4731
|
+
getGovernanceTemplateNames,
|
|
4732
|
+
getGovernanceTemplatesByCategory,
|
|
4733
|
+
getMigrationTemplate,
|
|
4734
|
+
getMigrationTemplateNames,
|
|
4735
|
+
getMigrationTemplatesByCategory,
|
|
3040
4736
|
isInsideComment,
|
|
3041
4737
|
isInsideStringLiteral,
|
|
3042
4738
|
loadConfig,
|
|
4739
|
+
mutuallyExclusive,
|
|
3043
4740
|
parseCallChain,
|
|
4741
|
+
requireIf,
|
|
4742
|
+
requireOneOf,
|
|
3044
4743
|
shouldSuppressWarning,
|
|
3045
4744
|
startsWithBase,
|
|
4745
|
+
suggestCrossFieldPattern,
|
|
3046
4746
|
transformMethodChain,
|
|
3047
|
-
validateConfig
|
|
4747
|
+
validateConfig,
|
|
4748
|
+
validateMigrationTemplate
|
|
3048
4749
|
});
|
|
3049
4750
|
//# sourceMappingURL=index.cjs.map
|