@valbuild/core 0.126.0 → 0.128.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.
Files changed (32) hide show
  1. package/CHANGELOG.md +296 -0
  2. package/dist/declarations/src/index.d.ts +5 -1
  3. package/dist/declarations/src/initSchema.d.ts +31 -15
  4. package/dist/declarations/src/locale.d.ts +55 -0
  5. package/dist/declarations/src/schema/array.d.ts +4 -0
  6. package/dist/declarations/src/schema/declaredKeys.d.ts +37 -0
  7. package/dist/declarations/src/schema/discriminatedUnion.d.ts +4 -0
  8. package/dist/declarations/src/schema/discriminatedUnionBranch.d.ts +20 -0
  9. package/dist/declarations/src/schema/enum.d.ts +9 -0
  10. package/dist/declarations/src/schema/file.d.ts +3 -3
  11. package/dist/declarations/src/schema/{files.d.ts → fileset.d.ts} +10 -10
  12. package/dist/declarations/src/schema/hasRemoteFileSchema.d.ts +3 -3
  13. package/dist/declarations/src/schema/image.d.ts +9 -5
  14. package/dist/declarations/src/schema/{images.d.ts → imageset.d.ts} +54 -21
  15. package/dist/declarations/src/schema/index.d.ts +116 -3
  16. package/dist/declarations/src/schema/literal.d.ts +2 -0
  17. package/dist/declarations/src/schema/locale.d.ts +132 -0
  18. package/dist/declarations/src/schema/object.d.ts +7 -0
  19. package/dist/declarations/src/schema/record.d.ts +57 -7
  20. package/dist/declarations/src/schema/settings.d.ts +54 -1
  21. package/dist/declarations/src/schema/validation/ValidationFix.d.ts +1 -1
  22. package/dist/declarations/src/source/settings.d.ts +51 -0
  23. package/dist/{index-6db78f3c.cjs.prod.js → index-053ccde2.cjs.prod.js} +1114 -91
  24. package/dist/{index-b41b638f.cjs.dev.js → index-c3ae637d.cjs.dev.js} +1114 -91
  25. package/dist/{index-cd25c2dc.esm.js → index-db41de3c.esm.js} +1109 -92
  26. package/dist/valbuild-core.cjs.dev.js +7 -1
  27. package/dist/valbuild-core.cjs.prod.js +7 -1
  28. package/dist/valbuild-core.esm.js +1 -1
  29. package/package.json +1 -1
  30. package/patch/dist/valbuild-core-patch.cjs.dev.js +1 -1
  31. package/patch/dist/valbuild-core-patch.cjs.prod.js +1 -1
  32. package/patch/dist/valbuild-core-patch.esm.js +2 -2
@@ -4,9 +4,9 @@ import { RecordSchema } from "./record.js";
4
4
  import { ObjectSchema } from "./object.js";
5
5
  import { StringSchema } from "./string.js";
6
6
  /**
7
- * Options for s.files()
7
+ * Options for s.fileset()
8
8
  */
