@valbuild/server 0.143.0 → 0.144.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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';
@@ -20236,7 +20236,7 @@ function readFileOrUndefined$1(absolutePath) {
20236
20236
  * be uploaded first). On the way DOWN the same playlists are rewritten back to
20237
20237
  * relative names, which is what lets a published stream be plain static files.
20238
20238
  */
20239
- function isRecord$2(value) {
20239
+ function isRecord$3(value) {
20240
20240
  return typeof value === "object" && value !== null && !Array.isArray(value);
20241
20241
  }
20242
20242
  function isPlaylist$1(ref) {
@@ -20245,14 +20245,14 @@ function isPlaylist$1(ref) {
20245
20245
 
20246
20246
  /** The paths a video value holds, each with where in the value it sits. */
20247
20247
  function videoPathsOf(video) {
20248
- if (!isRecord$2(video) || typeof video.path !== "string") {
20248
+ if (!isRecord$3(video) || typeof video.path !== "string") {
20249
20249
  return [];
20250
20250
  }
20251
20251
  const found = [{
20252
20252
  at: ["path"],
20253
20253
  path: video.path
20254
20254
  }];
20255
- if (isRecord$2(video.poster) && typeof video.poster.path === "string") {
20255
+ if (isRecord$3(video.poster) && typeof video.poster.path === "string") {
20256
20256
  found.push({
20257
20257
  at: ["poster", "path"],
20258
20258
  path: video.poster.path
@@ -20260,7 +20260,7 @@ function videoPathsOf(video) {
20260
20260
  }
20261
20261
  if (Array.isArray(video.captions)) {
20262
20262
  video.captions.forEach((track, index) => {
20263
- if (isRecord$2(track) && typeof track.path === "string") {
20263
+ if (isRecord$3(track) && typeof track.path === "string") {
20264
20264
  found.push({
20265
20265
  at: ["captions", String(index), "path"],
20266
20266
  path: track.path
@@ -20331,7 +20331,7 @@ function metadataOf(localRef, role, video) {
20331
20331
  height: video.height
20332
20332
  };
20333
20333
  }
20334
- if (role === "poster" && isRecord$2(video.poster)) {
20334
+ if (role === "poster" && isRecord$3(video.poster)) {
20335
20335
  return {
20336
20336
  mimeType: video.poster.mimeType ?? mimeType,
20337
20337
  width: video.poster.width,
@@ -20347,7 +20347,7 @@ function metadataOf(localRef, role, video) {
20347
20347
  * Places a video's local files on Val Remote, each once, and remembers where.
20348
20348
  *
20349
20349
  * 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()`
20350
+ * video field (`video:upload-remote`), and an entry of an `s.videodir()`
20351
20351
  * (`videos:upload-remote`). What differs between the two is which files go up
20352
20352
  * and what their refs are hashed against, so those are the arguments.
20353
20353
  *
@@ -20440,7 +20440,7 @@ function resolveVideo(ctx) {
20440
20440
  }
20441
20441
  const resolved = Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
20442
20442
  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") {
20443
+ if (((_resolved$schema = resolved.schema) === null || _resolved$schema === void 0 ? void 0 : _resolved$schema.type) !== "video" || !isRecord$3(video) || typeof video.path !== "string") {
20444
20444
  return {
20445
20445
  success: false,
20446
20446
  result: {
@@ -20460,7 +20460,7 @@ function resolveVideo(ctx) {
20460
20460
  * The paths of a video FIELD that its fix moves.
20461
20461
  *
20462
20462
  * 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
20463
+ * `s.videodir()` (`referencedModule`), only the poster and the captions: the
20464
20464
  * video file is the set's — its key there — and moving it from here would
20465
20465
  * leave the field naming a file the set does not have. The set's own fix
20466
20466
  * (`videos:upload-remote`) moves it, and rewrites the fields that name it.
@@ -20633,6 +20633,192 @@ async function handleVideoDownloadRemote(ctx) {
20633
20633
  };
20634
20634
  }
20635
20635
 
20636
+ /**
20637
+ * The fields that name an entry of a media collection by its key, and the
20638
+ * patches that point them at a new key.
20639
+ *
20640
+ * A collection — `s.imagedir()`, `s.filedir()`, `s.videodir()` — is a record
20641
+ * keyed by each entry's file path, and a field picked from it
20642
+ * (`s.image(imagesVal)`, `s.file(filesVal)`, `s.video(videosVal)`) stores that
20643
+ * key as its `path`. So when `--fix` renames a key — moving an entry of a
20644
+ * `.remote()` collection to Val Remote renames it from the local path to the
20645
+ * ref — every field holding the old key would name an entry the collection no
20646
+ * longer has, and nothing could repair it afterwards.
20647
+ *
20648
+ * Two kinds of value name a key:
20649
+ *
20650
+ * - **A field whose schema is backed by the collection** (`referencedModule`).
20651
+ * - **An inline image in rich text** whose `img` schema is backed by it
20652
+ * (`s.richtext({ img: s.image(imagesVal) })`). The field's own schema is
20653
+ * `richtext`, so a walk looking only for image leaves goes straight past
20654
+ * them — the Studio's reference scan learned that once already
20655
+ * (`getReferencedFiles`), and uses the same `forEachRichTextImage`.
20656
+ *
20657
+ * **A field is renamed when the key it names is the old key**, and "names" is
20658
+ * `galleryKeyOf` in core: the field's `path` when that is a key, and otherwise
20659
+ * the local path inside it when it is a ref. That is how a field is read
20660
+ * (`fillFromGallery`) and how it is validated, so it is exactly the set of
20661
+ * fields the rename would break. The second shape is real: an image uploaded
20662
+ * THROUGH an `s.image(remoteGallery)` field in the Studio stores the ref in
20663
+ * the field and keys the entry by the local path inside it, so uploading that
20664
+ * entry here has to move the field to the entry's new key as well. A ref that
20665
+ * is itself a key names its own entry, and is left alone.
20666
+ *
20667
+ * Nothing here imports `fixHandlers.ts` at runtime: that module calls this one.
20668
+ */
20669
+
20670
+ /** What of a `Service` the project-wide walk needs. */
20671
+
20672
+ /** A value that names an entry of a collection by its key. */
20673
+
20674
+ function isRecord$2(value) {
20675
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20676
+ }
20677
+
20678
+ /** Whether a serialized schema is backed by `collection` anywhere in it. */
20679
+ function isBackedBy(schema, collection) {
20680
+ if (Array.isArray(schema)) {
20681
+ return schema.some(item => isBackedBy(item, collection));
20682
+ }
20683
+ if (!isRecord$2(schema)) {
20684
+ return false;
20685
+ }
20686
+ if (schema.referencedModule === collection) {
20687
+ return true;
20688
+ }
20689
+ return Object.values(schema).some(value => isBackedBy(value, collection));
20690
+ }
20691
+
20692
+ /**
20693
+ * The references already found, per service and collection.
20694
+ *
20695
+ * `val validate --fix` renames a key once per local entry, so a gallery of N
20696
+ * local entries would otherwise walk the project N times — validating every
20697
+ * module that uses the gallery each time. Caching per `Service` is exact, not
20698
+ * an approximation: a `Service` answers `get` from the snapshot it was created
20699
+ * with (`patch` writes to disk and changes nothing it returns), so the walk
20700
+ * would find the same references every time. A new `Service` — the language
20701
+ * server makes one after every edit — starts with nothing cached.
20702
+ */
20703
+ const referencesByService = new WeakMap();
20704
+
20705
+ /**
20706
+ * Every value in the project that names an entry of `collection`, as the
20707
+ * service's snapshot has it. Found once per service; see
20708
+ * {@link referencesByService}.
20709
+ */
20710
+ function collectionReferencesOf(service, collection) {
20711
+ let byCollection = referencesByService.get(service);
20712
+ if (!byCollection) {
20713
+ byCollection = new Map();
20714
+ referencesByService.set(service, byCollection);
20715
+ }
20716
+ let references = byCollection.get(collection);
20717
+ if (!references) {
20718
+ references = findCollectionReferences(service, collection);
20719
+ // A walk that failed is not an answer; the next caller tries again.
20720
+ references.catch(() => byCollection.delete(collection));
20721
+ byCollection.set(collection, references);
20722
+ }
20723
+ return references;
20724
+ }
20725
+
20726
+ /**
20727
+ * The walk itself.
20728
+ *
20729
+ * Validated reads for the modules backed by the collection, because only
20730
+ * those come back with their `.jsonValues()` entries loaded — an unvalidated
20731
+ * read hands back each entry as a marker, and a field inside one would be
20732
+ * invisible. Every other module is read once, unvalidated, to find out it is
20733
+ * not one of them.
20734
+ */
20735
+ async function findCollectionReferences(service, collection) {
20736
+ const found = [];
20737
+ for (const moduleFilePath of service.getModuleFilePaths()) {
20738
+ const shallow = await service.get(moduleFilePath, "", {
20739
+ validate: false
20740
+ });
20741
+ if (!isBackedBy(shallow.schema, collection)) {
20742
+ continue;
20743
+ }
20744
+ const loaded = await service.get(moduleFilePath, "", {
20745
+ validate: true
20746
+ });
20747
+ if (loaded.source === undefined || !loaded.schema) {
20748
+ continue;
20749
+ }
20750
+ const add = (sourcePath, value) => {
20751
+ if (isRecord$2(value) && typeof value.path === "string") {
20752
+ found.push({
20753
+ moduleFilePath,
20754
+ sourcePath,
20755
+ path: value.path
20756
+ });
20757
+ }
20758
+ };
20759
+ traverseSchemaSource(loaded.source, loaded.schema, moduleFilePath, ({
20760
+ source,
20761
+ schema,
20762
+ path: sourcePath
20763
+ }) => {
20764
+ if (schema.type === "image" || schema.type === "file" || schema.type === "video") {
20765
+ if (schema.referencedModule === collection) {
20766
+ add(sourcePath, source);
20767
+ }
20768
+ } else if (schema.type === "richtext") {
20769
+ var _richTextImageSchema;
20770
+ if (((_richTextImageSchema = richTextImageSchema(schema)) === null || _richTextImageSchema === void 0 ? void 0 : _richTextImageSchema.referencedModule) === collection) {
20771
+ forEachRichTextImage(sourcePath, source, add);
20772
+ }
20773
+ }
20774
+ });
20775
+ }
20776
+ return found;
20777
+ }
20778
+
20779
+ /**
20780
+ * The patch path of `sourcePath`. Not `sourceToPatchPath`: a collection's keys
20781
+ * are file paths, and their dots do not survive its `split(".")`.
20782
+ */
20783
+ function patchPathOf$1(sourcePath) {
20784
+ const [, modulePath] = Internal.splitModuleFilePathAndModulePath(sourcePath);
20785
+ return Internal.splitModulePath(modulePath);
20786
+ }
20787
+
20788
+ /**
20789
+ * The patches that point every value naming a key in `renamed` at its new key.
20790
+ *
20791
+ * `renamed` is keyed by the collection's keys BEFORE the rename, and the
20792
+ * collection is read as it is now — so call this before its own patch lands.
20793
+ *
20794
+ * One `add` on each value's `path`, so the alt text, hotspot, poster and
20795
+ * captions beside it are never rewritten.
20796
+ */
20797
+ async function collectionReferencePatches(service, collection, renamed) {
20798
+ const collectionSource = (await service.get(collection, "", {
20799
+ validate: false
20800
+ })).source;
20801
+ const keys = new Set(isRecord$2(collectionSource) ? Object.keys(collectionSource) : []);
20802
+ const byModule = new Map();
20803
+ for (const reference of await collectionReferencesOf(service, collection)) {
20804
+ const key = Internal.media.galleryKeyOf(reference.path, candidate => keys.has(candidate));
20805
+ if (key === null || !Object.prototype.hasOwnProperty.call(renamed, key)) {
20806
+ continue;
20807
+ }
20808
+ const patch = byModule.get(reference.moduleFilePath) ?? [];
20809
+ patch.push({
20810
+ op: "add",
20811
+ path: patchPathOf$1(reference.sourcePath).concat("path"),
20812
+ value: renamed[key]
20813
+ });
20814
+ byModule.set(reference.moduleFilePath, patch);
20815
+ }
20816
+ return [...byModule].map(([moduleFilePath, patch]) => ({
20817
+ moduleFilePath,
20818
+ patch
20819
+ }));
20820
+ }
20821
+
20636
20822
  /**
20637
20823
  * What is out of step between a gallery's entries and its directory.
20638
20824
  *
@@ -20741,7 +20927,7 @@ function filesInDirectory({
20741
20927
  }
20742
20928
 
20743
20929
  /**
20744
- * The fixes of an `s.videoset()` — the video twin of an image gallery.
20930
+ * The fixes of an `s.videodir()` — the video twin of an image gallery.
20745
20931
  *
20746
20932
  * A set is a record keyed by each video's file path (an HLS stream by its
20747
20933
  * master playlist), so most of this is the gallery fixes again with the key as
@@ -21004,34 +21190,14 @@ async function filesNamedByVideoFields(service, options) {
21004
21190
  /**
21005
21191
  * The patches that point every `s.video(set)` field at a set entry's new key.
21006
21192
  *
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.
21193
+ * The same walk an image or file collection's rename uses
21194
+ * (`collectionReferencePatches`): one `add` on each field's `path`, so the
21195
+ * description, poster, times and captions beside it are never rewritten, and
21196
+ * a field of another set, or of no set, is left alone even when its path is
21197
+ * the same string — it names its own file.
21011
21198
  */
21012
21199
  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
- }));
21200
+ return collectionReferencePatches(service, setModuleFilePath, renamed);
21035
21201
  }
21036
21202
 
21037
21203
  /** The record at `sourcePath` and its entries, or why it could not be read. */
@@ -22615,13 +22781,13 @@ async function handleRemoteFileUpload(ctx) {
22615
22781
  return uploadRemoteFileCore(ctx, fileRefProp, fileSourceMetadata, resolveRemoteFileSchema);
22616
22782
  }
22617
22783
 
22618
- // Gallery (s.imageset({ ... }).remote() / s.fileset({ ... }).remote()) upload.
22784
+ // Gallery (s.imagedir(dir, { ... }).remote() / s.filedir(dir, { ... }).remote()) upload.
22619
22785
  // Unlike a single image/file field, a gallery entry is keyed by its local file
22620
22786
  // path and the value is bare metadata (no FileSource), so we derive the file
22621
22787
  // ref from the key and synthesize the image/file schema from the record's
22622
22788
  // media options.
22623
22789
  async function handleRemoteGalleryFileUpload(ctx) {
22624
- var _ctx$validationError$;
22790
+ var _ctx$validationError$, _ctx$remoteFiles$ctx$;
22625
22791
  if (!ctx.fix) {
22626
22792
  return {
22627
22793
  success: false,
@@ -22659,7 +22825,7 @@ async function handleRemoteGalleryFileUpload(ctx) {
22659
22825
  // `accept`/`dir` come from the RECORD that holds the entry, resolved
22660
22826
  // from the entry's PARENT path - not from the module root, which is only the
22661
22827
  // record when the gallery is the whole module. A nested gallery
22662
- // (s.object({ gallery: s.imageset(...) })) would otherwise synthesize a schema
22828
+ // (s.object({ gallery: s.imagedir(...) })) would otherwise synthesize a schema
22663
22829
  // with no options and bake a validation hash into the remote ref that can
22664
22830
  // never validate, so a mismatch fails fast instead of uploading.
22665
22831
  const [, parentModulePath] = Internal.splitModuleFilePathAndModulePath(Internal.parentOfSourcePath(ctx.sourcePath));
@@ -22694,7 +22860,24 @@ async function handleRemoteGalleryFileUpload(ctx) {
22694
22860
  } : {})
22695
22861
  }
22696
22862
  };
22697
- return uploadRemoteFileCore(ctx, fileRef, metadata, schema);
22863
+ const uploaded = await uploadRemoteFileCore(ctx, fileRef, metadata, schema);
22864
+ const ref = (_ctx$remoteFiles$ctx$ = ctx.remoteFiles[ctx.sourcePath]) === null || _ctx$remoteFiles$ctx$ === void 0 ? void 0 : _ctx$remoteFiles$ctx$.ref;
22865
+ if (!uploaded.success || !uploaded.shouldApplyPatch || ref === undefined) {
22866
+ return uploaded;
22867
+ }
22868
+ // `createFixPatch` renames the entry's key to the ref. Every field picked
22869
+ // from the gallery that holds the old key — and every rich text image from
22870
+ // it — would then name an image the gallery no longer has, so they are
22871
+ // pointed at the ref in the same run, as `videos:upload-remote` does.
22872
+ const otherModulePatches = await collectionReferencePatches(ctx.service, ctx.moduleFilePath, {
22873
+ [fileRef]: ref
22874
+ });
22875
+ return {
22876
+ ...uploaded,
22877
+ ...(otherModulePatches.length > 0 ? {
22878
+ otherModulePatches
22879
+ } : {})
22880
+ };
22698
22881
  }
22699
22882
  async function handleRemoteFileDownload(ctx) {
22700
22883
  if (ctx.fix) {
@@ -22713,13 +22896,47 @@ async function handleRemoteFileDownload(ctx) {
22713
22896
  };
22714
22897
  }
22715
22898
  }
22899
+
22900
+ /**
22901
+ * `image:check-remote` / `file:check-remote`: core's "Remote image was not
22902
+ * checked." — emitted for EVERY remote value of an `s.image().remote()` /
22903
+ * `s.file().remote()`, wrong or not, because core cannot read the bytes or the
22904
+ * ref's validation hash. It is a request to look, not a finding, so there is
22905
+ * nothing to report here: `createFixPatch` does the looking (`checkRemoteRef`)
22906
+ * and returns an error only for a ref that is actually wrong, with a fix where
22907
+ * the ref only needs rewriting.
22908
+ *
22909
+ * So these stay a hand-off. Reporting core's message here instead would put an
22910
+ * error on every remote image in a project, which is what the language server
22911
+ * learned to stop doing (`mediaChecks.ts`).
22912
+ */
22716
22913
  async function handleRemoteFileCheck() {
22717
- // Skip - no action needed
22718
22914
  return {
22719
22915
  success: true,
22720
22916
  shouldApplyPatch: true
22721
22917
  };
22722
22918
  }
22919
+
22920
+ /**
22921
+ * `images:check-remote` / `files:check-remote`: a gallery key that is wrong as
22922
+ * a key — a remote URL in a gallery that is not `.remote()`, a ref that does
22923
+ * not parse, one outside the gallery's directory (`validateMediaKey` in core).
22924
+ *
22925
+ * Unlike the singular codes, core emits these only when something IS wrong,
22926
+ * and its message says what. There is nothing to look up and nothing to move,
22927
+ * so it is reported as it is, for a person: only they know whether the key or
22928
+ * the gallery's options are the mistake. It used to share the singular codes'
22929
+ * handler, which hands off to `createFixPatch` — and that has no branch for a
22930
+ * gallery key, so the error vanished and the module was reported valid.
22931
+ *
22932
+ * The same answer as `videos:check-remote`.
22933
+ */
22934
+ async function handleGalleryCheckRemote(ctx) {
22935
+ return {
22936
+ success: false,
22937
+ errorMessage: ctx.validationError.message
22938
+ };
22939
+ }
22723
22940
  async function handleUniqueFolderCheck(ctx) {
22724
22941
  const value = ctx.validationError.value;
22725
22942
  if (!value || typeof value.dir !== "string") {
@@ -22914,9 +23131,9 @@ const currentFixHandlers = {
22914
23131
  "videos:check-unique-folder": handleUniqueFolderCheck,
22915
23132
  "videos:check-all-files": handleVideosetCheckAllFiles,
22916
23133
  "image:check-remote": handleRemoteFileCheck,
22917
- "images:check-remote": handleRemoteFileCheck,
23134
+ "images:check-remote": handleGalleryCheckRemote,
22918
23135
  "file:check-remote": handleRemoteFileCheck,
22919
- "files:check-remote": handleRemoteFileCheck,
23136
+ "files:check-remote": handleGalleryCheckRemote,
22920
23137
  "images:check-unique-folder": handleUniqueFolderCheck,
22921
23138
  "files:check-unique-folder": handleUniqueFolderCheck,
22922
23139
  "images:check-all-files": handleCheckAllFiles,
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.0",
19
+ "version": "0.144.0",
20
20
  "devDependencies": {
21
21
  "@prettier/sync": "^0.6.1",
22
22
  "@types/jest": "^30.0.0",
@@ -30,9 +30,9 @@
30
30
  "typescript": "^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.0",
35
- "@valbuild/core": "0.143.0"
33
+ "@valbuild/core": "0.144.0",
34
+ "@valbuild/shared": "0.144.0",
35
+ "@valbuild/ui": "0.144.0"
36
36
  },
37
37
  "engines": {
38
38
  "node": "^20.19.0 || >=22"