@valbuild/server 0.143.1 → 0.144.1

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.
@@ -8,7 +8,7 @@ import path__default from 'path';
8
8
  import fs from 'fs';
9
9
  import vm from 'node:vm';
10
10
  import { Module } from 'node:module';
11
- import { isSchemaSourceFixError, resolveSchemaSourceFixForError, Patch, getErrorMessageFromUnknownJson, PatchId as PatchId$1, JSONValue, PatchGroup, newestCommitSha, SerializedSchema, VAL_ENABLE_COOKIE_NAME, VAL_STATE_COOKIE, VAL_SESSION_COOKIE, parseJob, VAL_STUDIO_HEADER, Api, traverseSchemaSource } from '@valbuild/shared/internal';
11
+ import { isSchemaSourceFixError, resolveSchemaSourceFixForError, Patch, getErrorMessageFromUnknownJson, PatchId as PatchId$1, JSONValue, PatchGroup, newestCommitSha, SerializedSchema, VAL_ENABLE_COOKIE_NAME, VAL_STATE_COOKIE, VAL_SESSION_COOKIE, parseJob, VAL_STUDIO_HEADER, Api, traverseSchemaSource, richTextImageSchema, forEachRichTextImage } from '@valbuild/shared/internal';
12
12
  import { createUIRequestHandler } from '@valbuild/ui/server';
13
13
  import crypto$1 from 'crypto';
14
14
  import z$1, { z } from 'zod';
@@ -3556,6 +3556,12 @@ function findNestedJsonValuesModuleErrors(schemas) {
3556
3556
  }
3557
3557
  return errors;
3558
3558
  }
3559
+
3560
+ /**
3561
+ * What a save told this instance a `.jsonValues()` entry holds: its content --
3562
+ * JSON `null` included -- or that it was deleted. See `adoptedJsonEntries`.
3563
+ */
3564
+
3559
3565
  // #region ValOps
3560
3566
  class ValOps {
3561
3567
  /** Sources from val modules, immutable (without patches or anything) */
@@ -3604,7 +3610,9 @@ class ValOps {
3604
3610
  * unlike a module source there is nothing to re-extract, because the memo was
3605
3611
  * never holding the content in the first place.
3606
3612
  *
3607
- * `null` for an entry the commit deleted.
3613
+ * `deleted` for an entry the commit deleted: tagged, not `null`, because an
3614
+ * entry's content may itself be JSON `null` (a nullable item), and a saved
3615
+ * `null` read back as "deleted" would report the entry missing.
3608
3616
  *
3609
3617
  * Never cleared: it describes what is on disk. A host rebuild makes a new
3610
3618
  * instance, which is the right reset. Bounded by the project's entry count,
@@ -3623,6 +3631,16 @@ class ValOps {
3623
3631
  this.modulesErrors = null;
3624
3632
  this.shaEntries = null;
3625
3633
  this.shaModuleErrors = null;
3634
+ for (const [moduleFilePath, entries] of (options === null || options === void 0 ? void 0 : options.snapshotJsonEntries) ?? []) {
3635
+ const adopted = new Map();
3636
+ for (const [entryKey, content] of entries) {
3637
+ adopted.set(entryKey, {
3638
+ kind: "content",
3639
+ content
3640
+ });
3641
+ }
3642
+ this.adoptedJsonEntries.set(moduleFilePath, adopted);
3643
+ }
3626
3644
  }
3627
3645
 
3628
3646
  // #region stat
@@ -3764,7 +3782,13 @@ class ValOps {
3764
3782
  }
3765
3783
  const adopted = this.adoptedJsonEntries.get(moduleFilePath) ?? new Map();
3766
3784
  for (const [entryKey, content] of Object.entries(entries)) {
3767
- adopted.set(entryKey, content);
3785
+ // A commit's `null` is a deleted entry: see `patchedJsonEntries`.
3786
+ adopted.set(entryKey, content === null ? {
3787
+ kind: "deleted"
3788
+ } : {
3789
+ kind: "content",
3790
+ content
3791
+ });
3768
3792
  }
3769
3793
  this.adoptedJsonEntries.set(moduleFilePath, adopted);
3770
3794
  }
@@ -3998,6 +4022,7 @@ class ValOps {
3998
4022
  }
3999
4023
  }
