@valbuild/server 0.92.1 → 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
|
|
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
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
op
|
|
1785
|
-
|
|
1786
|
-
|
|
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
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
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$
|
|
2359
|
-
if ((_validationError$
|
|
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
|
-
|
|
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
|
}
|
|
@@ -5718,10 +5808,15 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
5718
5808
|
}
|
|
5719
5809
|
};
|
|
5720
5810
|
}
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5811
|
+
let patchOps = {
|
|
5812
|
+
patches: []
|
|
5813
|
+
};
|
|
5814
|
+
if (query.exclude_patches !== true) {
|
|
5815
|
+
patchOps = await serverOps.fetchPatches({
|
|
5816
|
+
patchIds: undefined,
|
|
5817
|
+
excludePatchOps: false
|
|
5818
|
+
});
|
|
5819
|
+
}
|
|
5725
5820
|
// We check authorization here, because it is the first call to the backend
|
|
5726
5821
|
if (patchOps.error && patchOps.unauthorized) {
|
|
5727
5822
|
return {
|
|
@@ -6764,6 +6859,9 @@ class ValFSHost {
|
|
|
6764
6859
|
readFile(fileName) {
|
|
6765
6860
|
return this.valFS.readFile(fileName);
|
|
6766
6861
|
}
|
|
6862
|
+
readBuffer(fileName) {
|
|
6863
|
+
return this.valFS.readBuffer(fileName);
|
|
6864
|
+
}
|
|
6767
6865
|
realpath(path) {
|
|
6768
6866
|
return this.valFS.realpath(path);
|
|
6769
6867
|
}
|
|
@@ -7238,6 +7336,88 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
7238
7336
|
metadata: v.metadata
|
|
7239
7337
|
} : value
|
|
7240
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
|
+
}
|
|
7241
7421
|
} else if (fix === "file:check-remote" || fix === "image:check-remote") {
|
|
7242
7422
|
const v = getRemoteValueFromValidationError(validationError);
|
|
7243
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
|
|
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
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
op
|
|
1785
|
-
|
|
1786
|
-
|
|
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
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
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$
|
|
2359
|
-
if ((_validationError$
|
|
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
|
-
|
|
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
|
}
|
|
@@ -5718,10 +5808,15 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
5718
5808
|
}
|
|
5719
5809
|
};
|
|
5720
5810
|
}
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5811
|
+
let patchOps = {
|
|
5812
|
+
patches: []
|
|
5813
|
+
};
|
|
5814
|
+
if (query.exclude_patches !== true) {
|
|
5815
|
+
patchOps = await serverOps.fetchPatches({
|
|
5816
|
+
patchIds: undefined,
|
|
5817
|
+
excludePatchOps: false
|
|
5818
|
+
});
|
|
5819
|
+
}
|
|
5725
5820
|
// We check authorization here, because it is the first call to the backend
|
|
5726
5821
|
if (patchOps.error && patchOps.unauthorized) {
|
|
5727
5822
|
return {
|
|
@@ -6764,6 +6859,9 @@ class ValFSHost {
|
|
|
6764
6859
|
readFile(fileName) {
|
|
6765
6860
|
return this.valFS.readFile(fileName);
|
|
6766
6861
|
}
|
|
6862
|
+
readBuffer(fileName) {
|
|
6863
|
+
return this.valFS.readBuffer(fileName);
|
|
6864
|
+
}
|
|
6767
6865
|
realpath(path) {
|
|
6768
6866
|
return this.valFS.realpath(path);
|
|
6769
6867
|
}
|
|
@@ -7238,6 +7336,88 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
7238
7336
|
metadata: v.metadata
|
|
7239
7337
|
} : value
|
|
7240
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
|
+
}
|
|
7241
7421
|
} else if (fix === "file:check-remote" || fix === "image:check-remote") {
|
|
7242
7422
|
const v = getRemoteValueFromValidationError(validationError);
|
|
7243
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
|
|
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
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
op
|
|
1754
|
-
|
|
1755
|
-
|
|
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
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
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$
|
|
2328
|
-
if ((_validationError$
|
|
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
|
-
|
|
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
|
}
|
|
@@ -5687,10 +5777,15 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
5687
5777
|
}
|
|
5688
5778
|
};
|
|
5689
5779
|
}
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5780
|
+
let patchOps = {
|
|
5781
|
+
patches: []
|
|
5782
|
+
};
|
|
5783
|
+
if (query.exclude_patches !== true) {
|
|
5784
|
+
patchOps = await serverOps.fetchPatches({
|
|
5785
|
+
patchIds: undefined,
|
|
5786
|
+
excludePatchOps: false
|
|
5787
|
+
});
|
|
5788
|
+
}
|
|
5694
5789
|
// We check authorization here, because it is the first call to the backend
|
|
5695
5790
|
if (patchOps.error && patchOps.unauthorized) {
|
|
5696
5791
|
return {
|
|
@@ -6733,6 +6828,9 @@ class ValFSHost {
|
|
|
6733
6828
|
readFile(fileName) {
|
|
6734
6829
|
return this.valFS.readFile(fileName);
|
|
6735
6830
|
}
|
|
6831
|
+
readBuffer(fileName) {
|
|
6832
|
+
return this.valFS.readBuffer(fileName);
|
|
6833
|
+
}
|
|
6736
6834
|
realpath(path) {
|
|
6737
6835
|
return this.valFS.realpath(path);
|
|
6738
6836
|
}
|
|
@@ -7207,6 +7305,88 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
7207
7305
|
metadata: v.metadata
|
|
7208
7306
|
} : value
|
|
7209
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
|
+
}
|
|
7210
7390
|
} else if (fix === "file:check-remote" || fix === "image:check-remote") {
|
|
7211
7391
|
const v = getRemoteValueFromValidationError(validationError);
|
|
7212
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.
|
|
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.
|
|
34
|
-
"@valbuild/shared": "0.
|
|
35
|
-
"@valbuild/ui": "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"
|