@valbuild/server 0.93.0 → 0.94.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.
@@ -4,10 +4,11 @@
4
4
  *
5
5
  */
6
6
  export interface ValFS {
7
- readDirectory(rootDir: string, extensions: readonly string[], excludes: readonly string[] | undefined, includes: readonly string[], depth?: number | undefined): readonly string[];
7
+ readDirectory(rootDir: string, extensions: readonly string[] | undefined, excludes: readonly string[] | undefined, includes: readonly string[], depth?: number | undefined): readonly string[];
8
8
  writeFile(filePath: string, data: string | Buffer, encoding: "binary" | "utf8"): void;
9
9
  fileExists(filePath: string): boolean;
10
10
  readFile(filePath: string): string | undefined;
11
+ readBuffer(filePath: string): Buffer | undefined;
11
12
  rmFile(filePath: string): void;
12
13
  realpath(path: string): string;
13
14
  }
@@ -7,20 +7,23 @@ import { Buffer } from "buffer";
7
7
  */
8
8
  export interface IValFSHost extends ts.ParseConfigHost, ts.ModuleResolutionHost {
9
9
  useCaseSensitiveFileNames: boolean;
10
+ readDirectory(rootDir: string, extensions: readonly string[] | undefined, excludes: readonly string[] | undefined, includes: readonly string[], depth?: number | undefined): readonly string[];
10
11
  writeFile(fileName: string, data: string | Buffer, encoding: "binary" | "utf8"): void;
11
12
  rmFile(fileName: string): void;
13
+ readBuffer(fileName: string): Buffer | undefined;
12
14
  }
13
15
  export declare class ValFSHost implements IValFSHost {
14
16
  protected readonly valFS: ValFS;
15
17
  protected readonly currentDirectory: string;
16
18
  constructor(valFS: ValFS, currentDirectory: string);
17
19
  useCaseSensitiveFileNames: boolean;
18
- readDirectory(rootDir: string, extensions: readonly string[], excludes: readonly string[] | undefined, includes: readonly string[], depth?: number | undefined): readonly string[];
20
+ readDirectory(rootDir: string, extensions: readonly string[] | undefined, excludes: readonly string[] | undefined, includes: readonly string[], depth?: number | undefined): readonly string[];
19
21
  rmFile(fileName: string): void;
20
22
  writeFile(fileName: string, text: string | Buffer, encoding: "binary" | "utf8"): void;
21
23
  getCurrentDirectory(): string;
22
24
  getCanonicalFileName(fileName: string): string;
23
25
  fileExists(fileName: string): boolean;
24
26
  readFile(fileName: string): string | undefined;
27
+ readBuffer(fileName: string): Buffer | undefined;
25
28
  realpath(path: string): string;
26
29
  }
@@ -659,7 +659,7 @@ function removeFromNode(document, node, key) {
659
659
  } else if (ts__default["default"].isObjectLiteralExpression(node)) {
660
660
  return fp.pipe(findObjectPropertyAssignment(node, key), fp.result.flatMap(assignment => {
661
661
  if (!assignment) {
662
- return fp.result.err(new patch.PatchError("Cannot replace object element which does not exist"));
662
+ return fp.result.err(new patch.PatchError("Cannot remove object element which does not exist"));
663
663
  }
664
664
  return fp.result.ok(assignment);
665
665
  }), fp.result.map(assignment => [removeAt(document, node.properties, node.properties.indexOf(assignment))[0], assignment.initializer]));
@@ -1026,7 +1026,14 @@ class ValSourceFileHandler {
1026
1026
  });
1027
1027
  fs__default["default"].writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
1028
1028
  },
1029
- rmFile: fs__default["default"].rmSync
1029
+ rmFile: fs__default["default"].rmSync,
1030
+ readBuffer: fileName => {
1031
+ try {
1032
+ return fs__default["default"].readFileSync(fileName);
1033
+ } catch {
1034
+ return undefined;
1035
+ }
1036
+ }
1030
1037
  }) {
1031
1038
  this.projectRoot = projectRoot;
1032
1039
  this.compilerOptions = compilerOptions;
@@ -1074,7 +1081,14 @@ class ValModuleLoader {
1074
1081
  });
1075
1082
  fs__default["default"].writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
1076
1083
  },
1077
- rmFile: fs__default["default"].rmSync
1084
+ rmFile: fs__default["default"].rmSync,
1085
+ readBuffer: fileName => {
1086
+ try {
1087
+ return fs__default["default"].readFileSync(fileName);
1088
+ } catch {
1089
+ return undefined;
1090
+ }
1091
+ }
1078
1092
  }, disableCache = false) {
1079
1093
  this.projectRoot = projectRoot;
1080
1094
  this.compilerOptions = compilerOptions;
@@ -1367,7 +1381,14 @@ async function createService(projectRoot, opts, host = {
1367
1381
  });
1368
1382
  fs__default["default"].writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
1369
1383
  },
