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