@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.
@@ -3593,6 +3593,12 @@ function findNestedJsonValuesModuleErrors(schemas) {
3593
3593
  }
3594
3594
  return errors;
3595
3595
  }
3596
+
3597
+ /**
3598
+ * What a save told this instance a `.jsonValues()` entry holds: its content --
3599
+ * JSON `null` included -- or that it was deleted. See `adoptedJsonEntries`.
3600
+ */
3601
+
3596
3602
  // #region ValOps
3597
3603
  class ValOps {
3598
3604
  /** Sources from val modules, immutable (without patches or anything) */
@@ -3641,7 +3647,9 @@ class ValOps {
3641
3647
  * unlike a module source there is nothing to re-extract, because the memo was
3642
3648
  * never holding the content in the first place.
3643
3649
  *
3644
- * `null` for an entry the commit deleted.
3650
+ * `deleted` for an entry the commit deleted: tagged, not `null`, because an
3651
+ * entry's content may itself be JSON `null` (a nullable item), and a saved
3652
+ * `null` read back as "deleted" would report the entry missing.
3645
3653
  *
3646
3654
  * Never cleared: it describes what is on disk. A host rebuild makes a new
3647
3655
  * instance, which is the right reset. Bounded by the project's entry count,
@@ -3660,6 +3668,16 @@ class ValOps {
3660
3668
  this.modulesErrors = null;
3661
3669
  this.shaEntries = null;
3662
3670
  this.shaModuleErrors = null;
3671
+ for (const [moduleFilePath, entries] of (options === null || options === void 0 ? void 0 : options.snapshotJsonEntries) ?? []) {
3672
+ const adopted = new Map();
3673
+ for (const [entryKey, content] of entries) {
3674
+ adopted.set(entryKey, {
3675
+ kind: "content",
3676
+ content
3677
+ });
3678
+ }
3679
+ this.adoptedJsonEntries.set(moduleFilePath, adopted);
3680
+ }
3663
3681
  }
3664
3682
 
3665
3683
  // #region stat
@@ -3801,7 +3819,13 @@ class ValOps {
3801
3819
  }
3802
3820
  const adopted = this.adoptedJsonEntries.get(moduleFilePath) ?? new Map();
3803
3821
  for (const [entryKey, content] of Object.entries(entries)) {
3804
- adopted.set(entryKey, content);
3822
+ // A commit's `null` is a deleted entry: see `patchedJsonEntries`.
3823
+ adopted.set(entryKey, content === null ? {
3824
+ kind: "deleted"
3825
+ } : {
3826
+ kind: "content",
3827
+ content
3828
+ });
3805
3829
  }
3806
3830
  this.adoptedJsonEntries.set(moduleFilePath, adopted);
3807
3831
  }
@@ -4035,6 +4059,7 @@ class ValOps {
4035
4059
  }
4036
4060
  }
