@valbuild/server 0.26.0 → 0.27.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/package.json +7 -4
- package/.babelrc.json +0 -5
- package/CHANGELOG.md +0 -0
- package/jest.config.js +0 -4
- package/src/LocalValServer.ts +0 -167
- package/src/ProxyValServer.ts +0 -542
- package/src/SerializedModuleContent.ts +0 -36
- package/src/Service.ts +0 -126
- package/src/ValFS.ts +0 -22
- package/src/ValFSHost.ts +0 -66
- package/src/ValModuleLoader.test.ts +0 -75
- package/src/ValModuleLoader.ts +0 -158
- package/src/ValQuickJSRuntime.ts +0 -85
- package/src/ValServer.ts +0 -24
- package/src/ValSourceFileHandler.ts +0 -57
- package/src/createFixPatch.ts +0 -170
- package/src/createRequestHandler.ts +0 -27
- package/src/expressHelpers.ts +0 -5
- package/src/getCompilerOptions.ts +0 -50
- package/src/hosting.ts +0 -290
- package/src/index.ts +0 -16
- package/src/jwt.ts +0 -93
- package/src/patch/ts/ops.test.ts +0 -937
- package/src/patch/ts/ops.ts +0 -897
- package/src/patch/ts/syntax.ts +0 -371
- package/src/patch/ts/valModule.test.ts +0 -26
- package/src/patch/ts/valModule.ts +0 -110
- package/src/patch/validation.ts +0 -81
- package/src/patchValFile.ts +0 -110
- package/src/readValFile.test.ts +0 -49
- package/src/readValFile.ts +0 -96
- package/test/example-projects/basic-next-javascript/jsconfig.json +0 -8
- package/test/example-projects/basic-next-javascript/package.json +0 -23
- package/test/example-projects/basic-next-javascript/pages/blogs.val.js +0 -20
- package/test/example-projects/basic-next-javascript/val.config.js +0 -4
- package/test/example-projects/basic-next-src-typescript/package.json +0 -23
- package/test/example-projects/basic-next-src-typescript/src/pages/blogs.val.ts +0 -20
- package/test/example-projects/basic-next-src-typescript/src/val.config.ts +0 -5
- package/test/example-projects/basic-next-src-typescript/tsconfig.json +0 -24
- package/test/example-projects/basic-next-typescript/package.json +0 -23
- package/test/example-projects/basic-next-typescript/pages/blogs.val.ts +0 -20
- package/test/example-projects/basic-next-typescript/tsconfig.json +0 -25
- package/test/example-projects/basic-next-typescript/val.config.ts +0 -5
- package/test/example-projects/typescript-description-files/README.md +0 -2
- package/test/example-projects/typescript-description-files/jsconfig.json +0 -8
- package/test/example-projects/typescript-description-files/package.json +0 -23
- package/test/example-projects/typescript-description-files/pages/blogs.val.d.ts +0 -7
- package/test/example-projects/typescript-description-files/pages/blogs.val.js +0 -19
- package/test/example-projects/typescript-description-files/val.config.d.ts +0 -3
- package/test/example-projects/typescript-description-files/val.config.js +0 -5
- package/tsconfig.json +0 -12
package/src/patch/ts/ops.ts
DELETED
@@ -1,897 +0,0 @@
|
|
1
|
-
import ts from "typescript";
|
2
|
-
import { result, array, pipe } from "@valbuild/core/fp";
|
3
|
-
import {
|
4
|
-
validateInitializers,
|
5
|
-
evaluateExpression,
|
6
|
-
findObjectPropertyAssignment,
|
7
|
-
ValSyntaxErrorTree,
|
8
|
-
shallowValidateExpression,
|
9
|
-
isValFileMethodCall,
|
10
|
-
findValFileNodeArg,
|
11
|
-
findValFileMetadataArg,
|
12
|
-
} from "./syntax";
|
13
|
-
import {
|
14
|
-
deepEqual,
|
15
|
-
isNotRoot,
|
16
|
-
Ops,
|
17
|
-
PatchError,
|
18
|
-
JSONValue,
|
19
|
-
parseAndValidateArrayIndex,
|
20
|
-
} from "@valbuild/core/patch";
|
21
|
-
import {
|
22
|
-
AnyRichTextOptions,
|
23
|
-
FILE_REF_PROP,
|
24
|
-
FileSource,
|
25
|
-
RichTextSource,
|
26
|
-
VAL_EXTENSION,
|
27
|
-
} from "@valbuild/core";
|
28
|
-
import { JsonPrimitive } from "@valbuild/core/src/Json";
|
29
|
-
import { LinkSource } from "@valbuild/core/src/source/link";
|
30
|
-
|
31
|
-
type TSOpsResult<T> = result.Result<T, PatchError | ValSyntaxErrorTree>;
|
32
|
-
|
33
|
-
declare module "typescript" {
|
34
|
-
interface PrinterOptions {
|
35
|
-
/**
|
36
|
-
* Internal option that stops printing unnecessary ASCII escape sequences
|
37
|
-
* in strings, though it might have unintended effects. Might be useful?
|
38
|
-
*/
|
39
|
-
neverAsciiEscape?: boolean;
|
40
|
-
}
|
41
|
-
}
|
42
|
-
|
43
|
-
function isValidIdentifier(text: string): boolean {
|
44
|
-
if (text.length === 0) {
|
45
|
-
return false;
|
46
|
-
}
|
47
|
-
|
48
|
-
if (!ts.isIdentifierStart(text.charCodeAt(0), ts.ScriptTarget.ES2020)) {
|
49
|
-
return false;
|
50
|
-
}
|
51
|
-
|
52
|
-
for (let i = 1; i < text.length; ++i) {
|
53
|
-
if (!ts.isIdentifierPart(text.charCodeAt(i), ts.ScriptTarget.ES2020)) {
|
54
|
-
return false;
|
55
|
-
}
|
56
|
-
}
|
57
|
-
|
58
|
-
return true;
|
59
|
-
}
|
60
|
-
|
61
|
-
function createPropertyAssignment(key: string, value: JSONValue) {
|
62
|
-
return ts.factory.createPropertyAssignment(
|
63
|
-
isValidIdentifier(key)
|
64
|
-
? ts.factory.createIdentifier(key)
|
65
|
-
: ts.factory.createStringLiteral(key),
|
66
|
-
toExpression(value)
|
67
|
-
);
|
68
|
-
}
|
69
|
-
|
70
|
-
function createValFileReference(value: FileSource) {
|
71
|
-
const args: ts.Expression[] = [
|
72
|
-
ts.factory.createStringLiteral(value[FILE_REF_PROP]),
|
73
|
-
];
|
74
|
-
if (value.metadata) {
|
75
|
-
args.push(toExpression(value.metadata));
|
76
|
-
}
|
77
|
-
|
78
|
-
return ts.factory.createCallExpression(
|
79
|
-
ts.factory.createPropertyAccessExpression(
|
80
|
-
ts.factory.createIdentifier("val"),
|
81
|
-
ts.factory.createIdentifier("file")
|
82
|
-
),
|
83
|
-
undefined,
|
84
|
-
args
|
85
|
-
);
|
86
|
-
}
|
87
|
-
|
88
|
-
function createValLink(value: LinkSource) {
|
89
|
-
const args: ts.Expression[] = [
|
90
|
-
ts.factory.createStringLiteral(value.children[0]),
|
91
|
-
toExpression({ href: value.href }),
|
92
|
-
];
|
93
|
-
|
94
|
-
return ts.factory.createCallExpression(
|
95
|
-
ts.factory.createPropertyAccessExpression(
|
96
|
-
ts.factory.createIdentifier("val"),
|
97
|
-
ts.factory.createIdentifier("link")
|
98
|
-
),
|
99
|
-
undefined,
|
100
|
-
args
|
101
|
-
);
|
102
|
-
}
|
103
|
-
|
104
|
-
function createValRichTextTaggedStringTemplate(
|
105
|
-
value: RichTextSource<AnyRichTextOptions>
|
106
|
-
): ts.Expression {
|
107
|
-
const {
|
108
|
-
templateStrings: [head, ...others],
|
109
|
-
exprs,
|
110
|
-
} = value;
|
111
|
-
const tag = ts.factory.createPropertyAccessExpression(
|
112
|
-
ts.factory.createIdentifier("val"),
|
113
|
-
ts.factory.createIdentifier("richtext")
|
114
|
-
);
|
115
|
-
if (exprs.length > 0) {
|
116
|
-
return ts.factory.createTaggedTemplateExpression(
|
117
|
-
tag,
|
118
|
-
undefined,
|
119
|
-
ts.factory.createTemplateExpression(
|
120
|
-
ts.factory.createTemplateHead(head, head),
|
121
|
-
others.map((s, i) =>
|
122
|
-
ts.factory.createTemplateSpan(
|
123
|
-
toExpression(exprs[i]),
|
124
|
-
i < others.length - 1
|
125
|
-
? ts.factory.createTemplateMiddle(s, s)
|
126
|
-
: ts.factory.createTemplateTail(s, s)
|
127
|
-
)
|
128
|
-
)
|
129
|
-
)
|
130
|
-
);
|
131
|
-
}
|
132
|
-
return ts.factory.createTaggedTemplateExpression(
|
133
|
-
tag,
|
134
|
-
undefined,
|
135
|
-
ts.factory.createNoSubstitutionTemplateLiteral(head, head)
|
136
|
-
);
|
137
|
-
}
|
138
|
-
|
139
|
-
function toExpression(value: JSONValue): ts.Expression {
|
140
|
-
if (typeof value === "string") {
|
141
|
-
// TODO: Use configuration/heuristics to determine use of single quote or double quote
|
142
|
-
return ts.factory.createStringLiteral(value);
|
143
|
-
} else if (typeof value === "number") {
|
144
|
-
return ts.factory.createNumericLiteral(value);
|
145
|
-
} else if (typeof value === "boolean") {
|
146
|
-
return value ? ts.factory.createTrue() : ts.factory.createFalse();
|
147
|
-
} else if (value === null) {
|
148
|
-
return ts.factory.createNull();
|
149
|
-
} else if (Array.isArray(value)) {
|
150
|
-
return ts.factory.createArrayLiteralExpression(value.map(toExpression));
|
151
|
-
} else if (typeof value === "object") {
|
152
|
-
if (isValFileValue(value)) {
|
153
|
-
return createValFileReference(value);
|
154
|
-
} else if (isValLinkValue(value)) {
|
155
|
-
return createValLink(value);
|
156
|
-
} else if (isValRichTextValue(value)) {
|
157
|
-
return createValRichTextTaggedStringTemplate(value);
|
158
|
-
}
|
159
|
-
return ts.factory.createObjectLiteralExpression(
|
160
|
-
Object.entries(value).map(([key, value]) =>
|
161
|
-
createPropertyAssignment(key, value)
|
162
|
-
)
|
163
|
-
);
|
164
|
-
} else {
|
165
|
-
return ts.factory.createStringLiteral(value);
|
166
|
-
}
|
167
|
-
}
|
168
|
-
|
169
|
-
// TODO: Choose newline based on project settings/heuristics/system default?
|
170
|
-
const newLine = ts.NewLineKind.LineFeed;
|
171
|
-
// TODO: Handle indentation of printed code
|
172
|
-
const printer = ts.createPrinter({
|
173
|
-
newLine: newLine,
|
174
|
-
// neverAsciiEscape: true,
|
175
|
-
});
|
176
|
-
|
177
|
-
function replaceNodeValue<T extends ts.Node>(
|
178
|
-
document: ts.SourceFile,
|
179
|
-
node: T,
|
180
|
-
value: JSONValue
|
181
|
-
): [document: ts.SourceFile, replaced: T] {
|
182
|
-
const replacementText = printer.printNode(
|
183
|
-
ts.EmitHint.Unspecified,
|
184
|
-
toExpression(value),
|
185
|
-
document
|
186
|
-
);
|
187
|
-
const span = ts.createTextSpanFromBounds(
|
188
|
-
node.getStart(document, false),
|
189
|
-
node.end
|
190
|
-
);
|
191
|
-
const newText = `${document.text.substring(
|
192
|
-
0,
|
193
|
-
span.start
|
194
|
-
)}${replacementText}${document.text.substring(ts.textSpanEnd(span))}`;
|
195
|
-
|
196
|
-
return [
|
197
|
-
document.update(
|
198
|
-
newText,
|
199
|
-
ts.createTextChangeRange(span, replacementText.length)
|
200
|
-
),
|
201
|
-
node,
|
202
|
-
];
|
203
|
-
}
|
204
|
-
|
205
|
-
function isIndentation(s: string): boolean {
|
206
|
-
for (let i = 0; i < s.length; ++i) {
|
207
|
-
const c = s.charAt(i);
|
208
|
-
if (c !== " " && c !== "\t") {
|
209
|
-
return false;
|
210
|
-
}
|
211
|
-
}
|
212
|
-
return true;
|
213
|
-
}
|
214
|
-
|
215
|
-
function newLineStr(kind: ts.NewLineKind) {
|
216
|
-
if (kind === ts.NewLineKind.CarriageReturnLineFeed) {
|
217
|
-
return "\r\n";
|
218
|
-
} else {
|
219
|
-
return "\n";
|
220
|
-
}
|
221
|
-
}
|
222
|
-
|
223
|
-
function getSeparator(document: ts.SourceFile, neighbor: ts.Node): string {
|
224
|
-
const startPos = neighbor.getStart(document, true);
|
225
|
-
const basis = document.getLineAndCharacterOfPosition(startPos);
|
226
|
-
const lineStartPos = document.getPositionOfLineAndCharacter(basis.line, 0);
|
227
|
-
const maybeIndentation = document.getText().substring(lineStartPos, startPos);
|
228
|
-
|
229
|
-
if (isIndentation(maybeIndentation)) {
|
230
|
-
return `,${newLineStr(newLine)}${maybeIndentation}`;
|
231
|
-
} else {
|
232
|
-
return `, `;
|
233
|
-
}
|
234
|
-
}
|
235
|
-
|
236
|
-
function insertAt<T extends ts.Node>(
|
237
|
-
document: ts.SourceFile,
|
238
|
-
nodes: ts.NodeArray<T>,
|
239
|
-
index: number,
|
240
|
-
node: T
|
241
|
-
): ts.SourceFile {
|
242
|
-
let span: ts.TextSpan;
|
243
|
-
let replacementText: string;
|
244
|
-
if (nodes.length === 0) {
|
245
|
-
// Replace entire range of nodes
|
246
|
-
replacementText = printer.printNode(
|
247
|
-
ts.EmitHint.Unspecified,
|
248
|
-
node,
|
249
|
-
document
|
250
|
-
);
|
251
|
-
span = ts.createTextSpanFromBounds(nodes.pos, nodes.end);
|
252
|
-
} else if (index === nodes.length) {
|
253
|
-
// Insert after last node
|
254
|
-
const neighbor = nodes[nodes.length - 1];
|
255
|
-
replacementText = `${getSeparator(document, neighbor)}${printer.printNode(
|
256
|
-
ts.EmitHint.Unspecified,
|
257
|
-
node,
|
258
|
-
document
|
259
|
-
)}`;
|
260
|
-
span = ts.createTextSpan(neighbor.end, 0);
|
261
|
-
} else {
|
262
|
-
// Insert before node
|
263
|
-
const neighbor = nodes[index];
|
264
|
-
replacementText = `${printer.printNode(
|
265
|
-
ts.EmitHint.Unspecified,
|
266
|
-
node,
|
267
|
-
document
|
268
|
-
)}${getSeparator(document, neighbor)}`;
|
269
|
-
span = ts.createTextSpan(neighbor.getStart(document, true), 0);
|
270
|
-
}
|
271
|
-
|
272
|
-
const newText = `${document.text.substring(
|
273
|
-
0,
|
274
|
-
span.start
|
275
|
-
)}${replacementText}${document.text.substring(ts.textSpanEnd(span))}`;
|
276
|
-
|
277
|
-
return document.update(
|
278
|
-
newText,
|
279
|
-
ts.createTextChangeRange(span, replacementText.length)
|
280
|
-
);
|
281
|
-
}
|
282
|
-
|
283
|
-
function removeAt<T extends ts.Node>(
|
284
|
-
document: ts.SourceFile,
|
285
|
-
nodes: ts.NodeArray<T>,
|
286
|
-
index: number
|
287
|
-
): [document: ts.SourceFile, removed: T] {
|
288
|
-
const node = nodes[index];
|
289
|
-
let span: ts.TextSpan;
|
290
|
-
|
291
|
-
if (nodes.length === 1) {
|
292
|
-
span = ts.createTextSpanFromBounds(nodes.pos, nodes.end);
|
293
|
-
} else if (index === nodes.length - 1) {
|
294
|
-
// Remove until previous node
|
295
|
-
const neighbor = nodes[index - 1];
|
296
|
-
span = ts.createTextSpanFromBounds(neighbor.end, node.end);
|
297
|
-
} else {
|
298
|
-
// Remove before next node
|
299
|
-
const neighbor = nodes[index + 1];
|
300
|
-
span = ts.createTextSpanFromBounds(
|
301
|
-
node.getStart(document, true),
|
302
|
-
neighbor.getStart(document, true)
|
303
|
-
);
|
304
|
-
}
|
305
|
-
const newText = `${document.text.substring(
|
306
|
-
0,
|
307
|
-
span.start
|
308
|
-
)}${document.text.substring(ts.textSpanEnd(span))}`;
|
309
|
-
|
310
|
-
return [document.update(newText, ts.createTextChangeRange(span, 0)), node];
|
311
|
-
}
|
312
|
-
|
313
|
-
function parseAndValidateArrayInsertIndex(
|
314
|
-
key: string,
|
315
|
-
nodes: ReadonlyArray<ts.Expression>
|
316
|
-
): TSOpsResult<number> {
|
317
|
-
if (key === "-") {
|
318
|
-
// For insertion, all nodes up until the insertion index must be valid
|
319
|
-
// initializers
|
320
|
-
const err = validateInitializers(nodes);
|
321
|
-
if (err) {
|
322
|
-
return result.err(err);
|
323
|
-
}
|
324
|
-
return result.ok(nodes.length);
|
325
|
-
}
|
326
|
-
|
327
|
-
return pipe(
|
328
|
-
parseAndValidateArrayIndex(key),
|
329
|
-
result.flatMap((index: number): TSOpsResult<number> => {
|
330
|
-
// For insertion, all nodes up until the insertion index must be valid
|
331
|
-
// initializers
|
332
|
-
const err = validateInitializers(nodes.slice(0, index));
|
333
|
-
if (err) {
|
334
|
-
return result.err(err);
|
335
|
-
}
|
336
|
-
if (index > nodes.length) {
|
337
|
-
return result.err(new PatchError("Array index out of bounds"));
|
338
|
-
} else {
|
339
|
-
return result.ok(index);
|
340
|
-
}
|
341
|
-
})
|
342
|
-
);
|
343
|
-
}
|
344
|
-
|
345
|
-
function parseAndValidateArrayInboundsIndex(
|
346
|
-
key: string,
|
347
|
-
nodes: ReadonlyArray<ts.Expression>
|
348
|
-
): TSOpsResult<number> {
|
349
|
-
return pipe(
|
350
|
-
parseAndValidateArrayIndex(key),
|
351
|
-
result.flatMap((index: number): TSOpsResult<number> => {
|
352
|
-
// For in-bounds operations, all nodes up until and including the index
|
353
|
-
// must be valid initializers
|
354
|
-
const err = validateInitializers(nodes.slice(0, index + 1));
|
355
|
-
if (err) {
|
356
|
-
return result.err(err);
|
357
|
-
}
|
358
|
-
if (index >= nodes.length) {
|
359
|
-
return result.err(new PatchError("Array index out of bounds"));
|
360
|
-
} else {
|
361
|
-
return result.ok(index);
|
362
|
-
}
|
363
|
-
})
|
364
|
-
);
|
365
|
-
}
|
366
|
-
|
367
|
-
function replaceInNode(
|
368
|
-
document: ts.SourceFile,
|
369
|
-
node: ts.Expression,
|
370
|
-
key: string,
|
371
|
-
value: JSONValue
|
372
|
-
): TSOpsResult<[document: ts.SourceFile, replaced: ts.Expression]> {
|
373
|
-
if (ts.isArrayLiteralExpression(node)) {
|
374
|
-
return pipe(
|
375
|
-
parseAndValidateArrayInboundsIndex(key, node.elements),
|
376
|
-
result.map((index: number) =>
|
377
|
-
replaceNodeValue(document, node.elements[index], value)
|
378
|
-
)
|
379
|
-
);
|
380
|
-
} else if (ts.isObjectLiteralExpression(node)) {
|
381
|
-
return pipe(
|
382
|
-
findObjectPropertyAssignment(node, key),
|
383
|
-
result.flatMap((assignment): TSOpsResult<ts.PropertyAssignment> => {
|
384
|
-
if (!assignment) {
|
385
|
-
return result.err(
|
386
|
-
new PatchError("Cannot replace object element which does not exist")
|
387
|
-
);
|
388
|
-
}
|
389
|
-
return result.ok(assignment);
|
390
|
-
}),
|
391
|
-
result.map((assignment: ts.PropertyAssignment) =>
|
392
|
-
replaceNodeValue(document, assignment.initializer, value)
|
393
|
-
)
|
394
|
-
);
|
395
|
-
} else if (isValFileMethodCall(node)) {
|
396
|
-
if (key === FILE_REF_PROP) {
|
397
|
-
if (typeof value !== "string") {
|
398
|
-
return result.err(
|
399
|
-
new PatchError(
|
400
|
-
"Cannot replace val.file reference with non-string value"
|
401
|
-
)
|
402
|
-
);
|
403
|
-
}
|
404
|
-
return pipe(
|
405
|
-
findValFileNodeArg(node),
|
406
|
-
result.map((refNode) => replaceNodeValue(document, refNode, value))
|
407
|
-
);
|
408
|
-
} else {
|
409
|
-
return pipe(
|
410
|
-
findValFileMetadataArg(node),
|
411
|
-
result.flatMap((metadataArgNode) => {
|
412
|
-
if (!metadataArgNode) {
|
413
|
-
return result.err(
|
414
|
-
new PatchError(
|
415
|
-
"Cannot replace in val.file metadata when it does not exist"
|
416
|
-
)
|
417
|
-
);
|
418
|
-
}
|
419
|
-
if (key !== "metadata") {
|
420
|
-
return result.err(
|
421
|
-
new PatchError(
|
422
|
-
`Cannot replace val.file metadata key ${key} when it does not exist`
|
423
|
-
)
|
424
|
-
);
|
425
|
-
}
|
426
|
-
return replaceInNode(
|
427
|
-
document,
|
428
|
-
// TODO: creating a fake object here might not be right - seems to work though
|
429
|
-
ts.factory.createObjectLiteralExpression([
|
430
|
-
ts.factory.createPropertyAssignment(key, metadataArgNode),
|
431
|
-
]),
|
432
|
-
key,
|
433
|
-
value
|
434
|
-
);
|
435
|
-
})
|
436
|
-
);
|
437
|
-
}
|
438
|
-
} else {
|
439
|
-
return result.err(
|
440
|
-
shallowValidateExpression(node) ??
|
441
|
-
new PatchError("Cannot replace in non-object/array")
|
442
|
-
);
|
443
|
-
}
|
444
|
-
}
|
445
|
-
|
446
|
-
function replaceAtPath(
|
447
|
-
document: ts.SourceFile,
|
448
|
-
rootNode: ts.Expression,
|
449
|
-
path: string[],
|
450
|
-
value: JSONValue
|
451
|
-
): TSOpsResult<[document: ts.SourceFile, replaced: ts.Expression]> {
|
452
|
-
if (isNotRoot(path)) {
|
453
|
-
return pipe(
|
454
|
-
getPointerFromPath(rootNode, path),
|
455
|
-
result.flatMap(([node, key]: Pointer) =>
|
456
|
-
replaceInNode(document, node, key, value)
|
457
|
-
)
|
458
|
-
);
|
459
|
-
} else {
|
460
|
-
return result.ok(replaceNodeValue(document, rootNode, value));
|
461
|
-
}
|
462
|
-
}
|
463
|
-
|
464
|
-
export function getFromNode(
|
465
|
-
node: ts.Expression,
|
466
|
-
key: string
|
467
|
-
): TSOpsResult<ts.Expression | undefined> {
|
468
|
-
if (ts.isArrayLiteralExpression(node)) {
|
469
|
-
return pipe(
|
470
|
-
parseAndValidateArrayInboundsIndex(key, node.elements),
|
471
|
-
result.map((index: number) => node.elements[index])
|
472
|
-
);
|
473
|
-
} else if (ts.isObjectLiteralExpression(node)) {
|
474
|
-
return pipe(
|
475
|
-
findObjectPropertyAssignment(node, key),
|
476
|
-
result.map(
|
477
|
-
(assignment: ts.PropertyAssignment | undefined) =>
|
478
|
-
assignment?.initializer
|
479
|
-
)
|
480
|
-
);
|
481
|
-
} else if (isValFileMethodCall(node)) {
|
482
|
-
if (key === FILE_REF_PROP) {
|
483
|
-
return findValFileNodeArg(node);
|
484
|
-
}
|
485
|
-
return findValFileMetadataArg(node);
|
486
|
-
} else {
|
487
|
-
return result.err(
|
488
|
-
shallowValidateExpression(node) ??
|
489
|
-
new PatchError("Cannot access non-object/array")
|
490
|
-
);
|
491
|
-
}
|
492
|
-
}
|
493
|
-
|
494
|
-
type Pointer = [node: ts.Expression, key: string];
|
495
|
-
function getPointerFromPath(
|
496
|
-
node: ts.Expression,
|
497
|
-
path: array.NonEmptyArray<string>
|
498
|
-
): TSOpsResult<Pointer> {
|
499
|
-
let targetNode: ts.Expression = node;
|
500
|
-
let key: string = path[0];
|
501
|
-
for (let i = 0; i < path.length - 1; ++i, key = path[i]) {
|
502
|
-
const childNode = getFromNode(targetNode, key);
|
503
|
-
if (result.isErr(childNode)) {
|
504
|
-
return childNode;
|
505
|
-
}
|
506
|
-
if (childNode.value === undefined) {
|
507
|
-
return result.err(
|
508
|
-
new PatchError("Path refers to non-existing object/array")
|
509
|
-
);
|
510
|
-
}
|
511
|
-
targetNode = childNode.value;
|
512
|
-
}
|
513
|
-
|
514
|
-
return result.ok([targetNode, key]);
|
515
|
-
}
|
516
|
-
|
517
|
-
function getAtPath(
|
518
|
-
rootNode: ts.Expression,
|
519
|
-
path: string[]
|
520
|
-
): TSOpsResult<ts.Expression> {
|
521
|
-
return pipe(
|
522
|
-
path,
|
523
|
-
result.flatMapReduce(
|
524
|
-
(node: ts.Expression, key: string) =>
|
525
|
-
pipe(
|
526
|
-
getFromNode(node, key),
|
527
|
-
result.filterOrElse(
|
528
|
-
(
|
529
|
-
childNode: ts.Expression | undefined
|
530
|
-
): childNode is ts.Expression => childNode !== undefined,
|
531
|
-
(): PatchError | ValSyntaxErrorTree =>
|
532
|
-
new PatchError("Path refers to non-existing object/array")
|
533
|
-
)
|
534
|
-
),
|
535
|
-
rootNode
|
536
|
-
)
|
537
|
-
);
|
538
|
-
}
|
539
|
-
|
540
|
-
function removeFromNode(
|
541
|
-
document: ts.SourceFile,
|
542
|
-
node: ts.Expression,
|
543
|
-
key: string
|
544
|
-
): TSOpsResult<[document: ts.SourceFile, removed: ts.Expression]> {
|
545
|
-
if (ts.isArrayLiteralExpression(node)) {
|
546
|
-
return pipe(
|
547
|
-
parseAndValidateArrayInboundsIndex(key, node.elements),
|
548
|
-
result.map((index: number) => removeAt(document, node.elements, index))
|
549
|
-
);
|
550
|
-
} else if (ts.isObjectLiteralExpression(node)) {
|
551
|
-
return pipe(
|
552
|
-
findObjectPropertyAssignment(node, key),
|
553
|
-
result.flatMap(
|
554
|
-
(
|
555
|
-
assignment: ts.PropertyAssignment | undefined
|
556
|
-
): TSOpsResult<ts.PropertyAssignment> => {
|
557
|
-
if (!assignment) {
|
558
|
-
return result.err(
|
559
|
-
new PatchError(
|
560
|
-
"Cannot replace object element which does not exist"
|
561
|
-
)
|
562
|
-
);
|
563
|
-
}
|
564
|
-
return result.ok(assignment);
|
565
|
-
}
|
566
|
-
),
|
567
|
-
result.map((assignment: ts.PropertyAssignment) => [
|
568
|
-
removeAt(
|
569
|
-
document,
|
570
|
-
node.properties,
|
571
|
-
node.properties.indexOf(assignment)
|
572
|
-
)[0],
|
573
|
-
assignment.initializer,
|
574
|
-
])
|
575
|
-
);
|
576
|
-
} else if (isValFileMethodCall(node)) {
|
577
|
-
if (key === FILE_REF_PROP) {
|
578
|
-
return result.err(new PatchError("Cannot remove a ref from val.file"));
|
579
|
-
} else {
|
580
|
-
return pipe(
|
581
|
-
findValFileMetadataArg(node),
|
582
|
-
result.flatMap((metadataArgNode) => {
|
583
|
-
if (!metadataArgNode) {
|
584
|
-
return result.err(
|
585
|
-
new PatchError(
|
586
|
-
"Cannot remove from val.file metadata when it does not exist"
|
587
|
-
)
|
588
|
-
);
|
589
|
-
}
|
590
|
-
return removeFromNode(document, metadataArgNode, key);
|
591
|
-
})
|
592
|
-
);
|
593
|
-
}
|
594
|
-
} else {
|
595
|
-
return result.err(
|
596
|
-
shallowValidateExpression(node) ??
|
597
|
-
new PatchError("Cannot remove from non-object/array")
|
598
|
-
);
|
599
|
-
}
|
600
|
-
}
|
601
|
-
|
602
|
-
function removeAtPath(
|
603
|
-
document: ts.SourceFile,
|
604
|
-
rootNode: ts.Expression,
|
605
|
-
path: array.NonEmptyArray<string>
|
606
|
-
): TSOpsResult<[document: ts.SourceFile, removed: ts.Expression]> {
|
607
|
-
return pipe(
|
608
|
-
getPointerFromPath(rootNode, path),
|
609
|
-
result.flatMap(([node, key]: Pointer) =>
|
610
|
-
removeFromNode(document, node, key)
|
611
|
-
)
|
612
|
-
);
|
613
|
-
}
|
614
|
-
|
615
|
-
export function isValFileValue(value: JSONValue): value is FileSource<{
|
616
|
-
[key: string]: JsonPrimitive;
|
617
|
-
}> {
|
618
|
-
return !!(
|
619
|
-
typeof value === "object" &&
|
620
|
-
value &&
|
621
|
-
// TODO: replace the below with this:
|
622
|
-
// VAL_EXTENSION in value &&
|
623
|
-
// value[VAL_EXTENSION] === "file" &&
|
624
|
-
FILE_REF_PROP in value &&
|
625
|
-
typeof value[FILE_REF_PROP] === "string"
|
626
|
-
);
|
627
|
-
}
|
628
|
-
function isValLinkValue(value: JSONValue): value is LinkSource {
|
629
|
-
return !!(
|
630
|
-
typeof value === "object" &&
|
631
|
-
value &&
|
632
|
-
VAL_EXTENSION in value &&
|
633
|
-
value[VAL_EXTENSION] === "link"
|
634
|
-
);
|
635
|
-
}
|
636
|
-
|
637
|
-
function isValRichTextValue(
|
638
|
-
value: JSONValue
|
639
|
-
): value is RichTextSource<AnyRichTextOptions> {
|
640
|
-
return !!(
|
641
|
-
typeof value === "object" &&
|
642
|
-
value &&
|
643
|
-
VAL_EXTENSION in value &&
|
644
|
-
value[VAL_EXTENSION] === "richtext" &&
|
645
|
-
"templateStrings" in value &&
|
646
|
-
typeof value.templateStrings === "object" &&
|
647
|
-
Array.isArray(value.templateStrings)
|
648
|
-
);
|
649
|
-
}
|
650
|
-
|
651
|
-
function addToNode(
|
652
|
-
document: ts.SourceFile,
|
653
|
-
node: ts.Expression,
|
654
|
-
key: string,
|
655
|
-
value: JSONValue
|
656
|
-
): TSOpsResult<[document: ts.SourceFile, replaced?: ts.Expression]> {
|
657
|
-
if (ts.isArrayLiteralExpression(node)) {
|
658
|
-
return pipe(
|
659
|
-
parseAndValidateArrayInsertIndex(key, node.elements),
|
660
|
-
result.map((index: number): [document: ts.SourceFile] => [
|
661
|
-
insertAt(document, node.elements, index, toExpression(value)),
|
662
|
-
])
|
663
|
-
);
|
664
|
-
} else if (ts.isObjectLiteralExpression(node)) {
|
665
|
-
if (key === FILE_REF_PROP) {
|
666
|
-
return result.err(new PatchError("Cannot add a key ref to object"));
|
667
|
-
}
|
668
|
-
return pipe(
|
669
|
-
findObjectPropertyAssignment(node, key),
|
670
|
-
result.map(
|
671
|
-
(
|
672
|
-
assignment: ts.PropertyAssignment | undefined
|
673
|
-
): [document: ts.SourceFile, replaced?: ts.Expression] => {
|
674
|
-
if (!assignment) {
|
675
|
-
return [
|
676
|
-
insertAt(
|
677
|
-
document,
|
678
|
-
node.properties,
|
679
|
-
node.properties.length,
|
680
|
-
createPropertyAssignment(key, value)
|
681
|
-
),
|
682
|
-
];
|
683
|
-
} else {
|
684
|
-
return replaceNodeValue(document, assignment.initializer, value);
|
685
|
-
}
|
686
|
-
}
|
687
|
-
)
|
688
|
-
);
|
689
|
-
} else if (isValFileMethodCall(node)) {
|
690
|
-
if (key === FILE_REF_PROP) {
|
691
|
-
if (typeof value !== "string") {
|
692
|
-
return result.err(
|
693
|
-
new PatchError(
|
694
|
-
`Cannot add ${FILE_REF_PROP} key to val.file with non-string value`
|
695
|
-
)
|
696
|
-
);
|
697
|
-
}
|
698
|
-
return pipe(
|
699
|
-
findValFileNodeArg(node),
|
700
|
-
result.map((arg: ts.Expression) =>
|
701
|
-
replaceNodeValue(document, arg, value)
|
702
|
-
)
|
703
|
-
);
|
704
|
-
} else {
|
705
|
-
return pipe(
|
706
|
-
findValFileMetadataArg(node),
|
707
|
-
result.flatMap((metadataArgNode) => {
|
708
|
-
if (metadataArgNode) {
|
709
|
-
return result.err(
|
710
|
-
new PatchError(
|
711
|
-
"Cannot add metadata to val.file when it already exists"
|
712
|
-
)
|
713
|
-
);
|
714
|
-
}
|
715
|
-
if (key !== "metadata") {
|
716
|
-
return result.err(
|
717
|
-
new PatchError(
|
718
|
-
`Cannot add ${key} key to val.file: only metadata is allowed`
|
719
|
-
)
|
720
|
-
);
|
721
|
-
}
|
722
|
-
return result.ok([
|
723
|
-
insertAt(
|
724
|
-
document,
|
725
|
-
node.arguments,
|
726
|
-
node.arguments.length,
|
727
|
-
toExpression(value)
|
728
|
-
),
|
729
|
-
]);
|
730
|
-
})
|
731
|
-
);
|
732
|
-
}
|
733
|
-
} else {
|
734
|
-
return result.err(
|
735
|
-
shallowValidateExpression(node) ??
|
736
|
-
new PatchError("Cannot add to non-object/array")
|
737
|
-
);
|
738
|
-
}
|
739
|
-
}
|
740
|
-
|
741
|
-
function addAtPath(
|
742
|
-
document: ts.SourceFile,
|
743
|
-
rootNode: ts.Expression,
|
744
|
-
path: string[],
|
745
|
-
value: JSONValue
|
746
|
-
): TSOpsResult<[document: ts.SourceFile, replaced?: ts.Expression]> {
|
747
|
-
if (isNotRoot(path)) {
|
748
|
-
return pipe(
|
749
|
-
getPointerFromPath(rootNode, path),
|
750
|
-
result.flatMap(([node, key]: Pointer) =>
|
751
|
-
addToNode(document, node, key, value)
|
752
|
-
)
|
753
|
-
);
|
754
|
-
} else {
|
755
|
-
return result.ok(replaceNodeValue(document, rootNode, value));
|
756
|
-
}
|
757
|
-
}
|
758
|
-
|
759
|
-
function pickDocument<
|
760
|
-
T extends readonly [document: ts.SourceFile, ...rest: unknown[]]
|
761
|
-
>([document]: T) {
|
762
|
-
return document;
|
763
|
-
}
|
764
|
-
|
765
|
-
export class TSOps implements Ops<ts.SourceFile, ValSyntaxErrorTree> {
|
766
|
-
constructor(
|
767
|
-
private findRoot: (
|
768
|
-
document: ts.SourceFile
|
769
|
-
) => result.Result<ts.Expression, ValSyntaxErrorTree>
|
770
|
-
) {}
|
771
|
-
get(document: ts.SourceFile, path: string[]): TSOpsResult<JSONValue> {
|
772
|
-
return pipe(
|
773
|
-
document,
|
774
|
-
this.findRoot,
|
775
|
-
result.flatMap((rootNode: ts.Expression) => getAtPath(rootNode, path)),
|
776
|
-
result.flatMap(evaluateExpression)
|
777
|
-
);
|
778
|
-
}
|
779
|
-
add(
|
780
|
-
document: ts.SourceFile,
|
781
|
-
path: string[],
|
782
|
-
value: JSONValue
|
783
|
-
): TSOpsResult<ts.SourceFile> {
|
784
|
-
return pipe(
|
785
|
-
document,
|
786
|
-
this.findRoot,
|
787
|
-
result.flatMap((rootNode: ts.Expression) =>
|
788
|
-
addAtPath(document, rootNode, path, value)
|
789
|
-
),
|
790
|
-
result.map(pickDocument)
|
791
|
-
);
|
792
|
-
}
|
793
|
-
remove(
|
794
|
-
document: ts.SourceFile,
|
795
|
-
path: array.NonEmptyArray<string>
|
796
|
-
): TSOpsResult<ts.SourceFile> {
|
797
|
-
return pipe(
|
798
|
-
document,
|
799
|
-
this.findRoot,
|
800
|
-
result.flatMap((rootNode: ts.Expression) =>
|
801
|
-
removeAtPath(document, rootNode, path)
|
802
|
-
),
|
803
|
-
result.map(pickDocument)
|
804
|
-
);
|
805
|
-
}
|
806
|
-
replace(
|
807
|
-
document: ts.SourceFile,
|
808
|
-
path: string[],
|
809
|
-
value: JSONValue
|
810
|
-
): TSOpsResult<ts.SourceFile> {
|
811
|
-
return pipe(
|
812
|
-
document,
|
813
|
-
this.findRoot,
|
814
|
-
result.flatMap((rootNode: ts.Expression) =>
|
815
|
-
replaceAtPath(document, rootNode, path, value)
|
816
|
-
),
|
817
|
-
result.map(pickDocument)
|
818
|
-
);
|
819
|
-
}
|
820
|
-
move(
|
821
|
-
document: ts.SourceFile,
|
822
|
-
from: array.NonEmptyArray<string>,
|
823
|
-
path: string[]
|
824
|
-
): TSOpsResult<ts.SourceFile> {
|
825
|
-
return pipe(
|
826
|
-
document,
|
827
|
-
this.findRoot,
|
828
|
-
result.flatMap((rootNode: ts.Expression) =>
|
829
|
-
removeAtPath(document, rootNode, from)
|
830
|
-
),
|
831
|
-
result.flatMap(
|
832
|
-
([document, removedNode]: [
|
833
|
-
doc: ts.SourceFile,
|
834
|
-
removedNode: ts.Expression
|
835
|
-
]) =>
|
836
|
-
pipe(
|
837
|
-
evaluateExpression(removedNode),
|
838
|
-
result.map(
|
839
|
-
(
|
840
|
-
removedValue: JSONValue
|
841
|
-
): [doc: ts.SourceFile, removedValue: JSONValue] => [
|
842
|
-
document,
|
843
|
-
removedValue,
|
844
|
-
]
|
845
|
-
)
|
846
|
-
)
|
847
|
-
),
|
848
|
-
result.flatMap(
|
849
|
-
([document, removedValue]: [
|
850
|
-
document: ts.SourceFile,
|
851
|
-
removedValue: JSONValue
|
852
|
-
]) =>
|
853
|
-
pipe(
|
854
|
-
document,
|
855
|
-
this.findRoot,
|
856
|
-
result.flatMap((root: ts.Expression) =>
|
857
|
-
addAtPath(document, root, path, removedValue)
|
858
|
-
)
|
859
|
-
)
|
860
|
-
),
|
861
|
-
result.map(pickDocument)
|
862
|
-
);
|
863
|
-
}
|
864
|
-
copy(
|
865
|
-
document: ts.SourceFile,
|
866
|
-
from: string[],
|
867
|
-
path: string[]
|
868
|
-
): TSOpsResult<ts.SourceFile> {
|
869
|
-
return pipe(
|
870
|
-
document,
|
871
|
-
this.findRoot,
|
872
|
-
result.flatMap((rootNode: ts.Expression) =>
|
873
|
-
pipe(
|
874
|
-
getAtPath(rootNode, from),
|
875
|
-
result.flatMap(evaluateExpression),
|
876
|
-
result.flatMap((value: JSONValue) =>
|
877
|
-
addAtPath(document, rootNode, path, value)
|
878
|
-
)
|
879
|
-
)
|
880
|
-
),
|
881
|
-
result.map(pickDocument)
|
882
|
-
);
|
883
|
-
}
|
884
|
-
test(
|
885
|
-
document: ts.SourceFile,
|
886
|
-
path: string[],
|
887
|
-
value: JSONValue
|
888
|
-
): TSOpsResult<boolean> {
|
889
|
-
return pipe(
|
890
|
-
document,
|
891
|
-
this.findRoot,
|
892
|
-
result.flatMap((rootNode: ts.Expression) => getAtPath(rootNode, path)),
|
893
|
-
result.flatMap(evaluateExpression),
|
894
|
-
result.map((documentValue: JSONValue) => deepEqual(value, documentValue))
|
895
|
-
);
|
896
|
-
}
|
897
|
-
}
|