@valbuild/core 0.124.0 → 0.126.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 +196 -0
- package/dist/declarations/src/getSourcePathFromRoute.d.ts +12 -3
- package/dist/declarations/src/index.d.ts +7 -3
- package/dist/declarations/src/initSchema.d.ts +46 -15
- package/dist/declarations/src/initVal.d.ts +108 -0
- package/dist/declarations/src/render.d.ts +5 -4
- package/dist/declarations/src/router.d.ts +45 -0
- package/dist/declarations/src/schema/array.d.ts +65 -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/discriminatedUnion.d.ts +166 -0
- package/dist/declarations/src/schema/enum.d.ts +122 -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 +42 -2
- package/dist/declarations/src/schema/keyOf.d.ts +50 -0
- package/dist/declarations/src/schema/literal.d.ts +46 -0
- package/dist/declarations/src/schema/number.d.ts +58 -0
- package/dist/declarations/src/schema/object.d.ts +63 -0
- package/dist/declarations/src/schema/record.d.ts +136 -0
- 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/string.d.ts +99 -0
- package/dist/declarations/src/schema/union.d.ts +54 -91
- package/dist/{index-22a40d42.cjs.prod.js → index-6db78f3c.cjs.prod.js} +2161 -423
- package/dist/{index-fcd087bd.cjs.dev.js → index-b41b638f.cjs.dev.js} +2161 -423
- package/dist/{index-9c14df37.esm.js → index-cd25c2dc.esm.js} +2155 -423
- package/dist/valbuild-core.cjs.dev.js +8 -2
- package/dist/valbuild-core.cjs.prod.js +8 -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,201 @@
|
|
|
1
1
|
# @valbuild/core
|
|
2
2
|
|
|
3
|
+
## 0.126.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#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`.
|
|
8
|
+
|
|
9
|
+
`s.union` did two unrelated jobs and worked out which one you meant from its
|
|
10
|
+
first argument: a string key meant a tagged union of objects, literal schemas
|
|
11
|
+
meant a set of allowed strings. Those are now two schemas with two names.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// A fixed set of strings — presents as a dropdown
|
|
15
|
+
s.enum("primary", "secondary", "ghost"); // Schema<"primary" | "secondary" | "ghost">
|
|
16
|
+
|
|
17
|
+
// One of several object shapes, told apart by a tag field
|
|
18
|
+
s.discriminatedUnion(
|
|
19
|
+
"type",
|
|
20
|
+
s.object({ type: s.literal("hero"), heading: s.string() }),
|
|
21
|
+
s.object({ type: s.literal("quote"), text: s.string() }),
|
|
22
|
+
);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`s.enum` takes the strings directly, so the `s.literal(...)` wrapper is gone.
|
|
26
|
+
|
|
27
|
+
**`s.union` still works** — it is deprecated, and it builds exactly the schema
|
|
28
|
+
above, so nothing has to change today:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
s.union(s.literal("one"), s.literal("two")); // → s.enum("one", "two")
|
|
32
|
+
s.union("type", pageA, pageB); // → s.discriminatedUnion("type", pageA, pageB)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The two are different kinds of node, and that is the reason for the split. A
|
|
36
|
+
discriminated union is a container: the selected variant's fields are the fields
|
|
37
|
+
being edited, and everything that walks a schema descends through it. An enum is
|
|
38
|
+
a leaf — a string with a closed domain — so nothing recurses into it. Told apart
|
|
39
|
+
only by the shape of `key`, every consumer had to re-derive which one it was
|
|
40
|
+
holding; each now has its own serialized type (`"discriminated-union"` and
|
|
41
|
+
`"enum"`) and Val Studio has a field per kind rather than one field that
|
|
42
|
+
branches.
|
|
43
|
+
|
|
44
|
+
Two behaviour changes fall out of the split, both of them fixes:
|
|
45
|
+
|
|
46
|
+
- A value that is not a string at all now fails an enum's validation with a
|
|
47
|
+
type error. `s.union` of literals only ever checked the value against its
|
|
48
|
+
literals when the value WAS a string, so a number or an object where an enum
|
|
49
|
+
was declared validated clean.
|
|
50
|
+
- An enum field now shows its validation errors in Val Studio where the field
|
|
51
|
+
is opened on its own — the module editor and the canvas's fields column — and
|
|
52
|
+
gets the compact error layout inside an inline list row. It is a leaf now, so
|
|
53
|
+
it goes through the same error rendering as every other leaf field; the string
|
|
54
|
+
union bypassed it and showed nothing in those places.
|
|
55
|
+
|
|
56
|
+
Several latent crashes in the old `s.union` are fixed on the way past, all of
|
|
57
|
+
them cases where it threw a `TypeError` instead of reporting:
|
|
58
|
+
|
|
59
|
+
- A required discriminated union holding `null` now reports a type error rather
|
|
60
|
+
than throwing, and resolving a path underneath a nullable one that is `null`
|
|
61
|
+
gives the error the API promises instead of a crash.
|
|
62
|
+
- `s.literal("")` is a legal discriminator tag, and `s.enum("")` a legal value.
|
|
63
|
+
Both used to be treated as absent by a truthiness check — in path resolution,
|
|
64
|
+
in stega encoding, and in the message that lists a union's valid tags. The
|
|
65
|
+
editor's dropdowns handle them too: an empty value is reserved by the select
|
|
66
|
+
component and had to be mapped around.
|
|
67
|
+
- A variant that omits the discriminator entirely is now reported as the schema
|
|
68
|
+
error it is, instead of throwing while the check looked for it.
|
|
69
|
+
- An enum's value is now indexed for search, like every other string leaf. The
|
|
70
|
+
old string union was never indexed at all, so searching for one of its values
|
|
71
|
+
could not find the field.
|
|
72
|
+
- A nullable discriminated union set to `null` no longer renders a spinner that
|
|
73
|
+
never resolves.
|
|
74
|
+
|
|
75
|
+
`s.discriminatedUnion` also requires at least one variant, as `s.enum` requires
|
|
76
|
+
at least one value: a union with nothing to select is not a thing to write, and
|
|
77
|
+
everything downstream reads the first variant where it needs any.
|
|
78
|
+
|
|
79
|
+
If you read serialized schemas yourself, that is the breaking part: `type` is no
|
|
80
|
+
longer `"union"`, an enum carries `values: string[]` instead of a `key` plus
|
|
81
|
+
`items` of literal schemas, and `UnionSchema` is no longer a class.
|
|
82
|
+
`SerializedUnionSchema`, `SerializedStringUnionSchema`,
|
|
83
|
+
`SerializedObjectUnionSchema` and `UnionSchema` remain as deprecated type
|
|
84
|
+
aliases.
|
|
85
|
+
|
|
86
|
+
### Patch Changes
|
|
87
|
+
|
|
88
|
+
- [#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
|
|
89
|
+
your editor shows what to write.
|
|
90
|
+
|
|
91
|
+
That covers the whole builder surface — `.describe()`, `.validate()`,
|
|
92
|
+
`.nullable()`, `.readonly()`, `.hidden()`, `.preview()`, `.render()`, and the
|
|
93
|
+
per-type methods such as `.minLength()`, `.regexp()`, `.raw()`, `.multiline()`,
|
|
94
|
+
`.from()` / `.to()`, `.remote()`, `.jsonValues()` and `.external()` — as well as
|
|
95
|
+
`c.define()`, `c.json()`, `c.external()` and the `val` helpers (`val.attrs`,
|
|
96
|
+
`val.raw`, `val.unstable_getPath` and friends).
|
|
97
|
+
|
|
98
|
+
One of the examples corrected a real trap: a custom validator returns
|
|
99
|
+
`false | string`, so the natural-looking
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
s.string().validate((val) => val.trim() === val || "No surrounding spaces");
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
does not type check — `||` yields `true`, and `true` is not one of the two
|
|
106
|
+
answers. Write it as a ternary instead:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
s.string().validate((val) =>
|
|
110
|
+
val.trim() === val ? false : "No surrounding spaces",
|
|
111
|
+
);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The examples are checked in CI, not just written: one test asks the TypeScript
|
|
115
|
+
checker for the doc each method actually resolves to and fails if it has no
|
|
116
|
+
`@example`, and another compiles every example it finds.
|
|
117
|
+
|
|
118
|
+
## 0.125.0
|
|
119
|
+
|
|
120
|
+
### Minor Changes
|
|
121
|
+
|
|
122
|
+
- [#638](https://github.com/valbuild/val/pull/638) [`c390397`](https://github.com/valbuild/val/commit/c390397cb5e203eb1bedae3a6fab15726e850b90) Thanks [@freekh](https://github.com/freekh)! - Val now runs on TanStack Start.
|
|
123
|
+
|
|
124
|
+
`@valbuild/tanstack` is a new package with everything `@valbuild/next` has: Val
|
|
125
|
+
Studio at `/val`, the on-page overlay and canvas, draft mode, the client hooks,
|
|
126
|
+
server-side content reads, images, and the MCP tools.
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
npm install @valbuild/tanstack
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
// val.config.ts
|
|
134
|
+
import { initVal } from "@valbuild/tanstack";
|
|
135
|
+
const { s, c, config, tanstackRouter } = initVal({ project: "org/project" });
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Routes are first class.** A Val module for a route is named after the route
|
|
139
|
+
file it sits beside — `src/routes/posts.$postId.tsx` is served content by
|
|
140
|
+
`src/routes/posts.$postId.val.ts` — and its keys are the URLs that route serves:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
export default c.define(
|
|
144
|
+
"/src/routes/posts.$postId.val.ts",
|
|
145
|
+
s.router(tanstackRouter, s.object({ title: s.string() })),
|
|
146
|
+
{ "/posts/hello-world": { title: "Hello world" } },
|
|
147
|
+
);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`.` and `/` both separate segments, `$param` is a parameter, `$` is a splat, and
|
|
151
|
+
`index`, `route`, `(groups)` and `_pathless` layouts add no URL segment — the
|
|
152
|
+
same rules TanStack Router uses for the route file itself. Val validates every
|
|
153
|
+
key against that pattern, and Val Studio shows these modules as a sitemap under
|
|
154
|
+
**Pages**, where an editor can add a page.
|
|
155
|
+
|
|
156
|
+
Point the route generator away from your content files, in `vite.config.ts` and
|
|
157
|
+
`tsr.config.json`:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
tanstackStart({ router: { routeFileIgnorePattern: "\\.val\\.[tj]sx?$" } });
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
See the [README](https://github.com/valbuild/val/blob/main/packages/tanstack/README.md)
|
|
164
|
+
for the full wiring, and `examples/tanstack` for a working app.
|
|
165
|
+
|
|
166
|
+
**Also fixed, for everyone:** `Internal.VERSION.core` was `null` in any ESM
|
|
167
|
+
bundle — it was read with `require("../package.json")` inside a `try`, so the
|
|
168
|
+
failure was silent. That version goes into every remote file ref and proxy mode
|
|
169
|
+
refuses to start without it. Every package now reads its version through a
|
|
170
|
+
static JSON import, which is inlined at build time.
|
|
171
|
+
|
|
172
|
+
Two smaller fixes that came out of running the whole toolchain against a
|
|
173
|
+
TanStack project:
|
|
174
|
+
|
|
175
|
+
- `val versions` reports `@valbuild/tanstack`, and `val debug` writes a snapshot
|
|
176
|
+
that names whichever framework package the captured project actually has.
|
|
177
|
+
- `@valbuild/eslint-plugin` reads a `tsconfig.json` that has comments in it. A
|
|
178
|
+
tsconfig is JSONC, and `JSON.parse` is not — so on any project whose tsconfig
|
|
179
|
+
carries a comment (the TanStack starter's does) the
|
|
180
|
+
`module-in-val-modules` rule threw `Expected double-quoted property name in
|
|
181
|
+
JSON` on the first file and took the whole lint run with it.
|
|
182
|
+
|
|
183
|
+
Three fixes found by driving the Studio against a real TanStack app:
|
|
184
|
+
|
|
185
|
+
- **A draft image now loads on the page.** The source the Studio pushes to the
|
|
186
|
+
host page carries `patch_id` for any file whose bytes are still in a patch, so
|
|
187
|
+
the page asks `/api/val/files/...?patch_id=` rather than a `/public` path that
|
|
188
|
+
nothing has written yet. This was silent — the image just did not appear — and
|
|
189
|
+
it affects any page that reads media through the hooks rather than on the
|
|
190
|
+
server, `@valbuild/next` included.
|
|
191
|
+
- **A splat route no longer logs an error on every render.** TanStack returns
|
|
192
|
+
both `_splat` and `*` for the same parameter; Val reported the second as a
|
|
193
|
+
parameter it could not place in the path.
|
|
194
|
+
- The TanStack example and starter render a 404 page instead of throwing
|
|
195
|
+
`notFound()` from a component, which escaped to the error boundary and logged
|
|
196
|
+
`Error in renderToReadableStream` (and, with no not-found component
|
|
197
|
+
configured, aborted the response) on every miss.
|
|
198
|
+
|
|
3
199
|
## 0.124.0
|
|
4
200
|
|
|
5
201
|
### Minor Changes
|
|
@@ -2,14 +2,23 @@ import type { ModuleFilePath, SourcePath } from "./val/index.js";
|
|
|
2
2
|
import type { SerializedSchema } from "./schema/index.js";
|
|
3
3
|
/**
|
|
4
4
|
* Given a URL pathname (e.g. "/blogs/blog-1") and all serialized module schemas,
|
|
5
|
-
* finds the
|
|
6
|
-
* and the source path for that route's content.
|
|
5
|
+
* finds the page-router module that serves that route and returns the module
|
|
6
|
+
* file path and the source path for that route's content.
|
|
7
7
|
*
|
|
8
8
|
* External routers (e.g. external-url-router) are intentionally skipped.
|
|
9
|
-
* Returns null if no
|
|
9
|
+
* Returns null if no page-router module matches the pathname.
|
|
10
10
|
*/
|
|
11
11
|
export declare function getSourcePathFromRoute(pathname: string, schemas: Record<ModuleFilePath, SerializedSchema>): {
|
|
12
12
|
moduleFilePath: ModuleFilePath;
|
|
13
13
|
sourcePath: SourcePath;
|
|
14
14
|
route: string;
|
|
15
15
|
} | null;
|
|
16
|
+
/**
|
|
17
|
+
* Whether a router id is one whose keys are routes of this site.
|
|
18
|
+
*
|
|
19
|
+
* The same question `getSourcePathFromRoute` asks, exported because the Studio
|
|
20
|
+
* asks it too — a page router gets the sitemap and the "Pages" menu, an
|
|
21
|
+
* external one gets a flat list of URLs — and the two must not disagree about
|
|
22
|
+
* which is which.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isPageRouter(routerId: string): boolean;
|
|
@@ -63,20 +63,23 @@ export { type SerializedDateTimeSchema, DateTimeSchema, } from "./schema/datetim
|
|
|
63
63
|
export { type SerializedKeyOfSchema, KeyOfSchema } from "./schema/keyOf.js";
|
|
64
64
|
export { type SerializedRouteSchema, RouteSchema } from "./schema/route.js";
|
|
65
65
|
export { type SerializedRichTextSchema, RichTextSchema, } from "./schema/richtext.js";
|
|
66
|
-
export { type
|
|
66
|
+
export { type SerializedDiscriminatedUnionSchema, type DiscriminatedUnionSourceOf, DiscriminatedUnionSchema, } from "./schema/discriminatedUnion.js";
|
|
67
|
+
export { type SerializedEnumSchema, EnumSchema } from "./schema/enum.js";
|
|
68
|
+
export { type SerializedUnionSchema, type UnionSchema, type SerializedStringUnionSchema, type SerializedObjectUnionSchema, } from "./schema/union.js";
|
|
67
69
|
export { type SerializedLiteralSchema, LiteralSchema } from "./schema/literal.js";
|
|
68
70
|
export { deserializeSchema } from "./schema/deserialize.js";
|
|
69
71
|
export { type PreviewItem, type ItemPreviewInput, type RecordPreview, type ArrayPreview, type ReifiedPreview, type PreviewScope, previewScope, } from "./preview.js";
|
|
70
72
|
export { type InlineRender, type FieldRender, isInlineRender } from "./render.js";
|
|
71
73
|
export type { ValRouter, RouteValidationError } from "./router.js";
|
|
72
|
-
export {
|
|
74
|
+
export { parseNextJsRoutePattern, parseTanStackRoutePattern, tanStackSegmentsOfRoutePath, validateUrlAgainstPattern, } from "./router.js";
|
|
75
|
+
export { getSourcePathFromRoute, isPageRouter } from "./getSourcePathFromRoute.js";
|
|
73
76
|
export declare const FATAL_ERROR_TYPES: readonly ["no-schema", "no-source", "invalid-id", "no-module", "invalid-patch"];
|
|
74
77
|
export type FatalErrorType = (typeof FATAL_ERROR_TYPES)[number];
|
|
75
78
|
export declare const DEFAULT_CONTENT_HOST = "https://content.val.build";
|
|
76
79
|
export declare const DEFAULT_APP_HOST = "https://admin.val.build";
|
|
77
80
|
declare const Internal: {
|
|
78
81
|
VERSION: {
|
|
79
|
-
core: string
|
|
82
|
+
core: string;
|
|
80
83
|
};
|
|
81
84
|
mediaUrl: typeof mediaUrl;
|
|
82
85
|
resolveMedia: typeof resolveMedia;
|
|
@@ -93,6 +96,7 @@ declare const Internal: {
|
|
|
93
96
|
splitModuleFilePathAndModulePath: typeof splitModuleFilePathAndModulePath;
|
|
94
97
|
joinModuleFilePathAndModulePath: typeof joinModuleFilePathAndModulePath;
|
|
95
98
|
nextAppRouter: import("./router.js").ValRouter;
|
|
99
|
+
tanstackRouter: import("./router.js").ValRouter;
|
|
96
100
|
externalPageRouter: import("./router.js").ValRouter;
|
|
97
101
|
color: {
|
|
98
102
|
parseColor: typeof parseColor;
|
|
@@ -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";
|
|
@@ -67,32 +69,58 @@ export type InitSchema = {
|
|
|
67
69
|
*/
|
|
68
70
|
readonly number: typeof number;
|
|
69
71
|
/**
|
|
70
|
-
* Define a
|
|
72
|
+
* Define one of several object shapes, told apart by a tag field.
|
|
71
73
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
74
|
+
* The first argument names the field that carries the tag; every object must
|
|
75
|
+
* set it to a distinct `s.literal(...)`. The editor shows a dropdown of the
|
|
76
|
+
* tags and the fields of whichever one is selected.
|
|
75
77
|
*
|
|
76
|
-
* @example
|
|
77
|
-
* const schema = s.
|
|
78
|
-
* type: s.literal("
|
|
79
|
-
* value: s.
|
|
80
|
-
*
|
|
81
|
-
* type: s.literal("test2"),
|
|
82
|
-
* value: s.string()
|
|
83
|
-
* }));
|
|
78
|
+
* @example
|
|
79
|
+
* const schema = s.discriminatedUnion("type",
|
|
80
|
+
* s.object({ type: s.literal("text"), value: s.string() }),
|
|
81
|
+
* s.object({ type: s.literal("image"), value: s.image() }),
|
|
82
|
+
* );
|
|
84
83
|
* export default c.define("/example.val.ts", schema, {
|
|
85
|
-
* type: "
|
|
84
|
+
* type: "text",
|
|
86
85
|
* value: "test"
|
|
87
86
|
* });
|
|
88
87
|
*
|
|
89
88
|
*/
|
|
89
|
+
readonly discriminatedUnion: typeof discriminatedUnion;
|
|
90
|
+
/**
|
|
91
|
+
* Define a string that must be one of a fixed set of values.
|
|
92
|
+
*
|
|
93
|
+
* The editor shows a dropdown of the values, in the order they are given.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* const schema = s.enum("primary", "secondary", "ghost");
|
|
97
|
+
* export default c.define("/example.val.ts", schema, "primary");
|
|
98
|
+
*
|
|
99
|
+
*/
|
|
100
|
+
readonly enum: typeof enumSchema;
|
|
101
|
+
/**
|
|
102
|
+
* Define a union.
|
|
103
|
+
*
|
|
104
|
+
* @deprecated Use `s.discriminatedUnion` for a tagged union of objects, or
|
|
105
|
+
* `s.enum` for one of a fixed set of strings.
|
|
106
|
+
*
|
|
107
|
+
* @example // was: a union of string literals
|
|
108
|
+
* const schema = s.enum("test", "test2");
|
|
109
|
+
*
|
|
110
|
+
* @example // was: a tagged union of objects
|
|
111
|
+
* const schema = s.discriminatedUnion("type",
|
|
112
|
+
* s.object({ type: s.literal("test"), value: s.string() }),
|
|
113
|
+
* s.object({ type: s.literal("test2"), value: s.string() }),
|
|
114
|
+
* );
|
|
115
|
+
*
|
|
116
|
+
*/
|
|
90
117
|
readonly union: typeof union;
|
|
91
118
|
/**
|
|
92
119
|
* Define a rich text.
|
|
93
120
|
*
|
|
94
121
|
* @example
|
|
95
|
-
*
|
|
122
|
+
* // Every tag and style is off until the options ask for it.
|
|
123
|
+
* const schema = s.richtext({ h1: true, bold: true });
|
|
96
124
|
* export default c.define("/example.val.ts", schema, [
|
|
97
125
|
* { tag: "h1", children: ["Title 1"] },
|
|
98
126
|
* ]);
|
|
@@ -119,6 +147,7 @@ export type InitSchema = {
|
|
|
119
147
|
*
|
|
120
148
|
* @example
|
|
121
149
|
* // Backed by a gallery: width, height and mimeType live there.
|
|
150
|
+
* import galleryVal from "./gallery.val"; // an s.images() module
|
|
122
151
|
* const schema = s.image(galleryVal);
|
|
123
152
|
* export default c.define("/example.val.ts", schema, {
|
|
124
153
|
* path: "/public/val/example.png",
|
|
@@ -346,7 +375,9 @@ export declare function initSchema(): {
|
|
|
346
375
|
max?: number;
|
|
347
376
|
min?: number;
|
|
348
377
|
}) => import("./index.js").NumberSchema<number>;
|
|
349
|
-
|
|
378
|
+
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>>;
|
|
379
|
+
enum: <T extends string>(values_0: T, ...values: T[]) => import("./index.js").EnumSchema<T>;
|
|
380
|
+
union: typeof union;
|
|
350
381
|
richtext: <O extends import("./index.js").RichTextOptions>(options?: O) => import("./index.js").RichTextSchema<O, import("./index.js").RichTextSource<O>>;
|
|
351
382
|
image: typeof image;
|
|
352
383
|
literal: <T extends string>(value: T) => import("./index.js").LiteralSchema<T>;
|
|
@@ -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;
|
|
@@ -59,10 +59,11 @@ export type FieldRender = InlineRender;
|
|
|
59
59
|
* question, and a disagreement between them is a row you can edit in place but
|
|
60
60
|
* that "add" navigates away from.
|
|
61
61
|
*
|
|
62
|
-
* A
|
|
63
|
-
* ANY of its variants does. A page-builder list is
|
|
64
|
-
* block, block, ...))` and the natural
|
|
65
|
-
* blocks, one per block type — the union
|
|
62
|
+
* A discriminated union counts as inline when the union itself declares it OR
|
|
63
|
+
* when ANY of its variants does. A page-builder list is
|
|
64
|
+
* `s.array(s.discriminatedUnion("type", block, block, ...))` and the natural
|
|
65
|
+
* place to write the render is on the blocks, one per block type — the union
|
|
66
|
+
* is a dispatch, not something the
|
|
66
67
|
* author thinks of as the field. `some` rather than `every` because the row
|
|
67
68
|
* draws the union's own editor (the tag selector plus the matched variant's
|
|
68
69
|
* fields), which handles every variant either way: with `every`, adding one
|
|
@@ -23,6 +23,51 @@ export declare const nextAppRouter: ValRouter;
|
|
|
23
23
|
* - /app/(..)(dashboard)/feed/page.val.ts -> ["feed"] (interception route)
|
|
24
24
|
*/
|
|
25
25
|
export declare function parseNextJsRoutePattern(moduleFilePath: string): string[];
|
|
26
|
+
/**
|
|
27
|
+
* Parse a TanStack Router route pattern out of a Val module file path.
|
|
28
|
+
*
|
|
29
|
+
* TanStack Router's file conventions are not Next's, so this is a different
|
|
30
|
+
* parser rather than a flag on the one above. A Val module for a route lives
|
|
31
|
+
* beside the route file and is named the same way — `routes/posts.$postId.tsx`
|
|
32
|
+
* is served content by `routes/posts.$postId.val.ts` — so the pattern is the
|
|
33
|
+
* file path with the conventions applied:
|
|
34
|
+
*
|
|
35
|
+
* - `.` and `/` both separate segments (`posts.$postId.val.ts` and
|
|
36
|
+
* `posts/$postId.val.ts` are the same route)
|
|
37
|
+
* - `$param` is a dynamic segment, `$` on its own is a splat
|
|
38
|
+
* - `index` is the directory's own route and contributes no segment
|
|
39
|
+
* - `route` is a layout for the directory, likewise
|
|
40
|
+
* - `(group)` folders and `_pathless` layout segments are not in the URL
|
|
41
|
+
* - a trailing `_` on a segment opts out of nesting and is not in the URL
|
|
42
|
+
*
|
|
43
|
+
* The result is expressed in the SAME vocabulary the Next parser uses
|
|
44
|
+
* (`[param]`, `[...param]`), because everything downstream — validation, the
|
|
45
|
+
* sitemap, the key inputs in the Studio — is written against that vocabulary
|
|
46
|
+
* and there is no reason for two.
|
|
47
|
+
*
|
|
48
|
+
* - /src/routes/posts.$postId.val.ts -> ["posts", "[postId]"]
|
|
49
|
+
* - /routes/posts/$postId.val.ts -> ["posts", "[postId]"]
|
|
50
|
+
* - /src/routes/index.val.ts -> []
|
|
51
|
+
* - /src/routes/files.$.val.ts -> ["files", "[..._splat]"]
|
|
52
|
+
* - /src/routes/(app)/_layout.about.val.ts -> ["about"]
|
|
53
|
+
*/
|
|
54
|
+
export declare function parseTanStackRoutePattern(moduleFilePath: string): string[];
|
|
55
|
+
/**
|
|
56
|
+
* The URL segments of a TanStack route path, given relative to `routes/`.
|
|
57
|
+
*
|
|
58
|
+
* Exported because the Studio derives the same pattern from the same file path
|
|
59
|
+
* and must not disagree with validation about what a route is.
|
|
60
|
+
*/
|
|
61
|
+
export declare function tanStackSegmentsOfRoutePath(routePath: string): string[];
|
|
62
|
+
/**
|
|
63
|
+
* The router for a TanStack Router (and TanStack Start) app.
|
|
64
|
+
*
|
|
65
|
+
* Like `nextAppRouter` this does not belong in core — it is here for the same
|
|
66
|
+
* reason that one is: the serialized schema carries only a router id, so the
|
|
67
|
+
* thing that turns an id back into a validator has to be somewhere every
|
|
68
|
+
* consumer can reach.
|
|
69
|
+
*/
|
|
70
|
+
export declare const tanstackRouter: ValRouter;
|
|
26
71
|
export interface ValRouter {
|
|
27
72
|
getRouterId(): string;
|
|
28
73
|
validate(moduleFilePath: ModuleFilePath, urlPaths: string[]): RouteValidationError[];
|
|
@@ -38,7 +38,47 @@ export declare class ArraySchema<T extends Schema<SelectorSource>, Src extends S
|
|
|
38
38
|
private readonly previewInput;
|
|
39
39
|
private readonly renderInput;
|
|
40
40
|
constructor(item: T, opt?: boolean, customValidateFunctions?: ((src: Src) => false | string)[], isReadonly?: boolean, isHidden?: boolean, description?: string | undefined, previewInput?: ItemPreviewInput<Src> | null, renderInput?: FieldRender | null);
|
|
41
|
+
/**
|
|
42
|
+
* Describe this field.
|
|
43
|
+
*
|
|
44
|
+
* The description is shown next to the field's label in the Val editor, so
|
|
45
|
+
* it is where you say what an editor needs to know but the field name cannot
|
|
46
|
+
* carry. It also travels in the serialized schema, which is what the AI
|
|
47
|
+
* assistant and the MCP tools read.
|
|
48
|
+
*
|
|
49
|
+
* Pass `null` to clear a description set earlier.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* const schema = s
|
|
53
|
+
* .array(s.string())
|
|
54
|
+
* .describe("Bullet points shown under the heading");
|
|
55
|
+
* export default c.define("/example.val.ts", schema, ["First", "Second"]);
|
|
56
|
+
*/
|
|
41
57
|
describe(description: string | null): ArraySchema<T, Src>;
|
|
58
|
+
/**
|
|
59
|
+
* Add a custom validation rule to this field.
|
|
60
|
+
*
|
|
61
|
+
* The function is called with the field's value and returns `false` when the
|
|
62
|
+
* value is fine, or a STRING with the message to show when it is not. Call it
|
|
63
|
+
* more than once to add more rules — they all run, and every message is
|
|
64
|
+
* reported.
|
|
65
|
+
*
|
|
66
|
+
* Write the check as a ternary, not as `ok || "message"`: that returns `true`
|
|
67
|
+
* when the value is fine, and `true` is not one of the two answers.
|
|
68
|
+
*
|
|
69
|
+
* Validation runs in the Studio as you type, in `npx val validate` and
|
|
70
|
+
* before a publish.
|
|
71
|
+
*
|
|
72
|
+
* The function is given the whole array. To validate one item, put a
|
|
73
|
+
* `.validate(...)` on the ITEM schema instead — that reports the error on
|
|
74
|
+
* the item's own row.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* const schema = s.array(s.string()).validate((val) =>
|
|
78
|
+
* val.length <= 3 ? false : "At most 3 items",
|
|
79
|
+
* );
|
|
80
|
+
* export default c.define("/example.val.ts", schema, ["First", "Second"]);
|
|
81
|
+
*/
|
|
42
82
|
validate(validationFunction: (src: Src) => false | string): ArraySchema<T, Src>;
|
|
43
83
|
protected executeValidate(path: SourcePath, src: Src): ValidationErrors;
|
|
44
84
|
protected executeAssert(path: SourcePath, src: unknown): SchemaAssertResult<Src>;
|
|
@@ -55,6 +95,25 @@ export declare class ArraySchema<T extends Schema<SelectorSource>, Src extends S
|
|
|
55
95
|
* is the item of another container, in search, in references. What its rows
|
|
56
96
|
* show is the ITEM schema's `preview`, not this. Never how the field is
|
|
57
97
|
* edited (that is `render`). See `preview.ts`.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* // The preview of the ARRAY, for where it is an item of something else.
|
|
101
|
+
* // What its own rows show is the ITEM schema's preview — see below.
|
|
102
|
+
* const tags = s.array(s.string()).preview(({ val }) => ({
|
|
103
|
+
* title: `${val.length} tags`,
|
|
104
|
+
* }));
|
|
105
|
+
* export default c.define("/example.val.ts", s.record(tags), {
|
|
106
|
+
* "a-post": ["news", "release"],
|
|
107
|
+
* });
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* // Rows of an array come from the ITEM's preview, not the array's:
|
|
111
|
+
* const author = s
|
|
112
|
+
* .object({ name: s.string(), role: s.string() })
|
|
113
|
+
* .preview(({ val }) => ({ title: val.name, subtitle: val.role }));
|
|
114
|
+
* export default c.define("/example.val.ts", s.array(author), [
|
|
115
|
+
* { name: "Ada", role: "Engineer" },
|
|
116
|
+
* ]);
|
|
58
117
|
*/
|
|
59
118
|
preview(select: ItemPreviewInput<Src>): ArraySchema<T, Src>;
|
|
60
119
|
/**
|
|
@@ -63,6 +122,12 @@ export declare class ArraySchema<T extends Schema<SelectorSource>, Src extends S
|
|
|
63
122
|
* instead of a preview row that navigates to it.
|
|
64
123
|
*
|
|
65
124
|
* Static configuration, not a callback — see `render.ts`.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* const schema = s.record(s.array(s.string()).render({ as: "inline" }));
|
|
128
|
+
* export default c.define("/example.val.ts", schema, {
|
|
129
|
+
* "a-post": ["news", "release"],
|
|
130
|
+
* });
|
|
66
131
|
*/
|
|
67
132
|
render(input: FieldRender): ArraySchema<T, Src>;
|
|
68
133
|
}
|