@valbuild/server 0.102.0 → 0.103.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.
@@ -4,8 +4,8 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var ts = require('typescript');
6
6
  var fp = require('@valbuild/core/fp');
7
- var core = require('@valbuild/core');
8
7
  var patch = require('@valbuild/core/patch');
8
+ var core = require('@valbuild/core');
9
9
  var path = require('path');
10
10
  var fs = require('fs');
11
11
  var vm = require('node:vm');
@@ -102,7 +102,7 @@ function validateObjectProperties(nodes) {
102
102
  * validating its children.
103
103
  */
104
104
  function shallowValidateExpression(value) {
105
- return ts__default["default"].isStringLiteralLike(value) || ts__default["default"].isNumericLiteral(value) || value.kind === ts__default["default"].SyntaxKind.TrueKeyword || value.kind === ts__default["default"].SyntaxKind.FalseKeyword || value.kind === ts__default["default"].SyntaxKind.NullKeyword || ts__default["default"].isArrayLiteralExpression(value) || ts__default["default"].isObjectLiteralExpression(value) || isValFileMethodCall(value) || isValImageMethodCall(value) || isValRemoteMethodCall(value) ? undefined : new ValSyntaxError("Expression must be a literal or call c.file / c.image / c.remote", value);
105
+ return ts__default["default"].isStringLiteralLike(value) || ts__default["default"].isNumericLiteral(value) || value.kind === ts__default["default"].SyntaxKind.TrueKeyword || value.kind === ts__default["default"].SyntaxKind.FalseKeyword || value.kind === ts__default["default"].SyntaxKind.NullKeyword || ts__default["default"].isArrayLiteralExpression(value) || ts__default["default"].isObjectLiteralExpression(value) ? undefined : new ValSyntaxError("Expression must be a literal", value);
106
106
  }
107
107
 
108
108
  /**
@@ -128,55 +128,8 @@ function evaluateExpression(value) {
128
128
  return fp.result.all(value.elements.map(evaluateExpression));
129
129
  } else if (ts__default["default"].isObjectLiteralExpression(value)) {
130
130
  return fp.pipe(validateObjectProperties(value.properties), fp.result.flatMap(assignments => fp.pipe(assignments.map(assignment => fp.pipe(evaluateExpression(assignment.initializer), fp.result.map(value => [assignment.name.text, value]))), fp.result.all)), fp.result.map(Object.fromEntries));
131
- } else if (isValFileMethodCall(value)) {
132
- return fp.pipe(findValFileNodeArg(value), fp.result.flatMap(ref => {
133
- if (value.arguments.length === 2) {
134
- return fp.pipe(evaluateExpression(value.arguments[1]), fp.result.map(metadata => ({
135
- [core.FILE_REF_PROP]: ref.text,
136
- _type: "file",
137
- metadata
138
- })));
139
- } else {
140
- return fp.result.ok({
141
- [core.FILE_REF_PROP]: ref.text,
142
- _type: "file"
143
- });
144
- }
145
- }));
146
- } else if (isValImageMethodCall(value)) {
147
- return fp.pipe(findValImageNodeArg(value), fp.result.flatMap(ref => {
148
- if (value.arguments.length === 2) {
149
- return fp.pipe(evaluateExpression(value.arguments[1]), fp.result.map(metadata => ({
150
- [core.FILE_REF_PROP]: ref.text,
151
- _type: "file",
152
- [core.FILE_REF_SUBTYPE_TAG]: "image",
153
- metadata
154
- })));
155
- } else {
156
- return fp.result.ok({
157
- [core.FILE_REF_PROP]: ref.text,
158
- _type: "file",
159
- [core.FILE_REF_SUBTYPE_TAG]: "image"
160
- });
161
- }
162
- }));
163
- } else if (isValRemoteMethodCall(value)) {
164
- return fp.pipe(findValRemoteNodeArg(value), fp.result.flatMap(ref => {
165
- if (value.arguments.length === 2) {
166
- return fp.pipe(evaluateExpression(value.arguments[1]), fp.result.map(metadata => ({
167
- [core.FILE_REF_PROP]: ref.text,
168
- _type: "remote",
169
- metadata
170
- })));
171
- } else {
172
- return fp.result.ok({
173
- [core.FILE_REF_PROP]: ref.text,
174
- _type: "remote"
175
- });
176
- }
177
- }));
178
131
  } else {
179
- return fp.result.err(new ValSyntaxError("Expression must be a literal or call c.file / c.image / c.remote", value));
132
+ return fp.result.err(new ValSyntaxError("Expression must be a literal", value));
180
133
  }
181
134
  }
182
135
  function findObjectPropertyAssignment(value, key) {
@@ -190,93 +143,6 @@ function findObjectPropertyAssignment(value, key) {
190
143
  return fp.result.ok(assignment);
191
144
  }));
192
145
  }
193
- function isValFileMethodCall(node) {
194
- return ts__default["default"].isCallExpression(node) && ts__default["default"].isPropertyAccessExpression(node.expression) && ts__default["default"].isIdentifier(node.expression.expression) && node.expression.expression.text === "c" && node.expression.name.text === "file";
195
- }
196
- function isValImageMethodCall(node) {
197
- return ts__default["default"].isCallExpression(node) && ts__default["default"].isPropertyAccessExpression(node.expression) && ts__default["default"].isIdentifier(node.expression.expression) && node.expression.expression.text === "c" && node.expression.name.text === "image";
198
- }
199
- function isValRemoteMethodCall(node) {
200
- return ts__default["default"].isCallExpression(node) && ts__default["default"].isPropertyAccessExpression(node.expression) && ts__default["default"].isIdentifier(node.expression.expression) && node.expression.expression.text === "c" && node.expression.name.text === "remote";
201
- }
202
- function findValFileNodeArg(node) {
203
- if (node.arguments.length === 0) {
204
- return fp.result.err(new ValSyntaxError(`Invalid c.file() call: missing ref argument`, node));
205
- } else if (node.arguments.length > 2) {
206
- return fp.result.err(new ValSyntaxError(`Invalid c.file() call: too many arguments ${node.arguments.length}}`, node));
207
- } else if (!ts__default["default"].isStringLiteral(node.arguments[0])) {
208
- return fp.result.err(new ValSyntaxError(`Invalid c.file() call: ref must be a string literal`, node));
209
- }
210
- const refNode = node.arguments[0];
211
- return fp.result.ok(refNode);
212
- }
213
- function findValRemoteNodeArg(node) {
214
- if (node.arguments.length === 0) {
215
- return fp.result.err(new ValSyntaxError(`Invalid c.remote() call: missing ref argument`, node));
216
- } else if (node.arguments.length > 2) {
217
- return fp.result.err(new ValSyntaxError(`Invalid c.remote() call: too many arguments ${node.arguments.length}}`, node));
218
- } else if (!ts__default["default"].isStringLiteral(node.arguments[0])) {
219
- return fp.result.err(new ValSyntaxError(`Invalid c.remote() call: ref must be a string literal`, node));
220
- }
221
- const refNode = node.arguments[0];
222
- return fp.result.ok(refNode);
223
- }
224
- function findValImageNodeArg(node) {
225
- if (node.arguments.length === 0) {
226
- return fp.result.err(new ValSyntaxError(`Invalid c.image() call: missing ref argument`, node));
227
- } else if (node.arguments.length > 2) {
228
- return fp.result.err(new ValSyntaxError(`Invalid c.image() call: too many arguments ${node.arguments.length}}`, node));
229
- } else if (!ts__default["default"].isStringLiteral(node.arguments[0])) {
230
- return fp.result.err(new ValSyntaxError(`Invalid c.image() call: ref must be a string literal`, node));
231
- }
232
- const refNode = node.arguments[0];
233
- return fp.result.ok(refNode);
234
- }
235
- function findValFileMetadataArg(node) {
236
- if (node.arguments.length === 0) {
237
- return fp.result.err(new ValSyntaxError(`Invalid c.file() call: missing ref argument`, node));
238
- } else if (node.arguments.length > 2) {
239
- return fp.result.err(new ValSyntaxError(`Invalid c.file() call: too many arguments ${node.arguments.length}}`, node));
240
- } else if (node.arguments.length === 2) {
241
- if (!ts__default["default"].isObjectLiteralExpression(node.arguments[1])) {
242
- return fp.result.err(new ValSyntaxError(`Invalid c.file() call: metadata must be a object literal`, node));
243
- }
244
- return fp.result.ok(node.arguments[1]);
245
- }
246
- return fp.result.ok(undefined);
247
- }
248
- function findValImageMetadataArg(node) {
249
- if (node.arguments.length === 0) {
250
- return fp.result.err(new ValSyntaxError(`Invalid c.image() call: missing ref argument`, node));
251
- } else if (node.arguments.length > 2) {
252
- return fp.result.err(new ValSyntaxError(`Invalid c.image() call: too many arguments ${node.arguments.length}}`, node));
253
- } else if (node.arguments.length === 2) {
254
- if (!ts__default["default"].isObjectLiteralExpression(node.arguments[1])) {
255
- return fp.result.err(new ValSyntaxError(`Invalid c.image() call: metadata must be a object literal`, node));
256
- }
257
- return fp.result.ok(node.arguments[1]);
258
- }
259
- return fp.result.ok(undefined);
260
- }
261
- function findValRemoteMetadataArg(node) {
262
- if (node.arguments.length === 0) {
263
- return fp.result.err(new ValSyntaxError(`Invalid c.remote() call: missing ref argument`, node));
264
- } else if (node.arguments.length > 2) {
265
- return fp.result.err(new ValSyntaxError(`Invalid c.remote() call: too many arguments ${node.arguments.length}}`, node));
266
- } else if (node.arguments.length === 2) {
267
- if (!ts__default["default"].isObjectLiteralExpression(node.arguments[1])) {
268
- return fp.result.err(new ValSyntaxError(`Invalid c.remote() call: metadata must be a object literal`, node));
269
- }
270
- return fp.result.ok(node.arguments[1]);
271
- }
272
- return fp.result.ok(undefined);
273
- }
274
-
275
- /**
276
- * Given a list of expressions, validates that all the expressions are not
277
- * spread elements. In other words, it ensures that the expressions are the
278
- * initializers of the values at their respective indices in the evaluated list.
279
- */
280
146
  function validateInitializers(nodes) {
281
147
  for (const node of nodes) {
282
148
  if (ts__default["default"].isSpreadElement(node)) {
@@ -358,27 +224,6 @@ function isValidIdentifier(text) {
358
224
  function createPropertyAssignment(key, value) {
359
225
  return ts__default["default"].factory.createPropertyAssignment(isValidIdentifier(key) ? ts__default["default"].factory.createIdentifier(key) : ts__default["default"].factory.createStringLiteral(key), toExpression(value));
360
226
  }
361
- function createValFileReference(value) {
362
- const args = [ts__default["default"].factory.createStringLiteral(value[core.FILE_REF_PROP])];
363
- if (value.metadata) {
364
- args.push(toExpression(value.metadata));
365
- }
366
- return ts__default["default"].factory.createCallExpression(ts__default["default"].factory.createPropertyAccessExpression(ts__default["default"].factory.createIdentifier("c"), ts__default["default"].factory.createIdentifier("file")), undefined, args);
367
- }
368
- function createValImageReference(value) {
369
- const args = [ts__default["default"].factory.createStringLiteral(value[core.FILE_REF_PROP])];
370
- if (value.metadata) {
371
- args.push(toExpression(value.metadata));
372
- }
373
- return ts__default["default"].factory.createCallExpression(ts__default["default"].factory.createPropertyAccessExpression(ts__default["default"].factory.createIdentifier("c"), ts__default["default"].factory.createIdentifier("image")), undefined, args);
374
- }
375
- function createValRemoteReference(value) {
376
- const args = [ts__default["default"].factory.createStringLiteral(value[core.FILE_REF_PROP])];
377
- if (value.metadata) {
378
- args.push(toExpression(value.metadata));
379
- }
380
- return ts__default["default"].factory.createCallExpression(ts__default["default"].factory.createPropertyAccessExpression(ts__default["default"].factory.createIdentifier("c"), ts__default["default"].factory.createIdentifier("remote")), undefined, args);
381
- }
382
227
 
383
228
  /**
384
229
  * Builds the expression `c.json(() => import("<importPath>"))` used to reference
@@ -464,16 +309,12 @@ function toExpression(value) {
464
309
  } else if (Array.isArray(value)) {
465
310
  return ts__default["default"].factory.createArrayLiteralExpression(value.map(toExpression));
466
311
  } else if (typeof value === "object") {
467
- if (isValFileValue(value)) {
468
- return createValFileReference(value);
469
- }
470
- if (isValImageValue(value)) {
471
- return createValImageReference(value);
472
- }
473
- if (isValRemoteValue(value)) {
474
- return createValRemoteReference(value);
475
- }
476
- return ts__default["default"].factory.createObjectLiteralExpression(Object.entries(value).map(([key, value]) => createPropertyAssignment(key, value)));
312
+ return ts__default["default"].factory.createObjectLiteralExpression(Object.entries(value)
313
+ // `patch_id` is injected server-side onto a media source whose bytes are
314
+ // not committed yet. It describes the draft, not the content, and a
315
+ // whole-object write built from the client's optimistic view would
316
+ // otherwise print it into the user's file.
317
+ .filter(([key]) => key !== "patch_id").map(([key, value]) => createPropertyAssignment(key, value)));
477
318
  } else {
478
319
  return ts__default["default"].factory.createStringLiteral(value);
479
320
  }
@@ -606,63 +447,6 @@ function replaceInNode(document, node, key, value) {
606
447
  }
607
448
  return fp.result.ok(assignment);
608
449
  }), fp.result.map(assignment => replaceNodeValue(document, assignment.initializer, value)));
609
- } else if (isValFileMethodCall(node)) {
610
- if (key === core.FILE_REF_PROP) {
611
- if (typeof value !== "string") {
612
- return fp.result.err(new patch.PatchError("Cannot replace c.file reference with non-string value"));
613
- }
614
- return fp.pipe(findValFileNodeArg(node), fp.result.map(refNode => replaceNodeValue(document, refNode, value)));
615
- } else {
616
- return fp.pipe(findValFileMetadataArg(node), fp.result.flatMap(metadataArgNode => {
617
- if (!metadataArgNode) {
618
- return fp.result.err(new patch.PatchError("Cannot replace in c.file metadata when it does not exist"));
619
- }
620
- if (key !== "metadata") {
621
- return fp.result.err(new patch.PatchError(`Cannot replace c.file metadata key ${key} when it does not exist`));
622
- }
623
- return replaceInNode(document,
624
- // TODO: creating a fake object here might not be right - seems to work though
625
- ts__default["default"].factory.createObjectLiteralExpression([ts__default["default"].factory.createPropertyAssignment(key, metadataArgNode)]), key, value);
626
- }));
627
- }
628
- } else if (isValImageMethodCall(node)) {
629
- if (key === core.FILE_REF_PROP) {
630
- if (typeof value !== "string") {
631
- return fp.result.err(new patch.PatchError("Cannot replace c.image reference with non-string value"));
632
- }
633
- return fp.pipe(findValImageNodeArg(node), fp.result.map(refNode => replaceNodeValue(document, refNode, value)));
634
- } else {
635
- return fp.pipe(findValImageMetadataArg(node), fp.result.flatMap(metadataArgNode => {
636
- if (!metadataArgNode) {
637
- return fp.result.err(new patch.PatchError("Cannot replace in c.image metadata when it does not exist"));
638
- }
639
- if (key !== "metadata") {
640
- return fp.result.err(new patch.PatchError(`Cannot replace c.image metadata key ${key} when it does not exist`));
641
- }
642
- return replaceInNode(document,
643
- // TODO: creating a fake object here might not be right - seems to work though
644
- ts__default["default"].factory.createObjectLiteralExpression([ts__default["default"].factory.createPropertyAssignment(key, metadataArgNode)]), key, value);
645
- }));
646
- }
647
- } else if (isValRemoteMethodCall(node)) {
648
- if (key === core.FILE_REF_PROP) {
649
- if (typeof value !== "string") {
650
- return fp.result.err(new patch.PatchError("Cannot replace c.image reference with non-string value"));
651
- }
652
- return fp.pipe(findValRemoteNodeArg(node), fp.result.map(refNode => replaceNodeValue(document, refNode, value)));
653
- } else {
654
- return fp.pipe(findValRemoteMetadataArg(node), fp.result.flatMap(metadataArgNode => {
655
- if (!metadataArgNode) {
656
- return fp.result.err(new patch.PatchError("Cannot replace in c.remote metadata when it does not exist"));
657
- }
658
- if (key !== "metadata") {
659
- return fp.result.err(new patch.PatchError(`Cannot replace c.remote metadata key ${key} when it does not exist`));
660
- }
661
- return replaceInNode(document,
662
- // TODO: creating a fake object here might not be right - seems to work though
663
- ts__default["default"].factory.createObjectLiteralExpression([ts__default["default"].factory.createPropertyAssignment(key, metadataArgNode)]), key, value);
664
- }));
665
- }
666
450
  } else {
667
451
  return fp.result.err(shallowValidateExpression(node) ?? new patch.PatchError("Cannot replace in non-object/array"));
668
452
  }
@@ -679,21 +463,6 @@ function getFromNode(node, key) {
679
463
  return fp.pipe(parseAndValidateArrayInboundsIndex(key, node.elements), fp.result.map(index => node.elements[index]));
680
464
  } else if (ts__default["default"].isObjectLiteralExpression(node)) {
681
465
  return fp.pipe(findObjectPropertyAssignment(node, key), fp.result.map(assignment => assignment === null || assignment === void 0 ? void 0 : assignment.initializer));
682
- } else if (isValFileMethodCall(node)) {
683
- if (key === core.FILE_REF_PROP) {
684
- return findValFileNodeArg(node);
685
- }
686
- return findValFileMetadataArg(node);
687
- } else if (isValImageMethodCall(node)) {
688
- if (key === core.FILE_REF_PROP) {
689
- return findValImageNodeArg(node);
690
- }
691
- return findValImageMetadataArg(node);
692
- } else if (isValRemoteMethodCall(node)) {
693
- if (key === core.FILE_REF_PROP) {
694
- return findValRemoteNodeArg(node);
695
- }
696
- return findValRemoteMetadataArg(node);
697
466
  } else {
698
467
  return fp.result.err(shallowValidateExpression(node) ?? new patch.PatchError("Cannot access non-object/array"));
699
468
  }
@@ -726,39 +495,6 @@ function removeFromNode(document, node, key) {
726
495
  }
727
496
  return fp.result.ok(assignment);
728
497
  }), fp.result.map(assignment => [removeAt(document, node.properties, node.properties.indexOf(assignment))[0], assignment.initializer]));
729
- } else if (isValFileMethodCall(node)) {
730
- if (key === core.FILE_REF_PROP) {
731
- return fp.result.err(new patch.PatchError("Cannot remove a ref from c.file"));
732
- } else {
733
- return fp.pipe(findValFileMetadataArg(node), fp.result.flatMap(metadataArgNode => {
734
- if (!metadataArgNode) {
735
- return fp.result.err(new patch.PatchError("Cannot remove from c.file metadata when it does not exist"));
736
- }
737
- return removeFromNode(document, metadataArgNode, key);
738
- }));
739
- }
740
- } else if (isValImageMethodCall(node)) {
741
- if (key === core.FILE_REF_PROP) {
742
- return fp.result.err(new patch.PatchError("Cannot remove a ref from c.image"));
743
- } else {
744
- return fp.pipe(findValImageMetadataArg(node), fp.result.flatMap(metadataArgNode => {
745
- if (!metadataArgNode) {
746
- return fp.result.err(new patch.PatchError("Cannot remove from c.image metadata when it does not exist"));
747
- }
748
- return removeFromNode(document, metadataArgNode, key);
749
- }));
750
- }
751
- } else if (isValRemoteMethodCall(node)) {
752
- if (key === core.FILE_REF_PROP) {
753
- return fp.result.err(new patch.PatchError("Cannot remove a ref from c.remote"));
754
- } else {
755
- return fp.pipe(findValRemoteMetadataArg(node), fp.result.flatMap(metadataArgNode => {
756
- if (!metadataArgNode) {
757
- return fp.result.err(new patch.PatchError("Cannot remove from c.remote metadata when it does not exist"));
758
- }
759
- return removeFromNode(document, metadataArgNode, key);
760
- }));
761
- }
762
498
  } else {
763
499
  return fp.result.err(shallowValidateExpression(node) ?? new patch.PatchError("Cannot remove from non-object/array"));
764
500
  }
@@ -766,22 +502,10 @@ function removeFromNode(document, node, key) {
766
502
  function removeAtPath(document, rootNode, path) {
767
503
  return fp.pipe(getPointerFromPath(rootNode, path), fp.result.flatMap(([node, key]) => removeFromNode(document, node, key)));
768
504
  }
769
- function isValFileValue(value) {
770
- return !!(typeof value === "object" && value && core.VAL_EXTENSION in value && value[core.VAL_EXTENSION] === "file" && core.FILE_REF_PROP in value && typeof value[core.FILE_REF_PROP] === "string" && value[core.FILE_REF_SUBTYPE_TAG] !== "image");
771
- }
772
- function isValImageValue(value) {
773
- return !!(typeof value === "object" && value && core.VAL_EXTENSION in value && value[core.VAL_EXTENSION] === "file" && core.FILE_REF_PROP in value && typeof value[core.FILE_REF_PROP] === "string" && value[core.FILE_REF_SUBTYPE_TAG] === "image");
774
- }
775
- function isValRemoteValue(value) {
776
- return !!(typeof value === "object" && value && core.VAL_EXTENSION in value && value[core.VAL_EXTENSION] === "remote" && core.FILE_REF_PROP in value && typeof value[core.FILE_REF_PROP] === "string");
777
- }
778
505
  function addToNode(document, node, key, value) {
779
506
  if (ts__default["default"].isArrayLiteralExpression(node)) {
780
507
  return fp.pipe(parseAndValidateArrayInsertIndex(key, node.elements), fp.result.map(index => [insertAt(document, node.elements, index, toExpression(value))]));
781
508
  } else if (ts__default["default"].isObjectLiteralExpression(node)) {
782
- if (key === core.FILE_REF_PROP) {
783
- return fp.result.err(new patch.PatchError("Cannot add a key ref to object"));
784
- }
785
509
  return fp.pipe(findObjectPropertyAssignment(node, key), fp.result.map(assignment => {
786
510
  if (!assignment) {
787
511
  return [insertAt(document, node.properties, node.properties.length, createPropertyAssignment(key, value))];
@@ -789,57 +513,6 @@ function addToNode(document, node, key, value) {
789
513
  return replaceNodeValue(document, assignment.initializer, value);
790
514
  }
791
515
  }));
792
- } else if (isValFileMethodCall(node)) {
793
- if (key === core.FILE_REF_PROP) {
794
- if (typeof value !== "string") {
795
- return fp.result.err(new patch.PatchError(`Cannot add ${core.FILE_REF_PROP} key to c.file with non-string value`));
796
- }
797
- return fp.pipe(findValFileNodeArg(node), fp.result.map(arg => replaceNodeValue(document, arg, value)));
798
- } else {
799
- return fp.pipe(findValFileMetadataArg(node), fp.result.flatMap(metadataArgNode => {
800
- if (metadataArgNode) {
801
- return fp.result.err(new patch.PatchError("Cannot add metadata to c.file when it already exists"));
802
- }
803
- if (key !== "metadata") {
804
- return fp.result.err(new patch.PatchError(`Cannot add ${key} key to c.file: only metadata is allowed`));
805
- }
806
- return fp.result.ok([insertAt(document, node.arguments, node.arguments.length, toExpression(value))]);
807
- }));
808
- }
809
- } else if (isValImageMethodCall(node)) {
810
- if (key === core.FILE_REF_PROP) {
811
- if (typeof value !== "string") {
812
- return fp.result.err(new patch.PatchError(`Cannot add ${core.FILE_REF_PROP} key to c.image with non-string value`));
813
- }
814
- return fp.pipe(findValImageNodeArg(node), fp.result.map(arg => replaceNodeValue(document, arg, value)));
815
- } else {
816
- return fp.pipe(findValImageMetadataArg(node), fp.result.flatMap(metadataArgNode => {
817
- if (metadataArgNode) {
818
- return fp.result.err(new patch.PatchError("Cannot add metadata to c.image when it already exists"));
819
- }
820
- if (key !== "metadata") {
821
- return fp.result.err(new patch.PatchError(`Cannot add ${key} key to c.image: only metadata is allowed`));
822
- }
823
- return fp.result.ok([insertAt(document, node.arguments, node.arguments.length, toExpression(value))]);
824
- }));
825
- }
826
- } else if (isValRemoteMethodCall(node)) {
827
- if (key === core.FILE_REF_PROP) {
828
- if (typeof value !== "string") {
829
- return fp.result.err(new patch.PatchError(`Cannot add ${core.FILE_REF_PROP} key to c.remote with non-string value`));
830
- }
831
- return fp.pipe(findValRemoteNodeArg(node), fp.result.map(arg => replaceNodeValue(document, arg, value)));
832
- } else {
833
- return fp.pipe(findValRemoteMetadataArg(node), fp.result.flatMap(metadataArgNode => {
834
- if (metadataArgNode) {
835
- return fp.result.err(new patch.PatchError("Cannot add metadata to c.remote when it already exists"));
836
- }
837
- if (key !== "metadata") {
838
- return fp.result.err(new patch.PatchError(`Cannot add ${key} key to c.remote: only metadata is allowed`));
839
- }
840
- return fp.result.ok([insertAt(document, node.arguments, node.arguments.length, toExpression(value))]);
841
- }));
842
- }
843
516
  } else {
844
517
  return fp.result.err(shallowValidateExpression(node) ?? new patch.PatchError("Cannot add to non-object/array"));
845
518
  }
@@ -1421,7 +1094,10 @@ function rebaseContentOp(op, prefixLen) {
1421
1094
  from: op.from.slice(prefixLen)
1422
1095
  });
1423
1096
  case "file":
1424
- return fp.result.err(new patch.PatchError("Cannot apply a file op to a jsonValues entry"));
1097
+ // A file op carries bytes, not content. The binary goes through the
1098
+ // normal upload pipeline; what lands in the entry is the `patch_id` that
1099
+ // marks the bytes as not yet committed, which the caller writes.
1100
+ return fp.result.err(new patch.PatchError("A file op is not a content op: rebase it via its path"));
1425
1101
  }
1426
1102
  }
1427
1103
 
@@ -1455,9 +1131,6 @@ function applyJsonValuesEntryPatches(args) {
1455
1131
  } of patches) {
1456
1132
  let touched = false;
1457
1133
  for (const op of patch$1) {
1458
- if (op.op === "file") {
1459
- continue;
1460
- }
1461
1134
  const cls = serializedSchema ? classifyJsonValuesOp(serializedSchema, op.path) : {
1462
1135
  kind: "normal"
1463
1136
  };
@@ -1465,6 +1138,32 @@ function applyJsonValuesEntryPatches(args) {
1465
1138
  continue;
1466
1139
  }
1467
1140
  touched = true;
1141
+ // A file op does not edit the entry's content — its bytes go through the
1142
+ // upload pipeline. What the entry needs is the `patch_id` saying those
1143
+ // bytes are not committed yet, so `mediaUrl` serves them from the patch
1144
+ // directory instead of a `/public` path that holds nothing. Without this a
1145
+ // just-uploaded image inside an entry renders broken.
1146
+ if (op.op === "file") {
1147
+ if (op.value === null || content === undefined) {
1148
+ // A delete carries no bytes to point at, and there is nothing to mark
1149
+ // on an entry that does not exist.
1150
+ continue;
1151
+ }
1152
+ const applied = patch.applyPatch(patch.deepClone(content), jsonOps$1, [{
1153
+ op: "add",
1154
+ path: cls.subPath.concat(...(op.nestedFilePath ?? [])).concat("patch_id"),
1155
+ value: patchId
1156
+ }]);
1157
+ if (fp.result.isErr(applied)) {
1158
+ return {
1159
+ kind: "error",
1160
+ message: applied.error.message,
1161
+ patchId
1162
+ };
1163
+ }
1164
+ content = applied.value;
1165
+ continue;
1166
+ }
1468
1167
  if (cls.subPath.length === 0) {
1469
1168
  if (op.op === "add" || op.op === "replace") {
1470
1169
  content = op.value;
@@ -2437,6 +2136,17 @@ class ValOps {
2437
2136
  const serializedSchema = jsonValuesSchemaFor(path);
2438
2137
  for (const op of patchData.patch) {
2439
2138
  if (op.op === "file") {
2139
+ // A file op inside a `.jsonValues()` entry has nothing to inject
2140
+ // HERE: the entry is an opaque marker in the module source, so an
2141
+ // `add` reaching into it fails and poisons the rest of this
2142
+ // module's chain. `applyJsonValuesEntryPatches` writes the patch_id
2143
+ // into the entry's draft content instead.
2144
+ const fileCls = serializedSchema ? classifyJsonValuesOp(serializedSchema, op.path) : {
2145
+ kind: "normal"
2146
+ };
2147
+ if (fileCls.kind === "entry" && fileCls.subPath.length > 0) {
2148
+ continue;
2149
+ }
2440
2150
  if (op.value !== null) {
2441
2151
  // NOTE: We insert the last patch_id that modify a file
2442
2152
  // when constructing the url we use the patch id (and the file path)
@@ -2737,7 +2447,7 @@ class ValOps {
2737
2447
  };
2738
2448
  }
2739
2449
  const type = schemaAtPath instanceof core.ImageSchema ? "image" : "file";
2740
- const filePath = value[core.FILE_REF_PROP];
2450
+ const filePath = value.path;
2741
2451
  const fileData = (fileLastUpdatedByPatchId === null || fileLastUpdatedByPatchId === void 0 ? void 0 : fileLastUpdatedByPatchId[filePath]) || null;
2742
2452
  let metadata;
2743
2453
  let metadataErrors;
@@ -2779,23 +2489,14 @@ class ValOps {
2779
2489
  }]
2780
2490
  };
2781
2491
  }
2782
- const metadataSourcePath = core.Internal.createValPathOfItem(sourcePath, "metadata");
2783
- if (!metadataSourcePath) {
2784
- throw new Error("Could not create metadata path");
2785
- }
2786
- const currentValueMetadata = value.metadata;
2787
- if (!currentValueMetadata) {
2788
- return {
2789
- [metadataSourcePath]: [{
2790
- message: "Missing metadata field: 'metadata'",
2791
- value
2792
- }]
2793
- };
2794
- }
2492
+ // The fields Val computes from the bytes sit next to `path` now, so the
2493
+ // error is reported at the field it is about rather than at a `metadata`
2494
+ // object that no longer exists.
2495
+ const currentValueMetadata = value;
2795
2496
  const fieldErrors = {};
2796
2497
  for (const field of getFieldsForType(type)) {
2797
2498
  const fieldMetadata = metadata[field];
2798
- const fieldSourcePath = core.Internal.createValPathOfItem(metadataSourcePath, field);
2499
+ const fieldSourcePath = core.Internal.createValPathOfItem(sourcePath, field);
2799
2500
  if (!fieldSourcePath) {
2800
2501
  throw new Error("Could not create field path");
2801
2502
  }
@@ -3387,11 +3088,15 @@ function isOnlyFileCheckValidationError(validationError) {
3387
3088
  }
3388
3089
  return false;
3389
3090
  }
3091
+
3092
+ /**
3093
+ * Whether a flagged validation value is media.
3094
+ *
3095
+ * The caller already knows the schema said image or file — this only guards
3096
+ * against a value that never got that far.
3097
+ */
3390
3098
  function isFileSource(value) {
3391
- if (typeof value === "object" && value !== null && core.FILE_REF_PROP in value && core.VAL_EXTENSION in value && value[core.VAL_EXTENSION] === "file" && core.FILE_REF_PROP) {
3392
- return true;
3393
- }
3394
- return false;
3099
+ return typeof value === "object" && value !== null && "path" in value && typeof value.path === "string";
3395
3100
  }
3396
3101
  function formatPatchSourceError(error) {
3397
3102
  if ("message" in error) {
@@ -4735,7 +4440,7 @@ const FSPatchBase = z.z.object({
4735
4440
  timestamp: z.z.string().datetime()
4736
4441
  });
4737
4442
 
4738
- const textEncoder$1 = new TextEncoder();
4443
+ const textEncoder$2 = new TextEncoder();
4739
4444
  const PatchId = z.z.string().refine(s => !!s); // TODO: validate
4740
4445
  const CommitSha = z.z.string().refine(s => !!s); // TODO: validate
4741
4446
  z.z.string().refine(s => !!s); // TODO: validate
@@ -5426,7 +5131,7 @@ class ValOpsHttp extends ValOps {
5426
5131
  });
5427
5132
  params.set("body_sha",
5428
5133
  // We use this for cache invalidation
5429
- core.Internal.getSHA256Hash(textEncoder$1.encode(stringifiedFiles)));
5134
+ core.Internal.getSHA256Hash(textEncoder$2.encode(stringifiedFiles)));
5430
5135
  return fetch(`${this.contentUrl}/v1/${this.project}/files?${params}`, {
5431
5136
  method: "PUT",
5432
5137
  // Yes, PUT is weird. Weirder to have a body in a GET request.
@@ -9154,15 +8859,21 @@ _input // TODO: use buffer to determine mimetype
9154
8859
  };
9155
8860
  }
9156
8861
 
8862
+ /**
8863
+ * The media path a validation error is about, if it is about media at all.
8864
+ *
8865
+ * There is no schema in hand here — a `ValidationError` carries only the value
8866
+ * it flagged — so the shape is all there is to go on.
8867
+ */
9157
8868
  function getValidationErrorFileRef(validationError) {
9158
- const maybeRef = validationError.value && typeof validationError.value === "object" && core.FILE_REF_PROP in validationError.value && typeof validationError.value[core.FILE_REF_PROP] === "string" ? validationError.value[core.FILE_REF_PROP] : undefined;
9159
- if (!maybeRef) {
8869
+ const maybePath = validationError.value && typeof validationError.value === "object" && "path" in validationError.value && typeof validationError.value.path === "string" ? validationError.value.path : undefined;
8870
+ if (!maybePath) {
9160
8871
  return null;
9161
8872
  }
9162
- return maybeRef;
8873
+ return maybePath;
9163
8874
  }
9164
8875
 
9165
- const textEncoder = new TextEncoder();
8876
+ const textEncoder$1 = new TextEncoder();
9166
8877
  async function checkRemoteRef(remoteHost, ref, projectRoot, schema, metadata) {
9167
8878
  const remoteRefDataRes = core.Internal.remote.splitRemoteRef(ref);
9168
8879
  if (remoteRefDataRes.status === "error") {
@@ -9192,10 +8903,10 @@ async function checkRemoteRef(remoteHost, ref, projectRoot, schema, metadata) {
9192
8903
  const fileExt = getFileExt(relativeFilePath);
9193
8904
  const currentValidationHash = core.Internal.remote.getValidationHash(coreVersion, schema, fileExt, metadata,
9194
8905
  // TODO: validate
9195
- fileHash, textEncoder);
8906
+ fileHash, textEncoder$1);
9196
8907
  const validationHashFromRemoteRefValues = core.Internal.remote.getValidationHash(remoteRefDataRes.version, schema, getFileExt(remoteRefDataRes.filePath), metadata,
9197
8908
  // TODO: validate
9198
- fileHash, textEncoder);
8909
+ fileHash, textEncoder$1);
9199
8910
  if (
9200
8911
  // Current validation hash is the same as the remote validation hash
9201
8912
  // We assume the uploaded file exists and is correct
@@ -9241,7 +8952,7 @@ async function checkRemoteRef(remoteHost, ref, projectRoot, schema, metadata) {
9241
8952
  ...metadata,
9242
8953
  ...currentMetadata
9243
8954
  };
9244
- const nextValidationHash = core.Internal.remote.getValidationHash(coreVersion, schema, fileExt, updatedMetadata, fileBufferRes.fileHash, textEncoder);
8955
+ const nextValidationHash = core.Internal.remote.getValidationHash(coreVersion, schema, fileExt, updatedMetadata, fileBufferRes.fileHash, textEncoder$1);
9245
8956
  if (!updatedMetadata.mimeType) {
9246
8957
  return {
9247
8958
  status: "error",
@@ -9371,6 +9082,36 @@ function parentPatchPathOfMediaEntry(sourcePath, entryKey) {
9371
9082
  }
9372
9083
 
9373
9084
  // TODO: find a better name? transformFixesToPatch?
9085
+ /**
9086
+ * A whole media value, for the fixes that replace one outright.
9087
+ *
9088
+ * The three remote fixes each wrote this shape out by hand, and one of them was
9089
+ * left behind when the shape changed. One name, so the next change cannot miss a
9090
+ * site.
9091
+ *
9092
+ * `path` last is deliberate: it is the field the fix is about, and a stray
9093
+ * `path` inside the metadata must not win over it.
9094
+ */
9095
+ function mediaValue(path, metadata) {
9096
+ const fields = metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata : {};
9097
+ return {
9098
+ ...fields,
9099
+ path
9100
+ };
9101
+ }
9102
+
9103
+ /**
9104
+ * The media value a validation error flagged, read as a plain record.
9105
+ *
9106
+ * A `ValidationError` carries only a value, never the schema that rejected it,
9107
+ * so the shape is all there is to go on here.
9108
+ */
9109
+ function mediaValueOf(value) {
9110
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
9111
+ return undefined;
9112
+ }
9113
+ return value;
9114
+ }
9374
9115
  async function createFixPatch(config, apply, sourcePath, validationError, remoteFiles, moduleSource, moduleSchema) {
9375
9116
  const remainingErrors = [];
9376
9117
  const patch$1 = [];
@@ -9384,57 +9125,38 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
9384
9125
  fixes: undefined
9385
9126
  });
9386
9127
  } else if (fix === "image:check-metadata") {
9387
- const currentValue = validationError.value;
9388
- const metadataIsCorrect =
9389
- // metadata is a prop that is an object
9390
- typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
9391
- // width is correct
9392
- "width" in currentValue.metadata && currentValue.metadata.width === imageMetadata.width &&
9393
- // height is correct
9394
- "height" in currentValue.metadata && currentValue.metadata.height === imageMetadata.height &&
9395
- // mimeType is correct
9396
- "mimeType" in currentValue.metadata && currentValue.metadata.mimeType === imageMetadata.mimeType;
9397
- // skips if the metadata is already correct
9398
- if (!metadataIsCorrect) {
9128
+ const currentValue = mediaValueOf(validationError.value);
9129
+ const derived = {
9130
+ width: imageMetadata.width,
9131
+ height: imageMetadata.height,
9132
+ mimeType: imageMetadata.mimeType
9133
+ };
9134
+ // One op per field, not one for the whole object: `alt` and `hotspot`
9135
+ // are authored and sit next to these, so a whole-object write would
9136
+ // throw away what a person typed.
9137
+ const wrong = Object.entries(derived).filter(([field, expected]) => (currentValue === null || currentValue === void 0 ? void 0 : currentValue[field]) !== expected);
9138
+ if (wrong.length > 0) {
9399
9139
  if (apply) {
9400
- patch$1.push({
9401
- op: "replace",
9402
- path: patch.sourceToPatchPath(sourcePath).concat("metadata"),
9403
- value: {
9404
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9405
- ...currentValue.metadata,
9406
- width: imageMetadata.width,
9407
- height: imageMetadata.height,
9408
- mimeType: imageMetadata.mimeType
9409
- }
9410
- });
9411
- } else {
9412
- if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
9413
- if (!("width" in currentValue.metadata) || currentValue.metadata.width !== imageMetadata.width) {
9414
- remainingErrors.push({
9415
- message: "Image metadata width is incorrect! Found: " + ("width" in currentValue.metadata ? currentValue.metadata.width : "<empty>") + ". Expected: " + imageMetadata.width,
9416
- fixes: undefined
9417
- });
9418
- }
9419
- if (!("height" in currentValue.metadata) || currentValue.metadata.height !== imageMetadata.height) {
9420
- remainingErrors.push({
9421
- message: "Image metadata height is incorrect! Found: " + ("height" in currentValue.metadata ? currentValue.metadata.height : "<empty>") + ". Expected: " + imageMetadata.height,
9422
- fixes: undefined
9423
- });
9424
- }
9425
- if (!("mimeType" in currentValue.metadata) || currentValue.metadata.mimeType !== imageMetadata.mimeType) {
9426
- remainingErrors.push({
9427
- message: "Image metadata mimeType is incorrect! Found: " + ("mimeType" in currentValue.metadata ? currentValue.metadata.mimeType : "<empty>") + ". Expected: " + imageMetadata.mimeType,
9428
- fixes: undefined
9429
- });
9430
- }
9431
- } else {
9140
+ for (const [field, expected] of wrong) {
9141
+ patch$1.push({
9142
+ op: "add",
9143
+ path: patch.sourceToPatchPath(sourcePath).concat(field),
9144
+ value: expected
9145
+ });
9146
+ }
9147
+ } else if (currentValue) {
9148
+ for (const [field, expected] of wrong) {
9432
9149
  remainingErrors.push({
9433
- ...validationError,
9434
- message: "Image metadata is not an object!",
9150
+ message: `Image ${field} is incorrect! Found: ` + (currentValue[field] ?? "<empty>") + ". Expected: " + expected,
9435
9151
  fixes: undefined
9436
9152
  });
9437
9153
  }
9154
+ } else {
9155
+ remainingErrors.push({
9156
+ ...validationError,
9157
+ message: "Image is not an object!",
9158
+ fixes: undefined
9159
+ });
9438
9160
  }
9439
9161
  }
9440
9162
  } else if (fix === "image:add-metadata") {
@@ -9445,14 +9167,19 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
9445
9167
  fixes: undefined
9446
9168
  });
9447
9169
  } else {
9170
+ const patchPath = patch.sourceToPatchPath(sourcePath);
9448
9171
  patch$1.push({
9449
9172
  op: "add",
9450
- path: patch.sourceToPatchPath(sourcePath).concat("metadata"),
9451
- value: {
9452
- width: imageMetadata.width,
9453
- height: imageMetadata.height,
9454
- mimeType: imageMetadata.mimeType
9455
- }
9173
+ path: patchPath.concat("width"),
9174
+ value: imageMetadata.width
9175
+ }, {
9176
+ op: "add",
9177
+ path: patchPath.concat("height"),
9178
+ value: imageMetadata.height
9179
+ }, {
9180
+ op: "add",
9181
+ path: patchPath.concat("mimeType"),
9182
+ value: imageMetadata.mimeType
9456
9183
  });
9457
9184
  }
9458
9185
  }
@@ -9465,53 +9192,32 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
9465
9192
  fixes: undefined
9466
9193
  });
9467
9194
  } else if (fix === "file:check-metadata") {
9468
- const currentValue = validationError.value;
9469
- const metadataIsCorrect =
9470
- // metadata is a prop that is an object
9471
- typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
9472
- // mimeType is correct
9473
- "mimeType" in currentValue.metadata && currentValue.metadata.mimeType === fileMetadata.mimeType;
9474
-
9475
- // skips if the metadata is already correct
9476
- if (!metadataIsCorrect) {
9195
+ const currentValue = mediaValueOf(validationError.value);
9196
+ if ((currentValue === null || currentValue === void 0 ? void 0 : currentValue.mimeType) !== fileMetadata.mimeType) {
9477
9197
  if (apply) {
9478
9198
  patch$1.push({
9479
- op: "replace",
9480
- path: patch.sourceToPatchPath(sourcePath).concat("metadata"),
9481
- value: {
9482
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9483
- ...currentValue.metadata,
9484
- ...(fileMetadata.mimeType ? {
9485
- mimeType: fileMetadata.mimeType
9486
- } : {})
9487
- }
9199
+ op: "add",
9200
+ path: patch.sourceToPatchPath(sourcePath).concat("mimeType"),
9201
+ value: fileMetadata.mimeType ?? null
9202
+ });
9203
+ } else if (currentValue) {
9204
+ remainingErrors.push({
9205
+ message: "File mimeType is incorrect! Found: " + (currentValue.mimeType ?? "<empty>") + ". Expected: " + fileMetadata.mimeType,
9206
+ fixes: undefined
9488
9207
  });
9489
9208
  } else {
9490
- if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
9491
- if (!("mimeType" in currentValue.metadata) || currentValue.metadata.mimeType !== fileMetadata.mimeType) {
9492
- remainingErrors.push({
9493
- message: "File metadata mimeType is incorrect! Found: " + ("mimeType" in currentValue.metadata ? currentValue.metadata.mimeType : "<empty>") + ". Expected: " + fileMetadata.mimeType,
9494
- fixes: undefined
9495
- });
9496
- }
9497
- } else {
9498
- remainingErrors.push({
9499
- ...validationError,
9500
- message: "Image metadata is not an object!",
9501
- fixes: undefined
9502
- });
9503
- }
9209
+ remainingErrors.push({
9210
+ ...validationError,
9211
+ message: "File is not an object!",
9212
+ fixes: undefined
9213
+ });
9504
9214
  }
9505
9215
  }
9506
9216
  } else if (fix === "file:add-metadata") {
9507
9217
  patch$1.push({
9508
9218
  op: "add",
9509
- path: patch.sourceToPatchPath(sourcePath).concat("metadata"),
9510
- value: {
9511
- ...(fileMetadata.mimeType ? {
9512
- mimeType: fileMetadata.mimeType
9513
- } : {})
9514
- }
9219
+ path: patch.sourceToPatchPath(sourcePath).concat("mimeType"),
9220
+ value: fileMetadata.mimeType ?? null
9515
9221
  });
9516
9222
  }
9517
9223
  } else if (fix === "image:upload-remote" || fix === "file:upload-remote") {
@@ -9539,11 +9245,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
9539
9245
  } else {
9540
9246
  patch$1.push({
9541
9247
  op: "replace",
9542
- value: {
9543
- _type: "remote",
9544
- _ref: remoteFile.ref,
9545
- metadata
9546
- },
9248
+ value: mediaValue(remoteFile.ref, metadata),
9547
9249
  path: patch.sourceToPatchPath(sourcePath)
9548
9250
  });
9549
9251
  }
@@ -9612,7 +9314,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
9612
9314
  });
