@valbuild/core 0.130.0 → 0.136.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,124 @@
1
1
  # @valbuild/core
2
2
 
3
+ ## 0.136.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#713](https://github.com/valbuild/val/pull/713) [`f3a4bb7`](https://github.com/valbuild/val/commit/f3a4bb7aea604943b92545c122243798c759715c) Thanks [@freekh](https://github.com/freekh)! - The Studio can load a bundler, and it stops telling a managed project that its
8
+ publish is on its way out.
9
+
10
+ **A managed project is one with no repository and no host watching one.** There
11
+ is nothing outside the browser to pick a commit up, so the Studio has been
12
+ showing it a story that belongs to a project with a repository: a `Building`
13
+ spinner, waiting for an event that can never arrive. It does not resolve on a
14
+ reload, on a retry, or tomorrow, and there is no way to tell it from a deploy
15
+ that is merely slow.
16
+
17
+ The content service now reports which kind of project this is, as `sourceMode`
18
+ on `/stat`, and the Studio narrates accordingly:
19
+
20
+ - a **connected** project keeps the deploy feed and keeps `Building`, because
21
+ there a host genuinely does pick the commit up;
22
+ - a **managed** one says `Saved, not yet live` instead — a durable condition,
23
+ not a phase, because nothing else will ever resolve it.
24
+
25
+ A server that reports no source mode keeps the behaviour it has today. Absence
26
+ means "not reported", never "managed": `fs` mode has no project to have a mode,
27
+ and neither does a content service that predates the field.
28
+
29
+ **The Studio also loads `@valbuild/tanstack-build` in the tab**, at mount and at
30
+ idle, for the managed projects that will need it — a browser that is the
31
+ deployer needs a bundler. This is the groundwork for browser-side publishing;
32
+ the publish path itself is unchanged in this release.
33
+
34
+ That bundler is `@rolldown/browser`, whose WebAssembly binary is 10.9 MB and is
35
+ **not** in the npm package. It is served from `static.val.build`, addressed by
36
+ the SHA-256 of its own bytes, so the build and the binary it needs cannot drift
37
+ apart. A deployment that must not reach that host — an air-gapped install, a
38
+ mirror — sets `globalThis.__VAL_ROLLDOWN_WASM_URL__` before the Studio loads and
39
+ needs no rebuild.
40
+
41
+ **Building in the browser requires a cross-origin isolated page.** Rolldown runs
42
+ WebAssembly on worker threads that share memory, and a browser will not hand a
43
+ `SharedArrayBuffer` to a worker otherwise. Without
44
+ `Cross-Origin-Opener-Policy: same-origin` and
45
+ `Cross-Origin-Embedder-Policy: require-corp` on the document Val is mounted in,
46
+ loading the bundler now fails with a message that says exactly that, instead of
47
+ a `DataCloneError` thrown from inside a worker. Nothing else in this release is
48
+ affected: a Studio that never builds in the browser never asks.
49
+
50
+ ## 0.134.0
51
+
52
+ ### Minor Changes
53
+
54
+ - [#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`.
55
+
56
+ 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:
57
+
58
+ ```tsx
59
+ const page = useVal(pageVal);
60
+ const header = useVal(page.header); // the header module's content, typed
61
+ ```
62
+
63
+ 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.
64
+
65
+ 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:
66
+
67
+ ```tsx
68
+ const page = useVal(pageVal);
69
+ const note = useValRoute(page.notes, params); // the router module the view names
70
+ ```
71
+
72
+ Works in draft mode, including when the page itself has a pending edit. Two things to know:
73
+
74
+ - **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.
75
+ - **`ValView<Source>` is now a member of `SelectorSource`**, since a reader accepts one.
76
+
77
+ - [#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.
78
+
79
+ 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:
80
+
81
+ ```ts
82
+ import employeesVal from "../data/employees.val";
83
+
84
+ const schema = s.object({
85
+ title: s.string(),
86
+ employees: s.view(employeesVal),
87
+ });
88
+
89
+ export default c.define("/app/menneskene/page.val.ts", schema, {
90
+ title: "Våre folk",
91
+ employees: { view: "/data/employees.val.ts" },
92
+ });
93
+ ```
94
+
95
+ 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.
96
+
97
+ A few things worth knowing:
98
+
99
+ - **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" }`.
100
+ - **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.
101
+ - **`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.
102
+ - **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.
103
+ - **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.
104
+ - **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.
105
+
106
+ 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:
107
+
108
+ ```ts
109
+ // Not in the nav, and on exactly one page.
110
+ export default c.define("/data/employees.val.ts", s.record(...).hidden(), { ... });
111
+ ```
112
+
113
+ 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.
114
+
115
+ One gap worth naming rather than leaving to be discovered: a view pointing at a
116
+ module the project does not have is reported by the FIELD — the row says the
117
+ target is missing — but not by `val validate`. The schema check compares the
118
+ pointer against the schema, and the cycle check deliberately skips a target that
119
+ is not a module of the project, so neither catches it. A project-level check
120
+ belongs with them and is not here yet.
121
+
3
122
  ## 0.130.0
4
123
 
5
124
  ### 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";
@@ -81,10 +86,29 @@ export declare const FATAL_ERROR_TYPES: readonly ["no-schema", "no-source", "inv
81
86
  export type FatalErrorType = (typeof FATAL_ERROR_TYPES)[number];
82
87
  export declare const DEFAULT_CONTENT_HOST = "https://content.val.build";
83
88
  export declare const DEFAULT_APP_HOST = "https://admin.val.build";
89
+ /**
90
+ * Where Val's own immutable build artifacts are served from.
91
+ *
92
+ * Only one kind of thing lives here so far: the WebAssembly binary of the
93
+ * bundler the Studio builds a site with. It cannot travel with the Studio's
94
+ * own bundle -- 10.9 MB of binary, and `/api/val/static` is a base64 record of
95
+ * TEXT inside the server bundle, which both corrupts it and would carry it
96
+ * into every project that installs `@valbuild/ui`.
97
+ *
98
+ * Everything under this host is addressed by the SHA-256 of its own bytes, so
99
+ * an upload is idempotent, a URL is immutable, and the artifact a build asks
100
+ * for cannot be a different one than the build was made against.
101
+ */
102
+ export declare const DEFAULT_STATIC_HOST = "https://static.val.build";
84
103
  declare const Internal: {
85
104
  VERSION: {
86
105
  core: string;
87
106
  };
107
+ createViewHandle: typeof createViewHandle;
108
+ isViewHandle: typeof isViewHandle;
109
+ resolveViewedModule: typeof resolveViewedModule;
110
+ viewHandleModule: typeof viewHandleModule;
111
+ viewModulesOf: typeof viewModulesOf;
88
112
  mediaUrl: typeof mediaUrl;
89
113
  resolveMedia: typeof resolveMedia;
90
114
  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 {};