@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
|
@@ -5,6 +5,7 @@ var chalk = require('chalk');
|
|
|
5
5
|
var path = require('path');
|
|
6
6
|
var server = require('@valbuild/server');
|
|
7
7
|
var core = require('@valbuild/core');
|
|
8
|
+
var internal = require('@valbuild/shared/internal');
|
|
8
9
|
var fastGlob = require('fast-glob');
|
|
9
10
|
var picocolors = require('picocolors');
|
|
10
11
|
var fs = require('fs/promises');
|
|
@@ -122,48 +123,446 @@ function getFileExt(filePath) {
|
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
const textEncoder = new TextEncoder();
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
126
|
+
|
|
127
|
+
// Types for handler system
|
|
128
|
+
|
|
129
|
+
// Handler functions
|
|
130
|
+
async function handleFileMetadata(ctx) {
|
|
131
|
+
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
|
|
132
|
+
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
133
|
+
return {
|
|
134
|
+
success: false,
|
|
135
|
+
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const fileSource = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
|
|
139
|
+
let filePath = null;
|
|
140
|
+
try {
|
|
141
|
+
var _fileSource$source;
|
|
142
|
+
filePath = path__default["default"].join(ctx.projectRoot, // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
143
|
+
(_fileSource$source = fileSource.source) === null || _fileSource$source === void 0 ? void 0 : _fileSource$source[core.FILE_REF_PROP]);
|
|
144
|
+
await fs__default["default"].access(filePath);
|
|
145
|
+
} catch {
|
|
146
|
+
if (filePath) {
|
|
145
147
|
return {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
success: false,
|
|
149
|
+
errorMessage: `File ${filePath} does not exist`
|
|
150
|
+
};
|
|
151
|
+
} else {
|
|
152
|
+
return {
|
|
153
|
+
success: false,
|
|
154
|
+
errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
success: true,
|
|
160
|
+
shouldApplyPatch: true
|
|
161
|
+
};
|
|
162
|
+
}
|
|
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);
|
|
187
|
+
if (res.error) {
|
|
188
|
+
return {
|
|
189
|
+
success: false,
|
|
190
|
+
errorMessage: res.message
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
success: true
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
async function handleRemoteFileUpload(ctx) {
|
|
198
|
+
var _ctx$valConfigFile2;
|
|
199
|
+
if (!ctx.fix) {
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
errorMessage: `Remote file ${ctx.sourcePath} needs to be uploaded (use --fix to upload)`
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
|
|
206
|
+
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
207
|
+
return {
|
|
208
|
+
success: false,
|
|
209
|
+
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const resolvedRemoteFileAtSourcePath = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
|
|
213
|
+
let filePath = null;
|
|
214
|
+
try {
|
|
215
|
+
var _resolvedRemoteFileAt;
|
|
216
|
+
filePath = path__default["default"].join(ctx.projectRoot, // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
217
|
+
(_resolvedRemoteFileAt = resolvedRemoteFileAtSourcePath.source) === null || _resolvedRemoteFileAt === void 0 ? void 0 : _resolvedRemoteFileAt[core.FILE_REF_PROP]);
|
|
218
|
+
await fs__default["default"].access(filePath);
|
|
219
|
+
} catch {
|
|
220
|
+
if (filePath) {
|
|
221
|
+
return {
|
|
222
|
+
success: false,
|
|
223
|
+
errorMessage: `File ${filePath} does not exist`
|
|
148
224
|
};
|
|
225
|
+
} else {
|
|
226
|
+
return {
|
|
227
|
+
success: false,
|
|
228
|
+
errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const patFile = server.getPersonalAccessTokenPath(ctx.projectRoot);
|
|
233
|
+
try {
|
|
234
|
+
await fs__default["default"].access(patFile);
|
|
235
|
+
} catch {
|
|
236
|
+
return {
|
|
237
|
+
success: false,
|
|
238
|
+
errorMessage: `File: ${path__default["default"].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`
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const parsedPatFile = server.parsePersonalAccessTokenFile(await fs__default["default"].readFile(patFile, "utf-8"));
|
|
242
|
+
if (!parsedPatFile.success) {
|
|
243
|
+
return {
|
|
244
|
+
success: false,
|
|
245
|
+
errorMessage: `Error parsing personal access token file: ${parsedPatFile.error}. You need to login again.`
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const {
|
|
249
|
+
pat
|
|
250
|
+
} = parsedPatFile.data;
|
|
251
|
+
if (ctx.remoteFiles[ctx.sourcePath]) {
|
|
252
|
+
console.log(picocolors__default["default"].yellow("⚠"), `Remote file ${filePath} already uploaded`);
|
|
253
|
+
return {
|
|
254
|
+
success: true
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
if (!resolvedRemoteFileAtSourcePath.schema) {
|
|
258
|
+
return {
|
|
259
|
+
success: false,
|
|
260
|
+
errorMessage: `Cannot upload remote file: schema not found for ${ctx.sourcePath}`
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const actualRemoteFileSource = resolvedRemoteFileAtSourcePath.source;
|
|
264
|
+
const fileSourceMetadata = core.Internal.isFile(actualRemoteFileSource) ? actualRemoteFileSource.metadata : undefined;
|
|
265
|
+
const resolveRemoteFileSchema = resolvedRemoteFileAtSourcePath.schema;
|
|
266
|
+
if (!resolveRemoteFileSchema) {
|
|
267
|
+
return {
|
|
268
|
+
success: false,
|
|
269
|
+
errorMessage: `Could not resolve schema for remote file: ${ctx.sourcePath}`
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
let publicProjectId = ctx.publicProjectId;
|
|
273
|
+
let remoteFileBuckets = ctx.remoteFileBuckets;
|
|
274
|
+
let remoteFilesCounter = ctx.remoteFilesCounter;
|
|
275
|
+
if (!publicProjectId || !remoteFileBuckets) {
|
|
276
|
+
let projectName = process.env.VAL_PROJECT;
|
|
277
|
+
if (!projectName) {
|
|
278
|
+
var _ctx$valConfigFile;
|
|
279
|
+
projectName = (_ctx$valConfigFile = ctx.valConfigFile) === null || _ctx$valConfigFile === void 0 ? void 0 : _ctx$valConfigFile.project;
|
|
149
280
|
}
|
|
150
|
-
if (
|
|
281
|
+
if (!projectName) {
|
|
151
282
|
return {
|
|
152
|
-
|
|
283
|
+
success: false,
|
|
284
|
+
errorMessage: "Project name not found. Set VAL_PROJECT environment variable or add project name to val.config"
|
|
153
285
|
};
|
|
154
286
|
}
|
|
155
|
-
|
|
287
|
+
const settingsRes = await server.getSettings(projectName, {
|
|
288
|
+
pat
|
|
289
|
+
});
|
|
290
|
+
if (!settingsRes.success) {
|
|
291
|
+
return {
|
|
292
|
+
success: false,
|
|
293
|
+
errorMessage: `Could not get public project id: ${settingsRes.message}.`
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
publicProjectId = settingsRes.data.publicProjectId;
|
|
297
|
+
remoteFileBuckets = settingsRes.data.remoteFileBuckets.map(b => b.bucket);
|
|
298
|
+
}
|
|
299
|
+
if (!publicProjectId) {
|
|
300
|
+
return {
|
|
301
|
+
success: false,
|
|
302
|
+
errorMessage: "Could not get public project id"
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (!((_ctx$valConfigFile2 = ctx.valConfigFile) !== null && _ctx$valConfigFile2 !== void 0 && _ctx$valConfigFile2.project)) {
|
|
306
|
+
return {
|
|
307
|
+
success: false,
|
|
308
|
+
errorMessage: `Could not get project. Check that your val.config has the 'project' field set, or set it using the VAL_PROJECT environment variable`
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
if (resolveRemoteFileSchema.type !== "image" && resolveRemoteFileSchema.type !== "file") {
|
|
312
|
+
return {
|
|
313
|
+
success: false,
|
|
314
|
+
errorMessage: `The schema is the remote is neither image nor file: ${ctx.sourcePath}`
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
remoteFilesCounter += 1;
|
|
318
|
+
const bucket = remoteFileBuckets[remoteFilesCounter % remoteFileBuckets.length];
|
|
319
|
+
if (!bucket) {
|
|
320
|
+
return {
|
|
321
|
+
success: false,
|
|
322
|
+
errorMessage: `Internal error: could not allocate a bucket for the remote file located at ${ctx.sourcePath}`
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
let fileBuffer;
|
|
326
|
+
try {
|
|
327
|
+
fileBuffer = await fs__default["default"].readFile(filePath);
|
|
328
|
+
} catch (e) {
|
|
329
|
+
return {
|
|
330
|
+
success: false,
|
|
331
|
+
errorMessage: `Error reading file: ${e}`
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const relativeFilePath = path__default["default"].relative(ctx.projectRoot, filePath).split(path__default["default"].sep).join("/");
|
|
335
|
+
if (!relativeFilePath.startsWith("public/val/")) {
|
|
336
|
+
return {
|
|
337
|
+
success: false,
|
|
338
|
+
errorMessage: `File path must be within the public/val/ directory (e.g. public/val/path/to/file.txt). Got: ${relativeFilePath}`
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
const fileHash = core.Internal.remote.getFileHash(fileBuffer);
|
|
342
|
+
const coreVersion = core.Internal.VERSION.core || "unknown";
|
|
343
|
+
const fileExt = getFileExt(filePath);
|
|
344
|
+
const schema = resolveRemoteFileSchema;
|
|
345
|
+
const metadata = fileSourceMetadata;
|
|
346
|
+
const ref = core.Internal.remote.createRemoteRef(ctx.valRemoteHost, {
|
|
347
|
+
publicProjectId,
|
|
348
|
+
coreVersion,
|
|
349
|
+
bucket,
|
|
350
|
+
validationHash: core.Internal.remote.getValidationHash(coreVersion, schema, fileExt, metadata, fileHash, textEncoder),
|
|
351
|
+
fileHash,
|
|
352
|
+
filePath: relativeFilePath
|
|
353
|
+
});
|
|
354
|
+
console.log(picocolors__default["default"].yellow("⚠"), `Uploading remote file: '${ref}'...`);
|
|
355
|
+
const remoteFileUpload = await server.uploadRemoteFile(ctx.contentHostUrl, ctx.valConfigFile.project, bucket, fileHash, fileExt, fileBuffer, {
|
|
356
|
+
pat
|
|
357
|
+
});
|
|
358
|
+
if (!remoteFileUpload.success) {
|
|
359
|
+
return {
|
|
360
|
+
success: false,
|
|
361
|
+
errorMessage: `Could not upload remote file: '${ref}'. Error: ${remoteFileUpload.error}`
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
console.log(picocolors__default["default"].green("✔"), `Completed upload of remote file: '${ref}'`);
|
|
365
|
+
ctx.remoteFiles[ctx.sourcePath] = {
|
|
366
|
+
ref,
|
|
367
|
+
metadata: fileSourceMetadata
|
|
368
|
+
};
|
|
369
|
+
return {
|
|
370
|
+
success: true,
|
|
371
|
+
shouldApplyPatch: true,
|
|
372
|
+
publicProjectId,
|
|
373
|
+
remoteFileBuckets,
|
|
374
|
+
remoteFilesCounter
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
async function handleRemoteFileDownload(ctx) {
|
|
378
|
+
if (ctx.fix) {
|
|
379
|
+
console.log(picocolors__default["default"].yellow("⚠"), `Downloading remote file in ${ctx.sourcePath}...`);
|
|
380
|
+
return {
|
|
381
|
+
success: true,
|
|
382
|
+
shouldApplyPatch: true
|
|
383
|
+
};
|
|
384
|
+
} else {
|
|
385
|
+
return {
|
|
386
|
+
success: false,
|
|
387
|
+
errorMessage: `Remote file ${ctx.sourcePath} needs to be downloaded (use --fix to download)`
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
async function handleRemoteFileCheck() {
|
|
392
|
+
// Skip - no action needed
|
|
393
|
+
return {
|
|
394
|
+
success: true,
|
|
395
|
+
shouldApplyPatch: true
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Helper function
|
|
400
|
+
async function checkKeyIsValid(key, sourcePath, service) {
|
|
401
|
+
const [moduleFilePath, modulePath] = core.Internal.splitModuleFilePathAndModulePath(sourcePath);
|
|
402
|
+
const keyOfModule = await service.get(moduleFilePath, modulePath, {
|
|
403
|
+
source: true,
|
|
404
|
+
schema: false,
|
|
405
|
+
validate: false
|
|
406
|
+
});
|
|
407
|
+
const keyOfModuleSource = keyOfModule.source;
|
|
408
|
+
const keyOfModuleSchema = keyOfModule.schema;
|
|
409
|
+
if (keyOfModuleSchema && keyOfModuleSchema.type !== "record") {
|
|
410
|
+
return {
|
|
411
|
+
error: true,
|
|
412
|
+
message: `Expected key at ${sourcePath} to be of type 'record'`
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
if (keyOfModuleSource && typeof keyOfModuleSource === "object" && key in keyOfModuleSource) {
|
|
416
|
+
return {
|
|
417
|
+
error: false
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
if (!keyOfModuleSource || typeof keyOfModuleSource !== "object") {
|
|
421
|
+
return {
|
|
422
|
+
error: true,
|
|
423
|
+
message: `Expected ${sourcePath} to be a truthy object`
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
const alternatives = findSimilar(key, Object.keys(keyOfModuleSource));
|
|
427
|
+
return {
|
|
428
|
+
error: true,
|
|
429
|
+
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 ? ", ..." : ""}`
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Check if a route is valid by scanning all router modules
|
|
435
|
+
* and validating against include/exclude patterns
|
|
436
|
+
*/
|
|
437
|
+
async function checkRouteIsValid(route, include, exclude, service, valFiles) {
|
|
438
|
+
// 1. Scan all val files to find modules with routers
|
|
439
|
+
const routerModules = {};
|
|
440
|
+
for (const file of valFiles) {
|
|
441
|
+
var _valModule$schema;
|
|
442
|
+
const moduleFilePath = `/${file}`;
|
|
443
|
+
const valModule = await service.get(moduleFilePath, "", {
|
|
444
|
+
source: true,
|
|
445
|
+
schema: true,
|
|
446
|
+
validate: false
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// Check if this module has a router defined
|
|
450
|
+
if (((_valModule$schema = valModule.schema) === null || _valModule$schema === void 0 ? void 0 : _valModule$schema.type) === "record" && valModule.schema.router) {
|
|
451
|
+
if (valModule.source && typeof valModule.source === "object") {
|
|
452
|
+
routerModules[moduleFilePath] = valModule.source;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// 2. Check if route exists in any router module
|
|
458
|
+
let foundInModule = null;
|
|
459
|
+
for (const [moduleFilePath, source] of Object.entries(routerModules)) {
|
|
460
|
+
if (route in source) {
|
|
461
|
+
foundInModule = moduleFilePath;
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (!foundInModule) {
|
|
466
|
+
// Route not found in any router module
|
|
467
|
+
let allRoutes = Object.values(routerModules).flatMap(source => Object.keys(source));
|
|
468
|
+
if (allRoutes.length === 0) {
|
|
156
469
|
return {
|
|
157
470
|
error: true,
|
|
158
|
-
message: `
|
|
471
|
+
message: `Route '${route}' could not be validated: No router modules found in the project. Use s.record(...).router(...) to define router modules.`
|
|
159
472
|
};
|
|
160
473
|
}
|
|
161
|
-
|
|
474
|
+
|
|
475
|
+
// Filter routes by include/exclude patterns for suggestions
|
|
476
|
+
allRoutes = internal.filterRoutesByPatterns(allRoutes, include, exclude);
|
|
477
|
+
const alternatives = findSimilar(route, allRoutes);
|
|
162
478
|
return {
|
|
163
479
|
error: true,
|
|
164
|
-
message: `
|
|
480
|
+
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."}`
|
|
165
481
|
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// 3. Validate against include/exclude patterns
|
|
485
|
+
const patternValidation = internal.validateRoutePatterns(route, include, exclude);
|
|
486
|
+
if (!patternValidation.valid) {
|
|
487
|
+
return {
|
|
488
|
+
error: true,
|
|
489
|
+
message: patternValidation.message
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
error: false
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Handler for router:check-route validation fix
|
|
499
|
+
*/
|
|
500
|
+
async function handleRouteCheck(ctx) {
|
|
501
|
+
const {
|
|
502
|
+
sourcePath,
|
|
503
|
+
validationError,
|
|
504
|
+
service,
|
|
505
|
+
valFiles
|
|
506
|
+
} = ctx;
|
|
507
|
+
|
|
508
|
+
// Extract route and patterns from validation error value
|
|
509
|
+
const value = validationError.value;
|
|
510
|
+
if (!value || typeof value.route !== "string") {
|
|
511
|
+
return {
|
|
512
|
+
success: false,
|
|
513
|
+
errorMessage: `Invalid route value in validation error: ${JSON.stringify(value)}`
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
const route = value.route;
|
|
517
|
+
|
|
518
|
+
// Check if the route is valid
|
|
519
|
+
const result = await checkRouteIsValid(route, value.include, value.exclude, service, valFiles);
|
|
520
|
+
if (result.error) {
|
|
521
|
+
return {
|
|
522
|
+
success: false,
|
|
523
|
+
errorMessage: `${sourcePath}: ${result.message}`
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Route is valid - no fix needed
|
|
528
|
+
console.log(picocolors__default["default"].green("✓"), `Route '${route}' is valid in`, sourcePath);
|
|
529
|
+
return {
|
|
530
|
+
success: true
|
|
166
531
|
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Fix handler registry
|
|
535
|
+
const currentFixHandlers = {
|
|
536
|
+
"image:check-metadata": handleFileMetadata,
|
|
537
|
+
"image:add-metadata": handleFileMetadata,
|
|
538
|
+
"file:check-metadata": handleFileMetadata,
|
|
539
|
+
"file:add-metadata": handleFileMetadata,
|
|
540
|
+
"keyof:check-keys": handleKeyOfCheck,
|
|
541
|
+
"router:check-route": handleRouteCheck,
|
|
542
|
+
"image:upload-remote": handleRemoteFileUpload,
|
|
543
|
+
"file:upload-remote": handleRemoteFileUpload,
|
|
544
|
+
"image:download-remote": handleRemoteFileDownload,
|
|
545
|
+
"file:download-remote": handleRemoteFileDownload,
|
|
546
|
+
"image:check-remote": handleRemoteFileCheck,
|
|
547
|
+
"file:check-remote": handleRemoteFileCheck
|
|
548
|
+
};
|
|
549
|
+
const deprecatedFixHandlers = {
|
|
550
|
+
"image:replace-metadata": handleFileMetadata
|
|
551
|
+
};
|
|
552
|
+
const fixHandlers = {
|
|
553
|
+
...deprecatedFixHandlers,
|
|
554
|
+
...currentFixHandlers
|
|
555
|
+
};
|
|
556
|
+
async function validate({
|
|
557
|
+
root,
|
|
558
|
+
fix
|
|
559
|
+
}) {
|
|
560
|
+
const valRemoteHost = process.env.VAL_REMOTE_HOST || core.DEFAULT_VAL_REMOTE_HOST;
|
|
561
|
+
const contentHostUrl = process.env.VAL_CONTENT_URL || core.DEFAULT_CONTENT_HOST;
|
|
562
|
+
const projectRoot = root ? path__default["default"].resolve(root) : process.cwd();
|
|
563
|
+
const valConfigFile = (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
|
|
564
|
+
console.log(picocolors__default["default"].greenBright(`Validating project${valConfigFile !== null && valConfigFile !== void 0 && valConfigFile.project ? ` '${picocolors__default["default"].inverse(valConfigFile === null || valConfigFile === void 0 ? void 0 : valConfigFile.project)}'` : ""}...`));
|
|
565
|
+
const service = await server.createService(projectRoot, {});
|
|
167
566
|
let prettier;
|
|
168
567
|
try {
|
|
169
568
|
prettier = (await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('prettier')); })).default;
|
|
@@ -177,7 +576,7 @@ async function validate({
|
|
|
177
576
|
let errors = 0;
|
|
178
577
|
console.log(picocolors__default["default"].greenBright(`Found ${valFiles.length} files...`));
|
|
179
578
|
let publicProjectId;
|
|
180
|
-
let didFix = false;
|
|
579
|
+
let didFix = false;
|
|
181
580
|
async function validateFile(file) {
|
|
182
581
|
const moduleFilePath = `/${file}`; // TODO: check if this always works? (Windows?)
|
|
183
582
|
const start = Date.now();
|
|
@@ -187,7 +586,7 @@ async function validate({
|
|
|
187
586
|
validate: true
|
|
188
587
|
});
|
|
189
588
|
const remoteFiles = {};
|
|
190
|
-
let remoteFileBuckets =
|
|
589
|
+
let remoteFileBuckets = undefined;
|
|
191
590
|
let remoteFilesCounter = 0;
|
|
192
591
|
if (!valModule.errors) {
|
|
193
592
|
console.log(picocolors__default["default"].green("✔"), moduleFilePath, "is valid (" + (Date.now() - start) + "ms)");
|
|
@@ -199,208 +598,61 @@ async function validate({
|
|
|
199
598
|
if (valModule.errors.validation) {
|
|
200
599
|
for (const [sourcePath, validationErrors] of Object.entries(valModule.errors.validation)) {
|
|
201
600
|
for (const v of validationErrors) {
|
|
202
|
-
if (v.fixes
|
|
601
|
+
if (!v.fixes || v.fixes.length === 0) {
|
|
602
|
+
// No fixes available - just report error
|
|
603
|
+
errors += 1;
|
|
604
|
+
console.log(picocolors__default["default"].red("✘"), "Got error in", `${sourcePath}:`, v.message);
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Find and execute appropriate handler
|
|
609
|
+
const fixType = v.fixes[0]; // Take first fix
|
|
610
|
+
const handler = fixHandlers[fixType];
|
|
611
|
+
if (!handler) {
|
|
612
|
+
console.log(picocolors__default["default"].red("✘"), "Unknown fix", v.fixes, "for", sourcePath);
|
|
613
|
+
errors += 1;
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Execute handler
|
|
618
|
+
const result = await handler({
|
|
619
|
+
sourcePath: sourcePath,
|
|
620
|
+
validationError: v,
|
|
621
|
+
valModule,
|
|
622
|
+
projectRoot,
|
|
623
|
+
fix: !!fix,
|
|
624
|
+
service,
|
|
625
|
+
valFiles,
|
|
626
|
+
moduleFilePath,
|
|
627
|
+
file,
|
|
628
|
+
remoteFiles,
|
|
629
|
+
publicProjectId,
|
|
630
|
+
remoteFileBuckets,
|
|
631
|
+
remoteFilesCounter,
|
|
632
|
+
valRemoteHost,
|
|
633
|
+
contentHostUrl,
|
|
634
|
+
valConfigFile: valConfigFile ?? undefined
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// Update shared state from handler result
|
|
638
|
+
if (result.publicProjectId !== undefined) {
|
|
639
|
+
publicProjectId = result.publicProjectId;
|
|
640
|
+
}
|
|
641
|
+
if (result.remoteFileBuckets !== undefined) {
|
|
642
|
+
remoteFileBuckets = result.remoteFileBuckets;
|
|
643
|
+
}
|
|
644
|
+
if (result.remoteFilesCounter !== undefined) {
|
|
645
|
+
remoteFilesCounter = result.remoteFilesCounter;
|
|
646
|
+
}
|
|
647
|
+
if (!result.success) {
|
|
648
|
+
console.log(picocolors__default["default"].red("✘"), result.errorMessage);
|
|
649
|
+
errors += 1;
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Apply patch if needed
|
|
654
|
+
if (result.shouldApplyPatch) {
|
|
203
655
|
var _fixPatch$remainingEr;
|
|
204
|
-
if (v.fixes.includes("image:replace-metadata" // TODO: we can remove this now - we needed before because we changed the name of the fix from replace-metadata to check-metadata
|
|
205
|
-
) || v.fixes.includes("image:check-metadata") || v.fixes.includes("image:add-metadata") || v.fixes.includes("file:check-metadata") || v.fixes.includes("file:add-metadata")) {
|
|
206
|
-
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(sourcePath);
|
|
207
|
-
if (valModule.source && valModule.schema) {
|
|
208
|
-
const fileSource = core.Internal.resolvePath(modulePath, valModule.source, valModule.schema);
|
|
209
|
-
let filePath = null;
|
|
210
|
-
try {
|
|
211
|
-
var _fileSource$source;
|
|
212
|
-
filePath = path__default["default"].join(projectRoot, // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
213
|
-
(_fileSource$source = fileSource.source) === null || _fileSource$source === void 0 ? void 0 : _fileSource$source[core.FILE_REF_PROP]);
|
|
214
|
-
await fs__default["default"].access(filePath);
|
|
215
|
-
} catch {
|
|
216
|
-
if (filePath) {
|
|
217
|
-
console.log(picocolors__default["default"].red("✘"), `File ${filePath} does not exist`);
|
|
218
|
-
} else {
|
|
219
|
-
console.log(picocolors__default["default"].red("✘"), `Expected file to be defined at: ${sourcePath} but no file was found`);
|
|
220
|
-
}
|
|
221
|
-
errors += 1;
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
} else if (v.fixes.includes("keyof:check-keys")) {
|
|
226
|
-
if (v.value && typeof v.value === "object" && "key" in v.value && "sourcePath" in v.value) {
|
|
227
|
-
const {
|
|
228
|
-
key,
|
|
229
|
-
sourcePath
|
|
230
|
-
} = v.value;
|
|
231
|
-
if (typeof key !== "string") {
|
|
232
|
-
console.log(picocolors__default["default"].red("✘"), "Unexpected error in", `${sourcePath}:`, v.message, " (Expected value property 'key' to be a string - this is likely a bug in Val)");
|
|
233
|
-
errors += 1;
|
|
234
|
-
} else if (typeof sourcePath !== "string") {
|
|
235
|
-
console.log(picocolors__default["default"].red("✘"), "Unexpected error in", `${sourcePath}:`, v.message, " (Expected value property 'sourcePath' to be a string - this is likely a bug in Val)");
|
|
236
|
-
errors += 1;
|
|
237
|
-
} else {
|
|
238
|
-
const res = await checkKeyIsValid(key, sourcePath);
|
|
239
|
-
if (res.error) {
|
|
240
|
-
console.log(picocolors__default["default"].red("✘"), res.message);
|
|
241
|
-
errors += 1;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
} else {
|
|
245
|
-
console.log(picocolors__default["default"].red("✘"), "Unexpected error in", `${sourcePath}:`, v.message, " (Expected value to be an object with 'key' and 'sourcePath' properties - this is likely a bug in Val)");
|
|
246
|
-
errors += 1;
|
|
247
|
-
}
|
|
248
|
-
} else if (v.fixes.includes("image:upload-remote") || v.fixes.includes("file:upload-remote")) {
|
|
249
|
-
if (!fix) {
|
|
250
|
-
console.log(picocolors__default["default"].red("✘"), `Remote file ${sourcePath} needs to be uploaded (use --fix to upload)`);
|
|
251
|
-
errors += 1;
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(sourcePath);
|
|
255
|
-
if (valModule.source && valModule.schema) {
|
|
256
|
-
const resolvedRemoteFileAtSourcePath = core.Internal.resolvePath(modulePath, valModule.source, valModule.schema);
|
|
257
|
-
let filePath = null;
|
|
258
|
-
try {
|
|
259
|
-
var _resolvedRemoteFileAt;
|
|
260
|
-
filePath = path__default["default"].join(projectRoot, // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
261
|
-
(_resolvedRemoteFileAt = resolvedRemoteFileAtSourcePath.source) === null || _resolvedRemoteFileAt === void 0 ? void 0 : _resolvedRemoteFileAt[core.FILE_REF_PROP]);
|
|
262
|
-
await fs__default["default"].access(filePath);
|
|
263
|
-
} catch {
|
|
264
|
-
if (filePath) {
|
|
265
|
-
console.log(picocolors__default["default"].red("✘"), `File ${filePath} does not exist`);
|
|
266
|
-
} else {
|
|
267
|
-
console.log(picocolors__default["default"].red("✘"), `Expected file to be defined at: ${sourcePath} but no file was found`);
|
|
268
|
-
}
|
|
269
|
-
errors += 1;
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
const patFile = server.getPersonalAccessTokenPath(projectRoot);
|
|
273
|
-
try {
|
|
274
|
-
await fs__default["default"].access(patFile);
|
|
275
|
-
} catch {
|
|
276
|
-
// TODO: display this error only once:
|
|
277
|
-
console.log(picocolors__default["default"].red("✘"), `File: ${path__default["default"].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
|
-
errors += 1;
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
const parsedPatFile = server.parsePersonalAccessTokenFile(await fs__default["default"].readFile(patFile, "utf-8"));
|
|
282
|
-
if (!parsedPatFile.success) {
|
|
283
|
-
console.log(picocolors__default["default"].red("✘"), `Error parsing personal access token file: ${parsedPatFile.error}. You need to login again.`);
|
|
284
|
-
errors += 1;
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
const {
|
|
288
|
-
pat
|
|
289
|
-
} = parsedPatFile.data;
|
|
290
|
-
if (remoteFiles[sourcePath]) {
|
|
291
|
-
console.log(picocolors__default["default"].yellow("⚠"), `Remote file ${filePath} already uploaded`);
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
// TODO: parallelize uploading files
|
|
295
|
-
if (!resolvedRemoteFileAtSourcePath.schema) {
|
|
296
|
-
console.log(picocolors__default["default"].red("✘"), `Cannot upload remote file: schema not found for ${sourcePath}`);
|
|
297
|
-
errors += 1;
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
const actualRemoteFileSource = resolvedRemoteFileAtSourcePath.source;
|
|
301
|
-
const fileSourceMetadata = core.Internal.isFile(actualRemoteFileSource) ? actualRemoteFileSource.metadata : undefined;
|
|
302
|
-
const resolveRemoteFileSchema = resolvedRemoteFileAtSourcePath.schema;
|
|
303
|
-
if (!resolveRemoteFileSchema) {
|
|
304
|
-
console.log(picocolors__default["default"].red("✘"), `Could not resolve schema for remote file: ${sourcePath}`);
|
|
305
|
-
errors += 1;
|
|
306
|
-
continue;
|
|
307
|
-
}
|
|
308
|
-
if (!publicProjectId || !remoteFileBuckets) {
|
|
309
|
-
let projectName = process.env.VAL_PROJECT;
|
|
310
|
-
if (!projectName) {
|
|
311
|
-
projectName = valConfigFile === null || valConfigFile === void 0 ? void 0 : valConfigFile.project;
|
|
312
|
-
}
|
|
313
|
-
if (!projectName) {
|
|
314
|
-
console.log(picocolors__default["default"].red("✘"), "Project name not found. Set VAL_PROJECT environment variable or add project name to val.config");
|
|
315
|
-
errors += 1;
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
const settingsRes = await server.getSettings(projectName, {
|
|
319
|
-
pat
|
|
320
|
-
});
|
|
321
|
-
if (!settingsRes.success) {
|
|
322
|
-
console.log(picocolors__default["default"].red("✘"), `Could not get public project id: ${settingsRes.message}.`);
|
|
323
|
-
errors += 1;
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
publicProjectId = settingsRes.data.publicProjectId;
|
|
327
|
-
remoteFileBuckets = settingsRes.data.remoteFileBuckets.map(b => b.bucket);
|
|
328
|
-
}
|
|
329
|
-
if (!publicProjectId) {
|
|
330
|
-
console.log(picocolors__default["default"].red("✘"), "Could not get public project id");
|
|
331
|
-
errors += 1;
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
if (!(valConfigFile !== null && valConfigFile !== void 0 && valConfigFile.project)) {
|
|
335
|
-
console.log(picocolors__default["default"].red("✘"), `Could not get project. Check that your val.config has the 'project' field set, or set it using the VAL_PROJECT environment variable`);
|
|
336
|
-
errors += 1;
|
|
337
|
-
continue;
|
|
338
|
-
}
|
|
339
|
-
if (resolveRemoteFileSchema.type !== "image" && resolveRemoteFileSchema.type !== "file") {
|
|
340
|
-
console.log(picocolors__default["default"].red("✘"), `The schema is the remote is neither image nor file: ${sourcePath}`);
|
|
341
|
-
}
|
|
342
|
-
remoteFilesCounter += 1;
|
|
343
|
-
const bucket = remoteFileBuckets[remoteFilesCounter % remoteFileBuckets.length];
|
|
344
|
-
if (!bucket) {
|
|
345
|
-
console.log(picocolors__default["default"].red("✘"), `Internal error: could not allocate a bucket for the remote file located at ${sourcePath}`);
|
|
346
|
-
errors += 1;
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
let fileBuffer;
|
|
350
|
-
try {
|
|
351
|
-
fileBuffer = await fs__default["default"].readFile(filePath);
|
|
352
|
-
} catch (e) {
|
|
353
|
-
console.log(picocolors__default["default"].red("✘"), `Error reading file: ${e}`);
|
|
354
|
-
errors += 1;
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
const relativeFilePath = path__default["default"].relative(projectRoot, filePath).split(path__default["default"].sep).join("/");
|
|
358
|
-
if (!relativeFilePath.startsWith("public/val/")) {
|
|
359
|
-
console.log(picocolors__default["default"].red("✘"), `File path must be within the public/val/ directory (e.g. public/val/path/to/file.txt). Got: ${relativeFilePath}`);
|
|
360
|
-
errors += 1;
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
const fileHash = core.Internal.remote.getFileHash(fileBuffer);
|
|
364
|
-
const coreVersion = core.Internal.VERSION.core || "unknown";
|
|
365
|
-
const fileExt = getFileExt(filePath);
|
|
366
|
-
const schema = resolveRemoteFileSchema;
|
|
367
|
-
const metadata = fileSourceMetadata;
|
|
368
|
-
const ref = core.Internal.remote.createRemoteRef(valRemoteHost, {
|
|
369
|
-
publicProjectId,
|
|
370
|
-
coreVersion,
|
|
371
|
-
bucket,
|
|
372
|
-
validationHash: core.Internal.remote.getValidationHash(coreVersion, schema, fileExt, metadata, fileHash, textEncoder),
|
|
373
|
-
fileHash,
|
|
374
|
-
filePath: relativeFilePath
|
|
375
|
-
});
|
|
376
|
-
console.log(picocolors__default["default"].yellow("⚠"), `Uploading remote file: '${ref}'...`);
|
|
377
|
-
const remoteFileUpload = await server.uploadRemoteFile(contentHostUrl, valConfigFile.project, bucket, fileHash, fileExt, fileBuffer, {
|
|
378
|
-
pat
|
|
379
|
-
});
|
|
380
|
-
if (!remoteFileUpload.success) {
|
|
381
|
-
console.log(picocolors__default["default"].red("✘"), `Could not upload remote file: '${ref}'. Error: ${remoteFileUpload.error}`);
|
|
382
|
-
errors += 1;
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
console.log(picocolors__default["default"].green("✔"), `Completed upload of remote file: '${ref}'`);
|
|
386
|
-
remoteFiles[sourcePath] = {
|
|
387
|
-
ref,
|
|
388
|
-
metadata: fileSourceMetadata
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
} else if (v.fixes.includes("image:download-remote") || v.fixes.includes("file:download-remote")) {
|
|
392
|
-
if (fix) {
|
|
393
|
-
console.log(picocolors__default["default"].yellow("⚠"), `Downloading remote file in ${sourcePath}...`);
|
|
394
|
-
} else {
|
|
395
|
-
console.log(picocolors__default["default"].red("✘"), `Remote file ${sourcePath} needs to be downloaded (use --fix to download)`);
|
|
396
|
-
errors += 1;
|
|
397
|
-
continue;
|
|
398
|
-
}
|
|
399
|
-
} else if (v.fixes.includes("image:check-remote") || v.fixes.includes("file:check-remote")) ; else {
|
|
400
|
-
console.log(picocolors__default["default"].red("✘"), "Unknown fix", v.fixes, "for", sourcePath);
|
|
401
|
-
errors += 1;
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
656
|
const fixPatch = await server.createFixPatch({
|
|
405
657
|
projectRoot,
|
|
406
658
|
remoteHost: valRemoteHost
|
|
@@ -415,9 +667,6 @@ async function validate({
|
|
|
415
667
|
errors += 1;
|
|
416
668
|
console.log(e.fixes && e.fixes.length ? picocolors__default["default"].yellow("⚠") : picocolors__default["default"].red("✘"), `Got ${e.fixes && e.fixes.length ? "fixable " : ""}error in`, `${sourcePath}:`, e.message);
|
|
417
669
|
});
|
|
418
|
-
} else {
|
|
419
|
-
errors += 1;
|
|
420
|
-
console.log(picocolors__default["default"].red("✘"), "Got error in", `${sourcePath}:`, v.message);
|
|
421
670
|
}
|
|
422
671
|
}
|
|
423
672
|
}
|