9613
9315
  continue;
9614
9316
  }
9615
- const splitRemoteRefDataRes = core.Internal.remote.splitRemoteRef(v._ref);
9317
+ const splitRemoteRefDataRes = core.Internal.remote.splitRemoteRef(v.path);
9616
9318
  if (splitRemoteRefDataRes.status === "error") {
9617
9319
  remainingErrors.push({
9618
9320
  ...validationError,
@@ -9621,7 +9323,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
9621
9323
  });
9622
9324
  continue;
9623
9325
  }
9624
- const url = v._ref;
9326
+ const url = v.path;
9625
9327
  const filePath = splitRemoteRefDataRes.filePath;
9626
9328
  if (!filePath) {
9627
9329
  remainingErrors.push({
@@ -9652,20 +9354,10 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
9652
9354
  });
9653
9355
  continue;
9654
9356
  }
9655
- const value = {
9656
- [core.VAL_EXTENSION]: "file",
9657
- [core.FILE_REF_PROP]: `/${filePath}`,
9658
- ...(fix === "image:download-remote" ? {
9659
- [core.FILE_REF_SUBTYPE_TAG]: "image"
9660
- } : {})
9661
- };
9662
9357
  patch$1.push({
9663
9358
  op: "replace",
9664
9359
  path: patch.sourceToPatchPath(sourcePath),
9665
- value: v.metadata ? {
9666
- ...value,
9667
- metadata: v.metadata
9668
- } : value
9360
+ value: mediaValue(`/${filePath}`, v.metadata)
9669
9361
  });
