@valbuild/server 0.123.0 → 0.123.2
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.
- package/CHANGELOG.md +31 -0
- package/dist/declarations/src/fixHandlers.d.ts +29 -0
- package/dist/valbuild-server.cjs.dev.js +127 -37
- package/dist/valbuild-server.cjs.prod.js +127 -37
- package/dist/valbuild-server.esm.js +127 -37
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @valbuild/server
|
|
2
2
|
|
|
3
|
+
## 0.123.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#619](https://github.com/valbuild/val/pull/619) [`8e58c34`](https://github.com/valbuild/val/commit/8e58c3495d1bf0f221a57082cb0a3045929722a1) Thanks [@freekh](https://github.com/freekh)! - Stop `val validate` reporting published remote gallery images as missing — and `--fix` deleting them
|
|
8
|
+
|
|
9
|
+
A remote gallery (`s.images({ remote: true })`) keys an uploaded entry by its
|
|
10
|
+
remote URL. Two separate checks read that key, normalised it back to the local
|
|
11
|
+
path it encodes, and then required a file to be sitting there:
|
|
12
|
+
|
|
13
|
+
- `val validate` reported _"Gallery … has tracked files that do not exist on
|
|
14
|
+
disk"_ for every published remote image;
|
|
15
|
+
- `val validate --fix` **removed the entry from the gallery**, silently deleting
|
|
16
|
+
the reference to a file that was safely on the content host.
|
|
17
|
+
|
|
18
|
+
Both were wrong for the same reason. Publishing uploads remote files to the
|
|
19
|
+
content host and copies only local ones into the working tree, so an image added
|
|
20
|
+
through the Studio — or over MCP — has no file in the repo by design. Putting one
|
|
21
|
+
there is exactly what remote storage exists to avoid.
|
|
22
|
+
|
|
23
|
+
Remote entries are now exempt from both the missing-file check and the
|
|
24
|
+
metadata-from-disk verification. Whether a remote entry is sound is
|
|
25
|
+
`image:check-remote`'s question, and it already asks it. Nothing changes for
|
|
26
|
+
local entries, or for a remote entry that does have a local file — `--fix`
|
|
27
|
+
promotes a local file to a remote ref and leaves the file where it was, and that
|
|
28
|
+
file is still counted as tracked rather than reported as untracked.
|
|
29
|
+
|
|
30
|
+
- Updated dependencies [[`04b6d4c`](https://github.com/valbuild/val/commit/04b6d4cbbecc131bbaf3c20633af9dad8c857310), [`3e93508`](https://github.com/valbuild/val/commit/3e93508b05d08b0c24947a97c6141a9ad8a3931e)]:
|
|
31
|
+
- @valbuild/shared@0.123.2
|
|
32
|
+
- @valbuild/ui@0.123.2
|
|
33
|
+
|
|
3
34
|
## 0.123.0
|
|
4
35
|
|
|
5
36
|
### Minor Changes
|
|
@@ -162,6 +162,35 @@ export declare function handleRemoteGalleryFileUpload(ctx: FixHandlerContext): P
|
|
|
162
162
|
export declare function handleRemoteFileDownload(ctx: FixHandlerContext): Promise<FixHandlerResult>;
|
|
163
163
|
export declare function handleRemoteFileCheck(): Promise<FixHandlerResult>;
|
|
164
164
|
export declare function handleUniqueFolderCheck(ctx: FixHandlerContext): Promise<FixHandlerResult>;
|
|
165
|
+
/**
|
|
166
|
+
* What is out of step between a gallery's entries and its directory.
|
|
167
|
+
*
|
|
168
|
+
* Two questions, and they treat a remote entry differently — which is the whole
|
|
169
|
+
* reason this is separate from the handler around it:
|
|
170
|
+
*
|
|
171
|
+
* - **Missing**: an entry with no bytes at its local path. Asked of LOCAL
|
|
172
|
+
* entries only. A remote entry's bytes live on the content host, and nothing
|
|
173
|
+
* puts a copy in the working tree: `saveOrUploadFiles` uploads the remote
|
|
174
|
+
* descriptors and copies only the local ones into the tree, so a remote entry
|
|
175
|
+
* added through the Studio (or over MCP) has no local file by design, and
|
|
176
|
+
* demanding one would mean committing remote bytes to git — which is what
|
|
177
|
+
* remote storage exists to avoid. Whether those bytes really are on the host
|
|
178
|
+
* is a different check, `image:check-remote`, which already runs for exactly
|
|
179
|
+
* these entries.
|
|
180
|
+
* - **Untracked**: a file in the directory that no entry claims. Asked of every
|
|
181
|
+
* entry, remote included, and that is why they are normalised to their local
|
|
182
|
+
* path: `val validate --fix` promotes a local file to a remote ref and leaves
|
|
183
|
+
* the file where it was, so a remote entry can perfectly well have one.
|
|
184
|
+
*/
|
|
185
|
+
export declare function checkGalleryFiles(input: {
|
|
186
|
+
entryKeys: string[];
|
|
187
|
+
directory: string;
|
|
188
|
+
projectRoot: string;
|
|
189
|
+
fs: Pick<IValFSHost, "fileExists" | "readDirectory">;
|
|
190
|
+
}): {
|
|
191
|
+
missingTrackedFiles: string[];
|
|
192
|
+
untrackedFiles: string[];
|
|
193
|
+
};
|
|
165
194
|
export declare function handleCheckAllFiles(ctx: FixHandlerContext): Promise<FixHandlerResult>;
|
|
166
195
|
export declare function handleJsonValuesExtractEntry(ctx: FixHandlerContext): Promise<FixHandlerResult>;
|
|
167
196
|
/**
|
|
@@ -13506,6 +13506,72 @@ async function extractFileMetadata(filename, _input // TODO: use buffer to deter
|
|
|
13506
13506
|
};
|
|
13507
13507
|
}
|
|
13508
13508
|
|
|
13509
|
+
/**
|
|
13510
|
+
* A gallery entry's key, read as what it actually is.
|
|
13511
|
+
*
|
|
13512
|
+
* A gallery is a record keyed by its entries' file paths. A REMOTE gallery keys
|
|
13513
|
+
* an uploaded entry by the remote URL instead — which encodes that same path —
|
|
13514
|
+
* so every entry has a local path, and the interesting question is whether the
|
|
13515
|
+
* bytes are expected to be at it.
|
|
13516
|
+
*
|
|
13517
|
+
* **They are not, for a remote entry added the ordinary way.** `saveOrUploadFiles`
|
|
13518
|
+
* uploads the remote descriptors to the content host and copies only the local
|
|
13519
|
+
* ones into the working tree, so an image added through the Studio (or over MCP)
|
|
13520
|
+
* and published has no file in the repo, by design — putting one there is what
|
|
13521
|
+
* remote storage exists to avoid. A remote entry CAN have one, because
|
|
13522
|
+
* `val validate --fix` promotes a local file to a remote ref and leaves the file
|
|
13523
|
+
* where it was; so "remote" means "do not require it", never "there is not one".
|
|
13524
|
+
*
|
|
13525
|
+
* One copy, in its own file, because this used to be two: a `remoteKeyToLocalPath`
|
|
13526
|
+
* in `fixHandlers` and a `galleryKeyToLocalPath` in `createFixPatch`, each
|
|
13527
|
+
* normalising the key correctly and each then treating the result as a file that
|
|
13528
|
+
* must exist. That agreement is what made a published remote image report as
|
|
13529
|
+
* missing from both sides at once.
|
|
13530
|
+
*/
|
|
13531
|
+
|
|
13532
|
+
/**
|
|
13533
|
+
* A key that names somewhere else, whether or not it is a ref we can read.
|
|
13534
|
+
*
|
|
13535
|
+
* Case-insensitive, because a scheme is (RFC 3986) — and because
|
|
13536
|
+
* `splitRemoteRef`'s pattern is not, so `HTTPS://…` is precisely one of the
|
|
13537
|
+
* spellings that arrives here unparsed and must not be mistaken for a path.
|
|
13538
|
+
*/
|
|
13539
|
+
function isRemoteUrl(key) {
|
|
13540
|
+
return /^https?:\/\//i.test(key);
|
|
13541
|
+
}
|
|
13542
|
+
function galleryEntryOf(key) {
|
|
13543
|
+
const remoteRefRes = core.Internal.remote.splitRemoteRef(key);
|
|
13544
|
+
if (remoteRefRes.status === "success") {
|
|
13545
|
+
return {
|
|
13546
|
+
localPath: `/${remoteRefRes.filePath}`,
|
|
13547
|
+
remote: true
|
|
13548
|
+
};
|
|
13549
|
+
}
|
|
13550
|
+
if (isRemoteUrl(key)) {
|
|
13551
|
+
// A URL that is not a ref this version can read: truncated by a hand edit,
|
|
13552
|
+
// a path outside `public/`, a `..` segment — or written by a core version
|
|
13553
|
+
// whose format `splitRemoteRef` rejects.
|
|
13554
|
+
//
|
|
13555
|
+
// Still not a path in the working tree, so the on-disk checks must not run
|
|
13556
|
+
// against it. They would find nothing at `<projectRoot>/https://…`, report
|
|
13557
|
+
// the entry as missing, and `--fix` would DELETE it — the exact failure
|
|
13558
|
+
// this module exists to prevent, arriving at the one moment the key is the
|
|
13559
|
+
// only remaining record of where the bytes went.
|
|
13560
|
+
//
|
|
13561
|
+
// Nothing is being hidden by this: a malformed key is already reported, by
|
|
13562
|
+
// the record schema's own "Invalid remote URL format". That is an error for
|
|
13563
|
+
// a person to look at, not a file for `--fix` to go missing.
|
|
13564
|
+
return {
|
|
13565
|
+
localPath: key,
|
|
13566
|
+
remote: true
|
|
13567
|
+
};
|
|
13568
|
+
}
|
|
13569
|
+
return {
|
|
13570
|
+
localPath: key,
|
|
13571
|
+
remote: false
|
|
13572
|
+
};
|
|
13573
|
+
}
|
|
13574
|
+
|
|
13509
13575
|
/**
|
|
13510
13576
|
* The media path a validation error is about, if it is about media at all.
|
|
13511
13577
|
*
|
|
@@ -13701,14 +13767,6 @@ async function downloadFileFromRemote(ref, filePath) {
|
|
|
13701
13767
|
// record-level fix expands into per-entry errors that should point at the
|
|
13702
13768
|
// individual entry (e.g. `?p="/public/val/logo.png"`) rather than the record.
|
|
13703
13769
|
|
|
13704
|
-
// Gallery entries are keyed by their file path. Remote galleries key uploaded
|
|
13705
|
-
// entries by a remote URL while keeping the file on disk at its local path, so
|
|
13706
|
-
// normalize a remote-URL key back to that local path for on-disk reads.
|
|
13707
|
-
function galleryKeyToLocalPath(key) {
|
|
13708
|
-
const res = core.Internal.remote.splitRemoteRef(key);
|
|
13709
|
-
return res.status === "success" ? `/${res.filePath}` : key;
|
|
13710
|
-
}
|
|
13711
|
-
|
|
13712
13770
|
// Patch path of the record that holds a media entry. Media keys are file paths
|
|
13713
13771
|
// that can contain dots, which sourceToPatchPath cannot round-trip, so strip
|
|
13714
13772
|
// the (JSON-encoded) key segment off the entry source path and convert the
|
|
@@ -14017,7 +14075,18 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
14017
14075
|
}
|
|
14018
14076
|
const gallerySource = moduleSource;
|
|
14019
14077
|
for (const [entryKey, storedEntry] of Object.entries(gallerySource)) {
|
|
14020
|
-
const
|
|
14078
|
+
const entry = galleryEntryOf(entryKey);
|
|
14079
|
+
if (entry.remote) {
|
|
14080
|
+
// A remote entry's bytes are on the content host, so there may be no
|
|
14081
|
+
// local file to re-derive its metadata from — and where there is one,
|
|
14082
|
+
// rewriting the metadata from it would invalidate the ref, which has
|
|
14083
|
+
// that metadata baked into its validation hash. Whether a remote
|
|
14084
|
+
// entry is sound is `image:check-remote`'s question, and it already
|
|
14085
|
+
// asks it. Reading the file here reported every published remote
|
|
14086
|
+
// image as unreadable, and with `--fix` REMOVED it from the gallery.
|
|
14087
|
+
continue;
|
|
14088
|
+
}
|
|
14089
|
+
const filename = path__namespace["default"].join(config.projectRoot, entry.localPath);
|
|
14021
14090
|
let buffer;
|
|
14022
14091
|
try {
|
|
14023
14092
|
buffer = fs__default["default"].readFileSync(filename);
|
|
@@ -14683,15 +14752,48 @@ async function handleUniqueFolderCheck(ctx) {
|
|
|
14683
14752
|
};
|
|
14684
14753
|
}
|
|
14685
14754
|
|
|
14686
|
-
|
|
14687
|
-
|
|
14688
|
-
|
|
14689
|
-
|
|
14690
|
-
|
|
14691
|
-
|
|
14692
|
-
|
|
14755
|
+
/**
|
|
14756
|
+
* What is out of step between a gallery's entries and its directory.
|
|
14757
|
+
*
|
|
14758
|
+
* Two questions, and they treat a remote entry differently — which is the whole
|
|
14759
|
+
* reason this is separate from the handler around it:
|
|
14760
|
+
*
|
|
14761
|
+
* - **Missing**: an entry with no bytes at its local path. Asked of LOCAL
|
|
14762
|
+
* entries only. A remote entry's bytes live on the content host, and nothing
|
|
14763
|
+
* puts a copy in the working tree: `saveOrUploadFiles` uploads the remote
|
|
14764
|
+
* descriptors and copies only the local ones into the tree, so a remote entry
|
|
14765
|
+
* added through the Studio (or over MCP) has no local file by design, and
|
|
14766
|
+
* demanding one would mean committing remote bytes to git — which is what
|
|
14767
|
+
* remote storage exists to avoid. Whether those bytes really are on the host
|
|
14768
|
+
* is a different check, `image:check-remote`, which already runs for exactly
|
|
14769
|
+
* these entries.
|
|
14770
|
+
* - **Untracked**: a file in the directory that no entry claims. Asked of every
|
|
14771
|
+
* entry, remote included, and that is why they are normalised to their local
|
|
14772
|
+
* path: `val validate --fix` promotes a local file to a remote ref and leaves
|
|
14773
|
+
* the file where it was, so a remote entry can perfectly well have one.
|
|
14774
|
+
*/
|
|
14775
|
+
function checkGalleryFiles(input) {
|
|
14776
|
+
const {
|
|
14777
|
+
directory,
|
|
14778
|
+
projectRoot,
|
|
14779
|
+
fs
|
|
14780
|
+
} = input;
|
|
14781
|
+
const entries = input.entryKeys.map(galleryEntryOf);
|
|
14782
|
+
const trackedFiles = new Set(entries.map(entry => entry.localPath));
|
|
14783
|
+
const missingTrackedFiles = entries.filter(entry => !entry.remote && !fs.fileExists(path__namespace["default"].join(projectRoot, entry.localPath))).map(entry => entry.localPath);
|
|
14784
|
+
const filesInDir = [];
|
|
14785
|
+
try {
|
|
14786
|
+
const found = fs.readDirectory(path__namespace["default"].join(projectRoot, directory), undefined, undefined, ["**/*"]);
|
|
14787
|
+
for (const entry of found) {
|
|
14788
|
+
filesInDir.push("/" + path__namespace["default"].relative(projectRoot, entry).split(path__namespace["default"].sep).join("/"));
|
|
14789
|
+
}
|
|
14790
|
+
} catch {
|
|
14791
|
+
// directory doesn't exist — no untracked files possible
|
|
14693
14792
|
}
|
|
14694
|
-
return
|
|
14793
|
+
return {
|
|
14794
|
+
missingTrackedFiles,
|
|
14795
|
+
untrackedFiles: filesInDir.filter(f => !trackedFiles.has(f))
|
|
14796
|
+
};
|
|
14695
14797
|
}
|
|
14696
14798
|
async function handleCheckAllFiles(ctx) {
|
|
14697
14799
|
const value = ctx.validationError.value;
|
|
@@ -14711,14 +14813,14 @@ async function handleCheckAllFiles(ctx) {
|
|
|
14711
14813
|
errorMessage: `Could not get source for ${ctx.sourcePath}`
|
|
14712
14814
|
};
|
|
14713
14815
|
}
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
14718
|
-
|
|
14719
|
-
|
|
14720
|
-
|
|
14721
|
-
|
|
14816
|
+
const {
|
|
14817
|
+
missingTrackedFiles,
|
|
14818
|
+
untrackedFiles
|
|
14819
|
+
} = checkGalleryFiles({
|
|
14820
|
+
entryKeys: Object.keys(source),
|
|
14821
|
+
directory,
|
|
14822
|
+
projectRoot: ctx.projectRoot,
|
|
14823
|
+
fs: ctx.fs
|
|
14722
14824
|
});
|
|
14723
14825
|
if (missingTrackedFiles.length > 0) {
|
|
14724
14826
|
if (!ctx.fix) {
|
|
@@ -14733,18 +14835,6 @@ async function handleCheckAllFiles(ctx) {
|
|
|
14733
14835
|
shouldApplyPatch: true
|
|
14734
14836
|
};
|
|
14735
14837
|
}
|
|
14736
|
-
const dirPath = path__namespace["default"].join(ctx.projectRoot, directory);
|
|
14737
|
-
const filesInDir = [];
|
|
14738
|
-
try {
|
|
14739
|
-
const entries = ctx.fs.readDirectory(dirPath, undefined, undefined, ["**/*"]);
|
|
14740
|
-
for (const entry of entries) {
|
|
14741
|
-
const relPath = "/" + path__namespace["default"].relative(ctx.projectRoot, entry).split(path__namespace["default"].sep).join("/");
|
|
14742
|
-
filesInDir.push(relPath);
|
|
14743
|
-
}
|
|
14744
|
-
} catch {
|
|
14745
|
-
// directory doesn't exist — no untracked files possible
|
|
14746
|
-
}
|
|
14747
|
-
const untrackedFiles = filesInDir.filter(f => !trackedFiles.has(f));
|
|
14748
14838
|
if (untrackedFiles.length > 0) {
|
|
14749
14839
|
return {
|
|
14750
14840
|
success: false,
|
|
@@ -13506,6 +13506,72 @@ async function extractFileMetadata(filename, _input // TODO: use buffer to deter
|
|
|
13506
13506
|
};
|
|
13507
13507
|
}
|
|
13508
13508
|
|
|
13509
|
+
/**
|
|
13510
|
+
* A gallery entry's key, read as what it actually is.
|
|
13511
|
+
*
|
|
13512
|
+
* A gallery is a record keyed by its entries' file paths. A REMOTE gallery keys
|
|
13513
|
+
* an uploaded entry by the remote URL instead — which encodes that same path —
|
|
13514
|
+
* so every entry has a local path, and the interesting question is whether the
|
|
13515
|
+
* bytes are expected to be at it.
|
|
13516
|
+
*
|
|
13517
|
+
* **They are not, for a remote entry added the ordinary way.** `saveOrUploadFiles`
|
|
13518
|
+
* uploads the remote descriptors to the content host and copies only the local
|
|
13519
|
+
* ones into the working tree, so an image added through the Studio (or over MCP)
|
|
13520
|
+
* and published has no file in the repo, by design — putting one there is what
|
|
13521
|
+
* remote storage exists to avoid. A remote entry CAN have one, because
|
|
13522
|
+
* `val validate --fix` promotes a local file to a remote ref and leaves the file
|
|
13523
|
+
* where it was; so "remote" means "do not require it", never "there is not one".
|
|
13524
|
+
*
|
|
13525
|
+
* One copy, in its own file, because this used to be two: a `remoteKeyToLocalPath`
|
|
13526
|
+
* in `fixHandlers` and a `galleryKeyToLocalPath` in `createFixPatch`, each
|
|
13527
|
+
* normalising the key correctly and each then treating the result as a file that
|
|
13528
|
+
* must exist. That agreement is what made a published remote image report as
|
|
13529
|
+
* missing from both sides at once.
|
|
13530
|
+
*/
|
|
13531
|
+
|
|
13532
|
+
/**
|
|
13533
|
+
* A key that names somewhere else, whether or not it is a ref we can read.
|
|
13534
|
+
*
|
|
13535
|
+
* Case-insensitive, because a scheme is (RFC 3986) — and because
|
|
13536
|
+
* `splitRemoteRef`'s pattern is not, so `HTTPS://…` is precisely one of the
|
|
13537
|
+
* spellings that arrives here unparsed and must not be mistaken for a path.
|
|
13538
|
+
*/
|
|
13539
|
+
function isRemoteUrl(key) {
|
|
13540
|
+
return /^https?:\/\//i.test(key);
|
|
13541
|
+
}
|
|
13542
|
+
function galleryEntryOf(key) {
|
|
13543
|
+
const remoteRefRes = core.Internal.remote.splitRemoteRef(key);
|
|
13544
|
+
if (remoteRefRes.status === "success") {
|
|
13545
|
+
return {
|
|
13546
|
+
localPath: `/${remoteRefRes.filePath}`,
|
|
13547
|
+
remote: true
|
|
13548
|
+
};
|
|
13549
|
+
}
|
|
13550
|
+
if (isRemoteUrl(key)) {
|
|
13551
|
+
// A URL that is not a ref this version can read: truncated by a hand edit,
|
|
13552
|
+
// a path outside `public/`, a `..` segment — or written by a core version
|
|
13553
|
+
// whose format `splitRemoteRef` rejects.
|
|
13554
|
+
//
|
|
13555
|
+
// Still not a path in the working tree, so the on-disk checks must not run
|
|
13556
|
+
// against it. They would find nothing at `<projectRoot>/https://…`, report
|
|
13557
|
+
// the entry as missing, and `--fix` would DELETE it — the exact failure
|
|
13558
|
+
// this module exists to prevent, arriving at the one moment the key is the
|
|
13559
|
+
// only remaining record of where the bytes went.
|
|
13560
|
+
//
|
|
13561
|
+
// Nothing is being hidden by this: a malformed key is already reported, by
|
|
13562
|
+
// the record schema's own "Invalid remote URL format". That is an error for
|
|
13563
|
+
// a person to look at, not a file for `--fix` to go missing.
|
|
13564
|
+
return {
|
|
13565
|
+
localPath: key,
|
|
13566
|
+
remote: true
|
|
13567
|
+
};
|
|
13568
|
+
}
|
|
13569
|
+
return {
|
|
13570
|
+
localPath: key,
|
|
13571
|
+
remote: false
|
|
13572
|
+
};
|
|
13573
|
+
}
|
|
13574
|
+
|
|
13509
13575
|
/**
|
|
13510
13576
|
* The media path a validation error is about, if it is about media at all.
|
|
13511
13577
|
*
|
|
@@ -13701,14 +13767,6 @@ async function downloadFileFromRemote(ref, filePath) {
|
|
|
13701
13767
|
// record-level fix expands into per-entry errors that should point at the
|
|
13702
13768
|
// individual entry (e.g. `?p="/public/val/logo.png"`) rather than the record.
|
|
13703
13769
|
|
|
13704
|
-
// Gallery entries are keyed by their file path. Remote galleries key uploaded
|
|
13705
|
-
// entries by a remote URL while keeping the file on disk at its local path, so
|
|
13706
|
-
// normalize a remote-URL key back to that local path for on-disk reads.
|
|
13707
|
-
function galleryKeyToLocalPath(key) {
|
|
13708
|
-
const res = core.Internal.remote.splitRemoteRef(key);
|
|
13709
|
-
return res.status === "success" ? `/${res.filePath}` : key;
|
|
13710
|
-
}
|
|
13711
|
-
|
|
13712
13770
|
// Patch path of the record that holds a media entry. Media keys are file paths
|
|
13713
13771
|
// that can contain dots, which sourceToPatchPath cannot round-trip, so strip
|
|
13714
13772
|
// the (JSON-encoded) key segment off the entry source path and convert the
|
|
@@ -14017,7 +14075,18 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
14017
14075
|
}
|
|
14018
14076
|
const gallerySource = moduleSource;
|
|
14019
14077
|
for (const [entryKey, storedEntry] of Object.entries(gallerySource)) {
|
|
14020
|
-
const
|
|
14078
|
+
const entry = galleryEntryOf(entryKey);
|
|
14079
|
+
if (entry.remote) {
|
|
14080
|
+
// A remote entry's bytes are on the content host, so there may be no
|
|
14081
|
+
// local file to re-derive its metadata from — and where there is one,
|
|
14082
|
+
// rewriting the metadata from it would invalidate the ref, which has
|
|
14083
|
+
// that metadata baked into its validation hash. Whether a remote
|
|
14084
|
+
// entry is sound is `image:check-remote`'s question, and it already
|
|
14085
|
+
// asks it. Reading the file here reported every published remote
|
|
14086
|
+
// image as unreadable, and with `--fix` REMOVED it from the gallery.
|
|
14087
|
+
continue;
|
|
14088
|
+
}
|
|
14089
|
+
const filename = path__namespace["default"].join(config.projectRoot, entry.localPath);
|
|
14021
14090
|
let buffer;
|
|
14022
14091
|
try {
|
|
14023
14092
|
buffer = fs__default["default"].readFileSync(filename);
|
|
@@ -14683,15 +14752,48 @@ async function handleUniqueFolderCheck(ctx) {
|
|
|
14683
14752
|
};
|
|
14684
14753
|
}
|
|
14685
14754
|
|
|
14686
|
-
|
|
14687
|
-
|
|
14688
|
-
|
|
14689
|
-
|
|
14690
|
-
|
|
14691
|
-
|
|
14692
|
-
|
|
14755
|
+
/**
|
|
14756
|
+
* What is out of step between a gallery's entries and its directory.
|
|
14757
|
+
*
|
|
14758
|
+
* Two questions, and they treat a remote entry differently — which is the whole
|
|
14759
|
+
* reason this is separate from the handler around it:
|
|
14760
|
+
*
|
|
14761
|
+
* - **Missing**: an entry with no bytes at its local path. Asked of LOCAL
|
|
14762
|
+
* entries only. A remote entry's bytes live on the content host, and nothing
|
|
14763
|
+
* puts a copy in the working tree: `saveOrUploadFiles` uploads the remote
|
|
14764
|
+
* descriptors and copies only the local ones into the tree, so a remote entry
|
|
14765
|
+
* added through the Studio (or over MCP) has no local file by design, and
|
|
14766
|
+
* demanding one would mean committing remote bytes to git — which is what
|
|
14767
|
+
* remote storage exists to avoid. Whether those bytes really are on the host
|
|
14768
|
+
* is a different check, `image:check-remote`, which already runs for exactly
|
|
14769
|
+
* these entries.
|
|
14770
|
+
* - **Untracked**: a file in the directory that no entry claims. Asked of every
|
|
14771
|
+
* entry, remote included, and that is why they are normalised to their local
|
|
14772
|
+
* path: `val validate --fix` promotes a local file to a remote ref and leaves
|
|
14773
|
+
* the file where it was, so a remote entry can perfectly well have one.
|
|
14774
|
+
*/
|
|
14775
|
+
function checkGalleryFiles(input) {
|
|
14776
|
+
const {
|
|
14777
|
+
directory,
|
|
14778
|
+
projectRoot,
|
|
14779
|
+
fs
|
|
14780
|
+
} = input;
|
|
14781
|
+
const entries = input.entryKeys.map(galleryEntryOf);
|
|
14782
|
+
const trackedFiles = new Set(entries.map(entry => entry.localPath));
|
|
14783
|
+
const missingTrackedFiles = entries.filter(entry => !entry.remote && !fs.fileExists(path__namespace["default"].join(projectRoot, entry.localPath))).map(entry => entry.localPath);
|
|
14784
|
+
const filesInDir = [];
|
|
14785
|
+
try {
|
|
14786
|
+
const found = fs.readDirectory(path__namespace["default"].join(projectRoot, directory), undefined, undefined, ["**/*"]);
|
|
14787
|
+
for (const entry of found) {
|
|
14788
|
+
filesInDir.push("/" + path__namespace["default"].relative(projectRoot, entry).split(path__namespace["default"].sep).join("/"));
|
|
14789
|
+
}
|
|
14790
|
+
} catch {
|
|
14791
|
+
// directory doesn't exist — no untracked files possible
|
|
14693
14792
|
}
|
|
14694
|
-
return
|
|
14793
|
+
return {
|
|
14794
|
+
missingTrackedFiles,
|
|
14795
|
+
untrackedFiles: filesInDir.filter(f => !trackedFiles.has(f))
|
|
14796
|
+
};
|
|
14695
14797
|
}
|
|
14696
14798
|
async function handleCheckAllFiles(ctx) {
|
|
14697
14799
|
const value = ctx.validationError.value;
|
|
@@ -14711,14 +14813,14 @@ async function handleCheckAllFiles(ctx) {
|
|
|
14711
14813
|
errorMessage: `Could not get source for ${ctx.sourcePath}`
|
|
14712
14814
|
};
|
|
14713
14815
|
}
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
14718
|
-
|
|
14719
|
-
|
|
14720
|
-
|
|
14721
|
-
|
|
14816
|
+
const {
|
|
14817
|
+
missingTrackedFiles,
|
|
14818
|
+
untrackedFiles
|
|
14819
|
+
} = checkGalleryFiles({
|
|
14820
|
+
entryKeys: Object.keys(source),
|
|
14821
|
+
directory,
|
|
14822
|
+
projectRoot: ctx.projectRoot,
|
|
14823
|
+
fs: ctx.fs
|
|
14722
14824
|
});
|
|
14723
14825
|
if (missingTrackedFiles.length > 0) {
|
|
14724
14826
|
if (!ctx.fix) {
|
|
@@ -14733,18 +14835,6 @@ async function handleCheckAllFiles(ctx) {
|
|
|
14733
14835
|
shouldApplyPatch: true
|
|
14734
14836
|
};
|
|
14735
14837
|
}
|
|
14736
|
-
const dirPath = path__namespace["default"].join(ctx.projectRoot, directory);
|
|
14737
|
-
const filesInDir = [];
|
|
14738
|
-
try {
|
|
14739
|
-
const entries = ctx.fs.readDirectory(dirPath, undefined, undefined, ["**/*"]);
|
|
14740
|
-
for (const entry of entries) {
|
|
14741
|
-
const relPath = "/" + path__namespace["default"].relative(ctx.projectRoot, entry).split(path__namespace["default"].sep).join("/");
|
|
14742
|
-
filesInDir.push(relPath);
|
|
14743
|
-
}
|
|
14744
|
-
} catch {
|
|
14745
|
-
// directory doesn't exist — no untracked files possible
|
|
14746
|
-
}
|
|
14747
|
-
const untrackedFiles = filesInDir.filter(f => !trackedFiles.has(f));
|
|
14748
14838
|
if (untrackedFiles.length > 0) {
|
|
14749
14839
|
return {
|
|
14750
14840
|
success: false,
|
|
@@ -13472,6 +13472,72 @@ async function extractFileMetadata(filename, _input // TODO: use buffer to deter
|
|
|
13472
13472
|
};
|
|
13473
13473
|
}
|
|
13474
13474
|
|
|
13475
|
+
/**
|
|
13476
|
+
* A gallery entry's key, read as what it actually is.
|
|
13477
|
+
*
|
|
13478
|
+
* A gallery is a record keyed by its entries' file paths. A REMOTE gallery keys
|
|
13479
|
+
* an uploaded entry by the remote URL instead — which encodes that same path —
|
|
13480
|
+
* so every entry has a local path, and the interesting question is whether the
|
|
13481
|
+
* bytes are expected to be at it.
|
|
13482
|
+
*
|
|
13483
|
+
* **They are not, for a remote entry added the ordinary way.** `saveOrUploadFiles`
|
|
13484
|
+
* uploads the remote descriptors to the content host and copies only the local
|
|
13485
|
+
* ones into the working tree, so an image added through the Studio (or over MCP)
|
|
13486
|
+
* and published has no file in the repo, by design — putting one there is what
|
|
13487
|
+
* remote storage exists to avoid. A remote entry CAN have one, because
|
|
13488
|
+
* `val validate --fix` promotes a local file to a remote ref and leaves the file
|
|
13489
|
+
* where it was; so "remote" means "do not require it", never "there is not one".
|
|
13490
|
+
*
|
|
13491
|
+
* One copy, in its own file, because this used to be two: a `remoteKeyToLocalPath`
|
|
13492
|
+
* in `fixHandlers` and a `galleryKeyToLocalPath` in `createFixPatch`, each
|
|
13493
|
+
* normalising the key correctly and each then treating the result as a file that
|
|
13494
|
+
* must exist. That agreement is what made a published remote image report as
|
|
13495
|
+
* missing from both sides at once.
|
|
13496
|
+
*/
|
|
13497
|
+
|
|
13498
|
+
/**
|
|
13499
|
+
* A key that names somewhere else, whether or not it is a ref we can read.
|
|
13500
|
+
*
|
|
13501
|
+
* Case-insensitive, because a scheme is (RFC 3986) — and because
|
|
13502
|
+
* `splitRemoteRef`'s pattern is not, so `HTTPS://…` is precisely one of the
|
|
13503
|
+
* spellings that arrives here unparsed and must not be mistaken for a path.
|
|
13504
|
+
*/
|
|
13505
|
+
function isRemoteUrl(key) {
|
|
13506
|
+
return /^https?:\/\//i.test(key);
|
|
13507
|
+
}
|
|
13508
|
+
function galleryEntryOf(key) {
|
|
13509
|
+
const remoteRefRes = Internal.remote.splitRemoteRef(key);
|
|
13510
|
+
if (remoteRefRes.status === "success") {
|
|
13511
|
+
return {
|
|
13512
|
+
localPath: `/${remoteRefRes.filePath}`,
|
|
13513
|
+
remote: true
|
|
13514
|
+
};
|
|
13515
|
+
}
|
|
13516
|
+
if (isRemoteUrl(key)) {
|
|
13517
|
+
// A URL that is not a ref this version can read: truncated by a hand edit,
|
|
13518
|
+
// a path outside `public/`, a `..` segment — or written by a core version
|
|
13519
|
+
// whose format `splitRemoteRef` rejects.
|
|
13520
|
+
//
|
|
13521
|
+
// Still not a path in the working tree, so the on-disk checks must not run
|
|
13522
|
+
// against it. They would find nothing at `<projectRoot>/https://…`, report
|
|
13523
|
+
// the entry as missing, and `--fix` would DELETE it — the exact failure
|
|
13524
|
+
// this module exists to prevent, arriving at the one moment the key is the
|
|
13525
|
+
// only remaining record of where the bytes went.
|
|
13526
|
+
//
|
|
13527
|
+
// Nothing is being hidden by this: a malformed key is already reported, by
|
|
13528
|
+
// the record schema's own "Invalid remote URL format". That is an error for
|
|
13529
|
+
// a person to look at, not a file for `--fix` to go missing.
|
|
13530
|
+
return {
|
|
13531
|
+
localPath: key,
|
|
13532
|
+
remote: true
|
|
13533
|
+
};
|
|
13534
|
+
}
|
|
13535
|
+
return {
|
|
13536
|
+
localPath: key,
|
|
13537
|
+
remote: false
|
|
13538
|
+
};
|
|
13539
|
+
}
|
|
13540
|
+
|
|
13475
13541
|
/**
|
|
13476
13542
|
* The media path a validation error is about, if it is about media at all.
|
|
13477
13543
|
*
|
|
@@ -13667,14 +13733,6 @@ async function downloadFileFromRemote(ref, filePath) {
|
|
|
13667
13733
|
// record-level fix expands into per-entry errors that should point at the
|
|
13668
13734
|
// individual entry (e.g. `?p="/public/val/logo.png"`) rather than the record.
|
|
13669
13735
|
|
|
13670
|
-
// Gallery entries are keyed by their file path. Remote galleries key uploaded
|
|
13671
|
-
// entries by a remote URL while keeping the file on disk at its local path, so
|
|
13672
|
-
// normalize a remote-URL key back to that local path for on-disk reads.
|
|
13673
|
-
function galleryKeyToLocalPath(key) {
|
|
13674
|
-
const res = Internal.remote.splitRemoteRef(key);
|
|
13675
|
-
return res.status === "success" ? `/${res.filePath}` : key;
|
|
13676
|
-
}
|
|
13677
|
-
|
|
13678
13736
|
// Patch path of the record that holds a media entry. Media keys are file paths
|
|
13679
13737
|
// that can contain dots, which sourceToPatchPath cannot round-trip, so strip
|
|
13680
13738
|
// the (JSON-encoded) key segment off the entry source path and convert the
|
|
@@ -13983,7 +14041,18 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
13983
14041
|
}
|
|
13984
14042
|
const gallerySource = moduleSource;
|
|
13985
14043
|
for (const [entryKey, storedEntry] of Object.entries(gallerySource)) {
|
|
13986
|
-
const
|
|
14044
|
+
const entry = galleryEntryOf(entryKey);
|
|
14045
|
+
if (entry.remote) {
|
|
14046
|
+
// A remote entry's bytes are on the content host, so there may be no
|
|
14047
|
+
// local file to re-derive its metadata from — and where there is one,
|
|
14048
|
+
// rewriting the metadata from it would invalidate the ref, which has
|
|
14049
|
+
// that metadata baked into its validation hash. Whether a remote
|
|
14050
|
+
// entry is sound is `image:check-remote`'s question, and it already
|
|
14051
|
+
// asks it. Reading the file here reported every published remote
|
|
14052
|
+
// image as unreadable, and with `--fix` REMOVED it from the gallery.
|
|
14053
|
+
continue;
|
|
14054
|
+
}
|
|
14055
|
+
const filename = path__default.join(config.projectRoot, entry.localPath);
|
|
13987
14056
|
let buffer;
|
|
13988
14057
|
try {
|
|
13989
14058
|
buffer = fs.readFileSync(filename);
|
|
@@ -14649,15 +14718,48 @@ async function handleUniqueFolderCheck(ctx) {
|
|
|
14649
14718
|
};
|
|
14650
14719
|
}
|
|
14651
14720
|
|
|
14652
|
-
|
|
14653
|
-
|
|
14654
|
-
|
|
14655
|
-
|
|
14656
|
-
|
|
14657
|
-
|
|
14658
|
-
|
|
14721
|
+
/**
|
|
14722
|
+
* What is out of step between a gallery's entries and its directory.
|
|
14723
|
+
*
|
|
14724
|
+
* Two questions, and they treat a remote entry differently — which is the whole
|
|
14725
|
+
* reason this is separate from the handler around it:
|
|
14726
|
+
*
|
|
14727
|
+
* - **Missing**: an entry with no bytes at its local path. Asked of LOCAL
|
|
14728
|
+
* entries only. A remote entry's bytes live on the content host, and nothing
|
|
14729
|
+
* puts a copy in the working tree: `saveOrUploadFiles` uploads the remote
|
|
14730
|
+
* descriptors and copies only the local ones into the tree, so a remote entry
|
|
14731
|
+
* added through the Studio (or over MCP) has no local file by design, and
|
|
14732
|
+
* demanding one would mean committing remote bytes to git — which is what
|
|
14733
|
+
* remote storage exists to avoid. Whether those bytes really are on the host
|
|
14734
|
+
* is a different check, `image:check-remote`, which already runs for exactly
|
|
14735
|
+
* these entries.
|
|
14736
|
+
* - **Untracked**: a file in the directory that no entry claims. Asked of every
|
|
14737
|
+
* entry, remote included, and that is why they are normalised to their local
|
|
14738
|
+
* path: `val validate --fix` promotes a local file to a remote ref and leaves
|
|
14739
|
+
* the file where it was, so a remote entry can perfectly well have one.
|
|
14740
|
+
*/
|
|
14741
|
+
function checkGalleryFiles(input) {
|
|
14742
|
+
const {
|
|
14743
|
+
directory,
|
|
14744
|
+
projectRoot,
|
|
14745
|
+
fs
|
|
14746
|
+
} = input;
|
|
14747
|
+
const entries = input.entryKeys.map(galleryEntryOf);
|
|
14748
|
+
const trackedFiles = new Set(entries.map(entry => entry.localPath));
|
|
14749
|
+
const missingTrackedFiles = entries.filter(entry => !entry.remote && !fs.fileExists(path__default.join(projectRoot, entry.localPath))).map(entry => entry.localPath);
|
|
14750
|
+
const filesInDir = [];
|
|
14751
|
+
try {
|
|
14752
|
+
const found = fs.readDirectory(path__default.join(projectRoot, directory), undefined, undefined, ["**/*"]);
|
|
14753
|
+
for (const entry of found) {
|
|
14754
|
+
filesInDir.push("/" + path__default.relative(projectRoot, entry).split(path__default.sep).join("/"));
|
|
14755
|
+
}
|
|
14756
|
+
} catch {
|
|
14757
|
+
// directory doesn't exist — no untracked files possible
|
|
14659
14758
|
}
|
|
14660
|
-
return
|
|
14759
|
+
return {
|
|
14760
|
+
missingTrackedFiles,
|
|
14761
|
+
untrackedFiles: filesInDir.filter(f => !trackedFiles.has(f))
|
|
14762
|
+
};
|
|
14661
14763
|
}
|
|
14662
14764
|
async function handleCheckAllFiles(ctx) {
|
|
14663
14765
|
const value = ctx.validationError.value;
|
|
@@ -14677,14 +14779,14 @@ async function handleCheckAllFiles(ctx) {
|
|
|
14677
14779
|
errorMessage: `Could not get source for ${ctx.sourcePath}`
|
|
14678
14780
|
};
|
|
14679
14781
|
}
|
|
14680
|
-
|
|
14681
|
-
|
|
14682
|
-
|
|
14683
|
-
|
|
14684
|
-
|
|
14685
|
-
|
|
14686
|
-
|
|
14687
|
-
|
|
14782
|
+
const {
|
|
14783
|
+
missingTrackedFiles,
|
|
14784
|
+
untrackedFiles
|
|
14785
|
+
} = checkGalleryFiles({
|
|
14786
|
+
entryKeys: Object.keys(source),
|
|
14787
|
+
directory,
|
|
14788
|
+
projectRoot: ctx.projectRoot,
|
|
14789
|
+
fs: ctx.fs
|
|
14688
14790
|
});
|
|
14689
14791
|
if (missingTrackedFiles.length > 0) {
|
|
14690
14792
|
if (!ctx.fix) {
|
|
@@ -14699,18 +14801,6 @@ async function handleCheckAllFiles(ctx) {
|
|
|
14699
14801
|
shouldApplyPatch: true
|
|
14700
14802
|
};
|
|
14701
14803
|
}
|
|
14702
|
-
const dirPath = path__default.join(ctx.projectRoot, directory);
|
|
14703
|
-
const filesInDir = [];
|
|
14704
|
-
try {
|
|
14705
|
-
const entries = ctx.fs.readDirectory(dirPath, undefined, undefined, ["**/*"]);
|
|
14706
|
-
for (const entry of entries) {
|
|
14707
|
-
const relPath = "/" + path__default.relative(ctx.projectRoot, entry).split(path__default.sep).join("/");
|
|
14708
|
-
filesInDir.push(relPath);
|
|
14709
|
-
}
|
|
14710
|
-
} catch {
|
|
14711
|
-
// directory doesn't exist — no untracked files possible
|
|
14712
|
-
}
|
|
14713
|
-
const untrackedFiles = filesInDir.filter(f => !trackedFiles.has(f));
|
|
14714
14804
|
if (untrackedFiles.length > 0) {
|
|
14715
14805
|
return {
|
|
14716
14806
|
success: false,
|
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.123.
|
|
19
|
+
"version": "0.123.2",
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@prettier/sync": "^0.6.1",
|
|
22
22
|
"@types/jest": "^30.0.0"
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"typescript": "^6.0.3",
|
|
30
30
|
"zod": "^4.4.3",
|
|
31
31
|
"zod-validation-error": "^5.0.0",
|
|
32
|
+
"@valbuild/shared": "0.123.2",
|
|
32
33
|
"@valbuild/core": "0.121.0",
|
|
33
|
-
"@valbuild/
|
|
34
|
-
"@valbuild/ui": "0.123.0"
|
|
34
|
+
"@valbuild/ui": "0.123.2"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": "^20.19.0 || >=22"
|