@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 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
- export type ValModule<T extends SelectorSource> = SelectorOf<T> & ValModuleBrand;
10
- export type ValModuleBrand = {
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>>(id: string, // TODO: `/${string}`
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 {};