9
- export type FilesOptions = {
9
+ export type FilesetOptions = {
10
10
  /**
11
11
  * The accepted mime type pattern (e.g., "application/pdf", "text/*", "*\/*")
12
12
  */
@@ -20,19 +20,19 @@ export type FilesOptions = {
20
20
  * collection that had simply not said where it wanted its files silently shared
21
21
  * a directory with every other one.
22
22
  */
23
- directory: "/public" | `/public/${string}`;
23
+ dir: "/public" | `/public/${string}`;
24
24
  };
25
25
  /**
26
26
  * Metadata for a file entry in the files record
27
27
  */
28
- export type FilesEntryMetadata = {
28
+ export type FilesetEntryMetadata = {
29
29
  mimeType: string;
30
30
  };
31
- export type SerializedFilesSchema = SerializedRecordSchema;
32
- type FilesItemProps = {
31
+ export type SerializedFilesetSchema = SerializedRecordSchema;
32
+ type FilesetItemProps = {
33
33
  mimeType: StringSchema<string>;
34
34
  };
35
- type FilesItemSrc = {
35
+ type FilesetItemSrc = {
36
36
  mimeType: string;
37
37
  };
38
38
  /**
@@ -42,9 +42,9 @@ type FilesItemSrc = {
42
42
  *
43
43
  * @example
44
44
  * ```typescript
45
- * const schema = s.files({
45
+ * const schema = s.fileset({
46
46
  * accept: "application/pdf",
47
- * directory: "/public/val/documents",
47
+ * dir: "/public/val/documents",
48
48
  * });
49
49
  * export default c.define("/content/documents.val.ts", schema, {
50
50
  * "/public/val/documents/report.pdf": {
@@ -53,5 +53,5 @@ type FilesItemSrc = {
53
53
  * });
54
54
  * ```
55
55
  */
56
- export declare const files: (options: FilesOptions) => RecordSchema<ObjectSchema<FilesItemProps, FilesItemSrc>, Schema<string>, Record<string, FilesEntryMetadata>>;
56
+ export declare const fileset: (options: FilesetOptions) => RecordSchema<ObjectSchema<FilesetItemProps, FilesetItemSrc>, Schema<string>, Record<string, FilesetEntryMetadata>>;
57
57
  export {};
@@ -5,7 +5,7 @@ import type { SerializedSchema } from "./index.js";
5
5
  * The one answer to that question. There used to be two — `hasRemoteFileSchema`
6
6
  * in the server, gating whether `/save` demands remote credentials, and
7
7
  * `findRequiredRemoteFiles` in the Studio, gating the `/remote/settings` fetch —
8
- * and they disagreed about `s.images({ ... }).remote()`: the Studio's version
8
+ * and they disagreed about `s.imageset({ ... }).remote()`: the Studio's version
9
9
  * counted a remote media record, the server's did not, because a media
10
10
  * collection serializes as a `record` of metadata rather than as an image
11
11
  * schema.
@@ -43,8 +43,8 @@ export declare function hasRemoteFileSchema(schema: SerializedSchema): boolean;
43
43
  *
44
44
  * It is a walk over the SERIALIZED schema for the reasons given above, and for
45
45
  * one more that is specific to this question: the two cases most likely to be
46
- * missed are invisible to a value type. A media collection (`s.images()` /
47
- * `s.files()`) serializes as a record of metadata with no image schema inside
46
+ * missed are invisible to a value type. A media collection (`s.imageset()` /
47
+ * `s.fileset()`) serializes as a record of metadata with no image schema inside
48
48
  * it, and a richtext inline image lives in a constructor argument. A check
49
49
  * written against the item TYPE would silently pass both — and the gallery case
50
50
  * already has a data-loss incident attached to it (see above).
@@ -5,7 +5,7 @@ import { ValidationError, ValidationErrors } from "./validation/ValidationError.
5
5
  import { ValModule } from "../index.js";
6
6
  import { ItemPreviewInput, PreviewItem, ReifiedPreview } from "../preview.js";
7
7
  import { FieldRender } from "../render.js";
8
- import { ImagesEntryMetadata } from "./images.js";
8
+ import { AltSource, ImagesetEntryMetadata } from "./imageset.js";
9
9
  /**
10
10
  * How an uploaded image is re-encoded in the browser, before it is uploaded.
11
11
  *
@@ -41,7 +41,7 @@ export type GalleryImageOptions = {
41
41
  encode?: ImageEncodeOption;
42
42
  };
43
43
  export type ImageOptions = {
44
- directory?: string;
44
+ dir?: string;
45
45
  accept?: string;
46
46
  encode?: ImageEncodeOption;
47
47
  };
@@ -81,7 +81,7 @@ export declare class ImageSchema<Src extends ImageSource | null> extends Schema<
81
81
  private readonly description?;
82
82
  private readonly renderInput;
83
83
  private readonly previewInput;
84
- constructor(options?: ImageOptions | undefined, opt?: boolean, isRemote?: boolean, customValidateFunctions?: CustomValidateFunction<Src>[], moduleMetadata?: Record<ModulePath, Record<string, ImagesEntryMetadata>>, isReadonly?: boolean, isHidden?: boolean, description?: string | undefined, renderInput?: FieldRender | null, previewInput?: ItemPreviewInput<Src> | null);
84
+ constructor(options?: ImageOptions | undefined, opt?: boolean, isRemote?: boolean, customValidateFunctions?: CustomValidateFunction<Src>[], moduleMetadata?: Record<ModulePath, Record<string, ImagesetEntryMetadata<AltSource>>>, isReadonly?: boolean, isHidden?: boolean, description?: string | undefined, renderInput?: FieldRender | null, previewInput?: ItemPreviewInput<Src> | null);
85
85
  /**
86
86
  * Describe this field.
87
87
  *
@@ -119,7 +119,7 @@ export declare class ImageSchema<Src extends ImageSource | null> extends Schema<
119
119
  *
120
120
  * @example
121
121
  * const schema = s
122
- * .image({ accept: "image/webp", directory: "/public/val/images" })
122
+ * .image({ accept: "image/webp", dir: "/public/val/images" })
123
123
  * .remote();
124
124
  * export default c.define("/example.val.ts", schema, {
125
125
  * path: "https://remote.val.build/file/p/my-project/b/01/v/1.0.0/h/8f2a1c/f/3b9d70/p/public/val/images/example.webp",
@@ -163,6 +163,10 @@ export declare class ImageSchema<Src extends ImageSource | null> extends Schema<
163
163
  /**
164
164
  * The entries of the gallery this field points at, or null when it is a
165
165
  * standalone field.
166
+ *
167
+ * `AltSource` rather than a specific alt type: a field never reads an entry's
168
+ * alt — it carries its own — so it only needs the entry keys and the
169
+ * dimensions, and a gallery of any alt shape can back it.
166
170
  */
167
171
  private galleryEntries;
168
172
  protected executeAssert(path: SourcePath, src: unknown): SchemaAssertResult<Src>;
@@ -221,6 +225,6 @@ export declare class ImageSchema<Src extends ImageSource | null> extends Schema<
221
225
  * An image picked from a gallery. Its dimensions and mime type live in the
222
226
  * gallery, so the field carries only what a person typed.
223
227
  */
224
- export declare function image(galleryModule: ValModule<Record<string, ImagesEntryMetadata>>, galleryOptions?: GalleryImageOptions): ImageSchema<GalleryImageSource>;
228
+ export declare function image(galleryModule: ValModule<Record<string, ImagesetEntryMetadata<AltSource>>>, galleryOptions?: GalleryImageOptions): ImageSchema<GalleryImageSource>;
225
229
  /** An image of its own, carrying its own dimensions and mime type. */
226
230
  export declare function image(options?: ImageOptions): ImageSchema<ImageSource>;
@@ -1,4 +1,5 @@
1
1
  import { Schema } from "./index.js";
2
+ import type { SelectorOfSchema } from "./index.js";
2
3
  import type { SerializedRecordSchema } from "./record.js";
3
4
  import { RecordSchema } from "./record.js";
4
5
  import { ObjectSchema } from "./object.js";
@@ -10,9 +11,43 @@ import type { ImageEncodeOption } from "./image.js";
10
11
  */
11
12
  export type AltSchema = StringSchema<string> | StringSchema<string | null> | RecordSchema<StringSchema<string>, Schema<string>, Record<string, string>>;
12
13
  /**
13
- * Options for s.images()
14
+ * What an entry's `alt` holds, for a given `alt` schema.
15
+ *
16
+ * This is the schema's own source type, not a second list that has to be kept
17
+ * in step with `AltSchema`: `s.string()` gives `string`, `s.string().nullable()`
18
+ * gives `string | null`, and `s.record(s.string())` gives
19
+ * `Record<string, string>`.
20
+ */
21
+ export type AltSourceOf<Alt extends AltSchema> = SelectorOfSchema<Alt>;
22
+ /** Every `alt` value any `AltSchema` can produce. */
23
+ export type AltSource = AltSourceOf<AltSchema>;
24
+ /**
25
+ * Options for s.imageset()
26
+ */
27
+ export type ImagesetOptions<Accept extends `image/${string}`, Alt extends AltSchema = StringSchema<string | null>> = ImagesetOptionsBase<Accept> & ImagesetAltOption<Alt>;
28
+ /**
29
+ * `alt` is optional only when it is the default schema.
30
+ *
31
+ * Naming a different `Alt` and then leaving `alt` out would make the fallback
32
+ * in `imageset()` a lie — `Alt` would say `s.string()` while the value was a
33
+ * nullable one. Requiring it in that case is what makes the fallback sound:
34
+ * the branch that uses it is reachable only when `Alt` IS the default.
14
35
  */
15
- export type ImagesOptions<Accept extends `image/${string}`> = {
36
+ type ImagesetAltOption<Alt extends AltSchema> = [
37
+ StringSchema<string | null>
38
+ ] extends [Alt] ? {
39
+ /**
40
+ * Alt text schema. Can be:
41
+ * - s.string() for required alt text
42
+ * - s.string().nullable() for optional alt text (default)
43
+ * - s.record(s.string(), s.string()) for locale-based alt text
44
+ */
45
+ alt?: Alt;
46
+ } : {
47
+ /** Alt text schema — required, because it is not the default one. */
48
+ alt: Alt;
49
+ };
50
+ type ImagesetOptionsBase<Accept extends `image/${string}`> = {
16
51
  /**
17
52
  * The accepted mime type pattern. Must be an image type (e.g., "image/png", "image/webp", "image/*")
18
53
  * @default "image/*"
@@ -27,14 +62,7 @@ export type ImagesOptions<Accept extends `image/${string}`> = {
27
62
  * gallery that had simply not said where it wanted its files silently shared a
28
63
  * directory with every other one.
29
64
  */
30
- directory: "/public" | `/public/${string}`;
31
- /**
32
- * Alt text schema. Can be:
33
- * - s.string() for required alt text
34
- * - s.string().nullable() for optional alt text (default)
35
- * - s.record(s.string(), s.string()) for locale-based alt text
36
- */
37
- alt?: AltSchema;
65
+ dir: "/public" | `/public/${string}`;
38
66
  /**
39
67
  * Re-encode uploads in the browser before they are uploaded.
40
68
  *
@@ -44,30 +72,35 @@ export type ImagesOptions<Accept extends `image/${string}`> = {
44
72
  encode?: ImageEncodeOption;
45
73
  };
46
74
  /**
47
- * Metadata for an image entry in the images record
75
+ * Metadata for an image entry in the images record.
76
+ *
77
+ * `alt` follows the schema's `alt` option, so it defaults to `string | null`
78
+ * and becomes `string` under `s.string()` or `Record<string, string>` under
79
+ * `s.record(s.string())`. Write `ImagesetEntryMetadata<AltSource>` where a
80
+ * gallery of any alt shape is acceptable.
48
81
  */
49
- export type ImagesEntryMetadata = {
82
+ export type ImagesetEntryMetadata<Alt extends AltSource = string | null> = {
50
83
  width: number;
51
84
  height: number;
52
85
  mimeType: string;
53
- alt: string | null;
86
+ alt: Alt;
54
87
  hotspot?: {
55
88
  x: number;
56
89
  y: number;
57
90
  };
58
91
  };
59
- export type SerializedImagesSchema = SerializedRecordSchema;
60
- type ImagesItemProps = {
92
+ export type SerializedImagesetSchema = SerializedRecordSchema;
93
+ type ImagesetItemProps<Alt extends AltSchema> = {
61
94
  width: NumberSchema<number>;
62
95
  height: NumberSchema<number>;
63
96
  mimeType: StringSchema<string>;
64
- alt: StringSchema<string | null>;
97
+ alt: Alt;
65
98
  };
66
- type ImagesItemSrc = {
99
+ type ImagesetItemSrc<Alt extends AltSchema> = {
67
100
  width: number;
68
101
  height: number;
69
102
  mimeType: string;
70
- alt: string | null;
103
+ alt: AltSourceOf<Alt>;
71
104
  };
72
105
  /**
73
106
  * Define a collection of images.
@@ -78,9 +111,9 @@ type ImagesItemSrc = {
78
111
  *
79
112
  * @example
80
113
  * ```typescript
81
- * const schema = s.images({
114
+ * const schema = s.imageset({
82
115
  * accept: "image/webp",
83
- * directory: "/public/val/images",
116
+ * dir: "/public/val/images",
84
117
  * alt: s.string().minLength(4),
85
118
  * });
86
119
  * export default c.define("/content/images.val.ts", schema, {
@@ -93,5 +126,5 @@ type ImagesItemSrc = {
93
126
  * });
94
127
  * ```
95
128
  */
96
- export declare const images: <Accept extends `image/${string}`>(options: ImagesOptions<Accept>) => RecordSchema<ObjectSchema<ImagesItemProps, ImagesItemSrc>, Schema<string>, Record<string, ImagesEntryMetadata>>;
129
+ export declare const imageset: <Accept extends `image/${string}`, Alt extends AltSchema = StringSchema<string | null>>(options: ImagesetOptions<Accept, Alt>) => RecordSchema<ObjectSchema<ImagesetItemProps<Alt>, ImagesetItemSrc<Alt>>, Schema<string>, Record<string, ImagesetEntryMetadata<AltSourceOf<Alt>>>>;
97
130
  export {};
@@ -17,13 +17,14 @@ import { SerializedCodeSchema } from "./code.js";
17
17
  import { SerializedColorSchema } from "./color.js";
18
18
  import { SerializedDateSchema } from "./date.js";
19
19
  import { SerializedDateTimeSchema } from "./datetime.js";
20
+ import { SerializedLocaleSchema } from "./locale.js";
20
21
  import { SerializedRouteSchema } from "./route.js";
21
22
  import { SerializedSettingsSchema } from "./settings.js";
22
23
  import { ValidationError, ValidationErrors } from "./validation/ValidationError.js";
23
24
  import { FileSource } from "../source/media.js";
24
25
  import { GenericRichTextSourceNode, RichTextSource } from "../source/richtext.js";
25
26
  import { ReifiedPreview, PreviewScope, PreviewItem } from "../preview.js";
26
- export type SerializedSchema = SerializedStringSchema | SerializedLiteralSchema | SerializedBooleanSchema | SerializedNumberSchema | SerializedObjectSchema | SerializedArraySchema | SerializedDiscriminatedUnionSchema | SerializedEnumSchema | SerializedRichTextSchema | SerializedRecordSchema | SerializedKeyOfSchema | SerializedFileSchema | SerializedDateSchema | SerializedDateTimeSchema | SerializedColorSchema | SerializedCodeSchema | SerializedRouteSchema | SerializedSettingsSchema | SerializedImageSchema;
27
+ export type SerializedSchema = SerializedStringSchema | SerializedLiteralSchema | SerializedBooleanSchema | SerializedNumberSchema | SerializedObjectSchema | SerializedArraySchema | SerializedDiscriminatedUnionSchema | SerializedEnumSchema | SerializedRichTextSchema | SerializedRecordSchema | SerializedKeyOfSchema | SerializedFileSchema | SerializedDateSchema | SerializedDateTimeSchema | SerializedColorSchema | SerializedCodeSchema | SerializedRouteSchema | SerializedLocaleSchema | SerializedSettingsSchema | SerializedImageSchema;
27
28
  type Primitives = number | string | boolean | null | FileSource;
28
29
  export type AssertError = {
29
30
  message: string;
@@ -74,6 +75,88 @@ export declare abstract class Schema<Src extends SelectorSource> {
74
75
  * against the real instance. Structural errors publish first; these merge in.
75
76
  */
76
77
  protected abstract executeCustomValidateAt(path: SourcePath, src: Src): ValidationError[];
78
+ /**
79
+ * Whether this node is a locale — the field that MARKS a scope, `s.locale()`.
80
+ *
81
+ * Only `LocaleSchema` overrides this. A base implementation rather than an
82
+ * abstract one on purpose: every other schema class answers no, and a
83
+ * question every class had to answer would be twenty edits for one yes.
84
+ *
85
+ * Not the same question as {@link opensLocaleScope}: a locale field marks the
86
+ * scope, the object AROUND it is the scope. See `localeScope.ts`.
87
+ */
88
+ protected isLocaleField(): boolean;
89
+ /**
90
+ * Whether this node OPENS a locale scope, and what opens it.
91
+ *
92
+ * An object with a `s.locale()` field opens one; so does a record keyed by
93
+ * `s.locale()`. Everything below such a node is in that one language, which
94
+ * is why a scope may not contain another — see `localeScope.ts` for the rule
95
+ * and the reason it is validated rather than typed.
96
+ */
97
+ protected opensLocaleScope(): "field" | "key" | null;
98
+ /**
99
+ * The child schemas a locale scope reaches, by the path segment that reaches
100
+ * them.
101
+ *
102
+ * A structural walk that deliberately does NOT go through `executeSerialize`:
103
+ * serializing a subtree at every node to ask a question about its shape is
104
+ * quadratic, and this is asked during validation. `*` stands for the item of
105
+ * an array or record, which has no name of its own.
106
+ */
107
+ protected localeScopeChildren(): {
108
+ key: string;
109
+ schema: Schema<SelectorSource>;
110
+ }[];
111
+ /**
112
+ * Every locale scope opened strictly BELOW this node, by path.
113
+ *
114
+ * Stops descending at each one it finds, so a scope three deep is reported
115
+ * once, by the scope immediately enclosing it, rather than by every ancestor.
116
+ */
117
+ protected localeScopesBelow(prefix?: string[]): {
118
+ path: string[];
119
+ kind: "field" | "key";
120
+ }[];
121
+ /**
122
+ * The scope rule, as errors at this node — or `false` if it is not broken.
123
+ *
124
+ * Two things are wrong and both are wrong in the SCHEMA rather than in the
125
+ * content, so both are `schemaError`s: an object with two locale fields (the
126
+ * subtree below it would be in two languages at once), and a locale scope
127
+ * inside another (the inner one would silently override the outer for part
128
+ * of a subtree that is supposed to be one language throughout).
129
+ *
130
+ * Validated rather than typed on purpose. Expressing "no scope below this
131
+ * one" as a constraint means threading it through every schema class's type
132
+ * parameter, and the errors that fall out of a recursive constraint like
133
+ * that name the whole tree — an unrelated typo in a `.val.ts` would print
134
+ * pages. See the design notes on the locales PR.
135
+ */
136
+ protected localeScopeErrors(): ValidationError[];
137
+ /**
138
+ * The answer, computed once per schema.
139
+ *
140
+ * Memoised because the question is asked from `executeValidate`, which runs
141
+ * once per SOURCE instance: an array of five hundred blocks asks the item
142
+ * schema five hundred times, and each answer walks the schema below it
143
+ * looking for a nested scope. The answer cannot differ between two instances
144
+ * of the same schema — nothing here reads the source — so the walk is done
145
+ * once and the result handed back after that.
146
+ *
147
+ * The error is still reported at every instance's path. Val's validation
148
+ * errors are keyed by source path and that is where the Studio draws them, so
149
+ * a schema error on a row is a schema error on every row; reporting it on one
150
+ * arbitrary row would put it somewhere nobody was looking.
151
+ */
152
+ private localeScopeErrorsMemo?;
153
+ private computeLocaleScopeErrors;
154
+ /**
155
+ * This node's own `s.locale()` fields, where it is an object that has any.
156
+ *
157
+ * Empty everywhere else, so `localeScopeErrors` can live on the base class.
158
+ */
159
+ protected localeFieldNames(): string[];
77
160
  protected executeCustomValidateFunctions(src: Src, customValidateFunctions: CustomValidateFunction<Src>[], ctx: {
78
161
  path: SourcePath;
79
162
  }): ValidationError[];
@@ -99,8 +182,38 @@ export declare abstract class Schema<Src extends SelectorSource> {
99
182
  * The editor gets a way to clear the field, and the type of the source
100
183
  * widens to include `null` — so consuming code has to handle it.
101
184
  *
102
- * Declare `.validate(...)` AFTER `.nullable()`: on most schema types a
103
- * validator added before it is not carried over.
185
+ * `.validate(...)` may be declared before or after `.nullable()`: the
186
+ * validator is carried over either way. Declared before, its argument is
187
+ * typed as non-null even though `null` can reach it, so guard for it.
188
+ *
189
+ * Implementations MUST carry `customValidateFunctions` over to the new
190
+ * instance. `.nullable()` returns a copy, so dropping them there silently
191
+ * un-declares the user's `.validate(...)` whenever it was written before the
192
+ * `.nullable()` — which is the order most people write it in. Thirteen schema
193
+ * classes passed `[]` here until this was fixed; `nullableCustomValidate.test.ts`
194
+ * pins one instance of every factory on `s` against that, and does not compile
195
+ * until a newly added schema is listed in it.
196
+ *
197
+ * The validators keep running when the value IS null: a nullable schema's
198
+ * validator sees `Src | null` and decides for itself. That is what the
199
+ * classes that never dropped them (string, record, route, file, image) have
200
+ * always done.
201
+ *
202
+ * Carrying them over needs a cast, because `Src` sits in a PARAMETER position
203
+ * of {@link CustomValidateFunction} and so `CustomValidateFunction<Src>[]` is
204
+ * not assignable to `CustomValidateFunction<Src | null>[]`. Widening what the
205
+ * functions can be CALLED with is the intent here rather than something the
206
+ * cast gets away with: `null` is handed to them, and the paragraph above is
207
+ * the behaviour that buys.
208
+ *
209
+ * A validator's parameter type was never a runtime guarantee to begin with.
210
+ * `executeValidate` runs the custom validators BEFORE the structural checks
211
+ * (see {@link CustomValidateFunction}'s callers, e.g. `NumberSchema`, which
212
+ * calls them ahead of its `typeof src !== "number"`), so one can already be
213
+ * called with a value of the wrong type entirely — hand-written content, or
214
+ * a node the Studio's walker reached before its type was checked. That is why
215
+ * {@link executeCustomValidateFunctions} catches what a validator throws and
216
+ * reports it as a `schemaError` instead of letting it escape.
104
217
  *
105
218
  * @example
106
219
  * const schema = s.object({
@@ -25,6 +25,8 @@ export declare class LiteralSchema<Src extends string | null> extends Schema<Src
25
25
  private readonly description?;
26
26
  private readonly renderInput;
27
27
  private readonly previewInput;
28
+ /** Type-only marker: as a record key, this declares the key set. See `LocaleSchema`. */
29
+ readonly __declaresRecordKeys: true;
28
30
  constructor(value: string, opt?: boolean, customValidateFunctions?: CustomValidateFunction<Src>[], isReadonly?: boolean, isHidden?: boolean, description?: string | undefined, renderInput?: FieldRender | null, previewInput?: ItemPreviewInput<Src> | null);
29
31
  /**
30
32
  * Describe this field.
@@ -0,0 +1,132 @@
1
+ import { Schema, SchemaAssertResult, SerializedSchema } from "./index.js";
2
+ import { ItemPreviewInput, PreviewItem, ReifiedPreview } from "../preview.js";
3
+ import { FieldRender } from "../render.js";
4
+ import { SourcePath } from "../val/index.js";
5
+ import { ValidationError, ValidationErrors } from "./validation/ValidationError.js";
6
+ export type SerializedLocaleSchema = {
7
+ type: "locale";
8
+ /** Static layout config, carried whole in the serialized schema — see `render.ts`. */
9
+ render?: FieldRender;
10
+ /** Set when this schema declares a `preview`. The closure itself cannot serialize. */
11
+ preview?: true;
12
+ opt: boolean;
13
+ customValidate?: boolean;
14
+ readonly?: boolean;
15
+ hidden?: boolean;
16
+ description?: string;
17
+ };
18
+ /**
19
+ * One of the languages the project publishes.
20
+ *
21
+ * The set is NOT here. It is declared in the settings module — `locales.available`
22
+ * — so that adding a language is something the people who write the content can
23
+ * do, and so that one list drives validation, the Studio's picker and the
24
+ * translation notes. This schema knows only that the value is one of them, which
25
+ * is checked cross-module through the `locale:check-locale` fix, exactly as
26
+ * `keyOf` and `route` check theirs.
27
+ *
28
+ * Never stega encoded (see `stegaEncode.ts`): a locale goes into `<html lang>`,
29
+ * into `hreflang` and into `Intl` constructors, none of which survive invisible
30
+ * characters.
31
+ */
32
+ export declare class LocaleSchema<Src extends string | null> extends Schema<Src> {
33
+ private readonly opt;
34
+ private readonly customValidateFunctions;
35
+ private readonly isReadonly;
36
+ private readonly isHidden;
37
+ private readonly description?;
38
+ private readonly renderInput;
39
+ private readonly previewInput;
40
+ /**
41
+ * Type-only marker: as a record key, this schema DECLARES the key set.
42
+ *
43
+ * `declare` so it exists in the type and nowhere at runtime — reading it off
44
+ * an instance gives `undefined`, and nothing should. It is how `record()`
45
+ * knows to type entries as `Item | null`: a language nobody has translated
46
+ * into is a null entry, not an absent key. See `DeclaredKeySet` and
47
+ * `RecordSrcOf`.
48
+ */
49
+ readonly __declaresRecordKeys: true;
50
+ constructor(opt?: boolean, customValidateFunctions?: ((src: Src) => false | string)[], isReadonly?: boolean, isHidden?: boolean, description?: string | undefined, renderInput?: FieldRender | null, previewInput?: ItemPreviewInput<Src> | null);
51
+ /**
52
+ * A note to whoever edits this field, shown under it in the Studio.
53
+ *
54
+ * Pass `null` to clear a description set earlier.
55
+ *
56
+ * @example
57
+ * const schema = s.object({
58
+ * locale: s.locale().describe("The language everything below is written in"),
59
+ * title: s.string(),
60
+ * });
61
+ * export default c.define("/example.val.ts", schema, {
62
+ * locale: "nb-NO",
63
+ * title: "Vinterjakke",
64
+ * });
65
+ */
66
+ describe(description: string | null): LocaleSchema<Src>;
67
+ /**
68
+ * A check of your own, on top of "is this one of the project's languages".
69
+ *
70
+ * Return a message when the value is WRONG and `false` when it is fine — the
71
+ * two answers are a complaint and no complaint, so write the check as a
72
+ * ternary rather than as `ok || "message"`, which returns `true` and `true`
73
+ * is neither.
74
+ *
75
+ * @example
76
+ * const schema = s.object({
77
+ * locale: s.locale().validate((val) =>
78
+ * val === "en-US" ? "This section is not translated into English yet" : false,
79
+ * ),
80
+ * title: s.string(),
81
+ * });
82
+ * export default c.define("/example.val.ts", schema, {
83
+ * locale: "nb-NO",
84
+ * title: "Vinterjakke",
85
+ * });
86
+ */
87
+ validate(validationFunction: (src: Src) => false | string): LocaleSchema<Src>;
88
+ protected executeValidate(path: SourcePath, src: Src): ValidationErrors;
89
+ protected executeAssert(path: SourcePath, src: unknown): SchemaAssertResult<Src>;
90
+ nullable(): LocaleSchema<Src | null>;
91
+ readonly(): LocaleSchema<Src>;
92
+ hidden(): LocaleSchema<Src>;
93
+ protected isLocaleField(): boolean;
94
+ protected executeCustomValidateAt(path: SourcePath, src: Src): ValidationError[];
95
+ /**
96
+ * How this field is laid out where it is the item of an array or a record.
97
+ * Static configuration, not a callback — see `render.ts`.
98
+ *
99
+ * @example
100
+ * const schema = s.array(s.locale().render({ as: "inline" }));
101
+ * export default c.define("/example.val.ts", schema, ["nb-NO", "en-US"]);
102
+ */
103
+ render(input: FieldRender): LocaleSchema<Src>;
104
+ /**
105
+ * How this VALUE is shown where a preview of it is needed. Never how the field
106
+ * itself is edited (that is `render`). See `preview.ts`.
107
+ *
108
+ * @example
109
+ * const schema = s.array(
110
+ * s.locale().preview(({ val }) => ({ title: val })),
111
+ * );
112
+ * export default c.define("/example.val.ts", schema, ["nb-NO", "en-US"]);
113
+ */
114
+ preview(select: ItemPreviewInput<Src>): LocaleSchema<Src>;
115
+ protected executePreviewItem(src: NonNullable<Src>): PreviewItem | null;
116
+ protected declaresItemPreview(): boolean;
117
+ protected executeSerialize(): SerializedSchema;
118
+ protected executePreview(): ReifiedPreview;
119
+ }
120
+ /**
121
+ * Define one of the project's languages.
122
+ *
123
+ * The languages themselves are declared in the settings module, under
124
+ * `locales.available` — this says only that a value is one of them.
125
+ *
126
+ * @example // a field: everything under this object is in this language
127
+ * const schema = s.object({ locale: s.locale(), title: s.string() });
128
+ *
129
+ * @example // a key: one entry per language
130
+ * const schema = s.record(s.locale(), s.object({ title: s.string() }));
131
+ */
132
+ export declare const locale: () => LocaleSchema<string>;
@@ -102,6 +102,13 @@ export declare class ObjectSchema<Props extends ObjectSchemaProps, Src extends O
102
102
  nullable(): ObjectSchema<Props, Src | null>;
103
103
  readonly(isReadonly?: boolean): ObjectSchema<Props, Src>;
104
104
  hidden(isHidden?: boolean): ObjectSchema<Props, Src>;
105
+ /** The names of this object's `s.locale()` fields, in declaration order. */
106
+ protected localeFieldNames(): string[];
107
+ protected opensLocaleScope(): "field" | "key" | null;
108
+ protected localeScopeChildren(): {
109
+ key: string;
110
+ schema: Schema<SelectorSource>;
111
+ }[];
105
112
  protected executeCustomValidateAt(path: SourcePath, src: Src): ValidationError[];
106
113
  /**
107
114
  * How this field is laid out in the editor when it is the item of an array