@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.
@@ -1,8 +1,8 @@
1
1
  import ts from 'typescript';
2
2
  import { pipe, result, array } from '@valbuild/core/fp';
3
- import { FILE_REF_PROP, FILE_REF_SUBTYPE_TAG, VAL_EXTENSION, derefPatch, RecordSchema, Internal, extractValModules, ImageSchema, DEFAULT_CONTENT_HOST, hasRemoteFileSchema } from '@valbuild/core';
4
- export { hasRemoteFileSchema } from '@valbuild/core';
5
3
  import { PatchError, deepEqual, parseAndValidateArrayIndex, isNotRoot, applyPatch, JSONOps, deepClone, sourceToPatchPath } from '@valbuild/core/patch';
4
+ import { derefPatch, RecordSchema, Internal, extractValModules, VAL_EXTENSION, ImageSchema, DEFAULT_CONTENT_HOST, hasRemoteFileSchema } from '@valbuild/core';
5
+ export { hasRemoteFileSchema } from '@valbuild/core';
6
6
  import * as path from 'path';
7
7
  import path__default from 'path';
8
8
  import fs, { promises } from 'fs';
@@ -69,7 +69,7 @@ function validateObjectProperties(nodes) {
69
69
  * validating its children.
70
70
  */
71
71
  function shallowValidateExpression(value) {
72
- return ts.isStringLiteralLike(value) || ts.isNumericLiteral(value) || value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword || value.kind === ts.SyntaxKind.NullKeyword || ts.isArrayLiteralExpression(value) || ts.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);
72
+ return ts.isStringLiteralLike(value) || ts.isNumericLiteral(value) || value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword || value.kind === ts.SyntaxKind.NullKeyword || ts.isArrayLiteralExpression(value) || ts.isObjectLiteralExpression(value) ? undefined : new ValSyntaxError("Expression must be a literal", value);
73
73
  }
74
74
 
75
75
  /**
@@ -95,55 +95,8 @@ function evaluateExpression(value) {
95
95
  return result.all(value.elements.map(evaluateExpression));
96
96
  } else if (ts.isObjectLiteralExpression(value)) {
97
97
  return pipe(validateObjectProperties(value.properties), result.flatMap(assignments => pipe(assignments.map(assignment => pipe(evaluateExpression(assignment.initializer), result.map(value => [assignment.name.text, value]))), result.all)), result.map(Object.fromEntries));
98
- } else if (isValFileMethodCall(value)) {
99
- return pipe(findValFileNodeArg(value), result.flatMap(ref => {
100
- if (value.arguments.length === 2) {
101
- return pipe(evaluateExpression(value.arguments[1]), result.map(metadata => ({
102
- [FILE_REF_PROP]: ref.text,
103
- _type: "file",
104
- metadata
105
- })));
106
- } else {
107
- return result.ok({
108
- [FILE_REF_PROP]: ref.text,
109
- _type: "file"
110
- });
111
- }
112
- }));
113
- } else if (isValImageMethodCall(value)) {
114
- return pipe(findValImageNodeArg(value), result.flatMap(ref => {
115
- if (value.arguments.length === 2) {
116
- return pipe(evaluateExpression(value.arguments[1]), result.map(metadata => ({
117
- [FILE_REF_PROP]: ref.text,
118
- _type: "file",
119
- [FILE_REF_SUBTYPE_TAG]: "image",
120
- metadata
121
- })));
122
- } else {
123
- return result.ok({
124
- [FILE_REF_PROP]: ref.text,
125
- _type: "file",
126
- [FILE_REF_SUBTYPE_TAG]: "image"
127
- });
128
- }
129
- }));
130
- } else if (isValRemoteMethodCall(value)) {
131
- return pipe(findValRemoteNodeArg(value), result.flatMap(ref => {
132
- if (value.arguments.length === 2) {
133
- return pipe(evaluateExpression(value.arguments[1]), result.map(metadata => ({
134
- [FILE_REF_PROP]: ref.text,
135
- _type: "remote",
136
- metadata
137
- })));
138
- } else {
139
- return result.ok({
140
- [FILE_REF_PROP]: ref.text,
141
- _type: "remote"
142
- });
143
- }
144
- }));
145
98
  } else {
146
- return result.err(new ValSyntaxError("Expression must be a literal or call c.file / c.image / c.remote", value));
99
+ return result.err(new ValSyntaxError("Expression must be a literal", value));
147
100
  }
148
101
  }
149
102
  function findObjectPropertyAssignment(value, key) {
@@ -157,93 +110,6 @@ function findObjectPropertyAssignment(value, key) {
157
110
  return result.ok(assignment);
158
111
  }));
159
112
  }
160
- function isValFileMethodCall(node) {
161
- return ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "c" && node.expression.name.text === "file";
162
- }
163
- function isValImageMethodCall(node) {
164
- return ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "c" && node.expression.name.text === "image";
165
- }
166
- function isValRemoteMethodCall(node) {
167
- return ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "c" && node.expression.name.text === "remote";
168
- }
169
- function findValFileNodeArg(node) {
170
- if (node.arguments.length === 0) {
171
- return result.err(new ValSyntaxError(`Invalid c.file() call: missing ref argument`, node));
172
- } else if (node.arguments.length > 2) {
173
- return result.err(new ValSyntaxError(`Invalid c.file() call: too many arguments ${node.arguments.length}}`, node));
174
- } else if (!ts.isStringLiteral(node.arguments[0])) {
175
- return result.err(new ValSyntaxError(`Invalid c.file() call: ref must be a string literal`, node));
176
- }
177
- const refNode = node.arguments[0];
178
- return result.ok(refNode);
179
- }
180
- function findValRemoteNodeArg(node) {
181
- if (node.arguments.length === 0) {
182
- return result.err(new ValSyntaxError(`Invalid c.remote() call: missing ref argument`, node));
183
- } else if (node.arguments.length > 2) {
184
- return result.err(new ValSyntaxError(`Invalid c.remote() call: too many arguments ${node.arguments.length}}`, node));
185
- } else if (!ts.isStringLiteral(node.arguments[0])) {
186
- return result.err(new ValSyntaxError(`Invalid c.remote() call: ref must be a string literal`, node));
187
- }
188
- const refNode = node.arguments[0];
189
- return result.ok(refNode);
190
- }
191
- function findValImageNodeArg(node) {
192
- if (node.arguments.length === 0) {
193
- return result.err(new ValSyntaxError(`Invalid c.image() call: missing ref argument`, node));
194
- } else if (node.arguments.length > 2) {
195
- return result.err(new ValSyntaxError(`Invalid c.image() call: too many arguments ${node.arguments.length}}`, node));
196
- } else if (!ts.isStringLiteral(node.arguments[0])) {
197
- return result.err(new ValSyntaxError(`Invalid c.image() call: ref must be a string literal`, node));
198
- }
199
- const refNode = node.arguments[0];
200
- return result.ok(refNode);
201
- }
202
- function findValFileMetadataArg(node) {
203
- if (node.arguments.length === 0) {
204
- return result.err(new ValSyntaxError(`Invalid c.file() call: missing ref argument`, node));
205
- } else if (node.arguments.length > 2) {
206
- return result.err(new ValSyntaxError(`Invalid c.file() call: too many arguments ${node.arguments.length}}`, node));
207
- } else if (node.arguments.length === 2) {
208
- if (!ts.isObjectLiteralExpression(node.arguments[1])) {
209
- return result.err(new ValSyntaxError(`Invalid c.file() call: metadata must be a object literal`, node));
210
- }
211
- return result.ok(node.arguments[1]);
212
- }
213
- return result.ok(undefined);
214
- }
215
- function findValImageMetadataArg(node) {
216
- if (node.arguments.length === 0) {
217
- return result.err(new ValSyntaxError(`Invalid c.image() call: missing ref argument`, node));
218
- } else if (node.arguments.length > 2) {
219
- return result.err(new ValSyntaxError(`Invalid c.image() call: too many arguments ${node.arguments.length}}`, node));
220
- } else if (node.arguments.length === 2) {
221
- if (!ts.isObjectLiteralExpression(node.arguments[1])) {
222
- return result.err(new ValSyntaxError(`Invalid c.image() call: metadata must be a object literal`, node));
223
- }
224
- return result.ok(node.arguments[1]);
225
- }
226
- return result.ok(undefined);
227
- }
228
- function findValRemoteMetadataArg(node) {
229
- if (node.arguments.length === 0) {
230
- return result.err(new ValSyntaxError(`Invalid c.remote() call: missing ref argument`, node));
231
- } else if (node.arguments.length > 2) {
232
- return result.err(new ValSyntaxError(`Invalid c.remote() call: too many arguments ${node.arguments.length}}`, node));
233
- } else if (node.arguments.length === 2) {
234
- if (!ts.isObjectLiteralExpression(node.arguments[1])) {
235
- return result.err(new ValSyntaxError(`Invalid c.remote() call: metadata must be a object literal`, node));
236
- }
237
- return result.ok(node.arguments[1]);
238
- }
239
- return result.ok(undefined);
240
- }
241
-
242
- /**
243
- * Given a list of expressions, validates that all the expressions are not
244
- * spread elements. In other words, it ensures that the expressions are the
245
- * initializers of the values at their respective indices in the evaluated list.
246
- */
247
113
  function validateInitializers(nodes) {
248
114
  for (const node of nodes) {
249
115
  if (ts.isSpreadElement(node)) {
@@ -325,27 +191,6 @@ function isValidIdentifier(text) {
325
191
  function createPropertyAssignment(key, value) {
326
192
  return ts.factory.createPropertyAssignment(isValidIdentifier(key) ? ts.factory.createIdentifier(key) : ts.factory.createStringLiteral(key), toExpression(value));
327
193
  }
328
- function createValFileReference(value) {
329
- const args = [ts.factory.createStringLiteral(value[FILE_REF_PROP])];
330
- if (value.metadata) {
331
- args.push(toExpression(value.metadata));
332
- }
333
- return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("c"), ts.factory.createIdentifier("file")), undefined, args);
334
- }
335
- function createValImageReference(value) {
336
- const args = [ts.factory.createStringLiteral(value[FILE_REF_PROP])];
337
- if (value.metadata) {
338
- args.push(toExpression(value.metadata));
339
- }
340
- return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("c"), ts.factory.createIdentifier("image")), undefined, args);
341
- }
342
- function createValRemoteReference(value) {
343
- const args = [ts.factory.createStringLiteral(value[FILE_REF_PROP])];
344
- if (value.metadata) {
345
- args.push(toExpression(value.metadata));
346
- }
347
- return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("c"), ts.factory.createIdentifier("remote")), undefined, args);
348
- }
349
194
 
