@valbuild/cli 0.92.1 → 0.94.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/cli/dist/valbuild-cli-cli.cjs.dev.js +415 -160
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +415 -160
- package/cli/dist/valbuild-cli-cli.esm.js +413 -158
- package/package.json +5 -5
- package/src/__fixtures__/basic/content/basic-errors.val.ts +7 -0
- package/src/__fixtures__/basic/content/basic-files.val.ts +14 -0
- package/src/__fixtures__/basic/content/basic-gallery-2.val.ts +17 -0
- package/src/__fixtures__/basic/content/basic-gallery-fail-on-non-unique-dir.val.ts +17 -0
- package/src/__fixtures__/basic/content/basic-gallery-missing-tracked.val.ts +17 -0
- package/src/__fixtures__/basic/content/basic-gallery-wrong-metadata.val.ts +17 -0
- package/src/__fixtures__/basic/content/basic-gallery.val.ts +18 -0
- package/src/__fixtures__/basic/content/basic-image-from-galleries.val.ts +15 -0
- package/src/__fixtures__/basic/content/basic-image-from-gallery.val.ts +12 -0
- package/src/__fixtures__/basic/content/basic-image.val.ts +7 -0
- package/src/__fixtures__/basic/content/basic-valid.val.ts +7 -0
- package/src/__fixtures__/basic/public/val/files/tracked.txt +1 -0
- package/src/__fixtures__/basic/public/val/files/untracked.txt +1 -0
- package/src/__fixtures__/basic/public/val/image.png +0 -0
- package/src/__fixtures__/basic/public/val/images/image.png +0 -0
- package/src/__fixtures__/basic/public/val/images2/image.png +0 -0
- package/src/__fixtures__/basic/public/val/images3/image.png +0 -0
- package/src/__fixtures__/basic/tsconfig.json +12 -0
- package/src/__fixtures__/basic/val.config.ts +5 -0
- package/src/runValidation.test.ts +386 -0
- package/src/runValidation.ts +1096 -0
- package/src/validate.ts +131 -887
package/src/validate.ts
CHANGED
|
@@ -1,675 +1,11 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
-
import {
|
|
3
|
-
createFixPatch,
|
|
4
|
-
createService,
|
|
5
|
-
getSettings,
|
|
6
|
-
getPersonalAccessTokenPath,
|
|
7
|
-
parsePersonalAccessTokenFile,
|
|
8
|
-
uploadRemoteFile,
|
|
9
|
-
Service,
|
|
10
|
-
} from "@valbuild/server";
|
|
11
|
-
import {
|
|
12
|
-
DEFAULT_CONTENT_HOST,
|
|
13
|
-
DEFAULT_VAL_REMOTE_HOST,
|
|
14
|
-
FILE_REF_PROP,
|
|
15
|
-
Internal,
|
|
16
|
-
ModuleFilePath,
|
|
17
|
-
ModulePath,
|
|
18
|
-
SerializedFileSchema,
|
|
19
|
-
SerializedImageSchema,
|
|
20
|
-
SourcePath,
|
|
21
|
-
ValidationFix,
|
|
22
|
-
} from "@valbuild/core";
|
|
23
|
-
import {
|
|
24
|
-
filterRoutesByPatterns,
|
|
25
|
-
validateRoutePatterns,
|
|
26
|
-
type SerializedRegExpPattern,
|
|
27
|
-
} from "@valbuild/shared/internal";
|
|
28
|
-
import { glob } from "fast-glob";
|
|
29
2
|
import picocolors from "picocolors";
|
|
30
3
|
import fs from "fs/promises";
|
|
4
|
+
import { glob } from "fast-glob";
|
|
5
|
+
import { DEFAULT_CONTENT_HOST, DEFAULT_VAL_REMOTE_HOST } from "@valbuild/core";
|
|
6
|
+
import { getSettings, uploadRemoteFile } from "@valbuild/server";
|
|
31
7
|
import { evalValConfigFile } from "./utils/evalValConfigFile";
|
|
32
|
-
import {
|
|
33
|
-
|
|
34
|
-
const textEncoder = new TextEncoder();
|
|
35
|
-
|
|
36
|
-
// Types for handler system
|
|
37
|
-
type ValModule = Awaited<ReturnType<Service["get"]>>;
|
|
38
|
-
|
|
39
|
-
type ValidationError = {
|
|
40
|
-
message: string;
|
|
41
|
-
value?: unknown;
|
|
42
|
-
fixes?: ValidationFix[];
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
// Cache types for avoiding redundant service.get() calls
|
|
46
|
-
type KeyOfCache = Map<
|
|
47
|
-
string, // moduleFilePath + modulePath key
|
|
48
|
-
{ source: unknown; schema: { type: string } | undefined }
|
|
49
|
-
>;
|
|
50
|
-
type RouterModulesCache = {
|
|
51
|
-
loaded: boolean;
|
|
52
|
-
modules: Record<string, Record<string, unknown>>;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
type FixHandlerContext = {
|
|
56
|
-
sourcePath: SourcePath;
|
|
57
|
-
validationError: ValidationError;
|
|
58
|
-
valModule: ValModule;
|
|
59
|
-
projectRoot: string;
|
|
60
|
-
fix: boolean;
|
|
61
|
-
service: Service;
|
|
62
|
-
valFiles: string[];
|
|
63
|
-
moduleFilePath: ModuleFilePath;
|
|
64
|
-
file: string;
|
|
65
|
-
// Shared state
|
|
66
|
-
remoteFiles: Record<
|
|
67
|
-
SourcePath,
|
|
68
|
-
{ ref: string; metadata?: Record<string, unknown> }
|
|
69
|
-
>;
|
|
70
|
-
publicProjectId?: string;
|
|
71
|
-
remoteFileBuckets?: string[];
|
|
72
|
-
remoteFilesCounter: number;
|
|
73
|
-
valRemoteHost: string;
|
|
74
|
-
contentHostUrl: string;
|
|
75
|
-
valConfigFile?: { project?: string };
|
|
76
|
-
// Caches for validation
|
|
77
|
-
keyOfCache: KeyOfCache;
|
|
78
|
-
routerModulesCache: RouterModulesCache;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
type FixHandlerResult = {
|
|
82
|
-
success: boolean;
|
|
83
|
-
errorMessage?: string;
|
|
84
|
-
shouldApplyPatch?: boolean;
|
|
85
|
-
// Updated shared state
|
|
86
|
-
publicProjectId?: string;
|
|
87
|
-
remoteFileBuckets?: string[];
|
|
88
|
-
remoteFilesCounter?: number;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
type FixHandler = (ctx: FixHandlerContext) => Promise<FixHandlerResult>;
|
|
92
|
-
|
|
93
|
-
// Handler functions
|
|
94
|
-
async function handleFileMetadata(
|
|
95
|
-
ctx: FixHandlerContext,
|
|
96
|
-
): Promise<FixHandlerResult> {
|
|
97
|
-
const [, modulePath] = Internal.splitModuleFilePathAndModulePath(
|
|
98
|
-
ctx.sourcePath,
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
102
|
-
return {
|
|
103
|
-
success: false,
|
|
104
|
-
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const fileSource = Internal.resolvePath(
|
|
109
|
-
modulePath,
|
|
110
|
-
ctx.valModule.source,
|
|
111
|
-
ctx.valModule.schema,
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
let filePath: string | null = null;
|
|
115
|
-
try {
|
|
116
|
-
filePath = path.join(
|
|
117
|
-
ctx.projectRoot,
|
|
118
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
119
|
-
(fileSource.source as any)?.[FILE_REF_PROP],
|
|
120
|
-
);
|
|
121
|
-
await fs.access(filePath);
|
|
122
|
-
} catch {
|
|
123
|
-
if (filePath) {
|
|
124
|
-
return {
|
|
125
|
-
success: false,
|
|
126
|
-
errorMessage: `File ${filePath} does not exist`,
|
|
127
|
-
};
|
|
128
|
-
} else {
|
|
129
|
-
return {
|
|
130
|
-
success: false,
|
|
131
|
-
errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return { success: true, shouldApplyPatch: true };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async function handleKeyOfCheck(
|
|
140
|
-
ctx: FixHandlerContext,
|
|
141
|
-
): Promise<FixHandlerResult> {
|
|
142
|
-
if (
|
|
143
|
-
!ctx.validationError.value ||
|
|
144
|
-
typeof ctx.validationError.value !== "object" ||
|
|
145
|
-
!("key" in ctx.validationError.value) ||
|
|
146
|
-
!("sourcePath" in ctx.validationError.value)
|
|
147
|
-
) {
|
|
148
|
-
return {
|
|
149
|
-
success: false,
|
|
150
|
-
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)`,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const { key, sourcePath } = ctx.validationError.value as {
|
|
155
|
-
key: unknown;
|
|
156
|
-
sourcePath: unknown;
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
if (typeof key !== "string") {
|
|
160
|
-
return {
|
|
161
|
-
success: false,
|
|
162
|
-
errorMessage: `Unexpected error in ${sourcePath}: ${ctx.validationError.message} (Expected value property 'key' to be a string - this is likely a bug in Val)`,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (typeof sourcePath !== "string") {
|
|
167
|
-
return {
|
|
168
|
-
success: false,
|
|
169
|
-
errorMessage: `Unexpected error in ${sourcePath}: ${ctx.validationError.message} (Expected value property 'sourcePath' to be a string - this is likely a bug in Val)`,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const res = await checkKeyIsValid(
|
|
174
|
-
key,
|
|
175
|
-
sourcePath,
|
|
176
|
-
ctx.service,
|
|
177
|
-
ctx.keyOfCache,
|
|
178
|
-
);
|
|
179
|
-
if (res.error) {
|
|
180
|
-
return {
|
|
181
|
-
success: false,
|
|
182
|
-
errorMessage: res.message,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return { success: true };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async function handleRemoteFileUpload(
|
|
190
|
-
ctx: FixHandlerContext,
|
|
191
|
-
): Promise<FixHandlerResult> {
|
|
192
|
-
if (!ctx.fix) {
|
|
193
|
-
return {
|
|
194
|
-
success: false,
|
|
195
|
-
errorMessage: `Remote file ${ctx.sourcePath} needs to be uploaded (use --fix to upload)`,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const [, modulePath] = Internal.splitModuleFilePathAndModulePath(
|
|
200
|
-
ctx.sourcePath,
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
204
|
-
return {
|
|
205
|
-
success: false,
|
|
206
|
-
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const resolvedRemoteFileAtSourcePath = Internal.resolvePath(
|
|
211
|
-
modulePath,
|
|
212
|
-
ctx.valModule.source,
|
|
213
|
-
ctx.valModule.schema,
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
let filePath: string | null = null;
|
|
217
|
-
try {
|
|
218
|
-
filePath = path.join(
|
|
219
|
-
ctx.projectRoot,
|
|
220
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
221
|
-
(resolvedRemoteFileAtSourcePath.source as any)?.[FILE_REF_PROP],
|
|
222
|
-
);
|
|
223
|
-
await fs.access(filePath);
|
|
224
|
-
} catch {
|
|
225
|
-
if (filePath) {
|
|
226
|
-
return {
|
|
227
|
-
success: false,
|
|
228
|
-
errorMessage: `File ${filePath} does not exist`,
|
|
229
|
-
};
|
|
230
|
-
} else {
|
|
231
|
-
return {
|
|
232
|
-
success: false,
|
|
233
|
-
errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const patFile = getPersonalAccessTokenPath(ctx.projectRoot);
|
|
239
|
-
try {
|
|
240
|
-
await fs.access(patFile);
|
|
241
|
-
} catch {
|
|
242
|
-
return {
|
|
243
|
-
success: false,
|
|
244
|
-
errorMessage: `File: ${path.join(ctx.projectRoot, ctx.file)} has remote images that are not uploaded and you are not logged in.\n\nFix this error by logging in:\n\t"npx val login"\n`,
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const parsedPatFile = parsePersonalAccessTokenFile(
|
|
249
|
-
await fs.readFile(patFile, "utf-8"),
|
|
250
|
-
);
|
|
251
|
-
if (!parsedPatFile.success) {
|
|
252
|
-
return {
|
|
253
|
-
success: false,
|
|
254
|
-
errorMessage: `Error parsing personal access token file: ${parsedPatFile.error}. You need to login again.`,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
const { pat } = parsedPatFile.data;
|
|
258
|
-
|
|
259
|
-
if (ctx.remoteFiles[ctx.sourcePath]) {
|
|
260
|
-
console.log(
|
|
261
|
-
picocolors.yellow("⚠"),
|
|
262
|
-
`Remote file ${filePath} already uploaded`,
|
|
263
|
-
);
|
|
264
|
-
return { success: true };
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (!resolvedRemoteFileAtSourcePath.schema) {
|
|
268
|
-
return {
|
|
269
|
-
success: false,
|
|
270
|
-
errorMessage: `Cannot upload remote file: schema not found for ${ctx.sourcePath}`,
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const actualRemoteFileSource = resolvedRemoteFileAtSourcePath.source;
|
|
275
|
-
const fileSourceMetadata = Internal.isFile(actualRemoteFileSource)
|
|
276
|
-
? actualRemoteFileSource.metadata
|
|
277
|
-
: undefined;
|
|
278
|
-
const resolveRemoteFileSchema = resolvedRemoteFileAtSourcePath.schema;
|
|
279
|
-
|
|
280
|
-
if (!resolveRemoteFileSchema) {
|
|
281
|
-
return {
|
|
282
|
-
success: false,
|
|
283
|
-
errorMessage: `Could not resolve schema for remote file: ${ctx.sourcePath}`,
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
let publicProjectId = ctx.publicProjectId;
|
|
288
|
-
let remoteFileBuckets = ctx.remoteFileBuckets;
|
|
289
|
-
let remoteFilesCounter = ctx.remoteFilesCounter;
|
|
290
|
-
|
|
291
|
-
if (!publicProjectId || !remoteFileBuckets) {
|
|
292
|
-
let projectName = process.env.VAL_PROJECT;
|
|
293
|
-
if (!projectName) {
|
|
294
|
-
projectName = ctx.valConfigFile?.project;
|
|
295
|
-
}
|
|
296
|
-
if (!projectName) {
|
|
297
|
-
return {
|
|
298
|
-
success: false,
|
|
299
|
-
errorMessage:
|
|
300
|
-
"Project name not found. Set VAL_PROJECT environment variable or add project name to val.config",
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
const settingsRes = await getSettings(projectName, { pat });
|
|
304
|
-
if (!settingsRes.success) {
|
|
305
|
-
return {
|
|
306
|
-
success: false,
|
|
307
|
-
errorMessage: `Could not get public project id: ${settingsRes.message}.`,
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
publicProjectId = settingsRes.data.publicProjectId;
|
|
311
|
-
remoteFileBuckets = settingsRes.data.remoteFileBuckets.map((b) => b.bucket);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (!publicProjectId) {
|
|
315
|
-
return {
|
|
316
|
-
success: false,
|
|
317
|
-
errorMessage: "Could not get public project id",
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (!ctx.valConfigFile?.project) {
|
|
322
|
-
return {
|
|
323
|
-
success: false,
|
|
324
|
-
errorMessage: `Could not get project. Check that your val.config has the 'project' field set, or set it using the VAL_PROJECT environment variable`,
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
if (
|
|
329
|
-
resolveRemoteFileSchema.type !== "image" &&
|
|
330
|
-
resolveRemoteFileSchema.type !== "file"
|
|
331
|
-
) {
|
|
332
|
-
return {
|
|
333
|
-
success: false,
|
|
334
|
-
errorMessage: `The schema is the remote is neither image nor file: ${ctx.sourcePath}`,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
remoteFilesCounter += 1;
|
|
339
|
-
const bucket =
|
|
340
|
-
remoteFileBuckets[remoteFilesCounter % remoteFileBuckets.length];
|
|
341
|
-
|
|
342
|
-
if (!bucket) {
|
|
343
|
-
return {
|
|
344
|
-
success: false,
|
|
345
|
-
errorMessage: `Internal error: could not allocate a bucket for the remote file located at ${ctx.sourcePath}`,
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
let fileBuffer: Buffer;
|
|
350
|
-
try {
|
|
351
|
-
fileBuffer = await fs.readFile(filePath);
|
|
352
|
-
} catch (e) {
|
|
353
|
-
return {
|
|
354
|
-
success: false,
|
|
355
|
-
errorMessage: `Error reading file: ${e}`,
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const relativeFilePath = path
|
|
360
|
-
.relative(ctx.projectRoot, filePath)
|
|
361
|
-
.split(path.sep)
|
|
362
|
-
.join("/") as `public/val/${string}`;
|
|
363
|
-
|
|
364
|
-
if (!relativeFilePath.startsWith("public/val/")) {
|
|
365
|
-
return {
|
|
366
|
-
success: false,
|
|
367
|
-
errorMessage: `File path must be within the public/val/ directory (e.g. public/val/path/to/file.txt). Got: ${relativeFilePath}`,
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const fileHash = Internal.remote.getFileHash(fileBuffer);
|
|
372
|
-
const coreVersion = Internal.VERSION.core || "unknown";
|
|
373
|
-
const fileExt = getFileExt(filePath);
|
|
374
|
-
const schema = resolveRemoteFileSchema as
|
|
375
|
-
| SerializedImageSchema
|
|
376
|
-
| SerializedFileSchema;
|
|
377
|
-
const metadata = fileSourceMetadata;
|
|
378
|
-
const ref = Internal.remote.createRemoteRef(ctx.valRemoteHost, {
|
|
379
|
-
publicProjectId,
|
|
380
|
-
coreVersion,
|
|
381
|
-
bucket,
|
|
382
|
-
validationHash: Internal.remote.getValidationHash(
|
|
383
|
-
coreVersion,
|
|
384
|
-
schema,
|
|
385
|
-
fileExt,
|
|
386
|
-
metadata,
|
|
387
|
-
fileHash,
|
|
388
|
-
textEncoder,
|
|
389
|
-
),
|
|
390
|
-
fileHash,
|
|
391
|
-
filePath: relativeFilePath,
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
console.log(picocolors.yellow("⚠"), `Uploading remote file: '${ref}'...`);
|
|
395
|
-
|
|
396
|
-
const remoteFileUpload = await uploadRemoteFile(
|
|
397
|
-
ctx.contentHostUrl,
|
|
398
|
-
ctx.valConfigFile.project,
|
|
399
|
-
bucket,
|
|
400
|
-
fileHash,
|
|
401
|
-
fileExt,
|
|
402
|
-
fileBuffer,
|
|
403
|
-
{ pat },
|
|
404
|
-
);
|
|
405
|
-
|
|
406
|
-
if (!remoteFileUpload.success) {
|
|
407
|
-
return {
|
|
408
|
-
success: false,
|
|
409
|
-
errorMessage: `Could not upload remote file: '${ref}'. Error: ${remoteFileUpload.error}`,
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
console.log(
|
|
414
|
-
picocolors.green("✔"),
|
|
415
|
-
`Completed upload of remote file: '${ref}'`,
|
|
416
|
-
);
|
|
417
|
-
|
|
418
|
-
ctx.remoteFiles[ctx.sourcePath] = {
|
|
419
|
-
ref,
|
|
420
|
-
metadata: fileSourceMetadata,
|
|
421
|
-
};
|
|
422
|
-
|
|
423
|
-
return {
|
|
424
|
-
success: true,
|
|
425
|
-
shouldApplyPatch: true,
|
|
426
|
-
publicProjectId,
|
|
427
|
-
remoteFileBuckets,
|
|
428
|
-
remoteFilesCounter,
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
async function handleRemoteFileDownload(
|
|
433
|
-
ctx: FixHandlerContext,
|
|
434
|
-
): Promise<FixHandlerResult> {
|
|
435
|
-
if (ctx.fix) {
|
|
436
|
-
console.log(
|
|
437
|
-
picocolors.yellow("⚠"),
|
|
438
|
-
`Downloading remote file in ${ctx.sourcePath}...`,
|
|
439
|
-
);
|
|
440
|
-
return { success: true, shouldApplyPatch: true };
|
|
441
|
-
} else {
|
|
442
|
-
return {
|
|
443
|
-
success: false,
|
|
444
|
-
errorMessage: `Remote file ${ctx.sourcePath} needs to be downloaded (use --fix to download)`,
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
async function handleRemoteFileCheck(): Promise<FixHandlerResult> {
|
|
450
|
-
// Skip - no action needed
|
|
451
|
-
return { success: true, shouldApplyPatch: true };
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Helper function
|
|
455
|
-
async function checkKeyIsValid(
|
|
456
|
-
key: string,
|
|
457
|
-
sourcePath: string,
|
|
458
|
-
service: Service,
|
|
459
|
-
cache: KeyOfCache,
|
|
460
|
-
): Promise<{ error: false } | { error: true; message: string }> {
|
|
461
|
-
const [moduleFilePath, modulePath] =
|
|
462
|
-
Internal.splitModuleFilePathAndModulePath(sourcePath as SourcePath);
|
|
463
|
-
|
|
464
|
-
const cacheKey = `${moduleFilePath}::${modulePath}`;
|
|
465
|
-
let keyOfModuleSource: unknown;
|
|
466
|
-
let keyOfModuleSchema: { type: string } | undefined;
|
|
467
|
-
|
|
468
|
-
const cached = cache.get(cacheKey);
|
|
469
|
-
if (cached) {
|
|
470
|
-
keyOfModuleSource = cached.source;
|
|
471
|
-
keyOfModuleSchema = cached.schema;
|
|
472
|
-
} else {
|
|
473
|
-
const keyOfModule = await service.get(moduleFilePath, modulePath, {
|
|
474
|
-
source: true,
|
|
475
|
-
schema: true,
|
|
476
|
-
validate: false,
|
|
477
|
-
});
|
|
478
|
-
keyOfModuleSource = keyOfModule.source;
|
|
479
|
-
keyOfModuleSchema = keyOfModule.schema as { type: string } | undefined;
|
|
480
|
-
cache.set(cacheKey, {
|
|
481
|
-
source: keyOfModuleSource,
|
|
482
|
-
schema: keyOfModuleSchema,
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (keyOfModuleSchema && keyOfModuleSchema.type !== "record") {
|
|
487
|
-
return {
|
|
488
|
-
error: true,
|
|
489
|
-
message: `Expected key at ${sourcePath} to be of type 'record'`,
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
if (
|
|
493
|
-
keyOfModuleSource &&
|
|
494
|
-
typeof keyOfModuleSource === "object" &&
|
|
495
|
-
key in keyOfModuleSource
|
|
496
|
-
) {
|
|
497
|
-
return { error: false };
|
|
498
|
-
}
|
|
499
|
-
if (!keyOfModuleSource || typeof keyOfModuleSource !== "object") {
|
|
500
|
-
return {
|
|
501
|
-
error: true,
|
|
502
|
-
message: `Expected ${sourcePath} to be a truthy object`,
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
const alternatives = findSimilar(key, Object.keys(keyOfModuleSource));
|
|
506
|
-
return {
|
|
507
|
-
error: true,
|
|
508
|
-
message: `Key '${key}' does not exist in ${sourcePath}. Closest match: '${alternatives[0].target}'. Other similar: ${alternatives
|
|
509
|
-
.slice(1, 4)
|
|
510
|
-
.map((a) => `'${a.target}'`)
|
|
511
|
-
.join(", ")}${alternatives.length > 4 ? ", ..." : ""}`,
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* Check if a route is valid by scanning all router modules
|
|
517
|
-
* and validating against include/exclude patterns
|
|
518
|
-
*/
|
|
519
|
-
async function checkRouteIsValid(
|
|
520
|
-
route: string,
|
|
521
|
-
include: SerializedRegExpPattern | undefined,
|
|
522
|
-
exclude: SerializedRegExpPattern | undefined,
|
|
523
|
-
service: Service,
|
|
524
|
-
valFiles: string[],
|
|
525
|
-
cache: RouterModulesCache,
|
|
526
|
-
): Promise<{ error: false } | { error: true; message: string }> {
|
|
527
|
-
// 1. Scan all val files to find modules with routers (use cache if available)
|
|
528
|
-
if (!cache.loaded) {
|
|
529
|
-
for (const file of valFiles) {
|
|
530
|
-
const moduleFilePath = `/${file}` as ModuleFilePath;
|
|
531
|
-
const valModule = await service.get(moduleFilePath, "" as ModulePath, {
|
|
532
|
-
source: true,
|
|
533
|
-
schema: true,
|
|
534
|
-
validate: false,
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
// Check if this module has a router defined
|
|
538
|
-
if (valModule.schema?.type === "record" && valModule.schema.router) {
|
|
539
|
-
if (valModule.source && typeof valModule.source === "object") {
|
|
540
|
-
cache.modules[moduleFilePath] = valModule.source as Record<
|
|
541
|
-
string,
|
|
542
|
-
unknown
|
|
543
|
-
>;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
cache.loaded = true;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const routerModules = cache.modules;
|
|
551
|
-
|
|
552
|
-
// 2. Check if route exists in any router module
|
|
553
|
-
let foundInModule: string | null = null;
|
|
554
|
-
for (const [moduleFilePath, source] of Object.entries(routerModules)) {
|
|
555
|
-
if (route in source) {
|
|
556
|
-
foundInModule = moduleFilePath;
|
|
557
|
-
break;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
if (!foundInModule) {
|
|
562
|
-
// Route not found in any router module
|
|
563
|
-
let allRoutes = Object.values(routerModules).flatMap((source) =>
|
|
564
|
-
Object.keys(source),
|
|
565
|
-
);
|
|
566
|
-
|
|
567
|
-
if (allRoutes.length === 0) {
|
|
568
|
-
return {
|
|
569
|
-
error: true,
|
|
570
|
-
message: `Route '${route}' could not be validated: No router modules found in the project. Use s.record(...).router(...) to define router modules.`,
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Filter routes by include/exclude patterns for suggestions
|
|
575
|
-
allRoutes = filterRoutesByPatterns(allRoutes, include, exclude);
|
|
576
|
-
|
|
577
|
-
const alternatives = findSimilar(route, allRoutes);
|
|
578
|
-
|
|
579
|
-
return {
|
|
580
|
-
error: true,
|
|
581
|
-
message: `Route '${route}' does not exist in any router module. ${
|
|
582
|
-
alternatives.length > 0
|
|
583
|
-
? `Closest match: '${alternatives[0].target}'. Other similar: ${alternatives
|
|
584
|
-
.slice(1, 4)
|
|
585
|
-
.map((a) => `'${a.target}'`)
|
|
586
|
-
.join(", ")}${alternatives.length > 4 ? ", ..." : ""}`
|
|
587
|
-
: "No similar routes found."
|
|
588
|
-
}`,
|
|
589
|
-
};
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// 3. Validate against include/exclude patterns
|
|
593
|
-
const patternValidation = validateRoutePatterns(route, include, exclude);
|
|
594
|
-
if (!patternValidation.valid) {
|
|
595
|
-
return {
|
|
596
|
-
error: true,
|
|
597
|
-
message: patternValidation.message,
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
return { error: false };
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* Handler for router:check-route validation fix
|
|
606
|
-
*/
|
|
607
|
-
async function handleRouteCheck(
|
|
608
|
-
ctx: FixHandlerContext,
|
|
609
|
-
): Promise<FixHandlerResult> {
|
|
610
|
-
const { sourcePath, validationError, service, valFiles, routerModulesCache } =
|
|
611
|
-
ctx;
|
|
612
|
-
|
|
613
|
-
// Extract route and patterns from validation error value
|
|
614
|
-
const value = validationError.value as
|
|
615
|
-
| {
|
|
616
|
-
route: unknown;
|
|
617
|
-
include?: { source: string; flags: string };
|
|
618
|
-
exclude?: { source: string; flags: string };
|
|
619
|
-
}
|
|
620
|
-
| undefined;
|
|
621
|
-
|
|
622
|
-
if (!value || typeof value.route !== "string") {
|
|
623
|
-
return {
|
|
624
|
-
success: false,
|
|
625
|
-
errorMessage: `Invalid route value in validation error: ${JSON.stringify(value)}`,
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
const route = value.route;
|
|
630
|
-
|
|
631
|
-
// Check if the route is valid
|
|
632
|
-
const result = await checkRouteIsValid(
|
|
633
|
-
route,
|
|
634
|
-
value.include,
|
|
635
|
-
value.exclude,
|
|
636
|
-
service,
|
|
637
|
-
valFiles,
|
|
638
|
-
routerModulesCache,
|
|
639
|
-
);
|
|
640
|
-
|
|
641
|
-
if (result.error) {
|
|
642
|
-
return {
|
|
643
|
-
success: false,
|
|
644
|
-
errorMessage: `${sourcePath}: ${result.message}`,
|
|
645
|
-
};
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
return { success: true };
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// Fix handler registry
|
|
652
|
-
const currentFixHandlers: Record<ValidationFix, FixHandler> = {
|
|
653
|
-
"image:check-metadata": handleFileMetadata,
|
|
654
|
-
"image:add-metadata": handleFileMetadata,
|
|
655
|
-
"file:check-metadata": handleFileMetadata,
|
|
656
|
-
"file:add-metadata": handleFileMetadata,
|
|
657
|
-
"keyof:check-keys": handleKeyOfCheck,
|
|
658
|
-
"router:check-route": handleRouteCheck,
|
|
659
|
-
"image:upload-remote": handleRemoteFileUpload,
|
|
660
|
-
"file:upload-remote": handleRemoteFileUpload,
|
|
661
|
-
"image:download-remote": handleRemoteFileDownload,
|
|
662
|
-
"file:download-remote": handleRemoteFileDownload,
|
|
663
|
-
"image:check-remote": handleRemoteFileCheck,
|
|
664
|
-
"file:check-remote": handleRemoteFileCheck,
|
|
665
|
-
};
|
|
666
|
-
const deprecatedFixHandlers: Record<string, FixHandler> = {
|
|
667
|
-
"image:replace-metadata": handleFileMetadata,
|
|
668
|
-
};
|
|
669
|
-
const fixHandlers: Record<string, FixHandler> = {
|
|
670
|
-
...deprecatedFixHandlers,
|
|
671
|
-
...currentFixHandlers,
|
|
672
|
-
};
|
|
8
|
+
import { createDefaultValFSHost, runValidation } from "./runValidation";
|
|
673
9
|
|
|
674
10
|
export async function validate({
|
|
675
11
|
root,
|
|
@@ -678,265 +14,173 @@ export async function validate({
|
|
|
678
14
|
root?: string;
|
|
679
15
|
fix?: boolean;
|
|
680
16
|
}) {
|
|
681
|
-
const valRemoteHost = process.env.VAL_REMOTE_HOST || DEFAULT_VAL_REMOTE_HOST;
|
|
682
|
-
const contentHostUrl = process.env.VAL_CONTENT_URL || DEFAULT_CONTENT_HOST;
|
|
683
17
|
const projectRoot = root ? path.resolve(root) : process.cwd();
|
|
18
|
+
|
|
684
19
|
const valConfigFile =
|
|
685
20
|
(await evalValConfigFile(projectRoot, "val.config.ts")) ||
|
|
686
21
|
(await evalValConfigFile(projectRoot, "val.config.js"));
|
|
22
|
+
|
|
23
|
+
const resolvedValConfigFile = valConfigFile
|
|
24
|
+
? {
|
|
25
|
+
...valConfigFile,
|
|
26
|
+
project: process.env.VAL_PROJECT || valConfigFile.project,
|
|
27
|
+
}
|
|
28
|
+
: process.env.VAL_PROJECT
|
|
29
|
+
? { project: process.env.VAL_PROJECT }
|
|
30
|
+
: undefined;
|
|
31
|
+
|
|
687
32
|
console.log(
|
|
688
33
|
picocolors.greenBright(
|
|
689
|
-
`Validating project${
|
|
34
|
+
`Validating project${resolvedValConfigFile?.project ? ` '${picocolors.inverse(resolvedValConfigFile.project)}'` : ""}...`,
|
|
690
35
|
),
|
|
691
36
|
);
|
|
692
|
-
const service = await createService(projectRoot, {});
|
|
693
|
-
let prettier;
|
|
694
|
-
try {
|
|
695
|
-
prettier = (await import("prettier")).default;
|
|
696
|
-
} catch {
|
|
697
|
-
console.log("Prettier not found, skipping formatting");
|
|
698
|
-
}
|
|
699
37
|
|
|
700
38
|
const valFiles: string[] = await glob("**/*.val.{js,ts}", {
|
|
701
39
|
ignore: ["node_modules/**"],
|
|
702
40
|
cwd: projectRoot,
|
|
703
41
|
});
|
|
704
42
|
|
|
705
|
-
let errors = 0;
|
|
706
43
|
console.log(picocolors.greenBright(`Found ${valFiles.length} files...`));
|
|
707
|
-
let publicProjectId: string | undefined;
|
|
708
|
-
let didFix = false;
|
|
709
|
-
|
|
710
|
-
// Create caches that persist across all file validations
|
|
711
|
-
const keyOfCache: KeyOfCache = new Map();
|
|
712
|
-
const routerModulesCache: RouterModulesCache = { loaded: false, modules: {} };
|
|
713
|
-
|
|
714
|
-
async function validateFile(file: string): Promise<number> {
|
|
715
|
-
const moduleFilePath = `/${file}` as ModuleFilePath; // TODO: check if this always works? (Windows?)
|
|
716
|
-
const start = Date.now();
|
|
717
|
-
const valModule = await service.get(moduleFilePath, "" as ModulePath, {
|
|
718
|
-
source: true,
|
|
719
|
-
schema: true,
|
|
720
|
-
validate: true,
|
|
721
|
-
});
|
|
722
|
-
const remoteFiles: Record<
|
|
723
|
-
SourcePath,
|
|
724
|
-
{ ref: string; metadata?: Record<string, unknown> }
|
|
725
|
-
> = {};
|
|
726
|
-
let remoteFileBuckets: string[] | undefined = undefined;
|
|
727
|
-
let remoteFilesCounter = 0;
|
|
728
|
-
if (!valModule.errors) {
|
|
729
|
-
console.log(
|
|
730
|
-
picocolors.green("✔"),
|
|
731
|
-
moduleFilePath,
|
|
732
|
-
"is valid (" + (Date.now() - start) + "ms)",
|
|
733
|
-
);
|
|
734
|
-
return 0;
|
|
735
|
-
} else {
|
|
736
|
-
let errors = 0;
|
|
737
|
-
let fixedErrors = 0;
|
|
738
|
-
if (valModule.errors) {
|
|
739
|
-
if (valModule.errors.validation) {
|
|
740
|
-
for (const [sourcePath, validationErrors] of Object.entries(
|
|
741
|
-
valModule.errors.validation,
|
|
742
|
-
)) {
|
|
743
|
-
for (const v of validationErrors) {
|
|
744
|
-
if (!v.fixes || v.fixes.length === 0) {
|
|
745
|
-
// No fixes available - just report error
|
|
746
|
-
errors += 1;
|
|
747
|
-
console.log(
|
|
748
|
-
picocolors.red("✘"),
|
|
749
|
-
"Got error in",
|
|
750
|
-
`${sourcePath}:`,
|
|
751
|
-
v.message,
|
|
752
|
-
);
|
|
753
|
-
continue;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// Find and execute appropriate handler
|
|
757
|
-
const fixType = v.fixes[0]; // Take first fix
|
|
758
|
-
const handler = fixHandlers[fixType];
|
|
759
44
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
sourcePath,
|
|
767
|
-
);
|
|
768
|
-
errors += 1;
|
|
769
|
-
continue;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
// Execute handler
|
|
773
|
-
const result = await handler({
|
|
774
|
-
sourcePath: sourcePath as SourcePath,
|
|
775
|
-
validationError: v,
|
|
776
|
-
valModule,
|
|
777
|
-
projectRoot,
|
|
778
|
-
fix: !!fix,
|
|
779
|
-
service,
|
|
780
|
-
valFiles,
|
|
781
|
-
moduleFilePath,
|
|
782
|
-
file,
|
|
783
|
-
remoteFiles,
|
|
784
|
-
publicProjectId,
|
|
785
|
-
remoteFileBuckets,
|
|
786
|
-
remoteFilesCounter,
|
|
787
|
-
valRemoteHost,
|
|
788
|
-
contentHostUrl,
|
|
789
|
-
valConfigFile: valConfigFile ?? undefined,
|
|
790
|
-
keyOfCache,
|
|
791
|
-
routerModulesCache,
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
// Update shared state from handler result
|
|
795
|
-
if (result.publicProjectId !== undefined) {
|
|
796
|
-
publicProjectId = result.publicProjectId;
|
|
797
|
-
}
|
|
798
|
-
if (result.remoteFileBuckets !== undefined) {
|
|
799
|
-
remoteFileBuckets = result.remoteFileBuckets;
|
|
800
|
-
}
|
|
801
|
-
if (result.remoteFilesCounter !== undefined) {
|
|
802
|
-
remoteFilesCounter = result.remoteFilesCounter;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
if (!result.success) {
|
|
806
|
-
console.log(picocolors.red("✘"), result.errorMessage);
|
|
807
|
-
errors += 1;
|
|
808
|
-
continue;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// Apply patch if needed
|
|
812
|
-
if (result.shouldApplyPatch) {
|
|
813
|
-
const fixPatch = await createFixPatch(
|
|
814
|
-
{ projectRoot, remoteHost: valRemoteHost },
|
|
815
|
-
!!fix,
|
|
816
|
-
sourcePath as SourcePath,
|
|
817
|
-
v,
|
|
818
|
-
remoteFiles,
|
|
819
|
-
valModule.source,
|
|
820
|
-
valModule.schema,
|
|
821
|
-
);
|
|
45
|
+
let prettier;
|
|
46
|
+
try {
|
|
47
|
+
prettier = (await import("prettier")).default;
|
|
48
|
+
} catch {
|
|
49
|
+
console.log("Prettier not found, skipping formatting");
|
|
50
|
+
}
|
|
822
51
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
didFix = true;
|
|
826
|
-
fixedErrors += 1;
|
|
827
|
-
console.log(
|
|
828
|
-
picocolors.yellow("⚠"),
|
|
829
|
-
"Applied fix for",
|
|
830
|
-
sourcePath,
|
|
831
|
-
);
|
|
832
|
-
}
|
|
52
|
+
const fixedFiles = new Set<string>();
|
|
53
|
+
let totalErrors = 0;
|
|
833
54
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
);
|
|
858
|
-
}
|
|
859
|
-
for (const fatalError of valModule.errors.fatal || []) {
|
|
860
|
-
errors += 1;
|
|
861
|
-
console.log(
|
|
862
|
-
picocolors.red("✘"),
|
|
863
|
-
moduleFilePath,
|
|
864
|
-
"is invalid:",
|
|
865
|
-
fatalError.message,
|
|
866
|
-
);
|
|
867
|
-
}
|
|
868
|
-
} else {
|
|
55
|
+
for await (const event of runValidation({
|
|
56
|
+
root: projectRoot,
|
|
57
|
+
fix: !!fix,
|
|
58
|
+
valFiles,
|
|
59
|
+
project: resolvedValConfigFile?.project,
|
|
60
|
+
remote: {
|
|
61
|
+
remoteHost: process.env.VAL_REMOTE_HOST || DEFAULT_VAL_REMOTE_HOST,
|
|
62
|
+
getSettings: (projectName, options) => getSettings(projectName, options),
|
|
63
|
+
uploadFile: (project, bucket, fileHash, fileExt, fileBuffer, options) =>
|
|
64
|
+
uploadRemoteFile(
|
|
65
|
+
process.env.VAL_CONTENT_URL || DEFAULT_CONTENT_HOST,
|
|
66
|
+
project,
|
|
67
|
+
bucket,
|
|
68
|
+
fileHash,
|
|
69
|
+
fileExt ?? "",
|
|
70
|
+
fileBuffer,
|
|
71
|
+
options,
|
|
72
|
+
),
|
|
73
|
+
},
|
|
74
|
+
fs: createDefaultValFSHost(),
|
|
75
|
+
})) {
|
|
76
|
+
switch (event.type) {
|
|
77
|
+
case "file-valid":
|
|
869
78
|
console.log(
|
|
870
79
|
picocolors.green("✔"),
|
|
871
|
-
|
|
872
|
-
"is valid (" +
|
|
80
|
+
event.file,
|
|
81
|
+
"is valid (" + event.durationMs + "ms)",
|
|
873
82
|
);
|
|
874
|
-
|
|
875
|
-
|
|
83
|
+
break;
|
|
84
|
+
case "file-error-count":
|
|
876
85
|
console.log(
|
|
877
86
|
picocolors.red("✘"),
|
|
878
|
-
`${
|
|
879
|
-
" (" +
|
|
87
|
+
`${event.file} contains ${event.errorCount} error${event.errorCount > 1 ? "s" : ""}`,
|
|
88
|
+
" (" + event.durationMs + "ms)",
|
|
880
89
|
);
|
|
881
|
-
|
|
882
|
-
|
|
90
|
+
totalErrors += event.errorCount;
|
|
91
|
+
break;
|
|
92
|
+
case "validation-error":
|
|
93
|
+
console.log(
|
|
94
|
+
picocolors.red("✘"),
|
|
95
|
+
"Got error in",
|
|
96
|
+
`${event.sourcePath}:`,
|
|
97
|
+
event.message,
|
|
98
|
+
);
|
|
99
|
+
break;
|
|
100
|
+
case "validation-fixable-error":
|
|
101
|
+
console.log(
|
|
102
|
+
event.fixable ? picocolors.yellow("⚠") : picocolors.red("✘"),
|
|
103
|
+
`Got ${event.fixable ? "fixable " : ""}error in`,
|
|
104
|
+
`${event.sourcePath}:`,
|
|
105
|
+
event.message,
|
|
106
|
+
);
|
|
107
|
+
break;
|
|
108
|
+
case "unknown-fix":
|
|
109
|
+
console.log(
|
|
110
|
+
picocolors.red("✘"),
|
|
111
|
+
"Unknown fix",
|
|
112
|
+
event.fixes,
|
|
113
|
+
"for",
|
|
114
|
+
event.sourcePath,
|
|
115
|
+
);
|
|
116
|
+
break;
|
|
117
|
+
case "fix-applied":
|
|
118
|
+
console.log(
|
|
119
|
+
picocolors.yellow("⚠"),
|
|
120
|
+
"Applied fix for",
|
|
121
|
+
event.sourcePath,
|
|
122
|
+
);
|
|
123
|
+
fixedFiles.add(event.file);
|
|
124
|
+
break;
|
|
125
|
+
case "fatal-error":
|
|
126
|
+
console.log(
|
|
127
|
+
picocolors.red("✘"),
|
|
128
|
+
event.file,
|
|
129
|
+
"is invalid:",
|
|
130
|
+
event.message,
|
|
131
|
+
);
|
|
132
|
+
break;
|
|
133
|
+
case "remote-uploading":
|
|
134
|
+
console.log(
|
|
135
|
+
picocolors.yellow("⚠"),
|
|
136
|
+
`Uploading remote file: '${event.ref}'...`,
|
|
137
|
+
);
|
|
138
|
+
break;
|
|
139
|
+
case "remote-uploaded":
|
|
140
|
+
console.log(
|
|
141
|
+
picocolors.green("✔"),
|
|
142
|
+
`Completed upload of remote file: '${event.ref}'`,
|
|
143
|
+
);
|
|
144
|
+
break;
|
|
145
|
+
case "remote-already-uploaded":
|
|
146
|
+
console.log(
|
|
147
|
+
picocolors.yellow("⚠"),
|
|
148
|
+
`Remote file ${event.filePath} already uploaded`,
|
|
149
|
+
);
|
|
150
|
+
break;
|
|
151
|
+
case "remote-downloading":
|
|
152
|
+
console.log(
|
|
153
|
+
picocolors.yellow("⚠"),
|
|
154
|
+
`Downloading remote file in ${event.sourcePath}...`,
|
|
155
|
+
);
|
|
156
|
+
break;
|
|
157
|
+
case "summary-errors":
|
|
158
|
+
case "summary-success":
|
|
159
|
+
break;
|
|
883
160
|
}
|
|
884
161
|
}
|
|
885
162
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
if (prettier && didFix) {
|
|
163
|
+
// Run prettier on files that had fixes applied
|
|
164
|
+
if (prettier) {
|
|
165
|
+
for (const file of fixedFiles) {
|
|
890
166
|
const filePath = path.join(projectRoot, file);
|
|
891
167
|
const fileContent = await fs.readFile(filePath, "utf-8");
|
|
892
|
-
const formattedContent = await prettier
|
|
168
|
+
const formattedContent = await prettier.format(fileContent, {
|
|
893
169
|
filepath: filePath,
|
|
894
170
|
});
|
|
895
171
|
await fs.writeFile(filePath, formattedContent);
|
|
896
172
|
}
|
|
897
173
|
}
|
|
898
|
-
|
|
174
|
+
|
|
175
|
+
if (totalErrors > 0) {
|
|
899
176
|
console.log(
|
|
900
177
|
picocolors.red("✘"),
|
|
901
178
|
"Got",
|
|
902
|
-
|
|
903
|
-
"error" + (
|
|
179
|
+
totalErrors,
|
|
180
|
+
"error" + (totalErrors > 1 ? "s" : ""),
|
|
904
181
|
);
|
|
905
182
|
process.exit(1);
|
|
906
183
|
} else {
|
|
907
184
|
console.log(picocolors.green("✔"), "No validation errors found");
|
|
908
185
|
}
|
|
909
|
-
|
|
910
|
-
service.dispose();
|
|
911
|
-
return;
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
// GPT generated levenshtein distance algorithm:
|
|
915
|
-
const levenshtein = (a: string, b: string): number => {
|
|
916
|
-
const [m, n] = [a.length, b.length];
|
|
917
|
-
if (!m || !n) return Math.max(m, n);
|
|
918
|
-
|
|
919
|
-
const dp = Array.from({ length: m + 1 }, (_, i) => i);
|
|
920
|
-
|
|
921
|
-
for (let j = 1; j <= n; j++) {
|
|
922
|
-
let prev = dp[0];
|
|
923
|
-
dp[0] = j;
|
|
924
|
-
|
|
925
|
-
for (let i = 1; i <= m; i++) {
|
|
926
|
-
const temp = dp[i];
|
|
927
|
-
dp[i] =
|
|
928
|
-
a[i - 1] === b[j - 1]
|
|
929
|
-
? prev
|
|
930
|
-
: Math.min(prev + 1, dp[i - 1] + 1, dp[i] + 1);
|
|
931
|
-
prev = temp;
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
return dp[m];
|
|
936
|
-
};
|
|
937
|
-
|
|
938
|
-
function findSimilar(key: string, targets: string[]) {
|
|
939
|
-
return targets
|
|
940
|
-
.map((target) => ({ target, distance: levenshtein(key, target) }))
|
|
941
|
-
.sort((a, b) => a.distance - b.distance);
|
|
942
186
|
}
|