@valbuild/core 0.101.0 → 0.102.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.
@@ -21,7 +21,7 @@ export type ExtractedValModules = {
21
21
  * Extracts schemas and sources from a ValModules registry and computes the
22
22
  * deterministic SHAs that the server and client both use to detect changes.
23
23
  *
24
- * Used by ValOps on the server and by ValSyncEngine on the client so the
24
+ * Used by ValOps on the server and by `HostStore` on the client so the
25
25
  * hash algorithm has a single source of truth.
26
26
  */
27
27
  export declare function extractValModules(valModules: ValModules): Promise<ExtractedValModules>;
@@ -3,6 +3,7 @@ export { modules, type ValModules } from "./modules.js";
3
3
  export { extractValModules, type ExtractedValModules, type ExtractedModuleError, } from "./extractValModules.js";
4
4
  export type { InitVal, ValConfig, ValConstructor, ContentConstructor, } from "./initVal.js";
5
5
  export { Schema, type SerializedSchema, type SelectorOfSchema } from "./schema/index.js";
6
+ export { hasRemoteFileSchema } from "./schema/hasRemoteFileSchema.js";
6
7
  export type { ImageMetadata } from "./schema/image.js";
7
8
  export type { FileMetadata } from "./schema/file.js";
8
9
  export type { ValModule, SerializedModule, InferValModuleType } from "./module.js";
@@ -60,7 +61,7 @@ export { type SerializedRichTextSchema, RichTextSchema, } from "./schema/richtex
60
61
  export { type SerializedUnionSchema, UnionSchema, type SerializedStringUnionSchema, type SerializedObjectUnionSchema, } from "./schema/union.js";
61
62
  export { type SerializedLiteralSchema, LiteralSchema } from "./schema/literal.js";
62
63
  export { deserializeSchema } from "./schema/deserialize.js";
63
- export { type ListRecordRender, type ListArrayRender, type ReifiedRender, type CodeLanguage, type CodeRender, } from "./render.js";
64
+ export { type ListRecordRender, type ListArrayRender, type ReifiedRender, type RenderScope, type CodeLanguage, type CodeRender, renderScope, } from "./render.js";
64
65
  export type { ValRouter, RouteValidationError } from "./router.js";
65
66
  export { getSourcePathFromRoute } from "./getSourcePathFromRoute.js";
66
67
  export declare const FATAL_ERROR_TYPES: readonly ["no-schema", "no-source", "invalid-id", "no-module", "invalid-patch"];
@@ -306,7 +306,7 @@ export declare function initSchema(): {
306
306
  color: (options?: import("./schema/color.js").ColorOptions) => import("./schema/color.js").ColorSchema<import("./schema/string.js").RawString>;
307
307
  route: <T extends string>(options?: Record<string, never>) => import("./schema/route.js").RouteSchema<T>;
308
308
  router: typeof router;
309
- images: <Accept extends `image/${string}`>(options?: import("./schema/images.js").ImagesOptions<Accept>) => import("./schema/record.js").RecordSchema<import("./schema/object.js").ObjectSchema<{
309
+ images: <Accept extends `image/${string}`>(options: import("./schema/images.js").ImagesOptions<Accept>) => import("./schema/record.js").RecordSchema<import("./schema/object.js").ObjectSchema<{
310
310
  width: import("./schema/number.js").NumberSchema<number>;
311
311
  height: import("./schema/number.js").NumberSchema<number>;
312
312
  mimeType: import("./schema/string.js").StringSchema<string>;
@@ -19,11 +19,25 @@ export type ListRecordRender = {
19
19
  export type ListArrayRender = {
20
20
  layout: "list";
21
21
  parent: "array";
22
- items: {
23
- title: string;
24
- subtitle?: string | null;
25
- image?: ImageSource | RemoteSource<ImageMetadata> | null;
26
- }[];
22
+ /**
23
+ * The rendered items, each PAIRED WITH ITS INDEX — the same
24
+ * `[key, value][]` shape {@link ListRecordRender} uses.
25
+ *
26
+ * Not a positionally-indexed array, and the reason is
27
+ * {@link RenderScope}: a render computed for a subset of paths carries only
28
+ * those items, so `items[i]` would silently read the wrong row. Carrying the
29
+ * index makes a windowed render impossible to misread — a consumer looks its
30
+ * index up rather than trusting a position — and makes the array and record
31
+ * shapes symmetric.
32
+ */
33
+ items: [
34
+ index: number,
35
+ value: {
36
+ title: string;
37
+ subtitle?: string | null;
38
+ image?: ImageSource | RemoteSource<ImageMetadata> | null;
39
+ }
40
+ ][];
27
41
  };
28
42
  export type TextareaRender = {
29
43
  layout: "textarea";
@@ -45,5 +59,42 @@ type WithStatus<T> = {
45
59
  data: T;
46
60
  };
47
61
  export type ReifiedRender = Record<SourcePath | ModuleFilePath, WithStatus<RenderTypes>>;
62
+ /**
63
+ * Which paths a render is being computed FOR.
64
+ *
65
+ * `executeRender` takes a whole module, so one request has always walked every
66
+ * node in it and run every `select` closure — for `handboka`, with `select` at
67
+ * two nested array levels, that is every chapter and every section to serve one
68
+ * visible row. A scope is what makes the walk proportional to what is being
69
+ * looked at instead of to the project.
70
+ *
71
+ * Two questions, because a container and its items need different answers:
72
+ *
73
+ * - {@link RenderScope.wants} — is a render AT this exact path wanted? A
74
+ * container answers `true` here when the whole of it is being shown, and its
75
+ * list render is then computed in full.
76
+ * - {@link RenderScope.wantsUnder} — could anything at or below this path be
77
+ * wanted? Recursion is pruned where this is `false`, and a container whose own
78
+ * path is not wanted but which has wanted descendants renders a WINDOW: only
79
+ * the items that were asked for. That is the case a single visible row is, and
80
+ * it is why {@link ListArrayRender} carries indices.
81
+ *
82
+ * Absent scope means the whole module, which is what every existing caller
83
+ * passes and what every existing caller got.
84
+ */
85
+ export type RenderScope = {
86
+ wants(path: SourcePath | ModuleFilePath): boolean;
87
+ wantsUnder(path: SourcePath | ModuleFilePath): boolean;
88
+ };
89
+ /**
90
+ * A scope covering exactly `paths` and their subtrees.
91
+ *
92
+ * Compares SEGMENTS rather than string prefixes. A source path's module path is
93
+ * a quoted, dot-joined encoding (`?p="a".1."b"`), so `startsWith` gets the
94
+ * ancestor test wrong wherever a key is a prefix of a sibling key or contains a
95
+ * dot or a quote — `"title"` against `"titles"` being the cheapest example.
96
+ * Segments are parsed once here, not per comparison.
97
+ */
98
+ export declare function renderScope(paths: readonly SourcePath[]): RenderScope;
48
99
  export type RenderSelector<T extends Schema<SelectorSource>> = T extends Schema<infer S> ? S : never;
49
100
  export {};
@@ -1,5 +1,5 @@
1
1
  import { Schema, SchemaAssertResult, SelectorOfSchema, SerializedSchema } from "./index.js";
2
- import { RenderSelector, ReifiedRender } from "../render.js";
2
+ import { RenderSelector, ReifiedRender, RenderScope } from "../render.js";
3
3
  import { SelectorSource } from "../selector/index.js";
4
4
  import { ImageSource } from "../source/image.js";
5
5
  import { ModuleFilePath, SourcePath } from "../val/index.js";
@@ -8,6 +8,16 @@ export type SerializedArraySchema = {
8
8
  type: "array";
9
9
  item: SerializedSchema;
10
10
  opt: boolean;
11
+ /**
12
+ * Set when this schema declares a `render`.
13
+ *
14
+ * The render itself cannot be serialized — `select` is a user closure — but
15
+ * whether one EXISTS can be, and that is worth carrying: it lets the non-host
16
+ * side skip asking the host to render a module that cannot produce one.
17
+ * Measured in a browser: mounting 260 fields across 141 modules spent ~2.3ms
18
+ * of 3.1ms calling `executeRender` on modules that returned nothing.
19
+ */
20
+ render?: true;
11
21
  customValidate?: boolean;
12
22
  readonly?: boolean;
13
23
  hidden?: boolean;
@@ -41,7 +51,7 @@ export declare class ArraySchema<T extends Schema<SelectorSource>, Src extends S
41
51
  hidden(): ArraySchema<T, Src>;
42
52
  protected executeCustomValidateAt(path: SourcePath, src: Src): ValidationError[];
43
53
  protected executeSerialize(): SerializedArraySchema;
44
- protected executeRender(sourcePath: SourcePath | ModuleFilePath, src: Src): ReifiedRender;
54
+ protected executeRender(sourcePath: SourcePath | ModuleFilePath, src: Src, scope?: RenderScope): ReifiedRender;
45
55
  render(input: ArrayRenderInput<T>): ArraySchema<T, Src>;
46
56
  }
47
57
  export declare const array: <S extends Schema<SelectorSource>>(schema: S) => ArraySchema<S, SelectorOfSchema<S>[]>;
@@ -14,9 +14,13 @@ export type FilesOptions = {
14
14
  /**
15
15
  * The directory where files should be stored.
16
16
  * Must start with "/public" (e.g., "/public/val/files")
17
- * @default "/public/val"
17
+ *
18
+ * Required, and deliberately so: it decides where every uploaded file in this
19
+ * collection lands, and it used to default to "/public/val" — which meant a
20
+ * collection that had simply not said where it wanted its files silently shared
21
+ * a directory with every other one.
18
22
  */
19
- directory?: "/public" | `/public/${string}`;
23
+ directory: "/public" | `/public/${string}`;
20
24
  /**
21
25
  * Whether remote files are allowed
22
26
  * @default false
@@ -0,0 +1,34 @@
1
+ import type { SerializedSchema } from "./index.js";
2
+ /**
3
+ * Does this schema store any file remotely?
4
+ *
5
+ * The one answer to that question. There used to be two — `hasRemoteFileSchema`
6
+ * in the server, gating whether `/save` demands remote credentials, and
7
+ * `findRequiredRemoteFiles` in the Studio, gating the `/remote/settings` fetch —
8
+ * and they disagreed about `s.images({ remote: true })`: the Studio's counted a
9
+ * remote media record, the server's did not, because a media collection
10
+ * serializes as a `record` of metadata rather than as an image schema.
11
+ *
12
+ * The server's answer was the wrong one, and silently so. With it false,
13
+ * `/save` runs `saveOrUploadFiles` in `skip-remote` mode, and that mode does not
14
+ * merely skip the upload — it drops every remote descriptor without an error. The
15
+ * commit then lands a remote ref with no bytes behind it: a broken image, in the
16
+ * repository, with nothing anywhere saying why.
17
+ *
18
+ * ## Why it stays a walk over the SERIALIZED schema
19
+ *
20
+ * Both callers have a `SerializedSchema` and not a `Schema`: the server reads
21
+ * them out of `getSchemas()`, and the Studio is handed them by `host.receive`.
22
+ * Asking the schema classes directly would mean a method on every one of them,
23
+ * and the two places that need the answer cannot call it.
24
+ *
25
+ * ## Throwing
26
+ *
27
+ * The `never` assignment at the end is the point of the default branch: adding a
28
+ * schema type without teaching this function about it must not compile. The throw
29
+ * is what happens if one arrives anyway — which for a caller reading schemas out
30
+ * of its own process should be impossible. A caller that cannot afford to throw
31
+ * (the Studio, whose whole UI is downstream of this) catches it; returning `false`
32
+ * instead would reintroduce exactly the silent skip described above.
33
+ */
34
+ export declare function hasRemoteFileSchema(schema: SerializedSchema): boolean;
@@ -20,9 +20,13 @@ export type ImagesOptions<Accept extends `image/${string}`> = {
20
20
  /**
21
21
  * The directory where images should be stored.
22
22
  * Must start with "/public" (e.g., "/public/val/images")
23
- * @default "/public/val"
23
+ *
24
+ * Required, and deliberately so: it decides where every uploaded file in this
25
+ * collection lands, and it used to default to "/public/val" — which meant a
26
+ * gallery that had simply not said where it wanted its files silently shared a
27
+ * directory with every other one.
24
28
  */
25
- directory?: "/public" | `/public/${string}`;
29
+ directory: "/public" | `/public/${string}`;
26
30
  /**
27
31
  * Alt text schema. Can be:
28
32
  * - s.string() for required alt text
@@ -65,8 +69,9 @@ type ImagesItemSrc = {
65
69
  /**
66
70
  * Define a collection of images.
67
71
  *
68
- * All options are optional: `s.images()` accepts any image type (`"image/*"`) in
69
- * the default `/public/val` directory, with nullable alt text and remote disabled.
72
+ * `directory` is required — it decides where uploads land, so it is not something
73
+ * to be inferred. The rest defaults: any image type (`"image/*"`), nullable alt
74
+ * text, remote disabled.
70
75
  *
71
76
  * @example
72
77
  * ```typescript
@@ -85,5 +90,5 @@ type ImagesItemSrc = {
85
90
  * });
86
91
  * ```
87
92
  */
88
- export declare const images: <Accept extends `image/${string}`>(options?: ImagesOptions<Accept>) => RecordSchema<ObjectSchema<ImagesItemProps, ImagesItemSrc>, Schema<string>, Record<string, ImagesEntryMetadata>>;
93
+ export declare const images: <Accept extends `image/${string}`>(options: ImagesOptions<Accept>) => RecordSchema<ObjectSchema<ImagesItemProps, ImagesItemSrc>, Schema<string>, Record<string, ImagesEntryMetadata>>;
89
94
  export {};
@@ -19,7 +19,7 @@ import { SerializedRouteSchema } from "./route.js";
19
19
  import { ValidationError, ValidationErrors } from "./validation/ValidationError.js";
20
20
  import { FileSource } from "../source/file.js";
21
21
  import { GenericRichTextSourceNode, RichTextSource } from "../source/richtext.js";
22
- import { ReifiedRender } from "../render.js";
22
+ import { ReifiedRender, RenderScope } from "../render.js";
23
23
  export type SerializedSchema = SerializedStringSchema | SerializedLiteralSchema | SerializedBooleanSchema | SerializedNumberSchema | SerializedObjectSchema | SerializedArraySchema | SerializedUnionSchema | SerializedRichTextSchema | SerializedRecordSchema | SerializedKeyOfSchema | SerializedFileSchema | SerializedDateSchema | SerializedDateTimeSchema | SerializedColorSchema | SerializedRouteSchema | SerializedImageSchema;
24
24
  type Primitives = number | string | boolean | null | FileSource;
25
25
  export type AssertError = {
@@ -106,7 +106,14 @@ export declare abstract class Schema<Src extends SelectorSource> {
106
106
  */
107
107
  abstract hidden(): Schema<Src>;
108
108
  protected abstract executeSerialize(): SerializedSchema;
109
- protected abstract executeRender(sourcePath: SourcePath | ModuleFilePath, src: Src): ReifiedRender;
109
+ /**
110
+ * @param scope Which paths the caller needs a render for. Absent means the
111
+ * whole module, which is what every caller passed before scoping existed.
112
+ * See {@link RenderScope}: a container prunes recursion where nothing is
113
+ * wanted, and renders a WINDOW when its own path is not wanted but some of its
114
+ * items are — which is the single-visible-row case.
115
+ */
116
+ protected abstract executeRender(sourcePath: SourcePath | ModuleFilePath, src: Src, scope?: RenderScope): ReifiedRender;
110
117
  /** MUTATES! since internal and perf sensitive */
111
118
  protected appendValidationError(current: ValidationErrors, path: SourcePath, message: string, value: unknown, schemaError?: boolean): ValidationErrors;
112
119
  /**
@@ -1,5 +1,5 @@
1
1
  import { CustomValidateFunction, Schema, SchemaAssertResult, SelectorOfSchema, SerializedSchema } from "./index.js";
2
- import { ReifiedRender } from "../render.js";
2
+ import { ReifiedRender, RenderScope } from "../render.js";
3
3
  import { SelectorSource } from "../selector/index.js";
4
4
  import { ModuleFilePath, SourcePath } from "../val/index.js";
5
5
  import { ValidationError, ValidationErrors } from "./validation/ValidationError.js";
@@ -52,7 +52,7 @@ export declare class ObjectSchema<Props extends ObjectSchemaProps, Src extends O
52
52
  hidden(): ObjectSchema<Props, Src>;
53
53
  protected executeCustomValidateAt(path: SourcePath, src: Src): ValidationError[];
54
54
  protected executeSerialize(): SerializedSchema;
55
- protected executeRender(sourcePath: SourcePath | ModuleFilePath, src: Src): ReifiedRender;
55
+ protected executeRender(sourcePath: SourcePath | ModuleFilePath, src: Src, scope?: RenderScope): ReifiedRender;
56
56
  }
57
57
  export declare const object: <Props extends ObjectSchemaProps>(schema: Props) => ObjectSchema<Props, ObjectSchemaSrcOf<Props>>;
58
58
  export {};
@@ -1,5 +1,5 @@
1
1
  import { CustomValidateFunction, Schema, SchemaAssertResult, SelectorOfSchema, SerializedSchema } from "./index.js";
2
- import { RenderSelector, ReifiedRender } from "../render.js";
2
+ import { RenderSelector, ReifiedRender, RenderScope } from "../render.js";
3
3
  import { ValRouter } from "../router.js";
4
4
  import { SelectorSource } from "../selector/index.js";
5
5
  import { ImageSource } from "../source/image.js";
@@ -20,6 +20,8 @@ export type SerializedRecordSchema = {
20
20
  item: SerializedSchema;
21
21
  key?: SerializedSchema;
22
22
  opt: boolean;
23
+ /** Set when this schema declares a `render`. See `SerializedArraySchema`. */
24
+ render?: true;
23
25
  router?: string;
24
26
  customValidate?: boolean;
25
27
  mediaType?: "files" | "images";
@@ -113,7 +115,7 @@ export declare class RecordSchema<T extends Schema<SelectorSource>, K extends Sc
113
115
  * defers).
114
116
  */
115
117
  validateJsonEntryContent(path: SourcePath, content: SelectorSource): ValidationErrors;
116
- protected executeRender(sourcePath: SourcePath | ModuleFilePath, src: Src): ReifiedRender;
118
+ protected executeRender(sourcePath: SourcePath | ModuleFilePath, src: Src, scope?: RenderScope): ReifiedRender;
117
119
  render(input: {
118
120
  as: "list";
119
121
  select: (input: {
@@ -1,5 +1,5 @@
1
1
  import { Schema, SchemaAssertResult, SerializedSchema } from "./index.js";
2
- import { CodeLanguage, ReifiedRender } from "../render.js";
2
+ import { CodeLanguage, ReifiedRender, RenderScope } from "../render.js";
3
3
  import { ModuleFilePath, SourcePath } from "../val/index.js";
4
4
  import { ValidationError, ValidationErrors } from "./validation/ValidationError.js";
5
5
  type StringOptions = {
@@ -10,6 +10,8 @@ type StringOptions = {
10
10
  };
11
11
  export type SerializedStringSchema = {
12
12
  type: "string";
13
+ /** Set when this schema declares a `render`. See `SerializedArraySchema`. */
14
+ render?: true;
13
15
  options?: {
14
16
  maxLength?: number;
15
17
  minLength?: number;
@@ -73,7 +75,7 @@ export declare class StringSchema<Src extends string | null> extends Schema<Src>
73
75
  as: "code";
74
76
  language: CodeLanguage;
75
77
  }): StringSchema<Src>;
76
- protected executeRender(sourcePath: SourcePath | ModuleFilePath, src: Src): ReifiedRender;
78
+ protected executeRender(sourcePath: SourcePath | ModuleFilePath, src: Src, _scope?: RenderScope): ReifiedRender;
77
79
  }
78
80
  export declare const string: <T extends string>(options?: Record<string, never>) => StringSchema<T>;
79
81
  export {};
@@ -1,5 +1,5 @@
1
1
  import { CustomValidateFunction, Schema, SchemaAssertResult, SerializedSchema } from "./index.js";
2
- import { ReifiedRender } from "../render.js";
2
+ import { ReifiedRender, RenderScope } from "../render.js";
3
3
  import { SelectorSource } from "../selector/index.js";
4
4
  import { SourceObject } from "../source/index.js";
5
5
  import { ModuleFilePath, SourcePath } from "../val/index.js";
@@ -57,7 +57,7 @@ export declare class UnionSchema<Key extends string | Schema<string>, T extends
57
57
  private matchedVariant;
58
58
  protected executeSerialize(): SerializedSchema;
59
59
  constructor(key: Key, items: T, opt?: boolean, customValidateFunctions?: CustomValidateFunction<Src>[], isReadonly?: boolean, isHidden?: boolean, description?: string | undefined);
60
- protected executeRender(sourcePath: SourcePath | ModuleFilePath, src: Src): ReifiedRender;
60
+ protected executeRender(sourcePath: SourcePath | ModuleFilePath, src: Src, scope?: RenderScope): ReifiedRender;
61
61
  }
62
62
  export declare const union: <Key extends string | Schema<string>, T extends Schema<Key extends string ? SourceObject & { [k in Key]: string; } : Key extends Schema<string> ? string : unknown>[]>(key: Key, ...objects: T) => UnionSchema<Key, T, SourceOf<Key, T>>;
63
63
  export {};