@valbuild/cli 0.97.0 → 0.97.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/dist/valbuild-cli-cli.cjs.dev.js +30 -222
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +30 -222
- package/cli/dist/valbuild-cli-cli.esm.js +31 -223
- package/package.json +4 -4
- package/src/runValidation.ts +37 -306
|
@@ -126,8 +126,6 @@ const textEncoder = new TextEncoder();
|
|
|
126
126
|
|
|
127
127
|
// Types for handler system
|
|
128
128
|
|
|
129
|
-
// Cache types for avoiding redundant service.get() calls
|
|
130
|
-
|
|
131
129
|
// Handler functions
|
|
132
130
|
async function handleFileMetadata(ctx) {
|
|
133
131
|
var _fileSource$source;
|
|
@@ -160,40 +158,6 @@ async function handleFileMetadata(ctx) {
|
|
|
160
158
|
shouldApplyPatch: true
|
|
161
159
|
};
|
|
162
160
|
}
|
|
163
|
-
async function handleKeyOfCheck(ctx) {
|
|
164
|
-
if (!ctx.validationError.value || typeof ctx.validationError.value !== "object" || !("key" in ctx.validationError.value) || !("sourcePath" in ctx.validationError.value)) {
|
|
165
|
-
return {
|
|
166
|
-
success: false,
|
|
167
|
-
errorMessage: `Unexpected error in ${ctx.sourcePath}: ${ctx.validationError.message} (Expected value to be an object with 'key' and 'sourcePath' properties - this is likely a bug in Val)`
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
const {
|
|
171
|
-
key,
|
|
172
|
-
sourcePath
|
|
173
|
-
} = ctx.validationError.value;
|
|
174
|
-
if (typeof key !== "string") {
|
|
175
|
-
return {
|
|
176
|
-
success: false,
|
|
177
|
-
errorMessage: `Unexpected error in ${sourcePath}: ${ctx.validationError.message} (Expected value property 'key' to be a string - this is likely a bug in Val)`
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
if (typeof sourcePath !== "string") {
|
|
181
|
-
return {
|
|
182
|
-
success: false,
|
|
183
|
-
errorMessage: `Unexpected error in ${sourcePath}: ${ctx.validationError.message} (Expected value property 'sourcePath' to be a string - this is likely a bug in Val)`
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
const res = await checkKeyIsValid(key, sourcePath, ctx.service, ctx.keyOfCache);
|
|
187
|
-
if (res.error) {
|
|
188
|
-
return {
|
|
189
|
-
success: false,
|
|
190
|
-
errorMessage: res.message
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
return {
|
|
194
|
-
success: true
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
161
|
async function handleRemoteFileUpload(ctx) {
|
|
198
162
|
var _resolvedRemoteFileAt;
|
|
199
163
|
if (!ctx.fix) {
|
|
@@ -402,155 +366,6 @@ async function handleRemoteFileCheck() {
|
|
|
402
366
|
shouldApplyPatch: true
|
|
403
367
|
};
|
|
404
368
|
}
|
|
405
|
-
|
|
406
|
-
// Helper function
|
|
407
|
-
async function checkKeyIsValid(key, sourcePath, service, cache) {
|
|
408
|
-
const [moduleFilePath, modulePath] = core.Internal.splitModuleFilePathAndModulePath(sourcePath);
|
|
409
|
-
const cacheKey = `${moduleFilePath}::${modulePath}`;
|
|
410
|
-
let keyOfModuleSource;
|
|
411
|
-
let keyOfModuleSchema;
|
|
412
|
-
const cached = cache.get(cacheKey);
|
|
413
|
-
if (cached) {
|
|
414
|
-
keyOfModuleSource = cached.source;
|
|
415
|
-
keyOfModuleSchema = cached.schema;
|
|
416
|
-
} else {
|
|
417
|
-
const keyOfModule = await service.get(moduleFilePath, modulePath, {
|
|
418
|
-
source: true,
|
|
419
|
-
schema: true,
|
|
420
|
-
validate: false
|
|
421
|
-
});
|
|
422
|
-
keyOfModuleSource = keyOfModule.source;
|
|
423
|
-
keyOfModuleSchema = keyOfModule.schema;
|
|
424
|
-
cache.set(cacheKey, {
|
|
425
|
-
source: keyOfModuleSource,
|
|
426
|
-
schema: keyOfModuleSchema
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
if (keyOfModuleSchema && keyOfModuleSchema.type !== "record") {
|
|
430
|
-
return {
|
|
431
|
-
error: true,
|
|
432
|
-
message: `Expected key at ${sourcePath} to be of type 'record'`
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
if (keyOfModuleSource && typeof keyOfModuleSource === "object" && key in keyOfModuleSource) {
|
|
436
|
-
return {
|
|
437
|
-
error: false
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
if (!keyOfModuleSource || typeof keyOfModuleSource !== "object") {
|
|
441
|
-
return {
|
|
442
|
-
error: true,
|
|
443
|
-
message: `Expected ${sourcePath} to be a truthy object`
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
const alternatives = findSimilar(key, Object.keys(keyOfModuleSource));
|
|
447
|
-
return {
|
|
448
|
-
error: true,
|
|
449
|
-
message: `Key '${key}' does not exist in ${sourcePath}. Closest match: '${alternatives[0].target}'. Other similar: ${alternatives.slice(1, 4).map(a => `'${a.target}'`).join(", ")}${alternatives.length > 4 ? ", ..." : ""}`
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Check if a route is valid by scanning all router modules
|
|
455
|
-
* and validating against include/exclude patterns
|
|
456
|
-
*/
|
|
457
|
-
async function checkRouteIsValid(route, include, exclude, service, valFiles, cache) {
|
|
458
|
-
// 1. Scan all val files to find modules with routers (use cache if available)
|
|
459
|
-
if (!cache.loaded) {
|
|
460
|
-
for (const file of valFiles) {
|
|
461
|
-
var _valModule$schema;
|
|
462
|
-
const moduleFilePath = `/${file}`;
|
|
463
|
-
const valModule = await service.get(moduleFilePath, "", {
|
|
464
|
-
source: true,
|
|
465
|
-
schema: true,
|
|
466
|
-
validate: false
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
// Check if this module has a router defined
|
|
470
|
-
if (((_valModule$schema = valModule.schema) === null || _valModule$schema === void 0 ? void 0 : _valModule$schema.type) === "record" && valModule.schema.router) {
|
|
471
|
-
if (valModule.source && typeof valModule.source === "object") {
|
|
472
|
-
cache.modules[moduleFilePath] = valModule.source;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
cache.loaded = true;
|
|
477
|
-
}
|
|
478
|
-
const routerModules = cache.modules;
|
|
479
|
-
|
|
480
|
-
// 2. Check if route exists in any router module
|
|
481
|
-
let foundInModule = null;
|
|
482
|
-
for (const [moduleFilePath, source] of Object.entries(routerModules)) {
|
|
483
|
-
if (route in source) {
|
|
484
|
-
foundInModule = moduleFilePath;
|
|
485
|
-
break;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
if (!foundInModule) {
|
|
489
|
-
// Route not found in any router module
|
|
490
|
-
let allRoutes = Object.values(routerModules).flatMap(source => Object.keys(source));
|
|
491
|
-
if (allRoutes.length === 0) {
|
|
492
|
-
return {
|
|
493
|
-
error: true,
|
|
494
|
-
message: `Route '${route}' could not be validated: No router modules found in the project. Use s.record(...).router(...) to define router modules.`
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Filter routes by include/exclude patterns for suggestions
|
|
499
|
-
allRoutes = internal.filterRoutesByPatterns(allRoutes, include, exclude);
|
|
500
|
-
const alternatives = findSimilar(route, allRoutes);
|
|
501
|
-
return {
|
|
502
|
-
error: true,
|
|
503
|
-
message: `Route '${route}' does not exist in any router module. ${alternatives.length > 0 ? `Closest match: '${alternatives[0].target}'. Other similar: ${alternatives.slice(1, 4).map(a => `'${a.target}'`).join(", ")}${alternatives.length > 4 ? ", ..." : ""}` : "No similar routes found."}`
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// 3. Validate against include/exclude patterns
|
|
508
|
-
const patternValidation = internal.validateRoutePatterns(route, include, exclude);
|
|
509
|
-
if (!patternValidation.valid) {
|
|
510
|
-
return {
|
|
511
|
-
error: true,
|
|
512
|
-
message: patternValidation.message
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
return {
|
|
516
|
-
error: false
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* Handler for router:check-route validation fix
|
|
522
|
-
*/
|
|
523
|
-
async function handleRouteCheck(ctx) {
|
|
524
|
-
const {
|
|
525
|
-
sourcePath,
|
|
526
|
-
validationError,
|
|
527
|
-
service,
|
|
528
|
-
valFiles,
|
|
529
|
-
routerModulesCache
|
|
530
|
-
} = ctx;
|
|
531
|
-
|
|
532
|
-
// Extract route and patterns from validation error value
|
|
533
|
-
const value = validationError.value;
|
|
534
|
-
if (!value || typeof value.route !== "string") {
|
|
535
|
-
return {
|
|
536
|
-
success: false,
|
|
537
|
-
errorMessage: `Invalid route value in validation error: ${JSON.stringify(value)}`
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
const route = value.route;
|
|
541
|
-
|
|
542
|
-
// Check if the route is valid
|
|
543
|
-
const result = await checkRouteIsValid(route, value.include, value.exclude, service, valFiles, routerModulesCache);
|
|
544
|
-
if (result.error) {
|
|
545
|
-
return {
|
|
546
|
-
success: false,
|
|
547
|
-
errorMessage: `${sourcePath}: ${result.message}`
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
return {
|
|
551
|
-
success: true
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
369
|
async function handleUniqueFolderCheck(ctx) {
|
|
555
370
|
const value = ctx.validationError.value;
|
|
556
371
|
if (!value || typeof value.directory !== "string") {
|
|
@@ -649,14 +464,14 @@ async function handleCheckAllFiles(ctx) {
|
|
|
649
464
|
};
|
|
650
465
|
}
|
|
651
466
|
|
|
652
|
-
// Fix handler registry
|
|
467
|
+
// Fix handler registry. `keyof:check-keys` and `router:check-route` are
|
|
468
|
+
// resolved upfront by the shared resolveSchemaSourceFixes — they never reach
|
|
469
|
+
// this registry, so they're excluded from the key set.
|
|
653
470
|
const currentFixHandlers = {
|
|
654
471
|
"image:check-metadata": handleFileMetadata,
|
|
655
472
|
"image:add-metadata": handleFileMetadata,
|
|
656
473
|
"file:check-metadata": handleFileMetadata,
|
|
657
474
|
"file:add-metadata": handleFileMetadata,
|
|
658
|
-
"keyof:check-keys": handleKeyOfCheck,
|
|
659
|
-
"router:check-route": handleRouteCheck,
|
|
660
475
|
"image:upload-remote": handleRemoteFileUpload,
|
|
661
476
|
"file:upload-remote": handleRemoteFileUpload,
|
|
662
477
|
"image:download-remote": handleRemoteFileDownload,
|
|
@@ -708,12 +523,27 @@ async function* runValidation({
|
|
|
708
523
|
const service = await server.createService(projectRoot, {}, fs);
|
|
709
524
|
let errors = 0;
|
|
710
525
|
|
|
711
|
-
//
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
526
|
+
// Build a single schema/source snapshot up front so the shared resolver
|
|
527
|
+
// can resolve keyof:check-keys / router:check-route references that span
|
|
528
|
+
// multiple val files.
|
|
529
|
+
const snapshot = {
|
|
530
|
+
schemas: {},
|
|
531
|
+
sources: {}
|
|
716
532
|
};
|
|
533
|
+
for (const file of valFiles) {
|
|
534
|
+
const moduleFilePath = `/${file}`;
|
|
535
|
+
const valModule = await service.get(moduleFilePath, "", {
|
|
536
|
+
source: true,
|
|
537
|
+
schema: true,
|
|
538
|
+
validate: false
|
|
539
|
+
});
|
|
540
|
+
if (valModule.schema) {
|
|
541
|
+
snapshot.schemas[moduleFilePath] = valModule.schema;
|
|
542
|
+
}
|
|
543
|
+
if (valModule.source !== undefined) {
|
|
544
|
+
snapshot.sources[moduleFilePath] = valModule.source;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
717
547
|
async function* validateFile(file) {
|
|
718
548
|
const moduleFilePath = `/${file}`; // TODO: check if this always works? (Windows?)
|
|
719
549
|
const start = Date.now();
|
|
@@ -737,7 +567,12 @@ async function* runValidation({
|
|
|
737
567
|
let fixedErrors = 0;
|
|
738
568
|
if (valModule.errors) {
|
|
739
569
|
if (valModule.errors.validation) {
|
|
740
|
-
|
|
570
|
+
// Resolve schema/source fixes (keyof:check-keys, router:check-route)
|
|
571
|
+
// against the snapshot before per-error dispatch. Resolved errors
|
|
572
|
+
// are dropped; invalid references come back with rewritten messages
|
|
573
|
+
// and fixes cleared, so they fall through the "no fixes" branch.
|
|
574
|
+
const resolvedValidationErrors = internal.resolveSchemaSourceFixes(valModule.errors.validation, snapshot);
|
|
575
|
+
for (const [sourcePath, validationErrors] of Object.entries(resolvedValidationErrors)) {
|
|
741
576
|
for (const v of validationErrors) {
|
|
742
577
|
if (!v.fixes || v.fixes.length === 0) {
|
|
743
578
|
// No fixes available - just report error
|
|
@@ -780,9 +615,7 @@ async function* runValidation({
|
|
|
780
615
|
remoteFileBuckets,
|
|
781
616
|
remoteFilesCounter,
|
|
782
617
|
remote,
|
|
783
|
-
project
|
|
784
|
-
keyOfCache,
|
|
785
|
-
routerModulesCache
|
|
618
|
+
project
|
|
786
619
|
});
|
|
787
620
|
|
|
788
621
|
// Yield any events from handler
|
|
@@ -894,31 +727,6 @@ async function* runValidation({
|
|
|
894
727
|
}
|
|
895
728
|
}
|
|
896
729
|
|
|
897
|
-
// GPT generated levenshtein distance algorithm:
|
|
898
|
-
const levenshtein = (a, b) => {
|
|
899
|
-
const [m, n] = [a.length, b.length];
|
|
900
|
-
if (!m || !n) return Math.max(m, n);
|
|
901
|
-
const dp = Array.from({
|
|
902
|
-
length: m + 1
|
|
903
|
-
}, (_, i) => i);
|
|
904
|
-
for (let j = 1; j <= n; j++) {
|
|
905
|
-
let prev = dp[0];
|
|
906
|
-
dp[0] = j;
|
|
907
|
-
for (let i = 1; i <= m; i++) {
|
|
908
|
-
const temp = dp[i];
|
|
909
|
-
dp[i] = a[i - 1] === b[j - 1] ? prev : Math.min(prev + 1, dp[i - 1] + 1, dp[i] + 1);
|
|
910
|
-
prev = temp;
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
return dp[m];
|
|
914
|
-
};
|
|
915
|
-
function findSimilar(key, targets) {
|
|
916
|
-
return targets.map(target => ({
|
|
917
|
-
target,
|
|
918
|
-
distance: levenshtein(key, target)
|
|
919
|
-
})).sort((a, b) => a.distance - b.distance);
|
|
920
|
-
}
|
|
921
|
-
|
|
922
730
|
async function validate({
|
|
923
731
|
root,
|
|
924
732
|
fix
|
|
@@ -126,8 +126,6 @@ const textEncoder = new TextEncoder();
|
|
|
126
126
|
|
|
127
127
|
// Types for handler system
|
|
128
128
|
|
|
129
|
-
// Cache types for avoiding redundant service.get() calls
|
|
130
|
-
|
|
131
129
|
// Handler functions
|
|
132
130
|
async function handleFileMetadata(ctx) {
|
|
133
131
|
var _fileSource$source;
|
|
@@ -160,40 +158,6 @@ async function handleFileMetadata(ctx) {
|
|
|
160
158
|
shouldApplyPatch: true
|
|
161
159
|
};
|
|
162
160
|
}
|
|
163
|
-
async function handleKeyOfCheck(ctx) {
|
|
164
|
-
if (!ctx.validationError.value || typeof ctx.validationError.value !== "object" || !("key" in ctx.validationError.value) || !("sourcePath" in ctx.validationError.value)) {
|
|
165
|
-
return {
|
|
166
|
-
success: false,
|
|
167
|
-
errorMessage: `Unexpected error in ${ctx.sourcePath}: ${ctx.validationError.message} (Expected value to be an object with 'key' and 'sourcePath' properties - this is likely a bug in Val)`
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
const {
|
|
171
|
-
key,
|
|
172
|
-
sourcePath
|
|
173
|
-
} = ctx.validationError.value;
|
|
174
|
-
if (typeof key !== "string") {
|
|
175
|
-
return {
|
|
176
|
-
success: false,
|
|
177
|
-
errorMessage: `Unexpected error in ${sourcePath}: ${ctx.validationError.message} (Expected value property 'key' to be a string - this is likely a bug in Val)`
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
if (typeof sourcePath !== "string") {
|
|
181
|
-
return {
|
|
182
|
-
success: false,
|
|
183
|
-
errorMessage: `Unexpected error in ${sourcePath}: ${ctx.validationError.message} (Expected value property 'sourcePath' to be a string - this is likely a bug in Val)`
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
const res = await checkKeyIsValid(key, sourcePath, ctx.service, ctx.keyOfCache);
|
|
187
|
-
if (res.error) {
|
|
188
|
-
return {
|
|
189
|
-
success: false,
|
|
190
|
-
errorMessage: res.message
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
return {
|
|
194
|
-
success: true
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
161
|
async function handleRemoteFileUpload(ctx) {
|
|
198
162
|
var _resolvedRemoteFileAt;
|
|
199
163
|
if (!ctx.fix) {
|
|
@@ -402,155 +366,6 @@ async function handleRemoteFileCheck() {
|
|
|
402
366
|
shouldApplyPatch: true
|
|
403
367
|
};
|
|
404
368
|
}
|
|
405
|
-
|
|
406
|
-
// Helper function
|
|
407
|
-
async function checkKeyIsValid(key, sourcePath, service, cache) {
|
|
408
|
-
const [moduleFilePath, modulePath] = core.Internal.splitModuleFilePathAndModulePath(sourcePath);
|
|
409
|
-
const cacheKey = `${moduleFilePath}::${modulePath}`;
|
|
410
|
-
let keyOfModuleSource;
|
|
411
|
-
let keyOfModuleSchema;
|
|
412
|
-
const cached = cache.get(cacheKey);
|
|
413
|
-
if (cached) {
|
|
414
|
-
keyOfModuleSource = cached.source;
|
|
415
|
-
keyOfModuleSchema = cached.schema;
|
|
416
|
-
} else {
|
|
417
|
-
const keyOfModule = await service.get(moduleFilePath, modulePath, {
|
|
418
|
-
source: true,
|
|
419
|
-
schema: true,
|
|
420
|
-
validate: false
|
|
421
|
-
});
|
|
422
|
-
keyOfModuleSource = keyOfModule.source;
|
|
423
|
-
keyOfModuleSchema = keyOfModule.schema;
|
|
424
|
-
cache.set(cacheKey, {
|
|
425
|
-
source: keyOfModuleSource,
|
|
426
|
-
schema: keyOfModuleSchema
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
if (keyOfModuleSchema && keyOfModuleSchema.type !== "record") {
|
|
430
|
-
return {
|
|
431
|
-
error: true,
|
|
432
|
-
message: `Expected key at ${sourcePath} to be of type 'record'`
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
if (keyOfModuleSource && typeof keyOfModuleSource === "object" && key in keyOfModuleSource) {
|
|
436
|
-
return {
|
|
437
|
-
error: false
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
if (!keyOfModuleSource || typeof keyOfModuleSource !== "object") {
|
|
441
|
-
return {
|
|
442
|
-
error: true,
|
|
443
|
-
message: `Expected ${sourcePath} to be a truthy object`
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
const alternatives = findSimilar(key, Object.keys(keyOfModuleSource));
|
|
447
|
-
return {
|
|
448
|
-
error: true,
|
|
449
|
-
message: `Key '${key}' does not exist in ${sourcePath}. Closest match: '${alternatives[0].target}'. Other similar: ${alternatives.slice(1, 4).map(a => `'${a.target}'`).join(", ")}${alternatives.length > 4 ? ", ..." : ""}`
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Check if a route is valid by scanning all router modules
|
|
455
|
-
* and validating against include/exclude patterns
|
|
456
|
-
*/
|
|
457
|
-
async function checkRouteIsValid(route, include, exclude, service, valFiles, cache) {
|
|
458
|
-
// 1. Scan all val files to find modules with routers (use cache if available)
|
|
459
|
-
if (!cache.loaded) {
|
|
460
|
-
for (const file of valFiles) {
|
|
461
|
-
var _valModule$schema;
|
|
462
|
-
const moduleFilePath = `/${file}`;
|
|
463
|
-
const valModule = await service.get(moduleFilePath, "", {
|
|
464
|
-
source: true,
|
|
465
|
-
schema: true,
|
|
466
|
-
validate: false
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
// Check if this module has a router defined
|
|
470
|
-
if (((_valModule$schema = valModule.schema) === null || _valModule$schema === void 0 ? void 0 : _valModule$schema.type) === "record" && valModule.schema.router) {
|
|
471
|
-
if (valModule.source && typeof valModule.source === "object") {
|
|
472
|
-
cache.modules[moduleFilePath] = valModule.source;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
cache.loaded = true;
|
|
477
|
-
}
|
|
478
|
-
const routerModules = cache.modules;
|
|
479
|
-
|
|
480
|
-
// 2. Check if route exists in any router module
|
|
481
|
-
let foundInModule = null;
|
|
482
|
-
for (const [moduleFilePath, source] of Object.entries(routerModules)) {
|
|
483
|
-
if (route in source) {
|
|
484
|
-
foundInModule = moduleFilePath;
|
|
485
|
-
break;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
if (!foundInModule) {
|
|
489
|
-
// Route not found in any router module
|
|
490
|
-
let allRoutes = Object.values(routerModules).flatMap(source => Object.keys(source));
|
|
491
|
-
if (allRoutes.length === 0) {
|
|
492
|
-
return {
|
|
493
|
-
error: true,
|
|
494
|
-
message: `Route '${route}' could not be validated: No router modules found in the project. Use s.record(...).router(...) to define router modules.`
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Filter routes by include/exclude patterns for suggestions
|
|
499
|
-
allRoutes = internal.filterRoutesByPatterns(allRoutes, include, exclude);
|
|
500
|
-
const alternatives = findSimilar(route, allRoutes);
|
|
501
|
-
return {
|
|
502
|
-
error: true,
|
|
503
|
-
message: `Route '${route}' does not exist in any router module. ${alternatives.length > 0 ? `Closest match: '${alternatives[0].target}'. Other similar: ${alternatives.slice(1, 4).map(a => `'${a.target}'`).join(", ")}${alternatives.length > 4 ? ", ..." : ""}` : "No similar routes found."}`
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// 3. Validate against include/exclude patterns
|
|
508
|
-
const patternValidation = internal.validateRoutePatterns(route, include, exclude);
|
|
509
|
-
if (!patternValidation.valid) {
|
|
510
|
-
return {
|
|
511
|
-
error: true,
|
|
512
|
-
message: patternValidation.message
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
return {
|
|
516
|
-
error: false
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* Handler for router:check-route validation fix
|
|
522
|
-
*/
|
|
523
|
-
async function handleRouteCheck(ctx) {
|
|
524
|
-
const {
|
|
525
|
-
sourcePath,
|
|
526
|
-
validationError,
|
|
527
|
-
service,
|
|
528
|
-
valFiles,
|
|
529
|
-
routerModulesCache
|
|
530
|
-
} = ctx;
|
|
531
|
-
|
|
532
|
-
// Extract route and patterns from validation error value
|
|
533
|
-
const value = validationError.value;
|
|
534
|
-
if (!value || typeof value.route !== "string") {
|
|
535
|
-
return {
|
|
536
|
-
success: false,
|
|
537
|
-
errorMessage: `Invalid route value in validation error: ${JSON.stringify(value)}`
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
const route = value.route;
|
|
541
|
-
|
|
542
|
-
// Check if the route is valid
|
|
543
|
-
const result = await checkRouteIsValid(route, value.include, value.exclude, service, valFiles, routerModulesCache);
|
|
544
|
-
if (result.error) {
|
|
545
|
-
return {
|
|
546
|
-
success: false,
|
|
547
|
-
errorMessage: `${sourcePath}: ${result.message}`
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
return {
|
|
551
|
-
success: true
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
369
|
async function handleUniqueFolderCheck(ctx) {
|
|
555
370
|
const value = ctx.validationError.value;
|
|
556
371
|
if (!value || typeof value.directory !== "string") {
|
|
@@ -649,14 +464,14 @@ async function handleCheckAllFiles(ctx) {
|
|
|
649
464
|
};
|
|
650
465
|
}
|
|
651
466
|
|
|
652
|
-
// Fix handler registry
|
|
467
|
+
// Fix handler registry. `keyof:check-keys` and `router:check-route` are
|
|
468
|
+
// resolved upfront by the shared resolveSchemaSourceFixes — they never reach
|
|
469
|
+
// this registry, so they're excluded from the key set.
|
|
653
470
|
const currentFixHandlers = {
|
|
654
471
|
"image:check-metadata": handleFileMetadata,
|
|
655
472
|
"image:add-metadata": handleFileMetadata,
|
|
656
473
|
"file:check-metadata": handleFileMetadata,
|
|
657
474
|
"file:add-metadata": handleFileMetadata,
|
|
658
|
-
"keyof:check-keys": handleKeyOfCheck,
|
|
659
|
-
"router:check-route": handleRouteCheck,
|
|
660
475
|
"image:upload-remote": handleRemoteFileUpload,
|
|
661
476
|
"file:upload-remote": handleRemoteFileUpload,
|
|
662
477
|
"image:download-remote": handleRemoteFileDownload,
|
|
@@ -708,12 +523,27 @@ async function* runValidation({
|
|
|
708
523
|
const service = await server.createService(projectRoot, {}, fs);
|
|
709
524
|
let errors = 0;
|
|
710
525
|
|
|
711
|
-
//
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
526
|
+
// Build a single schema/source snapshot up front so the shared resolver
|
|
527
|
+
// can resolve keyof:check-keys / router:check-route references that span
|
|
528
|
+
// multiple val files.
|
|
529
|
+
const snapshot = {
|
|
530
|
+
schemas: {},
|
|
531
|
+
sources: {}
|
|
716
532
|
};
|
|
533
|
+
for (const file of valFiles) {
|
|
534
|
+
const moduleFilePath = `/${file}`;
|
|
535
|
+
const valModule = await service.get(moduleFilePath, "", {
|
|
536
|
+
source: true,
|
|
537
|
+
schema: true,
|
|
538
|
+
validate: false
|
|
539
|
+
});
|
|
540
|
+
if (valModule.schema) {
|
|
541
|
+
snapshot.schemas[moduleFilePath] = valModule.schema;
|
|
542
|
+
}
|
|
543
|
+
if (valModule.source !== undefined) {
|
|
544
|
+
snapshot.sources[moduleFilePath] = valModule.source;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
717
547
|
async function* validateFile(file) {
|
|
718
548
|
const moduleFilePath = `/${file}`; // TODO: check if this always works? (Windows?)
|
|
719
549
|
const start = Date.now();
|
|
@@ -737,7 +567,12 @@ async function* runValidation({
|
|
|
737
567
|
let fixedErrors = 0;
|
|
738
568
|
if (valModule.errors) {
|
|
739
569
|
if (valModule.errors.validation) {
|
|
740
|
-
|
|
570
|
+
// Resolve schema/source fixes (keyof:check-keys, router:check-route)
|
|
571
|
+
// against the snapshot before per-error dispatch. Resolved errors
|
|
572
|
+
// are dropped; invalid references come back with rewritten messages
|
|
573
|
+
// and fixes cleared, so they fall through the "no fixes" branch.
|
|
574
|
+
const resolvedValidationErrors = internal.resolveSchemaSourceFixes(valModule.errors.validation, snapshot);
|
|
575
|
+
for (const [sourcePath, validationErrors] of Object.entries(resolvedValidationErrors)) {
|
|
741
576
|
for (const v of validationErrors) {
|
|
742
577
|
if (!v.fixes || v.fixes.length === 0) {
|
|
743
578
|
// No fixes available - just report error
|
|
@@ -780,9 +615,7 @@ async function* runValidation({
|
|
|
780
615
|
remoteFileBuckets,
|
|
781
616
|
remoteFilesCounter,
|
|
782
617
|
remote,
|
|
783
|
-
project
|
|
784
|
-
keyOfCache,
|
|
785
|
-
routerModulesCache
|
|
618
|
+
project
|
|
786
619
|
});
|
|
787
620
|
|
|
788
621
|
// Yield any events from handler
|
|
@@ -894,31 +727,6 @@ async function* runValidation({
|
|
|
894
727
|
}
|
|
895
728
|
}
|
|
896
729
|
|
|
897
|
-
// GPT generated levenshtein distance algorithm:
|
|
898
|
-
const levenshtein = (a, b) => {
|
|
899
|
-
const [m, n] = [a.length, b.length];
|
|
900
|
-
if (!m || !n) return Math.max(m, n);
|
|
901
|
-
const dp = Array.from({
|
|
902
|
-
length: m + 1
|
|
903
|
-
}, (_, i) => i);
|
|
904
|
-
for (let j = 1; j <= n; j++) {
|
|
905
|
-
let prev = dp[0];
|
|
906
|
-
dp[0] = j;
|
|
907
|
-
for (let i = 1; i <= m; i++) {
|
|
908
|
-
const temp = dp[i];
|
|
909
|
-
dp[i] = a[i - 1] === b[j - 1] ? prev : Math.min(prev + 1, dp[i - 1] + 1, dp[i] + 1);
|
|
910
|
-
prev = temp;
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
return dp[m];
|
|
914
|
-
};
|
|
915
|
-
function findSimilar(key, targets) {
|
|
916
|
-
return targets.map(target => ({
|
|
917
|
-
target,
|
|
918
|
-
distance: levenshtein(key, target)
|
|
919
|
-
})).sort((a, b) => a.distance - b.distance);
|
|
920
|
-
}
|
|
921
|
-
|
|
922
730
|
async function validate({
|
|
923
731
|
root,
|
|
924
732
|
fix
|
|
@@ -10,7 +10,7 @@ import vm from 'node:vm';
|
|
|
10
10
|
import ts from 'typescript';
|
|
11
11
|
import z from 'zod';
|
|
12
12
|
import { createRequire } from 'node:module';
|
|
13
|
-
import {
|
|
13
|
+
import { resolveSchemaSourceFixes } from '@valbuild/shared/internal';
|
|
14
14
|
import nodeFs from 'fs';
|
|
15
15
|
|
|
16
16
|
function error(message) {
|
|
@@ -94,8 +94,6 @@ const textEncoder = new TextEncoder();
|
|
|
94
94
|
|
|
95
95
|
// Types for handler system
|
|
96
96
|
|
|
97
|
-
// Cache types for avoiding redundant service.get() calls
|
|
98
|
-
|
|
99
97
|
// Handler functions
|
|
100
98
|
async function handleFileMetadata(ctx) {
|
|
101
99
|
var _fileSource$source;
|
|
@@ -128,40 +126,6 @@ async function handleFileMetadata(ctx) {
|
|
|
128
126
|
shouldApplyPatch: true
|
|
129
127
|
};
|
|
130
128
|
}
|
|
131
|
-
async function handleKeyOfCheck(ctx) {
|
|
132
|
-
if (!ctx.validationError.value || typeof ctx.validationError.value !== "object" || !("key" in ctx.validationError.value) || !("sourcePath" in ctx.validationError.value)) {
|
|
133
|
-
return {
|
|
134
|
-
success: false,
|
|
135
|
-
errorMessage: `Unexpected error in ${ctx.sourcePath}: ${ctx.validationError.message} (Expected value to be an object with 'key' and 'sourcePath' properties - this is likely a bug in Val)`
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
const {
|
|
139
|
-
key,
|
|
140
|
-
sourcePath
|
|
141
|
-
} = ctx.validationError.value;
|
|
142
|
-
if (typeof key !== "string") {
|
|
143
|
-
return {
|
|
144
|
-
success: false,
|
|
145
|
-
errorMessage: `Unexpected error in ${sourcePath}: ${ctx.validationError.message} (Expected value property 'key' to be a string - this is likely a bug in Val)`
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
if (typeof sourcePath !== "string") {
|
|
149
|
-
return {
|
|
150
|
-
success: false,
|
|
151
|
-
errorMessage: `Unexpected error in ${sourcePath}: ${ctx.validationError.message} (Expected value property 'sourcePath' to be a string - this is likely a bug in Val)`
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
const res = await checkKeyIsValid(key, sourcePath, ctx.service, ctx.keyOfCache);
|
|
155
|
-
if (res.error) {
|
|
156
|
-
return {
|
|
157
|
-
success: false,
|
|
158
|
-
errorMessage: res.message
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
return {
|
|
162
|
-
success: true
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
129
|
async function handleRemoteFileUpload(ctx) {
|
|
166
130
|
var _resolvedRemoteFileAt;
|
|
167
131
|
if (!ctx.fix) {
|
|
@@ -370,155 +334,6 @@ async function handleRemoteFileCheck() {
|
|
|
370
334
|
shouldApplyPatch: true
|
|
371
335
|
};
|
|
372
336
|
}
|
|
373
|
-
|
|
374
|
-
// Helper function
|
|
375
|
-
async function checkKeyIsValid(key, sourcePath, service, cache) {
|
|
376
|
-
const [moduleFilePath, modulePath] = Internal.splitModuleFilePathAndModulePath(sourcePath);
|
|
377
|
-
const cacheKey = `${moduleFilePath}::${modulePath}`;
|
|
378
|
-
let keyOfModuleSource;
|
|
379
|
-
let keyOfModuleSchema;
|
|
380
|
-
const cached = cache.get(cacheKey);
|
|
381
|
-
if (cached) {
|
|
382
|
-
keyOfModuleSource = cached.source;
|
|
383
|
-
keyOfModuleSchema = cached.schema;
|
|
384
|
-
} else {
|
|
385
|
-
const keyOfModule = await service.get(moduleFilePath, modulePath, {
|
|
386
|
-
source: true,
|
|
387
|
-
schema: true,
|
|
388
|
-
validate: false
|
|
389
|
-
});
|
|
390
|
-
keyOfModuleSource = keyOfModule.source;
|
|
391
|
-
keyOfModuleSchema = keyOfModule.schema;
|
|
392
|
-
cache.set(cacheKey, {
|
|
393
|
-
source: keyOfModuleSource,
|
|
394
|
-
schema: keyOfModuleSchema
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
if (keyOfModuleSchema && keyOfModuleSchema.type !== "record") {
|
|
398
|
-
return {
|
|
399
|
-
error: true,
|
|
400
|
-
message: `Expected key at ${sourcePath} to be of type 'record'`
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
if (keyOfModuleSource && typeof keyOfModuleSource === "object" && key in keyOfModuleSource) {
|
|
404
|
-
return {
|
|
405
|
-
error: false
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
if (!keyOfModuleSource || typeof keyOfModuleSource !== "object") {
|
|
409
|
-
return {
|
|
410
|
-
error: true,
|
|
411
|
-
message: `Expected ${sourcePath} to be a truthy object`
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
const alternatives = findSimilar(key, Object.keys(keyOfModuleSource));
|
|
415
|
-
return {
|
|
416
|
-
error: true,
|
|
417
|
-
message: `Key '${key}' does not exist in ${sourcePath}. Closest match: '${alternatives[0].target}'. Other similar: ${alternatives.slice(1, 4).map(a => `'${a.target}'`).join(", ")}${alternatives.length > 4 ? ", ..." : ""}`
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Check if a route is valid by scanning all router modules
|
|
423
|
-
* and validating against include/exclude patterns
|
|
424
|
-
*/
|
|
425
|
-
async function checkRouteIsValid(route, include, exclude, service, valFiles, cache) {
|
|
426
|
-
// 1. Scan all val files to find modules with routers (use cache if available)
|
|
427
|
-
if (!cache.loaded) {
|
|
428
|
-
for (const file of valFiles) {
|
|
429
|
-
var _valModule$schema;
|
|
430
|
-
const moduleFilePath = `/${file}`;
|
|
431
|
-
const valModule = await service.get(moduleFilePath, "", {
|
|
432
|
-
source: true,
|
|
433
|
-
schema: true,
|
|
434
|
-
validate: false
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
// Check if this module has a router defined
|
|
438
|
-
if (((_valModule$schema = valModule.schema) === null || _valModule$schema === void 0 ? void 0 : _valModule$schema.type) === "record" && valModule.schema.router) {
|
|
439
|
-
if (valModule.source && typeof valModule.source === "object") {
|
|
440
|
-
cache.modules[moduleFilePath] = valModule.source;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
cache.loaded = true;
|
|
445
|
-
}
|
|
446
|
-
const routerModules = cache.modules;
|
|
447
|
-
|
|
448
|
-
// 2. Check if route exists in any router module
|
|
449
|
-
let foundInModule = null;
|
|
450
|
-
for (const [moduleFilePath, source] of Object.entries(routerModules)) {
|
|
451
|
-
if (route in source) {
|
|
452
|
-
foundInModule = moduleFilePath;
|
|
453
|
-
break;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
if (!foundInModule) {
|
|
457
|
-
// Route not found in any router module
|
|
458
|
-
let allRoutes = Object.values(routerModules).flatMap(source => Object.keys(source));
|
|
459
|
-
if (allRoutes.length === 0) {
|
|
460
|
-
return {
|
|
461
|
-
error: true,
|
|
462
|
-
message: `Route '${route}' could not be validated: No router modules found in the project. Use s.record(...).router(...) to define router modules.`
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Filter routes by include/exclude patterns for suggestions
|
|
467
|
-
allRoutes = filterRoutesByPatterns(allRoutes, include, exclude);
|
|
468
|
-
const alternatives = findSimilar(route, allRoutes);
|
|
469
|
-
return {
|
|
470
|
-
error: true,
|
|
471
|
-
message: `Route '${route}' does not exist in any router module. ${alternatives.length > 0 ? `Closest match: '${alternatives[0].target}'. Other similar: ${alternatives.slice(1, 4).map(a => `'${a.target}'`).join(", ")}${alternatives.length > 4 ? ", ..." : ""}` : "No similar routes found."}`
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// 3. Validate against include/exclude patterns
|
|
476
|
-
const patternValidation = validateRoutePatterns(route, include, exclude);
|
|
477
|
-
if (!patternValidation.valid) {
|
|
478
|
-
return {
|
|
479
|
-
error: true,
|
|
480
|
-
message: patternValidation.message
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
return {
|
|
484
|
-
error: false
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Handler for router:check-route validation fix
|
|
490
|
-
*/
|
|
491
|
-
async function handleRouteCheck(ctx) {
|
|
492
|
-
const {
|
|
493
|
-
sourcePath,
|
|
494
|
-
validationError,
|
|
495
|
-
service,
|
|
496
|
-
valFiles,
|
|
497
|
-
routerModulesCache
|
|
498
|
-
} = ctx;
|
|
499
|
-
|
|
500
|
-
// Extract route and patterns from validation error value
|
|
501
|
-
const value = validationError.value;
|
|
502
|
-
if (!value || typeof value.route !== "string") {
|
|
503
|
-
return {
|
|
504
|
-
success: false,
|
|
505
|
-
errorMessage: `Invalid route value in validation error: ${JSON.stringify(value)}`
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
const route = value.route;
|
|
509
|
-
|
|
510
|
-
// Check if the route is valid
|
|
511
|
-
const result = await checkRouteIsValid(route, value.include, value.exclude, service, valFiles, routerModulesCache);
|
|
512
|
-
if (result.error) {
|
|
513
|
-
return {
|
|
514
|
-
success: false,
|
|
515
|
-
errorMessage: `${sourcePath}: ${result.message}`
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
return {
|
|
519
|
-
success: true
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
337
|
async function handleUniqueFolderCheck(ctx) {
|
|
523
338
|
const value = ctx.validationError.value;
|
|
524
339
|
if (!value || typeof value.directory !== "string") {
|
|
@@ -617,14 +432,14 @@ async function handleCheckAllFiles(ctx) {
|
|
|
617
432
|
};
|
|
618
433
|
}
|
|
619
434
|
|
|
620
|
-
// Fix handler registry
|
|
435
|
+
// Fix handler registry. `keyof:check-keys` and `router:check-route` are
|
|
436
|
+
// resolved upfront by the shared resolveSchemaSourceFixes — they never reach
|
|
437
|
+
// this registry, so they're excluded from the key set.
|
|
621
438
|
const currentFixHandlers = {
|
|
622
439
|
"image:check-metadata": handleFileMetadata,
|
|
623
440
|
"image:add-metadata": handleFileMetadata,
|
|
624
441
|
"file:check-metadata": handleFileMetadata,
|
|
625
442
|
"file:add-metadata": handleFileMetadata,
|
|
626
|
-
"keyof:check-keys": handleKeyOfCheck,
|
|
627
|
-
"router:check-route": handleRouteCheck,
|
|
628
443
|
"image:upload-remote": handleRemoteFileUpload,
|
|
629
444
|
"file:upload-remote": handleRemoteFileUpload,
|
|
630
445
|
"image:download-remote": handleRemoteFileDownload,
|
|
@@ -676,12 +491,27 @@ async function* runValidation({
|
|
|
676
491
|
const service = await createService(projectRoot, {}, fs);
|
|
677
492
|
let errors = 0;
|
|
678
493
|
|
|
679
|
-
//
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
494
|
+
// Build a single schema/source snapshot up front so the shared resolver
|
|
495
|
+
// can resolve keyof:check-keys / router:check-route references that span
|
|
496
|
+
// multiple val files.
|
|
497
|
+
const snapshot = {
|
|
498
|
+
schemas: {},
|
|
499
|
+
sources: {}
|
|
684
500
|
};
|
|
501
|
+
for (const file of valFiles) {
|
|
502
|
+
const moduleFilePath = `/${file}`;
|
|
503
|
+
const valModule = await service.get(moduleFilePath, "", {
|
|
504
|
+
source: true,
|
|
505
|
+
schema: true,
|
|
506
|
+
validate: false
|
|
507
|
+
});
|
|
508
|
+
if (valModule.schema) {
|
|
509
|
+
snapshot.schemas[moduleFilePath] = valModule.schema;
|
|
510
|
+
}
|
|
511
|
+
if (valModule.source !== undefined) {
|
|
512
|
+
snapshot.sources[moduleFilePath] = valModule.source;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
685
515
|
async function* validateFile(file) {
|
|
686
516
|
const moduleFilePath = `/${file}`; // TODO: check if this always works? (Windows?)
|
|
687
517
|
const start = Date.now();
|
|
@@ -705,7 +535,12 @@ async function* runValidation({
|
|
|
705
535
|
let fixedErrors = 0;
|
|
706
536
|
if (valModule.errors) {
|
|
707
537
|
if (valModule.errors.validation) {
|
|
708
|
-
|
|
538
|
+
// Resolve schema/source fixes (keyof:check-keys, router:check-route)
|
|
539
|
+
// against the snapshot before per-error dispatch. Resolved errors
|
|
540
|
+
// are dropped; invalid references come back with rewritten messages
|
|
541
|
+
// and fixes cleared, so they fall through the "no fixes" branch.
|
|
542
|
+
const resolvedValidationErrors = resolveSchemaSourceFixes(valModule.errors.validation, snapshot);
|
|
543
|
+
for (const [sourcePath, validationErrors] of Object.entries(resolvedValidationErrors)) {
|
|
709
544
|
for (const v of validationErrors) {
|
|
710
545
|
if (!v.fixes || v.fixes.length === 0) {
|
|
711
546
|
// No fixes available - just report error
|
|
@@ -748,9 +583,7 @@ async function* runValidation({
|
|
|
748
583
|
remoteFileBuckets,
|
|
749
584
|
remoteFilesCounter,
|
|
750
585
|
remote,
|
|
751
|
-
project
|
|
752
|
-
keyOfCache,
|
|
753
|
-
routerModulesCache
|
|
586
|
+
project
|
|
754
587
|
});
|
|
755
588
|
|
|
756
589
|
// Yield any events from handler
|
|
@@ -862,31 +695,6 @@ async function* runValidation({
|
|
|
862
695
|
}
|
|
863
696
|
}
|
|
864
697
|
|
|
865
|
-
// GPT generated levenshtein distance algorithm:
|
|
866
|
-
const levenshtein = (a, b) => {
|
|
867
|
-
const [m, n] = [a.length, b.length];
|
|
868
|
-
if (!m || !n) return Math.max(m, n);
|
|
869
|
-
const dp = Array.from({
|
|
870
|
-
length: m + 1
|
|
871
|
-
}, (_, i) => i);
|
|
872
|
-
for (let j = 1; j <= n; j++) {
|
|
873
|
-
let prev = dp[0];
|
|
874
|
-
dp[0] = j;
|
|
875
|
-
for (let i = 1; i <= m; i++) {
|
|
876
|
-
const temp = dp[i];
|
|
877
|
-
dp[i] = a[i - 1] === b[j - 1] ? prev : Math.min(prev + 1, dp[i - 1] + 1, dp[i] + 1);
|
|
878
|
-
prev = temp;
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
return dp[m];
|
|
882
|
-
};
|
|
883
|
-
function findSimilar(key, targets) {
|
|
884
|
-
return targets.map(target => ({
|
|
885
|
-
target,
|
|
886
|
-
distance: levenshtein(key, target)
|
|
887
|
-
})).sort((a, b) => a.distance - b.distance);
|
|
888
|
-
}
|
|
889
|
-
|
|
890
698
|
async function validate({
|
|
891
699
|
root,
|
|
892
700
|
fix
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valbuild/cli",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.97.
|
|
4
|
+
"version": "0.97.1",
|
|
5
5
|
"description": "Val CLI tools",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
"open": "^9.1.0",
|
|
26
26
|
"picocolors": "^1.1.1",
|
|
27
27
|
"zod": "^4.3.5",
|
|
28
|
-
"@valbuild/core": "0.97.
|
|
28
|
+
"@valbuild/core": "0.97.1",
|
|
29
29
|
"@valbuild/eslint-plugin": "0.93.0",
|
|
30
|
-
"@valbuild/server": "0.97.
|
|
31
|
-
"@valbuild/shared": "0.97.
|
|
30
|
+
"@valbuild/server": "0.97.1",
|
|
31
|
+
"@valbuild/shared": "0.97.1"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"prettier": "*",
|
package/src/runValidation.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import {
|
|
11
11
|
FILE_REF_PROP,
|
|
12
12
|
Internal,
|
|
13
|
+
type Json,
|
|
13
14
|
ModuleFilePath,
|
|
14
15
|
ModulePath,
|
|
15
16
|
SerializedFileSchema,
|
|
@@ -18,9 +19,8 @@ import {
|
|
|
18
19
|
ValidationFix,
|
|
19
20
|
} from "@valbuild/core";
|
|
20
21
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
type SerializedRegExpPattern,
|
|
22
|
+
resolveSchemaSourceFixes,
|
|
23
|
+
type SchemaSourceSnapshot,
|
|
24
24
|
} from "@valbuild/shared/internal";
|
|
25
25
|
import { getFileExt } from "./utils/getFileExt";
|
|
26
26
|
import ts from "typescript";
|
|
@@ -64,16 +64,6 @@ export type ValidationError = {
|
|
|
64
64
|
fixes?: ValidationFix[];
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
// Cache types for avoiding redundant service.get() calls
|
|
68
|
-
export type KeyOfCache = Map<
|
|
69
|
-
string, // moduleFilePath + modulePath key
|
|
70
|
-
{ source: unknown; schema: { type: string } | undefined }
|
|
71
|
-
>;
|
|
72
|
-
export type RouterModulesCache = {
|
|
73
|
-
loaded: boolean;
|
|
74
|
-
modules: Record<string, Record<string, unknown>>;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
67
|
export type FixHandlerContext = {
|
|
78
68
|
sourcePath: SourcePath;
|
|
79
69
|
validationError: ValidationError;
|
|
@@ -95,9 +85,6 @@ export type FixHandlerContext = {
|
|
|
95
85
|
remoteFilesCounter: number;
|
|
96
86
|
remote: IValRemote;
|
|
97
87
|
project: string | undefined;
|
|
98
|
-
// Caches for validation
|
|
99
|
-
keyOfCache: KeyOfCache;
|
|
100
|
-
routerModulesCache: RouterModulesCache;
|
|
101
88
|
};
|
|
102
89
|
|
|
103
90
|
export type FixHandlerResult = {
|
|
@@ -180,56 +167,6 @@ export async function handleFileMetadata(
|
|
|
180
167
|
return { success: true, shouldApplyPatch: true };
|
|
181
168
|
}
|
|
182
169
|
|
|
183
|
-
export async function handleKeyOfCheck(
|
|
184
|
-
ctx: FixHandlerContext,
|
|
185
|
-
): Promise<FixHandlerResult> {
|
|
186
|
-
if (
|
|
187
|
-
!ctx.validationError.value ||
|
|
188
|
-
typeof ctx.validationError.value !== "object" ||
|
|
189
|
-
!("key" in ctx.validationError.value) ||
|
|
190
|
-
!("sourcePath" in ctx.validationError.value)
|
|
191
|
-
) {
|
|
192
|
-
return {
|
|
193
|
-
success: false,
|
|
194
|
-
errorMessage: `Unexpected error in ${ctx.sourcePath}: ${ctx.validationError.message} (Expected value to be an object with 'key' and 'sourcePath' properties - this is likely a bug in Val)`,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const { key, sourcePath } = ctx.validationError.value as {
|
|
199
|
-
key: unknown;
|
|
200
|
-
sourcePath: unknown;
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
if (typeof key !== "string") {
|
|
204
|
-
return {
|
|
205
|
-
success: false,
|
|
206
|
-
errorMessage: `Unexpected error in ${sourcePath}: ${ctx.validationError.message} (Expected value property 'key' to be a string - this is likely a bug in Val)`,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (typeof sourcePath !== "string") {
|
|
211
|
-
return {
|
|
212
|
-
success: false,
|
|
213
|
-
errorMessage: `Unexpected error in ${sourcePath}: ${ctx.validationError.message} (Expected value property 'sourcePath' to be a string - this is likely a bug in Val)`,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const res = await checkKeyIsValid(
|
|
218
|
-
key,
|
|
219
|
-
sourcePath,
|
|
220
|
-
ctx.service,
|
|
221
|
-
ctx.keyOfCache,
|
|
222
|
-
);
|
|
223
|
-
if (res.error) {
|
|
224
|
-
return {
|
|
225
|
-
success: false,
|
|
226
|
-
errorMessage: res.message,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return { success: true };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
170
|
export async function handleRemoteFileUpload(
|
|
234
171
|
ctx: FixHandlerContext,
|
|
235
172
|
): Promise<FixHandlerResult> {
|
|
@@ -486,203 +423,6 @@ export async function handleRemoteFileCheck(): Promise<FixHandlerResult> {
|
|
|
486
423
|
return { success: true, shouldApplyPatch: true };
|
|
487
424
|
}
|
|
488
425
|
|
|
489
|
-
// Helper function
|
|
490
|
-
export async function checkKeyIsValid(
|
|
491
|
-
key: string,
|
|
492
|
-
sourcePath: string,
|
|
493
|
-
service: Service,
|
|
494
|
-
cache: KeyOfCache,
|
|
495
|
-
): Promise<{ error: false } | { error: true; message: string }> {
|
|
496
|
-
const [moduleFilePath, modulePath] =
|
|
497
|
-
Internal.splitModuleFilePathAndModulePath(sourcePath as SourcePath);
|
|
498
|
-
|
|
499
|
-
const cacheKey = `${moduleFilePath}::${modulePath}`;
|
|
500
|
-
let keyOfModuleSource: unknown;
|
|
501
|
-
let keyOfModuleSchema: { type: string } | undefined;
|
|
502
|
-
|
|
503
|
-
const cached = cache.get(cacheKey);
|
|
504
|
-
if (cached) {
|
|
505
|
-
keyOfModuleSource = cached.source;
|
|
506
|
-
keyOfModuleSchema = cached.schema;
|
|
507
|
-
} else {
|
|
508
|
-
const keyOfModule = await service.get(moduleFilePath, modulePath, {
|
|
509
|
-
source: true,
|
|
510
|
-
schema: true,
|
|
511
|
-
validate: false,
|
|
512
|
-
});
|
|
513
|
-
keyOfModuleSource = keyOfModule.source;
|
|
514
|
-
keyOfModuleSchema = keyOfModule.schema as { type: string } | undefined;
|
|
515
|
-
cache.set(cacheKey, {
|
|
516
|
-
source: keyOfModuleSource,
|
|
517
|
-
schema: keyOfModuleSchema,
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
if (keyOfModuleSchema && keyOfModuleSchema.type !== "record") {
|
|
522
|
-
return {
|
|
523
|
-
error: true,
|
|
524
|
-
message: `Expected key at ${sourcePath} to be of type 'record'`,
|
|
525
|
-
};
|
|
526
|
-
}
|
|
527
|
-
if (
|
|
528
|
-
keyOfModuleSource &&
|
|
529
|
-
typeof keyOfModuleSource === "object" &&
|
|
530
|
-
key in keyOfModuleSource
|
|
531
|
-
) {
|
|
532
|
-
return { error: false };
|
|
533
|
-
}
|
|
534
|
-
if (!keyOfModuleSource || typeof keyOfModuleSource !== "object") {
|
|
535
|
-
return {
|
|
536
|
-
error: true,
|
|
537
|
-
message: `Expected ${sourcePath} to be a truthy object`,
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
const alternatives = findSimilar(key, Object.keys(keyOfModuleSource));
|
|
541
|
-
return {
|
|
542
|
-
error: true,
|
|
543
|
-
message: `Key '${key}' does not exist in ${sourcePath}. Closest match: '${alternatives[0].target}'. Other similar: ${alternatives
|
|
544
|
-
.slice(1, 4)
|
|
545
|
-
.map((a) => `'${a.target}'`)
|
|
546
|
-
.join(", ")}${alternatives.length > 4 ? ", ..." : ""}`,
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
/**
|
|
551
|
-
* Check if a route is valid by scanning all router modules
|
|
552
|
-
* and validating against include/exclude patterns
|
|
553
|
-
*/
|
|
554
|
-
export async function checkRouteIsValid(
|
|
555
|
-
route: string,
|
|
556
|
-
include: SerializedRegExpPattern | undefined,
|
|
557
|
-
exclude: SerializedRegExpPattern | undefined,
|
|
558
|
-
service: Service,
|
|
559
|
-
valFiles: string[],
|
|
560
|
-
cache: RouterModulesCache,
|
|
561
|
-
): Promise<{ error: false } | { error: true; message: string }> {
|
|
562
|
-
// 1. Scan all val files to find modules with routers (use cache if available)
|
|
563
|
-
if (!cache.loaded) {
|
|
564
|
-
for (const file of valFiles) {
|
|
565
|
-
const moduleFilePath = `/${file}` as ModuleFilePath;
|
|
566
|
-
const valModule = await service.get(moduleFilePath, "" as ModulePath, {
|
|
567
|
-
source: true,
|
|
568
|
-
schema: true,
|
|
569
|
-
validate: false,
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
// Check if this module has a router defined
|
|
573
|
-
if (valModule.schema?.type === "record" && valModule.schema.router) {
|
|
574
|
-
if (valModule.source && typeof valModule.source === "object") {
|
|
575
|
-
cache.modules[moduleFilePath] = valModule.source as Record<
|
|
576
|
-
string,
|
|
577
|
-
unknown
|
|
578
|
-
>;
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
cache.loaded = true;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
const routerModules = cache.modules;
|
|
586
|
-
|
|
587
|
-
// 2. Check if route exists in any router module
|
|
588
|
-
let foundInModule: string | null = null;
|
|
589
|
-
for (const [moduleFilePath, source] of Object.entries(routerModules)) {
|
|
590
|
-
if (route in source) {
|
|
591
|
-
foundInModule = moduleFilePath;
|
|
592
|
-
break;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
if (!foundInModule) {
|
|
597
|
-
// Route not found in any router module
|
|
598
|
-
let allRoutes = Object.values(routerModules).flatMap((source) =>
|
|
599
|
-
Object.keys(source),
|
|
600
|
-
);
|
|
601
|
-
|
|
602
|
-
if (allRoutes.length === 0) {
|
|
603
|
-
return {
|
|
604
|
-
error: true,
|
|
605
|
-
message: `Route '${route}' could not be validated: No router modules found in the project. Use s.record(...).router(...) to define router modules.`,
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// Filter routes by include/exclude patterns for suggestions
|
|
610
|
-
allRoutes = filterRoutesByPatterns(allRoutes, include, exclude);
|
|
611
|
-
|
|
612
|
-
const alternatives = findSimilar(route, allRoutes);
|
|
613
|
-
|
|
614
|
-
return {
|
|
615
|
-
error: true,
|
|
616
|
-
message: `Route '${route}' does not exist in any router module. ${
|
|
617
|
-
alternatives.length > 0
|
|
618
|
-
? `Closest match: '${alternatives[0].target}'. Other similar: ${alternatives
|
|
619
|
-
.slice(1, 4)
|
|
620
|
-
.map((a) => `'${a.target}'`)
|
|
621
|
-
.join(", ")}${alternatives.length > 4 ? ", ..." : ""}`
|
|
622
|
-
: "No similar routes found."
|
|
623
|
-
}`,
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// 3. Validate against include/exclude patterns
|
|
628
|
-
const patternValidation = validateRoutePatterns(route, include, exclude);
|
|
629
|
-
if (!patternValidation.valid) {
|
|
630
|
-
return {
|
|
631
|
-
error: true,
|
|
632
|
-
message: patternValidation.message,
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
return { error: false };
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
/**
|
|
640
|
-
* Handler for router:check-route validation fix
|
|
641
|
-
*/
|
|
642
|
-
export async function handleRouteCheck(
|
|
643
|
-
ctx: FixHandlerContext,
|
|
644
|
-
): Promise<FixHandlerResult> {
|
|
645
|
-
const { sourcePath, validationError, service, valFiles, routerModulesCache } =
|
|
646
|
-
ctx;
|
|
647
|
-
|
|
648
|
-
// Extract route and patterns from validation error value
|
|
649
|
-
const value = validationError.value as
|
|
650
|
-
| {
|
|
651
|
-
route: unknown;
|
|
652
|
-
include?: { source: string; flags: string };
|
|
653
|
-
exclude?: { source: string; flags: string };
|
|
654
|
-
}
|
|
655
|
-
| undefined;
|
|
656
|
-
|
|
657
|
-
if (!value || typeof value.route !== "string") {
|
|
658
|
-
return {
|
|
659
|
-
success: false,
|
|
660
|
-
errorMessage: `Invalid route value in validation error: ${JSON.stringify(value)}`,
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
const route = value.route;
|
|
665
|
-
|
|
666
|
-
// Check if the route is valid
|
|
667
|
-
const result = await checkRouteIsValid(
|
|
668
|
-
route,
|
|
669
|
-
value.include,
|
|
670
|
-
value.exclude,
|
|
671
|
-
service,
|
|
672
|
-
valFiles,
|
|
673
|
-
routerModulesCache,
|
|
674
|
-
);
|
|
675
|
-
|
|
676
|
-
if (result.error) {
|
|
677
|
-
return {
|
|
678
|
-
success: false,
|
|
679
|
-
errorMessage: `${sourcePath}: ${result.message}`,
|
|
680
|
-
};
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
return { success: true };
|
|
684
|
-
}
|
|
685
|
-
|
|
686
426
|
export async function handleUniqueFolderCheck(
|
|
687
427
|
ctx: FixHandlerContext,
|
|
688
428
|
): Promise<FixHandlerResult> {
|
|
@@ -791,14 +531,17 @@ export async function handleCheckAllFiles(
|
|
|
791
531
|
return { success: true, shouldApplyPatch: true };
|
|
792
532
|
}
|
|
793
533
|
|
|
794
|
-
// Fix handler registry
|
|
795
|
-
|
|
534
|
+
// Fix handler registry. `keyof:check-keys` and `router:check-route` are
|
|
535
|
+
// resolved upfront by the shared resolveSchemaSourceFixes — they never reach
|
|
536
|
+
// this registry, so they're excluded from the key set.
|
|
537
|
+
export const currentFixHandlers: Record<
|
|
538
|
+
Exclude<ValidationFix, "keyof:check-keys" | "router:check-route">,
|
|
539
|
+
FixHandler
|
|
540
|
+
> = {
|
|
796
541
|
"image:check-metadata": handleFileMetadata,
|
|
797
542
|
"image:add-metadata": handleFileMetadata,
|
|
798
543
|
"file:check-metadata": handleFileMetadata,
|
|
799
544
|
"file:add-metadata": handleFileMetadata,
|
|
800
|
-
"keyof:check-keys": handleKeyOfCheck,
|
|
801
|
-
"router:check-route": handleRouteCheck,
|
|
802
545
|
"image:upload-remote": handleRemoteFileUpload,
|
|
803
546
|
"file:upload-remote": handleRemoteFileUpload,
|
|
804
547
|
"image:download-remote": handleRemoteFileDownload,
|
|
@@ -863,12 +606,24 @@ export async function* runValidation({
|
|
|
863
606
|
|
|
864
607
|
let errors = 0;
|
|
865
608
|
|
|
866
|
-
//
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
609
|
+
// Build a single schema/source snapshot up front so the shared resolver
|
|
610
|
+
// can resolve keyof:check-keys / router:check-route references that span
|
|
611
|
+
// multiple val files.
|
|
612
|
+
const snapshot: SchemaSourceSnapshot = { schemas: {}, sources: {} };
|
|
613
|
+
for (const file of valFiles) {
|
|
614
|
+
const moduleFilePath = `/${file}` as ModuleFilePath;
|
|
615
|
+
const valModule = await service.get(moduleFilePath, "" as ModulePath, {
|
|
616
|
+
source: true,
|
|
617
|
+
schema: true,
|
|
618
|
+
validate: false,
|
|
619
|
+
});
|
|
620
|
+
if (valModule.schema) {
|
|
621
|
+
snapshot.schemas[moduleFilePath] = valModule.schema;
|
|
622
|
+
}
|
|
623
|
+
if (valModule.source !== undefined) {
|
|
624
|
+
snapshot.sources[moduleFilePath] = valModule.source as Json;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
872
627
|
|
|
873
628
|
async function* validateFile(file: string): AsyncGenerator<ValidationEvent> {
|
|
874
629
|
const moduleFilePath = `/${file}` as ModuleFilePath; // TODO: check if this always works? (Windows?)
|
|
@@ -896,8 +651,16 @@ export async function* runValidation({
|
|
|
896
651
|
let fixedErrors = 0;
|
|
897
652
|
if (valModule.errors) {
|
|
898
653
|
if (valModule.errors.validation) {
|
|
899
|
-
|
|
654
|
+
// Resolve schema/source fixes (keyof:check-keys, router:check-route)
|
|
655
|
+
// against the snapshot before per-error dispatch. Resolved errors
|
|
656
|
+
// are dropped; invalid references come back with rewritten messages
|
|
657
|
+
// and fixes cleared, so they fall through the "no fixes" branch.
|
|
658
|
+
const resolvedValidationErrors = resolveSchemaSourceFixes(
|
|
900
659
|
valModule.errors.validation,
|
|
660
|
+
snapshot,
|
|
661
|
+
);
|
|
662
|
+
for (const [sourcePath, validationErrors] of Object.entries(
|
|
663
|
+
resolvedValidationErrors,
|
|
901
664
|
)) {
|
|
902
665
|
for (const v of validationErrors) {
|
|
903
666
|
if (!v.fixes || v.fixes.length === 0) {
|
|
@@ -943,8 +706,6 @@ export async function* runValidation({
|
|
|
943
706
|
remoteFilesCounter,
|
|
944
707
|
remote,
|
|
945
708
|
project,
|
|
946
|
-
keyOfCache,
|
|
947
|
-
routerModulesCache,
|
|
948
709
|
});
|
|
949
710
|
|
|
950
711
|
// Yield any events from handler
|
|
@@ -1064,33 +825,3 @@ export async function* runValidation({
|
|
|
1064
825
|
yield { type: "summary-success" };
|
|
1065
826
|
}
|
|
1066
827
|
}
|
|
1067
|
-
|
|
1068
|
-
// GPT generated levenshtein distance algorithm:
|
|
1069
|
-
export const levenshtein = (a: string, b: string): number => {
|
|
1070
|
-
const [m, n] = [a.length, b.length];
|
|
1071
|
-
if (!m || !n) return Math.max(m, n);
|
|
1072
|
-
|
|
1073
|
-
const dp = Array.from({ length: m + 1 }, (_, i) => i);
|
|
1074
|
-
|
|
1075
|
-
for (let j = 1; j <= n; j++) {
|
|
1076
|
-
let prev = dp[0];
|
|
1077
|
-
dp[0] = j;
|
|
1078
|
-
|
|
1079
|
-
for (let i = 1; i <= m; i++) {
|
|
1080
|
-
const temp = dp[i];
|
|
1081
|
-
dp[i] =
|
|
1082
|
-
a[i - 1] === b[j - 1]
|
|
1083
|
-
? prev
|
|
1084
|
-
: Math.min(prev + 1, dp[i - 1] + 1, dp[i] + 1);
|
|
1085
|
-
prev = temp;
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
return dp[m];
|
|
1090
|
-
};
|
|
1091
|
-
|
|
1092
|
-
export function findSimilar(key: string, targets: string[]) {
|
|
1093
|
-
return targets
|
|
1094
|
-
.map((target) => ({ target, distance: levenshtein(key, target) }))
|
|
1095
|
-
.sort((a, b) => a.distance - b.distance);
|
|
1096
|
-
}
|