4000
4024
  const loaded = await Promise.all([...requestedKeys, ...sourceKeys].map(async entryKey => {
4025
+ var _this$adoptedJsonEntr, _this$siteSourcesBefo;
4001
4026
  const marker = record[entryKey];
4002
4027
  if (marker !== undefined && !Internal.isJson(marker)) {
4003
4028
  // Not a jsonValues entry — return the inlined value as-is (defensive).
@@ -4032,15 +4057,20 @@ class ValOps {
4032
4057
  * The BASELINE only. Pending patches replay over it below exactly as
4033
4058
  * they do over the thunk's answer.
4034
4059
  */
4035
- const adopted = this.adoptedJsonEntries.get(moduleFilePath);
4036
- if (adopted !== undefined && adopted.has(entryKey)) {
4037
- const content = adopted.get(entryKey);
4060
+ const adopted = (_this$adoptedJsonEntr = this.adoptedJsonEntries.get(moduleFilePath)) === null || _this$adoptedJsonEntr === void 0 ? void 0 : _this$adoptedJsonEntr.get(entryKey);
4061
+ if (adopted !== undefined) {
4038
4062
  return {
4039
4063
  entryKey,
4040
- baseContent: content === null ? undefined : content
4064
+ baseContent: adopted.kind === "deleted" ? undefined : adopted.content
4041
4065
  };
4042
4066
  }
4043
- const thunk = Internal.getJsonImport(marker);
4067
+ /*
4068
+ * A snapshot's marker came as JSON, with no import behind it: the
4069
+ * entry the proposal did not save is the build's, behind the marker
4070
+ * the module evaluated to.
4071
+ */
4072
+ const siteMarker = (_this$siteSourcesBefo = this.siteSourcesBeforeSnapshot) === null || _this$siteSourcesBefo === void 0 || (_this$siteSourcesBefo = _this$siteSourcesBefo[moduleFilePath]) === null || _this$siteSourcesBefo === void 0 ? void 0 : _this$siteSourcesBefo[entryKey];
4073
+ const thunk = Internal.getJsonImport(marker) ?? (Internal.isJson(siteMarker) ? Internal.getJsonImport(siteMarker) : undefined);
4044
4074
  if (!thunk) {
4045
4075
  return {
4046
4076
  entryKey,
@@ -8951,6 +8981,116 @@ function proposalSnapshot(proposal) {
8951
8981
  }
8952
8982
  return snapshot;
8953
8983
  }
8984
+
8985
+ /**
8986
+ * The `.jsonValues()` entries the proposal saved, by module and key.
8987
+ *
8988
+ * An entry's content is not in its module's Source -- that holds a marker per
8989
+ * entry -- but in the entry's own `*.val.json`, and a save writes that file:
8990
+ * it reaches the proposal's address as saved TEXT ({@link ValProposal.files}),
8991
+ * not as Source. So the snapshot alone shows the build's entries. This is the
8992
+ * other half: each saved `*.val.json`, read as the entry its module names.
8993
+ *
8994
+ * Which entry a file is comes from the module's `.val.ts`, whose
8995
+ * `c.json(() => import("..."))` thunks name the files -- the saved text if
8996
+ * the save rewrote it, else the build's own (`projectSource`). An entry whose
8997
+ * module cannot be read that way is left to the build, as before: this adds
8998
+ * what it can find and never fails the address.
8999
+ */
9000
+ function proposalJsonEntries(proposal, projectSource) {
9001
+ const found = new Map();
9002
+ const savedJson = Object.keys(proposal.files).filter(path => path.endsWith(".val.json"));
9003
+ if (savedJson.length === 0) return found;
9004
+ const savedJsonPaths = new Set(savedJson);
9005
+ for (const [moduleFilePath, source] of Object.entries(proposal.modules)) {
9006
+ if (!holdsJsonMarkers(source)) continue;
9007
+ const relative = moduleFilePath.replace(/^\//, "");
9008
+ const text = proposal.files[relative] ?? (projectSource === null || projectSource === void 0 ? void 0 : projectSource[relative]);
9009
+ if (text === undefined) continue;
9010
+ const entries = jsonEntryPaths(relative, text);
9011
+ if (entries === null) continue;
9012
+ const byKey = new Map();
9013
+ for (const [key, importPath] of entries) {
9014
+ const jsonPath = resolveExistingJsonPath(moduleFilePath, importPath).replace(/^\//, "");
9015
+ if (!savedJsonPaths.has(jsonPath)) continue;
9016
+ const content = parseJson(proposal.files[jsonPath]);
9017
+ if (content !== undefined) byKey.set(key, content);
9018
+ }
9019
+ if (byKey.size > 0) found.set(moduleFilePath, byKey);
9020
+ }
9021
+ return found;
9022
+ }
9023
+
9024
+ /**
9025
+ * The snapshot as a page reads it: {@link proposalSnapshot}, with each saved
9026
+ * `.jsonValues()` entry's content in place of its marker -- the shape a draft
9027
+ * already has, where the entries it edits hold content and the rest are
9028
+ * markers the readers resolve from the build. See {@link proposalJsonEntries}.
9029
+ *
9030
+ * For the readers only. Val's own server keeps the markers
9031
+ * ({@link proposalSnapshot}): its patching works on them, and it takes the
9032
+ * saved entries separately.
9033
+ */
9034
+ function proposalSnapshotForReaders(proposal, projectSource) {
9035
+ const snapshot = proposalSnapshot(proposal);
9036
+ for (const [moduleFilePath, entries] of proposalJsonEntries(proposal, projectSource)) {
9037
+ const source = snapshot[moduleFilePath];
9038
+ if (!isObject$1(source)) continue;
9039
+ const filled = {};
9040
+ for (const [key, value] of Object.entries(source)) {
9041
+ if (isJsonValue(value)) filled[key] = value;
9042
+ }
9043
+ for (const [key, content] of entries) {
9044
+ // Own keys only: an entry may be named `toString`.
9045
+ if (Object.prototype.hasOwnProperty.call(filled, key)) {
9046
+ filled[key] = content;
9047
+ }
9048
+ }
9049
+ snapshot[moduleFilePath] = filled;
9050
+ }
9051
+ return snapshot;
9052
+ }
9053
+
9054
+ /** A record of `.jsonValues()` markers (`{ _type: "json" }`), at least one. */
9055
+ function holdsJsonMarkers(source) {
9056
+ return isObject$1(source) && Object.values(source).some(value => isObject$1(value) && value["_type"] === "json");
9057
+ }
9058
+
9059
+ /** Each entry key's import path, from a `.jsonValues()` module's text. */
9060
+ function jsonEntryPaths(fileName, text) {
9061
+ try {
9062
+ const analysis = analyzeValModule(ts.createSourceFile(fileName, text, ts.ScriptTarget.ES2015, true));
9063
+ if (result.isErr(analysis)) return null;
9064
+ const paths = new Map();
9065
+ for (const [key, entry] of analyzeJsonValuesEntries(analysis.value.source)) {
9066
+ paths.set(key, entry.importPath);
9067
+ }
9068
+ return paths;
9069
+ } catch {
9070
+ // Not a module this can read: its entries stay the build's.
9071
+ return null;
9072
+ }
9073
+ }
9074
+ function parseJson(text) {
9075
+ if (text === undefined) return undefined;
9076
+ try {
9077
+ const value = JSON.parse(text);
9078
+ return isMutableJsonValue(value) ? value : undefined;
9079
+ } catch {
9080
+ return undefined;
9081
+ }
9082
+ }
9083
+
9084
+ /** {@link isJsonValue}, for the mutable JSON a server patches. */
9085
+ function isMutableJsonValue(value) {
9086
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
9087
+ return true;
9088
+ }
9089
+ if (Array.isArray(value)) {
9090
+ return value.every(isMutableJsonValue);
9091
+ }
9092
+ return isObject$1(value) && Object.values(value).every(isMutableJsonValue);
9093
+ }
8954
9094
  function isObject$1(value) {
8955
9095
  return typeof value === "object" && value !== null && !Array.isArray(value);
8956
9096
  }
@@ -9395,7 +9535,8 @@ class ValOpsHttp extends ValOps {
9395
9535
  auth, valModules, options) {
9396
9536
  super(valModules, options !== null && options !== void 0 && options.proposal ? {
9397
9537
  ...options,
9398
- snapshotSources: proposalSnapshot(options.proposal)
9538
+ snapshotSources: proposalSnapshot(options.proposal),
9539
+ snapshotJsonEntries: proposalJsonEntries(options.proposal, options.projectSource)
9399
9540
  } : options);
9400
9541
  this.contentUrl = contentUrl;
9401
9542
  this.project = project;
@@ -20236,7 +20377,7 @@ function readFileOrUndefined$1(absolutePath) {
20236
20377
  * be uploaded first). On the way DOWN the same playlists are rewritten back to
20237
20378
  * relative names, which is what lets a published stream be plain static files.
20238
20379
  */
20239
- function isRecord$2(value) {
20380
+ function isRecord$3(value) {
20240
20381
  return typeof value === "object" && value !== null && !Array.isArray(value);
20241
20382
  }
20242
20383
  function isPlaylist$1(ref) {
@@ -20245,14 +20386,14 @@ function isPlaylist$1(ref) {
20245
20386
 
20246
20387
  /** The paths a video value holds, each with where in the value it sits. */
20247
20388
  function videoPathsOf(video) {
20248
- if (!isRecord$2(video) || typeof video.path !== "string") {
20389
+ if (!isRecord$3(video) || typeof video.path !== "string") {
20249
20390
  return [];
20250
20391
  }
20251
20392
  const found = [{
20252
20393
  at: ["path"],
20253
20394
  path: video.path
20254
20395
  }];
20255
- if (isRecord$2(video.poster) && typeof video.poster.path === "string") {
20396
+ if (isRecord$3(video.poster) && typeof video.poster.path === "string") {
20256
20397
  found.push({
20257
20398
  at: ["poster", "path"],
20258
20399
  path: video.poster.path
@@ -20260,7 +20401,7 @@ function videoPathsOf(video) {
20260
20401
  }
20261
20402
  if (Array.isArray(video.captions)) {
20262
20403
  video.captions.forEach((track, index) => {
20263
- if (isRecord$2(track) && typeof track.path === "string") {
20404
+ if (isRecord$3(track) && typeof track.path === "string") {
20264
20405
  found.push({
20265
20406
  at: ["captions", String(index), "path"],
20266
20407
  path: track.path
@@ -20331,7 +20472,7 @@ function metadataOf(localRef, role, video) {
20331
20472
  height: video.height
20332
20473
  };
20333
20474
  }
20334
- if (role === "poster" && isRecord$2(video.poster)) {
20475
+ if (role === "poster" && isRecord$3(video.poster)) {
20335
20476
  return {
20336
20477
  mimeType: video.poster.mimeType ?? mimeType,
20337
20478
  width: video.poster.width,
@@ -20347,7 +20488,7 @@ function metadataOf(localRef, role, video) {
20347
20488
  * Places a video's local files on Val Remote, each once, and remembers where.
20348
20489
  *
20349
20490
  * The one implementation of "upload a video" for every fix that does it: a
20350
- * video field (`video:upload-remote`), and an entry of an `s.videoset()`
20491
+ * video field (`video:upload-remote`), and an entry of an `s.videodir()`
20351
20492
  * (`videos:upload-remote`). What differs between the two is which files go up
20352
20493
  * and what their refs are hashed against, so those are the arguments.
20353
20494
  *
@@ -20440,7 +20581,7 @@ function resolveVideo(ctx) {
20440
20581
  }
20441
20582
  const resolved = Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
20442
20583
  const video = resolved.source;
20443
- if (((_resolved$schema = resolved.schema) === null || _resolved$schema === void 0 ? void 0 : _resolved$schema.type) !== "video" || !isRecord$2(video) || typeof video.path !== "string") {
20584
+ if (((_resolved$schema = resolved.schema) === null || _resolved$schema === void 0 ? void 0 : _resolved$schema.type) !== "video" || !isRecord$3(video) || typeof video.path !== "string") {
20444
20585
  return {
20445
20586
  success: false,
20446
20587
  result: {
@@ -20460,7 +20601,7 @@ function resolveVideo(ctx) {
20460
20601
  * The paths of a video FIELD that its fix moves.
20461
20602
  *
20462
20603
  * All of them for a video of its own. For a field picked from an
20463
- * `s.videoset()` (`referencedModule`), only the poster and the captions: the
20604
+ * `s.videodir()` (`referencedModule`), only the poster and the captions: the
20464
20605
  * video file is the set's — its key there — and moving it from here would
20465
20606
  * leave the field naming a file the set does not have. The set's own fix
20466
20607
  * (`videos:upload-remote`) moves it, and rewrites the fields that name it.
@@ -20633,6 +20774,192 @@ async function handleVideoDownloadRemote(ctx) {
20633
20774
  };
20634
20775
  }
20635
20776
 
20777
+ /**
20778
+ * The fields that name an entry of a media collection by its key, and the
20779
+ * patches that point them at a new key.
20780
+ *
20781
+ * A collection — `s.imagedir()`, `s.filedir()`, `s.videodir()` — is a record
20782
+ * keyed by each entry's file path, and a field picked from it
20783
+ * (`s.image(imagesVal)`, `s.file(filesVal)`, `s.video(videosVal)`) stores that
20784
+ * key as its `path`. So when `--fix` renames a key — moving an entry of a
20785
+ * `.remote()` collection to Val Remote renames it from the local path to the
20786
+ * ref — every field holding the old key would name an entry the collection no
20787
+ * longer has, and nothing could repair it afterwards.
20788
+ *
20789
+ * Two kinds of value name a key:
20790
+ *
20791
+ * - **A field whose schema is backed by the collection** (`referencedModule`).
20792
+ * - **An inline image in rich text** whose `img` schema is backed by it
20793
+ * (`s.richtext({ img: s.image(imagesVal) })`). The field's own schema is
20794
+ * `richtext`, so a walk looking only for image leaves goes straight past
20795
+ * them — the Studio's reference scan learned that once already
20796
+ * (`getReferencedFiles`), and uses the same `forEachRichTextImage`.
20797
+ *
20798
+ * **A field is renamed when the key it names is the old key**, and "names" is
20799
+ * `galleryKeyOf` in core: the field's `path` when that is a key, and otherwise
20800
+ * the local path inside it when it is a ref. That is how a field is read
20801
+ * (`fillFromGallery`) and how it is validated, so it is exactly the set of
20802
+ * fields the rename would break. The second shape is real: an image uploaded
20803
+ * THROUGH an `s.image(remoteGallery)` field in the Studio stores the ref in
20804
+ * the field and keys the entry by the local path inside it, so uploading that
20805
+ * entry here has to move the field to the entry's new key as well. A ref that
20806
+ * is itself a key names its own entry, and is left alone.
20807
+ *
20808
+ * Nothing here imports `fixHandlers.ts` at runtime: that module calls this one.
20809
+ */
20810
+
20811
+ /** What of a `Service` the project-wide walk needs. */
20812
+
20813
+ /** A value that names an entry of a collection by its key. */
20814
+
20815
+ function isRecord$2(value) {
20816
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20817
+ }
20818
+
20819
+ /** Whether a serialized schema is backed by `collection` anywhere in it. */
20820
+ function isBackedBy(schema, collection) {
20821
+ if (Array.isArray(schema)) {
20822
+ return schema.some(item => isBackedBy(item, collection));
20823
+ }
20824
+ if (!isRecord$2(schema)) {
20825
+ return false;
20826
+ }
20827
+ if (schema.referencedModule === collection) {
20828
+ return true;
20829
+ }
20830
+ return Object.values(schema).some(value => isBackedBy(value, collection));
20831
+ }
20832
+
20833
+ /**
20834
+ * The references already found, per service and collection.
20835
+ *
20836
+ * `val validate --fix` renames a key once per local entry, so a gallery of N
20837
+ * local entries would otherwise walk the project N times — validating every
20838
+ * module that uses the gallery each time. Caching per `Service` is exact, not
20839
+ * an approximation: a `Service` answers `get` from the snapshot it was created
20840
+ * with (`patch` writes to disk and changes nothing it returns), so the walk
20841
+ * would find the same references every time. A new `Service` — the language
20842
+ * server makes one after every edit — starts with nothing cached.
20843
+ */
20844
+ const referencesByService = new WeakMap();
20845
+
20846
+ /**
20847
+ * Every value in the project that names an entry of `collection`, as the
20848
+ * service's snapshot has it. Found once per service; see
20849
+ * {@link referencesByService}.
20850
+ */
20851
+ function collectionReferencesOf(service, collection) {
20852
+ let byCollection = referencesByService.get(service);
20853
+ if (!byCollection) {
20854
+ byCollection = new Map();
20855
+ referencesByService.set(service, byCollection);
20856
+ }
20857
+ let references = byCollection.get(collection);
20858
+ if (!references) {
20859
+ references = findCollectionReferences(service, collection);
20860
+ // A walk that failed is not an answer; the next caller tries again.
20861
+ references.catch(() => byCollection.delete(collection));
20862
+ byCollection.set(collection, references);
20863
+ }
20864
+ return references;
20865
+ }
20866
+
20867
+ /**
20868
+ * The walk itself.
20869
+ *
20870
+ * Validated reads for the modules backed by the collection, because only
20871
+ * those come back with their `.jsonValues()` entries loaded — an unvalidated
20872
+ * read hands back each entry as a marker, and a field inside one would be
20873
+ * invisible. Every other module is read once, unvalidated, to find out it is
20874
+ * not one of them.
20875
+ */
20876
+ async function findCollectionReferences(service, collection) {
20877
+ const found = [];
20878
+ for (const moduleFilePath of service.getModuleFilePaths()) {
20879
+ const shallow = await service.get(moduleFilePath, "", {
20880
+ validate: false
20881
+ });
20882
+ if (!isBackedBy(shallow.schema, collection)) {
20883
+ continue;
20884
+ }
20885
+ const loaded = await service.get(moduleFilePath, "", {
20886
+ validate: true
20887
+ });
20888
+ if (loaded.source === undefined || !loaded.schema) {
20889
+ continue;
20890
+ }
20891
+ const add = (sourcePath, value) => {
20892
+ if (isRecord$2(value) && typeof value.path === "string") {
20893
+ found.push({
20894
+ moduleFilePath,
20895
+ sourcePath,
20896
+ path: value.path
20897
+ });
20898
+ }
20899
+ };
20900
+ traverseSchemaSource(loaded.source, loaded.schema, moduleFilePath, ({
20901
+ source,
20902
+ schema,
20903
+ path: sourcePath
20904
+ }) => {
20905
+ if (schema.type === "image" || schema.type === "file" || schema.type === "video") {
20906
+ if (schema.referencedModule === collection) {
20907
+ add(sourcePath, source);
20908
+ }
20909
+ } else if (schema.type === "richtext") {
20910
+ var _richTextImageSchema;
20911
+ if (((_richTextImageSchema = richTextImageSchema(schema)) === null || _richTextImageSchema === void 0 ? void 0 : _richTextImageSchema.referencedModule) === collection) {
20912
+ forEachRichTextImage(sourcePath, source, add);
20913
+ }
20914
+ }
20915
+ });
20916
+ }
20917
+ return found;
20918
+ }
20919
+
20920
+ /**
20921
+ * The patch path of `sourcePath`. Not `sourceToPatchPath`: a collection's keys
20922
+ * are file paths, and their dots do not survive its `split(".")`.
20923
+ */
20924
+ function patchPathOf$1(sourcePath) {
20925
+ const [, modulePath] = Internal.splitModuleFilePathAndModulePath(sourcePath);
20926
+ return Internal.splitModulePath(modulePath);
20927
+ }
20928
+
20929
+ /**
20930
+ * The patches that point every value naming a key in `renamed` at its new key.
20931
+ *
20932
+ * `renamed` is keyed by the collection's keys BEFORE the rename, and the
20933
+ * collection is read as it is now — so call this before its own patch lands.
20934
+ *
20935
+ * One `add` on each value's `path`, so the alt text, hotspot, poster and
20936
+ * captions beside it are never rewritten.
20937
+ */
20938
+ async function collectionReferencePatches(service, collection, renamed) {
20939
+ const collectionSource = (await service.get(collection, "", {
20940
+ validate: false
20941
+ })).source;
20942
+ const keys = new Set(isRecord$2(collectionSource) ? Object.keys(collectionSource) : []);
20943
+ const byModule = new Map();
20944
+ for (const reference of await collectionReferencesOf(service, collection)) {
20945
+ const key = Internal.media.galleryKeyOf(reference.path, candidate => keys.has(candidate));
20946
+ if (key === null || !Object.prototype.hasOwnProperty.call(renamed, key)) {
20947
+ continue;
20948
+ }
20949
+ const patch = byModule.get(reference.moduleFilePath) ?? [];
20950
+ patch.push({
20951
+ op: "add",
20952
+ path: patchPathOf$1(reference.sourcePath).concat("path"),
20953
+ value: renamed[key]
20954
+ });
20955
+ byModule.set(reference.moduleFilePath, patch);
20956
+ }
20957
+ return [...byModule].map(([moduleFilePath, patch]) => ({
20958
+ moduleFilePath,
20959
+ patch
20960
+ }));
20961
+ }
20962
+
20636
20963
  /**
20637
20964
  * What is out of step between a gallery's entries and its directory.
20638
20965
  *
@@ -20741,7 +21068,7 @@ function filesInDirectory({
20741
21068
  }
20742
21069
 
20743
21070
  /**
20744
- * The fixes of an `s.videoset()` — the video twin of an image gallery.
21071
+ * The fixes of an `s.videodir()` — the video twin of an image gallery.
20745
21072
  *
20746
21073
  * A set is a record keyed by each video's file path (an HLS stream by its
20747
21074
  * master playlist), so most of this is the gallery fixes again with the key as
@@ -21004,34 +21331,14 @@ async function filesNamedByVideoFields(service, options) {
21004
21331
  /**
21005
21332
  * The patches that point every `s.video(set)` field at a set entry's new key.
21006
21333
  *
21007
- * One `add` on each field's `path`, so the description, poster, times and
21008
- * captions beside it are never rewritten. A field of another set, or of no
21009
- * set, is left alone even when its path is the same string: it names its own
21010
- * file.
21334
+ * The same walk an image or file collection's rename uses
21335
+ * (`collectionReferencePatches`): one `add` on each field's `path`, so the
21336
+ * description, poster, times and captions beside it are never rewritten, and
21337
+ * a field of another set, or of no set, is left alone even when its path is
21338
+ * the same string — it names its own file.
21011
21339
  */
21012
21340
  async function videosetReferencePatches(service, setModuleFilePath, renamed) {
21013
- const byModule = new Map();
21014
- for (const field of await videoFieldsOf(service)) {
21015
- if (field.schema.referencedModule !== setModuleFilePath) {
21016
- continue;
21017
- }
21018
- const value = field.source;
21019
- const current = isRecord$1(value) ? value.path : undefined;
21020
- if (typeof current !== "string" || !(current in renamed)) {
21021
- continue;
21022
- }
21023
- const patch = byModule.get(field.moduleFilePath) ?? [];
21024
- patch.push({
21025
- op: "add",
21026
- path: patchPathOf(field.sourcePath).concat("path"),
21027
- value: renamed[current]
21028
- });
21029
- byModule.set(field.moduleFilePath, patch);
21030
- }
21031
- return [...byModule].map(([moduleFilePath, patch]) => ({
21032
- moduleFilePath,
21033
- patch
21034
- }));
21341
+ return collectionReferencePatches(service, setModuleFilePath, renamed);
21035
21342
  }
21036
21343
 
21037
21344
  /** The record at `sourcePath` and its entries, or why it could not be read. */
@@ -22615,13 +22922,13 @@ async function handleRemoteFileUpload(ctx) {
22615
22922
  return uploadRemoteFileCore(ctx, fileRefProp, fileSourceMetadata, resolveRemoteFileSchema);
22616
22923
  }
22617
22924
 
22618
- // Gallery (s.imageset({ ... }).remote() / s.fileset({ ... }).remote()) upload.
22925
+ // Gallery (s.imagedir(dir, { ... }).remote() / s.filedir(dir, { ... }).remote()) upload.
22619
22926
  // Unlike a single image/file field, a gallery entry is keyed by its local file
22620
22927
  // path and the value is bare metadata (no FileSource), so we derive the file
22621
22928
  // ref from the key and synthesize the image/file schema from the record's
22622
22929
  // media options.
22623
22930
  async function handleRemoteGalleryFileUpload(ctx) {
22624
- var _ctx$validationError$;
22931
+ var _ctx$validationError$, _ctx$remoteFiles$ctx$;
22625
22932
  if (!ctx.fix) {
22626
22933
  return {
22627
22934
  success: false,
@@ -22659,7 +22966,7 @@ async function handleRemoteGalleryFileUpload(ctx) {
22659
22966
  // `accept`/`dir` come from the RECORD that holds the entry, resolved
22660
22967
  // from the entry's PARENT path - not from the module root, which is only the
22661
22968
  // record when the gallery is the whole module. A nested gallery
22662
- // (s.object({ gallery: s.imageset(...) })) would otherwise synthesize a schema
22969
+ // (s.object({ gallery: s.imagedir(...) })) would otherwise synthesize a schema
22663
22970
  // with no options and bake a validation hash into the remote ref that can
22664
22971
  // never validate, so a mismatch fails fast instead of uploading.
22665
22972
  const [, parentModulePath] = Internal.splitModuleFilePathAndModulePath(Internal.parentOfSourcePath(ctx.sourcePath));
@@ -22694,7 +23001,24 @@ async function handleRemoteGalleryFileUpload(ctx) {
22694
23001
  } : {})
22695
23002
  }
22696
23003
  };
22697
- return uploadRemoteFileCore(ctx, fileRef, metadata, schema);
23004
+ const uploaded = await uploadRemoteFileCore(ctx, fileRef, metadata, schema);
23005
+ const ref = (_ctx$remoteFiles$ctx$ = ctx.remoteFiles[ctx.sourcePath]) === null || _ctx$remoteFiles$ctx$ === void 0 ? void 0 : _ctx$remoteFiles$ctx$.ref;
23006
+ if (!uploaded.success || !uploaded.shouldApplyPatch || ref === undefined) {
23007
+ return uploaded;
23008
+ }
23009
+ // `createFixPatch` renames the entry's key to the ref. Every field picked
23010
+ // from the gallery that holds the old key — and every rich text image from
23011
+ // it — would then name an image the gallery no longer has, so they are
23012
+ // pointed at the ref in the same run, as `videos:upload-remote` does.
23013
+ const otherModulePatches = await collectionReferencePatches(ctx.service, ctx.moduleFilePath, {
23014
+ [fileRef]: ref
23015
+ });
23016
+ return {
23017
+ ...uploaded,
23018
+ ...(otherModulePatches.length > 0 ? {
23019
+ otherModulePatches
23020
+ } : {})
23021
+ };
22698
23022
  }
22699
23023
  async function handleRemoteFileDownload(ctx) {
22700
23024
  if (ctx.fix) {
@@ -22713,13 +23037,47 @@ async function handleRemoteFileDownload(ctx) {
22713
23037
  };
22714
23038
  }
22715
23039
  }
23040
+
23041
+ /**
23042
+ * `image:check-remote` / `file:check-remote`: core's "Remote image was not
23043
+ * checked." — emitted for EVERY remote value of an `s.image().remote()` /
23044
+ * `s.file().remote()`, wrong or not, because core cannot read the bytes or the
23045
+ * ref's validation hash. It is a request to look, not a finding, so there is
23046
+ * nothing to report here: `createFixPatch` does the looking (`checkRemoteRef`)
23047
+ * and returns an error only for a ref that is actually wrong, with a fix where
23048
+ * the ref only needs rewriting.
23049
+ *
23050
+ * So these stay a hand-off. Reporting core's message here instead would put an
23051
+ * error on every remote image in a project, which is what the language server
23052
+ * learned to stop doing (`mediaChecks.ts`).
23053
+ */
22716
23054
  async function handleRemoteFileCheck() {
22717
- // Skip - no action needed
22718
23055
  return {
22719
23056
  success: true,
22720
23057
  shouldApplyPatch: true
22721
23058
  };
22722
23059
  }
23060
+
23061
+ /**
23062
+ * `images:check-remote` / `files:check-remote`: a gallery key that is wrong as
23063
+ * a key — a remote URL in a gallery that is not `.remote()`, a ref that does
23064
+ * not parse, one outside the gallery's directory (`validateMediaKey` in core).
23065
+ *
23066
+ * Unlike the singular codes, core emits these only when something IS wrong,
23067
+ * and its message says what. There is nothing to look up and nothing to move,
23068
+ * so it is reported as it is, for a person: only they know whether the key or
23069
+ * the gallery's options are the mistake. It used to share the singular codes'
23070
+ * handler, which hands off to `createFixPatch` — and that has no branch for a
23071
+ * gallery key, so the error vanished and the module was reported valid.
23072
+ *
23073
+ * The same answer as `videos:check-remote`.
23074
+ */
23075
+ async function handleGalleryCheckRemote(ctx) {
23076
+ return {
23077
+ success: false,
23078
+ errorMessage: ctx.validationError.message
23079
+ };
23080
+ }
22723
23081
  async function handleUniqueFolderCheck(ctx) {
22724
23082
  const value = ctx.validationError.value;
22725
23083
  if (!value || typeof value.dir !== "string") {
@@ -22914,9 +23272,9 @@ const currentFixHandlers = {
22914
23272
  "videos:check-unique-folder": handleUniqueFolderCheck,
22915
23273
  "videos:check-all-files": handleVideosetCheckAllFiles,
22916
23274
  "image:check-remote": handleRemoteFileCheck,
22917
- "images:check-remote": handleRemoteFileCheck,
23275
+ "images:check-remote": handleGalleryCheckRemote,
22918
23276
  "file:check-remote": handleRemoteFileCheck,
22919
- "files:check-remote": handleRemoteFileCheck,
23277
+ "files:check-remote": handleGalleryCheckRemote,
22920
23278
  "images:check-unique-folder": handleUniqueFolderCheck,
22921
23279
  "files:check-unique-folder": handleUniqueFolderCheck,
22922
23280
  "images:check-all-files": handleCheckAllFiles,
@@ -23684,4 +24042,4 @@ function readCapturedReport(snapshotDir) {
23684
24042
  return JSON.parse(fs.readFileSync(reportPath, "utf-8"));
23685
24043
  }
23686
24044
 
23687
- export { APP_MODE_REQUIRES_REMOTE_FILES, DEFAULT_LOGIN_EXPIRES_IN_SECONDS, DEFAULT_LOGIN_HOST, DEFAULT_LOGIN_POLL_INTERVAL_SECONDS, EXTERNAL_RESULT, InMemoryPatchStore, Service, ValFSHost, ValLoginError, ValModuleLoader, ValOpsFS, ValOpsHttp, ValOpsMemory, ValSourceFileHandler, analyzeValModule, awaitValLoginConfirmation, checkRemoteRef, classifyJsonValuesOp, compareWithCapturedReport, createDefaultValFSHost, createFixPatch, createJsonEntryPathMap, createModulePathMap, createPrettierFormatter, createService, createValApiRouter, createValModuleFileInspector, createValOps, createValServer, currentFixHandlers, decodeJwtWithoutVerifying, defineExternal, describePatchStoreProblems, downloadFileFromRemote, encodeJwt, err, evalValConfigFile, extractFileMetadata, extractImageMetadata, extractJsonValuesEntry, extractVideoMetadata, extractVideoMetadataFromFile, filesOfVideo, filesOfVideosetEntry, findAndEvalValConfigFile, findDefaultExport, findJsonEntryFilePath, fixHandlers, formatPatchSourceError, formatSyntaxErrorTree, getCachedRemoteFileDir, getCachedRemoteFilePath, getCompilerOptions, getExpire, getFileExt, getModulePathRange, getPersonalAccessTokenPath, getSettings, getValidationErrorFileRef, handleCheckAllFiles, handleExternalUpload, handleFileMetadata, handleJsonValuesExtractEntry, handleRemoteFileCheck, handleRemoteFileDownload, handleRemoteFileUpload, handleRemoteGalleryFileUpload, handleUniqueFolderCheck, handleVideosetCheckAllFiles, handleVideosetCheckRemote, handleVideosetMetadata, handleVideosetUploadRemote, initHandlerOptions, isExternalResult, loadValModules, ok, parsePersonalAccessTokenFile, patchSourceFile, persistPersonalAccessToken, planJsonValuesEntryExtraction, proposalFromEnv, proposalSnapshot, readCapturedReport, readPatchStore, rebaseContentOp, replaySnapshot, resolveRemoteFileAuth, safeReadGit, startValLogin, uploadRemoteFile, validateMetadata, verifyJwt, videosetEntryVideoSchema };
24045
+ export { APP_MODE_REQUIRES_REMOTE_FILES, DEFAULT_LOGIN_EXPIRES_IN_SECONDS, DEFAULT_LOGIN_HOST, DEFAULT_LOGIN_POLL_INTERVAL_SECONDS, EXTERNAL_RESULT, InMemoryPatchStore, Service, ValFSHost, ValLoginError, ValModuleLoader, ValOpsFS, ValOpsHttp, ValOpsMemory, ValSourceFileHandler, analyzeValModule, awaitValLoginConfirmation, checkRemoteRef, classifyJsonValuesOp, compareWithCapturedReport, createDefaultValFSHost, createFixPatch, createJsonEntryPathMap, createModulePathMap, createPrettierFormatter, createService, createValApiRouter, createValModuleFileInspector, createValOps, createValServer, currentFixHandlers, decodeJwtWithoutVerifying, defineExternal, describePatchStoreProblems, downloadFileFromRemote, encodeJwt, err, evalValConfigFile, extractFileMetadata, extractImageMetadata, extractJsonValuesEntry, extractVideoMetadata, extractVideoMetadataFromFile, filesOfVideo, filesOfVideosetEntry, findAndEvalValConfigFile, findDefaultExport, findJsonEntryFilePath, fixHandlers, formatPatchSourceError, formatSyntaxErrorTree, getCachedRemoteFileDir, getCachedRemoteFilePath, getCompilerOptions, getExpire, getFileExt, getModulePathRange, getPersonalAccessTokenPath, getSettings, getValidationErrorFileRef, handleCheckAllFiles, handleExternalUpload, handleFileMetadata, handleJsonValuesExtractEntry, handleRemoteFileCheck, handleRemoteFileDownload, handleRemoteFileUpload, handleRemoteGalleryFileUpload, handleUniqueFolderCheck, handleVideosetCheckAllFiles, handleVideosetCheckRemote, handleVideosetMetadata, handleVideosetUploadRemote, initHandlerOptions, isExternalResult, loadValModules, ok, parsePersonalAccessTokenFile, patchSourceFile, persistPersonalAccessToken, planJsonValuesEntryExtraction, proposalFromEnv, proposalSnapshot, proposalSnapshotForReaders, readCapturedReport, readPatchStore, rebaseContentOp, replaySnapshot, resolveRemoteFileAuth, safeReadGit, startValLogin, uploadRemoteFile, validateMetadata, verifyJwt, videosetEntryVideoSchema };
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.143.1",
19
+ "version": "0.144.1",
20
20
  "devDependencies": {
21
21
  "@prettier/sync": "^0.6.1",
22
22
  "@types/jest": "^30.0.0",
@@ -30,9 +30,9 @@
30
30
  "typescript": "^6.0.3",
31
31
  "zod": "^4.4.3",
32
32
  "zod-validation-error": "^5.0.0",
33
- "@valbuild/shared": "0.143.0",
34
- "@valbuild/ui": "0.143.1",
35
- "@valbuild/core": "0.143.0"
33
+ "@valbuild/shared": "0.144.0",
34
+ "@valbuild/ui": "0.144.0",
35
+ "@valbuild/core": "0.144.0"
36
36
  },
37
37
  "engines": {
38
38
  "node": "^20.19.0 || >=22"