@valbuild/cli 0.87.5 → 0.89.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/cli/dist/valbuild-cli-cli.cjs.dev.js +483 -234
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +483 -234
- package/cli/dist/valbuild-cli-cli.esm.js +484 -235
- package/package.json +6 -6
- package/src/validate.test.ts +812 -0
- package/src/validate.ts +682 -422
package/src/validate.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getPersonalAccessTokenPath,
|
|
7
7
|
parsePersonalAccessTokenFile,
|
|
8
8
|
uploadRemoteFile,
|
|
9
|
+
Service,
|
|
9
10
|
} from "@valbuild/server";
|
|
10
11
|
import {
|
|
11
12
|
DEFAULT_CONTENT_HOST,
|
|
@@ -19,6 +20,11 @@ import {
|
|
|
19
20
|
SourcePath,
|
|
20
21
|
ValidationFix,
|
|
21
22
|
} from "@valbuild/core";
|
|
23
|
+
import {
|
|
24
|
+
filterRoutesByPatterns,
|
|
25
|
+
validateRoutePatterns,
|
|
26
|
+
type SerializedRegExpPattern,
|
|
27
|
+
} from "@valbuild/shared/internal";
|
|
22
28
|
import { glob } from "fast-glob";
|
|
23
29
|
import picocolors from "picocolors";
|
|
24
30
|
import fs from "fs/promises";
|
|
@@ -26,6 +32,611 @@ import { evalValConfigFile } from "./utils/evalValConfigFile";
|
|
|
26
32
|
import { getFileExt } from "./utils/getFileExt";
|
|
27
33
|
|
|
28
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
|
+
type FixHandlerContext = {
|
|
46
|
+
sourcePath: SourcePath;
|
|
47
|
+
validationError: ValidationError;
|
|
48
|
+
valModule: ValModule;
|
|
49
|
+
projectRoot: string;
|
|
50
|
+
fix: boolean;
|
|
51
|
+
service: Service;
|
|
52
|
+
valFiles: string[];
|
|
53
|
+
moduleFilePath: ModuleFilePath;
|
|
54
|
+
file: string;
|
|
55
|
+
// Shared state
|
|
56
|
+
remoteFiles: Record<
|
|
57
|
+
SourcePath,
|
|
58
|
+
{ ref: string; metadata?: Record<string, unknown> }
|
|
59
|
+
>;
|
|
60
|
+
publicProjectId?: string;
|
|
61
|
+
remoteFileBuckets?: string[];
|
|
62
|
+
remoteFilesCounter: number;
|
|
63
|
+
valRemoteHost: string;
|
|
64
|
+
contentHostUrl: string;
|
|
65
|
+
valConfigFile?: { project?: string };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type FixHandlerResult = {
|
|
69
|
+
success: boolean;
|
|
70
|
+
errorMessage?: string;
|
|
71
|
+
shouldApplyPatch?: boolean;
|
|
72
|
+
// Updated shared state
|
|
73
|
+
publicProjectId?: string;
|
|
74
|
+
remoteFileBuckets?: string[];
|
|
75
|
+
remoteFilesCounter?: number;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
type FixHandler = (ctx: FixHandlerContext) => Promise<FixHandlerResult>;
|
|
79
|
+
|
|
80
|
+
// Handler functions
|
|
81
|
+
async function handleFileMetadata(
|
|
82
|
+
ctx: FixHandlerContext,
|
|
83
|
+
): Promise<FixHandlerResult> {
|
|
84
|
+
const [, modulePath] = Internal.splitModuleFilePathAndModulePath(
|
|
85
|
+
ctx.sourcePath,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
89
|
+
return {
|
|
90
|
+
success: false,
|
|
91
|
+
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const fileSource = Internal.resolvePath(
|
|
96
|
+
modulePath,
|
|
97
|
+
ctx.valModule.source,
|
|
98
|
+
ctx.valModule.schema,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
let filePath: string | null = null;
|
|
102
|
+
try {
|
|
103
|
+
filePath = path.join(
|
|
104
|
+
ctx.projectRoot,
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
106
|
+
(fileSource.source as any)?.[FILE_REF_PROP],
|
|
107
|
+
);
|
|
108
|
+
await fs.access(filePath);
|
|
109
|
+
} catch {
|
|
110
|
+
if (filePath) {
|
|
111
|
+
return {
|
|
112
|
+
success: false,
|
|
113
|
+
errorMessage: `File ${filePath} does not exist`,
|
|
114
|
+
};
|
|
115
|
+
} else {
|
|
116
|
+
return {
|
|
117
|
+
success: false,
|
|
118
|
+
errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { success: true, shouldApplyPatch: true };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function handleKeyOfCheck(
|
|
127
|
+
ctx: FixHandlerContext,
|
|
128
|
+
): Promise<FixHandlerResult> {
|
|
129
|
+
if (
|
|
130
|
+
!ctx.validationError.value ||
|
|
131
|
+
typeof ctx.validationError.value !== "object" ||
|
|
132
|
+
!("key" in ctx.validationError.value) ||
|
|
133
|
+
!("sourcePath" in ctx.validationError.value)
|
|
134
|
+
) {
|
|
135
|
+
return {
|
|
136
|
+
success: false,
|
|
137
|
+
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)`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const { key, sourcePath } = ctx.validationError.value as {
|
|
142
|
+
key: unknown;
|
|
143
|
+
sourcePath: unknown;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (typeof key !== "string") {
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
errorMessage: `Unexpected error in ${sourcePath}: ${ctx.validationError.message} (Expected value property 'key' to be a string - this is likely a bug in Val)`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (typeof sourcePath !== "string") {
|
|
154
|
+
return {
|
|
155
|
+
success: false,
|
|
156
|
+
errorMessage: `Unexpected error in ${sourcePath}: ${ctx.validationError.message} (Expected value property 'sourcePath' to be a string - this is likely a bug in Val)`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const res = await checkKeyIsValid(key, sourcePath, ctx.service);
|
|
161
|
+
if (res.error) {
|
|
162
|
+
return {
|
|
163
|
+
success: false,
|
|
164
|
+
errorMessage: res.message,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { success: true };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function handleRemoteFileUpload(
|
|
172
|
+
ctx: FixHandlerContext,
|
|
173
|
+
): Promise<FixHandlerResult> {
|
|
174
|
+
if (!ctx.fix) {
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
errorMessage: `Remote file ${ctx.sourcePath} needs to be uploaded (use --fix to upload)`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const [, modulePath] = Internal.splitModuleFilePathAndModulePath(
|
|
182
|
+
ctx.sourcePath,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
186
|
+
return {
|
|
187
|
+
success: false,
|
|
188
|
+
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const resolvedRemoteFileAtSourcePath = Internal.resolvePath(
|
|
193
|
+
modulePath,
|
|
194
|
+
ctx.valModule.source,
|
|
195
|
+
ctx.valModule.schema,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
let filePath: string | null = null;
|
|
199
|
+
try {
|
|
200
|
+
filePath = path.join(
|
|
201
|
+
ctx.projectRoot,
|
|
202
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
203
|
+
(resolvedRemoteFileAtSourcePath.source as any)?.[FILE_REF_PROP],
|
|
204
|
+
);
|
|
205
|
+
await fs.access(filePath);
|
|
206
|
+
} catch {
|
|
207
|
+
if (filePath) {
|
|
208
|
+
return {
|
|
209
|
+
success: false,
|
|
210
|
+
errorMessage: `File ${filePath} does not exist`,
|
|
211
|
+
};
|
|
212
|
+
} else {
|
|
213
|
+
return {
|
|
214
|
+
success: false,
|
|
215
|
+
errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const patFile = getPersonalAccessTokenPath(ctx.projectRoot);
|
|
221
|
+
try {
|
|
222
|
+
await fs.access(patFile);
|
|
223
|
+
} catch {
|
|
224
|
+
return {
|
|
225
|
+
success: false,
|
|
226
|
+
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`,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const parsedPatFile = parsePersonalAccessTokenFile(
|
|
231
|
+
await fs.readFile(patFile, "utf-8"),
|
|
232
|
+
);
|
|
233
|
+
if (!parsedPatFile.success) {
|
|
234
|
+
return {
|
|
235
|
+
success: false,
|
|
236
|
+
errorMessage: `Error parsing personal access token file: ${parsedPatFile.error}. You need to login again.`,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const { pat } = parsedPatFile.data;
|
|
240
|
+
|
|
241
|
+
if (ctx.remoteFiles[ctx.sourcePath]) {
|
|
242
|
+
console.log(
|
|
243
|
+
picocolors.yellow("⚠"),
|
|
244
|
+
`Remote file ${filePath} already uploaded`,
|
|
245
|
+
);
|
|
246
|
+
return { success: true };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!resolvedRemoteFileAtSourcePath.schema) {
|
|
250
|
+
return {
|
|
251
|
+
success: false,
|
|
252
|
+
errorMessage: `Cannot upload remote file: schema not found for ${ctx.sourcePath}`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const actualRemoteFileSource = resolvedRemoteFileAtSourcePath.source;
|
|
257
|
+
const fileSourceMetadata = Internal.isFile(actualRemoteFileSource)
|
|
258
|
+
? actualRemoteFileSource.metadata
|
|
259
|
+
: undefined;
|
|
260
|
+
const resolveRemoteFileSchema = resolvedRemoteFileAtSourcePath.schema;
|
|
261
|
+
|
|
262
|
+
if (!resolveRemoteFileSchema) {
|
|
263
|
+
return {
|
|
264
|
+
success: false,
|
|
265
|
+
errorMessage: `Could not resolve schema for remote file: ${ctx.sourcePath}`,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let publicProjectId = ctx.publicProjectId;
|
|
270
|
+
let remoteFileBuckets = ctx.remoteFileBuckets;
|
|
271
|
+
let remoteFilesCounter = ctx.remoteFilesCounter;
|
|
272
|
+
|
|
273
|
+
if (!publicProjectId || !remoteFileBuckets) {
|
|
274
|
+
let projectName = process.env.VAL_PROJECT;
|
|
275
|
+
if (!projectName) {
|
|
276
|
+
projectName = ctx.valConfigFile?.project;
|
|
277
|
+
}
|
|
278
|
+
if (!projectName) {
|
|
279
|
+
return {
|
|
280
|
+
success: false,
|
|
281
|
+
errorMessage:
|
|
282
|
+
"Project name not found. Set VAL_PROJECT environment variable or add project name to val.config",
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const settingsRes = await getSettings(projectName, { pat });
|
|
286
|
+
if (!settingsRes.success) {
|
|
287
|
+
return {
|
|
288
|
+
success: false,
|
|
289
|
+
errorMessage: `Could not get public project id: ${settingsRes.message}.`,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
publicProjectId = settingsRes.data.publicProjectId;
|
|
293
|
+
remoteFileBuckets = settingsRes.data.remoteFileBuckets.map((b) => b.bucket);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!publicProjectId) {
|
|
297
|
+
return {
|
|
298
|
+
success: false,
|
|
299
|
+
errorMessage: "Could not get public project id",
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!ctx.valConfigFile?.project) {
|
|
304
|
+
return {
|
|
305
|
+
success: false,
|
|
306
|
+
errorMessage: `Could not get project. Check that your val.config has the 'project' field set, or set it using the VAL_PROJECT environment variable`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (
|
|
311
|
+
resolveRemoteFileSchema.type !== "image" &&
|
|
312
|
+
resolveRemoteFileSchema.type !== "file"
|
|
313
|
+
) {
|
|
314
|
+
return {
|
|
315
|
+
success: false,
|
|
316
|
+
errorMessage: `The schema is the remote is neither image nor file: ${ctx.sourcePath}`,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
remoteFilesCounter += 1;
|
|
321
|
+
const bucket =
|
|
322
|
+
remoteFileBuckets[remoteFilesCounter % remoteFileBuckets.length];
|
|
323
|
+
|
|
324
|
+
if (!bucket) {
|
|
325
|
+
return {
|
|
326
|
+
success: false,
|
|
327
|
+
errorMessage: `Internal error: could not allocate a bucket for the remote file located at ${ctx.sourcePath}`,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let fileBuffer: Buffer;
|
|
332
|
+
try {
|
|
333
|
+
fileBuffer = await fs.readFile(filePath);
|
|
334
|
+
} catch (e) {
|
|
335
|
+
return {
|
|
336
|
+
success: false,
|
|
337
|
+
errorMessage: `Error reading file: ${e}`,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const relativeFilePath = path
|
|
342
|
+
.relative(ctx.projectRoot, filePath)
|
|
343
|
+
.split(path.sep)
|
|
344
|
+
.join("/") as `public/val/${string}`;
|
|
345
|
+
|
|
346
|
+
if (!relativeFilePath.startsWith("public/val/")) {
|
|
347
|
+
return {
|
|
348
|
+
success: false,
|
|
349
|
+
errorMessage: `File path must be within the public/val/ directory (e.g. public/val/path/to/file.txt). Got: ${relativeFilePath}`,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const fileHash = Internal.remote.getFileHash(fileBuffer);
|
|
354
|
+
const coreVersion = Internal.VERSION.core || "unknown";
|
|
355
|
+
const fileExt = getFileExt(filePath);
|
|
356
|
+
const schema = resolveRemoteFileSchema as
|
|
357
|
+
| SerializedImageSchema
|
|
358
|
+
| SerializedFileSchema;
|
|
359
|
+
const metadata = fileSourceMetadata;
|
|
360
|
+
const ref = Internal.remote.createRemoteRef(ctx.valRemoteHost, {
|
|
361
|
+
publicProjectId,
|
|
362
|
+
coreVersion,
|
|
363
|
+
bucket,
|
|
364
|
+
validationHash: Internal.remote.getValidationHash(
|
|
365
|
+
coreVersion,
|
|
366
|
+
schema,
|
|
367
|
+
fileExt,
|
|
368
|
+
metadata,
|
|
369
|
+
fileHash,
|
|
370
|
+
textEncoder,
|
|
371
|
+
),
|
|
372
|
+
fileHash,
|
|
373
|
+
filePath: relativeFilePath,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
console.log(picocolors.yellow("⚠"), `Uploading remote file: '${ref}'...`);
|
|
377
|
+
|
|
378
|
+
const remoteFileUpload = await uploadRemoteFile(
|
|
379
|
+
ctx.contentHostUrl,
|
|
380
|
+
ctx.valConfigFile.project,
|
|
381
|
+
bucket,
|
|
382
|
+
fileHash,
|
|
383
|
+
fileExt,
|
|
384
|
+
fileBuffer,
|
|
385
|
+
{ pat },
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
if (!remoteFileUpload.success) {
|
|
389
|
+
return {
|
|
390
|
+
success: false,
|
|
391
|
+
errorMessage: `Could not upload remote file: '${ref}'. Error: ${remoteFileUpload.error}`,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
console.log(
|
|
396
|
+
picocolors.green("✔"),
|
|
397
|
+
`Completed upload of remote file: '${ref}'`,
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
ctx.remoteFiles[ctx.sourcePath] = {
|
|
401
|
+
ref,
|
|
402
|
+
metadata: fileSourceMetadata,
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
success: true,
|
|
407
|
+
shouldApplyPatch: true,
|
|
408
|
+
publicProjectId,
|
|
409
|
+
remoteFileBuckets,
|
|
410
|
+
remoteFilesCounter,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function handleRemoteFileDownload(
|
|
415
|
+
ctx: FixHandlerContext,
|
|
416
|
+
): Promise<FixHandlerResult> {
|
|
417
|
+
if (ctx.fix) {
|
|
418
|
+
console.log(
|
|
419
|
+
picocolors.yellow("⚠"),
|
|
420
|
+
`Downloading remote file in ${ctx.sourcePath}...`,
|
|
421
|
+
);
|
|
422
|
+
return { success: true, shouldApplyPatch: true };
|
|
423
|
+
} else {
|
|
424
|
+
return {
|
|
425
|
+
success: false,
|
|
426
|
+
errorMessage: `Remote file ${ctx.sourcePath} needs to be downloaded (use --fix to download)`,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function handleRemoteFileCheck(): Promise<FixHandlerResult> {
|
|
432
|
+
// Skip - no action needed
|
|
433
|
+
return { success: true, shouldApplyPatch: true };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Helper function
|
|
437
|
+
async function checkKeyIsValid(
|
|
438
|
+
key: string,
|
|
439
|
+
sourcePath: string,
|
|
440
|
+
service: Service,
|
|
441
|
+
): Promise<{ error: false } | { error: true; message: string }> {
|
|
442
|
+
const [moduleFilePath, modulePath] =
|
|
443
|
+
Internal.splitModuleFilePathAndModulePath(sourcePath as SourcePath);
|
|
444
|
+
const keyOfModule = await service.get(moduleFilePath, modulePath, {
|
|
445
|
+
source: true,
|
|
446
|
+
schema: false,
|
|
447
|
+
validate: false,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const keyOfModuleSource = keyOfModule.source;
|
|
451
|
+
const keyOfModuleSchema = keyOfModule.schema;
|
|
452
|
+
if (keyOfModuleSchema && keyOfModuleSchema.type !== "record") {
|
|
453
|
+
return {
|
|
454
|
+
error: true,
|
|
455
|
+
message: `Expected key at ${sourcePath} to be of type 'record'`,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
if (
|
|
459
|
+
keyOfModuleSource &&
|
|
460
|
+
typeof keyOfModuleSource === "object" &&
|
|
461
|
+
key in keyOfModuleSource
|
|
462
|
+
) {
|
|
463
|
+
return { error: false };
|
|
464
|
+
}
|
|
465
|
+
if (!keyOfModuleSource || typeof keyOfModuleSource !== "object") {
|
|
466
|
+
return {
|
|
467
|
+
error: true,
|
|
468
|
+
message: `Expected ${sourcePath} to be a truthy object`,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
const alternatives = findSimilar(key, Object.keys(keyOfModuleSource));
|
|
472
|
+
return {
|
|
473
|
+
error: true,
|
|
474
|
+
message: `Key '${key}' does not exist in ${sourcePath}. Closest match: '${alternatives[0].target}'. Other similar: ${alternatives
|
|
475
|
+
.slice(1, 4)
|
|
476
|
+
.map((a) => `'${a.target}'`)
|
|
477
|
+
.join(", ")}${alternatives.length > 4 ? ", ..." : ""}`,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Check if a route is valid by scanning all router modules
|
|
483
|
+
* and validating against include/exclude patterns
|
|
484
|
+
*/
|
|
485
|
+
async function checkRouteIsValid(
|
|
486
|
+
route: string,
|
|
487
|
+
include: SerializedRegExpPattern | undefined,
|
|
488
|
+
exclude: SerializedRegExpPattern | undefined,
|
|
489
|
+
service: Service,
|
|
490
|
+
valFiles: string[],
|
|
491
|
+
): Promise<{ error: false } | { error: true; message: string }> {
|
|
492
|
+
// 1. Scan all val files to find modules with routers
|
|
493
|
+
const routerModules: Record<string, Record<string, unknown>> = {};
|
|
494
|
+
|
|
495
|
+
for (const file of valFiles) {
|
|
496
|
+
const moduleFilePath = `/${file}` as ModuleFilePath;
|
|
497
|
+
const valModule = await service.get(moduleFilePath, "" as ModulePath, {
|
|
498
|
+
source: true,
|
|
499
|
+
schema: true,
|
|
500
|
+
validate: false,
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Check if this module has a router defined
|
|
504
|
+
if (valModule.schema?.type === "record" && valModule.schema.router) {
|
|
505
|
+
if (valModule.source && typeof valModule.source === "object") {
|
|
506
|
+
routerModules[moduleFilePath] = valModule.source as Record<
|
|
507
|
+
string,
|
|
508
|
+
unknown
|
|
509
|
+
>;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// 2. Check if route exists in any router module
|
|
515
|
+
let foundInModule: string | null = null;
|
|
516
|
+
for (const [moduleFilePath, source] of Object.entries(routerModules)) {
|
|
517
|
+
if (route in source) {
|
|
518
|
+
foundInModule = moduleFilePath;
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (!foundInModule) {
|
|
524
|
+
// Route not found in any router module
|
|
525
|
+
let allRoutes = Object.values(routerModules).flatMap((source) =>
|
|
526
|
+
Object.keys(source),
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
if (allRoutes.length === 0) {
|
|
530
|
+
return {
|
|
531
|
+
error: true,
|
|
532
|
+
message: `Route '${route}' could not be validated: No router modules found in the project. Use s.record(...).router(...) to define router modules.`,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Filter routes by include/exclude patterns for suggestions
|
|
537
|
+
allRoutes = filterRoutesByPatterns(allRoutes, include, exclude);
|
|
538
|
+
|
|
539
|
+
const alternatives = findSimilar(route, allRoutes);
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
error: true,
|
|
543
|
+
message: `Route '${route}' does not exist in any router module. ${
|
|
544
|
+
alternatives.length > 0
|
|
545
|
+
? `Closest match: '${alternatives[0].target}'. Other similar: ${alternatives
|
|
546
|
+
.slice(1, 4)
|
|
547
|
+
.map((a) => `'${a.target}'`)
|
|
548
|
+
.join(", ")}${alternatives.length > 4 ? ", ..." : ""}`
|
|
549
|
+
: "No similar routes found."
|
|
550
|
+
}`,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// 3. Validate against include/exclude patterns
|
|
555
|
+
const patternValidation = validateRoutePatterns(route, include, exclude);
|
|
556
|
+
if (!patternValidation.valid) {
|
|
557
|
+
return {
|
|
558
|
+
error: true,
|
|
559
|
+
message: patternValidation.message,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return { error: false };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Handler for router:check-route validation fix
|
|
568
|
+
*/
|
|
569
|
+
async function handleRouteCheck(
|
|
570
|
+
ctx: FixHandlerContext,
|
|
571
|
+
): Promise<FixHandlerResult> {
|
|
572
|
+
const { sourcePath, validationError, service, valFiles } = ctx;
|
|
573
|
+
|
|
574
|
+
// Extract route and patterns from validation error value
|
|
575
|
+
const value = validationError.value as
|
|
576
|
+
| {
|
|
577
|
+
route: unknown;
|
|
578
|
+
include?: { source: string; flags: string };
|
|
579
|
+
exclude?: { source: string; flags: string };
|
|
580
|
+
}
|
|
581
|
+
| undefined;
|
|
582
|
+
|
|
583
|
+
if (!value || typeof value.route !== "string") {
|
|
584
|
+
return {
|
|
585
|
+
success: false,
|
|
586
|
+
errorMessage: `Invalid route value in validation error: ${JSON.stringify(value)}`,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const route = value.route;
|
|
591
|
+
|
|
592
|
+
// Check if the route is valid
|
|
593
|
+
const result = await checkRouteIsValid(
|
|
594
|
+
route,
|
|
595
|
+
value.include,
|
|
596
|
+
value.exclude,
|
|
597
|
+
service,
|
|
598
|
+
valFiles,
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
if (result.error) {
|
|
602
|
+
return {
|
|
603
|
+
success: false,
|
|
604
|
+
errorMessage: `${sourcePath}: ${result.message}`,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Route is valid - no fix needed
|
|
609
|
+
console.log(
|
|
610
|
+
picocolors.green("✓"),
|
|
611
|
+
`Route '${route}' is valid in`,
|
|
612
|
+
sourcePath,
|
|
613
|
+
);
|
|
614
|
+
return { success: true };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Fix handler registry
|
|
618
|
+
const currentFixHandlers: Record<ValidationFix, FixHandler> = {
|
|
619
|
+
"image:check-metadata": handleFileMetadata,
|
|
620
|
+
"image:add-metadata": handleFileMetadata,
|
|
621
|
+
"file:check-metadata": handleFileMetadata,
|
|
622
|
+
"file:add-metadata": handleFileMetadata,
|
|
623
|
+
"keyof:check-keys": handleKeyOfCheck,
|
|
624
|
+
"router:check-route": handleRouteCheck,
|
|
625
|
+
"image:upload-remote": handleRemoteFileUpload,
|
|
626
|
+
"file:upload-remote": handleRemoteFileUpload,
|
|
627
|
+
"image:download-remote": handleRemoteFileDownload,
|
|
628
|
+
"file:download-remote": handleRemoteFileDownload,
|
|
629
|
+
"image:check-remote": handleRemoteFileCheck,
|
|
630
|
+
"file:check-remote": handleRemoteFileCheck,
|
|
631
|
+
};
|
|
632
|
+
const deprecatedFixHandlers: Record<string, FixHandler> = {
|
|
633
|
+
"image:replace-metadata": handleFileMetadata,
|
|
634
|
+
};
|
|
635
|
+
const fixHandlers: Record<string, FixHandler> = {
|
|
636
|
+
...deprecatedFixHandlers,
|
|
637
|
+
...currentFixHandlers,
|
|
638
|
+
};
|
|
639
|
+
|
|
29
640
|
export async function validate({
|
|
30
641
|
root,
|
|
31
642
|
fix,
|
|
@@ -45,48 +656,6 @@ export async function validate({
|
|
|
45
656
|
),
|
|
46
657
|
);
|
|
47
658
|
const service = await createService(projectRoot, {});
|
|
48
|
-
const checkKeyIsValid = async (
|
|
49
|
-
key: string,
|
|
50
|
-
sourcePath: string,
|
|
51
|
-
): Promise<{ error: false } | { error: true; message: string }> => {
|
|
52
|
-
const [moduleFilePath, modulePath] =
|
|
53
|
-
Internal.splitModuleFilePathAndModulePath(sourcePath as SourcePath);
|
|
54
|
-
const keyOfModule = await service.get(moduleFilePath, modulePath, {
|
|
55
|
-
source: true,
|
|
56
|
-
schema: false,
|
|
57
|
-
validate: false,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const keyOfModuleSource = keyOfModule.source;
|
|
61
|
-
const keyOfModuleSchema = keyOfModule.schema;
|
|
62
|
-
if (keyOfModuleSchema && keyOfModuleSchema.type !== "record") {
|
|
63
|
-
return {
|
|
64
|
-
error: true,
|
|
65
|
-
message: `Expected key at ${sourcePath} to be of type 'record'`,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
if (
|
|
69
|
-
keyOfModuleSource &&
|
|
70
|
-
typeof keyOfModuleSource === "object" &&
|
|
71
|
-
key in keyOfModuleSource
|
|
72
|
-
) {
|
|
73
|
-
return { error: false };
|
|
74
|
-
}
|
|
75
|
-
if (!keyOfModuleSource || typeof keyOfModuleSource !== "object") {
|
|
76
|
-
return {
|
|
77
|
-
error: true,
|
|
78
|
-
message: `Expected ${sourcePath} to be a truthy object`,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
const alternatives = findSimilar(key, Object.keys(keyOfModuleSource));
|
|
82
|
-
return {
|
|
83
|
-
error: true,
|
|
84
|
-
message: `Key '${key}' does not exist in ${sourcePath}. Closest match: '${alternatives[0].target}'. Other similar: ${alternatives
|
|
85
|
-
.slice(1, 4)
|
|
86
|
-
.map((a) => `'${a.target}'`)
|
|
87
|
-
.join(", ")}${alternatives.length > 4 ? ", ..." : ""}`,
|
|
88
|
-
};
|
|
89
|
-
};
|
|
90
659
|
let prettier;
|
|
91
660
|
try {
|
|
92
661
|
prettier = (await import("prettier")).default;
|
|
@@ -102,7 +671,7 @@ export async function validate({
|
|
|
102
671
|
let errors = 0;
|
|
103
672
|
console.log(picocolors.greenBright(`Found ${valFiles.length} files...`));
|
|
104
673
|
let publicProjectId: string | undefined;
|
|
105
|
-
let didFix = false;
|
|
674
|
+
let didFix = false;
|
|
106
675
|
async function validateFile(file: string): Promise<number> {
|
|
107
676
|
const moduleFilePath = `/${file}` as ModuleFilePath; // TODO: check if this always works? (Windows?)
|
|
108
677
|
const start = Date.now();
|
|
@@ -115,7 +684,7 @@ export async function validate({
|
|
|
115
684
|
SourcePath,
|
|
116
685
|
{ ref: string; metadata?: Record<string, unknown> }
|
|
117
686
|
> = {};
|
|
118
|
-
let remoteFileBuckets: string[] |
|
|
687
|
+
let remoteFileBuckets: string[] | undefined = undefined;
|
|
119
688
|
let remoteFilesCounter = 0;
|
|
120
689
|
if (!valModule.errors) {
|
|
121
690
|
console.log(
|
|
@@ -133,376 +702,73 @@ export async function validate({
|
|
|
133
702
|
valModule.errors.validation,
|
|
134
703
|
)) {
|
|
135
704
|
for (const v of validationErrors) {
|
|
136
|
-
if (v.fixes
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
v.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
" (Expected value property 'sourcePath' to be a string - this is likely a bug in Val)",
|
|
204
|
-
);
|
|
205
|
-
errors += 1;
|
|
206
|
-
} else {
|
|
207
|
-
const res = await checkKeyIsValid(key, sourcePath);
|
|
208
|
-
if (res.error) {
|
|
209
|
-
console.log(picocolors.red("✘"), res.message);
|
|
210
|
-
errors += 1;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
} else {
|
|
214
|
-
console.log(
|
|
215
|
-
picocolors.red("✘"),
|
|
216
|
-
"Unexpected error in",
|
|
217
|
-
`${sourcePath}:`,
|
|
218
|
-
v.message,
|
|
219
|
-
" (Expected value to be an object with 'key' and 'sourcePath' properties - this is likely a bug in Val)",
|
|
220
|
-
);
|
|
221
|
-
errors += 1;
|
|
222
|
-
}
|
|
223
|
-
} else if (
|
|
224
|
-
v.fixes.includes("image:upload-remote") ||
|
|
225
|
-
v.fixes.includes("file:upload-remote")
|
|
226
|
-
) {
|
|
227
|
-
if (!fix) {
|
|
228
|
-
console.log(
|
|
229
|
-
picocolors.red("✘"),
|
|
230
|
-
`Remote file ${sourcePath} needs to be uploaded (use --fix to upload)`,
|
|
231
|
-
);
|
|
232
|
-
errors += 1;
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
const [, modulePath] =
|
|
236
|
-
Internal.splitModuleFilePathAndModulePath(
|
|
237
|
-
sourcePath as SourcePath,
|
|
238
|
-
);
|
|
239
|
-
if (valModule.source && valModule.schema) {
|
|
240
|
-
const resolvedRemoteFileAtSourcePath = Internal.resolvePath(
|
|
241
|
-
modulePath,
|
|
242
|
-
valModule.source,
|
|
243
|
-
valModule.schema,
|
|
244
|
-
);
|
|
245
|
-
let filePath: string | null = null;
|
|
246
|
-
try {
|
|
247
|
-
filePath = path.join(
|
|
248
|
-
projectRoot,
|
|
249
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
250
|
-
(resolvedRemoteFileAtSourcePath.source as any)?.[
|
|
251
|
-
FILE_REF_PROP
|
|
252
|
-
],
|
|
253
|
-
);
|
|
254
|
-
await fs.access(filePath);
|
|
255
|
-
} catch {
|
|
256
|
-
if (filePath) {
|
|
257
|
-
console.log(
|
|
258
|
-
picocolors.red("✘"),
|
|
259
|
-
`File ${filePath} does not exist`,
|
|
260
|
-
);
|
|
261
|
-
} else {
|
|
262
|
-
console.log(
|
|
263
|
-
picocolors.red("✘"),
|
|
264
|
-
`Expected file to be defined at: ${sourcePath} but no file was found`,
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
errors += 1;
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
const patFile = getPersonalAccessTokenPath(projectRoot);
|
|
271
|
-
try {
|
|
272
|
-
await fs.access(patFile);
|
|
273
|
-
} catch {
|
|
274
|
-
// TODO: display this error only once:
|
|
275
|
-
console.log(
|
|
276
|
-
picocolors.red("✘"),
|
|
277
|
-
`File: ${path.join(projectRoot, 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`,
|
|
278
|
-
);
|
|
279
|
-
errors += 1;
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const parsedPatFile = parsePersonalAccessTokenFile(
|
|
284
|
-
await fs.readFile(patFile, "utf-8"),
|
|
285
|
-
);
|
|
286
|
-
if (!parsedPatFile.success) {
|
|
287
|
-
console.log(
|
|
288
|
-
picocolors.red("✘"),
|
|
289
|
-
`Error parsing personal access token file: ${parsedPatFile.error}. You need to login again.`,
|
|
290
|
-
);
|
|
291
|
-
errors += 1;
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
const { pat } = parsedPatFile.data;
|
|
295
|
-
|
|
296
|
-
if (remoteFiles[sourcePath as SourcePath]) {
|
|
297
|
-
console.log(
|
|
298
|
-
picocolors.yellow("⚠"),
|
|
299
|
-
`Remote file ${filePath} already uploaded`,
|
|
300
|
-
);
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
// TODO: parallelize uploading files
|
|
304
|
-
if (!resolvedRemoteFileAtSourcePath.schema) {
|
|
305
|
-
console.log(
|
|
306
|
-
picocolors.red("✘"),
|
|
307
|
-
`Cannot upload remote file: schema not found for ${sourcePath}`,
|
|
308
|
-
);
|
|
309
|
-
errors += 1;
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const actualRemoteFileSource =
|
|
314
|
-
resolvedRemoteFileAtSourcePath.source;
|
|
315
|
-
const fileSourceMetadata = Internal.isFile(
|
|
316
|
-
actualRemoteFileSource,
|
|
317
|
-
)
|
|
318
|
-
? actualRemoteFileSource.metadata
|
|
319
|
-
: undefined;
|
|
320
|
-
const resolveRemoteFileSchema =
|
|
321
|
-
resolvedRemoteFileAtSourcePath.schema;
|
|
322
|
-
if (!resolveRemoteFileSchema) {
|
|
323
|
-
console.log(
|
|
324
|
-
picocolors.red("✘"),
|
|
325
|
-
`Could not resolve schema for remote file: ${sourcePath}`,
|
|
326
|
-
);
|
|
327
|
-
errors += 1;
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
if (!publicProjectId || !remoteFileBuckets) {
|
|
331
|
-
let projectName = process.env.VAL_PROJECT;
|
|
332
|
-
if (!projectName) {
|
|
333
|
-
projectName = valConfigFile?.project;
|
|
334
|
-
}
|
|
335
|
-
if (!projectName) {
|
|
336
|
-
console.log(
|
|
337
|
-
picocolors.red("✘"),
|
|
338
|
-
"Project name not found. Set VAL_PROJECT environment variable or add project name to val.config",
|
|
339
|
-
);
|
|
340
|
-
errors += 1;
|
|
341
|
-
continue;
|
|
342
|
-
}
|
|
343
|
-
const settingsRes = await getSettings(projectName, {
|
|
344
|
-
pat,
|
|
345
|
-
});
|
|
346
|
-
if (!settingsRes.success) {
|
|
347
|
-
console.log(
|
|
348
|
-
picocolors.red("✘"),
|
|
349
|
-
`Could not get public project id: ${settingsRes.message}.`,
|
|
350
|
-
);
|
|
351
|
-
errors += 1;
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
publicProjectId = settingsRes.data.publicProjectId;
|
|
355
|
-
remoteFileBuckets =
|
|
356
|
-
settingsRes.data.remoteFileBuckets.map((b) => b.bucket);
|
|
357
|
-
}
|
|
358
|
-
if (!publicProjectId) {
|
|
359
|
-
console.log(
|
|
360
|
-
picocolors.red("✘"),
|
|
361
|
-
"Could not get public project id",
|
|
362
|
-
);
|
|
363
|
-
errors += 1;
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
if (!valConfigFile?.project) {
|
|
367
|
-
console.log(
|
|
368
|
-
picocolors.red("✘"),
|
|
369
|
-
`Could not get project. Check that your val.config has the 'project' field set, or set it using the VAL_PROJECT environment variable`,
|
|
370
|
-
);
|
|
371
|
-
errors += 1;
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
if (
|
|
375
|
-
resolveRemoteFileSchema.type !== "image" &&
|
|
376
|
-
resolveRemoteFileSchema.type !== "file"
|
|
377
|
-
) {
|
|
378
|
-
console.log(
|
|
379
|
-
picocolors.red("✘"),
|
|
380
|
-
`The schema is the remote is neither image nor file: ${sourcePath}`,
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
remoteFilesCounter += 1;
|
|
384
|
-
const bucket =
|
|
385
|
-
remoteFileBuckets[
|
|
386
|
-
remoteFilesCounter % remoteFileBuckets.length
|
|
387
|
-
];
|
|
388
|
-
if (!bucket) {
|
|
389
|
-
console.log(
|
|
390
|
-
picocolors.red("✘"),
|
|
391
|
-
`Internal error: could not allocate a bucket for the remote file located at ${sourcePath}`,
|
|
392
|
-
);
|
|
393
|
-
errors += 1;
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
let fileBuffer: Buffer;
|
|
397
|
-
try {
|
|
398
|
-
fileBuffer = await fs.readFile(filePath);
|
|
399
|
-
} catch (e) {
|
|
400
|
-
console.log(
|
|
401
|
-
picocolors.red("✘"),
|
|
402
|
-
`Error reading file: ${e}`,
|
|
403
|
-
);
|
|
404
|
-
errors += 1;
|
|
405
|
-
continue;
|
|
406
|
-
}
|
|
407
|
-
const relativeFilePath = path
|
|
408
|
-
.relative(projectRoot, filePath)
|
|
409
|
-
.split(path.sep)
|
|
410
|
-
.join("/") as `public/val/${string}`;
|
|
411
|
-
if (!relativeFilePath.startsWith("public/val/")) {
|
|
412
|
-
console.log(
|
|
413
|
-
picocolors.red("✘"),
|
|
414
|
-
`File path must be within the public/val/ directory (e.g. public/val/path/to/file.txt). Got: ${relativeFilePath}`,
|
|
415
|
-
);
|
|
416
|
-
errors += 1;
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const fileHash = Internal.remote.getFileHash(fileBuffer);
|
|
421
|
-
const coreVersion = Internal.VERSION.core || "unknown";
|
|
422
|
-
const fileExt = getFileExt(filePath);
|
|
423
|
-
const schema = resolveRemoteFileSchema as
|
|
424
|
-
| SerializedImageSchema
|
|
425
|
-
| SerializedFileSchema;
|
|
426
|
-
const metadata = fileSourceMetadata;
|
|
427
|
-
const ref = Internal.remote.createRemoteRef(valRemoteHost, {
|
|
428
|
-
publicProjectId,
|
|
429
|
-
coreVersion,
|
|
430
|
-
bucket,
|
|
431
|
-
validationHash: Internal.remote.getValidationHash(
|
|
432
|
-
coreVersion,
|
|
433
|
-
schema,
|
|
434
|
-
fileExt,
|
|
435
|
-
metadata,
|
|
436
|
-
fileHash,
|
|
437
|
-
textEncoder,
|
|
438
|
-
),
|
|
439
|
-
fileHash,
|
|
440
|
-
filePath: relativeFilePath,
|
|
441
|
-
});
|
|
442
|
-
console.log(
|
|
443
|
-
picocolors.yellow("⚠"),
|
|
444
|
-
`Uploading remote file: '${ref}'...`,
|
|
445
|
-
);
|
|
446
|
-
|
|
447
|
-
const remoteFileUpload = await uploadRemoteFile(
|
|
448
|
-
contentHostUrl,
|
|
449
|
-
valConfigFile.project,
|
|
450
|
-
bucket,
|
|
451
|
-
fileHash,
|
|
452
|
-
fileExt,
|
|
453
|
-
fileBuffer,
|
|
454
|
-
{ pat },
|
|
455
|
-
);
|
|
456
|
-
if (!remoteFileUpload.success) {
|
|
457
|
-
console.log(
|
|
458
|
-
picocolors.red("✘"),
|
|
459
|
-
`Could not upload remote file: '${ref}'. Error: ${remoteFileUpload.error}`,
|
|
460
|
-
);
|
|
461
|
-
errors += 1;
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
console.log(
|
|
465
|
-
picocolors.green("✔"),
|
|
466
|
-
`Completed upload of remote file: '${ref}'`,
|
|
467
|
-
);
|
|
468
|
-
remoteFiles[sourcePath as SourcePath] = {
|
|
469
|
-
ref,
|
|
470
|
-
metadata: fileSourceMetadata,
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
} else if (
|
|
474
|
-
v.fixes.includes("image:download-remote") ||
|
|
475
|
-
v.fixes.includes("file:download-remote")
|
|
476
|
-
) {
|
|
477
|
-
if (fix) {
|
|
478
|
-
console.log(
|
|
479
|
-
picocolors.yellow("⚠"),
|
|
480
|
-
`Downloading remote file in ${sourcePath}...`,
|
|
481
|
-
);
|
|
482
|
-
} else {
|
|
483
|
-
console.log(
|
|
484
|
-
picocolors.red("✘"),
|
|
485
|
-
`Remote file ${sourcePath} needs to be downloaded (use --fix to download)`,
|
|
486
|
-
);
|
|
487
|
-
errors += 1;
|
|
488
|
-
continue;
|
|
489
|
-
}
|
|
490
|
-
} else if (
|
|
491
|
-
v.fixes.includes("image:check-remote") ||
|
|
492
|
-
v.fixes.includes("file:check-remote")
|
|
493
|
-
) {
|
|
494
|
-
// skip
|
|
495
|
-
} else {
|
|
496
|
-
console.log(
|
|
497
|
-
picocolors.red("✘"),
|
|
498
|
-
"Unknown fix",
|
|
499
|
-
v.fixes,
|
|
500
|
-
"for",
|
|
501
|
-
sourcePath,
|
|
502
|
-
);
|
|
503
|
-
errors += 1;
|
|
504
|
-
continue;
|
|
505
|
-
}
|
|
705
|
+
if (!v.fixes || v.fixes.length === 0) {
|
|
706
|
+
// No fixes available - just report error
|
|
707
|
+
errors += 1;
|
|
708
|
+
console.log(
|
|
709
|
+
picocolors.red("✘"),
|
|
710
|
+
"Got error in",
|
|
711
|
+
`${sourcePath}:`,
|
|
712
|
+
v.message,
|
|
713
|
+
);
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Find and execute appropriate handler
|
|
718
|
+
const fixType = v.fixes[0]; // Take first fix
|
|
719
|
+
const handler = fixHandlers[fixType];
|
|
720
|
+
|
|
721
|
+
if (!handler) {
|
|
722
|
+
console.log(
|
|
723
|
+
picocolors.red("✘"),
|
|
724
|
+
"Unknown fix",
|
|
725
|
+
v.fixes,
|
|
726
|
+
"for",
|
|
727
|
+
sourcePath,
|
|
728
|
+
);
|
|
729
|
+
errors += 1;
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Execute handler
|
|
734
|
+
const result = await handler({
|
|
735
|
+
sourcePath: sourcePath as SourcePath,
|
|
736
|
+
validationError: v,
|
|
737
|
+
valModule,
|
|
738
|
+
projectRoot,
|
|
739
|
+
fix: !!fix,
|
|
740
|
+
service,
|
|
741
|
+
valFiles,
|
|
742
|
+
moduleFilePath,
|
|
743
|
+
file,
|
|
744
|
+
remoteFiles,
|
|
745
|
+
publicProjectId,
|
|
746
|
+
remoteFileBuckets,
|
|
747
|
+
remoteFilesCounter,
|
|
748
|
+
valRemoteHost,
|
|
749
|
+
contentHostUrl,
|
|
750
|
+
valConfigFile: valConfigFile ?? undefined,
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
// Update shared state from handler result
|
|
754
|
+
if (result.publicProjectId !== undefined) {
|
|
755
|
+
publicProjectId = result.publicProjectId;
|
|
756
|
+
}
|
|
757
|
+
if (result.remoteFileBuckets !== undefined) {
|
|
758
|
+
remoteFileBuckets = result.remoteFileBuckets;
|
|
759
|
+
}
|
|
760
|
+
if (result.remoteFilesCounter !== undefined) {
|
|
761
|
+
remoteFilesCounter = result.remoteFilesCounter;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (!result.success) {
|
|
765
|
+
console.log(picocolors.red("✘"), result.errorMessage);
|
|
766
|
+
errors += 1;
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Apply patch if needed
|
|
771
|
+
if (result.shouldApplyPatch) {
|
|
506
772
|
const fixPatch = await createFixPatch(
|
|
507
773
|
{ projectRoot, remoteHost: valRemoteHost },
|
|
508
774
|
!!fix,
|
|
@@ -512,6 +778,7 @@ export async function validate({
|
|
|
512
778
|
valModule.source,
|
|
513
779
|
valModule.schema,
|
|
514
780
|
);
|
|
781
|
+
|
|
515
782
|
if (fix && fixPatch?.patch && fixPatch?.patch.length > 0) {
|
|
516
783
|
await service.patch(moduleFilePath, fixPatch.patch);
|
|
517
784
|
didFix = true;
|
|
@@ -522,6 +789,7 @@ export async function validate({
|
|
|
522
789
|
sourcePath,
|
|
523
790
|
);
|
|
524
791
|
}
|
|
792
|
+
|
|
525
793
|
fixPatch?.remainingErrors?.forEach((e) => {
|
|
526
794
|
errors += 1;
|
|
527
795
|
console.log(
|
|
@@ -533,14 +801,6 @@ export async function validate({
|
|
|
533
801
|
e.message,
|
|
534
802
|
);
|
|
535
803
|
});
|
|
536
|
-
} else {
|
|
537
|
-
errors += 1;
|
|
538
|
-
console.log(
|
|
539
|
-
picocolors.red("✘"),
|
|
540
|
-
"Got error in",
|
|
541
|
-
`${sourcePath}:`,
|
|
542
|
-
v.message,
|
|
543
|
-
);
|
|
544
804
|
}
|
|
545
805
|
}
|
|
546
806
|
}
|