@schemashift/core 0.7.0 → 0.9.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/README.md +102 -0
- package/dist/index.cjs +1570 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +326 -1
- package/dist/index.d.ts +326 -1
- package/dist/index.js +1560 -57
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -20,18 +20,28 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
BehavioralWarningAnalyzer: () => BehavioralWarningAnalyzer,
|
|
24
|
+
BundleEstimator: () => BundleEstimator,
|
|
23
25
|
CompatibilityAnalyzer: () => CompatibilityAnalyzer,
|
|
26
|
+
ComplexityEstimator: () => ComplexityEstimator,
|
|
24
27
|
DetailedAnalyzer: () => DetailedAnalyzer,
|
|
25
28
|
EcosystemAnalyzer: () => EcosystemAnalyzer,
|
|
29
|
+
FormResolverMigrator: () => FormResolverMigrator,
|
|
26
30
|
GovernanceEngine: () => GovernanceEngine,
|
|
27
31
|
IncrementalTracker: () => IncrementalTracker,
|
|
32
|
+
MigrationAuditLog: () => MigrationAuditLog,
|
|
28
33
|
MigrationChain: () => MigrationChain,
|
|
34
|
+
MonorepoResolver: () => MonorepoResolver,
|
|
29
35
|
PackageUpdater: () => PackageUpdater,
|
|
36
|
+
PerformanceAnalyzer: () => PerformanceAnalyzer,
|
|
30
37
|
PluginLoader: () => PluginLoader,
|
|
31
38
|
SchemaAnalyzer: () => SchemaAnalyzer,
|
|
32
39
|
SchemaDependencyResolver: () => SchemaDependencyResolver,
|
|
40
|
+
TestScaffolder: () => TestScaffolder,
|
|
33
41
|
TransformEngine: () => TransformEngine,
|
|
42
|
+
TypeDedupDetector: () => TypeDedupDetector,
|
|
34
43
|
buildCallChain: () => buildCallChain,
|
|
44
|
+
computeParallelBatches: () => computeParallelBatches,
|
|
35
45
|
detectFormLibraries: () => detectFormLibraries,
|
|
36
46
|
detectSchemaLibrary: () => detectSchemaLibrary,
|
|
37
47
|
detectStandardSchema: () => detectStandardSchema,
|
|
@@ -312,6 +322,465 @@ function transformMethodChain(chain, newBase, factoryMapper, methodMapper) {
|
|
|
312
322
|
return buildCallChain(newBase, factory.name, factory.args, mappedMethods);
|
|
313
323
|
}
|
|
314
324
|
|
|
325
|
+
// src/audit-log.ts
|
|
326
|
+
var import_node_crypto = require("crypto");
|
|
327
|
+
var import_node_fs = require("fs");
|
|
328
|
+
var import_node_path = require("path");
|
|
329
|
+
var AUDIT_DIR = ".schemashift";
|
|
330
|
+
var AUDIT_FILE = "audit-log.json";
|
|
331
|
+
var AUDIT_VERSION = 1;
|
|
332
|
+
var MigrationAuditLog = class {
|
|
333
|
+
logDir;
|
|
334
|
+
logPath;
|
|
335
|
+
constructor(projectPath) {
|
|
336
|
+
this.logDir = (0, import_node_path.join)(projectPath, AUDIT_DIR);
|
|
337
|
+
this.logPath = (0, import_node_path.join)(this.logDir, AUDIT_FILE);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Append a new entry to the audit log.
|
|
341
|
+
*/
|
|
342
|
+
append(entry) {
|
|
343
|
+
const log = this.read();
|
|
344
|
+
log.entries.push(entry);
|
|
345
|
+
this.write(log);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Create an audit entry for a file transformation.
|
|
349
|
+
*/
|
|
350
|
+
createEntry(params) {
|
|
351
|
+
return {
|
|
352
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
353
|
+
migrationId: params.migrationId,
|
|
354
|
+
filePath: params.filePath,
|
|
355
|
+
action: "transform",
|
|
356
|
+
from: params.from,
|
|
357
|
+
to: params.to,
|
|
358
|
+
success: params.success,
|
|
359
|
+
beforeHash: this.hashContent(params.originalCode),
|
|
360
|
+
afterHash: params.transformedCode ? this.hashContent(params.transformedCode) : void 0,
|
|
361
|
+
warningCount: params.warningCount,
|
|
362
|
+
errorCount: params.errorCount,
|
|
363
|
+
riskScore: params.riskScore,
|
|
364
|
+
duration: params.duration,
|
|
365
|
+
user: this.getCurrentUser()
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Read the current audit log.
|
|
370
|
+
*/
|
|
371
|
+
read() {
|
|
372
|
+
if (!(0, import_node_fs.existsSync)(this.logPath)) {
|
|
373
|
+
return { version: AUDIT_VERSION, entries: [] };
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
const content = (0, import_node_fs.readFileSync)(this.logPath, "utf-8");
|
|
377
|
+
if (!content.trim()) {
|
|
378
|
+
return { version: AUDIT_VERSION, entries: [] };
|
|
379
|
+
}
|
|
380
|
+
return JSON.parse(content);
|
|
381
|
+
} catch {
|
|
382
|
+
return { version: AUDIT_VERSION, entries: [] };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Get entries for a specific migration.
|
|
387
|
+
*/
|
|
388
|
+
getByMigration(migrationId) {
|
|
389
|
+
const log = this.read();
|
|
390
|
+
return log.entries.filter((e) => e.migrationId === migrationId);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Get summary statistics for the audit log.
|
|
394
|
+
*/
|
|
395
|
+
getSummary() {
|
|
396
|
+
const log = this.read();
|
|
397
|
+
const migrationIds = new Set(log.entries.map((e) => e.migrationId));
|
|
398
|
+
const migrationPaths = [...new Set(log.entries.map((e) => `${e.from}->${e.to}`))];
|
|
399
|
+
return {
|
|
400
|
+
totalMigrations: migrationIds.size,
|
|
401
|
+
totalFiles: log.entries.length,
|
|
402
|
+
successCount: log.entries.filter((e) => e.success).length,
|
|
403
|
+
failureCount: log.entries.filter((e) => !e.success).length,
|
|
404
|
+
migrationPaths
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Clear the audit log.
|
|
409
|
+
*/
|
|
410
|
+
clear() {
|
|
411
|
+
this.write({ version: AUDIT_VERSION, entries: [] });
|
|
412
|
+
}
|
|
413
|
+
write(log) {
|
|
414
|
+
if (!(0, import_node_fs.existsSync)(this.logDir)) {
|
|
415
|
+
(0, import_node_fs.mkdirSync)(this.logDir, { recursive: true });
|
|
416
|
+
}
|
|
417
|
+
(0, import_node_fs.writeFileSync)(this.logPath, JSON.stringify(log, null, 2));
|
|
418
|
+
}
|
|
419
|
+
hashContent(content) {
|
|
420
|
+
return (0, import_node_crypto.createHash)("sha256").update(content).digest("hex").substring(0, 16);
|
|
421
|
+
}
|
|
422
|
+
getCurrentUser() {
|
|
423
|
+
return process.env.USER || process.env.USERNAME || void 0;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// src/behavioral-warnings.ts
|
|
428
|
+
var BEHAVIORAL_RULES = [
|
|
429
|
+
// Yup -> Zod: Type coercion differences
|
|
430
|
+
{
|
|
431
|
+
category: "type-coercion",
|
|
432
|
+
migrations: ["yup->zod"],
|
|
433
|
+
detect: (text, filePath) => {
|
|
434
|
+
const warnings = [];
|
|
435
|
+
if (/yup\.(number|date)\s*\(\)/.test(text)) {
|
|
436
|
+
warnings.push({
|
|
437
|
+
category: "type-coercion",
|
|
438
|
+
message: "Yup silently coerces types; Zod rejects mismatches.",
|
|
439
|
+
detail: `Yup's number() accepts strings like "42" and coerces them. Zod's number() rejects strings. Use z.coerce.number() for equivalent behavior, especially for HTML form inputs which always return strings.`,
|
|
440
|
+
filePath,
|
|
441
|
+
severity: "warning",
|
|
442
|
+
migration: "yup->zod"
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
return warnings;
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
// Yup -> Zod: Form input string values
|
|
449
|
+
{
|
|
450
|
+
category: "form-input",
|
|
451
|
+
migrations: ["yup->zod"],
|
|
452
|
+
detect: (text, filePath) => {
|
|
453
|
+
const warnings = [];
|
|
454
|
+
const hasFormImport = /yupResolver|useFormik|from\s+['"]formik['"]|from\s+['"]@hookform/.test(
|
|
455
|
+
text
|
|
456
|
+
);
|
|
457
|
+
const hasNumberOrDate = /yup\.(number|date)\s*\(\)/.test(text);
|
|
458
|
+
if (hasFormImport && hasNumberOrDate) {
|
|
459
|
+
warnings.push({
|
|
460
|
+
category: "form-input",
|
|
461
|
+
message: "HTML inputs return strings \u2014 Zod will reject unless using z.coerce.*",
|
|
462
|
+
detail: 'HTML <input type="number"> returns strings. Yup coerces automatically, but Zod requires explicit coercion. Use z.coerce.number() or register({ valueAsNumber: true }) in React Hook Form.',
|
|
463
|
+
filePath,
|
|
464
|
+
severity: "error",
|
|
465
|
+
migration: "yup->zod"
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
return warnings;
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
// Joi -> Zod: Error handling paradigm shift
|
|
472
|
+
{
|
|
473
|
+
category: "error-handling",
|
|
474
|
+
migrations: ["joi->zod"],
|
|
475
|
+
detect: (text, filePath) => {
|
|
476
|
+
const warnings = [];
|
|
477
|
+
if (/\.validate\s*\(/.test(text) && /[Jj]oi/.test(text)) {
|
|
478
|
+
warnings.push({
|
|
479
|
+
category: "error-handling",
|
|
480
|
+
message: "Joi .validate() returns { value, error }; Zod .parse() throws.",
|
|
481
|
+
detail: "Joi uses an inspection pattern: .validate() returns an object with value and error. Zod .parse() throws a ZodError on failure. Use .safeParse() for a non-throwing equivalent that returns { success, data, error }.",
|
|
482
|
+
filePath,
|
|
483
|
+
severity: "warning",
|
|
484
|
+
migration: "joi->zod"
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
return warnings;
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
// Joi -> Zod: Null handling differences
|
|
491
|
+
{
|
|
492
|
+
category: "null-handling",
|
|
493
|
+
migrations: ["joi->zod"],
|
|
494
|
+
detect: (text, filePath) => {
|
|
495
|
+
const warnings = [];
|
|
496
|
+
if (/\.allow\s*\(\s*null\s*\)/.test(text)) {
|
|
497
|
+
warnings.push({
|
|
498
|
+
category: "null-handling",
|
|
499
|
+
message: "Joi .allow(null) vs Zod .nullable() have subtle differences.",
|
|
500
|
+
detail: 'Joi .allow(null) permits null alongside the base type. Zod .nullable() wraps the type in a union with null. Joi .allow("", null) has no single Zod equivalent \u2014 use z.union() or .transform().',
|
|
501
|
+
filePath,
|
|
502
|
+
severity: "info",
|
|
503
|
+
migration: "joi->zod"
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
return warnings;
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
// Zod v3 -> v4: Default value behavior change
|
|
510
|
+
{
|
|
511
|
+
category: "default-values",
|
|
512
|
+
migrations: ["zod-v3->v4"],
|
|
513
|
+
detect: (text, filePath) => {
|
|
514
|
+
const warnings = [];
|
|
515
|
+
if (/\.default\s*\(/.test(text) && /\.optional\s*\(\)/.test(text)) {
|
|
516
|
+
warnings.push({
|
|
517
|
+
category: "default-values",
|
|
518
|
+
message: ".default() + .optional() behavior changed silently in Zod v4.",
|
|
519
|
+
detail: "In Zod v3, .default(val).optional() returned undefined when property was missing. In Zod v4, it always returns the default value. This can cause unexpected behavior in API responses and form handling.",
|
|
520
|
+
filePath,
|
|
521
|
+
severity: "error",
|
|
522
|
+
migration: "zod-v3->v4"
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
if (/\.catch\s*\(/.test(text) && /\.optional\s*\(\)/.test(text)) {
|
|
526
|
+
warnings.push({
|
|
527
|
+
category: "default-values",
|
|
528
|
+
message: ".catch() + .optional() behavior changed in Zod v4.",
|
|
529
|
+
detail: "In Zod v4, object properties with .catch() that are .optional() now always return the caught value, even when the property is missing from input.",
|
|
530
|
+
filePath,
|
|
531
|
+
severity: "warning",
|
|
532
|
+
migration: "zod-v3->v4"
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
return warnings;
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
// Zod v3 -> v4: Error format differences
|
|
539
|
+
{
|
|
540
|
+
category: "error-format",
|
|
541
|
+
migrations: ["zod-v3->v4"],
|
|
542
|
+
detect: (text, filePath) => {
|
|
543
|
+
const warnings = [];
|
|
544
|
+
if (/ZodError/.test(text) && /instanceof\s+Error/.test(text)) {
|
|
545
|
+
warnings.push({
|
|
546
|
+
category: "error-format",
|
|
547
|
+
message: "ZodError no longer extends Error in Zod v4.",
|
|
548
|
+
detail: 'In Zod v4, ZodError no longer extends Error. Code using "instanceof Error" to catch ZodErrors will silently miss them. Use "instanceof ZodError" or z.isZodError() instead.',
|
|
549
|
+
filePath,
|
|
550
|
+
severity: "error",
|
|
551
|
+
migration: "zod-v3->v4"
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
return warnings;
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
// Zod v3 -> v4: Validation behavior differences
|
|
558
|
+
{
|
|
559
|
+
category: "validation-behavior",
|
|
560
|
+
migrations: ["zod-v3->v4"],
|
|
561
|
+
detect: (text, filePath) => {
|
|
562
|
+
const warnings = [];
|
|
563
|
+
if (/\.transform\s*\(/.test(text) && /\.refine\s*\(/.test(text)) {
|
|
564
|
+
warnings.push({
|
|
565
|
+
category: "validation-behavior",
|
|
566
|
+
message: ".transform() after .refine() behavior changed in Zod v4.",
|
|
567
|
+
detail: "In Zod v4, .transform() after .refine() may execute even if the refinement fails. Previously, transform was skipped on refinement failure.",
|
|
568
|
+
filePath,
|
|
569
|
+
severity: "warning",
|
|
570
|
+
migration: "zod-v3->v4"
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
return warnings;
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
// Zod -> Valibot: Error handling differences
|
|
577
|
+
{
|
|
578
|
+
category: "error-handling",
|
|
579
|
+
migrations: ["zod->valibot"],
|
|
580
|
+
detect: (text, filePath) => {
|
|
581
|
+
const warnings = [];
|
|
582
|
+
if (/\.parse\s*\(/.test(text) && /z\./.test(text)) {
|
|
583
|
+
warnings.push({
|
|
584
|
+
category: "error-handling",
|
|
585
|
+
message: "Zod .parse() throws ZodError; Valibot v.parse() throws ValiError.",
|
|
586
|
+
detail: "Error class and structure differ between Zod and Valibot. ZodError has .issues array; ValiError has .issues with different structure. Update all error handling code that inspects validation errors.",
|
|
587
|
+
filePath,
|
|
588
|
+
severity: "warning",
|
|
589
|
+
migration: "zod->valibot"
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
return warnings;
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
// io-ts -> Zod: Either monad vs throw/safeParse
|
|
596
|
+
{
|
|
597
|
+
category: "error-handling",
|
|
598
|
+
migrations: ["io-ts->zod"],
|
|
599
|
+
detect: (text, filePath) => {
|
|
600
|
+
const warnings = [];
|
|
601
|
+
if (/\bEither\b/.test(text) || /\b(fold|chain|map)\s*\(/.test(text)) {
|
|
602
|
+
warnings.push({
|
|
603
|
+
category: "error-handling",
|
|
604
|
+
message: "io-ts uses Either monad for errors; Zod uses throw/safeParse.",
|
|
605
|
+
detail: "io-ts returns Either<Errors, T> (Right for success, Left for failure). Zod .parse() throws, .safeParse() returns { success, data, error }. All fold/chain/map patterns over Either must be rewritten.",
|
|
606
|
+
filePath,
|
|
607
|
+
severity: "error",
|
|
608
|
+
migration: "io-ts->zod"
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
return warnings;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
];
|
|
615
|
+
var BehavioralWarningAnalyzer = class {
|
|
616
|
+
analyze(sourceFiles, from, to) {
|
|
617
|
+
const migration = `${from}->${to}`;
|
|
618
|
+
const warnings = [];
|
|
619
|
+
const applicableRules = BEHAVIORAL_RULES.filter((r) => r.migrations.includes(migration));
|
|
620
|
+
for (const sourceFile of sourceFiles) {
|
|
621
|
+
const filePath = sourceFile.getFilePath();
|
|
622
|
+
const text = sourceFile.getFullText();
|
|
623
|
+
const hasSourceLib = this.fileUsesLibrary(sourceFile, from);
|
|
624
|
+
if (!hasSourceLib) continue;
|
|
625
|
+
for (const rule of applicableRules) {
|
|
626
|
+
const ruleWarnings = rule.detect(text, filePath);
|
|
627
|
+
warnings.push(...ruleWarnings);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
const summary = this.generateSummary(warnings, migration);
|
|
631
|
+
return { warnings, migrationPath: migration, summary };
|
|
632
|
+
}
|
|
633
|
+
fileUsesLibrary(sourceFile, library) {
|
|
634
|
+
for (const imp of sourceFile.getImportDeclarations()) {
|
|
635
|
+
const detected = detectSchemaLibrary(imp.getModuleSpecifierValue());
|
|
636
|
+
if (detected === library) return true;
|
|
637
|
+
if (library === "zod-v3" && detected === "zod") return true;
|
|
638
|
+
if (library === "zod" && detected === "zod") return true;
|
|
639
|
+
}
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
generateSummary(warnings, migration) {
|
|
643
|
+
if (warnings.length === 0) {
|
|
644
|
+
return `No behavioral differences detected for ${migration} migration.`;
|
|
645
|
+
}
|
|
646
|
+
const errorCount = warnings.filter((w) => w.severity === "error").length;
|
|
647
|
+
const warningCount = warnings.filter((w) => w.severity === "warning").length;
|
|
648
|
+
const infoCount = warnings.filter((w) => w.severity === "info").length;
|
|
649
|
+
const parts = [];
|
|
650
|
+
if (errorCount > 0) parts.push(`${errorCount} critical`);
|
|
651
|
+
if (warningCount > 0) parts.push(`${warningCount} warnings`);
|
|
652
|
+
if (infoCount > 0) parts.push(`${infoCount} info`);
|
|
653
|
+
return `Found ${warnings.length} behavioral difference(s) for ${migration}: ${parts.join(", ")}. Review before migrating.`;
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
// src/bundle-estimator.ts
|
|
658
|
+
var LIBRARY_SIZES = {
|
|
659
|
+
zod: { fullKb: 14, baseKb: 14, treeShakable: false },
|
|
660
|
+
"zod-v3": { fullKb: 14, baseKb: 14, treeShakable: false },
|
|
661
|
+
v4: { fullKb: 17.7, baseKb: 17.7, treeShakable: false },
|
|
662
|
+
"zod-v4": { fullKb: 17.7, baseKb: 17.7, treeShakable: false },
|
|
663
|
+
"zod-mini": { fullKb: 7.5, baseKb: 3.5, treeShakable: true },
|
|
664
|
+
yup: { fullKb: 13.6, baseKb: 13.6, treeShakable: false },
|
|
665
|
+
joi: { fullKb: 29.7, baseKb: 29.7, treeShakable: false },
|
|
666
|
+
"io-ts": { fullKb: 6.5, baseKb: 6.5, treeShakable: true },
|
|
667
|
+
valibot: { fullKb: 5.8, baseKb: 1.4, treeShakable: true }
|
|
668
|
+
};
|
|
669
|
+
var VALIDATOR_OVERHEAD = {
|
|
670
|
+
valibot: 0.05
|
|
671
|
+
};
|
|
672
|
+
var COMMON_VALIDATORS = /* @__PURE__ */ new Set([
|
|
673
|
+
"string",
|
|
674
|
+
"number",
|
|
675
|
+
"boolean",
|
|
676
|
+
"object",
|
|
677
|
+
"array",
|
|
678
|
+
"optional",
|
|
679
|
+
"nullable",
|
|
680
|
+
"enum",
|
|
681
|
+
"union",
|
|
682
|
+
"literal",
|
|
683
|
+
"date",
|
|
684
|
+
"email",
|
|
685
|
+
"url",
|
|
686
|
+
"uuid",
|
|
687
|
+
"min",
|
|
688
|
+
"max",
|
|
689
|
+
"regex",
|
|
690
|
+
"transform",
|
|
691
|
+
"refine",
|
|
692
|
+
"default",
|
|
693
|
+
"record",
|
|
694
|
+
"tuple",
|
|
695
|
+
"lazy",
|
|
696
|
+
"discriminatedUnion",
|
|
697
|
+
"intersection",
|
|
698
|
+
"partial",
|
|
699
|
+
"pick",
|
|
700
|
+
"omit",
|
|
701
|
+
"brand",
|
|
702
|
+
"pipe"
|
|
703
|
+
]);
|
|
704
|
+
var BundleEstimator = class {
|
|
705
|
+
estimate(sourceFiles, from, to) {
|
|
706
|
+
const usedValidators = this.countUsedValidators(sourceFiles);
|
|
707
|
+
const fromInfo = this.getLibraryInfo(from, usedValidators);
|
|
708
|
+
const toInfo = this.getLibraryInfo(to, usedValidators);
|
|
709
|
+
const estimatedDelta = toInfo.estimatedUsedKb - fromInfo.estimatedUsedKb;
|
|
710
|
+
const deltaPercent = fromInfo.estimatedUsedKb > 0 ? Math.round(estimatedDelta / fromInfo.estimatedUsedKb * 100) : 0;
|
|
711
|
+
const caveats = this.generateCaveats(from, to, usedValidators);
|
|
712
|
+
const summary = this.generateSummary(fromInfo, toInfo, estimatedDelta, deltaPercent);
|
|
713
|
+
return {
|
|
714
|
+
from: fromInfo,
|
|
715
|
+
to: toInfo,
|
|
716
|
+
estimatedDelta,
|
|
717
|
+
deltaPercent,
|
|
718
|
+
summary,
|
|
719
|
+
caveats
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
countUsedValidators(sourceFiles) {
|
|
723
|
+
const usedSet = /* @__PURE__ */ new Set();
|
|
724
|
+
for (const file of sourceFiles) {
|
|
725
|
+
const text = file.getFullText();
|
|
726
|
+
for (const validator of COMMON_VALIDATORS) {
|
|
727
|
+
const pattern = new RegExp(`\\.${validator}\\s*[(<]`, "g");
|
|
728
|
+
if (pattern.test(text)) {
|
|
729
|
+
usedSet.add(validator);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return usedSet.size;
|
|
734
|
+
}
|
|
735
|
+
getLibraryInfo(library, usedValidators) {
|
|
736
|
+
const sizeKey = library === "zod-v3" ? "zod" : library;
|
|
737
|
+
const sizes = LIBRARY_SIZES[sizeKey] ?? { fullKb: 10, baseKb: 10, treeShakable: false };
|
|
738
|
+
let estimatedUsedKb;
|
|
739
|
+
if (sizes.treeShakable) {
|
|
740
|
+
const overhead = VALIDATOR_OVERHEAD[sizeKey] ?? 0.05;
|
|
741
|
+
estimatedUsedKb = Math.min(sizes.baseKb + usedValidators * overhead, sizes.fullKb);
|
|
742
|
+
} else {
|
|
743
|
+
estimatedUsedKb = sizes.fullKb;
|
|
744
|
+
}
|
|
745
|
+
return {
|
|
746
|
+
library: sizeKey,
|
|
747
|
+
minifiedGzipKb: sizes.fullKb,
|
|
748
|
+
treeShakable: sizes.treeShakable,
|
|
749
|
+
estimatedUsedKb: Math.round(estimatedUsedKb * 10) / 10
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
generateCaveats(from, to, _usedValidators) {
|
|
753
|
+
const caveats = [
|
|
754
|
+
"Sizes are estimates based on minified+gzipped bundle analysis.",
|
|
755
|
+
"Actual impact depends on bundler configuration, tree-shaking, and code splitting."
|
|
756
|
+
];
|
|
757
|
+
if (to === "valibot") {
|
|
758
|
+
caveats.push(
|
|
759
|
+
"Valibot is fully tree-shakable \u2014 actual size depends on which validators you use."
|
|
760
|
+
);
|
|
761
|
+
caveats.push(
|
|
762
|
+
"Some developers report smaller-than-expected savings (6kB or less) in real projects."
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
if (from === "zod-v3" && to === "v4") {
|
|
766
|
+
caveats.push(
|
|
767
|
+
"Zod v4 is ~26% larger than v3 due to JIT compilation engine. Consider zod/mini for size-sensitive apps."
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
if (from === "joi") {
|
|
771
|
+
caveats.push(
|
|
772
|
+
"Joi is the largest schema library. Any migration will likely reduce bundle size."
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
return caveats;
|
|
776
|
+
}
|
|
777
|
+
generateSummary(from, to, delta, deltaPercent) {
|
|
778
|
+
const direction = delta > 0 ? "increase" : delta < 0 ? "decrease" : "no change";
|
|
779
|
+
const absDelta = Math.abs(Math.round(delta * 10) / 10);
|
|
780
|
+
return `Estimated bundle ${direction}: ${from.library} (${from.estimatedUsedKb}kB) \u2192 ${to.library} (${to.estimatedUsedKb}kB) = ${delta > 0 ? "+" : delta < 0 ? "-" : ""}${absDelta}kB (${deltaPercent > 0 ? "+" : ""}${deltaPercent}%)`;
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
|
|
315
784
|
// src/chain.ts
|
|
316
785
|
var import_ts_morph3 = require("ts-morph");
|
|
317
786
|
var MigrationChain = class {
|
|
@@ -378,12 +847,12 @@ var MigrationChain = class {
|
|
|
378
847
|
};
|
|
379
848
|
|
|
380
849
|
// src/compatibility.ts
|
|
381
|
-
var
|
|
382
|
-
var
|
|
850
|
+
var import_node_fs3 = require("fs");
|
|
851
|
+
var import_node_path3 = require("path");
|
|
383
852
|
|
|
384
853
|
// src/ecosystem.ts
|
|
385
|
-
var
|
|
386
|
-
var
|
|
854
|
+
var import_node_fs2 = require("fs");
|
|
855
|
+
var import_node_path2 = require("path");
|
|
387
856
|
var ECOSYSTEM_RULES = [
|
|
388
857
|
// ORM integrations
|
|
389
858
|
{
|
|
@@ -393,7 +862,8 @@ var ECOSYSTEM_RULES = [
|
|
|
393
862
|
check: () => ({
|
|
394
863
|
issue: "drizzle-zod may not support Zod v4. Check for a compatible version before upgrading.",
|
|
395
864
|
suggestion: "Upgrade drizzle-zod to the latest version that supports Zod v4, or use --legacy-peer-deps.",
|
|
396
|
-
severity: "warning"
|
|
865
|
+
severity: "warning",
|
|
866
|
+
upgradeCommand: "npm install drizzle-zod@latest"
|
|
397
867
|
})
|
|
398
868
|
},
|
|
399
869
|
{
|
|
@@ -403,7 +873,8 @@ var ECOSYSTEM_RULES = [
|
|
|
403
873
|
check: () => ({
|
|
404
874
|
issue: "zod-prisma generates Zod v3 schemas. Generated files will need regeneration after upgrading to Zod v4.",
|
|
405
875
|
suggestion: "Upgrade zod-prisma to a v4-compatible version and regenerate schemas.",
|
|
406
|
-
severity: "warning"
|
|
876
|
+
severity: "warning",
|
|
877
|
+
upgradeCommand: "npm install zod-prisma@latest"
|
|
407
878
|
})
|
|
408
879
|
},
|
|
409
880
|
{
|
|
@@ -413,7 +884,8 @@ var ECOSYSTEM_RULES = [
|
|
|
413
884
|
check: () => ({
|
|
414
885
|
issue: "zod-prisma-types generates Zod v3 schemas. Generated files will need regeneration.",
|
|
415
886
|
suggestion: "Check for a Zod v4-compatible version of zod-prisma-types.",
|
|
416
|
-
severity: "warning"
|
|
887
|
+
severity: "warning",
|
|
888
|
+
upgradeCommand: "npm install zod-prisma-types@latest"
|
|
417
889
|
})
|
|
418
890
|
},
|
|
419
891
|
// API framework integrations
|
|
@@ -428,7 +900,8 @@ var ECOSYSTEM_RULES = [
|
|
|
428
900
|
return {
|
|
429
901
|
issue: `tRPC v${major} expects Zod v3 types. A v3 ZodType is not assignable to a v4 ZodType.`,
|
|
430
902
|
suggestion: "Upgrade to tRPC v11+ which supports Zod v4 via Standard Schema.",
|
|
431
|
-
severity: "error"
|
|
903
|
+
severity: "error",
|
|
904
|
+
upgradeCommand: "npm install @trpc/server@latest"
|
|
432
905
|
};
|
|
433
906
|
}
|
|
434
907
|
return {
|
|
@@ -445,7 +918,8 @@ var ECOSYSTEM_RULES = [
|
|
|
445
918
|
check: () => ({
|
|
446
919
|
issue: "trpc-ui breaks entirely with Zod v4 schemas.",
|
|
447
920
|
suggestion: "Check for a Zod v4-compatible version of trpc-ui before upgrading.",
|
|
448
|
-
severity: "error"
|
|
921
|
+
severity: "error",
|
|
922
|
+
upgradeCommand: "npm install trpc-ui@latest"
|
|
449
923
|
})
|
|
450
924
|
},
|
|
451
925
|
// Validation utilities
|
|
@@ -460,7 +934,8 @@ var ECOSYSTEM_RULES = [
|
|
|
460
934
|
return {
|
|
461
935
|
issue: `zod-validation-error v${major} is not compatible with Zod v4.`,
|
|
462
936
|
suggestion: "Upgrade zod-validation-error to v5.0.0+ for Zod v4 support.",
|
|
463
|
-
severity: "error"
|
|
937
|
+
severity: "error",
|
|
938
|
+
upgradeCommand: "npm install zod-validation-error@^5.0.0"
|
|
464
939
|
};
|
|
465
940
|
}
|
|
466
941
|
return null;
|
|
@@ -476,13 +951,15 @@ var ECOSYSTEM_RULES = [
|
|
|
476
951
|
return {
|
|
477
952
|
issue: "@hookform/resolvers zodResolver may need updating for Zod v4.",
|
|
478
953
|
suggestion: "Upgrade @hookform/resolvers to the latest version with Zod v4 support.",
|
|
479
|
-
severity: "warning"
|
|
954
|
+
severity: "warning",
|
|
955
|
+
upgradeCommand: "npm install @hookform/resolvers@latest"
|
|
480
956
|
};
|
|
481
957
|
}
|
|
482
958
|
return {
|
|
483
959
|
issue: "@hookform/resolvers will need its resolver import updated for the new schema library.",
|
|
484
960
|
suggestion: "Switch from the old resolver (e.g., yupResolver) to zodResolver from @hookform/resolvers/zod.",
|
|
485
|
-
severity: "warning"
|
|
961
|
+
severity: "warning",
|
|
962
|
+
upgradeCommand: "npm install @hookform/resolvers@latest"
|
|
486
963
|
};
|
|
487
964
|
}
|
|
488
965
|
},
|
|
@@ -514,7 +991,8 @@ var ECOSYSTEM_RULES = [
|
|
|
514
991
|
check: () => ({
|
|
515
992
|
issue: "zod-openapi may not support Zod v4 yet.",
|
|
516
993
|
suggestion: "Check for a Zod v4-compatible version of zod-openapi.",
|
|
517
|
-
severity: "warning"
|
|
994
|
+
severity: "warning",
|
|
995
|
+
upgradeCommand: "npm install zod-openapi@latest"
|
|
518
996
|
})
|
|
519
997
|
},
|
|
520
998
|
{
|
|
@@ -524,7 +1002,60 @@ var ECOSYSTEM_RULES = [
|
|
|
524
1002
|
check: () => ({
|
|
525
1003
|
issue: "@asteasolutions/zod-to-openapi may not support Zod v4 yet.",
|
|
526
1004
|
suggestion: "Check for a Zod v4-compatible version of @asteasolutions/zod-to-openapi.",
|
|
527
|
-
severity: "warning"
|
|
1005
|
+
severity: "warning",
|
|
1006
|
+
upgradeCommand: "npm install @asteasolutions/zod-to-openapi@latest"
|
|
1007
|
+
})
|
|
1008
|
+
},
|
|
1009
|
+
// AI/MCP integrations
|
|
1010
|
+
{
|
|
1011
|
+
package: "@modelcontextprotocol/sdk",
|
|
1012
|
+
category: "api",
|
|
1013
|
+
migrations: ["zod-v3->v4"],
|
|
1014
|
+
check: () => ({
|
|
1015
|
+
issue: "MCP SDK may have Zod v4 compatibility issues. MCP servers typically expect Zod v3 schemas.",
|
|
1016
|
+
suggestion: "Check MCP SDK release notes for Zod v4 support before upgrading. Consider staying on Zod v3 for MCP servers.",
|
|
1017
|
+
severity: "warning",
|
|
1018
|
+
upgradeCommand: "npm install @modelcontextprotocol/sdk@latest"
|
|
1019
|
+
})
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
package: "@openai/agents",
|
|
1023
|
+
category: "api",
|
|
1024
|
+
migrations: ["zod-v3->v4"],
|
|
1025
|
+
check: () => ({
|
|
1026
|
+
issue: "OpenAI Agents SDK recommends pinning to zod@3.25.67 due to TS2589 errors with newer versions.",
|
|
1027
|
+
suggestion: "Pin zod to 3.25.67 for OpenAI Agents SDK compatibility, or wait for an SDK update with Zod v4 support.",
|
|
1028
|
+
severity: "error"
|
|
1029
|
+
})
|
|
1030
|
+
},
|
|
1031
|
+
// Additional validation utilities
|
|
1032
|
+
{
|
|
1033
|
+
package: "zod-to-json-schema",
|
|
1034
|
+
category: "validation-util",
|
|
1035
|
+
migrations: ["zod-v3->v4"],
|
|
1036
|
+
check: (version) => {
|
|
1037
|
+
const majorMatch = version.match(/(\d+)/);
|
|
1038
|
+
const major = majorMatch?.[1] ? Number.parseInt(majorMatch[1], 10) : 0;
|
|
1039
|
+
if (major < 4) {
|
|
1040
|
+
return {
|
|
1041
|
+
issue: "zod-to-json-schema v3 may not fully support Zod v4 schemas.",
|
|
1042
|
+
suggestion: "Upgrade to zod-to-json-schema v4+ for full Zod v4 support.",
|
|
1043
|
+
severity: "warning",
|
|
1044
|
+
upgradeCommand: "npm install zod-to-json-schema@latest"
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
},
|
|
1050
|
+
{
|
|
1051
|
+
package: "react-hook-form",
|
|
1052
|
+
category: "form",
|
|
1053
|
+
migrations: ["zod-v3->v4"],
|
|
1054
|
+
check: () => ({
|
|
1055
|
+
issue: "React Hook Form with zodResolver may throw uncaught ZodError instead of populating formState.errors with Zod v4.",
|
|
1056
|
+
suggestion: "Upgrade @hookform/resolvers to the latest version and test form validation thoroughly.",
|
|
1057
|
+
severity: "warning",
|
|
1058
|
+
upgradeCommand: "npm install @hookform/resolvers@latest react-hook-form@latest"
|
|
528
1059
|
})
|
|
529
1060
|
}
|
|
530
1061
|
];
|
|
@@ -534,13 +1065,13 @@ var EcosystemAnalyzer = class {
|
|
|
534
1065
|
const dependencies = [];
|
|
535
1066
|
const warnings = [];
|
|
536
1067
|
const blockers = [];
|
|
537
|
-
const pkgPath = (0,
|
|
538
|
-
if (!(0,
|
|
1068
|
+
const pkgPath = (0, import_node_path2.join)(projectPath, "package.json");
|
|
1069
|
+
if (!(0, import_node_fs2.existsSync)(pkgPath)) {
|
|
539
1070
|
return { dependencies, warnings, blockers };
|
|
540
1071
|
}
|
|
541
1072
|
let allDeps = {};
|
|
542
1073
|
try {
|
|
543
|
-
const pkg = JSON.parse((0,
|
|
1074
|
+
const pkg = JSON.parse((0, import_node_fs2.readFileSync)(pkgPath, "utf-8"));
|
|
544
1075
|
allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
545
1076
|
} catch {
|
|
546
1077
|
return { dependencies, warnings, blockers };
|
|
@@ -558,7 +1089,8 @@ var EcosystemAnalyzer = class {
|
|
|
558
1089
|
issue: result.issue,
|
|
559
1090
|
suggestion: result.suggestion,
|
|
560
1091
|
severity: result.severity,
|
|
561
|
-
category: rule.category
|
|
1092
|
+
category: rule.category,
|
|
1093
|
+
...result.upgradeCommand ? { upgradeCommand: result.upgradeCommand } : {}
|
|
562
1094
|
};
|
|
563
1095
|
dependencies.push(issue);
|
|
564
1096
|
if (result.severity === "error") {
|
|
@@ -569,6 +1101,20 @@ var EcosystemAnalyzer = class {
|
|
|
569
1101
|
}
|
|
570
1102
|
return { dependencies, warnings, blockers };
|
|
571
1103
|
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Returns a list of npm install commands needed to resolve ecosystem issues.
|
|
1106
|
+
*/
|
|
1107
|
+
getUpgradeCommands(report) {
|
|
1108
|
+
const commands = [];
|
|
1109
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1110
|
+
for (const dep of report.dependencies) {
|
|
1111
|
+
if (dep.upgradeCommand && !seen.has(dep.upgradeCommand)) {
|
|
1112
|
+
seen.add(dep.upgradeCommand);
|
|
1113
|
+
commands.push(dep.upgradeCommand);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return commands;
|
|
1117
|
+
}
|
|
572
1118
|
};
|
|
573
1119
|
|
|
574
1120
|
// src/compatibility.ts
|
|
@@ -646,10 +1192,10 @@ var CompatibilityAnalyzer = class {
|
|
|
646
1192
|
ecosystemAnalyzer = new EcosystemAnalyzer();
|
|
647
1193
|
detectVersions(projectPath) {
|
|
648
1194
|
const versions = [];
|
|
649
|
-
const pkgPath = (0,
|
|
650
|
-
if (!(0,
|
|
1195
|
+
const pkgPath = (0, import_node_path3.join)(projectPath, "package.json");
|
|
1196
|
+
if (!(0, import_node_fs3.existsSync)(pkgPath)) return versions;
|
|
651
1197
|
try {
|
|
652
|
-
const pkg = JSON.parse((0,
|
|
1198
|
+
const pkg = JSON.parse((0, import_node_fs3.readFileSync)(pkgPath, "utf-8"));
|
|
653
1199
|
const knownLibs = ["zod", "yup", "joi", "io-ts", "valibot"];
|
|
654
1200
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
655
1201
|
for (const lib of knownLibs) {
|
|
@@ -696,6 +1242,120 @@ var CompatibilityAnalyzer = class {
|
|
|
696
1242
|
}
|
|
697
1243
|
};
|
|
698
1244
|
|
|
1245
|
+
// src/complexity-estimator.ts
|
|
1246
|
+
var SCHEMA_FACTORY_PATTERN = /z\.(object|string|number|boolean|array|enum|union|discriminatedUnion|intersection|lazy|tuple|record|literal|nativeEnum|any|unknown|void|null|undefined|never|date|bigint|symbol|function|promise|map|set|custom|preprocess|pipeline|brand|coerce)\s*\(/g;
|
|
1247
|
+
var ADVANCED_PATTERNS = [
|
|
1248
|
+
[/z\.discriminatedUnion\s*\(/g, "discriminatedUnion"],
|
|
1249
|
+
[/z\.intersection\s*\(/g, "intersection"],
|
|
1250
|
+
[/z\.lazy\s*\(/g, "recursive"],
|
|
1251
|
+
[/\.brand\s*[<(]/g, "branded"],
|
|
1252
|
+
[/\.superRefine\s*\(/g, "superRefine"],
|
|
1253
|
+
[/\.transform\s*\(/g, "transform"],
|
|
1254
|
+
[/\.pipe\s*\(/g, "pipe"],
|
|
1255
|
+
[/\.refine\s*\(/g, "refine"]
|
|
1256
|
+
];
|
|
1257
|
+
function countMatches(text, pattern) {
|
|
1258
|
+
pattern.lastIndex = 0;
|
|
1259
|
+
let count = 0;
|
|
1260
|
+
while (pattern.exec(text)) count++;
|
|
1261
|
+
return count;
|
|
1262
|
+
}
|
|
1263
|
+
function getMaxChainDepth(text) {
|
|
1264
|
+
let maxDepth = 0;
|
|
1265
|
+
const lines = text.split("\n");
|
|
1266
|
+
for (const line of lines) {
|
|
1267
|
+
const dotCalls = line.match(/\.\w+\s*\(/g);
|
|
1268
|
+
if (dotCalls && dotCalls.length > maxDepth) {
|
|
1269
|
+
maxDepth = dotCalls.length;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
return maxDepth;
|
|
1273
|
+
}
|
|
1274
|
+
var ComplexityEstimator = class {
|
|
1275
|
+
estimate(files) {
|
|
1276
|
+
const fileResults = [];
|
|
1277
|
+
const warnings = [];
|
|
1278
|
+
const riskAreas = [];
|
|
1279
|
+
let totalSchemas = 0;
|
|
1280
|
+
let advancedPatternCount = 0;
|
|
1281
|
+
let hasDeepDiscriminatedUnions = false;
|
|
1282
|
+
for (const file of files) {
|
|
1283
|
+
const text = file.getFullText();
|
|
1284
|
+
const filePath = file.getFilePath();
|
|
1285
|
+
const lineCount = file.getEndLineNumber();
|
|
1286
|
+
const schemaCount = countMatches(text, new RegExp(SCHEMA_FACTORY_PATTERN.source, "g"));
|
|
1287
|
+
totalSchemas += schemaCount;
|
|
1288
|
+
const advancedPatterns = [];
|
|
1289
|
+
for (const [pattern, name] of ADVANCED_PATTERNS) {
|
|
1290
|
+
const count = countMatches(text, new RegExp(pattern.source, "g"));
|
|
1291
|
+
if (count > 0) {
|
|
1292
|
+
advancedPatterns.push(name);
|
|
1293
|
+
advancedPatternCount += count;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
const chainDepth = getMaxChainDepth(text);
|
|
1297
|
+
fileResults.push({ filePath, schemaCount, advancedPatterns, chainDepth, lineCount });
|
|
1298
|
+
if (lineCount > 500) {
|
|
1299
|
+
warnings.push({
|
|
1300
|
+
file: filePath,
|
|
1301
|
+
message: `Large file (${lineCount} lines) may be difficult to migrate in one pass`,
|
|
1302
|
+
severity: "warning"
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
if (schemaCount > 50) {
|
|
1306
|
+
warnings.push({
|
|
1307
|
+
file: filePath,
|
|
1308
|
+
message: `High schema density (${schemaCount} schemas) \u2014 consider splitting before migration`,
|
|
1309
|
+
severity: "warning"
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
if (chainDepth > 20) {
|
|
1313
|
+
warnings.push({
|
|
1314
|
+
file: filePath,
|
|
1315
|
+
message: `Long method chain (${chainDepth} calls) \u2014 higher transformation risk`,
|
|
1316
|
+
severity: "warning"
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
const duCount = countMatches(text, /z\.discriminatedUnion\s*\(/g);
|
|
1320
|
+
if (duCount > 10) {
|
|
1321
|
+
hasDeepDiscriminatedUnions = true;
|
|
1322
|
+
warnings.push({
|
|
1323
|
+
file: filePath,
|
|
1324
|
+
message: `${duCount} discriminated unions \u2014 TypeScript TS2589 performance risk in Zod v4`,
|
|
1325
|
+
severity: "error"
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
if (advancedPatterns.includes("recursive")) {
|
|
1329
|
+
riskAreas.push(`Recursive schemas in ${filePath}`);
|
|
1330
|
+
}
|
|
1331
|
+
if (advancedPatterns.includes("branded")) {
|
|
1332
|
+
riskAreas.push(`Branded types in ${filePath}`);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
const effort = this.calculateEffort(
|
|
1336
|
+
totalSchemas,
|
|
1337
|
+
advancedPatternCount,
|
|
1338
|
+
hasDeepDiscriminatedUnions
|
|
1339
|
+
);
|
|
1340
|
+
return {
|
|
1341
|
+
effort,
|
|
1342
|
+
totalSchemas,
|
|
1343
|
+
totalFiles: files.length,
|
|
1344
|
+
advancedPatternCount,
|
|
1345
|
+
files: fileResults,
|
|
1346
|
+
warnings,
|
|
1347
|
+
riskAreas
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
calculateEffort(totalSchemas, advancedCount, hasDeepDU) {
|
|
1351
|
+
if (totalSchemas >= 500 && hasDeepDU) return "extreme";
|
|
1352
|
+
if (totalSchemas >= 200 || advancedCount >= 20) return "high";
|
|
1353
|
+
if (totalSchemas >= 50 || advancedCount >= 5) return "moderate";
|
|
1354
|
+
if (totalSchemas >= 10) return "low";
|
|
1355
|
+
return "trivial";
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
|
|
699
1359
|
// src/config.ts
|
|
700
1360
|
var import_cosmiconfig = require("cosmiconfig");
|
|
701
1361
|
function validateConfig(config) {
|
|
@@ -757,6 +1417,8 @@ async function loadConfig(configPath) {
|
|
|
757
1417
|
}
|
|
758
1418
|
|
|
759
1419
|
// src/dependency-graph.ts
|
|
1420
|
+
var import_node_fs4 = require("fs");
|
|
1421
|
+
var import_node_path4 = require("path");
|
|
760
1422
|
var SchemaDependencyResolver = class {
|
|
761
1423
|
resolve(project, filePaths) {
|
|
762
1424
|
const fileSet = new Set(filePaths);
|
|
@@ -842,10 +1504,214 @@ var SchemaDependencyResolver = class {
|
|
|
842
1504
|
return parts.slice(-2).join("/");
|
|
843
1505
|
}
|
|
844
1506
|
};
|
|
1507
|
+
var SCHEMA_PACKAGES = /* @__PURE__ */ new Set(["zod", "yup", "joi", "io-ts", "valibot", "@effect/schema"]);
|
|
1508
|
+
function computeParallelBatches(packages, suggestedOrder) {
|
|
1509
|
+
const nameSet = new Set(packages.map((p) => p.name));
|
|
1510
|
+
const depMap = /* @__PURE__ */ new Map();
|
|
1511
|
+
for (const pkg of packages) {
|
|
1512
|
+
depMap.set(pkg.name, new Set(pkg.dependencies.filter((d) => nameSet.has(d))));
|
|
1513
|
+
}
|
|
1514
|
+
const depths = /* @__PURE__ */ new Map();
|
|
1515
|
+
const getDepth = (name, visited) => {
|
|
1516
|
+
const cached = depths.get(name);
|
|
1517
|
+
if (cached !== void 0) return cached;
|
|
1518
|
+
if (visited.has(name)) return 0;
|
|
1519
|
+
visited.add(name);
|
|
1520
|
+
const deps = depMap.get(name) ?? /* @__PURE__ */ new Set();
|
|
1521
|
+
let maxDepth = 0;
|
|
1522
|
+
for (const dep of deps) {
|
|
1523
|
+
maxDepth = Math.max(maxDepth, getDepth(dep, visited) + 1);
|
|
1524
|
+
}
|
|
1525
|
+
depths.set(name, maxDepth);
|
|
1526
|
+
return maxDepth;
|
|
1527
|
+
};
|
|
1528
|
+
for (const name of suggestedOrder) {
|
|
1529
|
+
getDepth(name, /* @__PURE__ */ new Set());
|
|
1530
|
+
}
|
|
1531
|
+
const batchMap = /* @__PURE__ */ new Map();
|
|
1532
|
+
for (const name of suggestedOrder) {
|
|
1533
|
+
const depth = depths.get(name) ?? 0;
|
|
1534
|
+
const batch = batchMap.get(depth) ?? [];
|
|
1535
|
+
batch.push(name);
|
|
1536
|
+
batchMap.set(depth, batch);
|
|
1537
|
+
}
|
|
1538
|
+
const batches = [];
|
|
1539
|
+
const sortedDepths = [...batchMap.keys()].sort((a, b) => a - b);
|
|
1540
|
+
for (const depth of sortedDepths) {
|
|
1541
|
+
const pkgs = batchMap.get(depth);
|
|
1542
|
+
if (pkgs) batches.push({ index: batches.length, packages: pkgs });
|
|
1543
|
+
}
|
|
1544
|
+
return batches;
|
|
1545
|
+
}
|
|
1546
|
+
var MonorepoResolver = class {
|
|
1547
|
+
detect(projectPath) {
|
|
1548
|
+
const pkgPath = (0, import_node_path4.join)(projectPath, "package.json");
|
|
1549
|
+
if ((0, import_node_fs4.existsSync)(pkgPath)) {
|
|
1550
|
+
try {
|
|
1551
|
+
const pkg = JSON.parse((0, import_node_fs4.readFileSync)(pkgPath, "utf-8"));
|
|
1552
|
+
if (pkg.workspaces) return true;
|
|
1553
|
+
} catch {
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
if ((0, import_node_fs4.existsSync)((0, import_node_path4.join)(projectPath, "pnpm-workspace.yaml"))) return true;
|
|
1557
|
+
return false;
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Detect which workspace manager is being used.
|
|
1561
|
+
*/
|
|
1562
|
+
detectManager(projectPath) {
|
|
1563
|
+
if ((0, import_node_fs4.existsSync)((0, import_node_path4.join)(projectPath, "pnpm-workspace.yaml"))) return "pnpm";
|
|
1564
|
+
const pkgPath = (0, import_node_path4.join)(projectPath, "package.json");
|
|
1565
|
+
if ((0, import_node_fs4.existsSync)(pkgPath)) {
|
|
1566
|
+
try {
|
|
1567
|
+
const pkg = JSON.parse((0, import_node_fs4.readFileSync)(pkgPath, "utf-8"));
|
|
1568
|
+
if (pkg.packageManager?.startsWith("yarn")) return "yarn";
|
|
1569
|
+
if (pkg.packageManager?.startsWith("pnpm")) return "pnpm";
|
|
1570
|
+
} catch {
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
if ((0, import_node_fs4.existsSync)((0, import_node_path4.join)(projectPath, "pnpm-lock.yaml"))) return "pnpm";
|
|
1574
|
+
if ((0, import_node_fs4.existsSync)((0, import_node_path4.join)(projectPath, "yarn.lock"))) return "yarn";
|
|
1575
|
+
return "npm";
|
|
1576
|
+
}
|
|
1577
|
+
analyze(projectPath) {
|
|
1578
|
+
const pkgPath = (0, import_node_path4.join)(projectPath, "package.json");
|
|
1579
|
+
if (!(0, import_node_fs4.existsSync)(pkgPath)) {
|
|
1580
|
+
return { isMonorepo: false, packages: [], suggestedOrder: [] };
|
|
1581
|
+
}
|
|
1582
|
+
let workspaceGlobs;
|
|
1583
|
+
try {
|
|
1584
|
+
workspaceGlobs = this.resolveWorkspaceGlobs(projectPath);
|
|
1585
|
+
if (workspaceGlobs.length === 0) {
|
|
1586
|
+
return { isMonorepo: false, packages: [], suggestedOrder: [] };
|
|
1587
|
+
}
|
|
1588
|
+
} catch {
|
|
1589
|
+
return { isMonorepo: false, packages: [], suggestedOrder: [] };
|
|
1590
|
+
}
|
|
1591
|
+
const packages = [];
|
|
1592
|
+
const resolvedDirs = this.resolveWorkspaceDirs(projectPath, workspaceGlobs);
|
|
1593
|
+
for (const dir of resolvedDirs) {
|
|
1594
|
+
const wsPkgPath = (0, import_node_path4.join)(dir, "package.json");
|
|
1595
|
+
if (!(0, import_node_fs4.existsSync)(wsPkgPath)) continue;
|
|
1596
|
+
try {
|
|
1597
|
+
const wsPkg = JSON.parse((0, import_node_fs4.readFileSync)(wsPkgPath, "utf-8"));
|
|
1598
|
+
if (!wsPkg.name) continue;
|
|
1599
|
+
const allDeps = { ...wsPkg.dependencies, ...wsPkg.devDependencies };
|
|
1600
|
+
const depNames = Object.keys(allDeps);
|
|
1601
|
+
const schemaLibrary = depNames.find((d) => SCHEMA_PACKAGES.has(d));
|
|
1602
|
+
packages.push({
|
|
1603
|
+
name: wsPkg.name,
|
|
1604
|
+
path: dir,
|
|
1605
|
+
schemaLibrary,
|
|
1606
|
+
dependencies: depNames
|
|
1607
|
+
});
|
|
1608
|
+
} catch {
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
const suggestedOrder = this.suggestOrder(packages);
|
|
1612
|
+
return { isMonorepo: true, packages, suggestedOrder };
|
|
1613
|
+
}
|
|
1614
|
+
suggestOrder(packages) {
|
|
1615
|
+
const nameSet = new Set(packages.map((p) => p.name));
|
|
1616
|
+
const depMap = /* @__PURE__ */ new Map();
|
|
1617
|
+
for (const pkg of packages) {
|
|
1618
|
+
const internalDeps = pkg.dependencies.filter((d) => nameSet.has(d));
|
|
1619
|
+
depMap.set(pkg.name, internalDeps);
|
|
1620
|
+
}
|
|
1621
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1622
|
+
const sorted = [];
|
|
1623
|
+
const visit = (name) => {
|
|
1624
|
+
if (visited.has(name)) return;
|
|
1625
|
+
visited.add(name);
|
|
1626
|
+
for (const dep of depMap.get(name) ?? []) {
|
|
1627
|
+
visit(dep);
|
|
1628
|
+
}
|
|
1629
|
+
sorted.push(name);
|
|
1630
|
+
};
|
|
1631
|
+
for (const pkg of packages) {
|
|
1632
|
+
visit(pkg.name);
|
|
1633
|
+
}
|
|
1634
|
+
return sorted;
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Resolve workspace glob patterns from any supported format.
|
|
1638
|
+
* Supports: npm/yarn workspaces (package.json), pnpm-workspace.yaml
|
|
1639
|
+
*/
|
|
1640
|
+
resolveWorkspaceGlobs(projectPath) {
|
|
1641
|
+
const pnpmPath = (0, import_node_path4.join)(projectPath, "pnpm-workspace.yaml");
|
|
1642
|
+
if ((0, import_node_fs4.existsSync)(pnpmPath)) {
|
|
1643
|
+
return this.parsePnpmWorkspace(pnpmPath);
|
|
1644
|
+
}
|
|
1645
|
+
const pkgPath = (0, import_node_path4.join)(projectPath, "package.json");
|
|
1646
|
+
if ((0, import_node_fs4.existsSync)(pkgPath)) {
|
|
1647
|
+
try {
|
|
1648
|
+
const pkg = JSON.parse((0, import_node_fs4.readFileSync)(pkgPath, "utf-8"));
|
|
1649
|
+
if (pkg.workspaces) {
|
|
1650
|
+
return Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages;
|
|
1651
|
+
}
|
|
1652
|
+
} catch {
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
return [];
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Parse pnpm-workspace.yaml to extract workspace package globs.
|
|
1659
|
+
* Simple YAML parsing for the common format:
|
|
1660
|
+
* ```
|
|
1661
|
+
* packages:
|
|
1662
|
+
* - 'packages/*'
|
|
1663
|
+
* - 'apps/*'
|
|
1664
|
+
* ```
|
|
1665
|
+
*/
|
|
1666
|
+
parsePnpmWorkspace(filePath) {
|
|
1667
|
+
const content = (0, import_node_fs4.readFileSync)(filePath, "utf-8");
|
|
1668
|
+
const globs = [];
|
|
1669
|
+
let inPackages = false;
|
|
1670
|
+
for (const line of content.split("\n")) {
|
|
1671
|
+
const trimmed = line.trim();
|
|
1672
|
+
if (trimmed === "packages:") {
|
|
1673
|
+
inPackages = true;
|
|
1674
|
+
continue;
|
|
1675
|
+
}
|
|
1676
|
+
if (inPackages && /^\w/.test(trimmed) && !trimmed.startsWith("-")) {
|
|
1677
|
+
break;
|
|
1678
|
+
}
|
|
1679
|
+
if (inPackages && trimmed.startsWith("-")) {
|
|
1680
|
+
const pattern = trimmed.replace(/^-\s*/, "").replace(/^['"]|['"]$/g, "");
|
|
1681
|
+
if (pattern) {
|
|
1682
|
+
globs.push(pattern);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
return globs;
|
|
1687
|
+
}
|
|
1688
|
+
resolveWorkspaceDirs(projectPath, globs) {
|
|
1689
|
+
const dirs = [];
|
|
1690
|
+
for (const glob of globs) {
|
|
1691
|
+
const clean = glob.replace(/\/?\*$/, "");
|
|
1692
|
+
const base = (0, import_node_path4.resolve)(projectPath, clean);
|
|
1693
|
+
if (!(0, import_node_fs4.existsSync)(base)) continue;
|
|
1694
|
+
if (glob.endsWith("*")) {
|
|
1695
|
+
try {
|
|
1696
|
+
const entries = (0, import_node_fs4.readdirSync)(base, { withFileTypes: true });
|
|
1697
|
+
for (const entry of entries) {
|
|
1698
|
+
if (entry.isDirectory()) {
|
|
1699
|
+
dirs.push((0, import_node_path4.join)(base, entry.name));
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
} catch {
|
|
1703
|
+
}
|
|
1704
|
+
} else {
|
|
1705
|
+
dirs.push(base);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
return dirs;
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
845
1711
|
|
|
846
1712
|
// src/detailed-analyzer.ts
|
|
847
|
-
var
|
|
848
|
-
var
|
|
1713
|
+
var import_node_fs5 = require("fs");
|
|
1714
|
+
var import_node_path5 = require("path");
|
|
849
1715
|
var COMPLEXITY_CHAIN_WEIGHT = 2;
|
|
850
1716
|
var COMPLEXITY_DEPTH_WEIGHT = 3;
|
|
851
1717
|
var COMPLEXITY_VALIDATION_WEIGHT = 1;
|
|
@@ -910,10 +1776,10 @@ var DetailedAnalyzer = class {
|
|
|
910
1776
|
}
|
|
911
1777
|
detectLibraryVersions(projectPath) {
|
|
912
1778
|
const versions = [];
|
|
913
|
-
const pkgPath = (0,
|
|
914
|
-
if (!(0,
|
|
1779
|
+
const pkgPath = (0, import_node_path5.join)(projectPath, "package.json");
|
|
1780
|
+
if (!(0, import_node_fs5.existsSync)(pkgPath)) return versions;
|
|
915
1781
|
try {
|
|
916
|
-
const pkg = JSON.parse((0,
|
|
1782
|
+
const pkg = JSON.parse((0, import_node_fs5.readFileSync)(pkgPath, "utf-8"));
|
|
917
1783
|
const knownLibs = ["zod", "yup", "joi", "io-ts", "valibot"];
|
|
918
1784
|
const allDeps = {
|
|
919
1785
|
...pkg.dependencies,
|
|
@@ -1086,9 +1952,94 @@ var DetailedAnalyzer = class {
|
|
|
1086
1952
|
}
|
|
1087
1953
|
};
|
|
1088
1954
|
|
|
1955
|
+
// src/form-resolver-migrator.ts
|
|
1956
|
+
var RESOLVER_MAPPINGS = {
|
|
1957
|
+
"yup->zod": [
|
|
1958
|
+
{
|
|
1959
|
+
fromImport: "@hookform/resolvers/yup",
|
|
1960
|
+
toImport: "@hookform/resolvers/zod",
|
|
1961
|
+
fromResolver: "yupResolver",
|
|
1962
|
+
toResolver: "zodResolver"
|
|
1963
|
+
}
|
|
1964
|
+
],
|
|
1965
|
+
"joi->zod": [
|
|
1966
|
+
{
|
|
1967
|
+
fromImport: "@hookform/resolvers/joi",
|
|
1968
|
+
toImport: "@hookform/resolvers/zod",
|
|
1969
|
+
fromResolver: "joiResolver",
|
|
1970
|
+
toResolver: "zodResolver"
|
|
1971
|
+
}
|
|
1972
|
+
],
|
|
1973
|
+
"zod->valibot": [
|
|
1974
|
+
{
|
|
1975
|
+
fromImport: "@hookform/resolvers/zod",
|
|
1976
|
+
toImport: "@hookform/resolvers/valibot",
|
|
1977
|
+
fromResolver: "zodResolver",
|
|
1978
|
+
toResolver: "valibotResolver"
|
|
1979
|
+
}
|
|
1980
|
+
]
|
|
1981
|
+
};
|
|
1982
|
+
var TODO_PATTERNS = [
|
|
1983
|
+
{
|
|
1984
|
+
pattern: /from\s+['"]formik['"]/,
|
|
1985
|
+
comment: "/* TODO(schemashift): Formik is unmaintained. Consider migrating to React Hook Form with zodResolver */"
|
|
1986
|
+
},
|
|
1987
|
+
{
|
|
1988
|
+
pattern: /from\s+['"]@mantine\/form['"]/,
|
|
1989
|
+
comment: "/* TODO(schemashift): Update @mantine/form to use Zod schema adapter */"
|
|
1990
|
+
}
|
|
1991
|
+
];
|
|
1992
|
+
var FormResolverMigrator = class {
|
|
1993
|
+
migrate(sourceFile, from, to) {
|
|
1994
|
+
const migration = `${from}->${to}`;
|
|
1995
|
+
let code = sourceFile.getFullText();
|
|
1996
|
+
const changes = [];
|
|
1997
|
+
const warnings = [];
|
|
1998
|
+
const mappings = RESOLVER_MAPPINGS[migration];
|
|
1999
|
+
if (mappings) {
|
|
2000
|
+
for (const mapping of mappings) {
|
|
2001
|
+
if (code.includes(mapping.fromImport)) {
|
|
2002
|
+
code = code.replaceAll(mapping.fromImport, mapping.toImport);
|
|
2003
|
+
code = code.replaceAll(mapping.fromResolver, mapping.toResolver);
|
|
2004
|
+
changes.push(
|
|
2005
|
+
`Replaced ${mapping.fromResolver} import from '${mapping.fromImport}' with ${mapping.toResolver} from '${mapping.toImport}'`
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
const lines = code.split("\n");
|
|
2011
|
+
const insertions = [];
|
|
2012
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2013
|
+
const line = lines[i] ?? "";
|
|
2014
|
+
for (const { pattern, comment } of TODO_PATTERNS) {
|
|
2015
|
+
if (pattern.test(line) && !code.includes(comment)) {
|
|
2016
|
+
insertions.push({ index: i, comment });
|
|
2017
|
+
warnings.push(comment.replace(/\/\*\s*|\s*\*\//g, "").trim());
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
for (let i = insertions.length - 1; i >= 0; i--) {
|
|
2022
|
+
const insertion = insertions[i];
|
|
2023
|
+
if (!insertion) continue;
|
|
2024
|
+
lines.splice(insertion.index, 0, insertion.comment);
|
|
2025
|
+
changes.push(`Added TODO comment for ${lines[insertion.index + 1]?.trim()}`);
|
|
2026
|
+
}
|
|
2027
|
+
if (insertions.length > 0) {
|
|
2028
|
+
code = lines.join("\n");
|
|
2029
|
+
}
|
|
2030
|
+
return {
|
|
2031
|
+
success: true,
|
|
2032
|
+
transformedCode: code,
|
|
2033
|
+
changes,
|
|
2034
|
+
warnings
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
};
|
|
2038
|
+
|
|
1089
2039
|
// src/governance.ts
|
|
1090
2040
|
var GovernanceEngine = class {
|
|
1091
2041
|
rules = /* @__PURE__ */ new Map();
|
|
2042
|
+
customRuleFunctions = /* @__PURE__ */ new Map();
|
|
1092
2043
|
configure(rules) {
|
|
1093
2044
|
this.rules.clear();
|
|
1094
2045
|
for (const [name, config] of Object.entries(rules)) {
|
|
@@ -1097,6 +2048,13 @@ var GovernanceEngine = class {
|
|
|
1097
2048
|
}
|
|
1098
2049
|
}
|
|
1099
2050
|
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Register a custom governance rule function.
|
|
2053
|
+
* Custom rules are executed per-file alongside built-in rules.
|
|
2054
|
+
*/
|
|
2055
|
+
registerRule(name, fn) {
|
|
2056
|
+
this.customRuleFunctions.set(name, fn);
|
|
2057
|
+
}
|
|
1100
2058
|
analyze(project) {
|
|
1101
2059
|
const violations = [];
|
|
1102
2060
|
let schemasChecked = 0;
|
|
@@ -1172,6 +2130,104 @@ var GovernanceEngine = class {
|
|
|
1172
2130
|
});
|
|
1173
2131
|
}
|
|
1174
2132
|
}
|
|
2133
|
+
if (this.rules.has("require-safeParse")) {
|
|
2134
|
+
if (text.includes(".parse(") && !text.includes(".safeParse(")) {
|
|
2135
|
+
violations.push({
|
|
2136
|
+
rule: "require-safeParse",
|
|
2137
|
+
message: `Schema "${schemaName}" uses .parse() \u2014 prefer .safeParse() for safer error handling`,
|
|
2138
|
+
filePath,
|
|
2139
|
+
lineNumber,
|
|
2140
|
+
schemaName,
|
|
2141
|
+
severity: "warning",
|
|
2142
|
+
fixable: true
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
if (this.rules.has("require-description")) {
|
|
2147
|
+
if (!text.includes(".describe(")) {
|
|
2148
|
+
violations.push({
|
|
2149
|
+
rule: "require-description",
|
|
2150
|
+
message: `Schema "${schemaName}" missing .describe() \u2014 add a description for documentation`,
|
|
2151
|
+
filePath,
|
|
2152
|
+
lineNumber,
|
|
2153
|
+
schemaName,
|
|
2154
|
+
severity: "warning",
|
|
2155
|
+
fixable: true
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
if (this.rules.has("no-coerce-in-api")) {
|
|
2160
|
+
if (/\.coerce\./.test(text)) {
|
|
2161
|
+
violations.push({
|
|
2162
|
+
rule: "no-coerce-in-api",
|
|
2163
|
+
message: `Schema "${schemaName}" uses z.coerce.* \u2014 coercion in API validation is a security risk`,
|
|
2164
|
+
filePath,
|
|
2165
|
+
lineNumber,
|
|
2166
|
+
schemaName,
|
|
2167
|
+
severity: "error",
|
|
2168
|
+
fixable: false
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
if (this.rules.has("require-max-length")) {
|
|
2173
|
+
if (text.includes(".string()") && !text.includes(".max(") && !text.includes(".length(")) {
|
|
2174
|
+
violations.push({
|
|
2175
|
+
rule: "require-max-length",
|
|
2176
|
+
message: `Schema "${schemaName}" has string without max length \u2014 required for DoS prevention`,
|
|
2177
|
+
filePath,
|
|
2178
|
+
lineNumber,
|
|
2179
|
+
schemaName,
|
|
2180
|
+
severity: "error",
|
|
2181
|
+
fixable: true
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
if (this.rules.has("max-nesting-depth")) {
|
|
2186
|
+
const config = this.rules.get("max-nesting-depth") ?? {};
|
|
2187
|
+
const maxDepth = config.threshold ?? 5;
|
|
2188
|
+
const depth = this.measureNestingDepth(text);
|
|
2189
|
+
if (depth > maxDepth) {
|
|
2190
|
+
violations.push({
|
|
2191
|
+
rule: "max-nesting-depth",
|
|
2192
|
+
message: `Schema "${schemaName}" nesting depth (${depth}) exceeds limit (${maxDepth})`,
|
|
2193
|
+
filePath,
|
|
2194
|
+
lineNumber,
|
|
2195
|
+
schemaName,
|
|
2196
|
+
severity: "warning",
|
|
2197
|
+
fixable: false
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
2204
|
+
const library = this.detectFileLibrary(sourceFile);
|
|
2205
|
+
if (library === "unknown") continue;
|
|
2206
|
+
const filePath = sourceFile.getFilePath();
|
|
2207
|
+
const text = sourceFile.getFullText();
|
|
2208
|
+
if (this.rules.has("no-dynamic-schemas")) {
|
|
2209
|
+
const dynamicPatterns = this.detectDynamicSchemas(text, library);
|
|
2210
|
+
for (const lineNumber of dynamicPatterns) {
|
|
2211
|
+
violations.push({
|
|
2212
|
+
rule: "no-dynamic-schemas",
|
|
2213
|
+
message: "Schema created inside function body \u2014 move to module level for performance",
|
|
2214
|
+
filePath,
|
|
2215
|
+
lineNumber,
|
|
2216
|
+
schemaName: "(dynamic)",
|
|
2217
|
+
severity: "warning",
|
|
2218
|
+
fixable: false
|
|
2219
|
+
});
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
for (const [ruleName, ruleFn] of this.customRuleFunctions) {
|
|
2224
|
+
const config = this.rules.get(ruleName);
|
|
2225
|
+
if (!config) continue;
|
|
2226
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
2227
|
+
const library = this.detectFileLibrary(sourceFile);
|
|
2228
|
+
if (library === "unknown") continue;
|
|
2229
|
+
const ruleViolations = ruleFn(sourceFile, config);
|
|
2230
|
+
violations.push(...ruleViolations);
|
|
1175
2231
|
}
|
|
1176
2232
|
}
|
|
1177
2233
|
return {
|
|
@@ -1188,6 +2244,57 @@ var GovernanceEngine = class {
|
|
|
1188
2244
|
}
|
|
1189
2245
|
return "unknown";
|
|
1190
2246
|
}
|
|
2247
|
+
measureNestingDepth(text) {
|
|
2248
|
+
let maxDepth = 0;
|
|
2249
|
+
let current = 0;
|
|
2250
|
+
for (const char of text) {
|
|
2251
|
+
if (char === "(") {
|
|
2252
|
+
current++;
|
|
2253
|
+
if (current > maxDepth) maxDepth = current;
|
|
2254
|
+
} else if (char === ")") {
|
|
2255
|
+
current--;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
return maxDepth;
|
|
2259
|
+
}
|
|
2260
|
+
detectDynamicSchemas(text, library) {
|
|
2261
|
+
const lineNumbers = [];
|
|
2262
|
+
const prefix = this.getSchemaPrefix(library);
|
|
2263
|
+
if (!prefix) return lineNumbers;
|
|
2264
|
+
const lines = text.split("\n");
|
|
2265
|
+
let insideFunction = 0;
|
|
2266
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2267
|
+
const line = lines[i] ?? "";
|
|
2268
|
+
const opens = (line.match(/\{/g) || []).length;
|
|
2269
|
+
const closes = (line.match(/\}/g) || []).length;
|
|
2270
|
+
if (/(?:function\s+\w+|=>)\s*\{/.test(line)) {
|
|
2271
|
+
insideFunction += opens;
|
|
2272
|
+
insideFunction -= closes;
|
|
2273
|
+
continue;
|
|
2274
|
+
}
|
|
2275
|
+
insideFunction += opens - closes;
|
|
2276
|
+
if (insideFunction > 0 && line.includes(prefix)) {
|
|
2277
|
+
lineNumbers.push(i + 1);
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
return lineNumbers;
|
|
2281
|
+
}
|
|
2282
|
+
getSchemaPrefix(library) {
|
|
2283
|
+
switch (library) {
|
|
2284
|
+
case "zod":
|
|
2285
|
+
return "z.";
|
|
2286
|
+
case "yup":
|
|
2287
|
+
return "yup.";
|
|
2288
|
+
case "joi":
|
|
2289
|
+
return "Joi.";
|
|
2290
|
+
case "io-ts":
|
|
2291
|
+
return "t.";
|
|
2292
|
+
case "valibot":
|
|
2293
|
+
return "v.";
|
|
2294
|
+
default:
|
|
2295
|
+
return null;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
1191
2298
|
isSchemaExpression(text, library) {
|
|
1192
2299
|
switch (library) {
|
|
1193
2300
|
case "zod":
|
|
@@ -1207,16 +2314,16 @@ var GovernanceEngine = class {
|
|
|
1207
2314
|
};
|
|
1208
2315
|
|
|
1209
2316
|
// src/incremental.ts
|
|
1210
|
-
var
|
|
1211
|
-
var
|
|
2317
|
+
var import_node_fs6 = require("fs");
|
|
2318
|
+
var import_node_path6 = require("path");
|
|
1212
2319
|
var STATE_DIR = ".schemashift";
|
|
1213
2320
|
var STATE_FILE = "incremental.json";
|
|
1214
2321
|
var IncrementalTracker = class {
|
|
1215
2322
|
stateDir;
|
|
1216
2323
|
statePath;
|
|
1217
2324
|
constructor(projectPath) {
|
|
1218
|
-
this.stateDir = (0,
|
|
1219
|
-
this.statePath = (0,
|
|
2325
|
+
this.stateDir = (0, import_node_path6.join)(projectPath, STATE_DIR);
|
|
2326
|
+
this.statePath = (0, import_node_path6.join)(this.stateDir, STATE_FILE);
|
|
1220
2327
|
}
|
|
1221
2328
|
start(files, from, to) {
|
|
1222
2329
|
const state = {
|
|
@@ -1251,9 +2358,9 @@ var IncrementalTracker = class {
|
|
|
1251
2358
|
this.saveState(state);
|
|
1252
2359
|
}
|
|
1253
2360
|
getState() {
|
|
1254
|
-
if (!(0,
|
|
2361
|
+
if (!(0, import_node_fs6.existsSync)(this.statePath)) return null;
|
|
1255
2362
|
try {
|
|
1256
|
-
return JSON.parse((0,
|
|
2363
|
+
return JSON.parse((0, import_node_fs6.readFileSync)(this.statePath, "utf-8"));
|
|
1257
2364
|
} catch {
|
|
1258
2365
|
return null;
|
|
1259
2366
|
}
|
|
@@ -1280,21 +2387,21 @@ var IncrementalTracker = class {
|
|
|
1280
2387
|
};
|
|
1281
2388
|
}
|
|
1282
2389
|
clear() {
|
|
1283
|
-
if ((0,
|
|
1284
|
-
(0,
|
|
2390
|
+
if ((0, import_node_fs6.existsSync)(this.statePath)) {
|
|
2391
|
+
(0, import_node_fs6.unlinkSync)(this.statePath);
|
|
1285
2392
|
}
|
|
1286
2393
|
}
|
|
1287
2394
|
saveState(state) {
|
|
1288
|
-
if (!(0,
|
|
1289
|
-
(0,
|
|
2395
|
+
if (!(0, import_node_fs6.existsSync)(this.stateDir)) {
|
|
2396
|
+
(0, import_node_fs6.mkdirSync)(this.stateDir, { recursive: true });
|
|
1290
2397
|
}
|
|
1291
|
-
(0,
|
|
2398
|
+
(0, import_node_fs6.writeFileSync)(this.statePath, JSON.stringify(state, null, 2));
|
|
1292
2399
|
}
|
|
1293
2400
|
};
|
|
1294
2401
|
|
|
1295
2402
|
// src/package-updater.ts
|
|
1296
|
-
var
|
|
1297
|
-
var
|
|
2403
|
+
var import_node_fs7 = require("fs");
|
|
2404
|
+
var import_node_path7 = require("path");
|
|
1298
2405
|
var TARGET_VERSIONS = {
|
|
1299
2406
|
"yup->zod": { zod: "^3.24.0" },
|
|
1300
2407
|
"joi->zod": { zod: "^3.24.0" },
|
|
@@ -1315,14 +2422,14 @@ var PackageUpdater = class {
|
|
|
1315
2422
|
const add = {};
|
|
1316
2423
|
const remove = [];
|
|
1317
2424
|
const warnings = [];
|
|
1318
|
-
const pkgPath = (0,
|
|
1319
|
-
if (!(0,
|
|
2425
|
+
const pkgPath = (0, import_node_path7.join)(projectPath, "package.json");
|
|
2426
|
+
if (!(0, import_node_fs7.existsSync)(pkgPath)) {
|
|
1320
2427
|
warnings.push("No package.json found. Cannot plan dependency updates.");
|
|
1321
2428
|
return { add, remove, warnings };
|
|
1322
2429
|
}
|
|
1323
2430
|
let pkg;
|
|
1324
2431
|
try {
|
|
1325
|
-
pkg = JSON.parse((0,
|
|
2432
|
+
pkg = JSON.parse((0, import_node_fs7.readFileSync)(pkgPath, "utf-8"));
|
|
1326
2433
|
} catch {
|
|
1327
2434
|
warnings.push("Could not parse package.json.");
|
|
1328
2435
|
return { add, remove, warnings };
|
|
@@ -1352,9 +2459,9 @@ var PackageUpdater = class {
|
|
|
1352
2459
|
return { add, remove, warnings };
|
|
1353
2460
|
}
|
|
1354
2461
|
apply(projectPath, plan) {
|
|
1355
|
-
const pkgPath = (0,
|
|
1356
|
-
if (!(0,
|
|
1357
|
-
const pkgText = (0,
|
|
2462
|
+
const pkgPath = (0, import_node_path7.join)(projectPath, "package.json");
|
|
2463
|
+
if (!(0, import_node_fs7.existsSync)(pkgPath)) return;
|
|
2464
|
+
const pkgText = (0, import_node_fs7.readFileSync)(pkgPath, "utf-8");
|
|
1358
2465
|
const pkg = JSON.parse(pkgText);
|
|
1359
2466
|
if (!pkg.dependencies) pkg.dependencies = {};
|
|
1360
2467
|
for (const [name, version] of Object.entries(plan.add)) {
|
|
@@ -1364,11 +2471,133 @@ var PackageUpdater = class {
|
|
|
1364
2471
|
pkg.dependencies[name] = version;
|
|
1365
2472
|
}
|
|
1366
2473
|
}
|
|
1367
|
-
(0,
|
|
2474
|
+
(0, import_node_fs7.writeFileSync)(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
1368
2475
|
`);
|
|
1369
2476
|
}
|
|
1370
2477
|
};
|
|
1371
2478
|
|
|
2479
|
+
// src/performance-analyzer.ts
|
|
2480
|
+
var PerformanceAnalyzer = class {
|
|
2481
|
+
analyze(sourceFiles, from, to) {
|
|
2482
|
+
const warnings = [];
|
|
2483
|
+
let parseCallSites = 0;
|
|
2484
|
+
let dynamicSchemaCount = 0;
|
|
2485
|
+
for (const file of sourceFiles) {
|
|
2486
|
+
const text = file.getFullText();
|
|
2487
|
+
const filePath = file.getFilePath();
|
|
2488
|
+
const parseMatches = text.match(/\.(parse|safeParse)\s*\(/g);
|
|
2489
|
+
if (parseMatches) {
|
|
2490
|
+
parseCallSites += parseMatches.length;
|
|
2491
|
+
}
|
|
2492
|
+
const dynamicResult = this.detectDynamicSchemas(text, filePath);
|
|
2493
|
+
dynamicSchemaCount += dynamicResult.count;
|
|
2494
|
+
warnings.push(...dynamicResult.warnings);
|
|
2495
|
+
this.addMigrationWarnings(text, filePath, from, to, warnings);
|
|
2496
|
+
}
|
|
2497
|
+
const recommendation = this.getRecommendation(from, to, parseCallSites, dynamicSchemaCount);
|
|
2498
|
+
const summary = this.generateSummary(warnings, parseCallSites, dynamicSchemaCount);
|
|
2499
|
+
return {
|
|
2500
|
+
warnings,
|
|
2501
|
+
parseCallSites,
|
|
2502
|
+
dynamicSchemaCount,
|
|
2503
|
+
recommendation,
|
|
2504
|
+
summary
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
detectDynamicSchemas(text, filePath) {
|
|
2508
|
+
const warnings = [];
|
|
2509
|
+
let count = 0;
|
|
2510
|
+
const functionBodyPattern = /(?:function\s+\w+\s*\([^)]*\)|const\s+\w+\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>)\s*\{[^}]*(?:z\.|yup\.|Joi\.|v\.)\w+\s*\(/g;
|
|
2511
|
+
for (const match of text.matchAll(functionBodyPattern)) {
|
|
2512
|
+
count++;
|
|
2513
|
+
const lineNumber = text.substring(0, match.index).split("\n").length;
|
|
2514
|
+
warnings.push({
|
|
2515
|
+
category: "dynamic-schemas",
|
|
2516
|
+
message: "Schema created inside function body \u2014 may cause performance issues with Zod v4.",
|
|
2517
|
+
detail: "Zod v4 uses JIT compilation, making schema creation ~17x slower than v3. Move schema definitions to module level to avoid re-creation on every call.",
|
|
2518
|
+
filePath,
|
|
2519
|
+
lineNumber,
|
|
2520
|
+
severity: "warning"
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
const reactComponentPattern = /(?:function\s+[A-Z]\w*\s*\([^)]*\)|const\s+[A-Z]\w*\s*[:=])[^{]*\{[^}]*(?:z\.|yup\.|Joi\.)\w+\s*\(/g;
|
|
2524
|
+
for (const match of text.matchAll(reactComponentPattern)) {
|
|
2525
|
+
count++;
|
|
2526
|
+
const lineNumber = text.substring(0, match.index).split("\n").length;
|
|
2527
|
+
warnings.push({
|
|
2528
|
+
category: "schema-creation",
|
|
2529
|
+
message: "Schema appears to be created inside a React component.",
|
|
2530
|
+
detail: "Schemas created inside React components are re-created on every render. Move schema definitions outside the component or wrap in useMemo(). This is especially important for Zod v4 due to JIT compilation overhead.",
|
|
2531
|
+
filePath,
|
|
2532
|
+
lineNumber,
|
|
2533
|
+
severity: "warning"
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
return { count, warnings };
|
|
2537
|
+
}
|
|
2538
|
+
addMigrationWarnings(text, filePath, from, to, warnings) {
|
|
2539
|
+
const migration = `${from}->${to}`;
|
|
2540
|
+
if (migration === "zod-v3->v4") {
|
|
2541
|
+
if (/edge-runtime|@vercel\/edge|cloudflare.*workers|deno\.serve|Deno\.serve/i.test(text) || /export\s+const\s+runtime\s*=\s*['"]edge['"]/i.test(text)) {
|
|
2542
|
+
warnings.push({
|
|
2543
|
+
category: "cold-start",
|
|
2544
|
+
message: "Edge/serverless environment detected \u2014 Zod v4 JIT compilation increases cold start time.",
|
|
2545
|
+
detail: "Zod v4 JIT trades slower schema creation for faster repeated parsing. In serverless/edge environments with short-lived instances, the JIT cost may not amortize. Consider Valibot or staying on Zod v3 for cold-start-sensitive code.",
|
|
2546
|
+
filePath,
|
|
2547
|
+
severity: "warning"
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
const parseCount = (text.match(/\.parse\s*\(/g) || []).length;
|
|
2551
|
+
if (parseCount > 10) {
|
|
2552
|
+
warnings.push({
|
|
2553
|
+
category: "repeated-parsing",
|
|
2554
|
+
message: `High parse() usage (${parseCount} call sites) \u2014 Zod v4 JIT will benefit here.`,
|
|
2555
|
+
detail: "Zod v4 JIT compilation makes repeated parsing ~8x faster. This file has many parse() calls and will see performance improvement.",
|
|
2556
|
+
filePath,
|
|
2557
|
+
severity: "info"
|
|
2558
|
+
});
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
if (migration === "zod->valibot" && /\.parse\s*\(/.test(text)) {
|
|
2562
|
+
warnings.push({
|
|
2563
|
+
category: "repeated-parsing",
|
|
2564
|
+
message: "Valibot parsing performance is comparable to Zod v4 for most schemas.",
|
|
2565
|
+
detail: "Valibot v1+ offers similar runtime performance to Zod v4 with significantly smaller bundle size. No JIT overhead means consistent performance across all environments.",
|
|
2566
|
+
filePath,
|
|
2567
|
+
severity: "info"
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
getRecommendation(from, to, parseCallSites, dynamicSchemaCount) {
|
|
2572
|
+
const migration = `${from}->${to}`;
|
|
2573
|
+
if (migration === "zod-v3->v4") {
|
|
2574
|
+
if (dynamicSchemaCount > 5) {
|
|
2575
|
+
return "Many dynamic schemas detected. Zod v4 JIT makes schema creation 17x slower. Move schemas to module level before migrating, or consider Valibot for size-sensitive apps.";
|
|
2576
|
+
}
|
|
2577
|
+
if (parseCallSites > 50) {
|
|
2578
|
+
return "High parse() volume detected. Zod v4 JIT will significantly benefit repeated parsing (up to 8x faster). Migration recommended for performance.";
|
|
2579
|
+
}
|
|
2580
|
+
return "Moderate usage detected. Zod v4 trades slower startup for faster runtime parsing.";
|
|
2581
|
+
}
|
|
2582
|
+
if (migration === "zod->valibot") {
|
|
2583
|
+
return "Valibot offers similar runtime performance with significantly smaller bundle size. Best suited for bundle-size-sensitive applications.";
|
|
2584
|
+
}
|
|
2585
|
+
if (from === "yup" || from === "joi") {
|
|
2586
|
+
return `Migrating from ${from} to ${to} should have neutral or positive performance impact.`;
|
|
2587
|
+
}
|
|
2588
|
+
return "Performance impact depends on usage patterns. Review warnings for details.";
|
|
2589
|
+
}
|
|
2590
|
+
generateSummary(warnings, parseCallSites, dynamicSchemaCount) {
|
|
2591
|
+
const parts = [];
|
|
2592
|
+
parts.push(`${parseCallSites} parse/safeParse call sites`);
|
|
2593
|
+
if (dynamicSchemaCount > 0) {
|
|
2594
|
+
parts.push(`${dynamicSchemaCount} dynamic schema creation sites`);
|
|
2595
|
+
}
|
|
2596
|
+
parts.push(`${warnings.length} performance warning(s)`);
|
|
2597
|
+
return parts.join(", ");
|
|
2598
|
+
}
|
|
2599
|
+
};
|
|
2600
|
+
|
|
1372
2601
|
// src/plugin-loader.ts
|
|
1373
2602
|
var PluginLoader = class {
|
|
1374
2603
|
async loadPlugins(pluginPaths) {
|
|
@@ -1414,8 +2643,8 @@ var PluginLoader = class {
|
|
|
1414
2643
|
};
|
|
1415
2644
|
|
|
1416
2645
|
// src/standard-schema.ts
|
|
1417
|
-
var
|
|
1418
|
-
var
|
|
2646
|
+
var import_node_fs8 = require("fs");
|
|
2647
|
+
var import_node_path8 = require("path");
|
|
1419
2648
|
var STANDARD_SCHEMA_LIBRARIES = {
|
|
1420
2649
|
zod: { minMajor: 3, minMinor: 23 },
|
|
1421
2650
|
// Zod v3.23+ and v4+
|
|
@@ -1444,16 +2673,16 @@ function isVersionCompatible(version, minMajor, minMinor) {
|
|
|
1444
2673
|
return false;
|
|
1445
2674
|
}
|
|
1446
2675
|
function detectStandardSchema(projectPath) {
|
|
1447
|
-
const pkgPath = (0,
|
|
1448
|
-
if (!(0,
|
|
1449
|
-
return { detected: false, compatibleLibraries: [], recommendation: "" };
|
|
2676
|
+
const pkgPath = (0, import_node_path8.join)(projectPath, "package.json");
|
|
2677
|
+
if (!(0, import_node_fs8.existsSync)(pkgPath)) {
|
|
2678
|
+
return { detected: false, compatibleLibraries: [], recommendation: "", interopTools: [] };
|
|
1450
2679
|
}
|
|
1451
2680
|
let allDeps = {};
|
|
1452
2681
|
try {
|
|
1453
|
-
const pkg = JSON.parse((0,
|
|
2682
|
+
const pkg = JSON.parse((0, import_node_fs8.readFileSync)(pkgPath, "utf-8"));
|
|
1454
2683
|
allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1455
2684
|
} catch {
|
|
1456
|
-
return { detected: false, compatibleLibraries: [], recommendation: "" };
|
|
2685
|
+
return { detected: false, compatibleLibraries: [], recommendation: "", interopTools: [] };
|
|
1457
2686
|
}
|
|
1458
2687
|
const hasExplicitStandardSchema = "@standard-schema/spec" in allDeps;
|
|
1459
2688
|
const compatibleLibraries = [];
|
|
@@ -1472,9 +2701,155 @@ function detectStandardSchema(projectPath) {
|
|
|
1472
2701
|
} else if (hasExplicitStandardSchema) {
|
|
1473
2702
|
recommendation = "Standard Schema spec detected. Ensure your validation library supports Standard Schema for maximum interoperability.";
|
|
1474
2703
|
}
|
|
1475
|
-
|
|
2704
|
+
let adoptionPath;
|
|
2705
|
+
if (detected && !hasExplicitStandardSchema) {
|
|
2706
|
+
adoptionPath = "Install @standard-schema/spec for explicit Standard Schema support. This enables library-agnostic validation consumers to accept your schemas without depending on a specific library. Run: npm install @standard-schema/spec";
|
|
2707
|
+
} else if (!detected) {
|
|
2708
|
+
adoptionPath = "Consider migrating to a Standard Schema-compatible library (Zod v3.23+, Valibot v1+, ArkType v2+) to future-proof your validation layer and reduce library lock-in.";
|
|
2709
|
+
}
|
|
2710
|
+
const interopTools = detected ? [
|
|
2711
|
+
"tRPC v11+ (Standard Schema input validation)",
|
|
2712
|
+
"TanStack Form (schema-agnostic validation)",
|
|
2713
|
+
"TanStack Router (route parameter validation)",
|
|
2714
|
+
"Hono (request validation middleware)",
|
|
2715
|
+
"Conform (progressive form validation)",
|
|
2716
|
+
"Nuxt (runtime config validation)"
|
|
2717
|
+
] : [];
|
|
2718
|
+
return { detected, compatibleLibraries, recommendation, adoptionPath, interopTools };
|
|
1476
2719
|
}
|
|
1477
2720
|
|
|
2721
|
+
// src/test-scaffolder.ts
|
|
2722
|
+
var TestScaffolder = class {
|
|
2723
|
+
scaffold(sourceFiles, from, to) {
|
|
2724
|
+
const tests = [];
|
|
2725
|
+
let totalSchemas = 0;
|
|
2726
|
+
for (const file of sourceFiles) {
|
|
2727
|
+
const schemas = this.extractSchemaNames(file, from);
|
|
2728
|
+
if (schemas.length === 0) continue;
|
|
2729
|
+
totalSchemas += schemas.length;
|
|
2730
|
+
const testCode = this.generateTestFile(file, schemas, from, to);
|
|
2731
|
+
const filePath = file.getFilePath().replace(/\.tsx?$/, ".migration-test.ts");
|
|
2732
|
+
tests.push({ filePath, testCode, schemaCount: schemas.length });
|
|
2733
|
+
}
|
|
2734
|
+
const summary = tests.length > 0 ? `Generated ${tests.length} test file(s) covering ${totalSchemas} schema(s) for ${from}->${to} migration.` : "No schemas found to generate tests for.";
|
|
2735
|
+
return { tests, totalSchemas, summary };
|
|
2736
|
+
}
|
|
2737
|
+
extractSchemaNames(file, library) {
|
|
2738
|
+
const names = [];
|
|
2739
|
+
const prefixes = this.getLibraryPrefixes(library);
|
|
2740
|
+
for (const varDecl of file.getVariableDeclarations()) {
|
|
2741
|
+
const initializer = varDecl.getInitializer();
|
|
2742
|
+
if (!initializer) continue;
|
|
2743
|
+
const text = initializer.getText();
|
|
2744
|
+
if (prefixes.some((p) => text.startsWith(p))) {
|
|
2745
|
+
names.push(varDecl.getName());
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
return names;
|
|
2749
|
+
}
|
|
2750
|
+
getLibraryPrefixes(library) {
|
|
2751
|
+
switch (library) {
|
|
2752
|
+
case "zod":
|
|
2753
|
+
case "zod-v3":
|
|
2754
|
+
return ["z.", "zod."];
|
|
2755
|
+
case "yup":
|
|
2756
|
+
return ["yup.", "Yup."];
|
|
2757
|
+
case "joi":
|
|
2758
|
+
return ["Joi.", "joi."];
|
|
2759
|
+
case "io-ts":
|
|
2760
|
+
return ["t."];
|
|
2761
|
+
case "valibot":
|
|
2762
|
+
return ["v.", "valibot."];
|
|
2763
|
+
default:
|
|
2764
|
+
return ["z."];
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
generateTestFile(file, schemaNames, from, to) {
|
|
2768
|
+
const relativePath = file.getFilePath();
|
|
2769
|
+
const schemaImports = schemaNames.join(", ");
|
|
2770
|
+
const parseMethod = this.getParseMethod(to);
|
|
2771
|
+
const errorClass = this.getErrorClass(to);
|
|
2772
|
+
const testCases = schemaNames.map((name) => this.generateSchemaTests(name, to, parseMethod, errorClass)).join("\n\n");
|
|
2773
|
+
return `/**
|
|
2774
|
+
* Migration validation tests for ${from} -> ${to}
|
|
2775
|
+
* Auto-generated by SchemaShift
|
|
2776
|
+
*
|
|
2777
|
+
* These tests verify that schema behavior is preserved after migration.
|
|
2778
|
+
* Run before and after migration to ensure equivalence.
|
|
2779
|
+
*
|
|
2780
|
+
* Source: ${relativePath}
|
|
2781
|
+
*/
|
|
2782
|
+
import { describe, expect, it } from 'vitest';
|
|
2783
|
+
import { ${schemaImports} } from '${relativePath.replace(/\.ts$/, ".js")}';
|
|
2784
|
+
|
|
2785
|
+
describe('Migration validation: ${relativePath}', () => {
|
|
2786
|
+
${testCases}
|
|
2787
|
+
});
|
|
2788
|
+
`;
|
|
2789
|
+
}
|
|
2790
|
+
getParseMethod(to) {
|
|
2791
|
+
switch (to) {
|
|
2792
|
+
case "valibot":
|
|
2793
|
+
return "v.safeParse";
|
|
2794
|
+
default:
|
|
2795
|
+
return ".safeParse";
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
getErrorClass(to) {
|
|
2799
|
+
switch (to) {
|
|
2800
|
+
case "valibot":
|
|
2801
|
+
return "ValiError";
|
|
2802
|
+
case "zod":
|
|
2803
|
+
case "v4":
|
|
2804
|
+
return "ZodError";
|
|
2805
|
+
default:
|
|
2806
|
+
return "Error";
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
generateSchemaTests(schemaName, to, _parseMethod, _errorClass) {
|
|
2810
|
+
if (to === "valibot") {
|
|
2811
|
+
return ` describe('${schemaName}', () => {
|
|
2812
|
+
it('should accept valid data', () => {
|
|
2813
|
+
// TODO(schemashift): Add valid test data for ${schemaName}
|
|
2814
|
+
// const result = v.safeParse(${schemaName}, validData);
|
|
2815
|
+
// expect(result.success).toBe(true);
|
|
2816
|
+
});
|
|
2817
|
+
|
|
2818
|
+
it('should reject invalid data', () => {
|
|
2819
|
+
// TODO(schemashift): Add invalid test data for ${schemaName}
|
|
2820
|
+
// const result = v.safeParse(${schemaName}, invalidData);
|
|
2821
|
+
// expect(result.success).toBe(false);
|
|
2822
|
+
});
|
|
2823
|
+
|
|
2824
|
+
it('should preserve error messages', () => {
|
|
2825
|
+
// TODO(schemashift): Verify custom error messages are preserved
|
|
2826
|
+
// const result = v.safeParse(${schemaName}, invalidData);
|
|
2827
|
+
// expect(result.issues?.[0]?.message).toContain('expected message');
|
|
2828
|
+
});
|
|
2829
|
+
});`;
|
|
2830
|
+
}
|
|
2831
|
+
return ` describe('${schemaName}', () => {
|
|
2832
|
+
it('should accept valid data', () => {
|
|
2833
|
+
// TODO(schemashift): Add valid test data for ${schemaName}
|
|
2834
|
+
// const result = ${schemaName}.safeParse(validData);
|
|
2835
|
+
// expect(result.success).toBe(true);
|
|
2836
|
+
});
|
|
2837
|
+
|
|
2838
|
+
it('should reject invalid data', () => {
|
|
2839
|
+
// TODO(schemashift): Add invalid test data for ${schemaName}
|
|
2840
|
+
// const result = ${schemaName}.safeParse(invalidData);
|
|
2841
|
+
// expect(result.success).toBe(false);
|
|
2842
|
+
});
|
|
2843
|
+
|
|
2844
|
+
it('should preserve error messages', () => {
|
|
2845
|
+
// TODO(schemashift): Verify custom error messages are preserved
|
|
2846
|
+
// const result = ${schemaName}.safeParse(invalidData);
|
|
2847
|
+
// expect(result.error?.issues[0]?.message).toContain('expected message');
|
|
2848
|
+
});
|
|
2849
|
+
});`;
|
|
2850
|
+
}
|
|
2851
|
+
};
|
|
2852
|
+
|
|
1478
2853
|
// src/transform.ts
|
|
1479
2854
|
var TransformEngine = class {
|
|
1480
2855
|
handlers = /* @__PURE__ */ new Map();
|
|
@@ -1489,9 +2864,10 @@ var TransformEngine = class {
|
|
|
1489
2864
|
}
|
|
1490
2865
|
getSupportedPaths() {
|
|
1491
2866
|
return Array.from(this.handlers.keys()).map((key) => {
|
|
1492
|
-
const
|
|
1493
|
-
|
|
1494
|
-
|
|
2867
|
+
const parts = key.split("->");
|
|
2868
|
+
if (parts.length !== 2) return null;
|
|
2869
|
+
return { from: parts[0], to: parts[1] };
|
|
2870
|
+
}).filter((entry) => entry !== null);
|
|
1495
2871
|
}
|
|
1496
2872
|
transform(sourceFile, from, to, options) {
|
|
1497
2873
|
const handler = this.getHandler(from, to);
|
|
@@ -1507,20 +2883,157 @@ var TransformEngine = class {
|
|
|
1507
2883
|
return handler.transform(sourceFile, options);
|
|
1508
2884
|
}
|
|
1509
2885
|
};
|
|
2886
|
+
|
|
2887
|
+
// src/type-dedup-detector.ts
|
|
2888
|
+
var import_ts_morph4 = require("ts-morph");
|
|
2889
|
+
var TypeDedupDetector = class {
|
|
2890
|
+
detect(sourceFiles) {
|
|
2891
|
+
const typeDefinitions = this.collectTypeDefinitions(sourceFiles);
|
|
2892
|
+
const schemaDefinitions = this.collectSchemaDefinitions(sourceFiles);
|
|
2893
|
+
const candidates = this.findMatches(typeDefinitions, schemaDefinitions);
|
|
2894
|
+
const summary = candidates.length > 0 ? `Found ${candidates.length} type definition(s) that may duplicate schema shapes. After migration, replace with z.infer<typeof schema>.` : "No duplicate type definitions detected.";
|
|
2895
|
+
return { candidates, summary };
|
|
2896
|
+
}
|
|
2897
|
+
collectTypeDefinitions(sourceFiles) {
|
|
2898
|
+
const types = [];
|
|
2899
|
+
for (const file of sourceFiles) {
|
|
2900
|
+
const filePath = file.getFilePath();
|
|
2901
|
+
for (const iface of file.getInterfaces()) {
|
|
2902
|
+
const fields = iface.getProperties().map((p) => p.getName());
|
|
2903
|
+
if (fields.length > 0) {
|
|
2904
|
+
types.push({
|
|
2905
|
+
name: iface.getName(),
|
|
2906
|
+
fields,
|
|
2907
|
+
filePath,
|
|
2908
|
+
lineNumber: iface.getStartLineNumber()
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
for (const typeAlias of file.getTypeAliases()) {
|
|
2913
|
+
const typeNode = typeAlias.getTypeNode();
|
|
2914
|
+
if (!typeNode) continue;
|
|
2915
|
+
if (import_ts_morph4.Node.isTypeLiteral(typeNode)) {
|
|
2916
|
+
const fields = typeNode.getProperties().map((p) => p.getName());
|
|
2917
|
+
if (fields.length > 0) {
|
|
2918
|
+
types.push({
|
|
2919
|
+
name: typeAlias.getName(),
|
|
2920
|
+
fields,
|
|
2921
|
+
filePath,
|
|
2922
|
+
lineNumber: typeAlias.getStartLineNumber()
|
|
2923
|
+
});
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
return types;
|
|
2929
|
+
}
|
|
2930
|
+
collectSchemaDefinitions(sourceFiles) {
|
|
2931
|
+
const schemas = [];
|
|
2932
|
+
for (const file of sourceFiles) {
|
|
2933
|
+
const filePath = file.getFilePath();
|
|
2934
|
+
for (const varDecl of file.getVariableDeclarations()) {
|
|
2935
|
+
const initializer = varDecl.getInitializer();
|
|
2936
|
+
if (!initializer) continue;
|
|
2937
|
+
const text = initializer.getText();
|
|
2938
|
+
const isSchema = /(?:z|zod|yup|Yup|Joi|joi|t|v|valibot)\.object\s*\(/.test(text) || /Joi\.object\s*\(/.test(text);
|
|
2939
|
+
if (!isSchema) continue;
|
|
2940
|
+
const fields = this.extractSchemaFields(text);
|
|
2941
|
+
if (fields.length > 0) {
|
|
2942
|
+
schemas.push({
|
|
2943
|
+
name: varDecl.getName(),
|
|
2944
|
+
fields,
|
|
2945
|
+
filePath,
|
|
2946
|
+
lineNumber: varDecl.getStartLineNumber()
|
|
2947
|
+
});
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
return schemas;
|
|
2952
|
+
}
|
|
2953
|
+
extractSchemaFields(text) {
|
|
2954
|
+
const fields = [];
|
|
2955
|
+
const fieldPattern = /\b(\w+)\s*:\s*(?:z|zod|yup|Yup|Joi|joi|t|v|valibot)\./g;
|
|
2956
|
+
for (const match of text.matchAll(fieldPattern)) {
|
|
2957
|
+
if (match[1]) {
|
|
2958
|
+
fields.push(match[1]);
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
return fields;
|
|
2962
|
+
}
|
|
2963
|
+
findMatches(types, schemas) {
|
|
2964
|
+
const candidates = [];
|
|
2965
|
+
for (const typeDef of types) {
|
|
2966
|
+
for (const schemaDef of schemas) {
|
|
2967
|
+
const matchedFields = this.getMatchedFields(typeDef.fields, schemaDef.fields);
|
|
2968
|
+
if (matchedFields.length < 2) continue;
|
|
2969
|
+
const typeFieldCount = typeDef.fields.length;
|
|
2970
|
+
const schemaFieldCount = schemaDef.fields.length;
|
|
2971
|
+
const matchRatio = matchedFields.length / Math.max(typeFieldCount, schemaFieldCount);
|
|
2972
|
+
let confidence;
|
|
2973
|
+
if (matchRatio >= 0.8) {
|
|
2974
|
+
confidence = "high";
|
|
2975
|
+
} else if (matchRatio >= 0.5) {
|
|
2976
|
+
confidence = "medium";
|
|
2977
|
+
} else {
|
|
2978
|
+
confidence = "low";
|
|
2979
|
+
}
|
|
2980
|
+
if (confidence === "low" && !this.namesRelated(typeDef.name, schemaDef.name)) {
|
|
2981
|
+
continue;
|
|
2982
|
+
}
|
|
2983
|
+
candidates.push({
|
|
2984
|
+
typeName: typeDef.name,
|
|
2985
|
+
typeFilePath: typeDef.filePath,
|
|
2986
|
+
typeLineNumber: typeDef.lineNumber,
|
|
2987
|
+
schemaName: schemaDef.name,
|
|
2988
|
+
schemaFilePath: schemaDef.filePath,
|
|
2989
|
+
schemaLineNumber: schemaDef.lineNumber,
|
|
2990
|
+
matchedFields,
|
|
2991
|
+
confidence,
|
|
2992
|
+
suggestion: `Replace "type/interface ${typeDef.name}" with "type ${typeDef.name} = z.infer<typeof ${schemaDef.name}>" (${matchedFields.length}/${typeFieldCount} fields match).`
|
|
2993
|
+
});
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
candidates.sort((a, b) => {
|
|
2997
|
+
const confidenceOrder = { high: 0, medium: 1, low: 2 };
|
|
2998
|
+
const diff = confidenceOrder[a.confidence] - confidenceOrder[b.confidence];
|
|
2999
|
+
if (diff !== 0) return diff;
|
|
3000
|
+
return b.matchedFields.length - a.matchedFields.length;
|
|
3001
|
+
});
|
|
3002
|
+
return candidates;
|
|
3003
|
+
}
|
|
3004
|
+
getMatchedFields(typeFields, schemaFields) {
|
|
3005
|
+
const schemaSet = new Set(schemaFields);
|
|
3006
|
+
return typeFields.filter((f) => schemaSet.has(f));
|
|
3007
|
+
}
|
|
3008
|
+
namesRelated(typeName, schemaName) {
|
|
3009
|
+
const normalize = (name) => name.toLowerCase().replace(/schema|type|interface|i$/gi, "");
|
|
3010
|
+
return normalize(typeName) === normalize(schemaName);
|
|
3011
|
+
}
|
|
3012
|
+
};
|
|
1510
3013
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1511
3014
|
0 && (module.exports = {
|
|
3015
|
+
BehavioralWarningAnalyzer,
|
|
3016
|
+
BundleEstimator,
|
|
1512
3017
|
CompatibilityAnalyzer,
|
|
3018
|
+
ComplexityEstimator,
|
|
1513
3019
|
DetailedAnalyzer,
|
|
1514
3020
|
EcosystemAnalyzer,
|
|
3021
|
+
FormResolverMigrator,
|
|
1515
3022
|
GovernanceEngine,
|
|
1516
3023
|
IncrementalTracker,
|
|
3024
|
+
MigrationAuditLog,
|
|
1517
3025
|
MigrationChain,
|
|
3026
|
+
MonorepoResolver,
|
|
1518
3027
|
PackageUpdater,
|
|
3028
|
+
PerformanceAnalyzer,
|
|
1519
3029
|
PluginLoader,
|
|
1520
3030
|
SchemaAnalyzer,
|
|
1521
3031
|
SchemaDependencyResolver,
|
|
3032
|
+
TestScaffolder,
|
|
1522
3033
|
TransformEngine,
|
|
3034
|
+
TypeDedupDetector,
|
|
1523
3035
|
buildCallChain,
|
|
3036
|
+
computeParallelBatches,
|
|
1524
3037
|
detectFormLibraries,
|
|
1525
3038
|
detectSchemaLibrary,
|
|
1526
3039
|
detectStandardSchema,
|