1370
- rmFile: fs__default["default"].rmSync
1384
+ rmFile: fs__default["default"].rmSync,
1385
+ readBuffer: fileName => {
1386
+ try {
1387
+ return fs__default["default"].readFileSync(fileName);
1388
+ } catch {
1389
+ return undefined;
1390
+ }
1391
+ }
1371
1392
  }, loader) {
1372
1393
  const compilerOptions = getCompilerOptions(projectRoot, host);
1373
1394
  const sourceFileHandler = new ValSourceFileHandler(projectRoot, compilerOptions, host);
@@ -1700,8 +1721,10 @@ class ValOps {
1700
1721
  const filePath = op.filePath;
1701
1722
  fileLastUpdatedByPatchId[filePath] = {
1702
1723
  patchId: patch.patchId,
1703
- remote: op.remote
1724
+ remote: op.remote,
1725
+ isDelete: op.value === null
1704
1726
  };
1727
+ continue;
1705
1728
  }
1706
1729
  const path = patch.path;
1707
1730
  if (!patchesByModule[path]) {
@@ -1776,15 +1799,19 @@ class ValOps {
1776
1799
  const fileFixOps = {};
1777
1800
  for (const op of patchData.patch) {
1778
1801
  if (op.op === "file") {
1779
- // NOTE: We insert the last patch_id that modify a file
1780
- // when constructing the url we use the patch id (and the file path)
1781
- // to fetch the right file
1782
- // NOTE: overwrite and use last patch_id if multiple patches modify the same file
1783
- fileFixOps[op.path.join("/")] = [{
1784
- op: "add",
1785
- path: op.path.concat(...(op.nestedFilePath || [])).concat("patch_id"),
1786
- value: patchId
1787
- }];
1802
+ if (op.value !== null) {
1803
+ // NOTE: We insert the last patch_id that modify a file
1804
+ // when constructing the url we use the patch id (and the file path)
1805
+ // to fetch the right file
1806
+ // NOTE: overwrite and use last patch_id if multiple patches modify the same file
1807
+ fileFixOps[op.path.join("/")] = [{
1808
+ op: "add",
1809
+ path: op.path.concat(...(op.nestedFilePath || [])).concat("patch_id"),
1810
+ value: patchId
1811
+ }];
1812
+ }
1813
+ // null value = delete: no patch_id to inject; the "remove" op in
1814
+ // the patch already removes the metadata entry from the source
1788
1815
  } else {
1789
1816
  applicableOps.push(op);
1790
1817
  }
@@ -1897,6 +1924,21 @@ class ValOps {
1897
1924
  const files = {};
1898
1925
  const remoteFiles = {};
1899
1926
  const entries = Object.entries(schemas);
1927
+ // Build a map of gallery directory → [ModuleFilePath, ...] across ALL modules
1928
+ // (must include all modules, not just those being validated, since conflicts can come from any module)
1929
+ const galleryDirectoryToModules = new Map();
1930
+ for (const [moduleFilePathS, schema] of entries) {
1931
+ const serialized = schema["executeSerialize"]();
1932
+ if (serialized.type === "record" && serialized.mediaType && serialized.directory) {
1933
+ const dir = serialized.directory;
1934
+ const existing = galleryDirectoryToModules.get(dir);
1935
+ if (existing) {
1936
+ existing.push(moduleFilePathS);
1937
+ } else {
1938
+ galleryDirectoryToModules.set(dir, [moduleFilePathS]);
1939
+ }
1940
+ }
1941
+ }
1900
1942
  const modulePathsToValidate = patchesByModule && Object.keys(patchesByModule);
1901
1943
  for (const [pathS, schema] of entries) {
1902
1944
  if (modulePathsToValidate && !modulePathsToValidate.includes(pathS)) {
@@ -1937,7 +1979,7 @@ class ValOps {
1937
1979
  };
1938
1980
  if (validationErrors) {
1939
1981
  for (const validationError of validationErrors) {
1940
- var _validationError$fixe, _validationError$fixe2, _validationError$fixe3, _validationError$fixe4;
1982
+ var _validationError$fixe, _validationError$fixe2, _validationError$fixe3, _validationError$fixe4, _validationError$fixe5, _validationError$fixe6, _validationError$fixe7, _validationError$fixe8;
1941
1983
  if (isOnlyFileCheckValidationError(validationError)) {
1942
1984
  if (files[sourcePath]) {
1943
1985
  throw new Error("Cannot have multiple files with same path. Path: " + sourcePath + "; Module: " + path);
@@ -2020,7 +2062,32 @@ class ValOps {
2020
2062
  }
2021
2063
  }
2022
2064
  }
2023
- } else {
2065
+ } else if ((_validationError$fixe5 = validationError.fixes) !== null && _validationError$fixe5 !== void 0 && _validationError$fixe5.includes("images:check-unique-folder") || (_validationError$fixe6 = validationError.fixes) !== null && _validationError$fixe6 !== void 0 && _validationError$fixe6.includes("files:check-unique-folder")) {
2066
+ const TYPE_ERROR_MESSAGE = `This is most likely a Val version mismatch or Val bug.`;
2067
+ if (!validationError.value || typeof validationError.value !== "object") {
2068
+ addError({
2069
+ message: `Could not find a directory value for gallery at ${sourcePath}. ${TYPE_ERROR_MESSAGE}`,
2070
+ typeError: true
2071
+ });
2072
+ } else {
2073
+ const directory = "directory" in validationError.value && validationError.value.directory;
2074
+ if (typeof directory !== "string") {
2075
+ addError({
2076
+ message: `Expected gallery validation error 'value' to have property 'directory' of type 'string'. Found: ${typeof directory}. ${TYPE_ERROR_MESSAGE}`,
2077
+ typeError: true
2078
+ });
2079
+ } else {
2080
+ const modulesUsingDir = galleryDirectoryToModules.get(directory) ?? [];
2081
+ const conflictingModules = modulesUsingDir.filter(m => m !== path);
2082
+ if (conflictingModules.length > 0) {
2083
+ addError({
2084
+ message: `Gallery directory '${directory}' in ${path} conflicts with: ${conflictingModules.join(", ")}. Each gallery must use a unique directory.`
2085
+ });
2086
+ }
2087
+ // If conflictingModules is empty, directory is unique — silently drop the error.
2088
+ }
2089
+ }
2090
+ } else if ((_validationError$fixe7 = validationError.fixes) !== null && _validationError$fixe7 !== void 0 && _validationError$fixe7.includes("images:check-all-files") || (_validationError$fixe8 = validationError.fixes) !== null && _validationError$fixe8 !== void 0 && _validationError$fixe8.includes("files:check-all-files")) ; else {
2024
2091
  addError(validationError);
2025
2092
  }
2026
2093
  }
@@ -2300,16 +2367,22 @@ class ValOps {
2300
2367
  await Promise.all(Object.entries(fileLastUpdatedByPatchId).map(async ([filePath, patchData]) => {
2301
2368
  const {
2302
2369
  patchId,
2303
- remote
2370
+ remote,
2371
+ isDelete
2304
2372
  } = patchData;
2305
2373
  if (globalAppliedPatches.includes(patchId)) {
2306
- // TODO: do we want to make sure the file is there? Then again, it should be rare that it happens (unless there's a Val bug) so it might be enough to fail later (at commit)
2307
- // TODO: include sha256? This way we can make sure we pick the right file since theoretically there could be multiple files with the same path in the same patch
2308
- // or is that the case? We are picking the latest file by path so, that should be enough?
2309
- patchedBinaryFilesDescriptors[filePath] = {
2310
- patchId,
2311
- remote
2312
- };
2374
+ if (isDelete) {
2375
+ // Signal file deletion via patchedSourceFiles null entry
2376
+ patchedSourceFiles[filePath] = null;
2377
+ } else {
2378
+ // TODO: do we want to make sure the file is there? Then again, it should be rare that it happens (unless there's a Val bug) so it might be enough to fail later (at commit)
2379
+ // TODO: include sha256? This way we can make sure we pick the right file since theoretically there could be multiple files with the same path in the same patch
2380
+ // or is that the case? We are picking the latest file by path so, that should be enough?
2381
+ patchedBinaryFilesDescriptors[filePath] = {
2382
+ patchId,
2383
+ remote
2384
+ };
2385
+ }
2313
2386
  } else {
2314
2387
  hasErrors = true;
2315
2388
  binaryFilePatchErrors[filePath] = {
@@ -2355,8 +2428,8 @@ class ValOps {
2355
2428
  // #region abstract ops
2356
2429
  }
2357
2430
  function isOnlyFileCheckValidationError(validationError) {
2358
- var _validationError$fixe5;
2359
- if ((_validationError$fixe5 = validationError.fixes) !== null && _validationError$fixe5 !== void 0 && _validationError$fixe5.every(f => f === "file:check-metadata" || f === "image:check-metadata")) {
2431
+ var _validationError$fixe9;
2432
+ if ((_validationError$fixe9 = validationError.fixes) !== null && _validationError$fixe9 !== void 0 && _validationError$fixe9.every(f => f === "file:check-metadata" || f === "image:check-metadata")) {
2360
2433
  return true;
2361
2434
  }
2362
2435
  return false;
@@ -2987,6 +3060,14 @@ class ValOpsFS extends ValOps {
2987
3060
  const patchFilePath = this.getBinaryFilePath(filePath, patchDir);
2988
3061
  const metadataFilePath = this.getBinaryFileMetadataPath(filePath, patchDir);
2989
3062
  try {
3063
+ if (data === null) {
3064
+ this.host.deleteFile(patchFilePath);
3065
+ this.host.deleteFile(metadataFilePath);
3066
+ return {
3067
+ patchId,
3068
+ filePath
3069
+ };
3070
+ }
2990
3071
  const buffer = bufferFromDataUrl(data);
2991
3072
  if (!buffer) {
2992
3073
  return {
@@ -3247,7 +3328,11 @@ class ValOpsFS extends ValOps {
3247
3328
  for (const [filePath, data] of Object.entries(preparedCommit.patchedSourceFiles)) {
3248
3329
  const absPath = path__namespace["default"].join(this.rootDir, ...filePath.split("/"));
3249
3330
  try {
3250
- this.host.writeUf8File(absPath, data);
3331
+ if (data === null) {
3332
+ this.host.deleteFile(absPath);
3333
+ } else {
3334
+ this.host.writeUf8File(absPath, data);
3335
+ }
3251
3336
  updatedFiles.push(absPath);
3252
3337
  } catch (err) {
3253
3338
  errors[absPath] = {
@@ -3373,6 +3458,11 @@ class FSOpsHost {
3373
3458
  });
3374
3459
  }
3375
3460
  }
3461
+ deleteFile(path) {
3462
+ if (this.fileExists(path)) {
3463
+ fs__default["default"].rmSync(path);
3464
+ }
3465
+ }
3376
3466
  moveDir(from, to) {
3377
3467
  fs__default["default"].renameSync(from, to);
3378
3468
  }
@@ -6769,6 +6859,9 @@ class ValFSHost {
6769
6859
  readFile(fileName) {
6770
6860
  return this.valFS.readFile(fileName);
6771
6861
  }
6862
+ readBuffer(fileName) {
6863
+ return this.valFS.readBuffer(fileName);
6864
+ }
6772
6865
  realpath(path) {
6773
6866
  return this.valFS.realpath(path);
6774
6867
  }
@@ -7243,6 +7336,88 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
7243
7336
  metadata: v.metadata
7244
7337
  } : value
7245
7338
  });
7339
+ } else if (fix === "images:check-all-files" || fix === "files:check-all-files") {
7340
+ if (!moduleSource || typeof moduleSource !== "object") {
7341
+ remainingErrors.push({
7342
+ ...validationError,
7343
+ message: "Unexpected error while checking gallery metadata (no moduleSource)",
7344
+ fixes: undefined
7345
+ });
7346
+ continue;
7347
+ }
7348
+ const gallerySource = moduleSource;
7349
+ for (const [entryKey, storedEntry] of Object.entries(gallerySource)) {
7350
+ const filename = path__namespace["default"].join(config.projectRoot, entryKey);
7351
+ let buffer;
7352
+ try {
7353
+ buffer = fs__default["default"].readFileSync(filename);
7354
+ } catch {
7355
+ if (apply) {
7356
+ const removePath = patch.sourceToPatchPath(sourcePath).concat([entryKey]);
7357
+ if (patch.isNotRoot(removePath)) {
7358
+ patch$1.push({
7359
+ op: "remove",
7360
+ path: removePath
7361
+ });
7362
+ }
7363
+ } else {
7364
+ remainingErrors.push({
7365
+ ...validationError,
7366
+ message: `Could not read file: ${filename} - file might not exist or can not be accessed`,
7367
+ fixes: undefined
7368
+ });
7369
+ }
7370
+ continue;
7371
+ }
7372
+ if (fix === "images:check-all-files") {
7373
+ const actualMetadata = await extractImageMetadata(filename, buffer);
7374
+ const stored = storedEntry;
7375
+ const metadataIsCorrect = stored.width === actualMetadata.width && stored.height === actualMetadata.height && stored.mimeType === actualMetadata.mimeType;
7376
+ if (!metadataIsCorrect) {
7377
+ if (apply) {
7378
+ patch$1.push({
7379
+ op: "replace",
7380
+ path: patch.sourceToPatchPath(sourcePath).concat([entryKey]),
7381
+ value: {
7382
+ ...stored,
7383
+ width: actualMetadata.width ?? 0,
7384
+ height: actualMetadata.height ?? 0,
7385
+ mimeType: actualMetadata.mimeType ?? "application/octet-stream"
7386
+ }
7387
+ });
7388
+ } else {
7389
+ remainingErrors.push({
7390
+ ...validationError,
7391
+ message: `Image metadata for '${entryKey}' is incorrect (width: ${stored.width ?? "<empty>"} vs ${actualMetadata.width}, height: ${stored.height ?? "<empty>"} vs ${actualMetadata.height}, mimeType: ${stored.mimeType ?? "<empty>"} vs ${actualMetadata.mimeType}). Use --fix to update.`
7392
+ });
7393
+ }
7394
+ }
7395
+ } else if (fix === "files:check-all-files") {
7396
+ const actualMetadata = await extractFileMetadata(filename);
7397
+ const stored = storedEntry;
7398
+ const metadataIsCorrect = stored.mimeType === actualMetadata.mimeType;
7399
+ if (!metadataIsCorrect) {
7400
+ if (apply) {
7401
+ patch$1.push({
7402
+ op: "replace",
7403
+ path: patch.sourceToPatchPath(sourcePath).concat([entryKey]),
7404
+ value: {
7405
+ ...stored,
7406
+ mimeType: actualMetadata.mimeType ?? "application/octet-stream"
7407
+ }
7408
+ });
7409
+ } else {
7410
+ remainingErrors.push({
7411
+ ...validationError,
7412
+ message: `File metadata for '${entryKey}' has incorrect mimeType: '${stored.mimeType ?? "<empty>"}' vs '${actualMetadata.mimeType}'. Use --fix to update.`
7413
+ });
7414
+ }
7415
+ }
7416
+ } else {
7417
+ const exhaustiveCheck = fix;
7418
+ throw new Error(`Internal error: unhandled fix type ${exhaustiveCheck}`);
7419
+ }
7420
+ }
7246
7421
  } else if (fix === "file:check-remote" || fix === "image:check-remote") {
7247
7422
  const v = getRemoteValueFromValidationError(validationError);
7248
7423
  if (!v.success) {
@@ -659,7 +659,7 @@ function removeFromNode(document, node, key) {
659
659
  } else if (ts__default["default"].isObjectLiteralExpression(node)) {
660
660
  return fp.pipe(findObjectPropertyAssignment(node, key), fp.result.flatMap(assignment => {
661
661
  if (!assignment) {
662
- return fp.result.err(new patch.PatchError("Cannot replace object element which does not exist"));
662
+ return fp.result.err(new patch.PatchError("Cannot remove object element which does not exist"));
663
663
  }
664
664
  return fp.result.ok(assignment);
665
665
  }), fp.result.map(assignment => [removeAt(document, node.properties, node.properties.indexOf(assignment))[0], assignment.initializer]));
@@ -1026,7 +1026,14 @@ class ValSourceFileHandler {
1026
1026
  });
1027
1027
  fs__default["default"].writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
1028
1028
  },
1029
- rmFile: fs__default["default"].rmSync
1029
+ rmFile: fs__default["default"].rmSync,
1030
+ readBuffer: fileName => {
1031
+ try {
1032
+ return fs__default["default"].readFileSync(fileName);
1033
+ } catch {
1034
+ return undefined;
1035
+ }
1036
+ }
1030
1037
  }) {
1031
1038
  this.projectRoot = projectRoot;
1032
1039
  this.compilerOptions = compilerOptions;
@@ -1074,7 +1081,14 @@ class ValModuleLoader {
1074
1081
  });
1075
1082
  fs__default["default"].writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
1076
1083
  },
1077
- rmFile: fs__default["default"].rmSync
1084
+ rmFile: fs__default["default"].rmSync,
1085
+ readBuffer: fileName => {
1086
+ try {
1087
+ return fs__default["default"].readFileSync(fileName);
1088
+ } catch {
1089
+ return undefined;
1090
+ }
1091
+ }
1078
1092
  }, disableCache = false) {
1079
1093
  this.projectRoot = projectRoot;
1080
1094
  this.compilerOptions = compilerOptions;
@@ -1367,7 +1381,14 @@ async function createService(projectRoot, opts, host = {
1367
1381
  });
1368
1382
  fs__default["default"].writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
1369
1383
  },
1370
- rmFile: fs__default["default"].rmSync
1384
+ rmFile: fs__default["default"].rmSync,
1385
+ readBuffer: fileName => {
1386
+ try {
1387
+ return fs__default["default"].readFileSync(fileName);
1388
+ } catch {
1389
+ return undefined;
1390
+ }
1391
+ }
1371
1392
  }, loader) {
1372
1393
  const compilerOptions = getCompilerOptions(projectRoot, host);
1373
1394
  const sourceFileHandler = new ValSourceFileHandler(projectRoot, compilerOptions, host);
@@ -1700,8 +1721,10 @@ class ValOps {
1700
1721
  const filePath = op.filePath;
1701
1722
  fileLastUpdatedByPatchId[filePath] = {
1702
1723
  patchId: patch.patchId,
1703
- remote: op.remote
1724
+ remote: op.remote,
1725
+ isDelete: op.value === null
1704
1726
  };
1727
+ continue;
1705
1728
  }
1706
1729
  const path = patch.path;
1707
1730
  if (!patchesByModule[path]) {
@@ -1776,15 +1799,19 @@ class ValOps {
1776
1799
  const fileFixOps = {};
1777
1800
  for (const op of patchData.patch) {
1778
1801
  if (op.op === "file") {
1779
- // NOTE: We insert the last patch_id that modify a file
1780
- // when constructing the url we use the patch id (and the file path)
1781
- // to fetch the right file
1782
- // NOTE: overwrite and use last patch_id if multiple patches modify the same file
1783
- fileFixOps[op.path.join("/")] = [{
1784
- op: "add",
1785
- path: op.path.concat(...(op.nestedFilePath || [])).concat("patch_id"),
1786
- value: patchId
1787
- }];
1802
+ if (op.value !== null) {
1803
+ // NOTE: We insert the last patch_id that modify a file
1804
+ // when constructing the url we use the patch id (and the file path)
1805
+ // to fetch the right file
1806
+ // NOTE: overwrite and use last patch_id if multiple patches modify the same file
1807
+ fileFixOps[op.path.join("/")] = [{
1808
+ op: "add",
1809
+ path: op.path.concat(...(op.nestedFilePath || [])).concat("patch_id"),
1810
+ value: patchId
1811
+ }];
1812
+ }
1813
+ // null value = delete: no patch_id to inject; the "remove" op in
1814
+ // the patch already removes the metadata entry from the source
1788
1815
  } else {
1789
1816
  applicableOps.push(op);
1790
1817
  }
@@ -1897,6 +1924,21 @@ class ValOps {
1897
1924
  const files = {};
1898
1925
  const remoteFiles = {};
1899
1926
  const entries = Object.entries(schemas);
1927
+ // Build a map of gallery directory → [ModuleFilePath, ...] across ALL modules
1928
+ // (must include all modules, not just those being validated, since conflicts can come from any module)
1929
+ const galleryDirectoryToModules = new Map();
1930
+ for (const [moduleFilePathS, schema] of entries) {
1931
+ const serialized = schema["executeSerialize"]();
1932
+ if (serialized.type === "record" && serialized.mediaType && serialized.directory) {
1933
+ const dir = serialized.directory;
1934
+ const existing = galleryDirectoryToModules.get(dir);
1935
+ if (existing) {
1936
+ existing.push(moduleFilePathS);
1937
+ } else {
1938
+ galleryDirectoryToModules.set(dir, [moduleFilePathS]);
1939
+ }
1940
+ }
1941
+ }
1900
1942
  const modulePathsToValidate = patchesByModule && Object.keys(patchesByModule);
1901
1943
  for (const [pathS, schema] of entries) {
1902
1944
  if (modulePathsToValidate && !modulePathsToValidate.includes(pathS)) {
@@ -1937,7 +1979,7 @@ class ValOps {
1937
1979
  };
1938
1980
  if (validationErrors) {
1939
1981
  for (const validationError of validationErrors) {
1940
- var _validationError$fixe, _validationError$fixe2, _validationError$fixe3, _validationError$fixe4;
1982
+ var _validationError$fixe, _validationError$fixe2, _validationError$fixe3, _validationError$fixe4, _validationError$fixe5, _validationError$fixe6, _validationError$fixe7, _validationError$fixe8;
1941
1983
  if (isOnlyFileCheckValidationError(validationError)) {
1942
1984
  if (files[sourcePath]) {
1943
1985
  throw new Error("Cannot have multiple files with same path. Path: " + sourcePath + "; Module: " + path);
@@ -2020,7 +2062,32 @@ class ValOps {
2020
2062
  }
2021
2063
  }
2022
2064
  }
2023
- } else {
2065
+ } else if ((_validationError$fixe5 = validationError.fixes) !== null && _validationError$fixe5 !== void 0 && _validationError$fixe5.includes("images:check-unique-folder") || (_validationError$fixe6 = validationError.fixes) !== null && _validationError$fixe6 !== void 0 && _validationError$fixe6.includes("files:check-unique-folder")) {
2066
+ const TYPE_ERROR_MESSAGE = `This is most likely a Val version mismatch or Val bug.`;
2067
+ if (!validationError.value || typeof validationError.value !== "object") {
2068
+ addError({
2069
+ message: `Could not find a directory value for gallery at ${sourcePath}. ${TYPE_ERROR_MESSAGE}`,
2070
+ typeError: true
2071
+ });
2072
+ } else {
2073
+ const directory = "directory" in validationError.value && validationError.value.directory;
2074
+ if (typeof directory !== "string") {
2075
+ addError({
2076
+ message: `Expected gallery validation error 'value' to have property 'directory' of type 'string'. Found: ${typeof directory}. ${TYPE_ERROR_MESSAGE}`,
2077
+ typeError: true
2078
+ });
2079
+ } else {
2080
+ const modulesUsingDir = galleryDirectoryToModules.get(directory) ?? [];
2081
+ const conflictingModules = modulesUsingDir.filter(m => m !== path);
2082
+ if (conflictingModules.length > 0) {
2083
+ addError({
2084
+ message: `Gallery directory '${directory}' in ${path} conflicts with: ${conflictingModules.join(", ")}. Each gallery must use a unique directory.`
2085
+ });
2086
+ }
2087
+ // If conflictingModules is empty, directory is unique — silently drop the error.
2088
+ }
2089
+ }
2090
+ } else if ((_validationError$fixe7 = validationError.fixes) !== null && _validationError$fixe7 !== void 0 && _validationError$fixe7.includes("images:check-all-files") || (_validationError$fixe8 = validationError.fixes) !== null && _validationError$fixe8 !== void 0 && _validationError$fixe8.includes("files:check-all-files")) ; else {
2024
2091
  addError(validationError);
2025
2092
  }
2026
2093
  }
@@ -2300,16 +2367,22 @@ class ValOps {
2300
2367
  await Promise.all(Object.entries(fileLastUpdatedByPatchId).map(async ([filePath, patchData]) => {
2301
2368
  const {
2302
2369
  patchId,
2303
- remote
2370
+ remote,
2371
+ isDelete
2304
2372
  } = patchData;
2305
2373
  if (globalAppliedPatches.includes(patchId)) {
2306
- // TODO: do we want to make sure the file is there? Then again, it should be rare that it happens (unless there's a Val bug) so it might be enough to fail later (at commit)
2307
- // TODO: include sha256? This way we can make sure we pick the right file since theoretically there could be multiple files with the same path in the same patch
2308
- // or is that the case? We are picking the latest file by path so, that should be enough?
2309
- patchedBinaryFilesDescriptors[filePath] = {
2310
- patchId,
2311
- remote
2312
- };
2374
+ if (isDelete) {
2375
+ // Signal file deletion via patchedSourceFiles null entry
2376
+ patchedSourceFiles[filePath] = null;
2377
+ } else {
2378
+ // TODO: do we want to make sure the file is there? Then again, it should be rare that it happens (unless there's a Val bug) so it might be enough to fail later (at commit)
2379
+ // TODO: include sha256? This way we can make sure we pick the right file since theoretically there could be multiple files with the same path in the same patch
2380
+ // or is that the case? We are picking the latest file by path so, that should be enough?
2381
+ patchedBinaryFilesDescriptors[filePath] = {
2382
+ patchId,
2383
+ remote
2384
+ };
2385
+ }
2313
2386
  } else {
2314
2387
  hasErrors = true;
2315
2388
  binaryFilePatchErrors[filePath] = {
@@ -2355,8 +2428,8 @@ class ValOps {
2355
2428
  // #region abstract ops
2356
2429
  }
2357
2430
  function isOnlyFileCheckValidationError(validationError) {
2358
- var _validationError$fixe5;
2359
- if ((_validationError$fixe5 = validationError.fixes) !== null && _validationError$fixe5 !== void 0 && _validationError$fixe5.every(f => f === "file:check-metadata" || f === "image:check-metadata")) {
2431
+ var _validationError$fixe9;
2432
+ if ((_validationError$fixe9 = validationError.fixes) !== null && _validationError$fixe9 !== void 0 && _validationError$fixe9.every(f => f === "file:check-metadata" || f === "image:check-metadata")) {
2360
2433
  return true;
2361
2434
  }
2362
2435
  return false;
@@ -2987,6 +3060,14 @@ class ValOpsFS extends ValOps {
2987
3060
  const patchFilePath = this.getBinaryFilePath(filePath, patchDir);
2988
3061
  const metadataFilePath = this.getBinaryFileMetadataPath(filePath, patchDir);
2989
3062
  try {
3063
+ if (data === null) {
3064
+ this.host.deleteFile(patchFilePath);
3065
+ this.host.deleteFile(metadataFilePath);
3066
+ return {
3067
+ patchId,
3068
+ filePath
3069
+ };
3070
+ }
2990
3071
  const buffer = bufferFromDataUrl(data);
2991
3072
  if (!buffer) {
2992
3073
  return {
@@ -3247,7 +3328,11 @@ class ValOpsFS extends ValOps {
3247
3328
  for (const [filePath, data] of Object.entries(preparedCommit.patchedSourceFiles)) {
3248
3329
  const absPath = path__namespace["default"].join(this.rootDir, ...filePath.split("/"));
3249
3330
  try {
3250
- this.host.writeUf8File(absPath, data);
3331
+ if (data === null) {
3332
+ this.host.deleteFile(absPath);
3333
+ } else {
3334
+ this.host.writeUf8File(absPath, data);
3335
+ }
3251
3336
  updatedFiles.push(absPath);
3252
3337
  } catch (err) {
3253
3338
  errors[absPath] = {
@@ -3373,6 +3458,11 @@ class FSOpsHost {
3373
3458
  });
3374
3459
  }
3375
3460
  }
3461
+ deleteFile(path) {
3462
+ if (this.fileExists(path)) {
3463
+ fs__default["default"].rmSync(path);
3464
+ }
3465
+ }
3376
3466
  moveDir(from, to) {
3377
3467
  fs__default["default"].renameSync(from, to);
3378
3468
  }
@@ -6769,6 +6859,9 @@ class ValFSHost {
6769
6859
  readFile(fileName) {
6770
6860
  return this.valFS.readFile(fileName);
6771
6861
  }
6862
+ readBuffer(fileName) {
6863
+ return this.valFS.readBuffer(fileName);
6864
+ }
6772
6865
  realpath(path) {
6773
6866
  return this.valFS.realpath(path);
6774
6867
  }
@@ -7243,6 +7336,88 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
7243
7336
  metadata: v.metadata
7244
7337
  } : value
7245
7338
  });
7339
+ } else if (fix === "images:check-all-files" || fix === "files:check-all-files") {
7340
+ if (!moduleSource || typeof moduleSource !== "object") {
7341
+ remainingErrors.push({
7342
+ ...validationError,
7343
+ message: "Unexpected error while checking gallery metadata (no moduleSource)",
7344
+ fixes: undefined
7345
+ });
7346
+ continue;
7347
+ }
7348
+ const gallerySource = moduleSource;
7349
+ for (const [entryKey, storedEntry] of Object.entries(gallerySource)) {
7350
+ const filename = path__namespace["default"].join(config.projectRoot, entryKey);
7351
+ let buffer;
7352
+ try {
7353
+ buffer = fs__default["default"].readFileSync(filename);
7354
+ } catch {
7355
+ if (apply) {
7356
+ const removePath = patch.sourceToPatchPath(sourcePath).concat([entryKey]);
7357
+ if (patch.isNotRoot(removePath)) {
7358
+ patch$1.push({
7359
+ op: "remove",
7360
+ path: removePath
7361
+ });
7362
+ }
7363
+ } else {
7364
+ remainingErrors.push({
7365
+ ...validationError,
7366
+ message: `Could not read file: ${filename} - file might not exist or can not be accessed`,
7367
+ fixes: undefined
7368
+ });
7369
+ }
7370
+ continue;
7371
+ }
7372
+ if (fix === "images:check-all-files") {
7373
+ const actualMetadata = await extractImageMetadata(filename, buffer);
7374
+ const stored = storedEntry;
7375
+ const metadataIsCorrect = stored.width === actualMetadata.width && stored.height === actualMetadata.height && stored.mimeType === actualMetadata.mimeType;
7376
+ if (!metadataIsCorrect) {
7377
+ if (apply) {
7378
+ patch$1.push({
7379
+ op: "replace",
7380
+ path: patch.sourceToPatchPath(sourcePath).concat([entryKey]),
7381
+ value: {
7382
+ ...stored,
7383
+ width: actualMetadata.width ?? 0,
7384
+ height: actualMetadata.height ?? 0,
7385
+ mimeType: actualMetadata.mimeType ?? "application/octet-stream"
7386
+ }
7387
+ });
7388
+ } else {
7389
+ remainingErrors.push({
7390
+ ...validationError,
7391
+ message: `Image metadata for '${entryKey}' is incorrect (width: ${stored.width ?? "<empty>"} vs ${actualMetadata.width}, height: ${stored.height ?? "<empty>"} vs ${actualMetadata.height}, mimeType: ${stored.mimeType ?? "<empty>"} vs ${actualMetadata.mimeType}). Use --fix to update.`
7392
+ });
7393
+ }
7394
+ }
7395
+ } else if (fix === "files:check-all-files") {
7396
+ const actualMetadata = await extractFileMetadata(filename);
7397
+ const stored = storedEntry;
7398
+ const metadataIsCorrect = stored.mimeType === actualMetadata.mimeType;
7399
+ if (!metadataIsCorrect) {
7400
+ if (apply) {
7401
+ patch$1.push({
7402
+ op: "replace",
7403
+ path: patch.sourceToPatchPath(sourcePath).concat([entryKey]),
7404
+ value: {
7405
+ ...stored,
7406
+ mimeType: actualMetadata.mimeType ?? "application/octet-stream"
7407
+ }
7408
+ });
7409
+ } else {
7410
+ remainingErrors.push({
7411
+ ...validationError,
7412
+ message: `File metadata for '${entryKey}' has incorrect mimeType: '${stored.mimeType ?? "<empty>"}' vs '${actualMetadata.mimeType}'. Use --fix to update.`
7413
+ });
7414
+ }
7415
+ }
7416
+ } else {
7417
+ const exhaustiveCheck = fix;
7418
+ throw new Error(`Internal error: unhandled fix type ${exhaustiveCheck}`);
7419
+ }
7420
+ }
7246
7421
  } else if (fix === "file:check-remote" || fix === "image:check-remote") {
7247
7422
  const v = getRemoteValueFromValidationError(validationError);
7248
7423
  if (!v.success) {
@@ -628,7 +628,7 @@ function removeFromNode(document, node, key) {
628
628
  } else if (ts.isObjectLiteralExpression(node)) {
629
629
  return pipe(findObjectPropertyAssignment(node, key), result.flatMap(assignment => {
630
630
  if (!assignment) {
631
- return result.err(new PatchError("Cannot replace object element which does not exist"));
631
+ return result.err(new PatchError("Cannot remove object element which does not exist"));
632
632
  }
633
633
  return result.ok(assignment);
634
634
  }), result.map(assignment => [removeAt(document, node.properties, node.properties.indexOf(assignment))[0], assignment.initializer]));
@@ -995,7 +995,14 @@ class ValSourceFileHandler {
995
995
  });
996
996
  fs.writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
997
997
  },
998
- rmFile: fs.rmSync
998
+ rmFile: fs.rmSync,
999
+ readBuffer: fileName => {
1000
+ try {
1001
+ return fs.readFileSync(fileName);
1002
+ } catch {
1003
+ return undefined;
1004
+ }
1005
+ }
999
1006
  }) {
1000
1007
  this.projectRoot = projectRoot;
1001
1008
  this.compilerOptions = compilerOptions;
@@ -1043,7 +1050,14 @@ class ValModuleLoader {
1043
1050
  });
1044
1051
  fs.writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
1045
1052
  },
1046
- rmFile: fs.rmSync
1053
+ rmFile: fs.rmSync,
1054
+ readBuffer: fileName => {
1055
+ try {
1056
+ return fs.readFileSync(fileName);
1057
+ } catch {
1058
+ return undefined;
1059
+ }
1060
+ }
1047
1061
  }, disableCache = false) {
1048
1062
  this.projectRoot = projectRoot;
1049
1063
  this.compilerOptions = compilerOptions;
@@ -1336,7 +1350,14 @@ async function createService(projectRoot, opts, host = {
1336
1350
  });
1337
1351
  fs.writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
1338
1352
  },
1339
- rmFile: fs.rmSync
1353
+ rmFile: fs.rmSync,
1354
+ readBuffer: fileName => {
1355
+ try {
1356
+ return fs.readFileSync(fileName);
1357
+ } catch {
1358
+ return undefined;
1359
+ }
1360
+ }
1340
1361
  }, loader) {
1341
1362
  const compilerOptions = getCompilerOptions(projectRoot, host);
1342
1363
  const sourceFileHandler = new ValSourceFileHandler(projectRoot, compilerOptions, host);
@@ -1669,8 +1690,10 @@ class ValOps {
1669
1690
  const filePath = op.filePath;
1670
1691
  fileLastUpdatedByPatchId[filePath] = {
1671
1692
  patchId: patch.patchId,
1672
- remote: op.remote
1693
+ remote: op.remote,
1694
+ isDelete: op.value === null
1673
1695
  };
1696
+ continue;
1674
1697
  }
1675
1698
  const path = patch.path;
1676
1699
  if (!patchesByModule[path]) {
@@ -1745,15 +1768,19 @@ class ValOps {
1745
1768
  const fileFixOps = {};
1746
1769
  for (const op of patchData.patch) {
1747
1770
  if (op.op === "file") {
1748
- // NOTE: We insert the last patch_id that modify a file
1749
- // when constructing the url we use the patch id (and the file path)
1750
- // to fetch the right file
1751
- // NOTE: overwrite and use last patch_id if multiple patches modify the same file
1752
- fileFixOps[op.path.join("/")] = [{
1753
- op: "add",
1754
- path: op.path.concat(...(op.nestedFilePath || [])).concat("patch_id"),
1755
- value: patchId
1756
- }];
1771
+ if (op.value !== null) {
1772
+ // NOTE: We insert the last patch_id that modify a file
1773
+ // when constructing the url we use the patch id (and the file path)
1774
+ // to fetch the right file
1775
+ // NOTE: overwrite and use last patch_id if multiple patches modify the same file
1776
+ fileFixOps[op.path.join("/")] = [{
1777
+ op: "add",
1778
+ path: op.path.concat(...(op.nestedFilePath || [])).concat("patch_id"),
1779
+ value: patchId
1780
+ }];
1781
+ }
1782
+ // null value = delete: no patch_id to inject; the "remove" op in
1783
+ // the patch already removes the metadata entry from the source
1757
1784
  } else {
1758
1785
  applicableOps.push(op);
1759
1786
  }
@@ -1866,6 +1893,21 @@ class ValOps {
1866
1893
  const files = {};
1867
1894
  const remoteFiles = {};
1868
1895
  const entries = Object.entries(schemas);
1896
+ // Build a map of gallery directory → [ModuleFilePath, ...] across ALL modules
1897
+ // (must include all modules, not just those being validated, since conflicts can come from any module)
1898
+ const galleryDirectoryToModules = new Map();
1899
+ for (const [moduleFilePathS, schema] of entries) {
1900
+ const serialized = schema["executeSerialize"]();
1901
+ if (serialized.type === "record" && serialized.mediaType && serialized.directory) {
1902
+ const dir = serialized.directory;
1903
+ const existing = galleryDirectoryToModules.get(dir);
1904
+ if (existing) {
1905
+ existing.push(moduleFilePathS);
1906
+ } else {
1907
+ galleryDirectoryToModules.set(dir, [moduleFilePathS]);
1908
+ }
1909
+ }
1910
+ }
1869
1911
  const modulePathsToValidate = patchesByModule && Object.keys(patchesByModule);
1870
1912
  for (const [pathS, schema] of entries) {
1871
1913
  if (modulePathsToValidate && !modulePathsToValidate.includes(pathS)) {
@@ -1906,7 +1948,7 @@ class ValOps {
1906
1948
  };
1907
1949
  if (validationErrors) {
1908
1950
  for (const validationError of validationErrors) {
1909
- var _validationError$fixe, _validationError$fixe2, _validationError$fixe3, _validationError$fixe4;
1951
+ var _validationError$fixe, _validationError$fixe2, _validationError$fixe3, _validationError$fixe4, _validationError$fixe5, _validationError$fixe6, _validationError$fixe7, _validationError$fixe8;
1910
1952
  if (isOnlyFileCheckValidationError(validationError)) {
1911
1953
  if (files[sourcePath]) {
1912
1954
  throw new Error("Cannot have multiple files with same path. Path: " + sourcePath + "; Module: " + path);
@@ -1989,7 +2031,32 @@ class ValOps {
1989
2031
  }
1990
2032
  }
1991
2033
  }
1992
- } else {
2034
+ } else if ((_validationError$fixe5 = validationError.fixes) !== null && _validationError$fixe5 !== void 0 && _validationError$fixe5.includes("images:check-unique-folder") || (_validationError$fixe6 = validationError.fixes) !== null && _validationError$fixe6 !== void 0 && _validationError$fixe6.includes("files:check-unique-folder")) {
2035
+ const TYPE_ERROR_MESSAGE = `This is most likely a Val version mismatch or Val bug.`;
2036
+ if (!validationError.value || typeof validationError.value !== "object") {
2037
+ addError({
2038
+ message: `Could not find a directory value for gallery at ${sourcePath}. ${TYPE_ERROR_MESSAGE}`,
2039
+ typeError: true
2040
+ });
2041
+ } else {
2042
+ const directory = "directory" in validationError.value && validationError.value.directory;
2043
+ if (typeof directory !== "string") {
2044
+ addError({
2045
+ message: `Expected gallery validation error 'value' to have property 'directory' of type 'string'. Found: ${typeof directory}. ${TYPE_ERROR_MESSAGE}`,
2046
+ typeError: true
2047
+ });
2048
+ } else {
2049
+ const modulesUsingDir = galleryDirectoryToModules.get(directory) ?? [];
2050
+ const conflictingModules = modulesUsingDir.filter(m => m !== path);
2051
+ if (conflictingModules.length > 0) {
2052
+ addError({
2053
+ message: `Gallery directory '${directory}' in ${path} conflicts with: ${conflictingModules.join(", ")}. Each gallery must use a unique directory.`
2054
+ });
2055
+ }
2056
+ // If conflictingModules is empty, directory is unique — silently drop the error.
2057
+ }
2058
+ }
2059
+ } else if ((_validationError$fixe7 = validationError.fixes) !== null && _validationError$fixe7 !== void 0 && _validationError$fixe7.includes("images:check-all-files") || (_validationError$fixe8 = validationError.fixes) !== null && _validationError$fixe8 !== void 0 && _validationError$fixe8.includes("files:check-all-files")) ; else {
1993
2060
  addError(validationError);
1994
2061
  }
1995
2062
  }
@@ -2269,16 +2336,22 @@ class ValOps {
2269
2336
  await Promise.all(Object.entries(fileLastUpdatedByPatchId).map(async ([filePath, patchData]) => {
2270
2337
  const {
2271
2338
  patchId,
2272
- remote
2339
+ remote,
2340
+ isDelete
2273
2341
  } = patchData;
2274
2342
  if (globalAppliedPatches.includes(patchId)) {
2275
- // TODO: do we want to make sure the file is there? Then again, it should be rare that it happens (unless there's a Val bug) so it might be enough to fail later (at commit)
2276
- // TODO: include sha256? This way we can make sure we pick the right file since theoretically there could be multiple files with the same path in the same patch
2277
- // or is that the case? We are picking the latest file by path so, that should be enough?
2278
- patchedBinaryFilesDescriptors[filePath] = {
2279
- patchId,
2280
- remote
2281
- };
2343
+ if (isDelete) {
2344
+ // Signal file deletion via patchedSourceFiles null entry
2345
+ patchedSourceFiles[filePath] = null;
2346
+ } else {
2347
+ // TODO: do we want to make sure the file is there? Then again, it should be rare that it happens (unless there's a Val bug) so it might be enough to fail later (at commit)
2348
+ // TODO: include sha256? This way we can make sure we pick the right file since theoretically there could be multiple files with the same path in the same patch
2349
+ // or is that the case? We are picking the latest file by path so, that should be enough?
2350
+ patchedBinaryFilesDescriptors[filePath] = {
2351
+ patchId,
2352
+ remote
2353
+ };
2354
+ }
2282
2355
  } else {
2283
2356
  hasErrors = true;
2284
2357
  binaryFilePatchErrors[filePath] = {
@@ -2324,8 +2397,8 @@ class ValOps {
2324
2397
  // #region abstract ops
2325
2398
  }
2326
2399
  function isOnlyFileCheckValidationError(validationError) {
2327
- var _validationError$fixe5;
2328
- if ((_validationError$fixe5 = validationError.fixes) !== null && _validationError$fixe5 !== void 0 && _validationError$fixe5.every(f => f === "file:check-metadata" || f === "image:check-metadata")) {
2400
+ var _validationError$fixe9;
2401
+ if ((_validationError$fixe9 = validationError.fixes) !== null && _validationError$fixe9 !== void 0 && _validationError$fixe9.every(f => f === "file:check-metadata" || f === "image:check-metadata")) {
2329
2402
  return true;
2330
2403
  }
2331
2404
  return false;
@@ -2956,6 +3029,14 @@ class ValOpsFS extends ValOps {
2956
3029
  const patchFilePath = this.getBinaryFilePath(filePath, patchDir);
2957
3030
  const metadataFilePath = this.getBinaryFileMetadataPath(filePath, patchDir);
2958
3031
  try {
3032
+ if (data === null) {
3033
+ this.host.deleteFile(patchFilePath);
3034
+ this.host.deleteFile(metadataFilePath);
3035
+ return {
3036
+ patchId,
3037
+ filePath
3038
+ };
3039
+ }
2959
3040
  const buffer = bufferFromDataUrl(data);
2960
3041
  if (!buffer) {
2961
3042
  return {
@@ -3216,7 +3297,11 @@ class ValOpsFS extends ValOps {
3216
3297
  for (const [filePath, data] of Object.entries(preparedCommit.patchedSourceFiles)) {
3217
3298
  const absPath = path__default.join(this.rootDir, ...filePath.split("/"));
3218
3299
  try {
3219
- this.host.writeUf8File(absPath, data);
3300
+ if (data === null) {
3301
+ this.host.deleteFile(absPath);
3302
+ } else {
3303
+ this.host.writeUf8File(absPath, data);
3304
+ }
3220
3305
  updatedFiles.push(absPath);
3221
3306
  } catch (err) {
3222
3307
  errors[absPath] = {
@@ -3342,6 +3427,11 @@ class FSOpsHost {
3342
3427
  });
3343
3428
  }
3344
3429
  }
3430
+ deleteFile(path) {
3431
+ if (this.fileExists(path)) {
3432
+ fs.rmSync(path);
3433
+ }
3434
+ }
3345
3435
  moveDir(from, to) {
3346
3436
  fs.renameSync(from, to);
3347
3437
  }
@@ -6738,6 +6828,9 @@ class ValFSHost {
6738
6828
  readFile(fileName) {
6739
6829
  return this.valFS.readFile(fileName);
6740
6830
  }
6831
+ readBuffer(fileName) {
6832
+ return this.valFS.readBuffer(fileName);
6833
+ }
6741
6834
  realpath(path) {
6742
6835
  return this.valFS.realpath(path);
6743
6836
  }
@@ -7212,6 +7305,88 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
7212
7305
  metadata: v.metadata
7213
7306
  } : value
7214
7307
  });
7308
+ } else if (fix === "images:check-all-files" || fix === "files:check-all-files") {
7309
+ if (!moduleSource || typeof moduleSource !== "object") {
7310
+ remainingErrors.push({
7311
+ ...validationError,
7312
+ message: "Unexpected error while checking gallery metadata (no moduleSource)",
7313
+ fixes: undefined
7314
+ });
7315
+ continue;
7316
+ }
7317
+ const gallerySource = moduleSource;
7318
+ for (const [entryKey, storedEntry] of Object.entries(gallerySource)) {
7319
+ const filename = path__default.join(config.projectRoot, entryKey);
7320
+ let buffer;
7321
+ try {
7322
+ buffer = fs.readFileSync(filename);
7323
+ } catch {
7324
+ if (apply) {
7325
+ const removePath = sourceToPatchPath(sourcePath).concat([entryKey]);
7326
+ if (isNotRoot(removePath)) {
7327
+ patch.push({
7328
+ op: "remove",
7329
+ path: removePath
7330
+ });
7331
+ }
7332
+ } else {
7333
+ remainingErrors.push({
7334
+ ...validationError,
7335
+ message: `Could not read file: ${filename} - file might not exist or can not be accessed`,
7336
+ fixes: undefined
7337
+ });
7338
+ }
7339
+ continue;
7340
+ }
7341
+ if (fix === "images:check-all-files") {
7342
+ const actualMetadata = await extractImageMetadata(filename, buffer);
7343
+ const stored = storedEntry;
7344
+ const metadataIsCorrect = stored.width === actualMetadata.width && stored.height === actualMetadata.height && stored.mimeType === actualMetadata.mimeType;
7345
+ if (!metadataIsCorrect) {
7346
+ if (apply) {
7347
+ patch.push({
7348
+ op: "replace",
7349
+ path: sourceToPatchPath(sourcePath).concat([entryKey]),
7350
+ value: {
7351
+ ...stored,
7352
+ width: actualMetadata.width ?? 0,
7353
+ height: actualMetadata.height ?? 0,
7354
+ mimeType: actualMetadata.mimeType ?? "application/octet-stream"
7355
+ }
7356
+ });
7357
+ } else {
7358
+ remainingErrors.push({
7359
+ ...validationError,
7360
+ message: `Image metadata for '${entryKey}' is incorrect (width: ${stored.width ?? "<empty>"} vs ${actualMetadata.width}, height: ${stored.height ?? "<empty>"} vs ${actualMetadata.height}, mimeType: ${stored.mimeType ?? "<empty>"} vs ${actualMetadata.mimeType}). Use --fix to update.`
7361
+ });
7362
+ }
7363
+ }
7364
+ } else if (fix === "files:check-all-files") {
7365
+ const actualMetadata = await extractFileMetadata(filename);
7366
+ const stored = storedEntry;
7367
+ const metadataIsCorrect = stored.mimeType === actualMetadata.mimeType;
7368
+ if (!metadataIsCorrect) {
7369
+ if (apply) {
7370
+ patch.push({
7371
+ op: "replace",
7372
+ path: sourceToPatchPath(sourcePath).concat([entryKey]),
7373
+ value: {
7374
+ ...stored,
7375
+ mimeType: actualMetadata.mimeType ?? "application/octet-stream"
7376
+ }
7377
+ });
7378
+ } else {
7379
+ remainingErrors.push({
7380
+ ...validationError,
7381
+ message: `File metadata for '${entryKey}' has incorrect mimeType: '${stored.mimeType ?? "<empty>"}' vs '${actualMetadata.mimeType}'. Use --fix to update.`
7382
+ });
7383
+ }
7384
+ }
7385
+ } else {
7386
+ const exhaustiveCheck = fix;
7387
+ throw new Error(`Internal error: unhandled fix type ${exhaustiveCheck}`);
7388
+ }
7389
+ }
7215
7390
  } else if (fix === "file:check-remote" || fix === "image:check-remote") {
7216
7391
  const v = getRemoteValueFromValidationError(validationError);
7217
7392
  if (!v.success) {
package/package.json CHANGED
@@ -16,7 +16,7 @@
16
16
  "./package.json": "./package.json"
17
17
  },
18
18
  "types": "dist/valbuild-server.cjs.d.ts",
19
- "version": "0.93.0",
19
+ "version": "0.94.0",
20
20
  "devDependencies": {
21
21
  "@prettier/sync": "^0.6.1",
22
22
  "@types/jest": "^30.0.0"
@@ -30,9 +30,9 @@
30
30
  "typescript": "^5.9.3",
31
31
  "zod": "^4.3.5",
32
32
  "zod-validation-error": "^5.0.0",
33
- "@valbuild/core": "0.93.0",
34
- "@valbuild/shared": "0.93.0",
35
- "@valbuild/ui": "0.93.0"
33
+ "@valbuild/core": "0.94.0",
34
+ "@valbuild/shared": "0.94.0",
35
+ "@valbuild/ui": "0.94.0"
36
36
  },
37
37
  "engines": {
38
38
  "node": ">=18.17.0"