@valbuild/server 0.101.1 → 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.
- package/dist/declarations/src/ValOps.d.ts +3 -3
- package/dist/declarations/src/createFixPatch.d.ts +12 -1
- package/dist/declarations/src/fixHandlers.d.ts +162 -0
- package/dist/declarations/src/getValidationErrorFileRef.d.ts +6 -0
- package/dist/declarations/src/index.d.ts +3 -1
- package/dist/declarations/src/patch/ts/syntax.d.ts +0 -14
- package/dist/valbuild-server.cjs.dev.js +872 -659
- package/dist/valbuild-server.cjs.prod.js +872 -659
- package/dist/valbuild-server.esm.js +860 -660
- package/package.json +4 -4
- package/dist/declarations/src/hasRemoteFileSchema.d.ts +0 -5
|
@@ -1,7 +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 } from '@valbuild/core';
|
|
4
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';
|
|
5
6
|
import * as path from 'path';
|
|
6
7
|
import path__default from 'path';
|
|
7
8
|
import fs, { promises } from 'fs';
|
|
@@ -68,7 +69,7 @@ function validateObjectProperties(nodes) {
|
|
|
68
69
|
* validating its children.
|
|
69
70
|
*/
|
|
70
71
|
function shallowValidateExpression(value) {
|
|
71
|
-
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)
|
|
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);
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
/**
|
|
@@ -94,55 +95,8 @@ function evaluateExpression(value) {
|
|
|
94
95
|
return result.all(value.elements.map(evaluateExpression));
|
|
95
96
|
} else if (ts.isObjectLiteralExpression(value)) {
|
|
96
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));
|
|
97
|
-
} else if (isValFileMethodCall(value)) {
|
|
98
|
-
return pipe(findValFileNodeArg(value), result.flatMap(ref => {
|
|
99
|
-
if (value.arguments.length === 2) {
|
|
100
|
-
return pipe(evaluateExpression(value.arguments[1]), result.map(metadata => ({
|
|
101
|
-
[FILE_REF_PROP]: ref.text,
|
|
102
|
-
_type: "file",
|
|
103
|
-
metadata
|
|
104
|
-
})));
|
|
105
|
-
} else {
|
|
106
|
-
return result.ok({
|
|
107
|
-
[FILE_REF_PROP]: ref.text,
|
|
108
|
-
_type: "file"
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
}));
|
|
112
|
-
} else if (isValImageMethodCall(value)) {
|
|
113
|
-
return pipe(findValImageNodeArg(value), result.flatMap(ref => {
|
|
114
|
-
if (value.arguments.length === 2) {
|
|
115
|
-
return pipe(evaluateExpression(value.arguments[1]), result.map(metadata => ({
|
|
116
|
-
[FILE_REF_PROP]: ref.text,
|
|
117
|
-
_type: "file",
|
|
118
|
-
[FILE_REF_SUBTYPE_TAG]: "image",
|
|
119
|
-
metadata
|
|
120
|
-
})));
|
|
121
|
-
} else {
|
|
122
|
-
return result.ok({
|
|
123
|
-
[FILE_REF_PROP]: ref.text,
|
|
124
|
-
_type: "file",
|
|
125
|
-
[FILE_REF_SUBTYPE_TAG]: "image"
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
}));
|
|
129
|
-
} else if (isValRemoteMethodCall(value)) {
|
|
130
|
-
return pipe(findValRemoteNodeArg(value), result.flatMap(ref => {
|
|
131
|
-
if (value.arguments.length === 2) {
|
|
132
|
-
return pipe(evaluateExpression(value.arguments[1]), result.map(metadata => ({
|
|
133
|
-
[FILE_REF_PROP]: ref.text,
|
|
134
|
-
_type: "remote",
|
|
135
|
-
metadata
|
|
136
|
-
})));
|
|
137
|
-
} else {
|
|
138
|
-
return result.ok({
|
|
139
|
-
[FILE_REF_PROP]: ref.text,
|
|
140
|
-
_type: "remote"
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
}));
|
|
144
98
|
} else {
|
|
145
|
-
return result.err(new ValSyntaxError("Expression must be a literal
|
|
99
|
+
return result.err(new ValSyntaxError("Expression must be a literal", value));
|
|
146
100
|
}
|
|
147
101
|
}
|
|
148
102
|
function findObjectPropertyAssignment(value, key) {
|
|
@@ -156,93 +110,6 @@ function findObjectPropertyAssignment(value, key) {
|
|
|
156
110
|
return result.ok(assignment);
|
|
157
111
|
}));
|
|
158
112
|
}
|
|
159
|
-
function isValFileMethodCall(node) {
|
|
160
|
-
return ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "c" && node.expression.name.text === "file";
|
|
161
|
-
}
|
|
162
|
-
function isValImageMethodCall(node) {
|
|
163
|
-
return ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "c" && node.expression.name.text === "image";
|
|
164
|
-
}
|
|
165
|
-
function isValRemoteMethodCall(node) {
|
|
166
|
-
return ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "c" && node.expression.name.text === "remote";
|
|
167
|
-
}
|
|
168
|
-
function findValFileNodeArg(node) {
|
|
169
|
-
if (node.arguments.length === 0) {
|
|
170
|
-
return result.err(new ValSyntaxError(`Invalid c.file() call: missing ref argument`, node));
|
|
171
|
-
} else if (node.arguments.length > 2) {
|
|
172
|
-
return result.err(new ValSyntaxError(`Invalid c.file() call: too many arguments ${node.arguments.length}}`, node));
|
|
173
|
-
} else if (!ts.isStringLiteral(node.arguments[0])) {
|
|
174
|
-
return result.err(new ValSyntaxError(`Invalid c.file() call: ref must be a string literal`, node));
|
|
175
|
-
}
|
|
176
|
-
const refNode = node.arguments[0];
|
|
177
|
-
return result.ok(refNode);
|
|
178
|
-
}
|
|
179
|
-
function findValRemoteNodeArg(node) {
|
|
180
|
-
if (node.arguments.length === 0) {
|
|
181
|
-
return result.err(new ValSyntaxError(`Invalid c.remote() call: missing ref argument`, node));
|
|
182
|
-
} else if (node.arguments.length > 2) {
|
|
183
|
-
return result.err(new ValSyntaxError(`Invalid c.remote() call: too many arguments ${node.arguments.length}}`, node));
|
|
184
|
-
} else if (!ts.isStringLiteral(node.arguments[0])) {
|
|
185
|
-
return result.err(new ValSyntaxError(`Invalid c.remote() call: ref must be a string literal`, node));
|
|
186
|
-
}
|
|
187
|
-
const refNode = node.arguments[0];
|
|
188
|
-
return result.ok(refNode);
|
|
189
|
-
}
|
|
190
|
-
function findValImageNodeArg(node) {
|
|
191
|
-
if (node.arguments.length === 0) {
|
|
192
|
-
return result.err(new ValSyntaxError(`Invalid c.image() call: missing ref argument`, node));
|
|
193
|
-
} else if (node.arguments.length > 2) {
|
|
194
|
-
return result.err(new ValSyntaxError(`Invalid c.image() call: too many arguments ${node.arguments.length}}`, node));
|
|
195
|
-
} else if (!ts.isStringLiteral(node.arguments[0])) {
|
|
196
|
-
return result.err(new ValSyntaxError(`Invalid c.image() call: ref must be a string literal`, node));
|
|
197
|
-
}
|
|
198
|
-
const refNode = node.arguments[0];
|
|
199
|
-
return result.ok(refNode);
|
|
200
|
-
}
|
|
201
|
-
function findValFileMetadataArg(node) {
|
|
202
|
-
if (node.arguments.length === 0) {
|
|
203
|
-
return result.err(new ValSyntaxError(`Invalid c.file() call: missing ref argument`, node));
|
|
204
|
-
} else if (node.arguments.length > 2) {
|
|
205
|
-
return result.err(new ValSyntaxError(`Invalid c.file() call: too many arguments ${node.arguments.length}}`, node));
|
|
206
|
-
} else if (node.arguments.length === 2) {
|
|
207
|
-
if (!ts.isObjectLiteralExpression(node.arguments[1])) {
|
|
208
|
-
return result.err(new ValSyntaxError(`Invalid c.file() call: metadata must be a object literal`, node));
|
|
209
|
-
}
|
|
210
|
-
return result.ok(node.arguments[1]);
|
|
211
|
-
}
|
|
212
|
-
return result.ok(undefined);
|
|
213
|
-
}
|
|
214
|
-
function findValImageMetadataArg(node) {
|
|
215
|
-
if (node.arguments.length === 0) {
|
|
216
|
-
return result.err(new ValSyntaxError(`Invalid c.image() call: missing ref argument`, node));
|
|
217
|
-
} else if (node.arguments.length > 2) {
|
|
218
|
-
return result.err(new ValSyntaxError(`Invalid c.image() call: too many arguments ${node.arguments.length}}`, node));
|
|
219
|
-
} else if (node.arguments.length === 2) {
|
|
220
|
-
if (!ts.isObjectLiteralExpression(node.arguments[1])) {
|
|
221
|
-
return result.err(new ValSyntaxError(`Invalid c.image() call: metadata must be a object literal`, node));
|
|
222
|
-
}
|
|
223
|
-
return result.ok(node.arguments[1]);
|
|
224
|
-
}
|
|
225
|
-
return result.ok(undefined);
|
|
226
|
-
}
|
|
227
|
-
function findValRemoteMetadataArg(node) {
|
|
228
|
-
if (node.arguments.length === 0) {
|
|
229
|
-
return result.err(new ValSyntaxError(`Invalid c.remote() call: missing ref argument`, node));
|
|
230
|
-
} else if (node.arguments.length > 2) {
|
|
231
|
-
return result.err(new ValSyntaxError(`Invalid c.remote() call: too many arguments ${node.arguments.length}}`, node));
|
|
232
|
-
} else if (node.arguments.length === 2) {
|
|
233
|
-
if (!ts.isObjectLiteralExpression(node.arguments[1])) {
|
|
234
|
-
return result.err(new ValSyntaxError(`Invalid c.remote() call: metadata must be a object literal`, node));
|
|
235
|
-
}
|
|
236
|
-
return result.ok(node.arguments[1]);
|
|
237
|
-
}
|
|
238
|
-
return result.ok(undefined);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Given a list of expressions, validates that all the expressions are not
|
|
243
|
-
* spread elements. In other words, it ensures that the expressions are the
|
|
244
|
-
* initializers of the values at their respective indices in the evaluated list.
|
|
245
|
-
*/
|
|
246
113
|
function validateInitializers(nodes) {
|
|
247
114
|
for (const node of nodes) {
|
|
248
115
|
if (ts.isSpreadElement(node)) {
|
|
@@ -324,27 +191,6 @@ function isValidIdentifier(text) {
|
|
|
324
191
|
function createPropertyAssignment(key, value) {
|
|
325
192
|
return ts.factory.createPropertyAssignment(isValidIdentifier(key) ? ts.factory.createIdentifier(key) : ts.factory.createStringLiteral(key), toExpression(value));
|
|
326
193
|
}
|
|
327
|
-
function createValFileReference(value) {
|
|
328
|
-
const args = [ts.factory.createStringLiteral(value[FILE_REF_PROP])];
|
|
329
|
-
if (value.metadata) {
|
|
330
|
-
args.push(toExpression(value.metadata));
|
|
331
|
-
}
|
|
332
|
-
return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("c"), ts.factory.createIdentifier("file")), undefined, args);
|
|
333
|
-
}
|
|
334
|
-
function createValImageReference(value) {
|
|
335
|
-
const args = [ts.factory.createStringLiteral(value[FILE_REF_PROP])];
|
|
336
|
-
if (value.metadata) {
|
|
337
|
-
args.push(toExpression(value.metadata));
|
|
338
|
-
}
|
|
339
|
-
return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("c"), ts.factory.createIdentifier("image")), undefined, args);
|
|
340
|
-
}
|
|
341
|
-
function createValRemoteReference(value) {
|
|
342
|
-
const args = [ts.factory.createStringLiteral(value[FILE_REF_PROP])];
|
|
343
|
-
if (value.metadata) {
|
|
344
|
-
args.push(toExpression(value.metadata));
|
|
345
|
-
}
|
|
346
|
-
return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("c"), ts.factory.createIdentifier("remote")), undefined, args);
|
|
347
|
-
}
|
|
348
194
|
|
|
349
195
|
/**
|
|
350
196
|
* Builds the expression `c.json(() => import("<importPath>"))` used to reference
|
|
@@ -430,16 +276,12 @@ function toExpression(value) {
|
|
|
430
276
|
} else if (Array.isArray(value)) {
|
|
431
277
|
return ts.factory.createArrayLiteralExpression(value.map(toExpression));
|
|
432
278
|
} else if (typeof value === "object") {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (isValRemoteValue(value)) {
|
|
440
|
-
return createValRemoteReference(value);
|
|
441
|
-
}
|
|
442
|
-
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)));
|
|
443
285
|
} else {
|
|
444
286
|
return ts.factory.createStringLiteral(value);
|
|
445
287
|
}
|
|
@@ -572,63 +414,6 @@ function replaceInNode(document, node, key, value) {
|
|
|
572
414
|
}
|
|
573
415
|
return result.ok(assignment);
|
|
574
416
|
}), result.map(assignment => replaceNodeValue(document, assignment.initializer, value)));
|
|
575
|
-
} else if (isValFileMethodCall(node)) {
|
|
576
|
-
if (key === FILE_REF_PROP) {
|
|
577
|
-
if (typeof value !== "string") {
|
|
578
|
-
return result.err(new PatchError("Cannot replace c.file reference with non-string value"));
|
|
579
|
-
}
|
|
580
|
-
return pipe(findValFileNodeArg(node), result.map(refNode => replaceNodeValue(document, refNode, value)));
|
|
581
|
-
} else {
|
|
582
|
-
return pipe(findValFileMetadataArg(node), result.flatMap(metadataArgNode => {
|
|
583
|
-
if (!metadataArgNode) {
|
|
584
|
-
return result.err(new PatchError("Cannot replace in c.file metadata when it does not exist"));
|
|
585
|
-
}
|
|
586
|
-
if (key !== "metadata") {
|
|
587
|
-
return result.err(new PatchError(`Cannot replace c.file metadata key ${key} when it does not exist`));
|
|
588
|
-
}
|
|
589
|
-
return replaceInNode(document,
|
|
590
|
-
// TODO: creating a fake object here might not be right - seems to work though
|
|
591
|
-
ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment(key, metadataArgNode)]), key, value);
|
|
592
|
-
}));
|
|
593
|
-
}
|
|
594
|
-
} else if (isValImageMethodCall(node)) {
|
|
595
|
-
if (key === FILE_REF_PROP) {
|
|
596
|
-
if (typeof value !== "string") {
|
|
597
|
-
return result.err(new PatchError("Cannot replace c.image reference with non-string value"));
|
|
598
|
-
}
|
|
599
|
-
return pipe(findValImageNodeArg(node), result.map(refNode => replaceNodeValue(document, refNode, value)));
|
|
600
|
-
} else {
|
|
601
|
-
return pipe(findValImageMetadataArg(node), result.flatMap(metadataArgNode => {
|
|
602
|
-
if (!metadataArgNode) {
|
|
603
|
-
return result.err(new PatchError("Cannot replace in c.image metadata when it does not exist"));
|
|
604
|
-
}
|
|
605
|
-
if (key !== "metadata") {
|
|
606
|
-
return result.err(new PatchError(`Cannot replace c.image metadata key ${key} when it does not exist`));
|
|
607
|
-
}
|
|
608
|
-
return replaceInNode(document,
|
|
609
|
-
// TODO: creating a fake object here might not be right - seems to work though
|
|
610
|
-
ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment(key, metadataArgNode)]), key, value);
|
|
611
|
-
}));
|
|
612
|
-
}
|
|
613
|
-
} else if (isValRemoteMethodCall(node)) {
|
|
614
|
-
if (key === FILE_REF_PROP) {
|
|
615
|
-
if (typeof value !== "string") {
|
|
616
|
-
return result.err(new PatchError("Cannot replace c.image reference with non-string value"));
|
|
617
|
-
}
|
|
618
|
-
return pipe(findValRemoteNodeArg(node), result.map(refNode => replaceNodeValue(document, refNode, value)));
|
|
619
|
-
} else {
|
|
620
|
-
return pipe(findValRemoteMetadataArg(node), result.flatMap(metadataArgNode => {
|
|
621
|
-
if (!metadataArgNode) {
|
|
622
|
-
return result.err(new PatchError("Cannot replace in c.remote metadata when it does not exist"));
|
|
623
|
-
}
|
|
624
|
-
if (key !== "metadata") {
|
|
625
|
-
return result.err(new PatchError(`Cannot replace c.remote metadata key ${key} when it does not exist`));
|
|
626
|
-
}
|
|
627
|
-
return replaceInNode(document,
|
|
628
|
-
// TODO: creating a fake object here might not be right - seems to work though
|
|
629
|
-
ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment(key, metadataArgNode)]), key, value);
|
|
630
|
-
}));
|
|
631
|
-
}
|
|
632
417
|
} else {
|
|
633
418
|
return result.err(shallowValidateExpression(node) ?? new PatchError("Cannot replace in non-object/array"));
|
|
634
419
|
}
|
|
@@ -645,21 +430,6 @@ function getFromNode(node, key) {
|
|
|
645
430
|
return pipe(parseAndValidateArrayInboundsIndex(key, node.elements), result.map(index => node.elements[index]));
|
|
646
431
|
} else if (ts.isObjectLiteralExpression(node)) {
|
|
647
432
|
return pipe(findObjectPropertyAssignment(node, key), result.map(assignment => assignment === null || assignment === void 0 ? void 0 : assignment.initializer));
|
|
648
|
-
} else if (isValFileMethodCall(node)) {
|
|
649
|
-
if (key === FILE_REF_PROP) {
|
|
650
|
-
return findValFileNodeArg(node);
|
|
651
|
-
}
|
|
652
|
-
return findValFileMetadataArg(node);
|
|
653
|
-
} else if (isValImageMethodCall(node)) {
|
|
654
|
-
if (key === FILE_REF_PROP) {
|
|
655
|
-
return findValImageNodeArg(node);
|
|
656
|
-
}
|
|
657
|
-
return findValImageMetadataArg(node);
|
|
658
|
-
} else if (isValRemoteMethodCall(node)) {
|
|
659
|
-
if (key === FILE_REF_PROP) {
|
|
660
|
-
return findValRemoteNodeArg(node);
|
|
661
|
-
}
|
|
662
|
-
return findValRemoteMetadataArg(node);
|
|
663
433
|
} else {
|
|
664
434
|
return result.err(shallowValidateExpression(node) ?? new PatchError("Cannot access non-object/array"));
|
|
665
435
|
}
|
|
@@ -692,39 +462,6 @@ function removeFromNode(document, node, key) {
|
|
|
692
462
|
}
|
|
693
463
|
return result.ok(assignment);
|
|
694
464
|
}), result.map(assignment => [removeAt(document, node.properties, node.properties.indexOf(assignment))[0], assignment.initializer]));
|
|
695
|
-
} else if (isValFileMethodCall(node)) {
|
|
696
|
-
if (key === FILE_REF_PROP) {
|
|
697
|
-
return result.err(new PatchError("Cannot remove a ref from c.file"));
|
|
698
|
-
} else {
|
|
699
|
-
return pipe(findValFileMetadataArg(node), result.flatMap(metadataArgNode => {
|
|
700
|
-
if (!metadataArgNode) {
|
|
701
|
-
return result.err(new PatchError("Cannot remove from c.file metadata when it does not exist"));
|
|
702
|
-
}
|
|
703
|
-
return removeFromNode(document, metadataArgNode, key);
|
|
704
|
-
}));
|
|
705
|
-
}
|
|
706
|
-
} else if (isValImageMethodCall(node)) {
|
|
707
|
-
if (key === FILE_REF_PROP) {
|
|
708
|
-
return result.err(new PatchError("Cannot remove a ref from c.image"));
|
|
709
|
-
} else {
|
|
710
|
-
return pipe(findValImageMetadataArg(node), result.flatMap(metadataArgNode => {
|
|
711
|
-
if (!metadataArgNode) {
|
|
712
|
-
return result.err(new PatchError("Cannot remove from c.image metadata when it does not exist"));
|
|
713
|
-
}
|
|
714
|
-
return removeFromNode(document, metadataArgNode, key);
|
|
715
|
-
}));
|
|
716
|
-
}
|
|
717
|
-
} else if (isValRemoteMethodCall(node)) {
|
|
718
|
-
if (key === FILE_REF_PROP) {
|
|
719
|
-
return result.err(new PatchError("Cannot remove a ref from c.remote"));
|
|
720
|
-
} else {
|
|
721
|
-
return pipe(findValRemoteMetadataArg(node), result.flatMap(metadataArgNode => {
|
|
722
|
-
if (!metadataArgNode) {
|
|
723
|
-
return result.err(new PatchError("Cannot remove from c.remote metadata when it does not exist"));
|
|
724
|
-
}
|
|
725
|
-
return removeFromNode(document, metadataArgNode, key);
|
|
726
|
-
}));
|
|
727
|
-
}
|
|
728
465
|
} else {
|
|
729
466
|
return result.err(shallowValidateExpression(node) ?? new PatchError("Cannot remove from non-object/array"));
|
|
730
467
|
}
|
|
@@ -732,22 +469,10 @@ function removeFromNode(document, node, key) {
|
|
|
732
469
|
function removeAtPath(document, rootNode, path) {
|
|
733
470
|
return pipe(getPointerFromPath(rootNode, path), result.flatMap(([node, key]) => removeFromNode(document, node, key)));
|
|
734
471
|
}
|
|
735
|
-
function isValFileValue(value) {
|
|
736
|
-
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");
|
|
737
|
-
}
|
|
738
|
-
function isValImageValue(value) {
|
|
739
|
-
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");
|
|
740
|
-
}
|
|
741
|
-
function isValRemoteValue(value) {
|
|
742
|
-
return !!(typeof value === "object" && value && VAL_EXTENSION in value && value[VAL_EXTENSION] === "remote" && FILE_REF_PROP in value && typeof value[FILE_REF_PROP] === "string");
|
|
743
|
-
}
|
|
744
472
|
function addToNode(document, node, key, value) {
|
|
745
473
|
if (ts.isArrayLiteralExpression(node)) {
|
|
746
474
|
return pipe(parseAndValidateArrayInsertIndex(key, node.elements), result.map(index => [insertAt(document, node.elements, index, toExpression(value))]));
|
|
747
475
|
} else if (ts.isObjectLiteralExpression(node)) {
|
|
748
|
-
if (key === FILE_REF_PROP) {
|
|
749
|
-
return result.err(new PatchError("Cannot add a key ref to object"));
|
|
750
|
-
}
|
|
751
476
|
return pipe(findObjectPropertyAssignment(node, key), result.map(assignment => {
|
|
752
477
|
if (!assignment) {
|
|
753
478
|
return [insertAt(document, node.properties, node.properties.length, createPropertyAssignment(key, value))];
|
|
@@ -755,57 +480,6 @@ function addToNode(document, node, key, value) {
|
|
|
755
480
|
return replaceNodeValue(document, assignment.initializer, value);
|
|
756
481
|
}
|
|
757
482
|
}));
|
|
758
|
-
} else if (isValFileMethodCall(node)) {
|
|
759
|
-
if (key === FILE_REF_PROP) {
|
|
760
|
-
if (typeof value !== "string") {
|
|
761
|
-
return result.err(new PatchError(`Cannot add ${FILE_REF_PROP} key to c.file with non-string value`));
|
|
762
|
-
}
|
|
763
|
-
return pipe(findValFileNodeArg(node), result.map(arg => replaceNodeValue(document, arg, value)));
|
|
764
|
-
} else {
|
|
765
|
-
return pipe(findValFileMetadataArg(node), result.flatMap(metadataArgNode => {
|
|
766
|
-
if (metadataArgNode) {
|
|
767
|
-
return result.err(new PatchError("Cannot add metadata to c.file when it already exists"));
|
|
768
|
-
}
|
|
769
|
-
if (key !== "metadata") {
|
|
770
|
-
return result.err(new PatchError(`Cannot add ${key} key to c.file: only metadata is allowed`));
|
|
771
|
-
}
|
|
772
|
-
return result.ok([insertAt(document, node.arguments, node.arguments.length, toExpression(value))]);
|
|
773
|
-
}));
|
|
774
|
-
}
|
|
775
|
-
} else if (isValImageMethodCall(node)) {
|
|
776
|
-
if (key === FILE_REF_PROP) {
|
|
777
|
-
if (typeof value !== "string") {
|
|
778
|
-
return result.err(new PatchError(`Cannot add ${FILE_REF_PROP} key to c.image with non-string value`));
|
|
779
|
-
}
|
|
780
|
-
return pipe(findValImageNodeArg(node), result.map(arg => replaceNodeValue(document, arg, value)));
|
|
781
|
-
} else {
|
|
782
|
-
return pipe(findValImageMetadataArg(node), result.flatMap(metadataArgNode => {
|
|
783
|
-
if (metadataArgNode) {
|
|
784
|
-
return result.err(new PatchError("Cannot add metadata to c.image when it already exists"));
|
|
785
|
-
}
|
|
786
|
-
if (key !== "metadata") {
|
|
787
|
-
return result.err(new PatchError(`Cannot add ${key} key to c.image: only metadata is allowed`));
|
|
788
|
-
}
|
|
789
|
-
return result.ok([insertAt(document, node.arguments, node.arguments.length, toExpression(value))]);
|
|
790
|
-
}));
|
|
791
|
-
}
|
|
792
|
-
} else if (isValRemoteMethodCall(node)) {
|
|
793
|
-
if (key === FILE_REF_PROP) {
|
|
794
|
-
if (typeof value !== "string") {
|
|
795
|
-
return result.err(new PatchError(`Cannot add ${FILE_REF_PROP} key to c.remote with non-string value`));
|
|
796
|
-
}
|
|
797
|
-
return pipe(findValRemoteNodeArg(node), result.map(arg => replaceNodeValue(document, arg, value)));
|
|
798
|
-
} else {
|
|
799
|
-
return pipe(findValRemoteMetadataArg(node), result.flatMap(metadataArgNode => {
|
|
800
|
-
if (metadataArgNode) {
|
|
801
|
-
return result.err(new PatchError("Cannot add metadata to c.remote when it already exists"));
|
|
802
|
-
}
|
|
803
|
-
if (key !== "metadata") {
|
|
804
|
-
return result.err(new PatchError(`Cannot add ${key} key to c.remote: only metadata is allowed`));
|
|
805
|
-
}
|
|
806
|
-
return result.ok([insertAt(document, node.arguments, node.arguments.length, toExpression(value))]);
|
|
807
|
-
}));
|
|
808
|
-
}
|
|
809
483
|
} else {
|
|
810
484
|
return result.err(shallowValidateExpression(node) ?? new PatchError("Cannot add to non-object/array"));
|
|
811
485
|
}
|
|
@@ -1387,7 +1061,10 @@ function rebaseContentOp(op, prefixLen) {
|
|
|
1387
1061
|
from: op.from.slice(prefixLen)
|
|
1388
1062
|
});
|
|
1389
1063
|
case "file":
|
|
1390
|
-
|
|
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"));
|
|
1391
1068
|
}
|
|
1392
1069
|
}
|
|
1393
1070
|
|
|
@@ -1421,9 +1098,6 @@ function applyJsonValuesEntryPatches(args) {
|
|
|
1421
1098
|
} of patches) {
|
|
1422
1099
|
let touched = false;
|
|
1423
1100
|
for (const op of patch) {
|
|
1424
|
-
if (op.op === "file") {
|
|
1425
|
-
continue;
|
|
1426
|
-
}
|
|
1427
1101
|
const cls = serializedSchema ? classifyJsonValuesOp(serializedSchema, op.path) : {
|
|
1428
1102
|
kind: "normal"
|
|
1429
1103
|
};
|
|
@@ -1431,6 +1105,32 @@ function applyJsonValuesEntryPatches(args) {
|
|
|
1431
1105
|
continue;
|
|
1432
1106
|
}
|
|
1433
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
|
+
}
|
|
1434
1134
|
if (cls.subPath.length === 0) {
|
|
1435
1135
|
if (op.op === "add" || op.op === "replace") {
|
|
1436
1136
|
content = op.value;
|
|
@@ -1528,6 +1228,35 @@ function resolveExistingJsonPath(moduleFilePath, importPath) {
|
|
|
1528
1228
|
* module errors (see `findNestedJsonValuesRecords`), so they can never reach
|
|
1529
1229
|
* here — if that guard is ever relaxed, this must become a recursive visitor or
|
|
1530
1230
|
* nested entries silently get no content validation at all.
|
|
1231
|
+
*
|
|
1232
|
+
* ## NOT validated, and it should be: the entry's FILE PATH
|
|
1233
|
+
*
|
|
1234
|
+
* The key↔file mapping is canonical. `getNewJsonEntryPaths` derives it, and both
|
|
1235
|
+
* write paths use it — the `jsonValues:extract-entry` fix and the patch commit:
|
|
1236
|
+
*
|
|
1237
|
+
* /content/kb.val.ts + key "kb-000" -> /content/kb/kb-000.val.json
|
|
1238
|
+
*
|
|
1239
|
+
* This function does not check it. It calls the thunk and validates whatever
|
|
1240
|
+
* comes back, so an entry may point at ANY file in the project and pass. The
|
|
1241
|
+
* repo's own fixture does exactly that — `examples/next/content/kb.val.ts` has
|
|
1242
|
+
* `"kb-000": c.json(() => import("./kb/entry-000.val.json"))`, where the
|
|
1243
|
+
* canonical path is `./kb/kb-000.val.json`, and `val validate` calls it valid.
|
|
1244
|
+
*
|
|
1245
|
+
* Two consequences, the first a live bug:
|
|
1246
|
+
*
|
|
1247
|
+
* - **A key rename writes to the DERIVED path**, which for a non-canonical entry
|
|
1248
|
+
* is not the file the old key was reading. The old file is orphaned and the new
|
|
1249
|
+
* key reads a file that was never written.
|
|
1250
|
+
* - It blocks localising entry staleness. `jsonEntriesSha` cannot say which entry
|
|
1251
|
+
* changed, so the client drops a whole module's loaded content; with the mapping
|
|
1252
|
+
* guaranteed, a changed file resolves to one key by derivation alone.
|
|
1253
|
+
*
|
|
1254
|
+
* The fix belongs here and needs no new machinery: the module's `.val.ts` is
|
|
1255
|
+
* already parsed (`ValSourceFileHandler`), so the import specifier is readable
|
|
1256
|
+
* from the AST — no `thunk.toString()`, which a bundler would defeat. A
|
|
1257
|
+
* `jsonValues:rename-entry-file` fix could move the file and rewrite the import,
|
|
1258
|
+
* exactly as `extract-entry` writes both. See `packages/ui/spa/stores/openquestions.md`
|
|
1259
|
+
* item 9b.
|
|
1531
1260
|
*/
|
|
1532
1261
|
async function validateJsonValuesEntries(schema, source, modulePath) {
|
|
1533
1262
|
const out = {};
|
|
@@ -2374,6 +2103,17 @@ class ValOps {
|
|
|
2374
2103
|
const serializedSchema = jsonValuesSchemaFor(path);
|
|
2375
2104
|
for (const op of patchData.patch) {
|
|
2376
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
|
+
}
|
|
2377
2117
|
if (op.value !== null) {
|
|
2378
2118
|
// NOTE: We insert the last patch_id that modify a file
|
|
2379
2119
|
// when constructing the url we use the patch id (and the file path)
|
|
@@ -2674,7 +2414,7 @@ class ValOps {
|
|
|
2674
2414
|
};
|
|
2675
2415
|
}
|
|
2676
2416
|
const type = schemaAtPath instanceof ImageSchema ? "image" : "file";
|
|
2677
|
-
const filePath = value
|
|
2417
|
+
const filePath = value.path;
|
|
2678
2418
|
const fileData = (fileLastUpdatedByPatchId === null || fileLastUpdatedByPatchId === void 0 ? void 0 : fileLastUpdatedByPatchId[filePath]) || null;
|
|
2679
2419
|
let metadata;
|
|
2680
2420
|
let metadataErrors;
|
|
@@ -2716,23 +2456,14 @@ class ValOps {
|
|
|
2716
2456
|
}]
|
|
2717
2457
|
};
|
|
2718
2458
|
}
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
const currentValueMetadata = value.metadata;
|
|
2724
|
-
if (!currentValueMetadata) {
|
|
2725
|
-
return {
|
|
2726
|
-
[metadataSourcePath]: [{
|
|
2727
|
-
message: "Missing metadata field: 'metadata'",
|
|
2728
|
-
value
|
|
2729
|
-
}]
|
|
2730
|
-
};
|
|
2731
|
-
}
|
|
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;
|
|
2732
2463
|
const fieldErrors = {};
|
|
2733
2464
|
for (const field of getFieldsForType(type)) {
|
|
2734
2465
|
const fieldMetadata = metadata[field];
|
|
2735
|
-
const fieldSourcePath = Internal.createValPathOfItem(
|
|
2466
|
+
const fieldSourcePath = Internal.createValPathOfItem(sourcePath, field);
|
|
2736
2467
|
if (!fieldSourcePath) {
|
|
2737
2468
|
throw new Error("Could not create field path");
|
|
2738
2469
|
}
|
|
@@ -3324,11 +3055,15 @@ function isOnlyFileCheckValidationError(validationError) {
|
|
|
3324
3055
|
}
|
|
3325
3056
|
return false;
|
|
3326
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
|
+
*/
|
|
3327
3065
|
function isFileSource(value) {
|
|
3328
|
-
|
|
3329
|
-
return true;
|
|
3330
|
-
}
|
|
3331
|
-
return false;
|
|
3066
|
+
return typeof value === "object" && value !== null && "path" in value && typeof value.path === "string";
|
|
3332
3067
|
}
|
|
3333
3068
|
function formatPatchSourceError(error) {
|
|
3334
3069
|
if ("message" in error) {
|
|
@@ -4672,7 +4407,7 @@ const FSPatchBase = z.object({
|
|
|
4672
4407
|
timestamp: z.string().datetime()
|
|
4673
4408
|
});
|
|
4674
4409
|
|
|
4675
|
-
const textEncoder$
|
|
4410
|
+
const textEncoder$2 = new TextEncoder();
|
|
4676
4411
|
const PatchId = z.string().refine(s => !!s); // TODO: validate
|
|
4677
4412
|
const CommitSha = z.string().refine(s => !!s); // TODO: validate
|
|
4678
4413
|
z.string().refine(s => !!s); // TODO: validate
|
|
@@ -5363,7 +5098,7 @@ class ValOpsHttp extends ValOps {
|
|
|
5363
5098
|
});
|
|
5364
5099
|
params.set("body_sha",
|
|
5365
5100
|
// We use this for cache invalidation
|
|
5366
|
-
Internal.getSHA256Hash(textEncoder$
|
|
5101
|
+
Internal.getSHA256Hash(textEncoder$2.encode(stringifiedFiles)));
|
|
5367
5102
|
return fetch(`${this.contentUrl}/v1/${this.project}/files?${params}`, {
|
|
5368
5103
|
method: "PUT",
|
|
5369
5104
|
// Yes, PUT is weird. Weirder to have a body in a GET request.
|
|
@@ -5789,51 +5524,6 @@ function parsePersonalAccessTokenFile(content) {
|
|
|
5789
5524
|
}
|
|
5790
5525
|
}
|
|
5791
5526
|
|
|
5792
|
-
/**
|
|
5793
|
-
* Iterates through all schemas and find if there is 1 or more remote files in them
|
|
5794
|
-
*/
|
|
5795
|
-
function hasRemoteFileSchema(schema) {
|
|
5796
|
-
if (schema.type === "file" || schema.type === "image") {
|
|
5797
|
-
return !!schema.remote;
|
|
5798
|
-
} else if (schema.type === "richtext") {
|
|
5799
|
-
var _schema$options;
|
|
5800
|
-
if (typeof ((_schema$options = schema.options) === null || _schema$options === void 0 || (_schema$options = _schema$options.inline) === null || _schema$options === void 0 ? void 0 : _schema$options.img) === "object") {
|
|
5801
|
-
return hasRemoteFileSchema(schema.options.inline.img);
|
|
5802
|
-
}
|
|
5803
|
-
return false;
|
|
5804
|
-
} else if (schema.type === "array" || schema.type === "record") {
|
|
5805
|
-
return hasRemoteFileSchema(schema.item);
|
|
5806
|
-
} else if (schema.type === "object") {
|
|
5807
|
-
for (const key in schema.items) {
|
|
5808
|
-
const hasRemoteFile = hasRemoteFileSchema(schema.items[key]);
|
|
5809
|
-
if (hasRemoteFile) {
|
|
5810
|
-
return true;
|
|
5811
|
-
}
|
|
5812
|
-
}
|
|
5813
|
-
return false;
|
|
5814
|
-
} else if (schema.type === "union") {
|
|
5815
|
-
const unionStringSchema = typeof schema.key === "object" && schema.key.type === "literal" ? schema : undefined;
|
|
5816
|
-
const unionObjectSchema = typeof schema.key === "string" ? schema : undefined;
|
|
5817
|
-
if (unionStringSchema) {
|
|
5818
|
-
return false;
|
|
5819
|
-
}
|
|
5820
|
-
if (unionObjectSchema) {
|
|
5821
|
-
for (const key in unionObjectSchema.items) {
|
|
5822
|
-
const hasRemoteFile = hasRemoteFileSchema(unionObjectSchema.items[key]);
|
|
5823
|
-
if (hasRemoteFile) {
|
|
5824
|
-
return true;
|
|
5825
|
-
}
|
|
5826
|
-
}
|
|
5827
|
-
}
|
|
5828
|
-
return false;
|
|
5829
|
-
} else if (schema.type === "boolean" || schema.type === "number" || schema.type === "string" || schema.type === "literal" || schema.type === "date" || schema.type === "dateTime" || schema.type === "color" || schema.type === "keyOf" || schema.type === "route") {
|
|
5830
|
-
return false;
|
|
5831
|
-
} else {
|
|
5832
|
-
const exhaustiveCheck = schema;
|
|
5833
|
-
throw new Error(`Unexpected schema: ${JSON.stringify(exhaustiveCheck)}`);
|
|
5834
|
-
}
|
|
5835
|
-
}
|
|
5836
|
-
|
|
5837
5527
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
5838
5528
|
const ValServer = (valModules, options, callbacks) => {
|
|
5839
5529
|
const AIContentBlock = z.union([z.object({
|
|
@@ -9136,15 +8826,21 @@ _input // TODO: use buffer to determine mimetype
|
|
|
9136
8826
|
};
|
|
9137
8827
|
}
|
|
9138
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
|
+
*/
|
|
9139
8835
|
function getValidationErrorFileRef(validationError) {
|
|
9140
|
-
const
|
|
9141
|
-
if (!
|
|
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) {
|
|
9142
8838
|
return null;
|
|
9143
8839
|
}
|
|
9144
|
-
return
|
|
8840
|
+
return maybePath;
|
|
9145
8841
|
}
|
|
9146
8842
|
|
|
9147
|
-
const textEncoder = new TextEncoder();
|
|
8843
|
+
const textEncoder$1 = new TextEncoder();
|
|
9148
8844
|
async function checkRemoteRef(remoteHost, ref, projectRoot, schema, metadata) {
|
|
9149
8845
|
const remoteRefDataRes = Internal.remote.splitRemoteRef(ref);
|
|
9150
8846
|
if (remoteRefDataRes.status === "error") {
|
|
@@ -9174,10 +8870,10 @@ async function checkRemoteRef(remoteHost, ref, projectRoot, schema, metadata) {
|
|
|
9174
8870
|
const fileExt = getFileExt(relativeFilePath);
|
|
9175
8871
|
const currentValidationHash = Internal.remote.getValidationHash(coreVersion, schema, fileExt, metadata,
|
|
9176
8872
|
// TODO: validate
|
|
9177
|
-
fileHash, textEncoder);
|
|
8873
|
+
fileHash, textEncoder$1);
|
|
9178
8874
|
const validationHashFromRemoteRefValues = Internal.remote.getValidationHash(remoteRefDataRes.version, schema, getFileExt(remoteRefDataRes.filePath), metadata,
|
|
9179
8875
|
// TODO: validate
|
|
9180
|
-
fileHash, textEncoder);
|
|
8876
|
+
fileHash, textEncoder$1);
|
|
9181
8877
|
if (
|
|
9182
8878
|
// Current validation hash is the same as the remote validation hash
|
|
9183
8879
|
// We assume the uploaded file exists and is correct
|
|
@@ -9223,7 +8919,7 @@ async function checkRemoteRef(remoteHost, ref, projectRoot, schema, metadata) {
|
|
|
9223
8919
|
...metadata,
|
|
9224
8920
|
...currentMetadata
|
|
9225
8921
|
};
|
|
9226
|
-
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);
|
|
9227
8923
|
if (!updatedMetadata.mimeType) {
|
|
9228
8924
|
return {
|
|
9229
8925
|
status: "error",
|
|
@@ -9353,6 +9049,36 @@ function parentPatchPathOfMediaEntry(sourcePath, entryKey) {
|
|
|
9353
9049
|
}
|
|
9354
9050
|
|
|
9355
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
|
+
}
|
|
9356
9082
|
async function createFixPatch(config, apply, sourcePath, validationError, remoteFiles, moduleSource, moduleSchema) {
|
|
9357
9083
|
const remainingErrors = [];
|
|
9358
9084
|
const patch = [];
|
|
@@ -9366,57 +9092,38 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
9366
9092
|
fixes: undefined
|
|
9367
9093
|
});
|
|
9368
9094
|
} else if (fix === "image:check-metadata") {
|
|
9369
|
-
const currentValue = validationError.value;
|
|
9370
|
-
const
|
|
9371
|
-
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
//
|
|
9376
|
-
|
|
9377
|
-
//
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
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) {
|
|
9381
9106
|
if (apply) {
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
}
|
|
9392
|
-
});
|
|
9393
|
-
} else {
|
|
9394
|
-
if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
|
|
9395
|
-
if (!("width" in currentValue.metadata) || currentValue.metadata.width !== imageMetadata.width) {
|
|
9396
|
-
remainingErrors.push({
|
|
9397
|
-
message: "Image metadata width is incorrect! Found: " + ("width" in currentValue.metadata ? currentValue.metadata.width : "<empty>") + ". Expected: " + imageMetadata.width,
|
|
9398
|
-
fixes: undefined
|
|
9399
|
-
});
|
|
9400
|
-
}
|
|
9401
|
-
if (!("height" in currentValue.metadata) || currentValue.metadata.height !== imageMetadata.height) {
|
|
9402
|
-
remainingErrors.push({
|
|
9403
|
-
message: "Image metadata height is incorrect! Found: " + ("height" in currentValue.metadata ? currentValue.metadata.height : "<empty>") + ". Expected: " + imageMetadata.height,
|
|
9404
|
-
fixes: undefined
|
|
9405
|
-
});
|
|
9406
|
-
}
|
|
9407
|
-
if (!("mimeType" in currentValue.metadata) || currentValue.metadata.mimeType !== imageMetadata.mimeType) {
|
|
9408
|
-
remainingErrors.push({
|
|
9409
|
-
message: "Image metadata mimeType is incorrect! Found: " + ("mimeType" in currentValue.metadata ? currentValue.metadata.mimeType : "<empty>") + ". Expected: " + imageMetadata.mimeType,
|
|
9410
|
-
fixes: undefined
|
|
9411
|
-
});
|
|
9412
|
-
}
|
|
9413
|
-
} 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) {
|
|
9414
9116
|
remainingErrors.push({
|
|
9415
|
-
|
|
9416
|
-
message: "Image metadata is not an object!",
|
|
9117
|
+
message: `Image ${field} is incorrect! Found: ` + (currentValue[field] ?? "<empty>") + ". Expected: " + expected,
|
|
9417
9118
|
fixes: undefined
|
|
9418
9119
|
});
|
|
9419
9120
|
}
|
|
9121
|
+
} else {
|
|
9122
|
+
remainingErrors.push({
|
|
9123
|
+
...validationError,
|
|
9124
|
+
message: "Image is not an object!",
|
|
9125
|
+
fixes: undefined
|
|
9126
|
+
});
|
|
9420
9127
|
}
|
|
9421
9128
|
}
|
|
9422
9129
|
} else if (fix === "image:add-metadata") {
|
|
@@ -9427,14 +9134,19 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
9427
9134
|
fixes: undefined
|
|
9428
9135
|
});
|
|
9429
9136
|
} else {
|
|
9137
|
+
const patchPath = sourceToPatchPath(sourcePath);
|
|
9430
9138
|
patch.push({
|
|
9431
9139
|
op: "add",
|
|
9432
|
-
path:
|
|
9433
|
-
value:
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
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
|
|
9438
9150
|
});
|
|
9439
9151
|
}
|
|
9440
9152
|
}
|
|
@@ -9447,53 +9159,32 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
9447
9159
|
fixes: undefined
|
|
9448
9160
|
});
|
|
9449
9161
|
} else if (fix === "file:check-metadata") {
|
|
9450
|
-
const currentValue = validationError.value;
|
|
9451
|
-
|
|
9452
|
-
// metadata is a prop that is an object
|
|
9453
|
-
typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
|
|
9454
|
-
// mimeType is correct
|
|
9455
|
-
"mimeType" in currentValue.metadata && currentValue.metadata.mimeType === fileMetadata.mimeType;
|
|
9456
|
-
|
|
9457
|
-
// skips if the metadata is already correct
|
|
9458
|
-
if (!metadataIsCorrect) {
|
|
9162
|
+
const currentValue = mediaValueOf(validationError.value);
|
|
9163
|
+
if ((currentValue === null || currentValue === void 0 ? void 0 : currentValue.mimeType) !== fileMetadata.mimeType) {
|
|
9459
9164
|
if (apply) {
|
|
9460
9165
|
patch.push({
|
|
9461
|
-
op: "
|
|
9462
|
-
path: sourceToPatchPath(sourcePath).concat("
|
|
9463
|
-
value:
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
}
|
|
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
|
|
9470
9174
|
});
|
|
9471
9175
|
} else {
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
});
|
|
9478
|
-
}
|
|
9479
|
-
} else {
|
|
9480
|
-
remainingErrors.push({
|
|
9481
|
-
...validationError,
|
|
9482
|
-
message: "Image metadata is not an object!",
|
|
9483
|
-
fixes: undefined
|
|
9484
|
-
});
|
|
9485
|
-
}
|
|
9176
|
+
remainingErrors.push({
|
|
9177
|
+
...validationError,
|
|
9178
|
+
message: "File is not an object!",
|
|
9179
|
+
fixes: undefined
|
|
9180
|
+
});
|
|
9486
9181
|
}
|
|
9487
9182
|
}
|
|
9488
9183
|
} else if (fix === "file:add-metadata") {
|
|
9489
9184
|
patch.push({
|
|
9490
9185
|
op: "add",
|
|
9491
|
-
path: sourceToPatchPath(sourcePath).concat("
|
|
9492
|
-
value:
|
|
9493
|
-
...(fileMetadata.mimeType ? {
|
|
9494
|
-
mimeType: fileMetadata.mimeType
|
|
9495
|
-
} : {})
|
|
9496
|
-
}
|
|
9186
|
+
path: sourceToPatchPath(sourcePath).concat("mimeType"),
|
|
9187
|
+
value: fileMetadata.mimeType ?? null
|
|
9497
9188
|
});
|
|
9498
9189
|
}
|
|
9499
9190
|
} else if (fix === "image:upload-remote" || fix === "file:upload-remote") {
|
|
@@ -9521,11 +9212,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
9521
9212
|
} else {
|
|
9522
9213
|
patch.push({
|
|
9523
9214
|
op: "replace",
|
|
9524
|
-
value:
|
|
9525
|
-
_type: "remote",
|
|
9526
|
-
_ref: remoteFile.ref,
|
|
9527
|
-
metadata
|
|
9528
|
-
},
|
|
9215
|
+
value: mediaValue(remoteFile.ref, metadata),
|
|
9529
9216
|
path: sourceToPatchPath(sourcePath)
|
|
9530
9217
|
});
|
|
9531
9218
|
}
|
|
@@ -9594,7 +9281,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
9594
9281
|
});
|
|
9595
9282
|
continue;
|
|
9596
9283
|
}
|
|
9597
|
-
const splitRemoteRefDataRes = Internal.remote.splitRemoteRef(v.
|
|
9284
|
+
const splitRemoteRefDataRes = Internal.remote.splitRemoteRef(v.path);
|
|
9598
9285
|
if (splitRemoteRefDataRes.status === "error") {
|
|
9599
9286
|
remainingErrors.push({
|
|
9600
9287
|
...validationError,
|
|
@@ -9603,7 +9290,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
9603
9290
|
});
|
|
9604
9291
|
continue;
|
|
9605
9292
|
}
|
|
9606
|
-
const url = v.
|
|
9293
|
+
const url = v.path;
|
|
9607
9294
|
const filePath = splitRemoteRefDataRes.filePath;
|
|
9608
9295
|
if (!filePath) {
|
|
9609
9296
|
remainingErrors.push({
|
|
@@ -9634,20 +9321,10 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
9634
9321
|
});
|
|
9635
9322
|
continue;
|
|
9636
9323
|
}
|
|
9637
|
-
const value = {
|
|
9638
|
-
[VAL_EXTENSION]: "file",
|
|
9639
|
-
[FILE_REF_PROP]: `/${filePath}`,
|
|
9640
|
-
...(fix === "image:download-remote" ? {
|
|
9641
|
-
[FILE_REF_SUBTYPE_TAG]: "image"
|
|
9642
|
-
} : {})
|
|
9643
|
-
};
|
|
9644
9324
|
patch.push({
|
|
9645
9325
|
op: "replace",
|
|
9646
9326
|
path: sourceToPatchPath(sourcePath),
|
|
9647
|
-
value: v.metadata
|
|
9648
|
-
...value,
|
|
9649
|
-
metadata: v.metadata
|
|
9650
|
-
} : value
|
|
9327
|
+
value: mediaValue(`/${filePath}`, v.metadata)
|
|
9651
9328
|
});
|
|
9652
9329
|
} else if (fix === "images:check-all-files" || fix === "files:check-all-files") {
|
|
9653
9330
|
if (!moduleSource || typeof moduleSource !== "object") {
|
|
@@ -9770,7 +9447,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
9770
9447
|
schema: schemaAtPath
|
|
9771
9448
|
} = Internal.resolvePath(modulePath, moduleSource, moduleSchema);
|
|
9772
9449
|
if (schemaAtPath.type === "image" || schemaAtPath.type === "file") {
|
|
9773
|
-
const res = await checkRemoteRef(config.remoteHost, v.
|
|
9450
|
+
const res = await checkRemoteRef(config.remoteHost, v.path, config.projectRoot, schemaAtPath, v.metadata);
|
|
9774
9451
|
if (res.status === "success") ; else if (res.status === "error") {
|
|
9775
9452
|
remainingErrors.push({
|
|
9776
9453
|
...validationError,
|
|
@@ -9782,11 +9459,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
9782
9459
|
patch.push({
|
|
9783
9460
|
op: "replace",
|
|
9784
9461
|
path: sourceToPatchPath(sourcePath),
|
|
9785
|
-
value:
|
|
9786
|
-
_type: "remote",
|
|
9787
|
-
_ref: res.ref,
|
|
9788
|
-
metadata: res.metadata
|
|
9789
|
-
}
|
|
9462
|
+
value: mediaValue(res.ref, res.metadata)
|
|
9790
9463
|
});
|
|
9791
9464
|
} else {
|
|
9792
9465
|
remainingErrors.push({
|
|
@@ -9833,37 +9506,26 @@ function getRemoteValueFromValidationError(v) {
|
|
|
9833
9506
|
message: "Unexpected error while checking remote (no value)"
|
|
9834
9507
|
};
|
|
9835
9508
|
}
|
|
9836
|
-
if (typeof v.value !== "object" || v.value === null || !(
|
|
9509
|
+
if (typeof v.value !== "object" || v.value === null || !("path" in v.value)) {
|
|
9837
9510
|
return {
|
|
9838
9511
|
success: false,
|
|
9839
|
-
message: "Unexpected error while checking remote (no
|
|
9512
|
+
message: "Unexpected error while checking remote (no path in value)"
|
|
9840
9513
|
};
|
|
9841
9514
|
}
|
|
9842
|
-
if (typeof v.value.
|
|
9515
|
+
if (typeof v.value.path !== "string") {
|
|
9843
9516
|
return {
|
|
9844
9517
|
success: false,
|
|
9845
|
-
message: "Unexpected error while checking remote (
|
|
9518
|
+
message: "Unexpected error while checking remote (path is not a string)"
|
|
9846
9519
|
};
|
|
9847
9520
|
}
|
|
9848
|
-
|
|
9849
|
-
|
|
9850
|
-
|
|
9851
|
-
|
|
9852
|
-
|
|
9853
|
-
message: "Unexpected error while checking remote (metadata is null)"
|
|
9854
|
-
};
|
|
9855
|
-
}
|
|
9856
|
-
if (Array.isArray(v.value.metadata)) {
|
|
9857
|
-
return {
|
|
9858
|
-
success: false,
|
|
9859
|
-
message: "Unexpected error while checking remote (metadata is an array)"
|
|
9860
|
-
};
|
|
9861
|
-
}
|
|
9862
|
-
metadata = v.value.metadata;
|
|
9863
|
-
}
|
|
9521
|
+
// Everything but the path is what Val read from the bytes.
|
|
9522
|
+
const {
|
|
9523
|
+
path,
|
|
9524
|
+
...metadata
|
|
9525
|
+
} = v.value;
|
|
9864
9526
|
return {
|
|
9865
9527
|
success: true,
|
|
9866
|
-
|
|
9528
|
+
path,
|
|
9867
9529
|
metadata
|
|
9868
9530
|
};
|
|
9869
9531
|
}
|
|
@@ -9888,25 +9550,651 @@ async function getFileMetadata(projectRoot, validationError) {
|
|
|
9888
9550
|
return extractFileMetadata(fileRef);
|
|
9889
9551
|
}
|
|
9890
9552
|
|
|
9891
|
-
|
|
9892
|
-
|
|
9893
|
-
|
|
9894
|
-
|
|
9895
|
-
|
|
9896
|
-
|
|
9897
|
-
|
|
9898
|
-
|
|
9899
|
-
|
|
9900
|
-
|
|
9901
|
-
|
|
9902
|
-
|
|
9903
|
-
|
|
9904
|
-
|
|
9905
|
-
|
|
9906
|
-
|
|
9907
|
-
|
|
9908
|
-
|
|
9909
|
-
}
|
|
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`);
|
|
9572
|
+
}
|
|
9573
|
+
const pathsRes = getNewJsonEntryPaths(moduleFilePath, entryKey);
|
|
9574
|
+
if (result.isErr(pathsRes)) {
|
|
9575
|
+
throw Error(formatPatchSourceError(pathsRes.error));
|
|
9576
|
+
}
|
|
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 {
|
|
9910
10198
|
if (anyKey in actualMetadata) {
|
|
9911
10199
|
const key = anyKey;
|
|
9912
10200
|
if (expectedMetadata[key] !== recordMetadata[key]) {
|
|
@@ -10180,9 +10468,9 @@ function getModulePathRange(modulePath, modulePathMap,
|
|
|
10180
10468
|
// Which part of an object/record member to point at. For an object property
|
|
10181
10469
|
// the resolved node's own range is the *key* (property name); the *value*
|
|
10182
10470
|
// range is stored under `children.val`. Array elements, leaf literals and
|
|
10183
|
-
//
|
|
10184
|
-
//
|
|
10185
|
-
//
|
|
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.
|
|
10186
10474
|
target = "key") {
|
|
10187
10475
|
var _range$children;
|
|
10188
10476
|
// Handle invalid module paths gracefully
|
|
@@ -10235,7 +10523,7 @@ target = "key") {
|
|
|
10235
10523
|
*
|
|
10236
10524
|
* NOTE: do not compute the start as `end.character - node.getWidth()`. That
|
|
10237
10525
|
* identity only holds while the node stays on a single line - for a multi-line
|
|
10238
|
-
* node (an object inside an array, a
|
|
10526
|
+
* node (an object inside an array, a media object's `path`, ...) it
|
|
10239
10527
|
* reports the *closing* line and a negative character.
|
|
10240
10528
|
*/
|
|
10241
10529
|
function rangeOf(node, sourceFile) {
|
|
@@ -10299,39 +10587,6 @@ function traverse(node, sourceFile) {
|
|
|
10299
10587
|
if (ts.isArrayLiteralExpression(node)) {
|
|
10300
10588
|
return traverseArrayLiteral(node, sourceFile);
|
|
10301
10589
|
}
|
|
10302
|
-
if (ts.isCallExpression(node)) {
|
|
10303
|
-
return traverseCallExpression(node, sourceFile);
|
|
10304
|
-
}
|
|
10305
|
-
}
|
|
10306
|
-
function traverseCallExpression(node, sourceFile) {
|
|
10307
|
-
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
10308
|
-
if (node.expression.expression.getText(sourceFile) === "c" && (node.expression.name.getText(sourceFile) === "file" || node.expression.name.getText(sourceFile) === "image")) {
|
|
10309
|
-
const val = {
|
|
10310
|
-
children: {},
|
|
10311
|
-
...rangeOf(node, sourceFile)
|
|
10312
|
-
};
|
|
10313
|
-
if (node.arguments[0]) {
|
|
10314
|
-
const _ref = {
|
|
10315
|
-
children: {},
|
|
10316
|
-
...rangeOf(node.arguments[0], sourceFile)
|
|
10317
|
-
};
|
|
10318
|
-
if (!node.arguments[1]) {
|
|
10319
|
-
return {
|
|
10320
|
-
val,
|
|
10321
|
-
_ref
|
|
10322
|
-
};
|
|
10323
|
-
}
|
|
10324
|
-
return {
|
|
10325
|
-
val,
|
|
10326
|
-
_ref,
|
|
10327
|
-
metadata: {
|
|
10328
|
-
children: {},
|
|
10329
|
-
...rangeOf(node.arguments[1], sourceFile)
|
|
10330
|
-
}
|
|
10331
|
-
};
|
|
10332
|
-
}
|
|
10333
|
-
}
|
|
10334
|
-
}
|
|
10335
10590
|
}
|
|
10336
10591
|
function traverseArrayLiteral(node, sourceFile) {
|
|
10337
10592
|
return node.elements.reduce((acc, element, index) => {
|
|
@@ -10405,61 +10660,6 @@ function findJsonEntryFilePath(moduleFilePath, valTsSourceFile, entryKey) {
|
|
|
10405
10660
|
return resolveExistingJsonPath(moduleFilePath, entry.importPath);
|
|
10406
10661
|
}
|
|
10407
10662
|
|
|
10408
|
-
/**
|
|
10409
|
-
* Moves ONE `.jsonValues()` entry that was written inline in the `.val.ts` into
|
|
10410
|
-
* its own `*.val.json`, replacing the inline value with
|
|
10411
|
-
* `c.json(() => import("./<key>.val.json"))`.
|
|
10412
|
-
*
|
|
10413
|
-
* This is the fix for the `jsonValues:extract-entry` validation error. It is not
|
|
10414
|
-
* expressible as a patch (a patch edits one `.val.ts` and cannot create the
|
|
10415
|
-
* backing JSON file), so it writes both files directly — JSON first, so a
|
|
10416
|
-
* failure part-way through never leaves the module pointing at a file that does
|
|
10417
|
-
* not exist.
|
|
10418
|
-
*
|
|
10419
|
-
* Root-only, like the rest of the `.jsonValues()` machinery: the entry is looked
|
|
10420
|
-
* up in the module's root record/router object literal.
|
|
10421
|
-
*/
|
|
10422
|
-
function extractJsonValuesEntry(moduleFilePath, rootDir, entryKey, content, sourceFileHandler) {
|
|
10423
|
-
const valTsPath = sourceFileHandler.resolveSourceModulePath(getSyntheticContainingPath(rootDir), `.${moduleFilePath.replace(".val.ts", ".val").replace(".val.js", ".val").replace(".val.jsx", ".val").replace(".val.tsx", ".val")}`);
|
|
10424
|
-
const sourceFile = sourceFileHandler.getSourceFile(valTsPath);
|
|
10425
|
-
if (!sourceFile) {
|
|
10426
|
-
throw Error(`Source file ${valTsPath} not found`);
|
|
10427
|
-
}
|
|
10428
|
-
const pathsRes = getNewJsonEntryPaths(moduleFilePath, entryKey);
|
|
10429
|
-
if (result.isErr(pathsRes)) {
|
|
10430
|
-
throw Error(formatPatchSourceError(pathsRes.error));
|
|
10431
|
-
}
|
|
10432
|
-
const {
|
|
10433
|
-
jsonPath,
|
|
10434
|
-
importPath
|
|
10435
|
-
} = pathsRes.value;
|
|
10436
|
-
const absoluteJsonPath = path__default.join(rootDir, jsonPath);
|
|
10437
|
-
if (sourceFileHandler.host.fileExists(absoluteJsonPath)) {
|
|
10438
|
-
throw Error(`Cannot extract .jsonValues() entry '${entryKey}' of ${moduleFilePath}: '${jsonPath}' already exists`);
|
|
10439
|
-
}
|
|
10440
|
-
|
|
10441
|
-
// Remove the inline property, then add the `c.json(...)` reference back. The
|
|
10442
|
-
// entry moves to the end of the record: entry ORDER in a jsonValues record is
|
|
10443
|
-
// not meaningful (the Studio and the runtime key entries by name), and
|
|
10444
|
-
// insert-in-place would mean reimplementing insertValJsonEntry.
|
|
10445
|
-
const removed = removeValJsonEntry(sourceFile, [], entryKey);
|
|
10446
|
-
if (result.isErr(removed)) {
|
|
10447
|
-
throw Error(`${valTsPath}\n${formatOpsError(removed.error, sourceFile)}`);
|
|
10448
|
-
}
|
|
10449
|
-
const inserted = insertValJsonEntry(removed.value, [], entryKey, importPath);
|
|
10450
|
-
if (result.isErr(inserted)) {
|
|
10451
|
-
throw Error(`${valTsPath}\n${formatOpsError(inserted.error, removed.value)}`);
|
|
10452
|
-
}
|
|
10453
|
-
sourceFileHandler.writeFile(absoluteJsonPath, JSON.stringify(content, null, 2) + "\n", "utf8");
|
|
10454
|
-
sourceFileHandler.writeSourceFile(inserted.value);
|
|
10455
|
-
}
|
|
10456
|
-
function formatOpsError(error, sourceFile) {
|
|
10457
|
-
if (error instanceof PatchError) {
|
|
10458
|
-
return error.message;
|
|
10459
|
-
}
|
|
10460
|
-
return flatMapErrors(error, e => formatSyntaxError(e, sourceFile)).join("\n");
|
|
10461
|
-
}
|
|
10462
|
-
|
|
10463
10663
|
/**
|
|
10464
10664
|
* Replays a `val debug` snapshot: applies its patches the way /save does and
|
|
10465
10665
|
* validates the result.
|
|
@@ -10559,4 +10759,4 @@ function readCapturedReport(snapshotDir) {
|
|
|
10559
10759
|
return JSON.parse(fs.readFileSync(reportPath, "utf-8"));
|
|
10560
10760
|
}
|
|
10561
10761
|
|
|
10562
|
-
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,
|
|
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 };
|