@valbuild/cli 0.102.0 → 0.103.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.
@@ -9,8 +9,8 @@ var fastGlob = require('fast-glob');
9
9
  var core = require('@valbuild/core');
10
10
  var server = require('@valbuild/server');
11
11
  var internal = require('@valbuild/shared/internal');
12
- var ts = require('typescript');
13
12
  var fs = require('fs');
13
+ var ts = require('typescript');
14
14
  var JSZip = require('jszip');
15
15
  var readline = require('readline');
16
16
 
@@ -39,8 +39,8 @@ var chalk__default = /*#__PURE__*/_interopDefault(chalk);
39
39
  var path__default = /*#__PURE__*/_interopDefault(path);
40
40
  var pc__default = /*#__PURE__*/_interopDefault(pc);
41
41
  var fs__default$1 = /*#__PURE__*/_interopDefault(fs$1);
42
- var ts__default = /*#__PURE__*/_interopDefault(ts);
43
42
  var fs__default = /*#__PURE__*/_interopDefault(fs);
43
+ var ts__default = /*#__PURE__*/_interopDefault(ts);
44
44
  var JSZip__default = /*#__PURE__*/_interopDefault(JSZip);
45
45
  var readline__default = /*#__PURE__*/_interopDefault(readline);
46
46
 
@@ -59,543 +59,6 @@ function info(message, opts = {}) {
59
59
  console.log(chalk__default["default"].blue("️ℹ️ : ") + message);
60
60
  }
61
61
 