350
195
  /**
351
196
  * Builds the expression `c.json(() => import("<importPath>"))` used to reference
@@ -431,16 +276,12 @@ function toExpression(value) {
431
276
  } else if (Array.isArray(value)) {
432
277
  return ts.factory.createArrayLiteralExpression(value.map(toExpression));
433
278
  } else if (typeof value === "object") {
434
- if (isValFileValue(value)) {
435
- return createValFileReference(value);
436
- }
437
- if (isValImageValue(value)) {
438
- return createValImageReference(value);
439
- }
440
- if (isValRemoteValue(value)) {
441
- return createValRemoteReference(value);
442
- }
443
- return ts.factory.createObjectLiteralExpression(Object.entries(value).map(([key, value]) => createPropertyAssignment(key, value)));
279
+ return ts.factory.createObjectLiteralExpression(Object.entries(value)
280
+ // `patch_id` is injected server-side onto a media source whose bytes are
281
+ // not committed yet. It describes the draft, not the content, and a
282
+ // whole-object write built from the client's optimistic view would
283
+ // otherwise print it into the user's file.
284
+ .filter(([key]) => key !== "patch_id").map(([key, value]) => createPropertyAssignment(key, value)));
444
285
  } else {
445
286
  return ts.factory.createStringLiteral(value);
446
287
  }
@@ -573,63 +414,6 @@ function replaceInNode(document, node, key, value) {
573
414
  }
574
415
  return result.ok(assignment);
575
416
  }), result.map(assignment => replaceNodeValue(document, assignment.initializer, value)));
576
- } else if (isValFileMethodCall(node)) {
577
- if (key === FILE_REF_PROP) {
578
- if (typeof value !== "string") {
579
- return result.err(new PatchError("Cannot replace c.file reference with non-string value"));
580
- }
581
- return pipe(findValFileNodeArg(node), result.map(refNode => replaceNodeValue(document, refNode, value)));
582
- } else {
583
- return pipe(findValFileMetadataArg(node), result.flatMap(metadataArgNode => {
584
- if (!metadataArgNode) {
585
- return result.err(new PatchError("Cannot replace in c.file metadata when it does not exist"));
586
- }
587
- if (key !== "metadata") {
588
- return result.err(new PatchError(`Cannot replace c.file metadata key ${key} when it does not exist`));
589
- }
590
- return replaceInNode(document,
591
- // TODO: creating a fake object here might not be right - seems to work though
592
- ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment(key, metadataArgNode)]), key, value);
593
- }));
594
- }
595
- } else if (isValImageMethodCall(node)) {
596
- if (key === FILE_REF_PROP) {
597
- if (typeof value !== "string") {
598
- return result.err(new PatchError("Cannot replace c.image reference with non-string value"));
599
- }
600
- return pipe(findValImageNodeArg(node), result.map(refNode => replaceNodeValue(document, refNode, value)));
601
- } else {
602
- return pipe(findValImageMetadataArg(node), result.flatMap(metadataArgNode => {
603
- if (!metadataArgNode) {
604
- return result.err(new PatchError("Cannot replace in c.image metadata when it does not exist"));
605
- }
606
- if (key !== "metadata") {
607
- return result.err(new PatchError(`Cannot replace c.image metadata key ${key} when it does not exist`));
608
- }
609
- return replaceInNode(document,
610
- // TODO: creating a fake object here might not be right - seems to work though
611
- ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment(key, metadataArgNode)]), key, value);
612
- }));
613
- }
614
- } else if (isValRemoteMethodCall(node)) {
615
- if (key === FILE_REF_PROP) {
616
- if (typeof value !== "string") {
617
- return result.err(new PatchError("Cannot replace c.image reference with non-string value"));
618
- }
619
- return pipe(findValRemoteNodeArg(node), result.map(refNode => replaceNodeValue(document, refNode, value)));
620
- } else {
621
- return pipe(findValRemoteMetadataArg(node), result.flatMap(metadataArgNode => {
622
- if (!metadataArgNode) {
623
- return result.err(new PatchError("Cannot replace in c.remote metadata when it does not exist"));
624
- }
625
- if (key !== "metadata") {
626
- return result.err(new PatchError(`Cannot replace c.remote metadata key ${key} when it does not exist`));
627
- }
628
- return replaceInNode(document,
629
- // TODO: creating a fake object here might not be right - seems to work though
630
- ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment(key, metadataArgNode)]), key, value);
631
- }));
632
- }
633
417
  } else {
634
418
  return result.err(shallowValidateExpression(node) ?? new PatchError("Cannot replace in non-object/array"));
635
419
  }
@@ -646,21 +430,6 @@ function getFromNode(node, key) {
646
430
  return pipe(parseAndValidateArrayInboundsIndex(key, node.elements), result.map(index => node.elements[index]));
647
431
  } else if (ts.isObjectLiteralExpression(node)) {
648
432
  return pipe(findObjectPropertyAssignment(node, key), result.map(assignment => assignment === null || assignment === void 0 ? void 0 : assignment.initializer));
649
- } else if (isValFileMethodCall(node)) {
650
- if (key === FILE_REF_PROP) {
651
- return findValFileNodeArg(node);
652
- }
653
- return findValFileMetadataArg(node);
654
- } else if (isValImageMethodCall(node)) {
655
- if (key === FILE_REF_PROP) {
656
- return findValImageNodeArg(node);
657
- }
658
- return findValImageMetadataArg(node);
659
- } else if (isValRemoteMethodCall(node)) {
660
- if (key === FILE_REF_PROP) {
661
- return findValRemoteNodeArg(node);
662
- }
663
- return findValRemoteMetadataArg(node);
664
433
  } else {
665
434
  return result.err(shallowValidateExpression(node) ?? new PatchError("Cannot access non-object/array"));
666
435
  }
@@ -693,39 +462,6 @@ function removeFromNode(document, node, key) {
693
462
  }
694
463
  return result.ok(assignment);
695
464
  }), result.map(assignment => [removeAt(document, node.properties, node.properties.indexOf(assignment))[0], assignment.initializer]));
696
- } else if (isValFileMethodCall(node)) {
697
- if (key === FILE_REF_PROP) {
698
- return result.err(new PatchError("Cannot remove a ref from c.file"));
699
- } else {
700
- return pipe(findValFileMetadataArg(node), result.flatMap(metadataArgNode => {
701
- if (!metadataArgNode) {
702
- return result.err(new PatchError("Cannot remove from c.file metadata when it does not exist"));
703
- }
704
- return removeFromNode(document, metadataArgNode, key);
705
- }));
706
- }
707
- } else if (isValImageMethodCall(node)) {
708
- if (key === FILE_REF_PROP) {
709
- return result.err(new PatchError("Cannot remove a ref from c.image"));
710
- } else {
711
- return pipe(findValImageMetadataArg(node), result.flatMap(metadataArgNode => {
712
- if (!metadataArgNode) {
713
- return result.err(new PatchError("Cannot remove from c.image metadata when it does not exist"));
714
- }
715
- return removeFromNode(document, metadataArgNode, key);
716
- }));
717
- }
718
- } else if (isValRemoteMethodCall(node)) {
719
- if (key === FILE_REF_PROP) {
720
- return result.err(new PatchError("Cannot remove a ref from c.remote"));
721
- } else {
722
- return pipe(findValRemoteMetadataArg(node), result.flatMap(metadataArgNode => {
723
- if (!metadataArgNode) {
724
- return result.err(new PatchError("Cannot remove from c.remote metadata when it does not exist"));
725
- }
726
- return removeFromNode(document, metadataArgNode, key);
727
- }));
728
- }
729
465
  } else {
730
466
  return result.err(shallowValidateExpression(node) ?? new PatchError("Cannot remove from non-object/array"));
731
467
  }
@@ -733,22 +469,10 @@ function removeFromNode(document, node, key) {
733
469
  function removeAtPath(document, rootNode, path) {
734
470
  return pipe(getPointerFromPath(rootNode, path), result.flatMap(([node, key]) => removeFromNode(document, node, key)));
735
471
  }
736
- function isValFileValue(value) {
737
- return !!(typeof value === "object" && value && VAL_EXTENSION in value && value[VAL_EXTENSION] === "file" && FILE_REF_PROP in value && typeof value[FILE_REF_PROP] === "string" && value[FILE_REF_SUBTYPE_TAG] !== "image");
738
- }
739
- function isValImageValue(value) {
740
- return !!(typeof value === "object" && value && VAL_EXTENSION in value && value[VAL_EXTENSION] === "file" && FILE_REF_PROP in value && typeof value[FILE_REF_PROP] === "string" && value[FILE_REF_SUBTYPE_TAG] === "image");
741
- }
742
- function isValRemoteValue(value) {
743
- return !!(typeof value === "object" && value && VAL_EXTENSION in value && value[VAL_EXTENSION] === "remote" && FILE_REF_PROP in value && typeof value[FILE_REF_PROP] === "string");
744
- }
745
472
  function addToNode(document, node, key, value) {
746
473
  if (ts.isArrayLiteralExpression(node)) {
747
474
  return pipe(parseAndValidateArrayInsertIndex(key, node.elements), result.map(index => [insertAt(document, node.elements, index, toExpression(value))]));
748
475
  } else if (ts.isObjectLiteralExpression(node)) {
749
- if (key === FILE_REF_PROP) {
750
- return result.err(new PatchError("Cannot add a key ref to object"));
751
- }
752
476
  return pipe(findObjectPropertyAssignment(node, key), result.map(assignment => {
753
477
  if (!assignment) {
754
478
  return [insertAt(document, node.properties, node.properties.length, createPropertyAssignment(key, value))];
@@ -756,57 +480,6 @@ function addToNode(document, node, key, value) {
756
480
  return replaceNodeValue(document, assignment.initializer, value);
757
481
  }
758
482
  }));
759
- } else if (isValFileMethodCall(node)) {
760
- if (key === FILE_REF_PROP) {
761
- if (typeof value !== "string") {
762
- return result.err(new PatchError(`Cannot add ${FILE_REF_PROP} key to c.file with non-string value`));
763
- }
764
- return pipe(findValFileNodeArg(node), result.map(arg => replaceNodeValue(document, arg, value)));
765
- } else {
766
- return pipe(findValFileMetadataArg(node), result.flatMap(metadataArgNode => {
767
- if (metadataArgNode) {
768
- return result.err(new PatchError("Cannot add metadata to c.file when it already exists"));
769
- }
770
- if (key !== "metadata") {
771
- return result.err(new PatchError(`Cannot add ${key} key to c.file: only metadata is allowed`));
772
- }
773
- return result.ok([insertAt(document, node.arguments, node.arguments.length, toExpression(value))]);
774
- }));
775
- }
776
- } else if (isValImageMethodCall(node)) {
777
- if (key === FILE_REF_PROP) {
778
- if (typeof value !== "string") {
779
- return result.err(new PatchError(`Cannot add ${FILE_REF_PROP} key to c.image with non-string value`));
780
- }
781
- return pipe(findValImageNodeArg(node), result.map(arg => replaceNodeValue(document, arg, value)));
782
- } else {
783
- return pipe(findValImageMetadataArg(node), result.flatMap(metadataArgNode => {
784
- if (metadataArgNode) {
785
- return result.err(new PatchError("Cannot add metadata to c.image when it already exists"));
786
- }
787
- if (key !== "metadata") {
788
- return result.err(new PatchError(`Cannot add ${key} key to c.image: only metadata is allowed`));
789
- }
790
- return result.ok([insertAt(document, node.arguments, node.arguments.length, toExpression(value))]);
791
- }));
792
- }
793
- } else if (isValRemoteMethodCall(node)) {
794
- if (key === FILE_REF_PROP) {
795
- if (typeof value !== "string") {
796
- return result.err(new PatchError(`Cannot add ${FILE_REF_PROP} key to c.remote with non-string value`));
797
- }
798
- return pipe(findValRemoteNodeArg(node), result.map(arg => replaceNodeValue(document, arg, value)));
799
- } else {
800
- return pipe(findValRemoteMetadataArg(node), result.flatMap(metadataArgNode => {
801
- if (metadataArgNode) {
802
- return result.err(new PatchError("Cannot add metadata to c.remote when it already exists"));
803
- }
804
- if (key !== "metadata") {
805
- return result.err(new PatchError(`Cannot add ${key} key to c.remote: only metadata is allowed`));
806
- }
807
- return result.ok([insertAt(document, node.arguments, node.arguments.length, toExpression(value))]);
808
- }));
809
- }
810
483
  } else {
811
484
  return result.err(shallowValidateExpression(node) ?? new PatchError("Cannot add to non-object/array"));
812
485
  }
@@ -1388,7 +1061,10 @@ function rebaseContentOp(op, prefixLen) {
1388
1061
  from: op.from.slice(prefixLen)
1389
1062
  });
1390
1063
  case "file":
1391
- return result.err(new PatchError("Cannot apply a file op to a jsonValues entry"));
1064
+ // A file op carries bytes, not content. The binary goes through the
1065
+ // normal upload pipeline; what lands in the entry is the `patch_id` that
1066
+ // marks the bytes as not yet committed, which the caller writes.
1067
+ return result.err(new PatchError("A file op is not a content op: rebase it via its path"));
1392
1068
  }
1393
1069
  }
1394
1070
 
@@ -1422,9 +1098,6 @@ function applyJsonValuesEntryPatches(args) {
1422
1098
  } of patches) {
1423
1099
  let touched = false;
1424
1100
  for (const op of patch) {
1425
- if (op.op === "file") {
1426
- continue;
1427
- }
1428
1101
  const cls = serializedSchema ? classifyJsonValuesOp(serializedSchema, op.path) : {
1429
1102
  kind: "normal"
1430
1103
  };
@@ -1432,6 +1105,32 @@ function applyJsonValuesEntryPatches(args) {
1432
1105
  continue;
1433
1106
  }
1434
1107
  touched = true;
1108
+ // A file op does not edit the entry's content — its bytes go through the
1109
+ // upload pipeline. What the entry needs is the `patch_id` saying those
1110
+ // bytes are not committed yet, so `mediaUrl` serves them from the patch
1111
+ // directory instead of a `/public` path that holds nothing. Without this a
1112
+ // just-uploaded image inside an entry renders broken.
1113
+ if (op.op === "file") {
1114
+ if (op.value === null || content === undefined) {
1115
+ // A delete carries no bytes to point at, and there is nothing to mark
1116
+ // on an entry that does not exist.
1117
+ continue;
1118
+ }
1119
+ const applied = applyPatch(deepClone(content), jsonOps$1, [{
1120
+ op: "add",
1121
+ path: cls.subPath.concat(...(op.nestedFilePath ?? [])).concat("patch_id"),
1122
+ value: patchId
1123
+ }]);
1124
+ if (result.isErr(applied)) {
1125
+ return {
1126
+ kind: "error",
1127
+ message: applied.error.message,
1128
+ patchId
1129
+ };
1130
+ }
1131
+ content = applied.value;
1132
+ continue;
1133
+ }
1435
1134
  if (cls.subPath.length === 0) {
1436
1135
  if (op.op === "add" || op.op === "replace") {
1437
1136
  content = op.value;
@@ -2404,6 +2103,17 @@ class ValOps {
2404
2103
  const serializedSchema = jsonValuesSchemaFor(path);
2405
2104
  for (const op of patchData.patch) {
2406
2105
  if (op.op === "file") {
2106
+ // A file op inside a `.jsonValues()` entry has nothing to inject
2107
+ // HERE: the entry is an opaque marker in the module source, so an
2108
+ // `add` reaching into it fails and poisons the rest of this
2109
+ // module's chain. `applyJsonValuesEntryPatches` writes the patch_id
2110
+ // into the entry's draft content instead.
2111
+ const fileCls = serializedSchema ? classifyJsonValuesOp(serializedSchema, op.path) : {
2112
+ kind: "normal"
2113
+ };
2114
+ if (fileCls.kind === "entry" && fileCls.subPath.length > 0) {
2115
+ continue;
2116
+ }
2407
2117
  if (op.value !== null) {
2408
2118
  // NOTE: We insert the last patch_id that modify a file
2409
2119
  // when constructing the url we use the patch id (and the file path)
@@ -2704,7 +2414,7 @@ class ValOps {
2704
2414
  };
2705
2415
  }
2706
2416
  const type = schemaAtPath instanceof ImageSchema ? "image" : "file";
2707
- const filePath = value[FILE_REF_PROP];
2417
+ const filePath = value.path;
2708
2418
  const fileData = (fileLastUpdatedByPatchId === null || fileLastUpdatedByPatchId === void 0 ? void 0 : fileLastUpdatedByPatchId[filePath]) || null;
2709
2419
  let metadata;
2710
2420
  let metadataErrors;
@@ -2746,23 +2456,14 @@ class ValOps {
2746
2456
  }]
2747
2457
  };
2748
2458
  }
2749
- const metadataSourcePath = Internal.createValPathOfItem(sourcePath, "metadata");
2750
- if (!metadataSourcePath) {
2751
- throw new Error("Could not create metadata path");
2752
- }
2753
- const currentValueMetadata = value.metadata;
2754
- if (!currentValueMetadata) {
2755
- return {
2756
- [metadataSourcePath]: [{
2757
- message: "Missing metadata field: 'metadata'",
2758
- value
2759
- }]
2760
- };
2761
- }
2459
+ // The fields Val computes from the bytes sit next to `path` now, so the
2460
+ // error is reported at the field it is about rather than at a `metadata`
2461
+ // object that no longer exists.
2462
+ const currentValueMetadata = value;
2762
2463
  const fieldErrors = {};
2763
2464
  for (const field of getFieldsForType(type)) {
2764
2465
  const fieldMetadata = metadata[field];
2765
- const fieldSourcePath = Internal.createValPathOfItem(metadataSourcePath, field);
2466
+ const fieldSourcePath = Internal.createValPathOfItem(sourcePath, field);
2766
2467
  if (!fieldSourcePath) {
2767
2468
  throw new Error("Could not create field path");
2768
2469
  }
@@ -3354,11 +3055,15 @@ function isOnlyFileCheckValidationError(validationError) {
3354
3055
  }
3355
3056
  return false;
3356
3057
  }
3058
+
3059
+ /**
3060
+ * Whether a flagged validation value is media.
3061
+ *
3062
+ * The caller already knows the schema said image or file — this only guards
3063
+ * against a value that never got that far.
3064
+ */
3357
3065
  function isFileSource(value) {
3358
- if (typeof value === "object" && value !== null && FILE_REF_PROP in value && VAL_EXTENSION in value && value[VAL_EXTENSION] === "file" && FILE_REF_PROP) {
3359
- return true;
3360
- }
3361
- return false;
3066
+ return typeof value === "object" && value !== null && "path" in value && typeof value.path === "string";
3362
3067
  }
