@valbuild/core 0.12.0 → 0.13.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 (71) hide show
  1. package/jest.config.js +4 -0
  2. package/package.json +1 -1
  3. package/src/Json.ts +4 -0
  4. package/src/expr/README.md +193 -0
  5. package/src/expr/eval.test.ts +202 -0
  6. package/src/expr/eval.ts +248 -0
  7. package/src/expr/expr.ts +91 -0
  8. package/src/expr/index.ts +3 -0
  9. package/src/expr/parser.test.ts +158 -0
  10. package/src/expr/parser.ts +229 -0
  11. package/src/expr/repl.ts +93 -0
  12. package/src/expr/tokenizer.test.ts +539 -0
  13. package/src/expr/tokenizer.ts +117 -0
  14. package/src/fetchVal.test.ts +164 -0
  15. package/src/fetchVal.ts +211 -0
  16. package/src/fp/array.ts +30 -0
  17. package/src/fp/index.ts +3 -0
  18. package/src/fp/result.ts +214 -0
  19. package/src/fp/util.ts +52 -0
  20. package/src/index.ts +55 -0
  21. package/src/initSchema.ts +45 -0
  22. package/src/initVal.ts +96 -0
  23. package/src/module.test.ts +170 -0
  24. package/src/module.ts +333 -0
  25. package/src/patch/deref.test.ts +300 -0
  26. package/src/patch/deref.ts +128 -0
  27. package/src/patch/index.ts +11 -0
  28. package/src/patch/json.test.ts +583 -0
  29. package/src/patch/json.ts +304 -0
  30. package/src/patch/operation.ts +74 -0
  31. package/src/patch/ops.ts +83 -0
  32. package/src/patch/parse.test.ts +202 -0
  33. package/src/patch/parse.ts +187 -0
  34. package/src/patch/patch.ts +46 -0
  35. package/src/patch/util.ts +67 -0
  36. package/src/schema/array.ts +52 -0
  37. package/src/schema/boolean.ts +38 -0
  38. package/src/schema/i18n.ts +65 -0
  39. package/src/schema/image.ts +70 -0
  40. package/src/schema/index.ts +46 -0
  41. package/src/schema/literal.ts +42 -0
  42. package/src/schema/number.ts +45 -0
  43. package/src/schema/object.ts +67 -0
  44. package/src/schema/oneOf.ts +60 -0
  45. package/src/schema/richtext.ts +417 -0
  46. package/src/schema/string.ts +49 -0
  47. package/src/schema/union.ts +62 -0
  48. package/src/selector/ExprProxy.test.ts +203 -0
  49. package/src/selector/ExprProxy.ts +209 -0
  50. package/src/selector/SelectorProxy.test.ts +172 -0
  51. package/src/selector/SelectorProxy.ts +237 -0
  52. package/src/selector/array.ts +37 -0
  53. package/src/selector/boolean.ts +4 -0
  54. package/src/selector/file.ts +14 -0
  55. package/src/selector/i18n.ts +13 -0
  56. package/src/selector/index.ts +159 -0
  57. package/src/selector/number.ts +4 -0
  58. package/src/selector/object.ts +22 -0
  59. package/src/selector/primitive.ts +17 -0
  60. package/src/selector/remote.ts +9 -0
  61. package/src/selector/selector.test.ts +453 -0
  62. package/src/selector/selectorOf.ts +7 -0
  63. package/src/selector/string.ts +4 -0
  64. package/src/source/file.ts +45 -0
  65. package/src/source/i18n.ts +60 -0
  66. package/src/source/index.ts +50 -0
  67. package/src/source/remote.ts +54 -0
  68. package/src/val/array.ts +10 -0
  69. package/src/val/index.ts +90 -0
  70. package/src/val/object.ts +13 -0
  71. package/src/val/primitive.ts +8 -0