4037
4061
  const loaded = await Promise.all([...requestedKeys, ...sourceKeys].map(async entryKey => {
4062
+ var _this$adoptedJsonEntr, _this$siteSourcesBefo;
4038
4063
  const marker = record[entryKey];
4039
4064
  if (marker !== undefined && !core.Internal.isJson(marker)) {
4040
4065
  // Not a jsonValues entry — return the inlined value as-is (defensive).
@@ -4069,15 +4094,20 @@ class ValOps {
4069
4094
  * The BASELINE only. Pending patches replay over it below exactly as
4070
4095
  * they do over the thunk's answer.
4071
4096
  */
4072
- const adopted = this.adoptedJsonEntries.get(moduleFilePath);
4073
- if (adopted !== undefined && adopted.has(entryKey)) {
4074
- const content = adopted.get(entryKey);
4097
+ const adopted = (_this$adoptedJsonEntr = this.adoptedJsonEntries.get(moduleFilePath)) === null || _this$adoptedJsonEntr === void 0 ? void 0 : _this$adoptedJsonEntr.get(entryKey);
4098
+ if (adopted !== undefined) {
4075
4099
  return {
4076
4100
  entryKey,
4077
- baseContent: content === null ? undefined : content
4101
+ baseContent: adopted.kind === "deleted" ? undefined : adopted.content
4078
4102
  };
4079
4103
  }
4080
- const thunk = core.Internal.getJsonImport(marker);
4104
+ /*
4105
+ * A snapshot's marker came as JSON, with no import behind it: the
4106
+ * entry the proposal did not save is the build's, behind the marker
4107
+ * the module evaluated to.
4108
+ */
4109
+ 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];
4110
+ const thunk = core.Internal.getJsonImport(marker) ?? (core.Internal.isJson(siteMarker) ? core.Internal.getJsonImport(siteMarker) : undefined);
4081
4111
  if (!thunk) {
4082
4112
  return {
4083
4113
  entryKey,
@@ -8988,6 +9018,116 @@ function proposalSnapshot(proposal) {
8988
9018
  }
8989
9019
  return snapshot;
8990
9020
  }
9021
+
9022
+ /**
9023
+ * The `.jsonValues()` entries the proposal saved, by module and key.
9024
+ *
9025
+ * An entry's content is not in its module's Source -- that holds a marker per
9026
+ * entry -- but in the entry's own `*.val.json`, and a save writes that file:
9027
+ * it reaches the proposal's address as saved TEXT ({@link ValProposal.files}),
9028
+ * not as Source. So the snapshot alone shows the build's entries. This is the
9029
+ * other half: each saved `*.val.json`, read as the entry its module names.
9030
+ *
9031
+ * Which entry a file is comes from the module's `.val.ts`, whose
9032
+ * `c.json(() => import("..."))` thunks name the files -- the saved text if
9033
+ * the save rewrote it, else the build's own (`projectSource`). An entry whose
9034
+ * module cannot be read that way is left to the build, as before: this adds
9035
+ * what it can find and never fails the address.
9036
+ */
9037
+ function proposalJsonEntries(proposal, projectSource) {
9038
+ const found = new Map();
9039
+ const savedJson = Object.keys(proposal.files).filter(path => path.endsWith(".val.json"));
9040
+ if (savedJson.length === 0) return found;
9041
+ const savedJsonPaths = new Set(savedJson);
9042
+ for (const [moduleFilePath, source] of Object.entries(proposal.modules)) {
9043
+ if (!holdsJsonMarkers(source)) continue;
9044
+ const relative = moduleFilePath.replace(/^\//, "");
9045
+ const text = proposal.files[relative] ?? (projectSource === null || projectSource === void 0 ? void 0 : projectSource[relative]);
9046
+ if (text === undefined) continue;
9047
+ const entries = jsonEntryPaths(relative, text);
9048
+ if (entries === null) continue;
9049
+ const byKey = new Map();
9050
+ for (const [key, importPath] of entries) {
9051
+ const jsonPath = resolveExistingJsonPath(moduleFilePath, importPath).replace(/^\//, "");
9052
+ if (!savedJsonPaths.has(jsonPath)) continue;
9053
+ const content = parseJson(proposal.files[jsonPath]);
9054
+ if (content !== undefined) byKey.set(key, content);
9055
+ }
9056
+ if (byKey.size > 0) found.set(moduleFilePath, byKey);
9057
+ }
9058
+ return found;
9059
+ }
9060
+
9061
+ /**
9062
+ * The snapshot as a page reads it: {@link proposalSnapshot}, with each saved
9063
+ * `.jsonValues()` entry's content in place of its marker -- the shape a draft
9064
+ * already has, where the entries it edits hold content and the rest are
9065
+ * markers the readers resolve from the build. See {@link proposalJsonEntries}.
9066
+ *
9067
+ * For the readers only. Val's own server keeps the markers
9068
+ * ({@link proposalSnapshot}): its patching works on them, and it takes the
9069
+ * saved entries separately.
9070
+ */
9071
+ function proposalSnapshotForReaders(proposal, projectSource) {
9072
+ const snapshot = proposalSnapshot(proposal);
9073
+ for (const [moduleFilePath, entries] of proposalJsonEntries(proposal, projectSource)) {
9074
+ const source = snapshot[moduleFilePath];
9075
+ if (!isObject$1(source)) continue;
9076
+ const filled = {};
9077
+ for (const [key, value] of Object.entries(source)) {
9078
+ if (isJsonValue(value)) filled[key] = value;
9079
+ }
9080
+ for (const [key, content] of entries) {
9081
+ // Own keys only: an entry may be named `toString`.
9082
+ if (Object.prototype.hasOwnProperty.call(filled, key)) {
9083
+ filled[key] = content;
9084
+ }
9085
+ }
9086
+ snapshot[moduleFilePath] = filled;
9087
+ }
9088
+ return snapshot;
9089
+ }
9090
+
9091
+ /** A record of `.jsonValues()` markers (`{ _type: "json" }`), at least one. */
9092
+ function holdsJsonMarkers(source) {
9093
+ return isObject$1(source) && Object.values(source).some(value => isObject$1(value) && value["_type"] === "json");
9094
+ }
9095
+
9096
+ /** Each entry key's import path, from a `.jsonValues()` module's text. */
9097
+ function jsonEntryPaths(fileName, text) {
9098
+ try {
9099
+ const analysis = analyzeValModule(ts__default["default"].createSourceFile(fileName, text, ts__default["default"].ScriptTarget.ES2015, true));
9100
+ if (fp.result.isErr(analysis)) return null;
9101
+ const paths = new Map();
9102
+ for (const [key, entry] of analyzeJsonValuesEntries(analysis.value.source)) {
9103
+ paths.set(key, entry.importPath);
9104
+ }
9105
+ return paths;
9106
+ } catch {
9107
+ // Not a module this can read: its entries stay the build's.
9108
+ return null;
9109
+ }
9110
+ }
9111
+ function parseJson(text) {
9112
+ if (text === undefined) return undefined;
9113
+ try {
9114
+ const value = JSON.parse(text);
9115
+ return isMutableJsonValue(value) ? value : undefined;
9116
+ } catch {
9117
+ return undefined;
9118
+ }
9119
+ }
9120
+
9121
+ /** {@link isJsonValue}, for the mutable JSON a server patches. */
9122
+ function isMutableJsonValue(value) {
9123
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
9124
+ return true;
9125
+ }
9126
+ if (Array.isArray(value)) {
9127
+ return value.every(isMutableJsonValue);
9128
+ }
9129
+ return isObject$1(value) && Object.values(value).every(isMutableJsonValue);
9130
+ }
8991
9131
  function isObject$1(value) {
8992
9132
  return typeof value === "object" && value !== null && !Array.isArray(value);
8993
9133
  }
@@ -9432,7 +9572,8 @@ class ValOpsHttp extends ValOps {
9432
9572
  auth, valModules, options) {
9433
9573
  super(valModules, options !== null && options !== void 0 && options.proposal ? {
9434
9574
  ...options,
9435
- snapshotSources: proposalSnapshot(options.proposal)
9575
+ snapshotSources: proposalSnapshot(options.proposal),
9576
+ snapshotJsonEntries: proposalJsonEntries(options.proposal, options.projectSource)
9436
9577
  } : options);
9437
9578
  this.contentUrl = contentUrl;
9438
9579
  this.project = project;
@@ -20273,7 +20414,7 @@ function readFileOrUndefined$1(absolutePath) {
20273
20414
  * be uploaded first). On the way DOWN the same playlists are rewritten back to
20274
20415
  * relative names, which is what lets a published stream be plain static files.
20275
20416
  */
20276
- function isRecord$2(value) {
20417
+ function isRecord$3(value) {
20277
20418
  return typeof value === "object" && value !== null && !Array.isArray(value);
20278
20419
  }
20279
20420
  function isPlaylist$1(ref) {
@@ -20282,14 +20423,14 @@ function isPlaylist$1(ref) {
20282
20423
 
20283
20424
  /** The paths a video value holds, each with where in the value it sits. */
20284
20425
  function videoPathsOf(video) {
20285
- if (!isRecord$2(video) || typeof video.path !== "string") {
20426
+ if (!isRecord$3(video) || typeof video.path !== "string") {
20286
20427
  return [];
20287
20428
  }
20288
20429
  const found = [{
20289
20430
  at: ["path"],
20290
20431
  path: video.path
20291
20432
  }];
20292
- if (isRecord$2(video.poster) && typeof video.poster.path === "string") {
20433
+ if (isRecord$3(video.poster) && typeof video.poster.path === "string") {
20293
20434
  found.push({
20294
20435
  at: ["poster", "path"],
20295
20436
  path: video.poster.path
@@ -20297,7 +20438,7 @@ function videoPathsOf(video) {
20297
20438
  }
20298
20439
  if (Array.isArray(video.captions)) {
20299
20440
  video.captions.forEach((track, index) => {
20300
- if (isRecord$2(track) && typeof track.path === "string") {
20441
+ if (isRecord$3(track) && typeof track.path === "string") {
20301
20442
  found.push({
20302
20443
  at: ["captions", String(index), "path"],
20303
20444
  path: track.path
@@ -20368,7 +20509,7 @@ function metadataOf(localRef, role, video) {
20368
20509
  height: video.height
20369
20510
  };
20370
20511
  }
20371
- if (role === "poster" && isRecord$2(video.poster)) {
20512
+ if (role === "poster" && isRecord$3(video.poster)) {
20372
20513
  return {
20373
20514
  mimeType: video.poster.mimeType ?? mimeType,
20374
20515
  width: video.poster.width,
@@ -20384,7 +20525,7 @@ function metadataOf(localRef, role, video) {
20384
20525
  * Places a video's local files on Val Remote, each once, and remembers where.
20385
20526
  *
20386
20527
  * The one implementation of "upload a video" for every fix that does it: a
20387
- * video field (`video:upload-remote`), and an entry of an `s.videoset()`
20528
+ * video field (`video:upload-remote`), and an entry of an `s.videodir()`
20388
20529
  * (`videos:upload-remote`). What differs between the two is which files go up
20389
20530
  * and what their refs are hashed against, so those are the arguments.
20390
20531
  *
@@ -20477,7 +20618,7 @@ function resolveVideo(ctx) {
20477
20618
  }
20478
20619
  const resolved = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
20479
20620
  const video = resolved.source;
20480
- if (((_resolved$schema = resolved.schema) === null || _resolved$schema === void 0 ? void 0 : _resolved$schema.type) !== "video" || !isRecord$2(video) || typeof video.path !== "string") {
20621
+ if (((_resolved$schema = resolved.schema) === null || _resolved$schema === void 0 ? void 0 : _resolved$schema.type) !== "video" || !isRecord$3(video) || typeof video.path !== "string") {
20481
20622
  return {
20482
20623
  success: false,
20483
20624
  result: {
@@ -20497,7 +20638,7 @@ function resolveVideo(ctx) {
20497
20638
  * The paths of a video FIELD that its fix moves.
20498
20639
  *
20499
20640
  * All of them for a video of its own. For a field picked from an
20500
- * `s.videoset()` (`referencedModule`), only the poster and the captions: the
20641
+ * `s.videodir()` (`referencedModule`), only the poster and the captions: the
20501
20642
  * video file is the set's — its key there — and moving it from here would
20502
20643
  * leave the field naming a file the set does not have. The set's own fix
20503
20644
  * (`videos:upload-remote`) moves it, and rewrites the fields that name it.
@@ -20670,6 +20811,192 @@ async function handleVideoDownloadRemote(ctx) {
20670
20811
  };
20671
20812
  }
20672
20813
 
20814
+ /**
20815
+ * The fields that name an entry of a media collection by its key, and the
20816
+ * patches that point them at a new key.
20817
+ *
20818
+ * A collection — `s.imagedir()`, `s.filedir()`, `s.videodir()` — is a record
20819
+ * keyed by each entry's file path, and a field picked from it
20820
+ * (`s.image(imagesVal)`, `s.file(filesVal)`, `s.video(videosVal)`) stores that
20821
+ * key as its `path`. So when `--fix` renames a key — moving an entry of a
20822
+ * `.remote()` collection to Val Remote renames it from the local path to the
20823
+ * ref — every field holding the old key would name an entry the collection no
20824
+ * longer has, and nothing could repair it afterwards.
20825
+ *
20826
+ * Two kinds of value name a key:
20827
+ *
20828
+ * - **A field whose schema is backed by the collection** (`referencedModule`).
20829
+ * - **An inline image in rich text** whose `img` schema is backed by it
20830
+ * (`s.richtext({ img: s.image(imagesVal) })`). The field's own schema is
20831
+ * `richtext`, so a walk looking only for image leaves goes straight past
20832
+ * them — the Studio's reference scan learned that once already
20833
+ * (`getReferencedFiles`), and uses the same `forEachRichTextImage`.
20834
+ *
20835
+ * **A field is renamed when the key it names is the old key**, and "names" is
20836
+ * `galleryKeyOf` in core: the field's `path` when that is a key, and otherwise
20837
+ * the local path inside it when it is a ref. That is how a field is read
20838
+ * (`fillFromGallery`) and how it is validated, so it is exactly the set of
20839
+ * fields the rename would break. The second shape is real: an image uploaded
20840
+ * THROUGH an `s.image(remoteGallery)` field in the Studio stores the ref in
20841
+ * the field and keys the entry by the local path inside it, so uploading that
20842
+ * entry here has to move the field to the entry's new key as well. A ref that
20843
+ * is itself a key names its own entry, and is left alone.
20844
+ *
20845
+ * Nothing here imports `fixHandlers.ts` at runtime: that module calls this one.
20846
+ */
20847
+
20848
+ /** What of a `Service` the project-wide walk needs. */
20849
+
20850
+ /** A value that names an entry of a collection by its key. */
20851
+
20852
+ function isRecord$2(value) {
20853
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20854
+ }
20855
+
20856
+ /** Whether a serialized schema is backed by `collection` anywhere in it. */
20857
+ function isBackedBy(schema, collection) {
20858
+ if (Array.isArray(schema)) {
20859
+ return schema.some(item => isBackedBy(item, collection));
20860
+ }
20861
+ if (!isRecord$2(schema)) {
20862
+ return false;
20863
+ }
20864
+ if (schema.referencedModule === collection) {
20865
+ return true;
20866
+ }
20867
+ return Object.values(schema).some(value => isBackedBy(value, collection));
20868
+ }
20869
+
20870
+ /**
20871
+ * The references already found, per service and collection.
20872
+ *
20873
+ * `val validate --fix` renames a key once per local entry, so a gallery of N
20874
+ * local entries would otherwise walk the project N times — validating every
20875
+ * module that uses the gallery each time. Caching per `Service` is exact, not
20876
+ * an approximation: a `Service` answers `get` from the snapshot it was created
20877
+ * with (`patch` writes to disk and changes nothing it returns), so the walk
20878
+ * would find the same references every time. A new `Service` — the language
20879
+ * server makes one after every edit — starts with nothing cached.
20880
+ */
20881
+ const referencesByService = new WeakMap();
20882
+
20883
+ /**
20884
+ * Every value in the project that names an entry of `collection`, as the
20885
+ * service's snapshot has it. Found once per service; see
20886
+ * {@link referencesByService}.
20887
+ */
20888
+ function collectionReferencesOf(service, collection) {
20889
+ let byCollection = referencesByService.get(service);
20890
+ if (!byCollection) {
20891
+ byCollection = new Map();
20892
+ referencesByService.set(service, byCollection);
20893
+ }
20894
+ let references = byCollection.get(collection);
20895
+ if (!references) {
20896
+ references = findCollectionReferences(service, collection);
20897
+ // A walk that failed is not an answer; the next caller tries again.
20898
+ references.catch(() => byCollection.delete(collection));
20899
+ byCollection.set(collection, references);
20900
+ }
20901
+ return references;
20902
+ }
20903
+
20904
+ /**
20905
+ * The walk itself.
20906
+ *
20907
+ * Validated reads for the modules backed by the collection, because only
20908
+ * those come back with their `.jsonValues()` entries loaded — an unvalidated
20909
+ * read hands back each entry as a marker, and a field inside one would be
20910
+ * invisible. Every other module is read once, unvalidated, to find out it is
20911
+ * not one of them.
20912
+ */
20913
+ async function findCollectionReferences(service, collection) {
20914
+ const found = [];
20915
+ for (const moduleFilePath of service.getModuleFilePaths()) {
20916
+ const shallow = await service.get(moduleFilePath, "", {
20917
+ validate: false
20918
+ });
20919
+ if (!isBackedBy(shallow.schema, collection)) {
20920
+ continue;
20921
+ }
20922
+ const loaded = await service.get(moduleFilePath, "", {
20923
+ validate: true
20924
+ });
20925
+ if (loaded.source === undefined || !loaded.schema) {
20926
+ continue;
20927
+ }
20928
+ const add = (sourcePath, value) => {
20929
+ if (isRecord$2(value) && typeof value.path === "string") {
20930
+ found.push({
20931
+ moduleFilePath,
20932
+ sourcePath,
20933
+ path: value.path
20934
+ });
20935
+ }
20936
+ };
20937
+ internal.traverseSchemaSource(loaded.source, loaded.schema, moduleFilePath, ({
20938
+ source,
20939
+ schema,
20940
+ path: sourcePath
20941
+ }) => {
20942
+ if (schema.type === "image" || schema.type === "file" || schema.type === "video") {
20943
+ if (schema.referencedModule === collection) {
20944
+ add(sourcePath, source);
20945
+ }
20946
+ } else if (schema.type === "richtext") {
20947
+ var _richTextImageSchema;
20948
+ if (((_richTextImageSchema = internal.richTextImageSchema(schema)) === null || _richTextImageSchema === void 0 ? void 0 : _richTextImageSchema.referencedModule) === collection) {
20949
+ internal.forEachRichTextImage(sourcePath, source, add);
20950
+ }
20951
+ }
20952
+ });
20953
+ }
20954
+ return found;
20955
+ }
20956
+
20957
+ /**
20958
+ * The patch path of `sourcePath`. Not `sourceToPatchPath`: a collection's keys
20959
+ * are file paths, and their dots do not survive its `split(".")`.
20960
+ */
20961
+ function patchPathOf$1(sourcePath) {
20962
+ const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(sourcePath);
20963
+ return core.Internal.splitModulePath(modulePath);
20964
+ }
20965
+
20966
+ /**
20967
+ * The patches that point every value naming a key in `renamed` at its new key.
20968
+ *
20969
+ * `renamed` is keyed by the collection's keys BEFORE the rename, and the
20970
+ * collection is read as it is now — so call this before its own patch lands.
20971
+ *
20972
+ * One `add` on each value's `path`, so the alt text, hotspot, poster and
20973
+ * captions beside it are never rewritten.
20974
+ */
20975
+ async function collectionReferencePatches(service, collection, renamed) {
20976
+ const collectionSource = (await service.get(collection, "", {
20977
+ validate: false
20978
+ })).source;
20979
+ const keys = new Set(isRecord$2(collectionSource) ? Object.keys(collectionSource) : []);
20980
+ const byModule = new Map();
20981
+ for (const reference of await collectionReferencesOf(service, collection)) {
20982
+ const key = core.Internal.media.galleryKeyOf(reference.path, candidate => keys.has(candidate));
20983
+ if (key === null || !Object.prototype.hasOwnProperty.call(renamed, key)) {
20984
+ continue;
20985
+ }
20986
+ const patch = byModule.get(reference.moduleFilePath) ?? [];
20987
+ patch.push({
20988
+ op: "add",
20989
+ path: patchPathOf$1(reference.sourcePath).concat("path"),
20990
+ value: renamed[key]
20991
+ });
20992
+ byModule.set(reference.moduleFilePath, patch);
20993
+ }
20994
+ return [...byModule].map(([moduleFilePath, patch]) => ({
20995
+ moduleFilePath,
20996
+ patch
20997
+ }));
20998
+ }
20999
+
20673
21000
  /**
20674
21001
  * What is out of step between a gallery's entries and its directory.
20675
21002
  *
@@ -20778,7 +21105,7 @@ function filesInDirectory({
20778
21105
  }
20779
21106
 
20780
21107
  /**
20781
- * The fixes of an `s.videoset()` — the video twin of an image gallery.
21108
+ * The fixes of an `s.videodir()` — the video twin of an image gallery.
20782
21109
  *
20783
21110
  * A set is a record keyed by each video's file path (an HLS stream by its
20784
21111
  * master playlist), so most of this is the gallery fixes again with the key as
@@ -21041,34 +21368,14 @@ async function filesNamedByVideoFields(service, options) {
21041
21368
  /**
21042
21369
  * The patches that point every `s.video(set)` field at a set entry's new key.
21043
21370
  *
21044
- * One `add` on each field's `path`, so the description, poster, times and
21045
- * captions beside it are never rewritten. A field of another set, or of no
21046
- * set, is left alone even when its path is the same string: it names its own
21047
- * file.
21371
+ * The same walk an image or file collection's rename uses
21372
+ * (`collectionReferencePatches`): one `add` on each field's `path`, so the
21373
+ * description, poster, times and captions beside it are never rewritten, and
21374
+ * a field of another set, or of no set, is left alone even when its path is
21375
+ * the same string — it names its own file.
21048
21376
  */
21049
21377
  async function videosetReferencePatches(service, setModuleFilePath, renamed) {
21050
- const byModule = new Map();
21051
- for (const field of await videoFieldsOf(service)) {
21052
- if (field.schema.referencedModule !== setModuleFilePath) {
21053
- continue;
21054
- }
21055
- const value = field.source;
21056
- const current = isRecord$1(value) ? value.path : undefined;
21057
- if (typeof current !== "string" || !(current in renamed)) {
21058
- continue;
21059
- }
21060
- const patch = byModule.get(field.moduleFilePath) ?? [];
21061
- patch.push({
21062
- op: "add",
21063
- path: patchPathOf(field.sourcePath).concat("path"),
21064
- value: renamed[current]
21065
- });
21066
- byModule.set(field.moduleFilePath, patch);
21067
- }
21068
- return [...byModule].map(([moduleFilePath, patch]) => ({
21069
- moduleFilePath,
21070
- patch
21071
- }));
21378
+ return collectionReferencePatches(service, setModuleFilePath, renamed);
21072
21379
  }
21073
21380
 
21074
21381
  /** The record at `sourcePath` and its entries, or why it could not be read. */
@@ -22652,13 +22959,13 @@ async function handleRemoteFileUpload(ctx) {
22652
22959
  return uploadRemoteFileCore(ctx, fileRefProp, fileSourceMetadata, resolveRemoteFileSchema);
22653
22960
  }
22654
22961
 
22655
- // Gallery (s.imageset({ ... }).remote() / s.fileset({ ... }).remote()) upload.
22962
+ // Gallery (s.imagedir(dir, { ... }).remote() / s.filedir(dir, { ... }).remote()) upload.
22656
22963
  // Unlike a single image/file field, a gallery entry is keyed by its local file
22657
22964
  // path and the value is bare metadata (no FileSource), so we derive the file
22658
22965
  // ref from the key and synthesize the image/file schema from the record's
22659
22966
  // media options.
22660
22967
  async function handleRemoteGalleryFileUpload(ctx) {
22661
- var _ctx$validationError$;
22968
+ var _ctx$validationError$, _ctx$remoteFiles$ctx$;
22662
22969
  if (!ctx.fix) {
22663
22970
  return {
22664
22971
  success: false,
@@ -22696,7 +23003,7 @@ async function handleRemoteGalleryFileUpload(ctx) {
22696
23003
  // `accept`/`dir` come from the RECORD that holds the entry, resolved
22697
23004
  // from the entry's PARENT path - not from the module root, which is only the
22698
23005
  // record when the gallery is the whole module. A nested gallery
22699
- // (s.object({ gallery: s.imageset(...) })) would otherwise synthesize a schema
23006
+ // (s.object({ gallery: s.imagedir(...) })) would otherwise synthesize a schema
22700
23007
  // with no options and bake a validation hash into the remote ref that can
22701
23008
  // never validate, so a mismatch fails fast instead of uploading.
22702
23009
  const [, parentModulePath] = core.Internal.splitModuleFilePathAndModulePath(core.Internal.parentOfSourcePath(ctx.sourcePath));
@@ -22731,7 +23038,24 @@ async function handleRemoteGalleryFileUpload(ctx) {
22731
23038
  } : {})
22732
23039
  }
22733
23040
  };
22734
- return uploadRemoteFileCore(ctx, fileRef, metadata, schema);
23041
+ const uploaded = await uploadRemoteFileCore(ctx, fileRef, metadata, schema);
23042
+ const ref = (_ctx$remoteFiles$ctx$ = ctx.remoteFiles[ctx.sourcePath]) === null || _ctx$remoteFiles$ctx$ === void 0 ? void 0 : _ctx$remoteFiles$ctx$.ref;
23043
+ if (!uploaded.success || !uploaded.shouldApplyPatch || ref === undefined) {
23044
+ return uploaded;
23045
+ }
23046
+ // `createFixPatch` renames the entry's key to the ref. Every field picked
23047
+ // from the gallery that holds the old key — and every rich text image from
23048
+ // it — would then name an image the gallery no longer has, so they are
23049
+ // pointed at the ref in the same run, as `videos:upload-remote` does.
23050
+ const otherModulePatches = await collectionReferencePatches(ctx.service, ctx.moduleFilePath, {
23051
+ [fileRef]: ref
23052
+ });
23053
+ return {
23054
+ ...uploaded,
23055
+ ...(otherModulePatches.length > 0 ? {
23056
+ otherModulePatches
23057
+ } : {})
23058
+ };
22735
23059
  }
22736
23060
  async function handleRemoteFileDownload(ctx) {
22737
23061
  if (ctx.fix) {
@@ -22750,13 +23074,47 @@ async function handleRemoteFileDownload(ctx) {
22750
23074
  };
22751
23075
  }
22752
23076
  }
23077
+
23078
+ /**
23079
+ * `image:check-remote` / `file:check-remote`: core's "Remote image was not
23080
+ * checked." — emitted for EVERY remote value of an `s.image().remote()` /
23081
+ * `s.file().remote()`, wrong or not, because core cannot read the bytes or the
23082
+ * ref's validation hash. It is a request to look, not a finding, so there is
23083
+ * nothing to report here: `createFixPatch` does the looking (`checkRemoteRef`)
23084
+ * and returns an error only for a ref that is actually wrong, with a fix where
23085
+ * the ref only needs rewriting.
23086
+ *
23087
+ * So these stay a hand-off. Reporting core's message here instead would put an
23088
+ * error on every remote image in a project, which is what the language server
23089
+ * learned to stop doing (`mediaChecks.ts`).
23090
+ */
22753
23091
  async function handleRemoteFileCheck() {
22754
- // Skip - no action needed
22755
23092
  return {
22756
23093
  success: true,
22757
23094
  shouldApplyPatch: true
22758
23095
  };
22759
23096
  }
23097
+
23098
+ /**
23099
+ * `images:check-remote` / `files:check-remote`: a gallery key that is wrong as
23100
+ * a key — a remote URL in a gallery that is not `.remote()`, a ref that does
23101
+ * not parse, one outside the gallery's directory (`validateMediaKey` in core).
23102
+ *
23103
+ * Unlike the singular codes, core emits these only when something IS wrong,
23104
+ * and its message says what. There is nothing to look up and nothing to move,
23105
+ * so it is reported as it is, for a person: only they know whether the key or
23106
+ * the gallery's options are the mistake. It used to share the singular codes'
23107
+ * handler, which hands off to `createFixPatch` — and that has no branch for a
23108
+ * gallery key, so the error vanished and the module was reported valid.
23109
+ *
23110
+ * The same answer as `videos:check-remote`.
23111
+ */
23112
+ async function handleGalleryCheckRemote(ctx) {
23113
+ return {
23114
+ success: false,
23115
+ errorMessage: ctx.validationError.message
23116
+ };
23117
+ }
22760
23118
  async function handleUniqueFolderCheck(ctx) {
22761
23119
  const value = ctx.validationError.value;
22762
23120
  if (!value || typeof value.dir !== "string") {
@@ -22951,9 +23309,9 @@ const currentFixHandlers = {
22951
23309
  "videos:check-unique-folder": handleUniqueFolderCheck,
22952
23310
  "videos:check-all-files": handleVideosetCheckAllFiles,
22953
23311
  "image:check-remote": handleRemoteFileCheck,
22954
- "images:check-remote": handleRemoteFileCheck,
23312
+ "images:check-remote": handleGalleryCheckRemote,
22955
23313
  "file:check-remote": handleRemoteFileCheck,
22956
- "files:check-remote": handleRemoteFileCheck,
23314
+ "files:check-remote": handleGalleryCheckRemote,
22957
23315
  "images:check-unique-folder": handleUniqueFolderCheck,
22958
23316
  "files:check-unique-folder": handleUniqueFolderCheck,
22959
23317
  "images:check-all-files": handleCheckAllFiles,
@@ -23807,6 +24165,7 @@ exports.persistPersonalAccessToken = persistPersonalAccessToken;
23807
24165
  exports.planJsonValuesEntryExtraction = planJsonValuesEntryExtraction;
23808
24166
  exports.proposalFromEnv = proposalFromEnv;
23809
24167
  exports.proposalSnapshot = proposalSnapshot;
24168
+ exports.proposalSnapshotForReaders = proposalSnapshotForReaders;
23810
24169
  exports.readCapturedReport = readCapturedReport;
23811
24170
  exports.readPatchStore = readPatchStore;
23812
24171
  exports.rebaseContentOp = rebaseContentOp;