3363
3068
  function formatPatchSourceError(error) {
3364
3069
  if ("message" in error) {
@@ -4702,7 +4407,7 @@ const FSPatchBase = z.object({
4702
4407
  timestamp: z.string().datetime()
4703
4408
  });
4704
4409
 
4705
- const textEncoder$1 = new TextEncoder();
4410
+ const textEncoder$2 = new TextEncoder();
4706
4411
  const PatchId = z.string().refine(s => !!s); // TODO: validate
4707
4412
  const CommitSha = z.string().refine(s => !!s); // TODO: validate
4708
4413
  z.string().refine(s => !!s); // TODO: validate
@@ -5393,7 +5098,7 @@ class ValOpsHttp extends ValOps {
5393
5098
  });
5394
5099
  params.set("body_sha",
5395
5100
  // We use this for cache invalidation
5396
- Internal.getSHA256Hash(textEncoder$1.encode(stringifiedFiles)));
5101
+ Internal.getSHA256Hash(textEncoder$2.encode(stringifiedFiles)));
5397
5102
  return fetch(`${this.contentUrl}/v1/${this.project}/files?${params}`, {
5398
5103
  method: "PUT",
5399
5104
  // Yes, PUT is weird. Weirder to have a body in a GET request.
@@ -9121,15 +8826,21 @@ _input // TODO: use buffer to determine mimetype
9121
8826
  };
9122
8827
  }
9123
8828
 
