@valbuild/cli 0.88.0 → 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.
@@ -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
- async function validate({
126
- root,
127
- fix
128
- }) {
129
- const valRemoteHost = process.env.VAL_REMOTE_HOST || core.DEFAULT_VAL_REMOTE_HOST;
130
- const contentHostUrl = process.env.VAL_CONTENT_URL || core.DEFAULT_CONTENT_HOST;
131
- const projectRoot = root ? path__default["default"].resolve(root) : process.cwd();
132
- const valConfigFile = (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
133
- 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)}'` : ""}...`));
134
- const service = await server.createService(projectRoot, {});
135
- const checkKeyIsValid = async (key, sourcePath) => {
136
- const [moduleFilePath, modulePath] = core.Internal.splitModuleFilePathAndModulePath(sourcePath);
137
- const keyOfModule = await service.get(moduleFilePath, modulePath, {
138
- source: true,
139
- schema: false,
140
- validate: false
141
- });
142
- const keyOfModuleSource = keyOfModule.source;
143
- const keyOfModuleSchema = keyOfModule.schema;
144
- if (keyOfModuleSchema && keyOfModuleSchema.type !== "record") {
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
- error: true,
147
- message: `Expected key at ${sourcePath} to be of type 'record'`
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 (keyOfModuleSource && typeof keyOfModuleSource === "object" && key in keyOfModuleSource) {
281
+ if (!projectName) {
151
282
  return {
152
- error: false
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
- if (!keyOfModuleSource || typeof keyOfModuleSource !== "object") {
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: `Expected ${sourcePath} to be a truthy object`
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
- const alternatives = findSimilar(key, Object.keys(keyOfModuleSource));
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: `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 ? ", ..." : ""}`
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; // TODO: ugly
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 = null;
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 && v.fixes.length > 0) {
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
  }