9670
9362
  } else if (fix === "images:check-all-files" || fix === "files:check-all-files") {
9671
9363
  if (!moduleSource || typeof moduleSource !== "object") {
@@ -9788,7 +9480,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
9788
9480
  schema: schemaAtPath
9789
9481
  } = core.Internal.resolvePath(modulePath, moduleSource, moduleSchema);
9790
9482
  if (schemaAtPath.type === "image" || schemaAtPath.type === "file") {
9791
- const res = await checkRemoteRef(config.remoteHost, v._ref, config.projectRoot, schemaAtPath, v.metadata);
9483
+ const res = await checkRemoteRef(config.remoteHost, v.path, config.projectRoot, schemaAtPath, v.metadata);
9792
9484
  if (res.status === "success") ; else if (res.status === "error") {
9793
9485
  remainingErrors.push({
9794
9486
  ...validationError,
@@ -9800,11 +9492,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
9800
9492
  patch$1.push({
9801
9493
  op: "replace",
9802
9494
  path: patch.sourceToPatchPath(sourcePath),
9803
- value: {
9804
- _type: "remote",
9805
- _ref: res.ref,
9806
- metadata: res.metadata
9807
- }
9495
+ value: mediaValue(res.ref, res.metadata)
9808
9496
  });
9809
9497
  } else {
9810
9498
  remainingErrors.push({
@@ -9851,37 +9539,26 @@ function getRemoteValueFromValidationError(v) {
9851
9539
  message: "Unexpected error while checking remote (no value)"
9852
9540
  };
9853
9541
  }
9854
- if (typeof v.value !== "object" || v.value === null || !(core.FILE_REF_PROP in v.value)) {
9542
+ if (typeof v.value !== "object" || v.value === null || !("path" in v.value)) {
9855
9543
  return {
9856
9544
  success: false,
9857
- message: "Unexpected error while checking remote (no _ref in value)"
9545
+ message: "Unexpected error while checking remote (no path in value)"
9858
9546
  };
9859
9547
  }
9860
- if (typeof v.value._ref !== "string") {
9548
+ if (typeof v.value.path !== "string") {
9861
9549
  return {
9862
9550
  success: false,
9863
- message: "Unexpected error while checking remote (_ref is not a string)"
9551
+ message: "Unexpected error while checking remote (path is not a string)"
9864
9552
  };
9865
9553
  }
9866
- let metadata;
9867
- if ("metadata" in v.value && typeof v.value.metadata === "object") {
9868
- if (v.value.metadata === null) {
9869
- return {
9870
- success: false,
9871
- message: "Unexpected error while checking remote (metadata is null)"
9872
- };
9873
- }
9874
- if (Array.isArray(v.value.metadata)) {
9875
- return {
9876
- success: false,
9877
- message: "Unexpected error while checking remote (metadata is an array)"
9878
- };
9879
- }
9880
- metadata = v.value.metadata;
9881
- }
9554
+ // Everything but the path is what Val read from the bytes.
9555
+ const {
9556
+ path,
9557
+ ...metadata
9558
+ } = v.value;
9882
9559
  return {
9883
9560
  success: true,
9884
- _ref: v.value._ref,
9561
+ path,
9885
9562
  metadata
9886
9563
  };
9887
9564
  }
@@ -9906,29 +9583,655 @@ async function getFileMetadata(projectRoot, validationError) {
9906
9583
  return extractFileMetadata(fileRef);
9907
9584
  }
9908
9585
 
9909
- function validateMetadata(actualMetadata, expectedMetadata) {
9910
- const missingMetadata = [];
9911
- const erroneousMetadata = {};
9912
- if (typeof actualMetadata !== "object" || actualMetadata === null) {
9913
- return {
9914
- globalErrors: ["Metadata is wrong type: must be an object."]
9915
- };
9586
+ /**
9587
+ * Moves ONE `.jsonValues()` entry that was written inline in the `.val.ts` into
9588
+ * its own `*.val.json`, replacing the inline value with
9589
+ * `c.json(() => import("./<key>.val.json"))`.
9590
+ *
9591
+ * This is the fix for the `jsonValues:extract-entry` validation error. It is not
9592
+ * expressible as a patch (a patch edits one `.val.ts` and cannot create the
9593
+ * backing JSON file), so it writes both files directly — JSON first, so a
9594
+ * failure part-way through never leaves the module pointing at a file that does
9595
+ * not exist.
9596
+ *
9597
+ * Root-only, like the rest of the `.jsonValues()` machinery: the entry is looked
9598
+ * up in the module's root record/router object literal.
9599
+ */
9600
+ function extractJsonValuesEntry(moduleFilePath, rootDir, entryKey, content, sourceFileHandler) {
9601
+ const valTsPath = sourceFileHandler.resolveSourceModulePath(getSyntheticContainingPath(rootDir), `.${moduleFilePath.replace(".val.ts", ".val").replace(".val.js", ".val").replace(".val.jsx", ".val").replace(".val.tsx", ".val")}`);
9602
+ const sourceFile = sourceFileHandler.getSourceFile(valTsPath);
9603
+ if (!sourceFile) {
9604
+ throw Error(`Source file ${valTsPath} not found`);
9916
9605
  }
9917
- if (Array.isArray(actualMetadata)) {
9918
- return {
9919
- globalErrors: ["Metadata is wrong type: cannot be an array."]
9920
- };
9606
+ const pathsRes = getNewJsonEntryPaths(moduleFilePath, entryKey);
9607
+ if (fp.result.isErr(pathsRes)) {
9608
+ throw Error(formatPatchSourceError(pathsRes.error));
9921
9609
  }
9922
- const recordMetadata = actualMetadata;
9923
- const globalErrors = [];
9924
- for (const anyKey in expectedMetadata) {
9925
- if (typeof anyKey !== "string") {
9926
- globalErrors.push(`Expected metadata has key '${anyKey}' that is not typeof 'string', but: '${typeof anyKey}'. This is most likely a Val bug.`);
9927
- } else {
9928
- if (anyKey in actualMetadata) {
9929
- const key = anyKey;
9930
- if (expectedMetadata[key] !== recordMetadata[key]) {
9931
- erroneousMetadata[key] = `Expected metadata '${key}' to be ${JSON.stringify(expectedMetadata[key])}, but got ${JSON.stringify(recordMetadata[key])}.`;
9610
+ const {
9611
+ jsonPath,
9612
+ importPath
9613
+ } = pathsRes.value;
9614
+ const absoluteJsonPath = path__namespace["default"].join(rootDir, jsonPath);
9615
+ if (sourceFileHandler.host.fileExists(absoluteJsonPath)) {
9616
+ throw Error(`Cannot extract .jsonValues() entry '${entryKey}' of ${moduleFilePath}: '${jsonPath}' already exists`);
9617
+ }
9618
+
9619
+ // Remove the inline property, then add the `c.json(...)` reference back. The
9620
+ // entry moves to the end of the record: entry ORDER in a jsonValues record is
9621
+ // not meaningful (the Studio and the runtime key entries by name), and
9622
+ // insert-in-place would mean reimplementing insertValJsonEntry.
9623
+ const removed = removeValJsonEntry(sourceFile, [], entryKey);
9624
+ if (fp.result.isErr(removed)) {
9625
+ throw Error(`${valTsPath}\n${formatOpsError(removed.error, sourceFile)}`);
9626
+ }
9627
+ const inserted = insertValJsonEntry(removed.value, [], entryKey, importPath);
9628
+ if (fp.result.isErr(inserted)) {
9629
+ throw Error(`${valTsPath}\n${formatOpsError(inserted.error, removed.value)}`);
9630
+ }
9631
+ sourceFileHandler.writeFile(absoluteJsonPath, JSON.stringify(content, null, 2) + "\n", "utf8");
9632
+ sourceFileHandler.writeSourceFile(inserted.value);
9633
+ }
9634
+ function formatOpsError(error, sourceFile) {
9635
+ if (error instanceof patch.PatchError) {
9636
+ return error.message;
9637
+ }
9638
+ return flatMapErrors(error, e => formatSyntaxError(e, sourceFile)).join("\n");
9639
+ }
9640
+
9641
+ /**
9642
+ * The fix handlers behind `val validate --fix`, and the editor quick fixes that
9643
+ * must agree with it.
9644
+ *
9645
+ * ## Why this lives in `@valbuild/server`
9646
+ *
9647
+ * A fix is two layers. `createFixPatch` is the second: given a validation
9648
+ * error it produces a patch. The first is everything that has to happen before a
9649
+ * patch can be produced — read the file, check it is actually on disk, extract
9650
+ * metadata, pick a remote bucket, upload the bytes, download them back. That
9651
+ * layer used to live in `packages/cli/src/runValidation.ts`, which made it
9652
+ * reachable only from the CLI: `@valbuild/cli` exports nothing but `./cli`, and
9653
+ * `@valbuild/language-server` cannot depend on it anyway without creating a
9654
+ * cycle (the CLI depends on the language server).
9655
+ *
9656
+ * The consequence was that an editor could offer the fixes needing no
9657
+ * precondition and had to reimplement or skip the rest. Both happened: the
9658
+ * VS Code extension grew its own remote-upload client, and the language server
9659
+ * declined to offer remote fixes at all.
9660
+ *
9661
+ * So the layer sits here, next to `createFixPatch`, and the CLI and the language
9662
+ * server are both callers. {@link FixHandlerContext} is deliberately made of
9663
+ * things any caller already has — a `Service`, an `IValFSHost`, a project root —
9664
+ * rather than of CLI concepts.
9665
+ *
9666
+ * What stays in the CLI is the driver: the `runValidation` async generator that
9667
+ * walks every module, decides what to report, and renders it to a terminal.
9668
+ */
9669
+ const textEncoder = new TextEncoder();
9670
+
9671
+ // Types for handler system
9672
+
9673
+ // Handler functions
9674
+ async function handleFileMetadata(ctx) {
9675
+ var _fileSource$source;
9676
+ const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
9677
+ if (!ctx.valModule.source || !ctx.valModule.schema) {
9678
+ return {
9679
+ success: false,
9680
+ errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
9681
+ };
9682
+ }
9683
+ const fileSource = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
9684
+
9685
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9686
+ const fileRefProp = (_fileSource$source = fileSource.source) === null || _fileSource$source === void 0 ? void 0 : _fileSource$source.path;
9687
+ if (!fileRefProp) {
9688
+ return {
9689
+ success: false,
9690
+ errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
9691
+ };
9692
+ }
9693
+ const filePath = path__namespace["default"].join(ctx.projectRoot, fileRefProp);
9694
+ if (!ctx.fs.fileExists(filePath)) {
9695
+ return {
9696
+ success: false,
9697
+ errorMessage: `File ${filePath} does not exist`
9698
+ };
9699
+ }
9700
+ return {
9701
+ success: true,
9702
+ shouldApplyPatch: true
9703
+ };
9704
+ }
9705
+
9706
+ // Shared upload core used by both the single-field (handleRemoteFileUpload)
9707
+ // and gallery (handleRemoteGalleryFileUpload) handlers. The two differ only in
9708
+ // how they derive the local file ref, metadata and serialized image/file
9709
+ // schema; everything from auth through upload is identical.
9710
+ async function uploadRemoteFileCore(ctx, fileRef, metadata, schema) {
9711
+ const filePath = path__namespace["default"].join(ctx.projectRoot, fileRef);
9712
+ if (!ctx.fs.fileExists(filePath)) {
9713
+ return {
9714
+ success: false,
9715
+ errorMessage: `File ${filePath} does not exist`
9716
+ };
9717
+ }
9718
+ const patFile = getPersonalAccessTokenPath(ctx.projectRoot);
9719
+ if (!ctx.fs.fileExists(patFile)) {
9720
+ return {
9721
+ success: false,
9722
+ errorMessage: `File: ${path__namespace["default"].join(ctx.projectRoot, ctx.file)} has remote images that are not uploaded and you are not logged in.\n\nFix this error by logging in:\n\t"npx val login"\n`
9723
+ };
9724
+ }
9725
+ const patFileContent = ctx.fs.readFile(patFile);
9726
+ if (patFileContent === undefined) {
9727
+ return {
9728
+ success: false,
9729
+ errorMessage: `Could not read personal access token file at ${patFile}`
9730
+ };
9731
+ }
9732
+ const parsedPatFile = parsePersonalAccessTokenFile(patFileContent);
9733
+ if (!parsedPatFile.success) {
9734
+ return {
9735
+ success: false,
9736
+ errorMessage: `Error parsing personal access token file: ${parsedPatFile.error}. You need to login again.`
9737
+ };
9738
+ }
9739
+ const {
9740
+ pat
9741
+ } = parsedPatFile.data;
9742
+ if (ctx.remoteFiles[ctx.sourcePath]) {
9743
+ return {
9744
+ success: true,
9745
+ events: [{
9746
+ type: "remote-already-uploaded",
9747
+ filePath
9748
+ }]
9749
+ };
9750
+ }
9751
+ const projectName = ctx.project;
9752
+ let publicProjectId = ctx.publicProjectId;
9753
+ let remoteFileBuckets = ctx.remoteFileBuckets;
9754
+ let remoteFilesCounter = ctx.remoteFilesCounter;
9755
+ if (!publicProjectId || !remoteFileBuckets) {
9756
+ if (!projectName) {
9757
+ return {
9758
+ success: false,
9759
+ errorMessage: "Project name not found. Add project name to val.config or set the VAL_PROJECT environment variable"
9760
+ };
9761
+ }
9762
+ const settingsRes = await ctx.remote.getSettings(projectName, {
9763
+ pat
9764
+ });
9765
+ if (!settingsRes.success) {
9766
+ return {
9767
+ success: false,
9768
+ errorMessage: `Could not get public project id: ${settingsRes.message}.`
9769
+ };
9770
+ }
9771
+ publicProjectId = settingsRes.data.publicProjectId;
9772
+ remoteFileBuckets = settingsRes.data.remoteFileBuckets.map(b => b.bucket);
9773
+ }
9774
+ if (!publicProjectId) {
9775
+ return {
9776
+ success: false,
9777
+ errorMessage: "Could not get public project id"
9778
+ };
9779
+ }
9780
+ if (!projectName) {
9781
+ return {
9782
+ success: false,
9783
+ errorMessage: `Could not get project. Check that your val.config has the 'project' field set, or set it using the VAL_PROJECT environment variable`
9784
+ };
9785
+ }
9786
+ remoteFilesCounter += 1;
9787
+ const bucket = remoteFileBuckets[remoteFilesCounter % remoteFileBuckets.length];
9788
+ if (!bucket) {
9789
+ return {
9790
+ success: false,
9791
+ errorMessage: `Internal error: could not allocate a bucket for the remote file located at ${ctx.sourcePath}`
9792
+ };
9793
+ }
9794
+ const fileBuffer = ctx.fs.readBuffer(filePath);
9795
+ if (fileBuffer === undefined) {
9796
+ return {
9797
+ success: false,
9798
+ errorMessage: `Error reading file: ${filePath}`
9799
+ };
9800
+ }
9801
+ const relativeFilePath = path__namespace["default"].relative(ctx.projectRoot, filePath).split(path__namespace["default"].sep).join("/");
9802
+ if (!relativeFilePath.startsWith("public/")) {
9803
+ return {
9804
+ success: false,
9805
+ errorMessage: `File path must be within the public/ directory (e.g. public/path/to/file.txt). Got: ${relativeFilePath}`
9806
+ };
9807
+ }
9808
+ const fileHash = core.Internal.remote.getFileHash(fileBuffer);
9809
+ const coreVersion = core.Internal.VERSION.core || "unknown";
9810
+ const fileExt = getFileExt(filePath);
9811
+ const ref = core.Internal.remote.createRemoteRef(ctx.remote.remoteHost, {
9812
+ publicProjectId,
9813
+ coreVersion,
9814
+ bucket,
9815
+ validationHash: core.Internal.remote.getValidationHash(coreVersion, schema, fileExt, metadata, fileHash, textEncoder),
9816
+ fileHash,
9817
+ filePath: relativeFilePath
9818
+ });
9819
+ const remoteFileUpload = await ctx.remote.uploadFile(projectName, bucket, fileHash, fileExt, fileBuffer, {
9820
+ pat
9821
+ });
9822
+ if (!remoteFileUpload.success) {
9823
+ return {
9824
+ success: false,
9825
+ errorMessage: `Could not upload remote file: '${ref}'. Error: ${remoteFileUpload.error}`
9826
+ };
9827
+ }
9828
+ ctx.remoteFiles[ctx.sourcePath] = {
9829
+ ref,
9830
+ metadata
9831
+ };
9832
+ return {
9833
+ success: true,
9834
+ shouldApplyPatch: true,
9835
+ publicProjectId,
9836
+ remoteFileBuckets,
9837
+ remoteFilesCounter,
9838
+ events: [{
9839
+ type: "remote-uploading",
9840
+ ref
9841
+ }, {
9842
+ type: "remote-uploaded",
9843
+ ref
9844
+ }]
9845
+ };
9846
+ }
9847
+ async function handleRemoteFileUpload(ctx) {
9848
+ var _resolvedRemoteFileAt;
9849
+ if (!ctx.fix) {
9850
+ return {
9851
+ success: false,
9852
+ // No sourcePath in the message: the reported location already points at it.
9853
+ errorMessage: `Remote file needs to be uploaded (use --fix to upload)`
9854
+ };
9855
+ }
9856
+ const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
9857
+ if (!ctx.valModule.source || !ctx.valModule.schema) {
9858
+ return {
9859
+ success: false,
9860
+ errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
9861
+ };
9862
+ }
9863
+ const resolvedRemoteFileAtSourcePath = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
9864
+
9865
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9866
+ const fileRefProp = (_resolvedRemoteFileAt = resolvedRemoteFileAtSourcePath.source) === null || _resolvedRemoteFileAt === void 0 ? void 0 : _resolvedRemoteFileAt.path;
9867
+ if (!fileRefProp) {
9868
+ return {
9869
+ success: false,
9870
+ errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
9871
+ };
9872
+ }
9873
+ const resolveRemoteFileSchema = resolvedRemoteFileAtSourcePath.schema;
9874
+ if (!resolveRemoteFileSchema) {
9875
+ return {
9876
+ success: false,
9877
+ errorMessage: `Cannot upload remote file: schema not found for ${ctx.sourcePath}`
9878
+ };
9879
+ }
9880
+ if (resolveRemoteFileSchema.type !== "image" && resolveRemoteFileSchema.type !== "file") {
9881
+ return {
9882
+ success: false,
9883
+ errorMessage: `The schema is the remote is neither image nor file: ${ctx.sourcePath}`
9884
+ };
9885
+ }
9886
+
9887
+ // The metadata is everything the media source carries besides its path: the
9888
+ // schema above already established that this IS media.
9889
+ const actualRemoteFileSource = resolvedRemoteFileAtSourcePath.source;
9890
+ const fileSourceMetadata = actualRemoteFileSource && typeof actualRemoteFileSource === "object" && "path" in actualRemoteFileSource ? (() => {
9891
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
9892
+ const {
9893
+ path: _path,
9894
+ ...metadata
9895
+ } = actualRemoteFileSource;
9896
+ return metadata;
9897
+ })() : undefined;
9898
+ return uploadRemoteFileCore(ctx, fileRefProp, fileSourceMetadata, resolveRemoteFileSchema);
9899
+ }
9900
+
9901
+ // Gallery (s.images({ remote: true }) / s.files({ remote: true })) upload.
9902
+ // Unlike a single image/file field, a gallery entry is keyed by its local file
9903
+ // path and the value is bare metadata (no FileSource), so we derive the file
9904
+ // ref from the key and synthesize the image/file schema from the record's
9905
+ // media options.
9906
+ async function handleRemoteGalleryFileUpload(ctx) {
9907
+ var _ctx$validationError$;
9908
+ if (!ctx.fix) {
9909
+ return {
9910
+ success: false,
9911
+ // No sourcePath in the message: the reported location already points at it.
9912
+ errorMessage: `Remote file needs to be uploaded (use --fix to upload)`
9913
+ };
9914
+ }
9915
+ const fix = (_ctx$validationError$ = ctx.validationError.fixes) === null || _ctx$validationError$ === void 0 ? void 0 : _ctx$validationError$[0];
9916
+ const mediaType = fix === "files:upload-remote" ? "file" : "image";
9917
+
9918
+ // The gallery entry is keyed by its local file path; validateMediaKey carries
9919
+ // that key as the error value.
9920
+ const fileRef = ctx.validationError.value;
9921
+ if (typeof fileRef !== "string") {
9922
+ return {
9923
+ success: false,
9924
+ errorMessage: `Expected a local file path for gallery entry at ${ctx.sourcePath}`
9925
+ };
9926
+ }
9927
+ const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
9928
+ if (!ctx.valModule.source || !ctx.valModule.schema) {
9929
+ return {
9930
+ success: false,
9931
+ errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
9932
+ };
9933
+ }
9934
+ const resolved = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
9935
+ const entrySource = resolved.source;
9936
+ const metadata = entrySource && typeof entrySource === "object" && !Array.isArray(entrySource) ? entrySource : undefined;
9937
+
9938
+ // The gallery item schema is an ObjectSchema, not an image/file schema, so
9939
+ // synthesize the serialized image/file schema (matching how single fields
9940
+ // serialize) for the remote ref's validation hash.
9941
+ //
9942
+ // `accept`/`directory` come from the RECORD that holds the entry, resolved
9943
+ // from the entry's PARENT path - not from the module root, which is only the
9944
+ // record when the gallery is the whole module. A nested gallery
9945
+ // (s.object({ gallery: s.images(...) })) would otherwise synthesize a schema
9946
+ // with no options and bake a validation hash into the remote ref that can
9947
+ // never validate, so a mismatch fails fast instead of uploading.
9948
+ const [, parentModulePath] = core.Internal.splitModuleFilePathAndModulePath(core.Internal.parentOfSourcePath(ctx.sourcePath));
9949
+ const recordSchema = core.Internal.resolvePath(parentModulePath, ctx.valModule.source, ctx.valModule.schema).schema;
9950
+ if ((recordSchema === null || recordSchema === void 0 ? void 0 : recordSchema.type) !== "record") {
9951
+ return {
9952
+ success: false,
9953
+ errorMessage: `Expected a gallery record at the parent of ${ctx.sourcePath}, got ${(recordSchema === null || recordSchema === void 0 ? void 0 : recordSchema.type) ?? "nothing"}`
9954
+ };
9955
+ }
9956
+ const {
9957
+ accept,
9958
+ directory
9959
+ } = recordSchema;
9960
+ const schema = mediaType === "image" ? {
9961
+ type: "image",
9962
+ opt: false,
9963
+ options: {
9964
+ ...(accept ? {
9965
+ accept
9966
+ } : {}),
9967
+ ...(directory ? {
9968
+ directory
9969
+ } : {})
9970
+ }
9971
+ } : {
9972
+ type: "file",
9973
+ opt: false,
9974
+ options: {
9975
+ ...(accept ? {
9976
+ accept
9977
+ } : {})
9978
+ }
9979
+ };
9980
+ return uploadRemoteFileCore(ctx, fileRef, metadata, schema);
9981
+ }
9982
+ async function handleRemoteFileDownload(ctx) {
9983
+ if (ctx.fix) {
9984
+ return {
9985
+ success: true,
9986
+ shouldApplyPatch: true,
9987
+ events: [{
9988
+ type: "remote-downloading",
9989
+ sourcePath: ctx.sourcePath
9990
+ }]
9991
+ };
9992
+ } else {
9993
+ return {
9994
+ success: false,
9995
+ errorMessage: `Remote file ${ctx.sourcePath} needs to be downloaded (use --fix to download)`
9996
+ };
9997
+ }
9998
+ }
9999
+ async function handleRemoteFileCheck() {
10000
+ // Skip - no action needed
10001
+ return {
10002
+ success: true,
10003
+ shouldApplyPatch: true
10004
+ };
10005
+ }
10006
+ async function handleUniqueFolderCheck(ctx) {
10007
+ const value = ctx.validationError.value;
10008
+ if (!value || typeof value.directory !== "string") {
10009
+ return {
10010
+ success: false,
10011
+ errorMessage: `Unexpected value in unique folder check for ${ctx.sourcePath}`
10012
+ };
10013
+ }
10014
+ const {
10015
+ directory
10016
+ } = value;
10017
+ const conflicts = [];
10018
+ for (const file of ctx.valFiles) {
10019
+ const otherModuleFilePath = `/${file}`;
10020
+ if (otherModuleFilePath === ctx.moduleFilePath) continue;
10021
+ const otherModule = await ctx.service.get(otherModuleFilePath, "", {
10022
+ validate: false
10023
+ });
10024
+ const schema = otherModule.schema;
10025
+ if ((schema === null || schema === void 0 ? void 0 : schema.type) === "record" && schema.directory === directory && schema.mediaType) {
10026
+ conflicts.push(otherModuleFilePath);
10027
+ }
10028
+ }
10029
+ if (conflicts.length > 0) {
10030
+ return {
10031
+ success: false,
10032
+ errorMessage: `Gallery directory '${directory}' in ${ctx.moduleFilePath} is also used by: ${conflicts.join(", ")}. Each gallery must use a unique directory.`
10033
+ };
10034
+ }
10035
+ return {
10036
+ success: true
10037
+ };
10038
+ }
10039
+
10040
+ // Maps a gallery key to its on-disk local path. Remote galleries key uploaded
10041
+ // entries by a remote URL while keeping the file on disk; everything else is
10042
+ // already a local path and is returned unchanged.
10043
+ function remoteKeyToLocalPath(key) {
10044
+ const remoteRefRes = core.Internal.remote.splitRemoteRef(key);
10045
+ if (remoteRefRes.status === "success") {
10046
+ return `/${remoteRefRes.filePath}`;
10047
+ }
10048
+ return key;
10049
+ }
10050
+ async function handleCheckAllFiles(ctx) {
10051
+ const value = ctx.validationError.value;
10052
+ if (!value || typeof value.directory !== "string") {
10053
+ return {
10054
+ success: false,
10055
+ errorMessage: `Unexpected value in check-all-files for ${ctx.sourcePath}`
10056
+ };
10057
+ }
10058
+ const {
10059
+ directory
10060
+ } = value;
10061
+ const source = ctx.valModule.source;
10062
+ if (!source || typeof source !== "object" || Array.isArray(source)) {
10063
+ return {
10064
+ success: false,
10065
+ errorMessage: `Could not get source for ${ctx.sourcePath}`
10066
+ };
10067
+ }
10068
+ // Gallery entries are keyed by their file path. Remote galleries key uploaded
10069
+ // entries by a remote URL, but the file is kept on disk at its local path, so
10070
+ // normalize remote-URL keys back to that local path for the on-disk checks.
10071
+ const trackedFiles = new Set(Object.keys(source).map(remoteKeyToLocalPath));
10072
+
10073
+ // Check that all tracked files exist on disk
10074
+ const missingTrackedFiles = Array.from(trackedFiles).filter(f => {
10075
+ return !ctx.fs.fileExists(path__namespace["default"].join(ctx.projectRoot, f));
10076
+ });
10077
+ if (missingTrackedFiles.length > 0) {
10078
+ if (!ctx.fix) {
10079
+ return {
10080
+ success: false,
10081
+ errorMessage: `Gallery in ${ctx.moduleFilePath} has tracked files that do not exist on disk: ${missingTrackedFiles.join(", ")}. Add the files or remove them from the gallery.`
10082
+ };
10083
+ }
10084
+ // fix: true — let createFixPatch remove the missing entries
10085
+ return {
10086
+ success: true,
10087
+ shouldApplyPatch: true
10088
+ };
10089
+ }
10090
+ const dirPath = path__namespace["default"].join(ctx.projectRoot, directory);
10091
+ const filesInDir = [];
10092
+ try {
10093
+ const entries = ctx.fs.readDirectory(dirPath, undefined, undefined, ["**/*"]);
10094
+ for (const entry of entries) {
10095
+ const relPath = "/" + path__namespace["default"].relative(ctx.projectRoot, entry).split(path__namespace["default"].sep).join("/");
10096
+ filesInDir.push(relPath);
10097
+ }
10098
+ } catch {
10099
+ // directory doesn't exist — no untracked files possible
10100
+ }
10101
+ const untrackedFiles = filesInDir.filter(f => !trackedFiles.has(f));
10102
+ if (untrackedFiles.length > 0) {
10103
+ return {
10104
+ success: false,
10105
+ errorMessage: `Gallery in ${ctx.moduleFilePath} has files not tracked: ${untrackedFiles.join(", ")}. Add these files to the gallery or remove them from the directory.`
10106
+ };
10107
+ }
10108
+
10109
+ // All files accounted for — trigger metadata verification via createFixPatch
10110
+ return {
10111
+ success: true,
10112
+ shouldApplyPatch: true
10113
+ };
10114
+ }
10115
+ async function handleJsonValuesExtractEntry(ctx) {
10116
+ const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
10117
+ const parts = core.Internal.splitModulePath(modulePath);
10118
+ // Root-only by contract (see findNestedJsonValuesRecords): the entry is a
10119
+ // direct child of the module's root record/router, so the path is one segment.
10120
+ if (parts.length !== 1) {
10121
+ return {
10122
+ success: false,
10123
+ errorMessage: `Cannot extract .jsonValues() entry at ${ctx.sourcePath}: expected a root record entry`
10124
+ };
10125
+ }
10126
+ const entryKey = parts[0];
10127
+ const source = ctx.valModule.source;
10128
+ if (source === null || typeof source !== "object" || Array.isArray(source)) {
10129
+ return {
10130
+ success: false,
10131
+ errorMessage: `Could not get source for ${ctx.moduleFilePath}`
10132
+ };
10133
+ }
10134
+ const content = source[entryKey];
10135
+ if (content === undefined) {
10136
+ return {
10137
+ success: false,
10138
+ errorMessage: `Could not find .jsonValues() entry '${entryKey}' in ${ctx.moduleFilePath}`
10139
+ };
10140
+ }
10141
+ if (!ctx.fix) {
10142
+ return {
10143
+ success: true,
10144
+ fixableErrorMessage: ctx.validationError.message
10145
+ };
10146
+ }
10147
+ try {
10148
+ extractJsonValuesEntry(ctx.moduleFilePath, ctx.projectRoot, entryKey, content, ctx.service.sourceFileHandler);
10149
+ } catch (err) {
10150
+ return {
10151
+ success: false,
10152
+ errorMessage: err instanceof Error ? err.message : String(err)
10153
+ };
10154
+ }
10155
+ return {
10156
+ success: true,
10157
+ appliedFix: true
10158
+ };
10159
+ }
10160
+
10161
+ // Fix handler registry. `keyof:check-keys` and `router:check-route` are
10162
+ // resolved upfront by the shared resolveSchemaSourceFixes — they never reach
10163
+ // this registry, so they're excluded from the key set.
10164
+ const currentFixHandlers = {
10165
+ "image:check-metadata": handleFileMetadata,
10166
+ "image:add-metadata": handleFileMetadata,
10167
+ "file:check-metadata": handleFileMetadata,
10168
+ "file:add-metadata": handleFileMetadata,
10169
+ "image:upload-remote": handleRemoteFileUpload,
10170
+ "file:upload-remote": handleRemoteFileUpload,
10171
+ "images:upload-remote": handleRemoteGalleryFileUpload,
10172
+ "files:upload-remote": handleRemoteGalleryFileUpload,
10173
+ "image:download-remote": handleRemoteFileDownload,
10174
+ "file:download-remote": handleRemoteFileDownload,
10175
+ "image:check-remote": handleRemoteFileCheck,
10176
+ "images:check-remote": handleRemoteFileCheck,
10177
+ "file:check-remote": handleRemoteFileCheck,
10178
+ "files:check-remote": handleRemoteFileCheck,
10179
+ "images:check-unique-folder": handleUniqueFolderCheck,
10180
+ "files:check-unique-folder": handleUniqueFolderCheck,
10181
+ "images:check-all-files": handleCheckAllFiles,
10182
+ "files:check-all-files": handleCheckAllFiles,
10183
+ "jsonValues:extract-entry": handleJsonValuesExtractEntry
10184
+ };
10185
+ const deprecatedFixHandlers = {
10186
+ "image:replace-metadata": handleFileMetadata
10187
+ };
10188
+ const fixHandlers = {
10189
+ ...deprecatedFixHandlers,
10190
+ ...currentFixHandlers
10191
+ };
10192
+ function createDefaultValFSHost() {
10193
+ return {
10194
+ ...ts__default["default"].sys,
10195
+ writeFile: (fileName, data, encoding) => {
10196
+ fs__default["default"].mkdirSync(path__namespace["default"].dirname(fileName), {
10197
+ recursive: true
10198
+ });
10199
+ fs__default["default"].writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
10200
+ },
10201
+ rmFile: fs__default["default"].rmSync,
10202
+ readBuffer: fileName => {
10203
+ try {
10204
+ return fs__default["default"].readFileSync(fileName);
10205
+ } catch {
10206
+ return undefined;
10207
+ }
10208
+ }
10209
+ };
10210
+ }
10211
+
10212
+ function validateMetadata(actualMetadata, expectedMetadata) {
10213
+ const missingMetadata = [];
10214
+ const erroneousMetadata = {};
10215
+ if (typeof actualMetadata !== "object" || actualMetadata === null) {
10216
+ return {
10217
+ globalErrors: ["Metadata is wrong type: must be an object."]
10218
+ };
10219
+ }
10220
+ if (Array.isArray(actualMetadata)) {
10221
+ return {
10222
+ globalErrors: ["Metadata is wrong type: cannot be an array."]
10223
+ };
10224
+ }
10225
+ const recordMetadata = actualMetadata;
10226
+ const globalErrors = [];
10227
+ for (const anyKey in expectedMetadata) {
10228
+ if (typeof anyKey !== "string") {
10229
+ globalErrors.push(`Expected metadata has key '${anyKey}' that is not typeof 'string', but: '${typeof anyKey}'. This is most likely a Val bug.`);
10230
+ } else {
10231
+ if (anyKey in actualMetadata) {
10232
+ const key = anyKey;
10233
+ if (expectedMetadata[key] !== recordMetadata[key]) {
10234
+ erroneousMetadata[key] = `Expected metadata '${key}' to be ${JSON.stringify(expectedMetadata[key])}, but got ${JSON.stringify(recordMetadata[key])}.`;
9932
10235
  }
9933
10236
  } else {
9934
10237
  missingMetadata.push(anyKey);
@@ -10198,9 +10501,9 @@ function getModulePathRange(modulePath, modulePathMap,
10198
10501
  // Which part of an object/record member to point at. For an object property
10199
10502
  // the resolved node's own range is the *key* (property name); the *value*
10200
10503
  // range is stored under `children.val`. Array elements, leaf literals and
10201
- // `c.image`/`c.file` `_ref`/`metadata` nodes have no `val` child, so "value"
10202
- // falls back to the node's own range for those. Defaults to "key" to preserve
10203
- // existing callers.
10504
+ // Array elements and leaf literals have no `val` child, so "value" falls back
10505
+ // to the node's own range for those. Defaults to "key" to preserve existing
10506
+ // callers.
10204
10507
  target = "key") {
10205
10508
  var _range$children;
10206
10509
  // Handle invalid module paths gracefully
@@ -10253,7 +10556,7 @@ target = "key") {
10253
10556
  *
10254
10557
  * NOTE: do not compute the start as `end.character - node.getWidth()`. That
10255
10558
  * identity only holds while the node stays on a single line - for a multi-line
10256
- * node (an object inside an array, a `c.image` metadata argument, ...) it
10559
+ * node (an object inside an array, a media object's `path`, ...) it
10257
10560
  * reports the *closing* line and a negative character.
10258
10561
  */
10259
10562
  function rangeOf(node, sourceFile) {
@@ -10317,39 +10620,6 @@ function traverse(node, sourceFile) {
10317
10620
  if (ts__default["default"].isArrayLiteralExpression(node)) {
10318
10621
  return traverseArrayLiteral(node, sourceFile);
10319
10622
  }
10320
- if (ts__default["default"].isCallExpression(node)) {
10321
- return traverseCallExpression(node, sourceFile);
10322
- }
10323
- }
10324
- function traverseCallExpression(node, sourceFile) {
10325
- if (ts__default["default"].isPropertyAccessExpression(node.expression)) {
10326
- if (node.expression.expression.getText(sourceFile) === "c" && (node.expression.name.getText(sourceFile) === "file" || node.expression.name.getText(sourceFile) === "image")) {
10327
- const val = {
10328
- children: {},
10329
- ...rangeOf(node, sourceFile)
10330
- };
10331
- if (node.arguments[0]) {
10332
- const _ref = {
10333
- children: {},
10334
- ...rangeOf(node.arguments[0], sourceFile)
10335
- };
10336
- if (!node.arguments[1]) {
10337
- return {
10338
- val,
10339
- _ref
10340
- };
10341
- }
10342
- return {
10343
- val,
10344
- _ref,
10345
- metadata: {
10346
- children: {},
10347
- ...rangeOf(node.arguments[1], sourceFile)
10348
- }
10349
- };
10350
- }
10351
- }
10352
- }
10353
10623
  }
10354
10624
  function traverseArrayLiteral(node, sourceFile) {
10355
10625
  return node.elements.reduce((acc, element, index) => {
@@ -10423,61 +10693,6 @@ function findJsonEntryFilePath(moduleFilePath, valTsSourceFile, entryKey) {
10423
10693
  return resolveExistingJsonPath(moduleFilePath, entry.importPath);
10424
10694
  }
10425
10695
 
10426
- /**
10427
- * Moves ONE `.jsonValues()` entry that was written inline in the `.val.ts` into
10428
- * its own `*.val.json`, replacing the inline value with
10429
- * `c.json(() => import("./<key>.val.json"))`.
10430
- *
10431
- * This is the fix for the `jsonValues:extract-entry` validation error. It is not
10432
- * expressible as a patch (a patch edits one `.val.ts` and cannot create the
10433
- * backing JSON file), so it writes both files directly — JSON first, so a
10434
- * failure part-way through never leaves the module pointing at a file that does
10435
- * not exist.
10436
- *
10437
- * Root-only, like the rest of the `.jsonValues()` machinery: the entry is looked
10438
- * up in the module's root record/router object literal.
10439
- */
10440
- function extractJsonValuesEntry(moduleFilePath, rootDir, entryKey, content, sourceFileHandler) {
10441
- const valTsPath = sourceFileHandler.resolveSourceModulePath(getSyntheticContainingPath(rootDir), `.${moduleFilePath.replace(".val.ts", ".val").replace(".val.js", ".val").replace(".val.jsx", ".val").replace(".val.tsx", ".val")}`);
10442
- const sourceFile = sourceFileHandler.getSourceFile(valTsPath);
10443
- if (!sourceFile) {
10444
- throw Error(`Source file ${valTsPath} not found`);
10445
- }
10446
- const pathsRes = getNewJsonEntryPaths(moduleFilePath, entryKey);
10447
- if (fp.result.isErr(pathsRes)) {
10448
- throw Error(formatPatchSourceError(pathsRes.error));
10449
- }
10450
- const {
10451
- jsonPath,
10452
- importPath
10453
- } = pathsRes.value;
10454
- const absoluteJsonPath = path__namespace["default"].join(rootDir, jsonPath);
10455
- if (sourceFileHandler.host.fileExists(absoluteJsonPath)) {
10456
- throw Error(`Cannot extract .jsonValues() entry '${entryKey}' of ${moduleFilePath}: '${jsonPath}' already exists`);
10457
- }
10458
-
10459
- // Remove the inline property, then add the `c.json(...)` reference back. The
10460
- // entry moves to the end of the record: entry ORDER in a jsonValues record is
10461
- // not meaningful (the Studio and the runtime key entries by name), and
10462
- // insert-in-place would mean reimplementing insertValJsonEntry.
10463
- const removed = removeValJsonEntry(sourceFile, [], entryKey);
10464
- if (fp.result.isErr(removed)) {
10465
- throw Error(`${valTsPath}\n${formatOpsError(removed.error, sourceFile)}`);
10466
- }
10467
- const inserted = insertValJsonEntry(removed.value, [], entryKey, importPath);
10468
- if (fp.result.isErr(inserted)) {
10469
- throw Error(`${valTsPath}\n${formatOpsError(inserted.error, removed.value)}`);
10470
- }
10471
- sourceFileHandler.writeFile(absoluteJsonPath, JSON.stringify(content, null, 2) + "\n", "utf8");
10472
- sourceFileHandler.writeSourceFile(inserted.value);
10473
- }
10474
- function formatOpsError(error, sourceFile) {
10475
- if (error instanceof patch.PatchError) {
10476
- return error.message;
10477
- }
10478
- return flatMapErrors(error, e => formatSyntaxError(e, sourceFile)).join("\n");
10479
- }
10480
-
10481
10696
  /**
10482
10697
  * Replays a `val debug` snapshot: applies its patches the way /save does and
10483
10698
  * validates the result.
@@ -10595,12 +10810,14 @@ exports.analyzeValModule = analyzeValModule;
10595
10810
  exports.awaitValLoginConfirmation = awaitValLoginConfirmation;
10596
10811
  exports.checkRemoteRef = checkRemoteRef;
10597
10812
  exports.compareWithCapturedReport = compareWithCapturedReport;
10813
+ exports.createDefaultValFSHost = createDefaultValFSHost;
10598
10814
  exports.createFixPatch = createFixPatch;
10599
10815
  exports.createJsonEntryPathMap = createJsonEntryPathMap;
10600
10816
  exports.createModulePathMap = createModulePathMap;
10601
10817
  exports.createService = createService;
10602
10818
  exports.createValApiRouter = createValApiRouter;
10603
10819
  exports.createValServer = createValServer;
10820
+ exports.currentFixHandlers = currentFixHandlers;
10604
10821
  exports.decodeJwt = decodeJwt;
10605
10822
  exports.downloadFileFromRemote = downloadFileFromRemote;
10606
10823
  exports.encodeJwt = encodeJwt;
@@ -10610,6 +10827,7 @@ exports.extractImageMetadata = extractImageMetadata;
10610
10827
  exports.extractJsonValuesEntry = extractJsonValuesEntry;
10611
10828
  exports.findAndEvalValConfigFile = findAndEvalValConfigFile;
10612
10829
  exports.findJsonEntryFilePath = findJsonEntryFilePath;
10830
+ exports.fixHandlers = fixHandlers;
10613
10831
  exports.formatPatchSourceError = formatPatchSourceError;
10614
10832
  exports.formatSyntaxErrorTree = formatSyntaxErrorTree;
10615
10833
  exports.getCachedRemoteFileDir = getCachedRemoteFileDir;
@@ -10621,6 +10839,14 @@ exports.getModulePathRange = getModulePathRange;
10621
10839
  exports.getPersonalAccessTokenPath = getPersonalAccessTokenPath;
10622
10840
  exports.getSettings = getSettings;
10623
10841
  exports.getValidationErrorFileRef = getValidationErrorFileRef;
10842
+ exports.handleCheckAllFiles = handleCheckAllFiles;
10843
+ exports.handleFileMetadata = handleFileMetadata;
10844
+ exports.handleJsonValuesExtractEntry = handleJsonValuesExtractEntry;
10845
+ exports.handleRemoteFileCheck = handleRemoteFileCheck;
10846
+ exports.handleRemoteFileDownload = handleRemoteFileDownload;
10847
+ exports.handleRemoteFileUpload = handleRemoteFileUpload;
10848
+ exports.handleRemoteGalleryFileUpload = handleRemoteGalleryFileUpload;
10849
+ exports.handleUniqueFolderCheck = handleUniqueFolderCheck;
10624
10850
  exports.loadValModules = loadValModules;
10625
10851
  exports.parsePersonalAccessTokenFile = parsePersonalAccessTokenFile;
10626
10852
  exports.patchSourceFile = patchSourceFile;