8829
+ /**
8830
+ * The media path a validation error is about, if it is about media at all.
8831
+ *
8832
+ * There is no schema in hand here — a `ValidationError` carries only the value
8833
+ * it flagged — so the shape is all there is to go on.
8834
+ */
9124
8835
  function getValidationErrorFileRef(validationError) {
9125
- const maybeRef = validationError.value && typeof validationError.value === "object" && FILE_REF_PROP in validationError.value && typeof validationError.value[FILE_REF_PROP] === "string" ? validationError.value[FILE_REF_PROP] : undefined;
9126
- if (!maybeRef) {
8836
+ const maybePath = validationError.value && typeof validationError.value === "object" && "path" in validationError.value && typeof validationError.value.path === "string" ? validationError.value.path : undefined;
8837
+ if (!maybePath) {
9127
8838
  return null;
9128
8839
  }
9129
- return maybeRef;
8840
+ return maybePath;
9130
8841
  }
9131
8842
 
9132
- const textEncoder = new TextEncoder();
8843
+ const textEncoder$1 = new TextEncoder();
9133
8844
  async function checkRemoteRef(remoteHost, ref, projectRoot, schema, metadata) {
9134
8845
  const remoteRefDataRes = Internal.remote.splitRemoteRef(ref);
9135
8846
  if (remoteRefDataRes.status === "error") {
@@ -9159,10 +8870,10 @@ async function checkRemoteRef(remoteHost, ref, projectRoot, schema, metadata) {
9159
8870
  const fileExt = getFileExt(relativeFilePath);
9160
8871
  const currentValidationHash = Internal.remote.getValidationHash(coreVersion, schema, fileExt, metadata,
9161
8872
  // TODO: validate
9162
- fileHash, textEncoder);
8873
+ fileHash, textEncoder$1);
9163
8874
  const validationHashFromRemoteRefValues = Internal.remote.getValidationHash(remoteRefDataRes.version, schema, getFileExt(remoteRefDataRes.filePath), metadata,
9164
8875
  // TODO: validate
9165
- fileHash, textEncoder);
8876
+ fileHash, textEncoder$1);
9166
8877
  if (
9167
8878
  // Current validation hash is the same as the remote validation hash
9168
8879
  // We assume the uploaded file exists and is correct
@@ -9208,7 +8919,7 @@ async function checkRemoteRef(remoteHost, ref, projectRoot, schema, metadata) {
9208
8919
  ...metadata,
9209
8920
  ...currentMetadata
9210
8921
  };
9211
- const nextValidationHash = Internal.remote.getValidationHash(coreVersion, schema, fileExt, updatedMetadata, fileBufferRes.fileHash, textEncoder);
8922
+ const nextValidationHash = Internal.remote.getValidationHash(coreVersion, schema, fileExt, updatedMetadata, fileBufferRes.fileHash, textEncoder$1);
9212
8923
  if (!updatedMetadata.mimeType) {
9213
8924
  return {
9214
8925
  status: "error",
@@ -9338,6 +9049,36 @@ function parentPatchPathOfMediaEntry(sourcePath, entryKey) {
9338
9049
  }
9339
9050
 
9340
9051
  // TODO: find a better name? transformFixesToPatch?
9052
+ /**
9053
+ * A whole media value, for the fixes that replace one outright.
9054
+ *
9055
+ * The three remote fixes each wrote this shape out by hand, and one of them was
9056
+ * left behind when the shape changed. One name, so the next change cannot miss a
9057
+ * site.
9058
+ *
9059
+ * `path` last is deliberate: it is the field the fix is about, and a stray
9060
+ * `path` inside the metadata must not win over it.
9061
+ */
9062
+ function mediaValue(path, metadata) {
9063
+ const fields = metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata : {};
9064
+ return {
9065
+ ...fields,
9066
+ path
9067
+ };
9068
+ }
9069
+
9070
+ /**
9071
+ * The media value a validation error flagged, read as a plain record.
9072
+ *
9073
+ * A `ValidationError` carries only a value, never the schema that rejected it,
9074
+ * so the shape is all there is to go on here.
9075
+ */
9076
+ function mediaValueOf(value) {
9077
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
9078
+ return undefined;
9079
+ }
9080
+ return value;
9081
+ }
9341
9082
  async function createFixPatch(config, apply, sourcePath, validationError, remoteFiles, moduleSource, moduleSchema) {
9342
9083
  const remainingErrors = [];
9343
9084
  const patch = [];
@@ -9351,57 +9092,38 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
9351
9092
  fixes: undefined
9352
9093
  });
9353
9094
  } else if (fix === "image:check-metadata") {
9354
- const currentValue = validationError.value;
9355
- const metadataIsCorrect =
9356
- // metadata is a prop that is an object
9357
- typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
9358
- // width is correct
9359
- "width" in currentValue.metadata && currentValue.metadata.width === imageMetadata.width &&
9360
- // height is correct
9361
- "height" in currentValue.metadata && currentValue.metadata.height === imageMetadata.height &&
9362
- // mimeType is correct
9363
- "mimeType" in currentValue.metadata && currentValue.metadata.mimeType === imageMetadata.mimeType;
9364
- // skips if the metadata is already correct
9365
- if (!metadataIsCorrect) {
9095
+ const currentValue = mediaValueOf(validationError.value);
9096
+ const derived = {
9097
+ width: imageMetadata.width,
9098
+ height: imageMetadata.height,
9099
+ mimeType: imageMetadata.mimeType
9100
+ };
9101
+ // One op per field, not one for the whole object: `alt` and `hotspot`
9102
+ // are authored and sit next to these, so a whole-object write would
9103
+ // throw away what a person typed.
9104
+ const wrong = Object.entries(derived).filter(([field, expected]) => (currentValue === null || currentValue === void 0 ? void 0 : currentValue[field]) !== expected);
9105
+ if (wrong.length > 0) {
9366
9106
  if (apply) {
9367
- patch.push({
9368
- op: "replace",
9369
- path: sourceToPatchPath(sourcePath).concat("metadata"),
9370
- value: {
9371
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9372
- ...currentValue.metadata,
9373
- width: imageMetadata.width,
9374
- height: imageMetadata.height,
9375
- mimeType: imageMetadata.mimeType
9376
- }
9377
- });
9378
- } else {
9379
- if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
9380
- if (!("width" in currentValue.metadata) || currentValue.metadata.width !== imageMetadata.width) {
9381
- remainingErrors.push({
9382
- message: "Image metadata width is incorrect! Found: " + ("width" in currentValue.metadata ? currentValue.metadata.width : "<empty>") + ". Expected: " + imageMetadata.width,
9383
- fixes: undefined
9384
- });
9385
- }
9386
- if (!("height" in currentValue.metadata) || currentValue.metadata.height !== imageMetadata.height) {
9387
- remainingErrors.push({
9388
- message: "Image metadata height is incorrect! Found: " + ("height" in currentValue.metadata ? currentValue.metadata.height : "<empty>") + ". Expected: " + imageMetadata.height,
9389
- fixes: undefined
9390
- });
9391
- }
9392
- if (!("mimeType" in currentValue.metadata) || currentValue.metadata.mimeType !== imageMetadata.mimeType) {
9393
- remainingErrors.push({
9394
- message: "Image metadata mimeType is incorrect! Found: " + ("mimeType" in currentValue.metadata ? currentValue.metadata.mimeType : "<empty>") + ". Expected: " + imageMetadata.mimeType,
9395
- fixes: undefined
9396
- });
9397
- }
9398
- } else {
9107
+ for (const [field, expected] of wrong) {
9108
+ patch.push({
9109
+ op: "add",
9110
+ path: sourceToPatchPath(sourcePath).concat(field),
9111
+ value: expected
9112
+ });
9113
+ }
9114
+ } else if (currentValue) {
9115
+ for (const [field, expected] of wrong) {
9399
9116
  remainingErrors.push({
9400
- ...validationError,
9401
- message: "Image metadata is not an object!",
9117
+ message: `Image ${field} is incorrect! Found: ` + (currentValue[field] ?? "<empty>") + ". Expected: " + expected,
9402
9118
  fixes: undefined
9403
9119
  });
9404
9120
  }
9121
+ } else {
9122
+ remainingErrors.push({
9123
+ ...validationError,
9124
+ message: "Image is not an object!",
9125
+ fixes: undefined
9126
+ });
9405
9127
  }
9406
9128
  }
9407
9129
  } else if (fix === "image:add-metadata") {
@@ -9412,14 +9134,19 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
9412
9134
  fixes: undefined
9413
9135
  });
9414
9136
  } else {
9137
+ const patchPath = sourceToPatchPath(sourcePath);
9415
9138
  patch.push({
9416
9139
  op: "add",
9417
- path: sourceToPatchPath(sourcePath).concat("metadata"),
9418
- value: {
9419
- width: imageMetadata.width,
9420
- height: imageMetadata.height,
9421
- mimeType: imageMetadata.mimeType
9422
- }
9140
+ path: patchPath.concat("width"),
9141
+ value: imageMetadata.width
9142
+ }, {
9143
+ op: "add",
9144
+ path: patchPath.concat("height"),
9145
+ value: imageMetadata.height
9146
+ }, {
9147
+ op: "add",
9148
+ path: patchPath.concat("mimeType"),
9149
+ value: imageMetadata.mimeType
9423
9150
  });
9424
9151
  }
9425
9152
  }
@@ -9432,53 +9159,32 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
9432
9159
  fixes: undefined
9433
9160
  });
9434
9161
  } else if (fix === "file:check-metadata") {
9435
- const currentValue = validationError.value;
9436
- const metadataIsCorrect =
9437
- // metadata is a prop that is an object
9438
- typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
9439
- // mimeType is correct
9440
- "mimeType" in currentValue.metadata && currentValue.metadata.mimeType === fileMetadata.mimeType;
9441
-
9442
- // skips if the metadata is already correct
9443
- if (!metadataIsCorrect) {
9162
+ const currentValue = mediaValueOf(validationError.value);
9163
+ if ((currentValue === null || currentValue === void 0 ? void 0 : currentValue.mimeType) !== fileMetadata.mimeType) {
9444
9164
  if (apply) {
9445
9165
  patch.push({
9446
- op: "replace",
9447
- path: sourceToPatchPath(sourcePath).concat("metadata"),
9448
- value: {
9449
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9450
- ...currentValue.metadata,
9451
- ...(fileMetadata.mimeType ? {
9452
- mimeType: fileMetadata.mimeType
9453
- } : {})
9454
- }
9166
+ op: "add",
9167
+ path: sourceToPatchPath(sourcePath).concat("mimeType"),
9168
+ value: fileMetadata.mimeType ?? null
9169
+ });
9170
+ } else if (currentValue) {
9171
+ remainingErrors.push({
9172
+ message: "File mimeType is incorrect! Found: " + (currentValue.mimeType ?? "<empty>") + ". Expected: " + fileMetadata.mimeType,
9173
+ fixes: undefined
9455
9174
  });
9456
9175
  } else {
9457
- if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
9458
- if (!("mimeType" in currentValue.metadata) || currentValue.metadata.mimeType !== fileMetadata.mimeType) {
9459
- remainingErrors.push({
9460
- message: "File metadata mimeType is incorrect! Found: " + ("mimeType" in currentValue.metadata ? currentValue.metadata.mimeType : "<empty>") + ". Expected: " + fileMetadata.mimeType,
9461
- fixes: undefined
9462
- });
9463
- }
9464
- } else {
9465
- remainingErrors.push({
9466
- ...validationError,
9467
- message: "Image metadata is not an object!",
9468
- fixes: undefined
9469
- });
9470
- }
9176
+ remainingErrors.push({
9177
+ ...validationError,
9178
+ message: "File is not an object!",
9179
+ fixes: undefined
9180
+ });
9471
9181
  }
9472
9182
  }
9473
9183
  } else if (fix === "file:add-metadata") {
9474
9184
  patch.push({
9475
9185
  op: "add",
9476
- path: sourceToPatchPath(sourcePath).concat("metadata"),
9477
- value: {
9478
- ...(fileMetadata.mimeType ? {
9479
- mimeType: fileMetadata.mimeType
9480
- } : {})
9481
- }
9186
+ path: sourceToPatchPath(sourcePath).concat("mimeType"),
9187
+ value: fileMetadata.mimeType ?? null
9482
9188
  });
9483
9189
  }
9484
9190
  } else if (fix === "image:upload-remote" || fix === "file:upload-remote") {
@@ -9506,11 +9212,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
9506
9212
  } else {
9507
9213
  patch.push({
9508
9214
  op: "replace",
9509
- value: {
9510
- _type: "remote",
9511
- _ref: remoteFile.ref,
9512
- metadata
9513
- },
9215
+ value: mediaValue(remoteFile.ref, metadata),
9514
9216
  path: sourceToPatchPath(sourcePath)
9515
9217
  });
9516
9218
  }
@@ -9579,7 +9281,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
9579
9281
  });
9580
9282
  continue;
9581
9283
  }
9582
- const splitRemoteRefDataRes = Internal.remote.splitRemoteRef(v._ref);
9284
+ const splitRemoteRefDataRes = Internal.remote.splitRemoteRef(v.path);
9583
9285
  if (splitRemoteRefDataRes.status === "error") {
9584
9286
  remainingErrors.push({
9585
9287
  ...validationError,
@@ -9588,7 +9290,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
9588
9290
  });
9589
9291
  continue;
9590
9292
  }
9591
- const url = v._ref;
9293
+ const url = v.path;
9592
9294
  const filePath = splitRemoteRefDataRes.filePath;
9593
9295
  if (!filePath) {
9594
9296
  remainingErrors.push({
@@ -9619,20 +9321,10 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
9619
9321
  });
9620
9322
  continue;
9621
9323
  }
