@valbuild/core 0.130.0 → 0.134.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 +72 -0
- package/dist/declarations/src/index.d.ts +10 -0
- package/dist/declarations/src/initSchema.d.ts +20 -0
- package/dist/declarations/src/module.d.ts +21 -4
- package/dist/declarations/src/schema/index.d.ts +2 -1
- package/dist/declarations/src/schema/object.d.ts +10 -0
- package/dist/declarations/src/schema/validation/ValidationFix.d.ts +1 -1
- package/dist/declarations/src/schema/view.d.ts +197 -0
- package/dist/declarations/src/selector/index.d.ts +4 -2
- package/dist/declarations/src/selector/view.d.ts +22 -0
- package/dist/declarations/src/source/index.d.ts +2 -1
- package/dist/declarations/src/source/view.d.ts +41 -0
- package/dist/declarations/src/source/viewHandle.d.ts +69 -0
- package/dist/{index-9e372bd7.cjs.prod.js → index-48a8f8a5.cjs.prod.js} +693 -5
- package/dist/{index-27d57011.cjs.dev.js → index-85a2ffba.cjs.dev.js} +693 -5
- package/dist/{index-71a59a88.esm.js → index-e3458565.esm.js} +692 -6
- package/dist/valbuild-core.cjs.dev.js +3 -1
- package/dist/valbuild-core.cjs.prod.js +3 -1
- 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,77 @@
|
|
|
1
1
|
# @valbuild/core
|
|
2
2
|
|
|
3
|
+
## 0.134.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#690](https://github.com/valbuild/val/pull/690) [`16c49ea`](https://github.com/valbuild/val/commit/16c49ea6dd97c7a96bbfdf211af9bab5884579e2) Thanks [@freekh](https://github.com/freekh)! - Read a view with `useVal` / `fetchVal`.
|
|
8
|
+
|
|
9
|
+
A `s.view()` field reads as a pointer with no properties on it. It can now be handed to a reader, which resolves it to the module it names:
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
const page = useVal(pageVal);
|
|
13
|
+
const header = useVal(page.header); // the header module's content, typed
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The point is single source of truth rather than a new capability: the page declares which module it shows, and a component follows that declaration instead of importing the target a second time. Change `s.view(headerVal)` to `s.view(navVal)` and the reader follows; an import would have kept reading the header.
|
|
17
|
+
|
|
18
|
+
The readers that take a MODULE rather than a value follow the same rule — `useValKey`, `useValRoute`, `useValRouteUrl` and the `fetchVal*` counterparts, in both the Next and TanStack packages:
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
const page = useVal(pageVal);
|
|
22
|
+
const note = useValRoute(page.notes, params); // the router module the view names
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Works in draft mode, including when the page itself has a pending edit. Two things to know:
|
|
26
|
+
|
|
27
|
+
- **Resolve a handle in the component that read the module containing it.** A handle carries the module it points at, and that cannot survive serialization — so one passed from a server component to a client component as a prop arrives empty. It throws with an explanation rather than handing back the pointer. This is why it throws rather than returning nothing: every one of the route and key readers already uses `null` / `undefined` to mean "no such entry", so a quiet answer would be indistinguishable from a 404.
|
|
28
|
+
- **`ValView<Source>` is now a member of `SelectorSource`**, since a reader accepts one.
|
|
29
|
+
|
|
30
|
+
- [#680](https://github.com/valbuild/val/pull/680) [`eaa265e`](https://github.com/valbuild/val/commit/eaa265e78d9f6d2a1b685c38901612b8a3704eed) Thanks [@freekh](https://github.com/freekh)! - Add `s.view()`: point at another module, so editors can reach it from the page it belongs to.
|
|
31
|
+
|
|
32
|
+
Content that has to live in its own module — a `keyOf` target, shared settings, a route-keyed record — is invisible from the page an editor thinks of it as part of. A view field puts a row on that page's screen that leads to it:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import employeesVal from "../data/employees.val";
|
|
36
|
+
|
|
37
|
+
const schema = s.object({
|
|
38
|
+
title: s.string(),
|
|
39
|
+
employees: s.view(employeesVal),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export default c.define("/app/menneskene/page.val.ts", schema, {
|
|
43
|
+
title: "Våre folk",
|
|
44
|
+
employees: { view: "/data/employees.val.ts" },
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The source is a pointer and nothing else. The module it names keeps its own source, patches, validation and address, and an editor who follows the row lands on that module's own screen — so it is clear they are changing something other pages use too.
|
|
49
|
+
|
|
50
|
+
A few things worth knowing:
|
|
51
|
+
|
|
52
|
+
- **The path autocompletes, and a wrong one does not compile.** A module now carries its own id in its type, so the source type of the field above is the literal `{ view: "/data/employees.val.ts" }`.
|
|
53
|
+
- **It is not readable in code.** `useVal(pageVal).employees` is a `ValView<…>` — an opaque pointer with no fields on it. Read the module it names directly, as before.
|
|
54
|
+
- **`view` is now a reserved object key**, like `_type` and `patch_id`: `s.object({ view: ... })` no longer compiles. A single `view: string` key is what a view pointer looks like, and an ordinary object with that shape would be indistinguishable from one.
|
|
55
|
+
- **Views may not form a cycle.** `A → B → A`, and a module viewing itself, are reported as module errors by `val validate` and in the Studio.
|
|
56
|
+
- **A pointer that disagrees with its schema is repaired automatically.** It can only happen in hand-written JSON, and the schema is the authority, so `val validate --fix` and saving in the Studio both write the module the schema names.
|
|
57
|
+
- **A view's `hidden` and `readonly` are its own, never the module's it points at.** A view whose target is hidden is still shown, and still leads there.
|
|
58
|
+
|
|
59
|
+
That last one comes with a change to what `hidden()` means on a **module's own schema**, which is the other half of making a shared module usable:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// Not in the nav, and on exactly one page.
|
|
63
|
+
export default c.define("/data/employees.val.ts", s.record(...).hidden(), { ... });
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
It now means the nav does not list the module — the Explorer for an ordinary module, Media for a gallery — and nothing more. Previously it also blanked the module's own page, so a module you had hidden was still in the nav and showed nothing when opened. A hidden module is now reached from an `s.view()` row, from search or from a validation error, and renders in full when you get there.
|
|
67
|
+
|
|
68
|
+
One gap worth naming rather than leaving to be discovered: a view pointing at a
|
|
69
|
+
module the project does not have is reported by the FIELD — the row says the
|
|
70
|
+
target is missing — but not by `val validate`. The schema check compares the
|
|
71
|
+
pointer against the schema, and the cycle check deliberately skips a target that
|
|
72
|
+
is not a module of the project, so neither catches it. A project-level check
|
|
73
|
+
belongs with them and is not here yet.
|
|
74
|
+
|
|
3
75
|
## 0.130.0
|
|
4
76
|
|
|
5
77
|
### Patch Changes
|
|
@@ -65,6 +65,11 @@ export { type SerializedCodeSchema, type CodeOptions, type CodeLanguage, CodeSch
|
|
|
65
65
|
export { type ColorFormat, type ParsedColor, COLOR_FORMATS, DEFAULT_COLOR_FORMAT, } from "./schema/colorFormat.js";
|
|
66
66
|
export { type SerializedDateTimeSchema, DateTimeSchema, } from "./schema/datetime.js";
|
|
67
67
|
export { type SerializedKeyOfSchema, KeyOfSchema } from "./schema/keyOf.js";
|
|
68
|
+
export { type SerializedValViewSchema, ValViewSchema } from "./schema/view.js";
|
|
69
|
+
export { type ValViewSource, isValViewSource } from "./source/view.js";
|
|
70
|
+
export type { ValView } from "./selector/view.js";
|
|
71
|
+
export type { ValViewHandle, ResolvableModule } from "./source/viewHandle.js";
|
|
72
|
+
import { createViewHandle, isViewHandle, resolveViewedModule, viewHandleModule, viewModulesOf } from "./source/viewHandle.js";
|
|
68
73
|
export { type SerializedRouteSchema, RouteSchema } from "./schema/route.js";
|
|
69
74
|
export { type SerializedRichTextSchema, RichTextSchema, } from "./schema/richtext.js";
|
|
70
75
|
export { type SerializedDiscriminatedUnionSchema, type DiscriminatedUnionSourceOf, DiscriminatedUnionSchema, } from "./schema/discriminatedUnion.js";
|
|
@@ -85,6 +90,11 @@ declare const Internal: {
|
|
|
85
90
|
VERSION: {
|
|
86
91
|
core: string;
|
|
87
92
|
};
|
|
93
|
+
createViewHandle: typeof createViewHandle;
|
|
94
|
+
isViewHandle: typeof isViewHandle;
|
|
95
|
+
resolveViewedModule: typeof resolveViewedModule;
|
|
96
|
+
viewHandleModule: typeof viewHandleModule;
|
|
97
|
+
viewModulesOf: typeof viewModulesOf;
|
|
88
98
|
mediaUrl: typeof mediaUrl;
|
|
89
99
|
resolveMedia: typeof resolveMedia;
|
|
90
100
|
isRemoteMediaPath: typeof isRemoteMediaPath;
|
|
@@ -10,6 +10,7 @@ import { richtext } from "./schema/richtext.js";
|
|
|
10
10
|
import { image } from "./schema/image.js";
|
|
11
11
|
import { literal } from "./schema/literal.js";
|
|
12
12
|
import { keyOf } from "./schema/keyOf.js";
|
|
13
|
+
import { view } from "./schema/view.js";
|
|
13
14
|
import { record } from "./schema/record.js";
|
|
14
15
|
import { file } from "./schema/file.js";
|
|
15
16
|
import { fileset } from "./schema/fileset.js";
|
|
@@ -175,6 +176,23 @@ export type InitSchema = {
|
|
|
175
176
|
*
|
|
176
177
|
*/
|
|
177
178
|
readonly keyOf: typeof keyOf;
|
|
179
|
+
/**
|
|
180
|
+
* Show another module as part of this one, in the Val editor only.
|
|
181
|
+
*
|
|
182
|
+
* The source is a pointer — `{ view: "/other.val.ts" }` — and nothing else.
|
|
183
|
+
* The module it names keeps its own source, patches, validation and address;
|
|
184
|
+
* the editor shows a row here that navigates to it.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* import otherVal from "./other.val"; // another module
|
|
188
|
+
* const schema = s.object({ shared: s.view(otherVal), title: s.string() });
|
|
189
|
+
* export default c.define("/example.val.ts", schema, {
|
|
190
|
+
* shared: { view: "/other.val.ts" },
|
|
191
|
+
* title: "Hello",
|
|
192
|
+
* });
|
|
193
|
+
*
|
|
194
|
+
*/
|
|
195
|
+
readonly view: typeof view;
|
|
178
196
|
/**
|
|
179
197
|
* Define a record.
|
|
180
198
|
*
|
|
@@ -381,6 +399,7 @@ export declare function initSchema(): {
|
|
|
381
399
|
val?: never;
|
|
382
400
|
_type?: never;
|
|
383
401
|
_ref?: never;
|
|
402
|
+
view?: never;
|
|
384
403
|
andThen?: never;
|
|
385
404
|
assert?: never;
|
|
386
405
|
fold?: never;
|
|
@@ -397,6 +416,7 @@ export declare function initSchema(): {
|
|
|
397
416
|
image: typeof image;
|
|
398
417
|
literal: <T extends string>(value: T) => import("./index.js").LiteralSchema<T>;
|
|
399
418
|
keyOf: <Src extends import("./selector/index.js").GenericSelector<import("./source/index.js").SourceObject> & import("./module.js").ValModuleBrand>(valModule: Src) => import("./index.js").KeyOfSchema<Src, Src extends import("./selector/index.js").GenericSelector<infer S extends import("./source/index.js").Source, undefined> ? S extends readonly import("./source/index.js").Source[] ? number : S extends import("./source/index.js").SourceObject ? string extends keyof S ? import("./index.js").RawString : keyof S : S extends Record<string, import("./source/index.js").Source> ? import("./index.js").RawString : never : never>;
|
|
419
|
+
view: <M extends import("./selector/index.js").GenericSelector<import("./source/index.js").Source> & import("./module.js").ValModuleBrand>(valModule: M) => import("./index.js").ValViewSchema<import("./module.js").ModuleIdOf<M>, M extends import("./selector/index.js").GenericSelector<infer S extends import("./source/index.js").Source, undefined> ? S : never>;
|
|
400
420
|
record: typeof record;
|
|
401
421
|
file: typeof file;
|
|
402
422
|
fileset: (options: import("./schema/fileset.js").FilesetOptions) => import("./index.js").RecordSchema<import("./index.js").ObjectSchema<{
|
|
@@ -6,10 +6,27 @@ import { ModuleFilePath, ModulePath, SourcePath } from "./val/index.js";
|
|
|
6
6
|
import { Json } from "./Json.js";
|
|
7
7
|
import { RawString } from "./schema/string.js";
|
|
8
8
|
declare const brand: unique symbol;
|
|
9
|
-
|
|
10
|
-
export type
|
|
9
|
+
declare const idBrand: unique symbol;
|
|
10
|
+
export type ValModule<T extends SelectorSource, Id extends string = string> = SelectorOf<T> & ValModuleBrand<Id>;
|
|
11
|
+
/**
|
|
12
|
+
* The brand that says "this is a module", carrying the module's own id.
|
|
13
|
+
*
|
|
14
|
+
* `Id` is inferred from {@link define}'s first argument, so a module's PATH is
|
|
15
|
+
* in its type. That is what lets `s.view(fooVal)` produce a schema whose source
|
|
16
|
+
* type is the literal `{ view: "/foo.val.ts" }` — the path autocompletes at the
|
|
17
|
+
* call site, and a source that names a different module than the schema does is
|
|
18
|
+
* a type error rather than something validation has to catch later.
|
|
19
|
+
*
|
|
20
|
+
* Defaulted to `string` so every `ValModule<T>` written without it keeps
|
|
21
|
+
* meaning what it did. The property is covariant, so a module with a known id
|
|
22
|
+
* is still assignable everywhere `ValModule<T>` is expected.
|
|
23
|
+
*/
|
|
24
|
+
export type ValModuleBrand<Id extends string = string> = {
|
|
11
25
|
[brand]: "ValModule";
|
|
26
|
+
[idBrand]: Id;
|
|
12
27
|
};
|
|
28
|
+
/** A module's own path, read back off its type. */
|
|
29
|
+
export type ModuleIdOf<M> = M extends ValModuleBrand<infer Id> ? Id : string;
|
|
13
30
|
export type InferValModuleType<T extends ValModule<SelectorSource>> = T extends GenericSelector<infer S> ? S : never;
|
|
14
31
|
export type ReplaceRawStringWithString<T extends SelectorSource> = SelectorSource extends T ? T : T extends RawString ? string : T extends {
|
|
15
32
|
[key in string]: SelectorSource;
|
|
@@ -32,8 +49,8 @@ export type ReplaceRawStringWithString<T extends SelectorSource> = SelectorSourc
|
|
|
32
49
|
* external at all.
|
|
33
50
|
*/
|
|
34
51
|
type InlineEntriesFor<S> = S extends ExternalRecordSrc<infer Item, string, boolean> ? Record<string, Item> : never;
|
|
35
|
-
export declare function define<T extends Schema<SelectorSource
|
|
36
|
-
schema: T, source: ReplaceRawStringWithString<SelectorOfSchema<T>> | InlineEntriesFor<SelectorOfSchema<T>>): ValModule<SelectorOfSchema<T
|
|
52
|
+
export declare function define<T extends Schema<SelectorSource>, Id extends string>(id: Id, // TODO: `/${string}`
|
|
53
|
+
schema: T, source: ReplaceRawStringWithString<SelectorOfSchema<T>> | InlineEntriesFor<SelectorOfSchema<T>>): ValModule<SelectorOfSchema<T>, Id>;
|
|
37
54
|
export declare function getSource<T extends Source>(valModule: ValModule<T>): T;
|
|
38
55
|
/**
|
|
39
56
|
* Whether `value` is a Val module, i.e. something {@link define} produced.
|
|
@@ -5,6 +5,7 @@ import { SerializedBooleanSchema } from "./boolean.js";
|
|
|
5
5
|
import { SerializedFileSchema } from "./file.js";
|
|
6
6
|
import { SerializedImageSchema } from "./image.js";
|
|
7
7
|
import { SerializedKeyOfSchema } from "./keyOf.js";
|
|
8
|
+
import { SerializedValViewSchema } from "./view.js";
|
|
8
9
|
import { SerializedLiteralSchema } from "./literal.js";
|
|
9
10
|
import { SerializedNumberSchema } from "./number.js";
|
|
10
11
|
import { SerializedObjectSchema } from "./object.js";
|
|
@@ -24,7 +25,7 @@ import { ValidationError, ValidationErrors } from "./validation/ValidationError.
|
|
|
24
25
|
import { FileSource } from "../source/media.js";
|
|
25
26
|
import { GenericRichTextSourceNode, RichTextSource } from "../source/richtext.js";
|
|
26
27
|
import { ReifiedPreview, PreviewScope, PreviewItem } from "../preview.js";
|
|
27
|
-
export type SerializedSchema = SerializedStringSchema | SerializedLiteralSchema | SerializedBooleanSchema | SerializedNumberSchema | SerializedObjectSchema | SerializedArraySchema | SerializedDiscriminatedUnionSchema | SerializedEnumSchema | SerializedRichTextSchema | SerializedRecordSchema | SerializedKeyOfSchema | SerializedFileSchema | SerializedDateSchema | SerializedDateTimeSchema | SerializedColorSchema | SerializedCodeSchema | SerializedRouteSchema | SerializedLocaleSchema | SerializedSettingsSchema | SerializedImageSchema;
|
|
28
|
+
export type SerializedSchema = SerializedStringSchema | SerializedLiteralSchema | SerializedBooleanSchema | SerializedNumberSchema | SerializedObjectSchema | SerializedArraySchema | SerializedDiscriminatedUnionSchema | SerializedEnumSchema | SerializedRichTextSchema | SerializedRecordSchema | SerializedKeyOfSchema | SerializedFileSchema | SerializedDateSchema | SerializedDateTimeSchema | SerializedColorSchema | SerializedCodeSchema | SerializedRouteSchema | SerializedLocaleSchema | SerializedSettingsSchema | SerializedImageSchema | SerializedValViewSchema;
|
|
28
29
|
type Primitives = number | string | boolean | null | FileSource;
|
|
29
30
|
export type AssertError = {
|
|
30
31
|
message: string;
|
|
@@ -28,6 +28,16 @@ type ObjectSchemaProps = {
|
|
|
28
28
|
_type?: never;
|
|
29
29
|
/** Cannot create object with key: _ref. It is a reserved name */
|
|
30
30
|
_ref?: never;
|
|
31
|
+
/**
|
|
32
|
+
* Cannot create object with key: view. It is a reserved name.
|
|
33
|
+
*
|
|
34
|
+
* A view field's source is `{ view: "/other.val.ts" }`, and the read side
|
|
35
|
+
* maps that shape to an opaque {@link ValView} handle. An ordinary object
|
|
36
|
+
* with a single `view: string` key is structurally identical, so it would be
|
|
37
|
+
* mapped the same way and lose every one of its fields — silently. Reserving
|
|
38
|
+
* the key is what makes the shape unambiguous.
|
|
39
|
+
*/
|
|
40
|
+
view?: never;
|
|
31
41
|
/** Cannot create object with key: andThen. It is a reserved name */
|
|
32
42
|
andThen?: never;
|
|
33
43
|
/** Cannot create object with key: assert. It is a reserved name */
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const ValidationFix: readonly ["image:add-metadata", "image:check-metadata", "image:upload-remote", "image:download-remote", "image:check-remote", "images:check-remote", "images:upload-remote", "file:add-metadata", "file:check-metadata", "file:upload-remote", "file:download-remote", "file:check-remote", "files:check-remote", "files:upload-remote", "keyof:check-keys", "router:check-route", "locale:check-locale", "images:check-unique-folder", "files:check-unique-folder", "images:check-all-files", "files:check-all-files", "jsonValues:extract-entry", "record:fill-keys", "external:upload"];
|
|
1
|
+
export declare const ValidationFix: readonly ["image:add-metadata", "image:check-metadata", "image:upload-remote", "image:download-remote", "image:check-remote", "images:check-remote", "images:upload-remote", "file:add-metadata", "file:check-metadata", "file:upload-remote", "file:download-remote", "file:check-remote", "files:check-remote", "files:upload-remote", "keyof:check-keys", "router:check-route", "locale:check-locale", "images:check-unique-folder", "files:check-unique-folder", "images:check-all-files", "files:check-all-files", "jsonValues:extract-entry", "record:fill-keys", "external:upload", "view:check-module"];
|
|
2
2
|
export type ValidationFix = (typeof ValidationFix)[number];
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { Schema, SchemaAssertResult, SerializedSchema } from "./index.js";
|
|
2
|
+
import type { ModuleIdOf, ValModuleBrand } from "../module.js";
|
|
3
|
+
import { ReifiedPreview } from "../preview.js";
|
|
4
|
+
import { FieldRender } from "../render.js";
|
|
5
|
+
import { GenericSelector } from "../selector/index.js";
|
|
6
|
+
import { Source } from "../source/index.js";
|
|
7
|
+
import { ValViewSource } from "../source/view.js";
|
|
8
|
+
import { ModuleFilePath, SourcePath } from "../val/index.js";
|
|
9
|
+
import { ValidationError, ValidationErrors } from "./validation/ValidationError.js";
|
|
10
|
+
export type SerializedValViewSchema = {
|
|
11
|
+
type: "view";
|
|
12
|
+
render?: FieldRender;
|
|
13
|
+
/** Never set: a view has no value of its own to preview. Carried for shape parity. */
|
|
14
|
+
preview?: true;
|
|
15
|
+
/** Always false: a view points at a module, and `null` is not a module. */
|
|
16
|
+
opt: false;
|
|
17
|
+
/** Always false: there is no value for a custom validator to look at. */
|
|
18
|
+
customValidate?: false;
|
|
19
|
+
/** The module this field points at. */
|
|
20
|
+
moduleFilePath: ModuleFilePath;
|
|
21
|
+
readonly?: boolean;
|
|
22
|
+
hidden?: boolean;
|
|
23
|
+
description?: string;
|
|
24
|
+
};
|
|
25
|
+
/** The source type of the module a selector points at. */
|
|
26
|
+
type SourceOf<M> = M extends GenericSelector<infer S> ? S : never;
|
|
27
|
+
/**
|
|
28
|
+
* A field that points at ANOTHER module, for the Val editor's benefit.
|
|
29
|
+
*
|
|
30
|
+
* The source is `{ view: "/other.val.ts" }` — a pointer, and nothing else. The
|
|
31
|
+
* module it names keeps its own source, its own patches, its own validation and
|
|
32
|
+
* its own address; this field puts a row on THIS module's screen that an editor
|
|
33
|
+
* clicks through to it.
|
|
34
|
+
*
|
|
35
|
+
* Two type parameters, each earning its place:
|
|
36
|
+
*
|
|
37
|
+
* - `Id` — the target's module file path as a literal, so the author writes
|
|
38
|
+
* `{ view: "/other.val.ts" }` with autocomplete, and naming a different module
|
|
39
|
+
* than the schema does is a type error rather than a validation error.
|
|
40
|
+
* - `T` — the target's source type, carried on a phantom slot so the READ side
|
|
41
|
+
* can say `ValView<T>` rather than `ValView<unknown>`. Nothing reads it yet;
|
|
42
|
+
* it is here so that resolving a view through `useVal` can be added without
|
|
43
|
+
* changing what is stored.
|
|
44
|
+
*/
|
|
45
|
+
export declare class ValViewSchema<Id extends string = string, T = unknown> extends Schema<ValViewSource<Id, T>> {
|
|
46
|
+
private readonly moduleFilePath;
|
|
47
|
+
/**
|
|
48
|
+
* The module this points at, as an object.
|
|
49
|
+
*
|
|
50
|
+
* Not serialized and not part of the schema's identity — it is here so the
|
|
51
|
+
* READ path can resolve a view without a path-to-module registry, which the
|
|
52
|
+
* app does not have (`val.modules` is lazy thunks, `<ValModulesClient>` is
|
|
53
|
+
* optional). Absent on a schema that came off the wire, which is fine: the
|
|
54
|
+
* Studio navigates to a view, it does not resolve one.
|
|
55
|
+
*/
|
|
56
|
+
private readonly valModule;
|
|
57
|
+
private readonly isReadonly;
|
|
58
|
+
private readonly isHidden;
|
|
59
|
+
private readonly description?;
|
|
60
|
+
private readonly renderInput;
|
|
61
|
+
constructor(moduleFilePath: Id,
|
|
62
|
+
/**
|
|
63
|
+
* The module this points at, as an object.
|
|
64
|
+
*
|
|
65
|
+
* Not serialized and not part of the schema's identity — it is here so the
|
|
66
|
+
* READ path can resolve a view without a path-to-module registry, which the
|
|
67
|
+
* app does not have (`val.modules` is lazy thunks, `<ValModulesClient>` is
|
|
68
|
+
* optional). Absent on a schema that came off the wire, which is fine: the
|
|
69
|
+
* Studio navigates to a view, it does not resolve one.
|
|
70
|
+
*/
|
|
71
|
+
valModule?: unknown, isReadonly?: boolean, isHidden?: boolean, description?: string | undefined, renderInput?: FieldRender | null);
|
|
72
|
+
/**
|
|
73
|
+
* Describe this field.
|
|
74
|
+
*
|
|
75
|
+
* Shown next to the field's label in the Val editor — here, the place to say
|
|
76
|
+
* WHY the other module is on this screen, since an editor who follows the row
|
|
77
|
+
* is about to change content that other pages use too.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* import otherVal from "./other.val"; // another module
|
|
81
|
+
* const schema = s.object({
|
|
82
|
+
* shared: s.view(otherVal).describe("Shared by every page"),
|
|
83
|
+
* title: s.string(),
|
|
84
|
+
* });
|
|
85
|
+
* export default c.define("/example.val.ts", schema, {
|
|
86
|
+
* shared: { view: "/other.val.ts" },
|
|
87
|
+
* title: "Hello",
|
|
88
|
+
* });
|
|
89
|
+
*/
|
|
90
|
+
describe(description: string | null): ValViewSchema<Id, T>;
|
|
91
|
+
/**
|
|
92
|
+
* The stored pointer must name the module the SCHEMA names.
|
|
93
|
+
*
|
|
94
|
+
* It cannot disagree in a `.val.ts` — the source type is the literal path, so
|
|
95
|
+
* a mismatch does not compile. It can disagree in hand-written JSON (a
|
|
96
|
+
* `.jsonValues()` entry, an external record), which is what this is for.
|
|
97
|
+
*/
|
|
98
|
+
protected executeValidate(path: SourcePath, src: ValViewSource<Id, T>): ValidationErrors;
|
|
99
|
+
protected executeCustomValidateAt(_path: SourcePath, _src: ValViewSource<Id, T>): ValidationError[];
|
|
100
|
+
protected executeAssert(path: SourcePath, src: unknown): SchemaAssertResult<ValViewSource<Id, T>>;
|
|
101
|
+
/**
|
|
102
|
+
* Not available: a view points at a module, and `null` is not a module.
|
|
103
|
+
* Throws if called.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* import otherVal from "./other.val"; // another module
|
|
107
|
+
* // `s.view(otherVal).nullable()` throws — a view is never nullable.
|
|
108
|
+
* const schema = s.object({ shared: s.view(otherVal), title: s.string() });
|
|
109
|
+
* export default c.define("/example.val.ts", schema, {
|
|
110
|
+
* shared: { view: "/other.val.ts" },
|
|
111
|
+
* title: "Hello",
|
|
112
|
+
* });
|
|
113
|
+
*/
|
|
114
|
+
nullable(): Schema<ValViewSource<Id, T> | null>;
|
|
115
|
+
/**
|
|
116
|
+
* Marks the row read-only.
|
|
117
|
+
*
|
|
118
|
+
* The view's OWN flag, and it says nothing about the module it names: that
|
|
119
|
+
* module has its own `readonly`, which is what governs editing once an
|
|
120
|
+
* editor is there. A view is a link rather than an editor, so this changes
|
|
121
|
+
* nothing about the row today — it exists because every schema has it, and
|
|
122
|
+
* because a view that renders its target inline would need it.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* import otherVal from "./other.val"; // another module
|
|
126
|
+
* const schema = s.object({
|
|
127
|
+
* shared: s.view(otherVal).readonly(),
|
|
128
|
+
* title: s.string(),
|
|
129
|
+
* });
|
|
130
|
+
* export default c.define("/example.val.ts", schema, {
|
|
131
|
+
* shared: { view: "/other.val.ts" },
|
|
132
|
+
* title: "Hello",
|
|
133
|
+
* });
|
|
134
|
+
*/
|
|
135
|
+
readonly(isReadonly?: boolean): ValViewSchema<Id, T>;
|
|
136
|
+
/**
|
|
137
|
+
* Hide the ROW in the editor.
|
|
138
|
+
*
|
|
139
|
+
* The view's OWN flag, and it is the only thing that decides whether the row
|
|
140
|
+
* is drawn — a view whose TARGET is hidden is still shown, and still leads
|
|
141
|
+
* there. That is the point of it: a module that many modules `keyOf` into is
|
|
142
|
+
* noise in the nav and belongs on exactly one page, so it is hidden from the
|
|
143
|
+
* listing and reached through the view that puts it where it belongs.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* import otherVal from "./other.val"; // another module
|
|
147
|
+
* const schema = s.object({
|
|
148
|
+
* shared: s.view(otherVal).hidden(),
|
|
149
|
+
* title: s.string(),
|
|
150
|
+
* });
|
|
151
|
+
* export default c.define("/example.val.ts", schema, {
|
|
152
|
+
* shared: { view: "/other.val.ts" },
|
|
153
|
+
* title: "Hello",
|
|
154
|
+
* });
|
|
155
|
+
*/
|
|
156
|
+
hidden(isHidden?: boolean): ValViewSchema<Id, T>;
|
|
157
|
+
/**
|
|
158
|
+
* How this field is laid out in the editor. Static configuration, not a
|
|
159
|
+
* callback — see `render.ts`.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* import otherVal from "./other.val"; // another module
|
|
163
|
+
* const schema = s.object({
|
|
164
|
+
* shared: s.view(otherVal).render({ as: "inline" }),
|
|
165
|
+
* title: s.string(),
|
|
166
|
+
* });
|
|
167
|
+
* export default c.define("/example.val.ts", schema, {
|
|
168
|
+
* shared: { view: "/other.val.ts" },
|
|
169
|
+
* title: "Hello",
|
|
170
|
+
* });
|
|
171
|
+
*/
|
|
172
|
+
render(input: FieldRender): ValViewSchema<Id, T>;
|
|
173
|
+
protected executeSerialize(): SerializedSchema;
|
|
174
|
+
protected executePreview(): ReifiedPreview;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Point at another module, so the Val editor shows a way into it from here.
|
|
178
|
+
*
|
|
179
|
+
* A view stores a pointer and nothing else: the module it names keeps its own
|
|
180
|
+
* source, patches, validation and address, and an editor reaches it by clicking
|
|
181
|
+
* through rather than by hunting for it. It is there so content that has to live
|
|
182
|
+
* in its own module — a `keyOf` target, shared settings, a route-keyed record —
|
|
183
|
+
* can still be found from the page it belongs to.
|
|
184
|
+
*
|
|
185
|
+
* Reading it in consuming code gives you a pointer with no fields on it. Read
|
|
186
|
+
* the module it names directly instead.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* import otherVal from "./other.val"; // another module
|
|
190
|
+
* const schema = s.object({ shared: s.view(otherVal), title: s.string() });
|
|
191
|
+
* export default c.define("/example.val.ts", schema, {
|
|
192
|
+
* shared: { view: "/other.val.ts" },
|
|
193
|
+
* title: "Hello",
|
|
194
|
+
* });
|
|
195
|
+
*/
|
|
196
|
+
export declare const view: <M extends GenericSelector<Source> & ValModuleBrand>(valModule: M) => ValViewSchema<ModuleIdOf<M>, SourceOf<M>>;
|
|
197
|
+
export {};
|
|
@@ -13,11 +13,13 @@ import { AllRichTextOptions, RichTextSource } from "../source/richtext.js";
|
|
|
13
13
|
import { RichTextSelector } from "./richtext.js";
|
|
14
14
|
import { JsonSource } from "../source/json.js";
|
|
15
15
|
import { ExternalRecordSrc } from "../source/external.js";
|
|
16
|
+
import { ValViewSource } from "../source/view.js";
|
|
17
|
+
import { ValView } from "./view.js";
|
|
16
18
|
import { SettingsSource } from "../source/settings.js";
|
|
17
|
-
export type Selector<T extends Source> = Source extends T ? GenericSelector<T> : T extends MediaSource ? GenericSelector<T> : T extends JsonSource ? GenericSelector<JsonSource> : T extends ExternalRecordSrc ? GenericSelector<T> : T extends RichTextSource<infer O> ? RichTextSelector<O> : T extends SourceObject ? ObjectSelector<T> : T extends SourceArray ? ArraySelector<T> : T extends SettingsSource ? GenericSelector<T> : T extends string ? StringSelector<T> : T extends number ? NumberSelector<T> : T extends boolean ? BooleanSelector<T> : T extends null ? PrimitiveSelector<null> : never;
|
|
19
|
+
export type Selector<T extends Source> = Source extends T ? GenericSelector<T> : T extends MediaSource ? GenericSelector<T> : T extends JsonSource ? GenericSelector<JsonSource> : T extends ExternalRecordSrc ? GenericSelector<T> : T extends ValViewSource<string, infer Target> ? ValView<Target> : T extends RichTextSource<infer O> ? RichTextSelector<O> : T extends SourceObject ? ObjectSelector<T> : T extends SourceArray ? ArraySelector<T> : T extends SettingsSource ? GenericSelector<T> : T extends string ? StringSelector<T> : T extends number ? NumberSelector<T> : T extends boolean ? BooleanSelector<T> : T extends null ? PrimitiveSelector<null> : never;
|
|
18
20
|
export type SelectorSource = SourcePrimitive | undefined | readonly SelectorSource[] | {
|
|
19
21
|
[key: string]: SelectorSource;
|
|
20
|
-
} | MediaSource | JsonSource | ExternalRecordSrc | SettingsSource | RichTextSource<AllRichTextOptions> | GenericSelector<Source>;
|
|
22
|
+
} | MediaSource | JsonSource | ExternalRecordSrc | ValViewSource | ValView<Source> | SettingsSource | RichTextSource<AllRichTextOptions> | GenericSelector<Source>;
|
|
21
23
|
/**
|
|
22
24
|
* @internal
|
|
23
25
|
*/
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-level-only slot naming the source type of the module a {@link ValView}
|
|
3
|
+
* points at. Required, not optional: nothing else should ever be a `ValView` by
|
|
4
|
+
* accident, and reading a property off one has to be an error.
|
|
5
|
+
*/
|
|
6
|
+
declare const ViewTarget: unique symbol;
|
|
7
|
+
/**
|
|
8
|
+
* A pointer to another module, as consuming code sees it.
|
|
9
|
+
*
|
|
10
|
+
* It has no properties on purpose. A view exists for the Val editor — it puts a
|
|
11
|
+
* way into another module on this module's screen — and there is nothing in it
|
|
12
|
+
* to render. Read the module it names directly.
|
|
13
|
+
*
|
|
14
|
+
* `T` is the target's source type. It is what lets `useVal(page.header)` be
|
|
15
|
+
* typed as the header's content — the overload that resolves a handle
|
|
16
|
+
* constrains `T` to `Source` there rather than here, because `ValViewSource` is a
|
|
17
|
+
* member of the `Source` union and a constraint here is a circular reference.
|
|
18
|
+
*/
|
|
19
|
+
export type ValView<T> = {
|
|
20
|
+
readonly [ViewTarget]: T;
|
|
21
|
+
};
|
|
22
|
+
export {};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ExternalRecordSrc } from "./external.js";
|
|
2
|
+
import { ValViewSource } from "./view.js";
|
|
2
3
|
import { JsonSource } from "./json.js";
|
|
3
4
|
import { MediaSource } from "./media.js";
|
|
4
5
|
import { RichTextOptions, RichTextSource } from "./richtext.js";
|
|
5
6
|
import { SettingsSource } from "./settings.js";
|
|
6
|
-
export type Source = SourcePrimitive | SourceObject | SourceArray | MediaSource | JsonSource | ExternalRecordSrc | SettingsSource | RichTextSource<RichTextOptions>;
|
|
7
|
+
export type Source = SourcePrimitive | SourceObject | SourceArray | MediaSource | JsonSource | ExternalRecordSrc | ValViewSource | SettingsSource | RichTextSource<RichTextOptions>;
|
|
7
8
|
export type SourceObject = {
|
|
8
9
|
[key in string]: Source;
|
|
9
10
|
} & {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-level-only slot carrying the SOURCE TYPE of the module a view names.
|
|
3
|
+
*
|
|
4
|
+
* Optional, and that is what makes the marker writable: `{ view: "/foo.val.ts" }`
|
|
5
|
+
* is a plain literal, and an optional symbol-keyed property costs the author
|
|
6
|
+
* nothing to satisfy. It exists so the read side can say WHAT is behind the
|
|
7
|
+
* pointer — `ValView<FooSrc>` rather than `ValView<unknown>` — without the
|
|
8
|
+
* author having to write a type they do not have.
|
|
9
|
+
*/
|
|
10
|
+
declare const ViewTargetSource: unique symbol;
|
|
11
|
+
/**
|
|
12
|
+
* The source of a view field: a pointer to another module, and nothing else.
|
|
13
|
+
*
|
|
14
|
+
* Written as a plain object, like media and for the same reason — the same
|
|
15
|
+
* value has to work in a `.val.ts` and in a `*.val.json` entry, and a literal
|
|
16
|
+
* survives the static extraction that a constructor call does not. See
|
|
17
|
+
* `architecture/media.md`.
|
|
18
|
+
*
|
|
19
|
+
* `Id` is the target's module file path as a LITERAL type, inferred from the
|
|
20
|
+
* module handed to `s.view(...)`. So the path autocompletes at the call site,
|
|
21
|
+
* and a source naming a different module than its schema does is a type error.
|
|
22
|
+
*
|
|
23
|
+
* Nothing decides "this is a view" by looking at the value; the SCHEMA does
|
|
24
|
+
* (`type === "view"`). {@link isValViewSource} exists for the few places that have
|
|
25
|
+
* a value and no schema, and is not the normal way to ask.
|
|
26
|
+
*/
|
|
27
|
+
export type ValViewSource<Id extends string = string, T = unknown> = {
|
|
28
|
+
readonly view: Id;
|
|
29
|
+
} & {
|
|
30
|
+
readonly [ViewTargetSource]?: T;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Is this value shaped like a view marker?
|
|
34
|
+
*
|
|
35
|
+
* A shape test, not an identity: `view` is a reserved object key
|
|
36
|
+
* (`ObjectSchemaProps`), so no ordinary object source can take this shape — but
|
|
37
|
+
* hand-written JSON can, which is why validation checks the value against the
|
|
38
|
+
* schema rather than trusting it.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isValViewSource(value: unknown): value is ValViewSource;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ValModule } from "../module.js";
|
|
2
|
+
import { Schema } from "../schema/index.js";
|
|
3
|
+
import type { GenericSelector, SelectorSource } from "../selector/index.js";
|
|
4
|
+
import type { ValView } from "../selector/view.js";
|
|
5
|
+
import type { Source, SourceObject } from "./index.js";
|
|
6
|
+
import { ValViewSource } from "./view.js";
|
|
7
|
+
/**
|
|
8
|
+
* Where a resolved view handle keeps the module it points at.
|
|
9
|
+
*
|
|
10
|
+
* A symbol, so it is invisible to `JSON.stringify`, `Object.keys`, the source
|
|
11
|
+
* SHAs and every walk that treats source as data — the handle still IS
|
|
12
|
+
* `{ view: "/foo.val.ts" }` everywhere that matters.
|
|
13
|
+
*/
|
|
14
|
+
declare const ViewTargetModule: unique symbol;
|
|
15
|
+
/**
|
|
16
|
+
* A view's source with the module it names attached.
|
|
17
|
+
*
|
|
18
|
+
* `useVal(page.header)` needs the target's SOURCE and its SCHEMA, and the
|
|
19
|
+
* stored pointer has neither — it is a path. The app has no way to turn a path
|
|
20
|
+
* back into a module either: `val.modules` holds lazy `import()` thunks, and
|
|
21
|
+
* `<ValModulesClient>` is optional. So the module travels with the handle.
|
|
22
|
+
*
|
|
23
|
+
* Attached by the READ path (`stegaEncode`) rather than written into source,
|
|
24
|
+
* and that is the whole reason it works in draft mode: with an edit pending,
|
|
25
|
+
* the source comes from the overlay store as plain JSON that never went near a
|
|
26
|
+
* module. The schema is the module's own either way, so the encoder always has
|
|
27
|
+
* it to hand.
|
|
28
|
+
*/
|
|
29
|
+
export type ValViewHandle = ValViewSource & {
|
|
30
|
+
readonly [ViewTargetModule]: unknown;
|
|
31
|
+
};
|
|
32
|
+
/** Attach the module a view names to its pointer. */
|
|
33
|
+
export declare function createViewHandle(source: ValViewSource, valModule: unknown): ValViewHandle;
|
|
34
|
+
/** Is this a view pointer that carries the module it names? */
|
|
35
|
+
export declare function isViewHandle(value: unknown): value is ValViewHandle;
|
|
36
|
+
/** The module a handle carries, or undefined if it carries none. */
|
|
37
|
+
export declare function viewHandleModule(value: unknown): unknown;
|
|
38
|
+
/**
|
|
39
|
+
* What a reader that needs a MODULE accepts, rather than a value.
|
|
40
|
+
*
|
|
41
|
+
* `useValKey`, `useValRoute`, `useValRouteUrl` and their `fetch*` counterparts
|
|
42
|
+
* all read a path, a schema and a source off what they are handed, so — unlike
|
|
43
|
+
* `useVal` — they cannot take an arbitrary selector. They CAN take a view
|
|
44
|
+
* pointing at one, which is the point of a page declaring which module it
|
|
45
|
+
* shows. {@link resolveViewedModule} is the runtime half, and takes exactly
|
|
46
|
+
* this, so the two cannot drift.
|
|
47
|
+
*/
|
|
48
|
+
export type ResolvableModule<S extends Source = SourceObject> = ValModule<GenericSelector<S>> | ValView<S>;
|
|
49
|
+
/**
|
|
50
|
+
* What a reader was actually handed: the module itself, or — when it is a view
|
|
51
|
+
* handle — the module that view points at.
|
|
52
|
+
*
|
|
53
|
+
* Every reader that needs a module rather than a value goes through this:
|
|
54
|
+
* `useValKey`, `useValRoute`, `useValRouteUrl` and their `fetch*` counterparts
|
|
55
|
+
* all pull the path, the schema and the source off the thing they are given,
|
|
56
|
+
* and a view handle has none of those. It is `{ view: "/foo.val.ts" }` with the
|
|
57
|
+
* module on a symbol, so reading `Internal.getValPath` off it gives
|
|
58
|
+
* `undefined`, and every one of those readers treats `undefined` as its own
|
|
59
|
+
* "no route matched" answer. Without this the call is not an error — it is a
|
|
60
|
+
* silently empty one.
|
|
61
|
+
*
|
|
62
|
+
* A POINTER with no module on it throws rather than being passed through, for
|
|
63
|
+
* the same reason `stegaEncode` throws on one: symbols do not survive
|
|
64
|
+
* serialization, so a handle that crossed the server/client boundary as a prop
|
|
65
|
+
* arrives looking like content and is not.
|
|
66
|
+
*/
|
|
67
|
+
export declare function resolveViewedModule<S extends Source>(selector: ResolvableModule<S>): ValModule<GenericSelector<S>>;
|
|
68
|
+
export declare function viewModulesOf(schema: Schema<SelectorSource> | undefined): Map<string, unknown>;
|
|
69
|
+
export {};
|