@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.
Files changed (51) hide show
  1. package/package.json +7 -4
  2. package/.babelrc.json +0 -5
  3. package/CHANGELOG.md +0 -0
  4. package/jest.config.js +0 -4
  5. package/src/LocalValServer.ts +0 -167
  6. package/src/ProxyValServer.ts +0 -542
  7. package/src/SerializedModuleContent.ts +0 -36
  8. package/src/Service.ts +0 -126
  9. package/src/ValFS.ts +0 -22
  10. package/src/ValFSHost.ts +0 -66
  11. package/src/ValModuleLoader.test.ts +0 -75
  12. package/src/ValModuleLoader.ts +0 -158
  13. package/src/ValQuickJSRuntime.ts +0 -85
  14. package/src/ValServer.ts +0 -24
  15. package/src/ValSourceFileHandler.ts +0 -57
  16. package/src/createFixPatch.ts +0 -170
  17. package/src/createRequestHandler.ts +0 -27
  18. package/src/expressHelpers.ts +0 -5
  19. package/src/getCompilerOptions.ts +0 -50
  20. package/src/hosting.ts +0 -290
  21. package/src/index.ts +0 -16
  22. package/src/jwt.ts +0 -93
  23. package/src/patch/ts/ops.test.ts +0 -937
  24. package/src/patch/ts/ops.ts +0 -897
  25. package/src/patch/ts/syntax.ts +0 -371
  26. package/src/patch/ts/valModule.test.ts +0 -26
  27. package/src/patch/ts/valModule.ts +0 -110
  28. package/src/patch/validation.ts +0 -81
  29. package/src/patchValFile.ts +0 -110
  30. package/src/readValFile.test.ts +0 -49
  31. package/src/readValFile.ts +0 -96
  32. package/test/example-projects/basic-next-javascript/jsconfig.json +0 -8
  33. package/test/example-projects/basic-next-javascript/package.json +0 -23
  34. package/test/example-projects/basic-next-javascript/pages/blogs.val.js +0 -20
  35. package/test/example-projects/basic-next-javascript/val.config.js +0 -4
  36. package/test/example-projects/basic-next-src-typescript/package.json +0 -23
  37. package/test/example-projects/basic-next-src-typescript/src/pages/blogs.val.ts +0 -20
  38. package/test/example-projects/basic-next-src-typescript/src/val.config.ts +0 -5
  39. package/test/example-projects/basic-next-src-typescript/tsconfig.json +0 -24
  40. package/test/example-projects/basic-next-typescript/package.json +0 -23
  41. package/test/example-projects/basic-next-typescript/pages/blogs.val.ts +0 -20
  42. package/test/example-projects/basic-next-typescript/tsconfig.json +0 -25
  43. package/test/example-projects/basic-next-typescript/val.config.ts +0 -5
  44. package/test/example-projects/typescript-description-files/README.md +0 -2
  45. package/test/example-projects/typescript-description-files/jsconfig.json +0 -8
  46. package/test/example-projects/typescript-description-files/package.json +0 -23
  47. package/test/example-projects/typescript-description-files/pages/blogs.val.d.ts +0 -7
  48. package/test/example-projects/typescript-description-files/pages/blogs.val.js +0 -19
  49. package/test/example-projects/typescript-description-files/val.config.d.ts +0 -3
  50. package/test/example-projects/typescript-description-files/val.config.js +0 -5
  51. package/tsconfig.json +0 -12
@@ -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
- }