9622
- const value = {
9623
- [VAL_EXTENSION]: "file",
9624
- [FILE_REF_PROP]: `/${filePath}`,
9625
- ...(fix === "image:download-remote" ? {
9626
- [FILE_REF_SUBTYPE_TAG]: "image"
9627
- } : {})
9628
- };
9629
9324
  patch.push({
9630
9325
  op: "replace",
9631
9326
  path: sourceToPatchPath(sourcePath),
9632
- value: v.metadata ? {
9633
- ...value,
9634
- metadata: v.metadata
9635
- } : value
9327
+ value: mediaValue(`/${filePath}`, v.metadata)
9636
9328
  });
9637
9329
  } else if (fix === "images:check-all-files" || fix === "files:check-all-files") {
9638
9330
  if (!moduleSource || typeof moduleSource !== "object") {
@@ -9755,7 +9447,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
9755
9447
  schema: schemaAtPath
9756
9448
  } = Internal.resolvePath(modulePath, moduleSource, moduleSchema);
9757
9449
  if (schemaAtPath.type === "image" || schemaAtPath.type === "file") {
9758
- const res = await checkRemoteRef(config.remoteHost, v._ref, config.projectRoot, schemaAtPath, v.metadata);
9450
+ const res = await checkRemoteRef(config.remoteHost, v.path, config.projectRoot, schemaAtPath, v.metadata);
9759
9451
  if (res.status === "success") ; else if (res.status === "error") {
9760
9452
  remainingErrors.push({
9761
9453
  ...validationError,
@@ -9767,11 +9459,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
9767
9459
  patch.push({
9768
9460
  op: "replace",
9769
9461
  path: sourceToPatchPath(sourcePath),
9770
- value: {
9771
- _type: "remote",
9772
- _ref: res.ref,
9773
- metadata: res.metadata
9774
- }
9462
+ value: mediaValue(res.ref, res.metadata)
9775
9463
  });
9776
9464
  } else {
9777
9465
  remainingErrors.push({
@@ -9818,37 +9506,26 @@ function getRemoteValueFromValidationError(v) {
9818
9506
  message: "Unexpected error while checking remote (no value)"
9819
9507
  };
9820
9508
  }
9821
- if (typeof v.value !== "object" || v.value === null || !(FILE_REF_PROP in v.value)) {
9509
+ if (typeof v.value !== "object" || v.value === null || !("path" in v.value)) {
9822
9510
  return {
9823
9511
  success: false,
9824
- message: "Unexpected error while checking remote (no _ref in value)"
9512
+ message: "Unexpected error while checking remote (no path in value)"
9825
9513
  };
9826
9514
  }
9827
- if (typeof v.value._ref !== "string") {
9515
+ if (typeof v.value.path !== "string") {
9828
9516
  return {
9829
9517
  success: false,
9830
- message: "Unexpected error while checking remote (_ref is not a string)"
9518
+ message: "Unexpected error while checking remote (path is not a string)"
9831
9519
  };
9832
9520
  }
9833
- let metadata;
9834
- if ("metadata" in v.value && typeof v.value.metadata === "object") {
9835
- if (v.value.metadata === null) {
9836
- return {
9837
- success: false,
9838
- message: "Unexpected error while checking remote (metadata is null)"
9839
- };
9840
- }
9841
- if (Array.isArray(v.value.metadata)) {
9842
- return {
9843
- success: false,
9844
- message: "Unexpected error while checking remote (metadata is an array)"
9845
- };
9846
- }
9847
- metadata = v.value.metadata;
9848
- }
9521
+ // Everything but the path is what Val read from the bytes.
9522
+ const {
9523
+ path,
9524
+ ...metadata
9525
+ } = v.value;
9849
9526
  return {
9850
9527
  success: true,
9851
- _ref: v.value._ref,
9528
+ path,
9852
9529
  metadata
9853
9530
  };
9854
9531
  }
@@ -9873,29 +9550,655 @@ async function getFileMetadata(projectRoot, validationError) {
9873
9550
  return extractFileMetadata(fileRef);
9874
9551
  }
9875
9552
 
9876
- function validateMetadata(actualMetadata, expectedMetadata) {
9877
- const missingMetadata = [];
9878
- const erroneousMetadata = {};
9879
- if (typeof actualMetadata !== "object" || actualMetadata === null) {
9880
- return {
9881
- globalErrors: ["Metadata is wrong type: must be an object."]
9882
- };
9553
+ /**
9554
+ * Moves ONE `.jsonValues()` entry that was written inline in the `.val.ts` into
9555
+ * its own `*.val.json`, replacing the inline value with
9556
+ * `c.json(() => import("./<key>.val.json"))`.
9557
+ *
9558
+ * This is the fix for the `jsonValues:extract-entry` validation error. It is not
9559
+ * expressible as a patch (a patch edits one `.val.ts` and cannot create the
9560
+ * backing JSON file), so it writes both files directly — JSON first, so a
9561
+ * failure part-way through never leaves the module pointing at a file that does
9562
+ * not exist.
9563
+ *
9564
+ * Root-only, like the rest of the `.jsonValues()` machinery: the entry is looked
9565
+ * up in the module's root record/router object literal.
9566
+ */
9567
+ function extractJsonValuesEntry(moduleFilePath, rootDir, entryKey, content, sourceFileHandler) {
9568
+ const valTsPath = sourceFileHandler.resolveSourceModulePath(getSyntheticContainingPath(rootDir), `.${moduleFilePath.replace(".val.ts", ".val").replace(".val.js", ".val").replace(".val.jsx", ".val").replace(".val.tsx", ".val")}`);
9569
+ const sourceFile = sourceFileHandler.getSourceFile(valTsPath);
9570
+ if (!sourceFile) {
9571
+ throw Error(`Source file ${valTsPath} not found`);
9883
9572
  }
9884
- if (Array.isArray(actualMetadata)) {
9885
- return {
9886
- globalErrors: ["Metadata is wrong type: cannot be an array."]
9887
- };
9573
+ const pathsRes = getNewJsonEntryPaths(moduleFilePath, entryKey);
9574
+ if (result.isErr(pathsRes)) {
9575
+ throw Error(formatPatchSourceError(pathsRes.error));
9888
9576
  }
9889
- const recordMetadata = actualMetadata;
9890
- const globalErrors = [];
9891
- for (const anyKey in expectedMetadata) {
9892
- if (typeof anyKey !== "string") {
9893
- globalErrors.push(`Expected metadata has key '${anyKey}' that is not typeof 'string', but: '${typeof anyKey}'. This is most likely a Val bug.`);
9894
- } else {
9895
- if (anyKey in actualMetadata) {
9896
- const key = anyKey;
9897
- if (expectedMetadata[key] !== recordMetadata[key]) {
9898
- erroneousMetadata[key] = `Expected metadata '${key}' to be ${JSON.stringify(expectedMetadata[key])}, but got ${JSON.stringify(recordMetadata[key])}.`;
9577
+ const {
9578
+ jsonPath,
9579
+ importPath
9580
+ } = pathsRes.value;
9581
+ const absoluteJsonPath = path__default.join(rootDir, jsonPath);
9582
+ if (sourceFileHandler.host.fileExists(absoluteJsonPath)) {
9583
+ throw Error(`Cannot extract .jsonValues() entry '${entryKey}' of ${moduleFilePath}: '${jsonPath}' already exists`);
9584
+ }
9585
+
9586
+ // Remove the inline property, then add the `c.json(...)` reference back. The
9587
+ // entry moves to the end of the record: entry ORDER in a jsonValues record is
9588
+ // not meaningful (the Studio and the runtime key entries by name), and
9589
+ // insert-in-place would mean reimplementing insertValJsonEntry.
9590
+ const removed = removeValJsonEntry(sourceFile, [], entryKey);
9591
+ if (result.isErr(removed)) {
9592
+ throw Error(`${valTsPath}\n${formatOpsError(removed.error, sourceFile)}`);
9593
+ }
9594
+ const inserted = insertValJsonEntry(removed.value, [], entryKey, importPath);
9595
+ if (result.isErr(inserted)) {
9596
+ throw Error(`${valTsPath}\n${formatOpsError(inserted.error, removed.value)}`);
9597
+ }
9598
+ sourceFileHandler.writeFile(absoluteJsonPath, JSON.stringify(content, null, 2) + "\n", "utf8");
9599
+ sourceFileHandler.writeSourceFile(inserted.value);
9600
+ }
9601
+ function formatOpsError(error, sourceFile) {
9602
+ if (error instanceof PatchError) {
9603
+ return error.message;
9604
+ }
9605
+ return flatMapErrors(error, e => formatSyntaxError(e, sourceFile)).join("\n");
9606
+ }
9607
+
9608
+ /**
9609
+ * The fix handlers behind `val validate --fix`, and the editor quick fixes that
9610
+ * must agree with it.
9611
+ *
9612
+ * ## Why this lives in `@valbuild/server`
9613
+ *
9614
+ * A fix is two layers. `createFixPatch` is the second: given a validation
9615
+ * error it produces a patch. The first is everything that has to happen before a
9616
+ * patch can be produced — read the file, check it is actually on disk, extract
9617
+ * metadata, pick a remote bucket, upload the bytes, download them back. That
9618
+ * layer used to live in `packages/cli/src/runValidation.ts`, which made it
9619
+ * reachable only from the CLI: `@valbuild/cli` exports nothing but `./cli`, and
9620
+ * `@valbuild/language-server` cannot depend on it anyway without creating a
9621
+ * cycle (the CLI depends on the language server).
9622
+ *
9623
+ * The consequence was that an editor could offer the fixes needing no
9624
+ * precondition and had to reimplement or skip the rest. Both happened: the
9625
+ * VS Code extension grew its own remote-upload client, and the language server
9626
+ * declined to offer remote fixes at all.
9627
+ *
9628
+ * So the layer sits here, next to `createFixPatch`, and the CLI and the language
9629
+ * server are both callers. {@link FixHandlerContext} is deliberately made of
9630
+ * things any caller already has — a `Service`, an `IValFSHost`, a project root —
9631
+ * rather than of CLI concepts.
9632
+ *
9633
+ * What stays in the CLI is the driver: the `runValidation` async generator that
9634
+ * walks every module, decides what to report, and renders it to a terminal.
9635
+ */
9636
+ const textEncoder = new TextEncoder();
9637
+
9638
+ // Types for handler system
9639
+
9640
+ // Handler functions
9641
+ async function handleFileMetadata(ctx) {
9642
+ var _fileSource$source;
9643
+ const [, modulePath] = Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
9644
+ if (!ctx.valModule.source || !ctx.valModule.schema) {
9645
+ return {
9646
+ success: false,
9647
+ errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
9648
+ };
9649
+ }
9650
+ const fileSource = Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
9651
+
9652
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9653
+ const fileRefProp = (_fileSource$source = fileSource.source) === null || _fileSource$source === void 0 ? void 0 : _fileSource$source.path;
9654
+ if (!fileRefProp) {
9655
+ return {
9656
+ success: false,
9657
+ errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
9658
+ };
9659
+ }
9660
+ const filePath = path__default.join(ctx.projectRoot, fileRefProp);
9661
+ if (!ctx.fs.fileExists(filePath)) {
9662
+ return {
9663
+ success: false,
9664
+ errorMessage: `File ${filePath} does not exist`
9665
+ };
9666
+ }
9667
+ return {
9668
+ success: true,
9669
+ shouldApplyPatch: true
9670
+ };
9671
+ }
9672
+
9673
+ // Shared upload core used by both the single-field (handleRemoteFileUpload)
9674
+ // and gallery (handleRemoteGalleryFileUpload) handlers. The two differ only in
9675
+ // how they derive the local file ref, metadata and serialized image/file
9676
+ // schema; everything from auth through upload is identical.
9677
+ async function uploadRemoteFileCore(ctx, fileRef, metadata, schema) {
9678
+ const filePath = path__default.join(ctx.projectRoot, fileRef);
9679
+ if (!ctx.fs.fileExists(filePath)) {
9680
+ return {
9681
+ success: false,
9682
+ errorMessage: `File ${filePath} does not exist`
9683
+ };
9684
+ }
9685
+ const patFile = getPersonalAccessTokenPath(ctx.projectRoot);
9686
+ if (!ctx.fs.fileExists(patFile)) {
9687
+ return {
9688
+ success: false,
9689
+ errorMessage: `File: ${path__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`
9690
+ };
9691
+ }
9692
+ const patFileContent = ctx.fs.readFile(patFile);
9693
+ if (patFileContent === undefined) {
9694
+ return {
9695
+ success: false,
9696
+ errorMessage: `Could not read personal access token file at ${patFile}`
9697
+ };
9698
+ }
9699
+ const parsedPatFile = parsePersonalAccessTokenFile(patFileContent);
9700
+ if (!parsedPatFile.success) {
9701
+ return {
9702
+ success: false,
9703
+ errorMessage: `Error parsing personal access token file: ${parsedPatFile.error}. You need to login again.`
9704
+ };
9705
+ }
9706
+ const {
9707
+ pat
9708
+ } = parsedPatFile.data;
9709
+ if (ctx.remoteFiles[ctx.sourcePath]) {
9710
+ return {
9711
+ success: true,
9712
+ events: [{
9713
+ type: "remote-already-uploaded",
9714
+ filePath
9715
+ }]
9716
+ };
9717
+ }
9718
+ const projectName = ctx.project;
9719
+ let publicProjectId = ctx.publicProjectId;
9720
+ let remoteFileBuckets = ctx.remoteFileBuckets;
9721
+ let remoteFilesCounter = ctx.remoteFilesCounter;
9722
+ if (!publicProjectId || !remoteFileBuckets) {
9723
+ if (!projectName) {
9724
+ return {
9725
+ success: false,
9726
+ errorMessage: "Project name not found. Add project name to val.config or set the VAL_PROJECT environment variable"
9727
+ };
9728
+ }
9729
+ const settingsRes = await ctx.remote.getSettings(projectName, {
9730
+ pat
9731
+ });
9732
+ if (!settingsRes.success) {
9733
+ return {
9734
+ success: false,
9735
+ errorMessage: `Could not get public project id: ${settingsRes.message}.`
9736
+ };
9737
+ }
9738
+ publicProjectId = settingsRes.data.publicProjectId;
9739
+ remoteFileBuckets = settingsRes.data.remoteFileBuckets.map(b => b.bucket);
9740
+ }
9741
+ if (!publicProjectId) {
9742
+ return {
9743
+ success: false,
9744
+ errorMessage: "Could not get public project id"
9745
+ };
9746
+ }
9747
+ if (!projectName) {
9748
+ return {
9749
+ success: false,
9750
+ errorMessage: `Could not get project. Check that your val.config has the 'project' field set, or set it using the VAL_PROJECT environment variable`
9751
+ };
9752
+ }
9753
+ remoteFilesCounter += 1;
9754
+ const bucket = remoteFileBuckets[remoteFilesCounter % remoteFileBuckets.length];
9755
+ if (!bucket) {
9756
+ return {
9757
+ success: false,
9758
+ errorMessage: `Internal error: could not allocate a bucket for the remote file located at ${ctx.sourcePath}`
9759
+ };
9760
+ }
9761
+ const fileBuffer = ctx.fs.readBuffer(filePath);
9762
+ if (fileBuffer === undefined) {
9763
+ return {
9764
+ success: false,
9765
+ errorMessage: `Error reading file: ${filePath}`
9766
+ };
9767
+ }
9768
+ const relativeFilePath = path__default.relative(ctx.projectRoot, filePath).split(path__default.sep).join("/");
9769
+ if (!relativeFilePath.startsWith("public/")) {
9770
+ return {
9771
+ success: false,
9772
+ errorMessage: `File path must be within the public/ directory (e.g. public/path/to/file.txt). Got: ${relativeFilePath}`
9773
+ };
9774
+ }
9775
+ const fileHash = Internal.remote.getFileHash(fileBuffer);
9776
+ const coreVersion = Internal.VERSION.core || "unknown";
9777
+ const fileExt = getFileExt(filePath);
9778
+ const ref = Internal.remote.createRemoteRef(ctx.remote.remoteHost, {
9779
+ publicProjectId,
9780
+ coreVersion,
9781
+ bucket,
9782
+ validationHash: Internal.remote.getValidationHash(coreVersion, schema, fileExt, metadata, fileHash, textEncoder),
9783
+ fileHash,
9784
+ filePath: relativeFilePath
9785
+ });
9786
+ const remoteFileUpload = await ctx.remote.uploadFile(projectName, bucket, fileHash, fileExt, fileBuffer, {
9787
+ pat
9788
+ });
9789
+ if (!remoteFileUpload.success) {
9790
+ return {
9791
+ success: false,
9792
+ errorMessage: `Could not upload remote file: '${ref}'. Error: ${remoteFileUpload.error}`
9793
+ };
9794
+ }
9795
+ ctx.remoteFiles[ctx.sourcePath] = {
9796
+ ref,
9797
+ metadata
9798
+ };
9799
+ return {
9800
+ success: true,
9801
+ shouldApplyPatch: true,
9802
+ publicProjectId,
9803
+ remoteFileBuckets,
9804
+ remoteFilesCounter,
9805
+ events: [{
9806
+ type: "remote-uploading",
9807
+ ref
9808
+ }, {
9809
+ type: "remote-uploaded",
9810
+ ref
9811
+ }]
9812
+ };
9813
+ }
9814
+ async function handleRemoteFileUpload(ctx) {
9815
+ var _resolvedRemoteFileAt;
9816
+ if (!ctx.fix) {
9817
+ return {
9818
+ success: false,
9819
+ // No sourcePath in the message: the reported location already points at it.
9820
+ errorMessage: `Remote file needs to be uploaded (use --fix to upload)`
9821
+ };
9822
+ }
9823
+ const [, modulePath] = Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
9824
+ if (!ctx.valModule.source || !ctx.valModule.schema) {
9825
+ return {
9826
+ success: false,
9827
+ errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
9828
+ };
9829
+ }
9830
+ const resolvedRemoteFileAtSourcePath = Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
9831
+
9832
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9833
+ const fileRefProp = (_resolvedRemoteFileAt = resolvedRemoteFileAtSourcePath.source) === null || _resolvedRemoteFileAt === void 0 ? void 0 : _resolvedRemoteFileAt.path;
9834
+ if (!fileRefProp) {
9835
+ return {
9836
+ success: false,
9837
+ errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
9838
+ };
9839
+ }
9840
+ const resolveRemoteFileSchema = resolvedRemoteFileAtSourcePath.schema;
9841
+ if (!resolveRemoteFileSchema) {
9842
+ return {
9843
+ success: false,
9844
+ errorMessage: `Cannot upload remote file: schema not found for ${ctx.sourcePath}`
9845
+ };
9846
+ }
9847
+ if (resolveRemoteFileSchema.type !== "image" && resolveRemoteFileSchema.type !== "file") {
9848
+ return {
9849
+ success: false,
9850
+ errorMessage: `The schema is the remote is neither image nor file: ${ctx.sourcePath}`
9851
+ };
9852
+ }
9853
+
9854
+ // The metadata is everything the media source carries besides its path: the
9855
+ // schema above already established that this IS media.
9856
+ const actualRemoteFileSource = resolvedRemoteFileAtSourcePath.source;
9857
+ const fileSourceMetadata = actualRemoteFileSource && typeof actualRemoteFileSource === "object" && "path" in actualRemoteFileSource ? (() => {
9858
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
9859
+ const {
9860
+ path: _path,
9861
+ ...metadata
9862
+ } = actualRemoteFileSource;
9863
+ return metadata;
9864
+ })() : undefined;
9865
+ return uploadRemoteFileCore(ctx, fileRefProp, fileSourceMetadata, resolveRemoteFileSchema);
9866
+ }
9867
+
9868
+ // Gallery (s.images({ remote: true }) / s.files({ remote: true })) upload.
9869
+ // Unlike a single image/file field, a gallery entry is keyed by its local file
9870
+ // path and the value is bare metadata (no FileSource), so we derive the file
9871
+ // ref from the key and synthesize the image/file schema from the record's
9872
+ // media options.
9873
+ async function handleRemoteGalleryFileUpload(ctx) {
9874
+ var _ctx$validationError$;
9875
+ if (!ctx.fix) {
9876
+ return {
9877
+ success: false,
9878
+ // No sourcePath in the message: the reported location already points at it.
9879
+ errorMessage: `Remote file needs to be uploaded (use --fix to upload)`
9880
+ };
9881
+ }
9882
+ const fix = (_ctx$validationError$ = ctx.validationError.fixes) === null || _ctx$validationError$ === void 0 ? void 0 : _ctx$validationError$[0];
9883
+ const mediaType = fix === "files:upload-remote" ? "file" : "image";
9884
+
9885
+ // The gallery entry is keyed by its local file path; validateMediaKey carries
9886
+ // that key as the error value.
9887
+ const fileRef = ctx.validationError.value;
9888
+ if (typeof fileRef !== "string") {
9889
+ return {
9890
+ success: false,
9891
+ errorMessage: `Expected a local file path for gallery entry at ${ctx.sourcePath}`
9892
+ };
9893
+ }
9894
+ const [, modulePath] = Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
9895
+ if (!ctx.valModule.source || !ctx.valModule.schema) {
9896
+ return {
9897
+ success: false,
9898
+ errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
9899
+ };
9900
+ }
9901
+ const resolved = Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
9902
+ const entrySource = resolved.source;
9903
+ const metadata = entrySource && typeof entrySource === "object" && !Array.isArray(entrySource) ? entrySource : undefined;
9904
+
9905
+ // The gallery item schema is an ObjectSchema, not an image/file schema, so
9906
+ // synthesize the serialized image/file schema (matching how single fields
9907
+ // serialize) for the remote ref's validation hash.
9908
+ //
9909
+ // `accept`/`directory` come from the RECORD that holds the entry, resolved
9910
+ // from the entry's PARENT path - not from the module root, which is only the
9911
+ // record when the gallery is the whole module. A nested gallery
9912
+ // (s.object({ gallery: s.images(...) })) would otherwise synthesize a schema
9913
+ // with no options and bake a validation hash into the remote ref that can
9914
+ // never validate, so a mismatch fails fast instead of uploading.
9915
+ const [, parentModulePath] = Internal.splitModuleFilePathAndModulePath(Internal.parentOfSourcePath(ctx.sourcePath));
9916
+ const recordSchema = Internal.resolvePath(parentModulePath, ctx.valModule.source, ctx.valModule.schema).schema;
9917
+ if ((recordSchema === null || recordSchema === void 0 ? void 0 : recordSchema.type) !== "record") {
9918
+ return {
9919
+ success: false,
9920
+ errorMessage: `Expected a gallery record at the parent of ${ctx.sourcePath}, got ${(recordSchema === null || recordSchema === void 0 ? void 0 : recordSchema.type) ?? "nothing"}`
9921
+ };
9922
+ }
9923
+ const {
9924
+ accept,
9925
+ directory
9926
+ } = recordSchema;
9927
+ const schema = mediaType === "image" ? {
9928
+ type: "image",
9929
+ opt: false,
9930
+ options: {
9931
+ ...(accept ? {
9932
+ accept
9933
+ } : {}),
9934
+ ...(directory ? {
9935
+ directory
9936
+ } : {})
9937
+ }
9938
+ } : {
9939
+ type: "file",
9940
+ opt: false,
9941
+ options: {
9942
+ ...(accept ? {
9943
+ accept
9944
+ } : {})
9945
+ }
9946
+ };
9947
+ return uploadRemoteFileCore(ctx, fileRef, metadata, schema);
9948
+ }
9949
+ async function handleRemoteFileDownload(ctx) {
9950
+ if (ctx.fix) {
9951
+ return {
9952
+ success: true,
9953
+ shouldApplyPatch: true,
9954
+ events: [{
9955
+ type: "remote-downloading",
9956
+ sourcePath: ctx.sourcePath
9957
+ }]
9958
+ };
9959
+ } else {
9960
+ return {
9961
+ success: false,
9962
+ errorMessage: `Remote file ${ctx.sourcePath} needs to be downloaded (use --fix to download)`
9963
+ };
9964
+ }
9965
+ }
9966
+ async function handleRemoteFileCheck() {
9967
+ // Skip - no action needed
9968
+ return {
9969
+ success: true,
9970
+ shouldApplyPatch: true
9971
+ };
9972
+ }
9973
+ async function handleUniqueFolderCheck(ctx) {
9974
+ const value = ctx.validationError.value;
9975
+ if (!value || typeof value.directory !== "string") {
9976
+ return {
9977
+ success: false,
9978
+ errorMessage: `Unexpected value in unique folder check for ${ctx.sourcePath}`
9979
+ };
9980
+ }
9981
+ const {
9982
+ directory
9983
+ } = value;
9984
+ const conflicts = [];
9985
+ for (const file of ctx.valFiles) {
9986
+ const otherModuleFilePath = `/${file}`;
9987
+ if (otherModuleFilePath === ctx.moduleFilePath) continue;
9988
+ const otherModule = await ctx.service.get(otherModuleFilePath, "", {
9989
+ validate: false
9990
+ });
9991
+ const schema = otherModule.schema;
9992
+ if ((schema === null || schema === void 0 ? void 0 : schema.type) === "record" && schema.directory === directory && schema.mediaType) {
9993
+ conflicts.push(otherModuleFilePath);
9994
+ }
9995
+ }
9996
+ if (conflicts.length > 0) {
9997
+ return {
9998
+ success: false,
9999
+ errorMessage: `Gallery directory '${directory}' in ${ctx.moduleFilePath} is also used by: ${conflicts.join(", ")}. Each gallery must use a unique directory.`
10000
+ };
10001
+ }
10002
+ return {
10003
+ success: true
10004
+ };
10005
+ }
10006
+
10007
+ // Maps a gallery key to its on-disk local path. Remote galleries key uploaded
10008
+ // entries by a remote URL while keeping the file on disk; everything else is
10009
+ // already a local path and is returned unchanged.
10010
+ function remoteKeyToLocalPath(key) {
10011
+ const remoteRefRes = Internal.remote.splitRemoteRef(key);
10012
+ if (remoteRefRes.status === "success") {
10013
+ return `/${remoteRefRes.filePath}`;
10014
+ }
10015
+ return key;
10016
+ }
10017
+ async function handleCheckAllFiles(ctx) {
10018
+ const value = ctx.validationError.value;
10019
+ if (!value || typeof value.directory !== "string") {
10020
+ return {
10021
+ success: false,
10022
+ errorMessage: `Unexpected value in check-all-files for ${ctx.sourcePath}`
10023
+ };
10024
+ }
10025
+ const {
10026
+ directory
10027
+ } = value;
10028
+ const source = ctx.valModule.source;
10029
+ if (!source || typeof source !== "object" || Array.isArray(source)) {
10030
+ return {
10031
+ success: false,
10032
+ errorMessage: `Could not get source for ${ctx.sourcePath}`
10033
+ };
10034
+ }
10035
+ // Gallery entries are keyed by their file path. Remote galleries key uploaded
10036
+ // entries by a remote URL, but the file is kept on disk at its local path, so
10037
+ // normalize remote-URL keys back to that local path for the on-disk checks.
10038
+ const trackedFiles = new Set(Object.keys(source).map(remoteKeyToLocalPath));
10039
+
10040
+ // Check that all tracked files exist on disk
10041
+ const missingTrackedFiles = Array.from(trackedFiles).filter(f => {
10042
+ return !ctx.fs.fileExists(path__default.join(ctx.projectRoot, f));
10043
+ });
10044
+ if (missingTrackedFiles.length > 0) {
10045
+ if (!ctx.fix) {
10046
+ return {
10047
+ success: false,
10048
+ 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.`
10049
+ };
10050
+ }
10051
+ // fix: true — let createFixPatch remove the missing entries
10052
+ return {
10053
+ success: true,
10054
+ shouldApplyPatch: true
10055
+ };
10056
+ }
10057
+ const dirPath = path__default.join(ctx.projectRoot, directory);
10058
+ const filesInDir = [];
10059
+ try {
10060
+ const entries = ctx.fs.readDirectory(dirPath, undefined, undefined, ["**/*"]);
10061
+ for (const entry of entries) {
10062
+ const relPath = "/" + path__default.relative(ctx.projectRoot, entry).split(path__default.sep).join("/");
10063
+ filesInDir.push(relPath);
10064
+ }
10065
+ } catch {
10066
+ // directory doesn't exist — no untracked files possible
10067
+ }
10068
+ const untrackedFiles = filesInDir.filter(f => !trackedFiles.has(f));
10069
+ if (untrackedFiles.length > 0) {
10070
+ return {
10071
+ success: false,
10072
+ errorMessage: `Gallery in ${ctx.moduleFilePath} has files not tracked: ${untrackedFiles.join(", ")}. Add these files to the gallery or remove them from the directory.`
10073
+ };
10074
+ }
10075
+
10076
+ // All files accounted for — trigger metadata verification via createFixPatch
10077
+ return {
10078
+ success: true,
10079
+ shouldApplyPatch: true
10080
+ };
10081
+ }
10082
+ async function handleJsonValuesExtractEntry(ctx) {
10083
+ const [, modulePath] = Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
10084
+ const parts = Internal.splitModulePath(modulePath);
10085
+ // Root-only by contract (see findNestedJsonValuesRecords): the entry is a
10086
+ // direct child of the module's root record/router, so the path is one segment.
10087
+ if (parts.length !== 1) {
10088
+ return {
10089
+ success: false,
10090
+ errorMessage: `Cannot extract .jsonValues() entry at ${ctx.sourcePath}: expected a root record entry`
10091
+ };
10092
+ }
10093
+ const entryKey = parts[0];
10094
+ const source = ctx.valModule.source;
10095
+ if (source === null || typeof source !== "object" || Array.isArray(source)) {
10096
+ return {
10097
+ success: false,
10098
+ errorMessage: `Could not get source for ${ctx.moduleFilePath}`
10099
+ };
10100
+ }
10101
+ const content = source[entryKey];
10102
+ if (content === undefined) {
10103
+ return {
10104
+ success: false,
10105
+ errorMessage: `Could not find .jsonValues() entry '${entryKey}' in ${ctx.moduleFilePath}`
10106
+ };
10107
+ }
10108
+ if (!ctx.fix) {
10109
+ return {
10110
+ success: true,
10111
+ fixableErrorMessage: ctx.validationError.message
10112
+ };
10113
+ }
10114
+ try {
10115
+ extractJsonValuesEntry(ctx.moduleFilePath, ctx.projectRoot, entryKey, content, ctx.service.sourceFileHandler);
10116
+ } catch (err) {
10117
+ return {
10118
+ success: false,
10119
+ errorMessage: err instanceof Error ? err.message : String(err)
10120
+ };
10121
+ }
10122
+ return {
10123
+ success: true,
10124
+ appliedFix: true
10125
+ };
10126
+ }
10127
+
10128
+ // Fix handler registry. `keyof:check-keys` and `router:check-route` are
10129
+ // resolved upfront by the shared resolveSchemaSourceFixes — they never reach
10130
+ // this registry, so they're excluded from the key set.
10131
+ const currentFixHandlers = {
10132
+ "image:check-metadata": handleFileMetadata,
10133
+ "image:add-metadata": handleFileMetadata,
10134
+ "file:check-metadata": handleFileMetadata,
10135
+ "file:add-metadata": handleFileMetadata,
10136
+ "image:upload-remote": handleRemoteFileUpload,
10137
+ "file:upload-remote": handleRemoteFileUpload,
10138
+ "images:upload-remote": handleRemoteGalleryFileUpload,
10139
+ "files:upload-remote": handleRemoteGalleryFileUpload,
10140
+ "image:download-remote": handleRemoteFileDownload,
10141
+ "file:download-remote": handleRemoteFileDownload,
10142
+ "image:check-remote": handleRemoteFileCheck,
10143
+ "images:check-remote": handleRemoteFileCheck,
10144
+ "file:check-remote": handleRemoteFileCheck,
10145
+ "files:check-remote": handleRemoteFileCheck,
10146
+ "images:check-unique-folder": handleUniqueFolderCheck,
10147
+ "files:check-unique-folder": handleUniqueFolderCheck,
10148
+ "images:check-all-files": handleCheckAllFiles,
10149
+ "files:check-all-files": handleCheckAllFiles,
10150
+ "jsonValues:extract-entry": handleJsonValuesExtractEntry
10151
+ };
10152
+ const deprecatedFixHandlers = {
10153
+ "image:replace-metadata": handleFileMetadata
10154
+ };
10155
+ const fixHandlers = {
10156
+ ...deprecatedFixHandlers,
10157
+ ...currentFixHandlers
10158
+ };
10159
+ function createDefaultValFSHost() {
10160
+ return {
10161
+ ...ts.sys,
10162
+ writeFile: (fileName, data, encoding) => {
10163
+ fs.mkdirSync(path__default.dirname(fileName), {
10164
+ recursive: true
10165
+ });
10166
+ fs.writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
10167
+ },
10168
+ rmFile: fs.rmSync,
10169
+ readBuffer: fileName => {
10170
+ try {
10171
+ return fs.readFileSync(fileName);
10172
+ } catch {
10173
+ return undefined;
10174
+ }
10175
+ }
10176
+ };
10177
+ }
10178
+
10179
+ function validateMetadata(actualMetadata, expectedMetadata) {
10180
+ const missingMetadata = [];
10181
+ const erroneousMetadata = {};
10182
+ if (typeof actualMetadata !== "object" || actualMetadata === null) {
10183
+ return {
10184
+ globalErrors: ["Metadata is wrong type: must be an object."]
10185
+ };
10186
+ }
10187
+ if (Array.isArray(actualMetadata)) {
10188
+ return {
10189
+ globalErrors: ["Metadata is wrong type: cannot be an array."]
10190
+ };
10191
+ }
10192
+ const recordMetadata = actualMetadata;
10193
+ const globalErrors = [];
10194
+ for (const anyKey in expectedMetadata) {
10195
+ if (typeof anyKey !== "string") {
10196
+ globalErrors.push(`Expected metadata has key '${anyKey}' that is not typeof 'string', but: '${typeof anyKey}'. This is most likely a Val bug.`);
10197
+ } else {
10198
+ if (anyKey in actualMetadata) {
10199
+ const key = anyKey;
10200
+ if (expectedMetadata[key] !== recordMetadata[key]) {
10201
+ erroneousMetadata[key] = `Expected metadata '${key}' to be ${JSON.stringify(expectedMetadata[key])}, but got ${JSON.stringify(recordMetadata[key])}.`;
9899
10202
  }
9900
10203
  } else {
9901
10204
  missingMetadata.push(anyKey);
@@ -10165,9 +10468,9 @@ function getModulePathRange(modulePath, modulePathMap,
10165
10468
  // Which part of an object/record member to point at. For an object property
10166
10469
  // the resolved node's own range is the *key* (property name); the *value*
10167
10470
  // range is stored under `children.val`. Array elements, leaf literals and
10168
- // `c.image`/`c.file` `_ref`/`metadata` nodes have no `val` child, so "value"
10169
- // falls back to the node's own range for those. Defaults to "key" to preserve
10170
- // existing callers.
10471
+ // Array elements and leaf literals have no `val` child, so "value" falls back
10472
+ // to the node's own range for those. Defaults to "key" to preserve existing
10473
+ // callers.
10171
10474
  target = "key") {
10172
10475
  var _range$children;
10173
10476
  // Handle invalid module paths gracefully
@@ -10220,7 +10523,7 @@ target = "key") {
10220
10523
  *
10221
10524
  * NOTE: do not compute the start as `end.character - node.getWidth()`. That
10222
10525
  * identity only holds while the node stays on a single line - for a multi-line
10223
- * node (an object inside an array, a `c.image` metadata argument, ...) it
10526
+ * node (an object inside an array, a media object's `path`, ...) it
10224
10527
  * reports the *closing* line and a negative character.
10225
10528
  */
10226
10529
  function rangeOf(node, sourceFile) {
@@ -10284,39 +10587,6 @@ function traverse(node, sourceFile) {
10284
10587
  if (ts.isArrayLiteralExpression(node)) {
10285
10588
  return traverseArrayLiteral(node, sourceFile);
10286
10589
  }
10287
- if (ts.isCallExpression(node)) {
10288
- return traverseCallExpression(node, sourceFile);
10289
- }
10290
- }
10291
- function traverseCallExpression(node, sourceFile) {
10292
- if (ts.isPropertyAccessExpression(node.expression)) {
10293
- if (node.expression.expression.getText(sourceFile) === "c" && (node.expression.name.getText(sourceFile) === "file" || node.expression.name.getText(sourceFile) === "image")) {
10294
- const val = {
10295
- children: {},
10296
- ...rangeOf(node, sourceFile)
10297
- };
10298
- if (node.arguments[0]) {
10299
- const _ref = {
10300
- children: {},
10301
- ...rangeOf(node.arguments[0], sourceFile)
10302
- };
10303
- if (!node.arguments[1]) {
10304
- return {
10305
- val,
10306
- _ref
10307
- };
10308
- }
10309
- return {
10310
- val,
10311
- _ref,
10312
- metadata: {
10313
- children: {},
10314
- ...rangeOf(node.arguments[1], sourceFile)
10315
- }
10316
- };
10317
- }
10318
- }
10319
- }
10320
10590
  }
10321
10591
  function traverseArrayLiteral(node, sourceFile) {
10322
10592
  return node.elements.reduce((acc, element, index) => {
@@ -10390,61 +10660,6 @@ function findJsonEntryFilePath(moduleFilePath, valTsSourceFile, entryKey) {
10390
10660
  return resolveExistingJsonPath(moduleFilePath, entry.importPath);
10391
10661
  }
10392
10662
 
10393
- /**
10394
- * Moves ONE `.jsonValues()` entry that was written inline in the `.val.ts` into
10395
- * its own `*.val.json`, replacing the inline value with
10396
- * `c.json(() => import("./<key>.val.json"))`.
10397
- *
10398
- * This is the fix for the `jsonValues:extract-entry` validation error. It is not
10399
- * expressible as a patch (a patch edits one `.val.ts` and cannot create the
10400
- * backing JSON file), so it writes both files directly — JSON first, so a
10401
- * failure part-way through never leaves the module pointing at a file that does
10402
- * not exist.
10403
- *
10404
- * Root-only, like the rest of the `.jsonValues()` machinery: the entry is looked
10405
- * up in the module's root record/router object literal.
10406
- */
10407
- function extractJsonValuesEntry(moduleFilePath, rootDir, entryKey, content, sourceFileHandler) {
10408
- const valTsPath = sourceFileHandler.resolveSourceModulePath(getSyntheticContainingPath(rootDir), `.${moduleFilePath.replace(".val.ts", ".val").replace(".val.js", ".val").replace(".val.jsx", ".val").replace(".val.tsx", ".val")}`);
10409
- const sourceFile = sourceFileHandler.getSourceFile(valTsPath);
10410
- if (!sourceFile) {
10411
- throw Error(`Source file ${valTsPath} not found`);
10412
- }
10413
- const pathsRes = getNewJsonEntryPaths(moduleFilePath, entryKey);
10414
- if (result.isErr(pathsRes)) {
10415
- throw Error(formatPatchSourceError(pathsRes.error));
10416
- }
10417
- const {
10418
- jsonPath,
10419
- importPath
10420
- } = pathsRes.value;
10421
- const absoluteJsonPath = path__default.join(rootDir, jsonPath);
10422
- if (sourceFileHandler.host.fileExists(absoluteJsonPath)) {
10423
- throw Error(`Cannot extract .jsonValues() entry '${entryKey}' of ${moduleFilePath}: '${jsonPath}' already exists`);
10424
- }
10425
-
10426
- // Remove the inline property, then add the `c.json(...)` reference back. The
10427
- // entry moves to the end of the record: entry ORDER in a jsonValues record is
10428
- // not meaningful (the Studio and the runtime key entries by name), and
10429
- // insert-in-place would mean reimplementing insertValJsonEntry.
10430
- const removed = removeValJsonEntry(sourceFile, [], entryKey);
10431
- if (result.isErr(removed)) {
10432
- throw Error(`${valTsPath}\n${formatOpsError(removed.error, sourceFile)}`);
10433
- }
10434
- const inserted = insertValJsonEntry(removed.value, [], entryKey, importPath);
10435
- if (result.isErr(inserted)) {
10436
- throw Error(`${valTsPath}\n${formatOpsError(inserted.error, removed.value)}`);
10437
- }
10438
- sourceFileHandler.writeFile(absoluteJsonPath, JSON.stringify(content, null, 2) + "\n", "utf8");
10439
- sourceFileHandler.writeSourceFile(inserted.value);
10440
- }
10441
- function formatOpsError(error, sourceFile) {
10442
- if (error instanceof PatchError) {
10443
- return error.message;
10444
- }
10445
- return flatMapErrors(error, e => formatSyntaxError(e, sourceFile)).join("\n");
10446
- }
10447
-
10448
10663
  /**
10449
10664
  * Replays a `val debug` snapshot: applies its patches the way /save does and
10450
10665
  * validates the result.
@@ -10544,4 +10759,4 @@ function readCapturedReport(snapshotDir) {
10544
10759
  return JSON.parse(fs.readFileSync(reportPath, "utf-8"));
10545
10760
  }
10546
10761
 
10547
- export { DEFAULT_LOGIN_HOST, DEFAULT_LOGIN_MAX_DURATION, DEFAULT_LOGIN_POLL_INTERVAL, Service, ValFSHost, ValLoginError, ValModuleLoader, ValOpsFS, ValOpsHttp, ValSourceFileHandler, analyzeValModule, awaitValLoginConfirmation, checkRemoteRef, compareWithCapturedReport, createFixPatch, createJsonEntryPathMap, createModulePathMap, createService, createValApiRouter, createValServer, decodeJwt, downloadFileFromRemote, encodeJwt, evalValConfigFile, extractFileMetadata, extractImageMetadata, extractJsonValuesEntry, findAndEvalValConfigFile, findJsonEntryFilePath, formatPatchSourceError, formatSyntaxErrorTree, getCachedRemoteFileDir, getCachedRemoteFilePath, getCompilerOptions, getExpire, getFileExt, getModulePathRange, getPersonalAccessTokenPath, getSettings, getValidationErrorFileRef, loadValModules, parsePersonalAccessTokenFile, patchSourceFile, persistPersonalAccessToken, readCapturedReport, replaySnapshot, safeReadGit, startValLogin, uploadRemoteFile, validateMetadata };
10762
+ export { DEFAULT_LOGIN_HOST, DEFAULT_LOGIN_MAX_DURATION, DEFAULT_LOGIN_POLL_INTERVAL, Service, ValFSHost, ValLoginError, ValModuleLoader, ValOpsFS, ValOpsHttp, ValSourceFileHandler, analyzeValModule, awaitValLoginConfirmation, checkRemoteRef, compareWithCapturedReport, createDefaultValFSHost, createFixPatch, createJsonEntryPathMap, createModulePathMap, createService, createValApiRouter, createValServer, currentFixHandlers, decodeJwt, downloadFileFromRemote, encodeJwt, evalValConfigFile, extractFileMetadata, extractImageMetadata, extractJsonValuesEntry, findAndEvalValConfigFile, findJsonEntryFilePath, fixHandlers, formatPatchSourceError, formatSyntaxErrorTree, getCachedRemoteFileDir, getCachedRemoteFilePath, getCompilerOptions, getExpire, getFileExt, getModulePathRange, getPersonalAccessTokenPath, getSettings, getValidationErrorFileRef, handleCheckAllFiles, handleFileMetadata, handleJsonValuesExtractEntry, handleRemoteFileCheck, handleRemoteFileDownload, handleRemoteFileUpload, handleRemoteGalleryFileUpload, handleUniqueFolderCheck, loadValModules, parsePersonalAccessTokenFile, patchSourceFile, persistPersonalAccessToken, readCapturedReport, replaySnapshot, safeReadGit, startValLogin, uploadRemoteFile, validateMetadata };