62
- function getFileExt(filePath) {
63
- // NOTE: We do not import the path module. This code is copied in different projects. We want the same implementation and which means that this might running in browser where path is not available).
64
- return filePath.split(".").pop() || "";
65
- }
66
-
67
- const textEncoder = new TextEncoder();
68
-
69
- // Types for handler system
70
-
71
- // Handler functions
72
- async function handleFileMetadata(ctx) {
73
- var _fileSource$source;
74
- const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
75
- if (!ctx.valModule.source || !ctx.valModule.schema) {
76
- return {
77
- success: false,
78
- errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
79
- };
80
- }
81
- const fileSource = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
82
-
83
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
84
- const fileRefProp = (_fileSource$source = fileSource.source) === null || _fileSource$source === void 0 ? void 0 : _fileSource$source[core.FILE_REF_PROP];
85
- if (!fileRefProp) {
86
- return {
87
- success: false,
88
- errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
89
- };
90
- }
91
- const filePath = path__default["default"].join(ctx.projectRoot, fileRefProp);
92
- if (!ctx.fs.fileExists(filePath)) {
93
- return {
94
- success: false,
95
- errorMessage: `File ${filePath} does not exist`
96
- };
97
- }
98
- return {
99
- success: true,
100
- shouldApplyPatch: true
101
- };
102
- }
103
-
104
- // Shared upload core used by both the single-field (handleRemoteFileUpload)
105
- // and gallery (handleRemoteGalleryFileUpload) handlers. The two differ only in
106
- // how they derive the local file ref, metadata and serialized image/file
107
- // schema; everything from auth through upload is identical.
108
- async function uploadRemoteFileCore(ctx, fileRef, metadata, schema) {
109
- const filePath = path__default["default"].join(ctx.projectRoot, fileRef);
110
- if (!ctx.fs.fileExists(filePath)) {
111
- return {
112
- success: false,
113
- errorMessage: `File ${filePath} does not exist`
114
- };
115
- }
116
- const patFile = server.getPersonalAccessTokenPath(ctx.projectRoot);
117
- if (!ctx.fs.fileExists(patFile)) {
118
- return {
119
- success: false,
120
- 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`
121
- };
122
- }
123
- const patFileContent = ctx.fs.readFile(patFile);
124
- if (patFileContent === undefined) {
125
- return {
126
- success: false,
127
- errorMessage: `Could not read personal access token file at ${patFile}`
128
- };
129
- }
130
- const parsedPatFile = server.parsePersonalAccessTokenFile(patFileContent);
131
- if (!parsedPatFile.success) {
132
- return {
133
- success: false,
134
- errorMessage: `Error parsing personal access token file: ${parsedPatFile.error}. You need to login again.`
135
- };
136
- }
137
- const {
138
- pat
139
- } = parsedPatFile.data;
140
- if (ctx.remoteFiles[ctx.sourcePath]) {
141
- return {
142
- success: true,
143
- events: [{
144
- type: "remote-already-uploaded",
145
- filePath
146
- }]
147
- };
148
- }
149
- const projectName = ctx.project;
150
- let publicProjectId = ctx.publicProjectId;
151
- let remoteFileBuckets = ctx.remoteFileBuckets;
152
- let remoteFilesCounter = ctx.remoteFilesCounter;
153
- if (!publicProjectId || !remoteFileBuckets) {
154
- if (!projectName) {
155
- return {
156
- success: false,
157
- errorMessage: "Project name not found. Add project name to val.config or set the VAL_PROJECT environment variable"
158
- };
159
- }
160
- const settingsRes = await ctx.remote.getSettings(projectName, {
161
- pat
162
- });
163
- if (!settingsRes.success) {
164
- return {
165
- success: false,
166
- errorMessage: `Could not get public project id: ${settingsRes.message}.`
167
- };
168
- }
169
- publicProjectId = settingsRes.data.publicProjectId;
170
- remoteFileBuckets = settingsRes.data.remoteFileBuckets.map(b => b.bucket);
171
- }
172
- if (!publicProjectId) {
173
- return {
174
- success: false,
175
- errorMessage: "Could not get public project id"
176
- };
177
- }
178
- if (!projectName) {
179
- return {
180
- success: false,
181
- errorMessage: `Could not get project. Check that your val.config has the 'project' field set, or set it using the VAL_PROJECT environment variable`
182
- };
183
- }
184
- remoteFilesCounter += 1;
185
- const bucket = remoteFileBuckets[remoteFilesCounter % remoteFileBuckets.length];
186
- if (!bucket) {
187
- return {
188
- success: false,
189
- errorMessage: `Internal error: could not allocate a bucket for the remote file located at ${ctx.sourcePath}`
190
- };
191
- }
192
- const fileBuffer = ctx.fs.readBuffer(filePath);
193
- if (fileBuffer === undefined) {
194
- return {
195
- success: false,
196
- errorMessage: `Error reading file: ${filePath}`
197
- };
198
- }
199
- const relativeFilePath = path__default["default"].relative(ctx.projectRoot, filePath).split(path__default["default"].sep).join("/");
200
- if (!relativeFilePath.startsWith("public/")) {
201
- return {
202
- success: false,
203
- errorMessage: `File path must be within the public/ directory (e.g. public/path/to/file.txt). Got: ${relativeFilePath}`
204
- };
205
- }
206
- const fileHash = core.Internal.remote.getFileHash(fileBuffer);
207
- const coreVersion = core.Internal.VERSION.core || "unknown";
208
- const fileExt = getFileExt(filePath);
209
- const ref = core.Internal.remote.createRemoteRef(ctx.remote.remoteHost, {
210
- publicProjectId,
211
- coreVersion,
212
- bucket,
213
- validationHash: core.Internal.remote.getValidationHash(coreVersion, schema, fileExt, metadata, fileHash, textEncoder),
214
- fileHash,
215
- filePath: relativeFilePath
216
- });
217
- const remoteFileUpload = await ctx.remote.uploadFile(projectName, bucket, fileHash, fileExt, fileBuffer, {
218
- pat
219
- });
220
- if (!remoteFileUpload.success) {
221
- return {
222
- success: false,
223
- errorMessage: `Could not upload remote file: '${ref}'. Error: ${remoteFileUpload.error}`
224
- };
225
- }
226
- ctx.remoteFiles[ctx.sourcePath] = {
227
- ref,
228
- metadata
229
- };
230
- return {
231
- success: true,
232
- shouldApplyPatch: true,
233
- publicProjectId,
234
- remoteFileBuckets,
235
- remoteFilesCounter,
236
- events: [{
237
- type: "remote-uploading",
238
- ref
239
- }, {
240
- type: "remote-uploaded",
241
- ref
242
- }]
243
- };
244
- }
245
- async function handleRemoteFileUpload(ctx) {
246
- var _resolvedRemoteFileAt;
247
- if (!ctx.fix) {
248
- return {
249
- success: false,
250
- // No sourcePath in the message: the reported location already points at it.
251
- errorMessage: `Remote file needs to be uploaded (use --fix to upload)`
252
- };
253
- }
254
- const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
255
- if (!ctx.valModule.source || !ctx.valModule.schema) {
256
- return {
257
- success: false,
258
- errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
259
- };
260
- }
261
- const resolvedRemoteFileAtSourcePath = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
262
-
263
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
264
- const fileRefProp = (_resolvedRemoteFileAt = resolvedRemoteFileAtSourcePath.source) === null || _resolvedRemoteFileAt === void 0 ? void 0 : _resolvedRemoteFileAt[core.FILE_REF_PROP];
265
- if (!fileRefProp) {
266
- return {
267
- success: false,
268
- errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
269
- };
270
- }
271
- const resolveRemoteFileSchema = resolvedRemoteFileAtSourcePath.schema;
272
- if (!resolveRemoteFileSchema) {
273
- return {
274
- success: false,
275
- errorMessage: `Cannot upload remote file: schema not found for ${ctx.sourcePath}`
276
- };
277
- }
278
- if (resolveRemoteFileSchema.type !== "image" && resolveRemoteFileSchema.type !== "file") {
279
- return {
280
- success: false,
281
- errorMessage: `The schema is the remote is neither image nor file: ${ctx.sourcePath}`
282
- };
283
- }
284
- const actualRemoteFileSource = resolvedRemoteFileAtSourcePath.source;
285
- const fileSourceMetadata = core.Internal.isFile(actualRemoteFileSource) ? actualRemoteFileSource.metadata : undefined;
286
- return uploadRemoteFileCore(ctx, fileRefProp, fileSourceMetadata, resolveRemoteFileSchema);
287
- }
288
-
289
- // Gallery (s.images({ remote: true }) / s.files({ remote: true })) upload.
290
- // Unlike a single image/file field, a gallery entry is keyed by its local file
291
- // path and the value is bare metadata (no FileSource), so we derive the file
292
- // ref from the key and synthesize the image/file schema from the record's
293
- // media options.
294
- async function handleRemoteGalleryFileUpload(ctx) {
295
- var _ctx$validationError$;
296
- if (!ctx.fix) {
297
- return {
298
- success: false,
299
- // No sourcePath in the message: the reported location already points at it.
300
- errorMessage: `Remote file needs to be uploaded (use --fix to upload)`
301
- };
302
- }
303
- const fix = (_ctx$validationError$ = ctx.validationError.fixes) === null || _ctx$validationError$ === void 0 ? void 0 : _ctx$validationError$[0];
304
- const mediaType = fix === "files:upload-remote" ? "file" : "image";
305
-
306
- // The gallery entry is keyed by its local file path; validateMediaKey carries
307
- // that key as the error value.
308
- const fileRef = ctx.validationError.value;
309
- if (typeof fileRef !== "string") {
310
- return {
311
- success: false,
312
- errorMessage: `Expected a local file path for gallery entry at ${ctx.sourcePath}`
313
- };
314
- }
315
- const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
316
- if (!ctx.valModule.source || !ctx.valModule.schema) {
317
- return {
318
- success: false,
319
- errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
320
- };
321
- }
322
- const resolved = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
323
- const entrySource = resolved.source;
324
- const metadata = entrySource && typeof entrySource === "object" && !Array.isArray(entrySource) ? entrySource : undefined;
325
-
326
- // The gallery item schema is an ObjectSchema, not an image/file schema, so
327
- // synthesize the serialized image/file schema (matching how single fields
328
- // serialize) for the remote ref's validation hash.
329
- //
330
- // `accept`/`directory` come from the RECORD that holds the entry, resolved
331
- // from the entry's PARENT path - not from the module root, which is only the
332
- // record when the gallery is the whole module. A nested gallery
333
- // (s.object({ gallery: s.images(...) })) would otherwise synthesize a schema
334
- // with no options and bake a validation hash into the remote ref that can
335
- // never validate, so a mismatch fails fast instead of uploading.
336
- const [, parentModulePath] = core.Internal.splitModuleFilePathAndModulePath(core.Internal.parentOfSourcePath(ctx.sourcePath));
337
- const recordSchema = core.Internal.resolvePath(parentModulePath, ctx.valModule.source, ctx.valModule.schema).schema;
338
- if ((recordSchema === null || recordSchema === void 0 ? void 0 : recordSchema.type) !== "record") {
339
- return {
340
- success: false,
341
- errorMessage: `Expected a gallery record at the parent of ${ctx.sourcePath}, got ${(recordSchema === null || recordSchema === void 0 ? void 0 : recordSchema.type) ?? "nothing"}`
342
- };
343
- }
344
- const {
345
- accept,
346
- directory
347
- } = recordSchema;
348
- const schema = mediaType === "image" ? {
349
- type: "image",
350
- opt: false,
351
- options: {
352
- ...(accept ? {
353
- accept
354
- } : {}),
355
- ...(directory ? {
356
- directory
357
- } : {})
358
- }
359
- } : {
360
- type: "file",
361
- opt: false,
362
- options: {
363
- ...(accept ? {
364
- accept
365
- } : {})
366
- }
367
- };
368
- return uploadRemoteFileCore(ctx, fileRef, metadata, schema);
369
- }
370
- async function handleRemoteFileDownload(ctx) {
371
- if (ctx.fix) {
372
- return {
373
- success: true,
374
- shouldApplyPatch: true,
375
- events: [{
376
- type: "remote-downloading",
377
- sourcePath: ctx.sourcePath
378
- }]
379
- };
380
- } else {
381
- return {
382
- success: false,
383
- errorMessage: `Remote file ${ctx.sourcePath} needs to be downloaded (use --fix to download)`
384
- };
385
- }
386
- }
387
- async function handleRemoteFileCheck() {
388
- // Skip - no action needed
389
- return {
390
- success: true,
391
- shouldApplyPatch: true
392
- };
393
- }
394
- async function handleUniqueFolderCheck(ctx) {
395
- const value = ctx.validationError.value;
396
- if (!value || typeof value.directory !== "string") {
397
- return {
398
- success: false,
399
- errorMessage: `Unexpected value in unique folder check for ${ctx.sourcePath}`
400
- };
401
- }
402
- const {
403
- directory
404
- } = value;
405
- const conflicts = [];
406
- for (const file of ctx.valFiles) {
407
- const otherModuleFilePath = `/${file}`;
408
- if (otherModuleFilePath === ctx.moduleFilePath) continue;
409
- const otherModule = await ctx.service.get(otherModuleFilePath, "", {
410
- validate: false
411
- });
412
- const schema = otherModule.schema;
413
- if ((schema === null || schema === void 0 ? void 0 : schema.type) === "record" && schema.directory === directory && schema.mediaType) {
414
- conflicts.push(otherModuleFilePath);
415
- }
416
- }
417
- if (conflicts.length > 0) {
418
- return {
419
- success: false,
420
- errorMessage: `Gallery directory '${directory}' in ${ctx.moduleFilePath} is also used by: ${conflicts.join(", ")}. Each gallery must use a unique directory.`
421
- };
422
- }
423
- return {
424
- success: true
425
- };
426
- }
427
-
428
- // Maps a gallery key to its on-disk local path. Remote galleries key uploaded
429
- // entries by a remote URL while keeping the file on disk; everything else is
430
- // already a local path and is returned unchanged.
431
- function remoteKeyToLocalPath(key) {
432
- const remoteRefRes = core.Internal.remote.splitRemoteRef(key);
433
- if (remoteRefRes.status === "success") {
434
- return `/${remoteRefRes.filePath}`;
435
- }
436
- return key;
437
- }
438
- async function handleCheckAllFiles(ctx) {
439
- const value = ctx.validationError.value;
440
- if (!value || typeof value.directory !== "string") {
441
- return {
442
- success: false,
443
- errorMessage: `Unexpected value in check-all-files for ${ctx.sourcePath}`
444
- };
445
- }
446
- const {
447
- directory
448
- } = value;
449
- const source = ctx.valModule.source;
450
- if (!source || typeof source !== "object" || Array.isArray(source)) {
451
- return {
452
- success: false,
453
- errorMessage: `Could not get source for ${ctx.sourcePath}`
454
- };
455
- }
456
- // Gallery entries are keyed by their file path. Remote galleries key uploaded
457
- // entries by a remote URL, but the file is kept on disk at its local path, so
458
- // normalize remote-URL keys back to that local path for the on-disk checks.
459
- const trackedFiles = new Set(Object.keys(source).map(remoteKeyToLocalPath));
460
-
461
- // Check that all tracked files exist on disk
462
- const missingTrackedFiles = [...trackedFiles].filter(f => {
463
- return !ctx.fs.fileExists(path__default["default"].join(ctx.projectRoot, f));
464
- });
465
- if (missingTrackedFiles.length > 0) {
466
- if (!ctx.fix) {
467
- return {
468
- success: false,
469
- errorMessage: `Gallery in ${ctx.moduleFilePath} has tracked files that do not exist on disk: ${missingTrackedFiles.join(", ")}. Add the files or remove them from the gallery.`
470
- };
471
- }
472
- // fix: true — let createFixPatch remove the missing entries
473
- return {
474
- success: true,
475
- shouldApplyPatch: true
476
- };
477
- }
478
- const dirPath = path__default["default"].join(ctx.projectRoot, directory);
479
- const filesInDir = [];
480
- try {
481
- const entries = ctx.fs.readDirectory(dirPath, undefined, undefined, ["**/*"]);
482
- for (const entry of entries) {
483
- const relPath = "/" + path__default["default"].relative(ctx.projectRoot, entry).split(path__default["default"].sep).join("/");
484
- filesInDir.push(relPath);
485
- }
486
- } catch {
487
- // directory doesn't exist — no untracked files possible
488
- }
489
- const untrackedFiles = filesInDir.filter(f => !trackedFiles.has(f));
490
- if (untrackedFiles.length > 0) {
491
- return {
492
- success: false,
493
- errorMessage: `Gallery in ${ctx.moduleFilePath} has files not tracked: ${untrackedFiles.join(", ")}. Add these files to the gallery or remove them from the directory.`
494
- };
495
- }
496
-
497
- // All files accounted for — trigger metadata verification via createFixPatch
498
- return {
499
- success: true,
500
- shouldApplyPatch: true
501
- };
502
- }
503
- async function handleJsonValuesExtractEntry(ctx) {
504
- const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
505
- const parts = core.Internal.splitModulePath(modulePath);
506
- // Root-only by contract (see findNestedJsonValuesRecords): the entry is a
507
- // direct child of the module's root record/router, so the path is one segment.
508
- if (parts.length !== 1) {
509
- return {
510
- success: false,
511
- errorMessage: `Cannot extract .jsonValues() entry at ${ctx.sourcePath}: expected a root record entry`
512
- };
513
- }
514
- const entryKey = parts[0];
515
- const source = ctx.valModule.source;
516
- if (source === null || typeof source !== "object" || Array.isArray(source)) {
517
- return {
518
- success: false,
519
- errorMessage: `Could not get source for ${ctx.moduleFilePath}`
520
- };
521
- }
522
- const content = source[entryKey];
523
- if (content === undefined) {
524
- return {
525
- success: false,
526
- errorMessage: `Could not find .jsonValues() entry '${entryKey}' in ${ctx.moduleFilePath}`
527
- };
528
- }
529
- if (!ctx.fix) {
530
- return {
531
- success: true,
532
- fixableErrorMessage: ctx.validationError.message
533
- };
534
- }
535
- try {
536
- server.extractJsonValuesEntry(ctx.moduleFilePath, ctx.projectRoot, entryKey, content, ctx.service.sourceFileHandler);
537
- } catch (err) {
538
- return {
539
- success: false,
540
- errorMessage: err instanceof Error ? err.message : String(err)
541
- };
542
- }
543
- return {
544
- success: true,
545
- appliedFix: true
546
- };
547
- }
548
-
549
- // Fix handler registry. `keyof:check-keys` and `router:check-route` are
550
- // resolved upfront by the shared resolveSchemaSourceFixes — they never reach
551
- // this registry, so they're excluded from the key set.
552
- const currentFixHandlers = {
553
- "image:check-metadata": handleFileMetadata,
554
- "image:add-metadata": handleFileMetadata,
555
- "file:check-metadata": handleFileMetadata,
556
- "file:add-metadata": handleFileMetadata,
557
- "image:upload-remote": handleRemoteFileUpload,
558
- "file:upload-remote": handleRemoteFileUpload,
559
- "images:upload-remote": handleRemoteGalleryFileUpload,
560
- "files:upload-remote": handleRemoteGalleryFileUpload,
561
- "image:download-remote": handleRemoteFileDownload,
562
- "file:download-remote": handleRemoteFileDownload,
563
- "image:check-remote": handleRemoteFileCheck,
564
- "images:check-remote": handleRemoteFileCheck,
565
- "file:check-remote": handleRemoteFileCheck,
566
- "files:check-remote": handleRemoteFileCheck,
567
- "images:check-unique-folder": handleUniqueFolderCheck,
568
- "files:check-unique-folder": handleUniqueFolderCheck,
569
- "images:check-all-files": handleCheckAllFiles,
570
- "files:check-all-files": handleCheckAllFiles,
571
- "jsonValues:extract-entry": handleJsonValuesExtractEntry
572
- };
573
- const deprecatedFixHandlers = {
574
- "image:replace-metadata": handleFileMetadata
575
- };
576
- const fixHandlers = {
577
- ...deprecatedFixHandlers,
578
- ...currentFixHandlers
579
- };
580
- function createDefaultValFSHost() {
581
- return {
582
- ...ts__default["default"].sys,
583
- writeFile: (fileName, data, encoding) => {
584
- fs__default["default"].mkdirSync(path__default["default"].dirname(fileName), {
585
- recursive: true
586
- });
587
- fs__default["default"].writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
588
- },
589
- rmFile: fs__default["default"].rmSync,
590
- readBuffer: fileName => {
591
- try {
592
- return fs__default["default"].readFileSync(fileName);
593
- } catch {
594
- return undefined;
595
- }
596
- }
597
- };
598
- }
599
62
  async function* runValidation({
600
63
  root,
601
64
  fix,
@@ -682,7 +145,7 @@ async function* runValidation({
682
145
 
683
146
  // Find and execute appropriate handler
684
147
  const fixType = v.fixes[0]; // Take first fix
685
- const handler = fixHandlers[fixType];
148
+ const handler = server.fixHandlers[fixType];
686
149
  if (!handler) {
687
150
  yield {
688
151
  type: "unknown-fix",
@@ -1136,7 +599,7 @@ async function validate({
1136
599
  getSettings: (projectName, options) => server.getSettings(projectName, options),
1137
600
  uploadFile: (project, bucket, fileHash, fileExt, fileBuffer, options) => server.uploadRemoteFile(process.env.VAL_CONTENT_URL || core.DEFAULT_CONTENT_HOST, project, bucket, fileHash, fileExt ?? "", fileBuffer, options)
1138
601
  },
1139
- fs: createDefaultValFSHost()
602
+ fs: server.createDefaultValFSHost()
1140
603
  })) {
1141
604
  switch (event.type) {
1142
605
  case "file-valid":
@@ -1413,7 +876,7 @@ async function listUnusedFiles({
1413
876
  for (const error of validationError) {
1414
877
  const value = error.value;
1415
878
  if (isFileRef(value)) {
1416
- const absoluteFilePathUsedByVal = path__default["default"].join(projectRoot, ...value[core.FILE_REF_PROP].split("/"));
879
+ const absoluteFilePathUsedByVal = path__default["default"].join(projectRoot, ...value.path.split("/"));
1417
880
  filesUsedByVal.push(absoluteFilePathUsedByVal);
1418
881
  }
1419
882
  }
@@ -1438,15 +901,15 @@ async function listUnusedFiles({
1438
901
  service.dispose();
1439
902
  return;
1440
903
  }
904
+
905
+ /**
906
+ * Whether a flagged validation value names a file.
907
+ *
908
+ * A `ValidationError` carries no schema, so the shape is all there is to go on —
909
+ * the same heuristic this whole function documents as a TODO.
910
+ */
1441
911
  function isFileRef(value) {
1442
- if (!value) return false;
1443
- if (typeof value !== "object") return false;
1444
- if (core.FILE_REF_PROP in value && core.VAL_EXTENSION in value) {
1445
- if (value[core.VAL_EXTENSION] === "file" && typeof value[core.FILE_REF_PROP] === "string") {
1446
- return true;
1447
- }
1448
- }
1449
- return false;
912
+ return !!value && typeof value === "object" && "path" in value && typeof value.path === "string";
1450
913
  }
1451
914
 
1452
915
  const getVersions = () => {