@@ -0,0 +1,453 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { Selector, GenericSelector, SourceOrExpr, Path } from ".";
3
+ import { string } from "../schema/string";
4
+ import { array } from "../schema/array";
5
+ import { SourcePath } from "../val";
6
+ import { Source } from "../source";
7
+ import { evaluate } from "../expr/eval";
8
+ import * as expr from "../expr/expr";
9
+ import { result } from "../../fp";
10
+ import { object } from "../schema/object";
11
+ import { newSelectorProxy, selectorToVal } from "./SelectorProxy";
12
+ import { newExprSelectorProxy } from "./ExprProxy";
13
+ import { remote, RemoteSource } from "../source/remote";
14
+
15
+ const modules = {
16
+ "/app/text": "text1",
17
+ "/app/texts": ["text1", "text2"] as string[],
18
+ "/app/blog": { title: "blog1", text: "text1" } as {
19
+ title: string | null;
20
+ text: string;
21
+ },
22
+ "/app/blogs": [
23
+ { title: "blog1", text: "text1" },
24
+ { title: undefined, text: "text2" },
25
+ ] as { title: string | null; text: string }[],
26
+ "/app/empty": "",
27
+ "/app/large/nested": BFV(),
28
+ };
29
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
30
+ const remoteModules: {
31
+ [key in keyof TestModules]: RemoteSource<TestModules[key]>;
32
+ } = {
33
+ "/app/text": remote("/app/text"),
34
+ "/app/texts": remote("/app/texts"),
35
+ "/app/blog": remote("/app/blog"),
36
+ "/app/blogs": remote("/app/blogs"),
37
+ "/app/empty": remote("/app/empty"),
38
+ "/app/large/nested": remote("/app/large/nested"),
39
+ };
40
+
41
+ const SelectorModuleTestCases: {
42
+ description: string;
43
+ input: (remote: boolean) => GenericSelector<Source>;
44
+ expected: Expected;
45
+ }[] = [
46
+ {
47
+ description: "string module lookup",
48
+ input: (remote) => testModule("/app/text", remote),
49
+ expected: {
50
+ val: "text1",
51
+ [Path]: "/app/text",
52
+ },
53
+ },
54
+ {
55
+ description: "basic eq",
56
+ input: (remote) => testModule("/app/text", remote).eq("text1"),
57
+ expected: {
58
+ val: true,
59
+ [Path]: undefined,
60
+ },
61
+ },
62
+ {
63
+ description: "andThen noop",
64
+ input: (remote) => testModule("/app/text", remote).andThen((v) => v),
65
+ expected: {
66
+ val: "text1",
67
+ [Path]: "/app/text",
68
+ },
69
+ },
70
+ {
71
+ description: "array module lookup",
72
+ input: (remote) => testModule("/app/texts", remote),
73
+ expected: {
74
+ val: ["text1", "text2"],
75
+ [Path]: "/app/texts",
76
+ },
77
+ },
78
+ {
79
+ description: "string andThen eq",
80
+ input: (remote) =>
81
+ testModule("/app/text", remote).andThen((v) => v.eq("text1")),
82
+ expected: {
83
+ val: true,
84
+ [Path]: undefined,
85
+ },
86
+ },
87
+ {
88
+ description: "empty string andThen eq",
89
+ input: (remote) =>
90
+ testModule("/app/empty", remote).andThen((v) => v.eq("text1")),
91
+ expected: {
92
+ val: "",
93
+ [Path]: "/app/empty",
94
+ },
95
+ },
96
+ {
97
+ description: "andThen literal eq",
98
+ input: (remote) =>
99
+ testModule("/app/text", remote)
100
+ .andThen(() => "foo")
101
+ .eq("foo"),
102
+ expected: {
103
+ val: true,
104
+ [Path]: undefined,
105
+ },
106
+ },
107
+ {
108
+ description: "andThen undefined literal eq",
109
+ input: (remote) =>
110
+ testModule("/app/text", remote)
111
+ .andThen(() => undefined)
112
+ .eq("foo"),
113
+ expected: {
114
+ val: false,
115
+ [Path]: undefined,
116
+ },
117
+ },
118
+ {
119
+ description: "empty andThen literal eq",
120
+ input: (remote) =>
121
+ testModule("/app/empty", remote)
122
+ .andThen(() => "foo")
123
+ .eq("foo"),
124
+ expected: {
125
+ val: false,
126
+ [Path]: undefined,
127
+ },
128
+ },
129
+ {
130
+ description: "string andThen array literal and index",
131
+ input: (remote) =>
132
+ testModule("/app/text", remote).andThen((v) => [v, "text2"])[0],
133
+ expected: { val: "text1", [Path]: "/app/text" },
134
+ },
135
+ {
136
+ description: "string map undefined -> null literal conversion",
137
+ input: (remote) =>
138
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
139
+ testModule("/app/blogs", remote).map((v) => ({ title: undefined })),
140
+ expected: {
141
+ val: [{ title: null }, { title: null }],
142
+ [Path]: "/app/blogs",
143
+ },
144
+ },
145
+ {
146
+ description: "array map noop",
147
+ input: (remote) => testModule("/app/texts", remote).map((v) => v),
148
+ expected: { val: ["text1", "text2"], [Path]: "/app/texts" },
149
+ },
150
+ {
151
+ description: "array map projection with undefined",
152
+ input: (remote) =>
153
+ testModule("/app/blogs", remote).map((v) => ({
154
+ otherTitle: v.title,
155
+ other: undefined,
156
+ })),
157
+ expected: {
158
+ val: [
159
+ { otherTitle: "blog1", other: null },
160
+ { otherTitle: null, other: null },
161
+ ],
162
+ [Path]: "/app/blogs",
163
+ },
164
+ },
165
+ {
166
+ description: "array index with eq",
167
+ input: (remote) => testModule("/app/texts", remote)[0].eq("text1"),
168
+ expected: { val: true, [Path]: undefined },
169
+ },
170
+ {
171
+ description: "object module lookup",
172
+ input: (remote) => testModule("/app/blog", remote),
173
+ expected: { val: { text: "text1", title: "blog1" }, [Path]: "/app/blog" },
174
+ },
175
+ {
176
+ description: "object andThen property lookup",
177
+ input: (remote) => testModule("/app/blog", remote).andThen((v) => v.title),
178
+ expected: { val: "blog1", [Path]: '/app/blog."title"' },
179
+ },
180
+ {
181
+ description: "array object manipulation: basic indexed obj",
182
+ input: (remote) =>
183
+ testModule("/app/blogs", remote)
184
+ .map((v) => v)[0]
185
+ .title.eq("blog1"),
186
+ expected: {
187
+ val: true,
188
+ [Path]: undefined,
189
+ },
190
+ },
191
+ {
192
+ description: "array object manipulation: filter",
193
+ input: (remote) =>
194
+ testModule("/app/blogs", remote).filter((v) => v.title.eq("blog1")),
195
+ expected: {
196
+ val: [{ text: "text1", title: "blog1" }],
197
+ [Path]: "/app/blogs",
198
+ },
199
+ },
200
+ {
201
+ description: "array object manipulation: map with tuple literal",
202
+ input: (remote) =>
203
+ testModule("/app/blogs", remote).map((a) => [1, a.title]),
204
+ expected: {
205
+ val: [
206
+ [1, "blog1"],
207
+ [1, null],
208
+ ],
209
+ [Path]: "/app/blogs",
210
+ },
211
+ },
212
+ // TODO: tuple literal was reverted
213
+ // {
214
+ // description: "array object manipulation: map with tuple literal",
215
+ // input: (remote) =>
216
+ // testModule("/app/blogs", remote).map((a) => [1, a])[0][1].title,
217
+ // expected: {
218
+ // val: "blog1",
219
+ // [Path]: "/app/blogs.0.title",
220
+ // },
221
+ // },
222
+ {
223
+ description: "array object manipulation: with literals",
224
+ input: (remote) =>
225
+ testModule("/app/blogs", remote)
226
+ .map((v) => ({
227
+ title: {
228
+ foo: "string",
229
+ },
230
+ subTitle: { bar: v.title },
231
+ }))[0]
232
+ .title.foo.eq("string"),
233
+ expected: { val: true, [Path]: undefined },
234
+ },
235
+ {
236
+ description: "array object manipulation: with literals",
237
+ input: (remote) =>
238
+ testModule("/app/blogs", remote)
239
+ .map((v) => [v.title, v.title])[0][0]
240
+ .eq("blog1"),
241
+ expected: { val: true, [Path]: undefined },
242
+ },
243
+ {
244
+ description: "array object manipulation: with large nested objects",
245
+ input: (remote) =>
246
+ testModule("/app/large/nested", remote).map((v) => ({
247
+ title: {
248
+ foo: "string",
249
+ },
250
+ subTitle: { bar: v },
251
+ }))[0].subTitle.bar.that.even.more.even[0].more.even.more.even.more,
252
+ expected: {
253
+ val: "that.even.more.even.more",
254
+ [Path]:
255
+ '/app/large/nested.0."that"."even"."more"."even".0."more"."even"."more"."even"."more"',
256
+ },
257
+ },
258
+ ];
259
+
260
+ const RemoteAndLocaleSelectorModuleTestCases = SelectorModuleTestCases.flatMap(
261
+ (testCase) => [
262
+ {
263
+ input: () => testCase.input(false),
264
+ description: `local ${testCase.description}`,
265
+ expected: testCase.expected,
266
+ remote: false,
267
+ },
268
+ {
269
+ input: () => testCase.input(true),
270
+ description: `remote ${testCase.description}`,
271
+ expected: testCase.expected,
272
+ remote: true,
273
+ },
274
+ ]
275
+ );
276
+
277
+ describe("selector", () => {
278
+ test.each(RemoteAndLocaleSelectorModuleTestCases)(
279
+ "$description",
280
+ ({ input, expected, remote }) => {
281
+ if (input instanceof Error) {
282
+ throw input;
283
+ }
284
+ // TODO: ideally we should be able to evaluate remote and local
285
+ if (!remote) {
286
+ const localeRes = input();
287
+ expect(selectorToVal(localeRes)).toStrictEqual(expected);
288
+ } else {
289
+ const res = evaluate(
290
+ // @ts-expect-error TODO: fix this
291
+ input()[SourceOrExpr],
292
+ (ref) =>
293
+ newSelectorProxy(
294
+ modules[ref as keyof typeof modules],
295
+ ref as SourcePath
296
+ ),
297
+ []
298
+ );
299
+ if (result.isErr(res)) {
300
+ throw res.error;
301
+ }
302
+ expect(selectorToVal(res.value)).toStrictEqual(
303
+ // NOTE: all expected values for REMOTE should be changed to return Vals
304
+ expected
305
+ );
306
+ }
307
+ }
308
+ );
309
+ });
310
+
311
+ type TestModules = typeof modules;
312
+
313
+ type Expected = any; // TODO: should be Val | Expr
314
+
315
+ function testModule<P extends keyof TestModules>(
316
+ sourcePath: P,
317
+ remote: boolean
318
+ ): Selector<TestModules[P]> {
319
+ try {
320
+ if (remote) {
321
+ return newExprSelectorProxy(
322
+ root(sourcePath as SourcePath)
323
+ ) as unknown as Selector<TestModules[P]>;
324
+ }
325
+ return newSelectorProxy(modules[sourcePath], sourcePath as SourcePath);
326
+ } catch (e) {
327
+ // avoid failing all test suite failure on test case creation, instead returns error and throws it inside the test
328
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
329
+ return e as any;
330
+ }
331
+ }
332
+
333
+ /** A big schema */
334
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
335
+ function BFS() {
336
+ return array(
337
+ object({
338
+ title: object({
339
+ foo: object({
340
+ inner: array(
341
+ object({
342
+ innerInnerTitle: object({
343
+ even: object({
344
+ more: string(),
345
+ }),
346
+ }),
347
+ })
348
+ ),
349
+ }),
350
+ }),
351
+ bar: string(),
352
+ many: array(string()),
353
+ props: string(),
354
+ are: string(),
355
+ here: object({
356
+ even: object({
357
+ more: string(),
358
+ }),
359
+ }),
360
+ for: string(),
361
+ testing: string(),
362
+ purposes: string(),
363
+ and: string(),
364
+ to: string(),
365
+ make: string(),
366
+ sure: string(),
367
+ that: object({
368
+ even: object({
369
+ more: object({
370
+ even: array(
371
+ object({
372
+ more: object({
373
+ even: object({
374
+ more: object({
375
+ even: object({
376
+ more: string(),
377
+ }),
378
+ }),
379
+ }),
380
+ }),
381
+ })
382
+ ),
383
+ }),
384
+ }),
385
+ }),
386
+ the: string(),
387
+ type: string(),
388
+ system: string(),
389
+ works: string(),
390
+ as: string(),
391
+ expected: string(),
392
+ })
393
+ );
394
+ }
395
+
396
+ /** A big value */
397
+ function BFV() {
398
+ return [
399
+ {
400
+ title: {
401
+ foo: {
402
+ inner: [
403
+ {
404
+ innerInnerTitle: {
405
+ even: { more: "inner.innerInnerTitle.even.more" },
406
+ },
407
+ },
408
+ ],
409
+ },
410
+ },
411
+ bar: "bar",
412
+ many: ["many1", "many2", "many3"],
413
+ props: "props",
414
+ are: "are",
415
+ here: { even: { more: "here.even.more" } },
416
+ for: "for",
417
+ testing: "testing",
418
+ purposes: "purposes",
419
+ and: "and",
420
+ to: "to",
421
+ make: "make",
422
+ sure: "sure",
423
+ that: {
424
+ even: {
425
+ more: {
426
+ even: [
427
+ {
428
+ more: {
429
+ even: {
430
+ more: { even: { more: "that.even.more.even.more" } },
431
+ },
432
+ },
433
+ },
434
+ ],
435
+ },
436
+ },
437
+ },
438
+ the: "the",
439
+ type: "type",
440
+ system: "system",
441
+ works: "works",
442
+ as: "as",
443
+ expected: "expected",
444
+ },
445
+ ];
446
+ }
447
+
448
+ function root(sourcePath: string) {
449
+ return new expr.Call(
450
+ [new expr.Sym("val"), new expr.StringLiteral(sourcePath)],
451
+ false
452
+ );
453
+ }
@@ -0,0 +1,7 @@
1
+ import { Selector } from ".";
2
+ import { newSelectorProxy } from "./SelectorProxy";
3
+ import { Source } from "../source";
4
+
5
+ export function selectorOf<T extends Source>(t: T): Selector<T> {
6
+ return newSelectorProxy(t);
7
+ }
@@ -0,0 +1,4 @@
1
+ import { Selector as PrimitiveSelector } from "./primitive";
2
+
3
+ // TODO: docs
4
+ export type Selector<T extends string> = PrimitiveSelector<T>;
@@ -0,0 +1,45 @@
1
+ import { VAL_EXTENSION } from ".";
2
+ import { JsonPrimitive } from "../Json";
3
+
4
+ export const FILE_REF_PROP = "_ref" as const;
5
+
6
+ /**
7
+ * A file source represents the path to a (local) file.
8
+ *
9
+ * It will be resolved into a Asset object.
10
+ *
11
+ */
12
+ export type FileSource<
13
+ Metadata extends { readonly [key: string]: JsonPrimitive } | undefined =
14
+ | { readonly [key: string]: JsonPrimitive }
15
+ | undefined
16
+ > = {
17
+ readonly [FILE_REF_PROP]: string;
18
+ readonly [VAL_EXTENSION]: "file";
19
+ readonly metadata?: Metadata;
20
+ };
21
+
22
+ export function file<
23
+ Metadata extends { readonly [key: string]: JsonPrimitive }
24
+ >(ref: string, metadata: Metadata): FileSource<Metadata>;
25
+ export function file(ref: string, metadata?: undefined): FileSource<undefined>;
26
+ export function file<
27
+ Metadata extends { readonly [key: string]: JsonPrimitive } | undefined
28
+ >(ref: string, metadata?: Metadata): FileSource<Metadata> {
29
+ return {
30
+ [FILE_REF_PROP]: ref,
31
+ [VAL_EXTENSION]: "file",
32
+ metadata,
33
+ } as FileSource<Metadata>;
34
+ }
35
+
36
+ export function isFile(obj: unknown): obj is FileSource {
37
+ return (
38
+ typeof obj === "object" &&
39
+ obj !== null &&
40
+ VAL_EXTENSION in obj &&
41
+ obj[VAL_EXTENSION] === "file" &&
42
+ FILE_REF_PROP in obj &&
43
+ typeof obj[FILE_REF_PROP] === "string"
44
+ );
45
+ }
@@ -0,0 +1,60 @@
1
+ import { F } from "ts-toolbelt";
2
+ import { SourcePrimitive, VAL_EXTENSION } from ".";
3
+ import { FileSource } from "./file";
4
+
5
+ /**
6
+ * I18n sources cannot have nested remote sources.
7
+ */
8
+ export type I18nCompatibleSource =
9
+ | SourcePrimitive
10
+ | I18nObject
11
+ | I18nArray
12
+ | FileSource;
13
+ export type I18nObject = { [key in string]: I18nCompatibleSource };
14
+ export type I18nArray = readonly I18nCompatibleSource[];
15
+
16
+ /**
17
+ * An i18n source is a map of locales to sources.
18
+ *
19
+ * Its selector will default to the underlying source. It is possible to call `.all` on i18n sources, which returns an object with all the locales
20
+ *
21
+ */
22
+ export type I18nSource<
23
+ Locales extends readonly string[],
24
+ T extends I18nCompatibleSource
25
+ > = {
26
+ readonly [locale in Locales[number]]: T;
27
+ } & {
28
+ readonly [VAL_EXTENSION]: "i18n";
29
+ };
30
+
31
+ export type I18n<Locales extends readonly string[]> = <
32
+ Src extends I18nCompatibleSource
33
+ >(source: {
34
+ [locale in Locales[number]]: Src;
35
+ }) => I18nSource<Locales, Src>;
36
+ export function i18n<Locales extends readonly string[]>(
37
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
38
+ locales: F.Narrow<Locales>
39
+ ): <Src extends I18nCompatibleSource>(source: {
40
+ [locale in Locales[number]]: Src;
41
+ }) => I18nSource<Locales, Src> {
42
+ return (source) => {
43
+ return {
44
+ ...source,
45
+ [VAL_EXTENSION]: "i18n",
46
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
+ } as I18nSource<Locales, any>;
48
+ };
49
+ }
50
+
51
+ export function isI18n(
52
+ obj: unknown
53
+ ): obj is I18nSource<string[], I18nCompatibleSource> {
54
+ return (
55
+ typeof obj === "object" &&
56
+ obj !== null &&
57
+ VAL_EXTENSION in obj &&
58
+ obj[VAL_EXTENSION] === "i18n"
59
+ );
60
+ }
@@ -0,0 +1,50 @@
1
+ import { FileSource } from "./file";
2
+ import { I18nSource, I18nCompatibleSource } from "./i18n";
3
+ import { RemoteSource, RemoteCompatibleSource } from "./remote";
4
+
5
+ export type Source =
6
+ | SourcePrimitive
7
+ | SourceObject
8
+ | SourceArray
9
+ | I18nSource<string[], I18nCompatibleSource>
10
+ | RemoteSource<RemoteCompatibleSource>
11
+ | FileSource;
12
+
13
+ export type SourceObject = { [key in string]: Source } & {
14
+ // TODO: update these restricted parameters:
15
+ fold?: never;
16
+ andThen?: never;
17
+ _ref?: never;
18
+ _type?: never;
19
+ val?: never;
20
+ valPath?: never; // used when serializing vals
21
+ };
22
+ export type SourceArray = readonly Source[];
23
+ export type SourcePrimitive = string | number | boolean | null;
24
+
25
+ /* Branded extension types: file, remote, i18n */
26
+ export const VAL_EXTENSION = "_type" as const;
27
+
28
+ export function getValExtension(source: Source) {
29
+ return (
30
+ source &&
31
+ typeof source === "object" &&
32
+ VAL_EXTENSION in source &&
33
+ source[VAL_EXTENSION]
34
+ );
35
+ }
36
+
37
+ /**
38
+ * A phantom type parameter is one that doesn't show up at runtime, but is checked statically (and only) at compile time.
39
+ *
40
+ * An example where this is useful is remote types, where the type of the remote source is known at compile time,
41
+ * but the value is not there before it is fetched.
42
+ *
43
+ * @example
44
+ * type Example<T> = string & PhantomType<T>;
45
+ *
46
+ **/
47
+ declare const PhantomType: unique symbol;
48
+ export type PhantomType<T> = {
49
+ [PhantomType]: T;
50
+ };
@@ -0,0 +1,54 @@
1
+ import { SourcePrimitive, VAL_EXTENSION, PhantomType } from ".";
2
+ import { RichText } from "../schema/richtext";
3
+ import { FileSource } from "./file";
4
+ import { I18nCompatibleSource, I18nSource } from "./i18n";
5
+
6
+ /**
7
+ * Remote sources cannot include other remote sources.
8
+ */
9
+ export type RemoteCompatibleSource =
10
+ | SourcePrimitive
11
+ | RemoteObject
12
+ | RemoteArray
13
+ | RichText
14
+ | FileSource
15
+ | I18nSource<string[], I18nCompatibleSource>;
16
+ export type RemoteObject = { [key in string]: RemoteCompatibleSource };
17
+ export type RemoteArray = readonly RemoteCompatibleSource[];
18
+
19
+ export const REMOTE_REF_PROP = "_ref" as const; // TODO: same as FILE_REF_PROP so use same prop?
20
+
21
+ declare const brand: unique symbol;
22
+ export type RemoteRef = string & { readonly [brand]: "RemoteRef" };
23
+
24
+ /**
25
+ * A remote source is a hash that represents a remote object.
26
+ *
27
+ * It will be resolved into a ValRemote object.
28
+ */
29
+ export type RemoteSource<Src extends RemoteCompatibleSource> = {
30
+ readonly [REMOTE_REF_PROP]: RemoteRef;
31
+ readonly [VAL_EXTENSION]: "remote";
32
+ } & PhantomType<Src>;
33
+
34
+ export function remote<Src extends RemoteCompatibleSource>(
35
+ ref: string
36
+ ): RemoteSource<Src> {
37
+ return {
38
+ [REMOTE_REF_PROP]: ref as RemoteRef,
39
+ [VAL_EXTENSION]: "remote",
40
+ } as RemoteSource<Src>;
41
+ }
42
+
43
+ export function isRemote(
44
+ obj: unknown
45
+ ): obj is RemoteSource<RemoteCompatibleSource> {
46
+ return (
47
+ typeof obj === "object" &&
48
+ obj !== null &&
49
+ VAL_EXTENSION in obj &&
50
+ obj[VAL_EXTENSION] === "remote" &&
51
+ REMOTE_REF_PROP in obj &&
52
+ typeof obj[REMOTE_REF_PROP] === "string"
53
+ );
54
+ }
@@ -0,0 +1,10 @@
1
+ import { SourcePath, Val as UnknownVal } from ".";
2
+ import { JsonArray } from "../Json";
3
+ import { Path } from "../selector";
4
+
5
+ export type Val<T extends JsonArray> = {
6
+ readonly [key in keyof T]: UnknownVal<T[key]>;
7
+ } & {
8
+ readonly [Path]: SourcePath | undefined;
9
+ readonly val: T;
10
+ };