@valbuild/core 0.125.0 → 0.127.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/CHANGELOG.md +299 -0
- package/dist/declarations/src/index.d.ts +8 -2
- package/dist/declarations/src/initSchema.d.ts +62 -15
- package/dist/declarations/src/initVal.d.ts +108 -0
- package/dist/declarations/src/locale.d.ts +55 -0
- package/dist/declarations/src/render.d.ts +5 -4
- package/dist/declarations/src/schema/array.d.ts +69 -0
- package/dist/declarations/src/schema/boolean.d.ts +44 -0
- package/dist/declarations/src/schema/code.d.ts +63 -0
- package/dist/declarations/src/schema/color.d.ts +54 -0
- package/dist/declarations/src/schema/date.d.ts +64 -2
- package/dist/declarations/src/schema/datetime.d.ts +87 -2
- package/dist/declarations/src/schema/declaredKeys.d.ts +37 -0
- package/dist/declarations/src/schema/discriminatedUnion.d.ts +170 -0
- package/dist/declarations/src/schema/discriminatedUnionBranch.d.ts +20 -0
- package/dist/declarations/src/schema/enum.d.ts +131 -0
- package/dist/declarations/src/schema/file.d.ts +77 -0
- package/dist/declarations/src/schema/image.d.ts +102 -0
- package/dist/declarations/src/schema/index.d.ts +155 -2
- package/dist/declarations/src/schema/keyOf.d.ts +50 -0
- package/dist/declarations/src/schema/literal.d.ts +48 -0
- package/dist/declarations/src/schema/locale.d.ts +132 -0
- package/dist/declarations/src/schema/number.d.ts +58 -0
- package/dist/declarations/src/schema/object.d.ts +70 -0
- package/dist/declarations/src/schema/record.d.ts +187 -1
- package/dist/declarations/src/schema/richtext.d.ts +78 -0
- package/dist/declarations/src/schema/route.d.ts +44 -0
- package/dist/declarations/src/schema/settings.d.ts +54 -1
- package/dist/declarations/src/schema/string.d.ts +99 -0
- package/dist/declarations/src/schema/union.d.ts +54 -91
- package/dist/declarations/src/schema/validation/ValidationFix.d.ts +1 -1
- package/dist/declarations/src/source/settings.d.ts +51 -0
- package/dist/{index-915dd675.esm.js → index-5a6e6398.esm.js} +3057 -573
- package/dist/{index-daa122af.cjs.dev.js → index-889ac441.cjs.dev.js} +3064 -573
- package/dist/{index-a3846321.cjs.prod.js → index-d44deccb.cjs.prod.js} +3064 -573
- package/dist/valbuild-core.cjs.dev.js +9 -2
- package/dist/valbuild-core.cjs.prod.js +9 -2
- package/dist/valbuild-core.esm.js +1 -1
- package/package.json +1 -1
- package/patch/dist/valbuild-core-patch.cjs.dev.js +1 -1
- package/patch/dist/valbuild-core-patch.cjs.prod.js +1 -1
- package/patch/dist/valbuild-core-patch.esm.js +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,304 @@
|
|
|
1
1
|
# @valbuild/core
|
|
2
2
|
|
|
3
|
+
## 0.127.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#608](https://github.com/valbuild/val/pull/608) [`7fa8699`](https://github.com/valbuild/val/commit/7fa869974bc895af992a7d5c18b76253636d7d65) Thanks [@freekh](https://github.com/freekh)! - A record whose schema declares its keys now holds every one of them.
|
|
8
|
+
|
|
9
|
+
Two key schemas enumerate their keys: `s.locale()`, whose set is the project's
|
|
10
|
+
`locales.available`, and a union of literals. For those, the keys are part of the
|
|
11
|
+
schema, so a missing one is a hole in the content rather than content nobody has
|
|
12
|
+
written yet — and validation now says so, naming what is missing.
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
s.record(s.locale(), s.object({ title: s.string() }));
|
|
16
|
+
// Missing key: 'nb-NO'. This record's keys are declared by its schema, so
|
|
17
|
+
// every one of them is an entry — an entry nobody has written yet is null,
|
|
18
|
+
// not absent.
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**An entry nobody has written yet is `null`.** Not an absent key: a null entry is
|
|
22
|
+
data you can count, filter and see in a diff, and it means half-translated
|
|
23
|
+
content stays _valid_ rather than blocking a publish. The value type of such a
|
|
24
|
+
record widens by `null` to match, so writing one in a `.val.ts` type-checks:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
c.define(
|
|
28
|
+
"/content/jacket.val.ts",
|
|
29
|
+
s.record(s.locale(), s.object({ title: s.string() })),
|
|
30
|
+
{
|
|
31
|
+
"en-US": { title: "Winter jacket" },
|
|
32
|
+
"nb-NO": null, // nobody has translated this yet
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**This changes `s.record(s.union(...), item)`**, and closes a gap that was
|
|
38
|
+
already there: `s.record(s.union(s.literal("a"), s.literal("b")), item)` types as
|
|
39
|
+
`Record<"a" | "b", T>`, so TypeScript demanded both keys while the validator only
|
|
40
|
+
checked the ones present. It now checks them too, and — as above — accepts `null`
|
|
41
|
+
for an entry that has not been filled in. If you have such a record with keys
|
|
42
|
+
missing, validation will report them; adding the keys with `null` values is the
|
|
43
|
+
fix, and creating one from the Studio does it for you.
|
|
44
|
+
|
|
45
|
+
`emptyOf` creates these records with every key already in them rather than
|
|
46
|
+
empty, since an empty one is already missing keys. In the Studio use the
|
|
47
|
+
`useEmptyOf()` hook rather than importing `emptyOf` directly: a locale record's
|
|
48
|
+
keys are in the settings module, and the hook is what has read it.
|
|
49
|
+
|
|
50
|
+
- [#608](https://github.com/valbuild/val/pull/608) [`5b7fe05`](https://github.com/valbuild/val/commit/5b7fe05cec6365f9cd1de9ba31e65ed4a87edb63) Thanks [@freekh](https://github.com/freekh)! - `s.locale()`: one of the project's languages.
|
|
51
|
+
|
|
52
|
+
The languages themselves are declared in the settings module (`locales.available`);
|
|
53
|
+
this says that a value is one of them.
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// a field: everything in this entry is in this language
|
|
57
|
+
s.record(s.string(), s.object({ locale: s.locale(), title: s.string() }));
|
|
58
|
+
|
|
59
|
+
// a key: one entry per language
|
|
60
|
+
s.record(s.locale(), s.object({ title: s.string() }));
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Every locale in content is checked against the project's list, the way `keyOf`
|
|
64
|
+
and `route` are checked against what they point at. An undeclared language names
|
|
65
|
+
the ones the project has; a project that has declared none is told to declare
|
|
66
|
+
them rather than told the value is wrong.
|
|
67
|
+
|
|
68
|
+
A locale is stored as the tag itself — the value in content is `nb-NO`, and a
|
|
69
|
+
record keyed by `s.locale()` has `nb-NO` as its key. Spelling one differently
|
|
70
|
+
where it is stored (`/no/…` as a URL segment) is a real need and is deliberately
|
|
71
|
+
not in this release: it changes what is accepted as well as what is shown, so it
|
|
72
|
+
is being designed on its own rather than folded in here.
|
|
73
|
+
|
|
74
|
+
A locale is **never stega encoded**: it ends up in `<html lang>`, in `hreflang`
|
|
75
|
+
and in `Intl` constructors, none of which survive invisible characters.
|
|
76
|
+
|
|
77
|
+
`assistant.translation` joins the settings module alongside `context` and `tone`
|
|
78
|
+
— a note per language, keyed by language, so only the target language's rules are
|
|
79
|
+
sent when translating into it.
|
|
80
|
+
|
|
81
|
+
- [#608](https://github.com/valbuild/val/pull/608) [`88262ac`](https://github.com/valbuild/val/commit/88262ac8db068650a664981ef73556457d87741a) Thanks [@freekh](https://github.com/freekh)! - Locale scopes: a subtree in one language, and one function that answers which.
|
|
82
|
+
|
|
83
|
+
A **locale scope** is content governed by a single language. Two things open one
|
|
84
|
+
today (a third, locale segments in routes, follows):
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// a locale field: this object and everything below it is in one language
|
|
88
|
+
s.object({ locale: s.locale(), title: s.string(), body: s.richtext() });
|
|
89
|
+
|
|
90
|
+
// a locale-keyed record: each entry is in the language its key names
|
|
91
|
+
s.record(s.locale(), s.object({ title: s.string() }));
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**A scope may not contain another scope**, and an object may have only one
|
|
95
|
+
locale field. Both are reported as schema errors, naming what to move:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
An object can be in one language, so it can have one locale field.
|
|
99
|
+
Found 'locale', 'language'.
|
|
100
|
+
|
|
101
|
+
Everything here is already in one language, so 'byLanguage' cannot set
|
|
102
|
+
another. Move the locale-keyed record out of this object, or take the outer
|
|
103
|
+
one away.
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
A scope three levels deep is reported once, by the scope immediately enclosing
|
|
107
|
+
it, rather than by every ancestor.
|
|
108
|
+
|
|
109
|
+
The rule is **validated rather than typed**. Expressing "no scope below this
|
|
110
|
+
one" as a type constraint means threading it through every schema class's type
|
|
111
|
+
parameter, and the errors a recursive constraint like that produces name the
|
|
112
|
+
whole tree — an unrelated typo in a `.val.ts` would print pages.
|
|
113
|
+
|
|
114
|
+
`localeAt(path, snapshot)` (from `@valbuild/shared/internal`) answers which
|
|
115
|
+
language governs a path, and is the one implementation of that question, so the
|
|
116
|
+
Studio, the server and the validation worker cannot disagree. It returns the
|
|
117
|
+
tag, which is what `<html lang>`, `Intl` and `locales.available` all want.
|
|
118
|
+
|
|
119
|
+
It answers `null` where no scope governs the path, where the project has
|
|
120
|
+
declared no languages, and where a locale field holds something that is not one
|
|
121
|
+
of them — validation is already reporting the last, and guessing would put a
|
|
122
|
+
language in `<html lang>` that nobody chose.
|
|
123
|
+
|
|
124
|
+
- [#608](https://github.com/valbuild/val/pull/608) [`29811c3`](https://github.com/valbuild/val/commit/29811c3f8c7e001a950e6f4833af6888e6a4efea) Thanks [@freekh](https://github.com/freekh)! - Settings: declare the languages a project publishes.
|
|
125
|
+
|
|
126
|
+
A new `locales` section in the settings module says which languages a project
|
|
127
|
+
has:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
export default c.define("/settings.val.ts", s.settings(), {
|
|
131
|
+
locales: {
|
|
132
|
+
available: ["en-US", "fr-FR", "nb-NO"],
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The order is the project's own and is kept rather than sorted: it is the order
|
|
138
|
+
of the Studio's locale picker and of the rows in a locale-keyed record. There
|
|
139
|
+
is no default language — every locale-specific field asks which language it is
|
|
140
|
+
in, and a default is the answer that lets that question go unanswered.
|
|
141
|
+
|
|
142
|
+
Like every other settings section it is optional, so a project that is not
|
|
143
|
+
translated writes nothing and sees nothing: no locale controls appear anywhere
|
|
144
|
+
until `available` has something in it.
|
|
145
|
+
|
|
146
|
+
This is content rather than configuration, and deliberately: which languages a
|
|
147
|
+
site has is a decision the people who write it make, and under a build-time
|
|
148
|
+
constant it took a developer and a deploy. It is the same move `assistant`
|
|
149
|
+
already makes with `enabled`.
|
|
150
|
+
|
|
151
|
+
Tags are BCP 47 (`en-US`, `nb-NO`), checked through `Intl.getCanonicalLocales` —
|
|
152
|
+
the same implementation `<html lang>` and every `Intl` constructor use — and
|
|
153
|
+
they have to be in canonical form. `nb-no` parses, but nothing else in the stack
|
|
154
|
+
agrees it is the same string as `nb-NO`, and a locale is compared as a string
|
|
155
|
+
everywhere it is used. Validation names the spelling to use, and reports a
|
|
156
|
+
language declared twice on the repeat rather than on the list — that is the row
|
|
157
|
+
to delete, and a message on the list itself would not say which.
|
|
158
|
+
|
|
159
|
+
Edited under Settings → Locales in the Studio, which names each language in its
|
|
160
|
+
own language.
|
|
161
|
+
|
|
162
|
+
### Patch Changes
|
|
163
|
+
|
|
164
|
+
- [#665](https://github.com/valbuild/val/pull/665) [`7072e07`](https://github.com/valbuild/val/commit/7072e07623c953a09ac14388ae22dada0b431ce3) Thanks [@freekh](https://github.com/freekh)! - `.nullable()` no longer drops the field's `.validate(...)` functions.
|
|
165
|
+
|
|
166
|
+
`.nullable()` returns a copy of the schema, and most of the schema classes built
|
|
167
|
+
that copy with an empty list of custom validators — so a validator declared
|
|
168
|
+
before the `.nullable()` was silently thrown away:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
s.number()
|
|
172
|
+
.validate((n) => (n > 100 ? "Too big" : false))
|
|
173
|
+
.nullable(); // the validator never ran
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`array`, `object`, `discriminatedUnion`, `enum`, `number`, `boolean`, `literal`,
|
|
177
|
+
`keyOf`, `date`, `dateTime`, `code`, `color` and `richtext` were affected;
|
|
178
|
+
`string`, `record`, `route`, `file` and `image` already carried them over, which
|
|
179
|
+
is why the bug was easy to miss. Validators now survive `.nullable()` on every
|
|
180
|
+
schema, in either order.
|
|
181
|
+
|
|
182
|
+
A validator on a nullable schema is called with `null` when the value is unset,
|
|
183
|
+
rather than being skipped — the same thing the schemas that never dropped them
|
|
184
|
+
have always done. If yours was written before the `.nullable()`, its argument is
|
|
185
|
+
typed as non-null even though `null` can reach it, so guard for it.
|
|
186
|
+
|
|
187
|
+
## 0.126.0
|
|
188
|
+
|
|
189
|
+
### Minor Changes
|
|
190
|
+
|
|
191
|
+
- [#652](https://github.com/valbuild/val/pull/652) [`f2fe70d`](https://github.com/valbuild/val/commit/f2fe70dab2b65000dfaf289f09c70b4a8291467a) Thanks [@freekh](https://github.com/freekh)! - `s.union` is now `s.discriminatedUnion` and `s.enum`.
|
|
192
|
+
|
|
193
|
+
`s.union` did two unrelated jobs and worked out which one you meant from its
|
|
194
|
+
first argument: a string key meant a tagged union of objects, literal schemas
|
|
195
|
+
meant a set of allowed strings. Those are now two schemas with two names.
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
// A fixed set of strings — presents as a dropdown
|
|
199
|
+
s.enum("primary", "secondary", "ghost"); // Schema<"primary" | "secondary" | "ghost">
|
|
200
|
+
|
|
201
|
+
// One of several object shapes, told apart by a tag field
|
|
202
|
+
s.discriminatedUnion(
|
|
203
|
+
"type",
|
|
204
|
+
s.object({ type: s.literal("hero"), heading: s.string() }),
|
|
205
|
+
s.object({ type: s.literal("quote"), text: s.string() }),
|
|
206
|
+
);
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`s.enum` takes the strings directly, so the `s.literal(...)` wrapper is gone.
|
|
210
|
+
|
|
211
|
+
**`s.union` still works** — it is deprecated, and it builds exactly the schema
|
|
212
|
+
above, so nothing has to change today:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
s.union(s.literal("one"), s.literal("two")); // → s.enum("one", "two")
|
|
216
|
+
s.union("type", pageA, pageB); // → s.discriminatedUnion("type", pageA, pageB)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The two are different kinds of node, and that is the reason for the split. A
|
|
220
|
+
discriminated union is a container: the selected variant's fields are the fields
|
|
221
|
+
being edited, and everything that walks a schema descends through it. An enum is
|
|
222
|
+
a leaf — a string with a closed domain — so nothing recurses into it. Told apart
|
|
223
|
+
only by the shape of `key`, every consumer had to re-derive which one it was
|
|
224
|
+
holding; each now has its own serialized type (`"discriminated-union"` and
|
|
225
|
+
`"enum"`) and Val Studio has a field per kind rather than one field that
|
|
226
|
+
branches.
|
|
227
|
+
|
|
228
|
+
Two behaviour changes fall out of the split, both of them fixes:
|
|
229
|
+
|
|
230
|
+
- A value that is not a string at all now fails an enum's validation with a
|
|
231
|
+
type error. `s.union` of literals only ever checked the value against its
|
|
232
|
+
literals when the value WAS a string, so a number or an object where an enum
|
|
233
|
+
was declared validated clean.
|
|
234
|
+
- An enum field now shows its validation errors in Val Studio where the field
|
|
235
|
+
is opened on its own — the module editor and the canvas's fields column — and
|
|
236
|
+
gets the compact error layout inside an inline list row. It is a leaf now, so
|
|
237
|
+
it goes through the same error rendering as every other leaf field; the string
|
|
238
|
+
union bypassed it and showed nothing in those places.
|
|
239
|
+
|
|
240
|
+
Several latent crashes in the old `s.union` are fixed on the way past, all of
|
|
241
|
+
them cases where it threw a `TypeError` instead of reporting:
|
|
242
|
+
|
|
243
|
+
- A required discriminated union holding `null` now reports a type error rather
|
|
244
|
+
than throwing, and resolving a path underneath a nullable one that is `null`
|
|
245
|
+
gives the error the API promises instead of a crash.
|
|
246
|
+
- `s.literal("")` is a legal discriminator tag, and `s.enum("")` a legal value.
|
|
247
|
+
Both used to be treated as absent by a truthiness check — in path resolution,
|
|
248
|
+
in stega encoding, and in the message that lists a union's valid tags. The
|
|
249
|
+
editor's dropdowns handle them too: an empty value is reserved by the select
|
|
250
|
+
component and had to be mapped around.
|
|
251
|
+
- A variant that omits the discriminator entirely is now reported as the schema
|
|
252
|
+
error it is, instead of throwing while the check looked for it.
|
|
253
|
+
- An enum's value is now indexed for search, like every other string leaf. The
|
|
254
|
+
old string union was never indexed at all, so searching for one of its values
|
|
255
|
+
could not find the field.
|
|
256
|
+
- A nullable discriminated union set to `null` no longer renders a spinner that
|
|
257
|
+
never resolves.
|
|
258
|
+
|
|
259
|
+
`s.discriminatedUnion` also requires at least one variant, as `s.enum` requires
|
|
260
|
+
at least one value: a union with nothing to select is not a thing to write, and
|
|
261
|
+
everything downstream reads the first variant where it needs any.
|
|
262
|
+
|
|
263
|
+
If you read serialized schemas yourself, that is the breaking part: `type` is no
|
|
264
|
+
longer `"union"`, an enum carries `values: string[]` instead of a `key` plus
|
|
265
|
+
`items` of literal schemas, and `UnionSchema` is no longer a class.
|
|
266
|
+
`SerializedUnionSchema`, `SerializedStringUnionSchema`,
|
|
267
|
+
`SerializedObjectUnionSchema` and `UnionSchema` remain as deprecated type
|
|
268
|
+
aliases.
|
|
269
|
+
|
|
270
|
+
### Patch Changes
|
|
271
|
+
|
|
272
|
+
- [#661](https://github.com/valbuild/val/pull/661) [`171208a`](https://github.com/valbuild/val/commit/171208a20177e68ed5a8b1a6fdaabfe893a6aa5f) Thanks [@freekh](https://github.com/freekh)! - Every schema method now has a worked `@example` in its JSDoc, so hovering it in
|
|
273
|
+
your editor shows what to write.
|
|
274
|
+
|
|
275
|
+
That covers the whole builder surface — `.describe()`, `.validate()`,
|
|
276
|
+
`.nullable()`, `.readonly()`, `.hidden()`, `.preview()`, `.render()`, and the
|
|
277
|
+
per-type methods such as `.minLength()`, `.regexp()`, `.raw()`, `.multiline()`,
|
|
278
|
+
`.from()` / `.to()`, `.remote()`, `.jsonValues()` and `.external()` — as well as
|
|
279
|
+
`c.define()`, `c.json()`, `c.external()` and the `val` helpers (`val.attrs`,
|
|
280
|
+
`val.raw`, `val.unstable_getPath` and friends).
|
|
281
|
+
|
|
282
|
+
One of the examples corrected a real trap: a custom validator returns
|
|
283
|
+
`false | string`, so the natural-looking
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
s.string().validate((val) => val.trim() === val || "No surrounding spaces");
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
does not type check — `||` yields `true`, and `true` is not one of the two
|
|
290
|
+
answers. Write it as a ternary instead:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
s.string().validate((val) =>
|
|
294
|
+
val.trim() === val ? false : "No surrounding spaces",
|
|
295
|
+
);
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The examples are checked in CI, not just written: one test asks the TypeScript
|
|
299
|
+
checker for the doc each method actually resolves to and fails if it has no
|
|
300
|
+
`@example`, and another compiles every example it finds.
|
|
301
|
+
|
|
3
302
|
## 0.125.0
|
|
4
303
|
|
|
5
304
|
### Minor Changes
|
|
@@ -54,8 +54,12 @@ export { type SerializedImageSchema, ImageSchema } from "./schema/image.js";
|
|
|
54
54
|
export { type SerializedFileSchema, FileSchema } from "./schema/file.js";
|
|
55
55
|
export { type SerializedDateSchema, DateSchema } from "./schema/date.js";
|
|
56
56
|
export { type SerializedSettingsSchema, SettingsSchema, } from "./schema/settings.js";
|
|
57
|
-
export { type SettingsSource, type AssistantSettingsSource, type AssistantAvailability, ASSISTANT_SETTINGS_MAX_LENGTH, assistantAvailability, } from "./source/settings.js";
|
|
57
|
+
export { type SettingsSource, type AssistantSettingsSource, type LocalesSettingsSource, type AssistantAvailability, ASSISTANT_SETTINGS_MAX_LENGTH, assistantAvailability, } from "./source/settings.js";
|
|
58
58
|
export { type ResolvedSettingsModule, SETTINGS_MODULE_CONVENTION, isRootModuleFilePath, resolveSettingsModule, } from "./settingsModule.js";
|
|
59
|
+
export { type Locale, localeTagError, localeOfValue } from "./locale.js";
|
|
60
|
+
export { type SerializedLocaleSchema, LocaleSchema } from "./schema/locale.js";
|
|
61
|
+
export { type DeclaredKeySet, declaredKeySetOf, missingDeclaredKeys, } from "./schema/declaredKeys.js";
|
|
62
|
+
export { discriminatedUnionBranchOf } from "./schema/discriminatedUnionBranch.js";
|
|
59
63
|
export { type SerializedColorSchema, type ColorOptions, ColorSchema, } from "./schema/color.js";
|
|
60
64
|
export { type SerializedCodeSchema, type CodeOptions, type CodeLanguage, CodeSchema, CODE_LANGUAGES, } from "./schema/code.js";
|
|
61
65
|
export { type ColorFormat, type ParsedColor, COLOR_FORMATS, DEFAULT_COLOR_FORMAT, } from "./schema/colorFormat.js";
|
|
@@ -63,7 +67,9 @@ export { type SerializedDateTimeSchema, DateTimeSchema, } from "./schema/datetim
|
|
|
63
67
|
export { type SerializedKeyOfSchema, KeyOfSchema } from "./schema/keyOf.js";
|
|
64
68
|
export { type SerializedRouteSchema, RouteSchema } from "./schema/route.js";
|
|
65
69
|
export { type SerializedRichTextSchema, RichTextSchema, } from "./schema/richtext.js";
|
|
66
|
-
export { type
|
|
70
|
+
export { type SerializedDiscriminatedUnionSchema, type DiscriminatedUnionSourceOf, DiscriminatedUnionSchema, } from "./schema/discriminatedUnion.js";
|
|
71
|
+
export { type SerializedEnumSchema, EnumSchema } from "./schema/enum.js";
|
|
72
|
+
export { type SerializedUnionSchema, type UnionSchema, type SerializedStringUnionSchema, type SerializedObjectUnionSchema, } from "./schema/union.js";
|
|
67
73
|
export { type SerializedLiteralSchema, LiteralSchema } from "./schema/literal.js";
|
|
68
74
|
export { deserializeSchema } from "./schema/deserialize.js";
|
|
69
75
|
export { type PreviewItem, type ItemPreviewInput, type RecordPreview, type ArrayPreview, type ReifiedPreview, type PreviewScope, previewScope, } from "./preview.js";
|
|
@@ -4,6 +4,8 @@ import { object } from "./schema/object.js";
|
|
|
4
4
|
import { string } from "./schema/string.js";
|
|
5
5
|
import { boolean } from "./schema/boolean.js";
|
|
6
6
|
import { union } from "./schema/union.js";
|
|
7
|
+
import { discriminatedUnion } from "./schema/discriminatedUnion.js";
|
|
8
|
+
import { enumSchema } from "./schema/enum.js";
|
|
7
9
|
import { richtext } from "./schema/richtext.js";
|
|
8
10
|
import { image } from "./schema/image.js";
|
|
9
11
|
import { literal } from "./schema/literal.js";
|
|
@@ -15,6 +17,7 @@ import { date } from "./schema/date.js";
|
|
|
15
17
|
import { datetime } from "./schema/datetime.js";
|
|
16
18
|
import { code } from "./schema/code.js";
|
|
17
19
|
import { color } from "./schema/color.js";
|
|
20
|
+
import { locale } from "./schema/locale.js";
|
|
18
21
|
import { route } from "./schema/route.js";
|
|
19
22
|
import { router } from "./schema/router.js";
|
|
20
23
|
import { images } from "./schema/images.js";
|
|
@@ -67,32 +70,58 @@ export type InitSchema = {
|
|
|
67
70
|
*/
|
|
68
71
|
readonly number: typeof number;
|
|
69
72
|
/**
|
|
70
|
-
* Define a
|
|
73
|
+
* Define one of several object shapes, told apart by a tag field.
|
|
71
74
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
+
* The first argument names the field that carries the tag; every object must
|
|
76
|
+
* set it to a distinct `s.literal(...)`. The editor shows a dropdown of the
|
|
77
|
+
* tags and the fields of whichever one is selected.
|
|
75
78
|
*
|
|
76
|
-
* @example
|
|
77
|
-
* const schema = s.
|
|
78
|
-
* type: s.literal("
|
|
79
|
-
* value: s.
|
|
80
|
-
*
|
|
81
|
-
* type: s.literal("test2"),
|
|
82
|
-
* value: s.string()
|
|
83
|
-
* }));
|
|
79
|
+
* @example
|
|
80
|
+
* const schema = s.discriminatedUnion("type",
|
|
81
|
+
* s.object({ type: s.literal("text"), value: s.string() }),
|
|
82
|
+
* s.object({ type: s.literal("image"), value: s.image() }),
|
|
83
|
+
* );
|
|
84
84
|
* export default c.define("/example.val.ts", schema, {
|
|
85
|
-
* type: "
|
|
85
|
+
* type: "text",
|
|
86
86
|
* value: "test"
|
|
87
87
|
* });
|
|
88
88
|
*
|
|
89
89
|
*/
|
|
90
|
+
readonly discriminatedUnion: typeof discriminatedUnion;
|
|
91
|
+
/**
|
|
92
|
+
* Define a string that must be one of a fixed set of values.
|
|
93
|
+
*
|
|
94
|
+
* The editor shows a dropdown of the values, in the order they are given.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* const schema = s.enum("primary", "secondary", "ghost");
|
|
98
|
+
* export default c.define("/example.val.ts", schema, "primary");
|
|
99
|
+
*
|
|
100
|
+
*/
|
|
101
|
+
readonly enum: typeof enumSchema;
|
|
102
|
+
/**
|
|
103
|
+
* Define a union.
|
|
104
|
+
*
|
|
105
|
+
* @deprecated Use `s.discriminatedUnion` for a tagged union of objects, or
|
|
106
|
+
* `s.enum` for one of a fixed set of strings.
|
|
107
|
+
*
|
|
108
|
+
* @example // was: a union of string literals
|
|
109
|
+
* const schema = s.enum("test", "test2");
|
|
110
|
+
*
|
|
111
|
+
* @example // was: a tagged union of objects
|
|
112
|
+
* const schema = s.discriminatedUnion("type",
|
|
113
|
+
* s.object({ type: s.literal("test"), value: s.string() }),
|
|
114
|
+
* s.object({ type: s.literal("test2"), value: s.string() }),
|
|
115
|
+
* );
|
|
116
|
+
*
|
|
117
|
+
*/
|
|
90
118
|
readonly union: typeof union;
|
|
91
119
|
/**
|
|
92
120
|
* Define a rich text.
|
|
93
121
|
*
|
|
94
122
|
* @example
|
|
95
|
-
*
|
|
123
|
+
* // Every tag and style is off until the options ask for it.
|
|
124
|
+
* const schema = s.richtext({ h1: true, bold: true });
|
|
96
125
|
* export default c.define("/example.val.ts", schema, [
|
|
97
126
|
* { tag: "h1", children: ["Title 1"] },
|
|
98
127
|
* ]);
|
|
@@ -119,6 +148,7 @@ export type InitSchema = {
|
|
|
119
148
|
*
|
|
120
149
|
* @example
|
|
121
150
|
* // Backed by a gallery: width, height and mimeType live there.
|
|
151
|
+
* import galleryVal from "./gallery.val"; // an s.images() module
|
|
122
152
|
* const schema = s.image(galleryVal);
|
|
123
153
|
* export default c.define("/example.val.ts", schema, {
|
|
124
154
|
* path: "/public/val/example.png",
|
|
@@ -237,6 +267,20 @@ export type InitSchema = {
|
|
|
237
267
|
* ```
|
|
238
268
|
*/
|
|
239
269
|
readonly route: typeof route;
|
|
270
|
+
/**
|
|
271
|
+
* Define one of the project's languages.
|
|
272
|
+
*
|
|
273
|
+
* The languages themselves are declared in the settings module, under
|
|
274
|
+
* `locales.available` — this says only that a value is one of them, which is
|
|
275
|
+
* checked against that list.
|
|
276
|
+
*
|
|
277
|
+
* @example // a field: everything under this object is in this language
|
|
278
|
+
* const schema = s.object({ locale: s.locale(), title: s.string() });
|
|
279
|
+
*
|
|
280
|
+
* @example // a key: one entry per language
|
|
281
|
+
* const schema = s.record(s.locale(), s.object({ title: s.string() }));
|
|
282
|
+
*/
|
|
283
|
+
readonly locale: typeof locale;
|
|
240
284
|
/**
|
|
241
285
|
* Create a page router.
|
|
242
286
|
* Each key is the path of the page.
|
|
@@ -346,7 +390,9 @@ export declare function initSchema(): {
|
|
|
346
390
|
max?: number;
|
|
347
391
|
min?: number;
|
|
348
392
|
}) => import("./index.js").NumberSchema<number>;
|
|
349
|
-
|
|
393
|
+
discriminatedUnion: <Key extends string, T extends import("./schema/discriminatedUnion.js").DiscriminatedUnionItems<Key>>(key: Key, ...objects: T) => import("./index.js").DiscriminatedUnionSchema<Key, T, import("./index.js").DiscriminatedUnionSourceOf<Key, T>>;
|
|
394
|
+
enum: <T extends string>(values_0: T, ...values: T[]) => import("./index.js").EnumSchema<T>;
|
|
395
|
+
union: typeof union;
|
|
350
396
|
richtext: <O extends import("./index.js").RichTextOptions>(options?: O) => import("./index.js").RichTextSchema<O, import("./index.js").RichTextSource<O>>;
|
|
351
397
|
image: typeof image;
|
|
352
398
|
literal: <T extends string>(value: T) => import("./index.js").LiteralSchema<T>;
|
|
@@ -362,6 +408,7 @@ export declare function initSchema(): {
|
|
|
362
408
|
datetime: (options?: Record<string, never>) => import("./index.js").DateTimeSchema<import("./index.js").RawString>;
|
|
363
409
|
color: (options?: import("./index.js").ColorOptions) => import("./index.js").ColorSchema<import("./index.js").RawString>;
|
|
364
410
|
code: (options?: import("./index.js").CodeOptions) => import("./index.js").CodeSchema<import("./index.js").RawString>;
|
|
411
|
+
locale: () => import("./index.js").LocaleSchema<string>;
|
|
365
412
|
route: <T extends string>(options?: Record<string, never>) => import("./index.js").RouteSchema<T>;
|
|
366
413
|
router: typeof router;
|
|
367
414
|
images: <Accept extends `image/${string}`>(options: import("./schema/images.js").ImagesOptions<Accept>) => import("./index.js").RecordSchema<import("./index.js").ObjectSchema<{
|
|
@@ -4,11 +4,71 @@ import { getValPath as getPath } from "./val/index.js";
|
|
|
4
4
|
import { json } from "./source/json.js";
|
|
5
5
|
import { external } from "./source/external.js";
|
|
6
6
|
export type ContentConstructor = {
|
|
7
|
+
/**
|
|
8
|
+
* Define a content module: a schema, and source that matches it.
|
|
9
|
+
*
|
|
10
|
+
* The first argument is the module's own file path, starting at the root of
|
|
11
|
+
* the project. It has to match the file the call is written in - that is how
|
|
12
|
+
* the Studio, validation and patching find the module again - so a
|
|
13
|
+
* `c.define` is always the default export of one `*.val.ts` file.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const schema = s.object({ title: s.string() });
|
|
17
|
+
* export default c.define("/content/page.val.ts", schema, {
|
|
18
|
+
* title: "Hello",
|
|
19
|
+
* });
|
|
20
|
+
*/
|
|
7
21
|
define: typeof define;
|
|
22
|
+
/**
|
|
23
|
+
* Put one entry of a `.jsonValues()` record in its own `*.val.json` file,
|
|
24
|
+
* loaded lazily.
|
|
25
|
+
*
|
|
26
|
+
* What is written in the module is the `import()` thunk, not the content: the
|
|
27
|
+
* `.val.ts` stays small, and the runtime, the Studio and validation each work
|
|
28
|
+
* an entry at a time. That is what lets a record or a router hold many
|
|
29
|
+
* thousands of entries.
|
|
30
|
+
*
|
|
31
|
+
* Only for the entries of a record that said `.jsonValues()`.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* import { nextAppRouter } from "../val.config";
|
|
35
|
+
* const schema = s
|
|
36
|
+
* .router(nextAppRouter, s.object({ title: s.string() }))
|
|
37
|
+
* .jsonValues();
|
|
38
|
+
* export default c.define("/app/support/[slug]/page.val.ts", schema, {
|
|
39
|
+
* "/support/faq": c.json(() => import("./content/faq.val.json")),
|
|
40
|
+
* });
|
|
41
|
+
*/
|
|
8
42
|
json: typeof json;
|
|
43
|
+
/**
|
|
44
|
+
* The source of a record whose entries live behind an adapter - a database,
|
|
45
|
+
* an HTTP API, a bucket - instead of in the module.
|
|
46
|
+
*
|
|
47
|
+
* It takes no arguments, and there is nothing to fill in: it says only that
|
|
48
|
+
* the entries are somewhere else. WHICH adapter is the schema's business
|
|
49
|
+
* (`.external("posts")`), and the binding is registered where the app is set
|
|
50
|
+
* up.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* const schema = s.record(s.object({ title: s.string() })).external("posts");
|
|
54
|
+
* export default c.define("/content/posts.val.ts", schema, c.external());
|
|
55
|
+
*/
|
|
9
56
|
external: typeof external;
|
|
10
57
|
};
|
|
11
58
|
export type ValConstructor = {
|
|
59
|
+
/**
|
|
60
|
+
* The source path of a value inside a module, or `undefined` for something
|
|
61
|
+
* that did not come from Val.
|
|
62
|
+
*
|
|
63
|
+
* A path is what every Val API addresses content by, so this is the way from
|
|
64
|
+
* a value you are holding back to the thing the Studio, a patch or a
|
|
65
|
+
* validation error names. Unstable: the shape of a path is not part of the
|
|
66
|
+
* public API yet.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* import pageVal from "./page.val";
|
|
70
|
+
* const path = val.unstable_getPath(pageVal); // "/page.val.ts"
|
|
71
|
+
*/
|
|
12
72
|
unstable_getPath: typeof getPath;
|
|
13
73
|
};
|
|
14
74
|
export type ConfigDirectory = `/public` | `/public/${string}`;
|
|
@@ -39,9 +99,57 @@ export type ValConfig = {
|
|
|
39
99
|
};
|
|
40
100
|
};
|
|
41
101
|
export type InitVal = {
|
|
102
|
+
/**
|
|
103
|
+
* The content constructor: `c.define` for a module, `c.json` and
|
|
104
|
+
* `c.external` for entries that live outside it.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* const schema = s.object({ title: s.string() });
|
|
108
|
+
* export default c.define("/content/page.val.ts", schema, {
|
|
109
|
+
* title: "Hello",
|
|
110
|
+
* });
|
|
111
|
+
*/
|
|
42
112
|
c: ContentConstructor;
|
|
113
|
+
/**
|
|
114
|
+
* Helpers for working with content your app is already holding, rather than
|
|
115
|
+
* for declaring it.
|
|
116
|
+
*
|
|
117
|
+
* `@valbuild/next` and `@valbuild/tanstack` add to this: `val.attrs` for
|
|
118
|
+
* visual editing, `val.raw` to strip the stega encoding off a string.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* import pageVal from "./page.val";
|
|
122
|
+
* const path = val.unstable_getPath(pageVal);
|
|
123
|
+
*/
|
|
43
124
|
val: ValConstructor;
|
|
125
|
+
/**
|
|
126
|
+
* The schema constructor: one method per type of content Val can hold.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* const schema = s.object({
|
|
130
|
+
* title: s.string().describe("Shown in the browser tab"),
|
|
131
|
+
* image: s.image().nullable(),
|
|
132
|
+
* });
|
|
133
|
+
* export default c.define("/content/page.val.ts", schema, {
|
|
134
|
+
* title: "Hello",
|
|
135
|
+
* image: null,
|
|
136
|
+
* });
|
|
137
|
+
*/
|
|
44
138
|
s: InitSchema;
|
|
139
|
+
/**
|
|
140
|
+
* The config this `initVal` was called with, as the rest of Val reads it.
|
|
141
|
+
*
|
|
142
|
+
* Exported from `val.config.ts` so that the Val API route and the Studio get
|
|
143
|
+
* the same object the content modules were built with.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* import { initVal } from "@valbuild/core";
|
|
147
|
+
* const { s, c, val, config } = initVal({
|
|
148
|
+
* project: "myorg/myproject",
|
|
149
|
+
* files: { directory: "/public/val" },
|
|
150
|
+
* });
|
|
151
|
+
* export { s, c, val, config };
|
|
152
|
+
*/
|
|
45
153
|
config: ValConfig;
|
|
46
154
|
};
|
|
47
155
|
export declare const initVal: (config?: ValConfig) => InitVal;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language tags, and what makes one wrong.
|
|
3
|
+
*
|
|
4
|
+
* Val does not ship a list of locales. Which languages a project publishes is a
|
|
5
|
+
* property of that project, so it is declared in the settings module — see
|
|
6
|
+
* `LocalesSettingsSource` — and everything downstream reads it from there.
|
|
7
|
+
*
|
|
8
|
+
* What Val does own is the SPELLING. A tag is checked against BCP 47
|
|
9
|
+
* (RFC 5646) through `Intl.getCanonicalLocales`, which is the same
|
|
10
|
+
* implementation the browser, `<html lang>` and every `Intl` constructor use.
|
|
11
|
+
* Delegating rather than writing a matcher is the point: a tag this accepts is
|
|
12
|
+
* a tag those accept.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* What is wrong with `tag` as a language tag, or `false` if nothing is.
|
|
16
|
+
*
|
|
17
|
+
* Canonical form, not merely parseable, and the difference is what makes this
|
|
18
|
+
* worth checking at all. `nb-no` parses; `Intl.DisplayNames` and a
|
|
19
|
+
* case-sensitive comparison against a stored key do not agree about it. Since a
|
|
20
|
+
* locale is compared as a string everywhere — a record key, a URL segment, a
|
|
21
|
+
* value in content — one spelling has to win, and the canonical one is the
|
|
22
|
+
* spelling every other tool already produces.
|
|
23
|
+
*
|
|
24
|
+
* The message names the canonical form where there is one, because "not
|
|
25
|
+
* canonical" is not an instruction and `nb-NO` is.
|
|
26
|
+
*/
|
|
27
|
+
export declare function localeTagError(tag: string): string | false;
|
|
28
|
+
/**
|
|
29
|
+
* A language tag, as content holds it.
|
|
30
|
+
*
|
|
31
|
+
* A *flavoured* string rather than a union or a strict brand. Which languages a
|
|
32
|
+
* project has is declared in its settings module, where the project can change
|
|
33
|
+
* it, so there is no set for a type to enumerate — and a strict brand would make
|
|
34
|
+
* `"nb-NO"` unassignable, which would mean writing every locale in a `.val.ts`
|
|
35
|
+
* through a constructor. Content stays plain data.
|
|
36
|
+
*
|
|
37
|
+
* So this carries no compile-time guarantee, and is not meant to. It names the
|
|
38
|
+
* concept where a bare `string` would say nothing, and the guarantee is the
|
|
39
|
+
* `locale:check-locale` validation against `locales.available`.
|
|
40
|
+
*/
|
|
41
|
+
declare const localeBrand: unique symbol;
|
|
42
|
+
export type Locale = string & {
|
|
43
|
+
readonly [localeBrand]?: "locale";
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* The locale a stored value means, or `null` if it means none of them.
|
|
47
|
+
*
|
|
48
|
+
* A lookup against the project's languages, and today that is all it is. It
|
|
49
|
+
* stays a named function because it is the one place the question is asked —
|
|
50
|
+
* the Studio, the validation worker and the server all come here rather than
|
|
51
|
+
* comparing strings themselves, so they cannot disagree about what a stored
|
|
52
|
+
* value is.
|
|
53
|
+
*/
|
|
54
|
+
export declare function localeOfValue(value: string, available: readonly string[]): string | null;
|
|
55
|
+
export {};
|