@valbuild/server 0.98.0 → 0.99.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.
@@ -1,2 +1,2 @@
1
1
  import { ValidationError } from "@valbuild/core";
2
- export declare function getValidationErrorFileRef(validationError: ValidationError): string | null;
2
+ export declare function getValidationErrorFileRef(validationError: ValidationError): any;
@@ -1187,14 +1187,24 @@ class Service {
1187
1187
  const schema = this.extracted.schemas[moduleFilePath];
1188
1188
  const serializedSchema = this.extracted.serializedSchemas[moduleFilePath];
1189
1189
  const moduleError = this.extracted.moduleErrors.find(e => e.path === moduleFilePath);
1190
+ // A module whose `def()` threw is recorded WITHOUT a path: the import never
1191
+ // got far enough to reveal one, so it cannot be matched above. Those errors
1192
+ // are precisely why a module can be missing here, so report them too -
1193
+ // "was not found in val.modules" alone sends the reader hunting for a
1194
+ // registration that is already there.
1195
+ const unattributedModuleErrors = this.extracted.moduleErrors.filter(e => e.path === undefined);
1190
1196
  if (source === undefined || schema === undefined || serializedSchema === undefined) {
1191
1197
  return {
1192
1198
  path: moduleFilePath,
1193
1199
  errors: {
1194
1200
  invalidModulePath: moduleFilePath,
1195
- fatal: [{
1196
- message: (moduleError === null || moduleError === void 0 ? void 0 : moduleError.message) ?? `Module '${moduleFilePath}' was not found in val.modules`
1197
- }]
1201
+ fatal: moduleError ? [{
1202
+ message: moduleError.message
1203
+ }] : [{
1204
+ message: `Module '${moduleFilePath}' was not found in val.modules`
1205
+ }, ...unattributedModuleErrors.map(e => ({
1206
+ message: e.message
1207
+ }))]
1198
1208
  }
1199
1209
  };
1200
1210
  }
@@ -5550,6 +5560,7 @@ const ValServer = (valModules, options, callbacks) => {
5550
5560
  // #region sources
5551
5561
  "/sources/~": {
5552
5562
  PUT: async req => {
5563
+ var _patchOps$patches;
5553
5564
  const query = req.query;
5554
5565
  const cookies = req.cookies;
5555
5566
  // TODO: filter results by moduleFilePath
@@ -5607,27 +5618,34 @@ const ValServer = (valModules, options, callbacks) => {
5607
5618
  const unpatchedSources = sourcesRes.sources;
5608
5619
  // Default to true to keep the legacy contract for older clients.
5609
5620
  // The studio client always passes false: it owns patch application
5610
- // and rendering, and treats /sources/~ as a pure un-patched read.
5621
+ // and validation, and treats /sources/~ as a pure un-patched read.
5611
5622
  const applyPatches = query.apply_patches !== false;
5612
- if (applyPatches) {
5623
+ // NOTE: renders are computed here even when the client applies patches
5624
+ // itself: the render select functions live on the Schema instances and
5625
+ // are not part of the serialized schema, so the client cannot derive
5626
+ // them. They are computed on the patched sources, so that previews
5627
+ // (list titles / subtitles / images) reflect the patches that apply.
5628
+ let patchedSources = sourcesRes.sources;
5629
+ if ((((_patchOps$patches = patchOps.patches) === null || _patchOps$patches === void 0 ? void 0 : _patchOps$patches.length) ?? 0) > 0) {
5613
5630
  const onlyPatchedTreeModules = await serverOps.getSources({
5614
5631
  ...patchAnalysis,
5615
5632
  ...patchOps
5616
5633
  });
5617
- sourcesRes = {
5618
- sources: {
5619
- ...sourcesRes.sources,
5620
- ...(onlyPatchedTreeModules.sources || {})
5621
- },
5622
- errors: {
5623
- ...sourcesRes.errors,
5624
- ...(onlyPatchedTreeModules.errors || {})
5625
- }
5634
+ patchedSources = {
5635
+ ...sourcesRes.sources,
5636
+ ...(onlyPatchedTreeModules.sources || {})
5626
5637
  };
5638
+ if (applyPatches) {
5639
+ sourcesRes = {
5640
+ sources: patchedSources,
5641
+ errors: {
5642
+ ...sourcesRes.errors,
5643
+ ...(onlyPatchedTreeModules.errors || {})
5644
+ }
5645
+ };
5646
+ }
5627
5647
  }
5628
- const renderRes = applyPatches ? await serverOps.getRenders(schemasRes, sourcesRes.sources) : {
5629
- renders: {}
5630
- };
5648
+ const renderRes = await serverOps.getRenders(schemasRes, patchedSources);
5631
5649
  let sourcesValidation = {
5632
5650
  errors: {},
5633
5651
  files: {},
@@ -7778,6 +7796,33 @@ async function downloadFileFromRemote(ref, filePath) {
7778
7796
  // record-level fix expands into per-entry errors that should point at the
7779
7797
  // individual entry (e.g. `?p="/public/val/logo.png"`) rather than the record.
7780
7798
 
7799
+ // Gallery entries are keyed by their file path. Remote galleries key uploaded
7800
+ // entries by a remote URL while keeping the file on disk at its local path, so
7801
+ // normalize a remote-URL key back to that local path for on-disk reads.
7802
+ function galleryKeyToLocalPath(key) {
7803
+ const res = core.Internal.remote.splitRemoteRef(key);
7804
+ return res.status === "success" ? `/${res.filePath}` : key;
7805
+ }
7806
+
7807
+ // Patch path of the record that holds a media entry. Media keys are file paths
7808
+ // that can contain dots, which sourceToPatchPath cannot round-trip, so strip
7809
+ // the (JSON-encoded) key segment off the entry source path and convert the
7810
+ // remaining parent path — whose ancestors are plain object keys / array
7811
+ // indices — instead.
7812
+ function parentPatchPathOfMediaEntry(sourcePath, entryKey) {
7813
+ const jsonKey = JSON.stringify(entryKey);
7814
+ let parent = sourcePath;
7815
+ if (parent.endsWith(jsonKey)) {
7816
+ parent = parent.slice(0, parent.length - jsonKey.length);
7817
+ }
7818
+ if (parent.endsWith(".")) {
7819
+ parent = parent.slice(0, -1);
7820
+ } else if (parent.endsWith(core.Internal.ModuleFilePathSep)) {
7821
+ parent = parent.slice(0, parent.length - core.Internal.ModuleFilePathSep.length);
7822
+ }
7823
+ return patch.sourceToPatchPath(parent);
7824
+ }
7825
+
7781
7826
  // TODO: find a better name? transformFixesToPatch?
7782
7827
  async function createFixPatch(config, apply, sourcePath, validationError, remoteFiles, moduleSource, moduleSchema) {
7783
7828
  const remainingErrors = [];
@@ -7955,6 +8000,61 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
7955
8000
  path: patch.sourceToPatchPath(sourcePath)
7956
8001
  });
7957
8002
  }
8003
+ } else if (fix === "images:upload-remote" || fix === "files:upload-remote") {
8004
+ // Gallery entry: the record is keyed by the file path, so uploading to
8005
+ // remote means renaming the key from the local path to the remote URL
8006
+ // (remove the old key, add the new one with the same metadata).
8007
+ const remoteFile = remoteFiles[sourcePath];
8008
+ if (!remoteFile) {
8009
+ remainingErrors.push({
8010
+ ...validationError,
8011
+ message: "Cannot fix local to remote gallery entry: remote file was not uploaded",
8012
+ fixes: undefined
8013
+ });
8014
+ continue;
8015
+ }
8016
+ const metadata = remoteFile.metadata;
8017
+ if (!metadata) {
8018
+ remainingErrors.push({
8019
+ ...validationError,
8020
+ message: "Failed to get metadata for remote gallery file",
8021
+ fixes: undefined
8022
+ });
8023
+ continue;
8024
+ }
8025
+ // The entry is keyed by its (local) file path, which can contain dots
8026
+ // (e.g. image.png). sourceToPatchPath can't round-trip such keys, so
8027
+ // build the patch path from the parent record path plus the key (which
8028
+ // validateMediaKey carries as the error value), like the UI does.
8029
+ const entryKey = validationError.value;
8030
+ if (typeof entryKey !== "string") {
8031
+ remainingErrors.push({
8032
+ ...validationError,
8033
+ message: "Cannot rewrite remote gallery entry: missing entry key",
8034
+ fixes: undefined
8035
+ });
8036
+ continue;
8037
+ }
8038
+ const parentPath = parentPatchPathOfMediaEntry(sourcePath, entryKey);
8039
+ const removePath = parentPath.concat(entryKey);
8040
+ const addPath = parentPath.concat(remoteFile.ref);
8041
+ if (!patch.isNotRoot(removePath) || !patch.isNotRoot(addPath)) {
8042
+ remainingErrors.push({
8043
+ ...validationError,
8044
+ message: "Cannot rewrite remote gallery entry at root path",
8045
+ fixes: undefined
8046
+ });
8047
+ continue;
8048
+ }
8049
+ patch$1.push({
8050
+ op: "remove",
8051
+ path: removePath
8052
+ });
8053
+ patch$1.push({
8054
+ op: "add",
8055
+ path: addPath,
8056
+ value: metadata
8057
+ });
7958
8058
  } else if (fix === "image:download-remote" || fix === "file:download-remote") {
7959
8059
  const v = getRemoteValueFromValidationError(validationError);
7960
8060
  if (!v.success) {
@@ -8031,7 +8131,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
8031
8131
  }
8032
8132
  const gallerySource = moduleSource;
8033
8133
  for (const [entryKey, storedEntry] of Object.entries(gallerySource)) {
8034
- const filename = path__namespace["default"].join(config.projectRoot, entryKey);
8134
+ const filename = path__namespace["default"].join(config.projectRoot, galleryKeyToLocalPath(entryKey));
8035
8135
  let buffer;
8036
8136
  try {
8037
8137
  buffer = fs__default["default"].readFileSync(filename);
@@ -8556,31 +8656,39 @@ function getModulePathRange(modulePath, modulePathMap,
8556
8656
  // existing callers.
8557
8657
  target = "key") {
8558
8658
  var _range$children;
8559
- // Handle empty or invalid module paths gracefully
8560
- if (!modulePath || typeof modulePath !== "string") {
8561
- return undefined;
8562
- }
8563
- let segments;
8564
- try {
8565
- // Quote-aware splitter that correctly handles keys containing dots
8566
- // (e.g. file refs like `"/public/val/images/logo.png"`), unlike a naive
8567
- // split on ".". Throws on malformed input (e.g. unbalanced quotes).
8568
- segments = core.Internal.splitModulePath(modulePath);
8569
- } catch {
8570
- // Return undefined if the module path is malformed. This can happen when
8571
- // there are upstream errors in schema serialization.
8659
+ // Handle invalid module paths gracefully
8660
+ if (typeof modulePath !== "string") {
8572
8661
  return undefined;
8573
8662
  }
8574
- if (segments.length === 0) {
8575
- return undefined;
8576
- }
8577
- let range = modulePathMap[segments[0]];
8578
- for (const pathSegment of segments.slice(1)) {
8579
- var _range;
8580
- if (!range) {
8581
- break;
8663
+ let range;
8664
+ if (modulePath === "") {
8665
+ // The empty module path is the module root, stored under the "" key: the
8666
+ // source argument of `c.define`. This is what module-level diagnostics
8667
+ // (which have no module path) resolve to.
8668
+ range = modulePathMap[""];
8669
+ } else {
8670
+ let segments;
8671
+ try {
8672
+ // Quote-aware splitter that correctly handles keys containing dots
8673
+ // (e.g. file refs like `"/public/val/images/logo.png"`), unlike a naive
8674
+ // split on ".". Throws on malformed input (e.g. unbalanced quotes).
8675
+ segments = core.Internal.splitModulePath(modulePath);
8676
+ } catch {
8677
+ // Return undefined if the module path is malformed. This can happen when
8678
+ // there are upstream errors in schema serialization.
8679
+ return undefined;
8680
+ }
8681
+ if (segments.length === 0) {
8682
+ return undefined;
8683
+ }
8684
+ range = modulePathMap[segments[0]];
8685
+ for (const pathSegment of segments.slice(1)) {
8686
+ var _range;
8687
+ if (!range) {
8688
+ break;
8689
+ }
8690
+ range = (_range = range) === null || _range === void 0 || (_range = _range.children) === null || _range === void 0 ? void 0 : _range[pathSegment];
8582
8691
  }
8583
- range = (_range = range) === null || _range === void 0 || (_range = _range.children) === null || _range === void 0 ? void 0 : _range[pathSegment];
8584
8692
  }
8585
8693
  if (!range) {
8586
8694
  return undefined;
@@ -8612,7 +8720,20 @@ function createModulePathMap(sourceFile) {
8612
8720
  if (ts__default["default"].isExportAssignment(child)) {
8613
8721
  const contentNode = child.expression && ts__default["default"].isCallExpression(child.expression) && child.expression.arguments[2];
8614
8722
  if (contentNode) {
8615
- return traverse(contentNode, sourceFile);
8723
+ const map = traverse(contentNode, sourceFile) ?? {};
8724
+ // Always expose the module root under the "" key so diagnostics without
8725
+ // a module path still resolve to a location. `traverse` already emits ""
8726
+ // for primitive roots (with a tighter range), so keep that if present.
8727
+ // Object/record keys are never "" (see traverseObjectLiteral), so this
8728
+ // cannot collide with a real key.
8729
+ return {
8730
+ ...map,
8731
+ "": map[""] ?? {
8732
+ children: {},
8733
+ start: sourceFile.getLineAndCharacterOfPosition(contentNode.getStart(sourceFile)),
8734
+ end: sourceFile.getLineAndCharacterOfPosition(contentNode.getEnd())
8735
+ }
8736
+ };
8616
8737
  }
8617
8738
  }
8618
8739
  }
@@ -1187,14 +1187,24 @@ class Service {
1187
1187
  const schema = this.extracted.schemas[moduleFilePath];
1188
1188
  const serializedSchema = this.extracted.serializedSchemas[moduleFilePath];
1189
1189
  const moduleError = this.extracted.moduleErrors.find(e => e.path === moduleFilePath);
1190
+ // A module whose `def()` threw is recorded WITHOUT a path: the import never
1191
+ // got far enough to reveal one, so it cannot be matched above. Those errors
1192
+ // are precisely why a module can be missing here, so report them too -
1193
+ // "was not found in val.modules" alone sends the reader hunting for a
1194
+ // registration that is already there.
1195
+ const unattributedModuleErrors = this.extracted.moduleErrors.filter(e => e.path === undefined);
1190
1196
  if (source === undefined || schema === undefined || serializedSchema === undefined) {
1191
1197
  return {
1192
1198
  path: moduleFilePath,
1193
1199
  errors: {
1194
1200
  invalidModulePath: moduleFilePath,
1195
- fatal: [{
1196
- message: (moduleError === null || moduleError === void 0 ? void 0 : moduleError.message) ?? `Module '${moduleFilePath}' was not found in val.modules`
1197
- }]
1201
+ fatal: moduleError ? [{
1202
+ message: moduleError.message
1203
+ }] : [{
1204
+ message: `Module '${moduleFilePath}' was not found in val.modules`
1205
+ }, ...unattributedModuleErrors.map(e => ({
1206
+ message: e.message
1207
+ }))]
1198
1208
  }
1199
1209
  };
1200
1210
  }
@@ -5550,6 +5560,7 @@ const ValServer = (valModules, options, callbacks) => {
5550
5560
  // #region sources
5551
5561
  "/sources/~": {
5552
5562
  PUT: async req => {
5563
+ var _patchOps$patches;
5553
5564
  const query = req.query;
5554
5565
  const cookies = req.cookies;
5555
5566
  // TODO: filter results by moduleFilePath
@@ -5607,27 +5618,34 @@ const ValServer = (valModules, options, callbacks) => {
5607
5618
  const unpatchedSources = sourcesRes.sources;
5608
5619
  // Default to true to keep the legacy contract for older clients.
5609
5620
  // The studio client always passes false: it owns patch application
5610
- // and rendering, and treats /sources/~ as a pure un-patched read.
5621
+ // and validation, and treats /sources/~ as a pure un-patched read.
5611
5622
  const applyPatches = query.apply_patches !== false;
5612
- if (applyPatches) {
5623
+ // NOTE: renders are computed here even when the client applies patches
5624
+ // itself: the render select functions live on the Schema instances and
5625
+ // are not part of the serialized schema, so the client cannot derive
5626
+ // them. They are computed on the patched sources, so that previews
5627
+ // (list titles / subtitles / images) reflect the patches that apply.
5628
+ let patchedSources = sourcesRes.sources;
5629
+ if ((((_patchOps$patches = patchOps.patches) === null || _patchOps$patches === void 0 ? void 0 : _patchOps$patches.length) ?? 0) > 0) {
5613
5630
  const onlyPatchedTreeModules = await serverOps.getSources({
5614
5631
  ...patchAnalysis,
5615
5632
  ...patchOps
5616
5633
  });
5617
- sourcesRes = {
5618
- sources: {
5619
- ...sourcesRes.sources,
5620
- ...(onlyPatchedTreeModules.sources || {})
5621
- },
5622
- errors: {
5623
- ...sourcesRes.errors,
5624
- ...(onlyPatchedTreeModules.errors || {})
5625
- }
5634
+ patchedSources = {
5635
+ ...sourcesRes.sources,
5636
+ ...(onlyPatchedTreeModules.sources || {})
5626
5637
  };
5638
+ if (applyPatches) {
5639
+ sourcesRes = {
5640
+ sources: patchedSources,
5641
+ errors: {
5642
+ ...sourcesRes.errors,
5643
+ ...(onlyPatchedTreeModules.errors || {})
5644
+ }
5645
+ };
5646
+ }
5627
5647
  }
5628
- const renderRes = applyPatches ? await serverOps.getRenders(schemasRes, sourcesRes.sources) : {
5629
- renders: {}
5630
- };
5648
+ const renderRes = await serverOps.getRenders(schemasRes, patchedSources);
5631
5649
  let sourcesValidation = {
5632
5650
  errors: {},
5633
5651
  files: {},
@@ -7778,6 +7796,33 @@ async function downloadFileFromRemote(ref, filePath) {
7778
7796
  // record-level fix expands into per-entry errors that should point at the
7779
7797
  // individual entry (e.g. `?p="/public/val/logo.png"`) rather than the record.
7780
7798
 
7799
+ // Gallery entries are keyed by their file path. Remote galleries key uploaded
7800
+ // entries by a remote URL while keeping the file on disk at its local path, so
7801
+ // normalize a remote-URL key back to that local path for on-disk reads.
7802
+ function galleryKeyToLocalPath(key) {
7803
+ const res = core.Internal.remote.splitRemoteRef(key);
7804
+ return res.status === "success" ? `/${res.filePath}` : key;
7805
+ }
7806
+
7807
+ // Patch path of the record that holds a media entry. Media keys are file paths
7808
+ // that can contain dots, which sourceToPatchPath cannot round-trip, so strip
7809
+ // the (JSON-encoded) key segment off the entry source path and convert the
7810
+ // remaining parent path — whose ancestors are plain object keys / array
7811
+ // indices — instead.
7812
+ function parentPatchPathOfMediaEntry(sourcePath, entryKey) {
7813
+ const jsonKey = JSON.stringify(entryKey);
7814
+ let parent = sourcePath;
7815
+ if (parent.endsWith(jsonKey)) {
7816
+ parent = parent.slice(0, parent.length - jsonKey.length);
7817
+ }
7818
+ if (parent.endsWith(".")) {
7819
+ parent = parent.slice(0, -1);
7820
+ } else if (parent.endsWith(core.Internal.ModuleFilePathSep)) {
7821
+ parent = parent.slice(0, parent.length - core.Internal.ModuleFilePathSep.length);
7822
+ }
7823
+ return patch.sourceToPatchPath(parent);
7824
+ }
7825
+
7781
7826
  // TODO: find a better name? transformFixesToPatch?
7782
7827
  async function createFixPatch(config, apply, sourcePath, validationError, remoteFiles, moduleSource, moduleSchema) {
7783
7828
  const remainingErrors = [];
@@ -7955,6 +8000,61 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
7955
8000
  path: patch.sourceToPatchPath(sourcePath)
7956
8001
  });
7957
8002
  }
8003
+ } else if (fix === "images:upload-remote" || fix === "files:upload-remote") {
8004
+ // Gallery entry: the record is keyed by the file path, so uploading to
8005
+ // remote means renaming the key from the local path to the remote URL
8006
+ // (remove the old key, add the new one with the same metadata).
8007
+ const remoteFile = remoteFiles[sourcePath];
8008
+ if (!remoteFile) {
8009
+ remainingErrors.push({
8010
+ ...validationError,
8011
+ message: "Cannot fix local to remote gallery entry: remote file was not uploaded",
8012
+ fixes: undefined
8013
+ });
8014
+ continue;
8015
+ }
8016
+ const metadata = remoteFile.metadata;
8017
+ if (!metadata) {
8018
+ remainingErrors.push({
8019
+ ...validationError,
8020
+ message: "Failed to get metadata for remote gallery file",
8021
+ fixes: undefined
8022
+ });
8023
+ continue;
8024
+ }
8025
+ // The entry is keyed by its (local) file path, which can contain dots
8026
+ // (e.g. image.png). sourceToPatchPath can't round-trip such keys, so
8027
+ // build the patch path from the parent record path plus the key (which
8028
+ // validateMediaKey carries as the error value), like the UI does.
8029
+ const entryKey = validationError.value;
8030
+ if (typeof entryKey !== "string") {
8031
+ remainingErrors.push({
8032
+ ...validationError,
8033
+ message: "Cannot rewrite remote gallery entry: missing entry key",
8034
+ fixes: undefined
8035
+ });
8036
+ continue;
8037
+ }
8038
+ const parentPath = parentPatchPathOfMediaEntry(sourcePath, entryKey);
8039
+ const removePath = parentPath.concat(entryKey);
8040
+ const addPath = parentPath.concat(remoteFile.ref);
8041
+ if (!patch.isNotRoot(removePath) || !patch.isNotRoot(addPath)) {
8042
+ remainingErrors.push({
8043
+ ...validationError,
8044
+ message: "Cannot rewrite remote gallery entry at root path",
8045
+ fixes: undefined
8046
+ });
8047
+ continue;
8048
+ }
8049
+ patch$1.push({
8050
+ op: "remove",
8051
+ path: removePath
8052
+ });
8053
+ patch$1.push({
8054
+ op: "add",
8055
+ path: addPath,
8056
+ value: metadata
8057
+ });
7958
8058
  } else if (fix === "image:download-remote" || fix === "file:download-remote") {
7959
8059
  const v = getRemoteValueFromValidationError(validationError);
7960
8060
  if (!v.success) {
@@ -8031,7 +8131,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
8031
8131
  }
8032
8132
  const gallerySource = moduleSource;
8033
8133
  for (const [entryKey, storedEntry] of Object.entries(gallerySource)) {
8034
- const filename = path__namespace["default"].join(config.projectRoot, entryKey);
8134
+ const filename = path__namespace["default"].join(config.projectRoot, galleryKeyToLocalPath(entryKey));
8035
8135
  let buffer;
8036
8136
  try {
8037
8137
  buffer = fs__default["default"].readFileSync(filename);
@@ -8556,31 +8656,39 @@ function getModulePathRange(modulePath, modulePathMap,
8556
8656
  // existing callers.
8557
8657
  target = "key") {
8558
8658
  var _range$children;
8559
- // Handle empty or invalid module paths gracefully
8560
- if (!modulePath || typeof modulePath !== "string") {
8561
- return undefined;
8562
- }
8563
- let segments;
8564
- try {
8565
- // Quote-aware splitter that correctly handles keys containing dots
8566
- // (e.g. file refs like `"/public/val/images/logo.png"`), unlike a naive
8567
- // split on ".". Throws on malformed input (e.g. unbalanced quotes).
8568
- segments = core.Internal.splitModulePath(modulePath);
8569
- } catch {
8570
- // Return undefined if the module path is malformed. This can happen when
8571
- // there are upstream errors in schema serialization.
8659
+ // Handle invalid module paths gracefully
8660
+ if (typeof modulePath !== "string") {
8572
8661
  return undefined;
8573
8662
  }
8574
- if (segments.length === 0) {
8575
- return undefined;
8576
- }
8577
- let range = modulePathMap[segments[0]];
8578
- for (const pathSegment of segments.slice(1)) {
8579
- var _range;
8580
- if (!range) {
8581
- break;
8663
+ let range;
8664
+ if (modulePath === "") {
8665
+ // The empty module path is the module root, stored under the "" key: the
8666
+ // source argument of `c.define`. This is what module-level diagnostics
8667
+ // (which have no module path) resolve to.
8668
+ range = modulePathMap[""];
8669
+ } else {
8670
+ let segments;
8671
+ try {
8672
+ // Quote-aware splitter that correctly handles keys containing dots
8673
+ // (e.g. file refs like `"/public/val/images/logo.png"`), unlike a naive
8674
+ // split on ".". Throws on malformed input (e.g. unbalanced quotes).
8675
+ segments = core.Internal.splitModulePath(modulePath);
8676
+ } catch {
8677
+ // Return undefined if the module path is malformed. This can happen when
8678
+ // there are upstream errors in schema serialization.
8679
+ return undefined;
8680
+ }
8681
+ if (segments.length === 0) {
8682
+ return undefined;
8683
+ }
8684
+ range = modulePathMap[segments[0]];
8685
+ for (const pathSegment of segments.slice(1)) {
8686
+ var _range;
8687
+ if (!range) {
8688
+ break;
8689
+ }
8690
+ range = (_range = range) === null || _range === void 0 || (_range = _range.children) === null || _range === void 0 ? void 0 : _range[pathSegment];
8582
8691
  }
8583
- range = (_range = range) === null || _range === void 0 || (_range = _range.children) === null || _range === void 0 ? void 0 : _range[pathSegment];
8584
8692
  }
8585
8693
  if (!range) {
8586
8694
  return undefined;
@@ -8612,7 +8720,20 @@ function createModulePathMap(sourceFile) {
8612
8720
  if (ts__default["default"].isExportAssignment(child)) {
8613
8721
  const contentNode = child.expression && ts__default["default"].isCallExpression(child.expression) && child.expression.arguments[2];
8614
8722
  if (contentNode) {
8615
- return traverse(contentNode, sourceFile);
8723
+ const map = traverse(contentNode, sourceFile) ?? {};
8724
+ // Always expose the module root under the "" key so diagnostics without
8725
+ // a module path still resolve to a location. `traverse` already emits ""
8726
+ // for primitive roots (with a tighter range), so keep that if present.
8727
+ // Object/record keys are never "" (see traverseObjectLiteral), so this
8728
+ // cannot collide with a real key.
8729
+ return {
8730
+ ...map,
8731
+ "": map[""] ?? {
8732
+ children: {},
8733
+ start: sourceFile.getLineAndCharacterOfPosition(contentNode.getStart(sourceFile)),
8734
+ end: sourceFile.getLineAndCharacterOfPosition(contentNode.getEnd())
8735
+ }
8736
+ };
8616
8737
  }
8617
8738
  }
8618
8739
  }
@@ -1153,14 +1153,24 @@ class Service {
1153
1153
  const schema = this.extracted.schemas[moduleFilePath];
1154
1154
  const serializedSchema = this.extracted.serializedSchemas[moduleFilePath];
1155
1155
  const moduleError = this.extracted.moduleErrors.find(e => e.path === moduleFilePath);
1156
+ // A module whose `def()` threw is recorded WITHOUT a path: the import never
1157
+ // got far enough to reveal one, so it cannot be matched above. Those errors
1158
+ // are precisely why a module can be missing here, so report them too -
1159
+ // "was not found in val.modules" alone sends the reader hunting for a
1160
+ // registration that is already there.
1161
+ const unattributedModuleErrors = this.extracted.moduleErrors.filter(e => e.path === undefined);
1156
1162
  if (source === undefined || schema === undefined || serializedSchema === undefined) {
1157
1163
  return {
1158
1164
  path: moduleFilePath,
1159
1165
  errors: {
1160
1166
  invalidModulePath: moduleFilePath,
1161
- fatal: [{
1162
- message: (moduleError === null || moduleError === void 0 ? void 0 : moduleError.message) ?? `Module '${moduleFilePath}' was not found in val.modules`
1163
- }]
1167
+ fatal: moduleError ? [{
1168
+ message: moduleError.message
1169
+ }] : [{
1170
+ message: `Module '${moduleFilePath}' was not found in val.modules`
1171
+ }, ...unattributedModuleErrors.map(e => ({
1172
+ message: e.message
1173
+ }))]
1164
1174
  }
1165
1175
  };
1166
1176
  }
@@ -5516,6 +5526,7 @@ const ValServer = (valModules, options, callbacks) => {
5516
5526
  // #region sources
5517
5527
  "/sources/~": {
5518
5528
  PUT: async req => {
5529
+ var _patchOps$patches;
5519
5530
  const query = req.query;
5520
5531
  const cookies = req.cookies;
5521
5532
  // TODO: filter results by moduleFilePath
@@ -5573,27 +5584,34 @@ const ValServer = (valModules, options, callbacks) => {
5573
5584
  const unpatchedSources = sourcesRes.sources;
5574
5585
  // Default to true to keep the legacy contract for older clients.
5575
5586
  // The studio client always passes false: it owns patch application
5576
- // and rendering, and treats /sources/~ as a pure un-patched read.
5587
+ // and validation, and treats /sources/~ as a pure un-patched read.
5577
5588
  const applyPatches = query.apply_patches !== false;
5578
- if (applyPatches) {
5589
+ // NOTE: renders are computed here even when the client applies patches
5590
+ // itself: the render select functions live on the Schema instances and
5591
+ // are not part of the serialized schema, so the client cannot derive
5592
+ // them. They are computed on the patched sources, so that previews
5593
+ // (list titles / subtitles / images) reflect the patches that apply.
5594
+ let patchedSources = sourcesRes.sources;
5595
+ if ((((_patchOps$patches = patchOps.patches) === null || _patchOps$patches === void 0 ? void 0 : _patchOps$patches.length) ?? 0) > 0) {
5579
5596
  const onlyPatchedTreeModules = await serverOps.getSources({
5580
5597
  ...patchAnalysis,
5581
5598
  ...patchOps
5582
5599
  });
5583
- sourcesRes = {
5584
- sources: {
5585
- ...sourcesRes.sources,
5586
- ...(onlyPatchedTreeModules.sources || {})
5587
- },
5588
- errors: {
5589
- ...sourcesRes.errors,
5590
- ...(onlyPatchedTreeModules.errors || {})
5591
- }
5600
+ patchedSources = {
5601
+ ...sourcesRes.sources,
5602
+ ...(onlyPatchedTreeModules.sources || {})
5592
5603
  };
5604
+ if (applyPatches) {
5605
+ sourcesRes = {
5606
+ sources: patchedSources,
5607
+ errors: {
5608
+ ...sourcesRes.errors,
5609
+ ...(onlyPatchedTreeModules.errors || {})
5610
+ }
5611
+ };
5612
+ }
5593
5613
  }
5594
- const renderRes = applyPatches ? await serverOps.getRenders(schemasRes, sourcesRes.sources) : {
5595
- renders: {}
5596
- };
5614
+ const renderRes = await serverOps.getRenders(schemasRes, patchedSources);
5597
5615
  let sourcesValidation = {
5598
5616
  errors: {},
5599
5617
  files: {},
@@ -7744,6 +7762,33 @@ async function downloadFileFromRemote(ref, filePath) {
7744
7762
  // record-level fix expands into per-entry errors that should point at the
7745
7763
  // individual entry (e.g. `?p="/public/val/logo.png"`) rather than the record.
7746
7764
 
7765
+ // Gallery entries are keyed by their file path. Remote galleries key uploaded
7766
+ // entries by a remote URL while keeping the file on disk at its local path, so
7767
+ // normalize a remote-URL key back to that local path for on-disk reads.
7768
+ function galleryKeyToLocalPath(key) {
7769
+ const res = Internal.remote.splitRemoteRef(key);
7770
+ return res.status === "success" ? `/${res.filePath}` : key;
7771
+ }
7772
+
7773
+ // Patch path of the record that holds a media entry. Media keys are file paths
7774
+ // that can contain dots, which sourceToPatchPath cannot round-trip, so strip
7775
+ // the (JSON-encoded) key segment off the entry source path and convert the
7776
+ // remaining parent path — whose ancestors are plain object keys / array
7777
+ // indices — instead.
7778
+ function parentPatchPathOfMediaEntry(sourcePath, entryKey) {
7779
+ const jsonKey = JSON.stringify(entryKey);
7780
+ let parent = sourcePath;
7781
+ if (parent.endsWith(jsonKey)) {
7782
+ parent = parent.slice(0, parent.length - jsonKey.length);
7783
+ }
7784
+ if (parent.endsWith(".")) {
7785
+ parent = parent.slice(0, -1);
7786
+ } else if (parent.endsWith(Internal.ModuleFilePathSep)) {
7787
+ parent = parent.slice(0, parent.length - Internal.ModuleFilePathSep.length);
7788
+ }
7789
+ return sourceToPatchPath(parent);
7790
+ }
7791
+
7747
7792
  // TODO: find a better name? transformFixesToPatch?
7748
7793
  async function createFixPatch(config, apply, sourcePath, validationError, remoteFiles, moduleSource, moduleSchema) {
7749
7794
  const remainingErrors = [];
@@ -7921,6 +7966,61 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
7921
7966
  path: sourceToPatchPath(sourcePath)
7922
7967
  });
7923
7968
  }
7969
+ } else if (fix === "images:upload-remote" || fix === "files:upload-remote") {
7970
+ // Gallery entry: the record is keyed by the file path, so uploading to
7971
+ // remote means renaming the key from the local path to the remote URL
7972
+ // (remove the old key, add the new one with the same metadata).
7973
+ const remoteFile = remoteFiles[sourcePath];
7974
+ if (!remoteFile) {
7975
+ remainingErrors.push({
7976
+ ...validationError,
7977
+ message: "Cannot fix local to remote gallery entry: remote file was not uploaded",
7978
+ fixes: undefined
7979
+ });
7980
+ continue;
7981
+ }
7982
+ const metadata = remoteFile.metadata;
7983
+ if (!metadata) {
7984
+ remainingErrors.push({
7985
+ ...validationError,
7986
+ message: "Failed to get metadata for remote gallery file",
7987
+ fixes: undefined
7988
+ });
7989
+ continue;
7990
+ }
7991
+ // The entry is keyed by its (local) file path, which can contain dots
7992
+ // (e.g. image.png). sourceToPatchPath can't round-trip such keys, so
7993
+ // build the patch path from the parent record path plus the key (which
7994
+ // validateMediaKey carries as the error value), like the UI does.
7995
+ const entryKey = validationError.value;
7996
+ if (typeof entryKey !== "string") {
7997
+ remainingErrors.push({
7998
+ ...validationError,
7999
+ message: "Cannot rewrite remote gallery entry: missing entry key",
8000
+ fixes: undefined
8001
+ });
8002
+ continue;
8003
+ }
8004
+ const parentPath = parentPatchPathOfMediaEntry(sourcePath, entryKey);
8005
+ const removePath = parentPath.concat(entryKey);
8006
+ const addPath = parentPath.concat(remoteFile.ref);
8007
+ if (!isNotRoot(removePath) || !isNotRoot(addPath)) {
8008
+ remainingErrors.push({
8009
+ ...validationError,
8010
+ message: "Cannot rewrite remote gallery entry at root path",
8011
+ fixes: undefined
8012
+ });
8013
+ continue;
8014
+ }
8015
+ patch.push({
8016
+ op: "remove",
8017
+ path: removePath
8018
+ });
8019
+ patch.push({
8020
+ op: "add",
8021
+ path: addPath,
8022
+ value: metadata
8023
+ });
7924
8024
  } else if (fix === "image:download-remote" || fix === "file:download-remote") {
7925
8025
  const v = getRemoteValueFromValidationError(validationError);
7926
8026
  if (!v.success) {
@@ -7997,7 +8097,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
7997
8097
  }
7998
8098
  const gallerySource = moduleSource;
7999
8099
  for (const [entryKey, storedEntry] of Object.entries(gallerySource)) {
8000
- const filename = path__default.join(config.projectRoot, entryKey);
8100
+ const filename = path__default.join(config.projectRoot, galleryKeyToLocalPath(entryKey));
8001
8101
  let buffer;
8002
8102
  try {
8003
8103
  buffer = fs.readFileSync(filename);
@@ -8522,31 +8622,39 @@ function getModulePathRange(modulePath, modulePathMap,
8522
8622
  // existing callers.
8523
8623
  target = "key") {
8524
8624
  var _range$children;
8525
- // Handle empty or invalid module paths gracefully
8526
- if (!modulePath || typeof modulePath !== "string") {
8527
- return undefined;
8528
- }
8529
- let segments;
8530
- try {
8531
- // Quote-aware splitter that correctly handles keys containing dots
8532
- // (e.g. file refs like `"/public/val/images/logo.png"`), unlike a naive
8533
- // split on ".". Throws on malformed input (e.g. unbalanced quotes).
8534
- segments = Internal.splitModulePath(modulePath);
8535
- } catch {
8536
- // Return undefined if the module path is malformed. This can happen when
8537
- // there are upstream errors in schema serialization.
8625
+ // Handle invalid module paths gracefully
8626
+ if (typeof modulePath !== "string") {
8538
8627
  return undefined;
8539
8628
  }
8540
- if (segments.length === 0) {
8541
- return undefined;
8542
- }
8543
- let range = modulePathMap[segments[0]];
8544
- for (const pathSegment of segments.slice(1)) {
8545
- var _range;
8546
- if (!range) {
8547
- break;
8629
+ let range;
8630
+ if (modulePath === "") {
8631
+ // The empty module path is the module root, stored under the "" key: the
8632
+ // source argument of `c.define`. This is what module-level diagnostics
8633
+ // (which have no module path) resolve to.
8634
+ range = modulePathMap[""];
8635
+ } else {
8636
+ let segments;
8637
+ try {
8638
+ // Quote-aware splitter that correctly handles keys containing dots
8639
+ // (e.g. file refs like `"/public/val/images/logo.png"`), unlike a naive
8640
+ // split on ".". Throws on malformed input (e.g. unbalanced quotes).
8641
+ segments = Internal.splitModulePath(modulePath);
8642
+ } catch {
8643
+ // Return undefined if the module path is malformed. This can happen when
8644
+ // there are upstream errors in schema serialization.
8645
+ return undefined;
8646
+ }
8647
+ if (segments.length === 0) {
8648
+ return undefined;
8649
+ }
8650
+ range = modulePathMap[segments[0]];
8651
+ for (const pathSegment of segments.slice(1)) {
8652
+ var _range;
8653
+ if (!range) {
8654
+ break;
8655
+ }
8656
+ range = (_range = range) === null || _range === void 0 || (_range = _range.children) === null || _range === void 0 ? void 0 : _range[pathSegment];
8548
8657
  }
8549
- range = (_range = range) === null || _range === void 0 || (_range = _range.children) === null || _range === void 0 ? void 0 : _range[pathSegment];
8550
8658
  }
8551
8659
  if (!range) {
8552
8660
  return undefined;
@@ -8578,7 +8686,20 @@ function createModulePathMap(sourceFile) {
8578
8686
  if (ts.isExportAssignment(child)) {
8579
8687
  const contentNode = child.expression && ts.isCallExpression(child.expression) && child.expression.arguments[2];
8580
8688
  if (contentNode) {
8581
- return traverse(contentNode, sourceFile);
8689
+ const map = traverse(contentNode, sourceFile) ?? {};
8690
+ // Always expose the module root under the "" key so diagnostics without
8691
+ // a module path still resolve to a location. `traverse` already emits ""
8692
+ // for primitive roots (with a tighter range), so keep that if present.
8693
+ // Object/record keys are never "" (see traverseObjectLiteral), so this
8694
+ // cannot collide with a real key.
8695
+ return {
8696
+ ...map,
8697
+ "": map[""] ?? {
8698
+ children: {},
8699
+ start: sourceFile.getLineAndCharacterOfPosition(contentNode.getStart(sourceFile)),
8700
+ end: sourceFile.getLineAndCharacterOfPosition(contentNode.getEnd())
8701
+ }
8702
+ };
8582
8703
  }
8583
8704
  }
8584
8705
  }
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.98.0",
19
+ "version": "0.99.1",
20
20
  "devDependencies": {
21
21
  "@prettier/sync": "^0.6.1",
22
22
  "@types/jest": "^30.0.0"
@@ -29,9 +29,9 @@
29
29
  "typescript": "^5.9.3",
30
30
  "zod": "^4.3.5",
31
31
  "zod-validation-error": "^5.0.0",
32
- "@valbuild/core": "0.97.7",
33
- "@valbuild/shared": "0.97.7",
34
- "@valbuild/ui": "0.97.7"
32
+ "@valbuild/core": "0.99.1",
33
+ "@valbuild/shared": "0.99.1",
34
+ "@valbuild/ui": "0.99.1"
35
35
  },
36
36
  "engines": {
37
37
  "node": ">=18.17.0"