@valbuild/shared 0.126.0 → 0.127.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,131 @@
1
1
  # @valbuild/shared
2
2
 
3
+ ## 0.127.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#608](https://github.com/valbuild/val/pull/608) [`7fa8699`](https://github.com/valbuild/val/commit/7fa869974bc895af992a7d5c18b76253636d7d65) Thanks [@freekh](https://github.com/freekh)! - A record whose schema declares its keys now holds every one of them.
8
+
9
+ Two key schemas enumerate their keys: `s.locale()`, whose set is the project's
10
+ `locales.available`, and a union of literals. For those, the keys are part of the
11
+ schema, so a missing one is a hole in the content rather than content nobody has
12
+ written yet — and validation now says so, naming what is missing.
13
+
14
+ ```typescript
15
+ s.record(s.locale(), s.object({ title: s.string() }));
16
+ // Missing key: 'nb-NO'. This record's keys are declared by its schema, so
17
+ // every one of them is an entry — an entry nobody has written yet is null,
18
+ // not absent.
19
+ ```
20
+
21
+ **An entry nobody has written yet is `null`.** Not an absent key: a null entry is
22
+ data you can count, filter and see in a diff, and it means half-translated
23
+ content stays _valid_ rather than blocking a publish. The value type of such a
24
+ record widens by `null` to match, so writing one in a `.val.ts` type-checks:
25
+
26
+ ```typescript
27
+ c.define(
28
+ "/content/jacket.val.ts",
29
+ s.record(s.locale(), s.object({ title: s.string() })),
30
+ {
31
+ "en-US": { title: "Winter jacket" },
32
+ "nb-NO": null, // nobody has translated this yet
33
+ },
34
+ );
35
+ ```
36
+
37
+ **This changes `s.record(s.union(...), item)`**, and closes a gap that was
38
+ already there: `s.record(s.union(s.literal("a"), s.literal("b")), item)` types as
39
+ `Record<"a" | "b", T>`, so TypeScript demanded both keys while the validator only
40
+ checked the ones present. It now checks them too, and — as above — accepts `null`
41
+ for an entry that has not been filled in. If you have such a record with keys
42
+ missing, validation will report them; adding the keys with `null` values is the
43
+ fix, and creating one from the Studio does it for you.
44
+
45
+ `emptyOf` creates these records with every key already in them rather than
46
+ empty, since an empty one is already missing keys. In the Studio use the
47
+ `useEmptyOf()` hook rather than importing `emptyOf` directly: a locale record's
48
+ keys are in the settings module, and the hook is what has read it.
49
+
50
+ - [#608](https://github.com/valbuild/val/pull/608) [`5b7fe05`](https://github.com/valbuild/val/commit/5b7fe05cec6365f9cd1de9ba31e65ed4a87edb63) Thanks [@freekh](https://github.com/freekh)! - `s.locale()`: one of the project's languages.
51
+
52
+ The languages themselves are declared in the settings module (`locales.available`);
53
+ this says that a value is one of them.
54
+
55
+ ```typescript
56
+ // a field: everything in this entry is in this language
57
+ s.record(s.string(), s.object({ locale: s.locale(), title: s.string() }));
58
+
59
+ // a key: one entry per language
60
+ s.record(s.locale(), s.object({ title: s.string() }));
61
+ ```
62
+
63
+ Every locale in content is checked against the project's list, the way `keyOf`
64
+ and `route` are checked against what they point at. An undeclared language names
65
+ the ones the project has; a project that has declared none is told to declare
66
+ them rather than told the value is wrong.
67
+
68
+ A locale is stored as the tag itself — the value in content is `nb-NO`, and a
69
+ record keyed by `s.locale()` has `nb-NO` as its key. Spelling one differently
70
+ where it is stored (`/no/…` as a URL segment) is a real need and is deliberately
71
+ not in this release: it changes what is accepted as well as what is shown, so it
72
+ is being designed on its own rather than folded in here.
73
+
74
+ A locale is **never stega encoded**: it ends up in `<html lang>`, in `hreflang`
75
+ and in `Intl` constructors, none of which survive invisible characters.
76
+
77
+ `assistant.translation` joins the settings module alongside `context` and `tone`
78
+ — a note per language, keyed by language, so only the target language's rules are
79
+ sent when translating into it.
80
+
81
+ - [#608](https://github.com/valbuild/val/pull/608) [`88262ac`](https://github.com/valbuild/val/commit/88262ac8db068650a664981ef73556457d87741a) Thanks [@freekh](https://github.com/freekh)! - Locale scopes: a subtree in one language, and one function that answers which.
82
+
83
+ A **locale scope** is content governed by a single language. Two things open one
84
+ today (a third, locale segments in routes, follows):
85
+
86
+ ```typescript
87
+ // a locale field: this object and everything below it is in one language
88
+ s.object({ locale: s.locale(), title: s.string(), body: s.richtext() });
89
+
90
+ // a locale-keyed record: each entry is in the language its key names
91
+ s.record(s.locale(), s.object({ title: s.string() }));
92
+ ```
93
+
94
+ **A scope may not contain another scope**, and an object may have only one
95
+ locale field. Both are reported as schema errors, naming what to move:
96
+
97
+ ```
98
+ An object can be in one language, so it can have one locale field.
99
+ Found 'locale', 'language'.
100
+
101
+ Everything here is already in one language, so 'byLanguage' cannot set
102
+ another. Move the locale-keyed record out of this object, or take the outer
103
+ one away.
104
+ ```
105
+
106
+ A scope three levels deep is reported once, by the scope immediately enclosing
107
+ it, rather than by every ancestor.
108
+
109
+ The rule is **validated rather than typed**. Expressing "no scope below this
110
+ one" as a type constraint means threading it through every schema class's type
111
+ parameter, and the errors a recursive constraint like that produces name the
112
+ whole tree — an unrelated typo in a `.val.ts` would print pages.
113
+
114
+ `localeAt(path, snapshot)` (from `@valbuild/shared/internal`) answers which
115
+ language governs a path, and is the one implementation of that question, so the
116
+ Studio, the server and the validation worker cannot disagree. It returns the
117
+ tag, which is what `<html lang>`, `Intl` and `locales.available` all want.
118
+
119
+ It answers `null` where no scope governs the path, where the project has
120
+ declared no languages, and where a locale field holds something that is not one
121
+ of them — validation is already reporting the last, and guessing would put a
122
+ language in `<html lang>` that nobody chose.
123
+
124
+ ### Patch Changes
125
+
126
+ - Updated dependencies [[`7fa8699`](https://github.com/valbuild/val/commit/7fa869974bc895af992a7d5c18b76253636d7d65), [`5b7fe05`](https://github.com/valbuild/val/commit/5b7fe05cec6365f9cd1de9ba31e65ed4a87edb63), [`88262ac`](https://github.com/valbuild/val/commit/88262ac8db068650a664981ef73556457d87741a), [`7072e07`](https://github.com/valbuild/val/commit/7072e07623c953a09ac14388ae22dada0b431ce3), [`29811c3`](https://github.com/valbuild/val/commit/29811c3f8c7e001a950e6f4833af6888e6a4efea)]:
127
+ - @valbuild/core@0.127.0
128
+
3
129
  ## 0.126.0
4
130
 
5
131
  ### Minor Changes
@@ -948,7 +948,7 @@ export declare const Api: {
948
948
  message: z.ZodString;
949
949
  value: z.ZodOptional<z.ZodUnknown>;
950
950
  fatal: z.ZodOptional<z.ZodBoolean>;
951
- fixes: z.ZodOptional<z.ZodArray<z.ZodType<"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" | "images:check-unique-folder" | "files:check-unique-folder" | "images:check-all-files" | "files:check-all-files" | "jsonValues:extract-entry" | "external:upload", unknown, z.core.$ZodTypeInternals<"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" | "images:check-unique-folder" | "files:check-unique-folder" | "images:check-all-files" | "files:check-all-files" | "jsonValues:extract-entry" | "external:upload", unknown>>>>;
951
+ fixes: z.ZodOptional<z.ZodArray<z.ZodType<"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", unknown, z.core.$ZodTypeInternals<"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", unknown>>>>;
952
952
  }, z.core.$strip>>>>;
953
953
  }, z.core.$strip>>;
954
954
  }, z.core.$strip>;
@@ -1,4 +1,5 @@
1
1
  import type { SerializedSchema, Source } from "@valbuild/core";
2
+ import { type EmptyOfContext } from "../emptyOf.js";
2
3
  import { type BuildResult } from "./aiImageToolPatches.js";
3
4
  export declare function buildDuplicatePatch(args: {
4
5
  sourcePath: string[];
@@ -6,7 +7,7 @@ export declare function buildDuplicatePatch(args: {
6
7
  }, moduleSchema: SerializedSchema, moduleSource: Source | undefined): BuildResult;
7
8
  export declare function buildEmptyAtPathPatch(args: {
8
9
  destinationPath: string[];
9
- }, moduleSchema: SerializedSchema, moduleSource: Source | undefined): BuildResult;
10
+ }, moduleSchema: SerializedSchema, moduleSource: Source | undefined, context?: EmptyOfContext): BuildResult;
10
11
  /**
11
12
  * What kind of container a path points at.
12
13
  *
@@ -1,2 +1,25 @@
1
1
  import { SerializedSchema, Json } from "@valbuild/core";
2
- export declare function emptyOf(schema: SerializedSchema): Json;
2
+ /**
3
+ * What `emptyOf` cannot read off a serialized schema.
4
+ *
5
+ * The project's languages, and which one is being worked in. Neither is in the
6
+ * schema: a locale-keyed record has one entry per language and the languages
7
+ * are declared in the settings module, and "which language am I writing" is a
8
+ * fact about the editor, not about the content. Optional throughout: a caller
9
+ * that has no context gets an empty record and an unset locale, which
10
+ * validation then reports, rather than wrong ones.
11
+ */
12
+ export type EmptyOfContext = {
13
+ /** `locales.available` from the settings module. */
14
+ locales?: string[];
15
+ /**
16
+ * The one language being worked in, where the editor has narrowed to one.
17
+ *
18
+ * A new `s.locale()` field is created already set to it. Someone filtered to
19
+ * Norwegian is writing Norwegian, and a new item that defaulted to unset
20
+ * would fail validation and vanish from the list they are looking at, in
21
+ * that order.
22
+ */
23
+ selectedLocale?: string;
24
+ };
25
+ export declare function emptyOf(schema: SerializedSchema, context?: EmptyOfContext): Json;
@@ -16,6 +16,7 @@ export * from "./parseRoutePattern.js";
16
16
  export * from "./getNextAppRouterSourceFolder.js";
17
17
  export * from "./routeValidation.js";
18
18
  export * from "./resolveSchemaSourceFixes.js";
19
+ export * from "./localeAt.js";
19
20
  export * from "./getErrorMessageFromUnknownJson.js";
20
21
  export * from "./zod/SerializedSchema.js";
21
22
  export * from "./valCanvasProtocol.js";
@@ -0,0 +1,19 @@
1
+ import { type SourcePath } from "@valbuild/core";
2
+ import { type SchemaSourceSnapshot } from "./resolveSchemaSourceFixes.js";
3
+ /**
4
+ * Which of the project's languages governs a path, or `null` for none.
5
+ *
6
+ * THE question the whole feature turns on. A locale scope is a subtree in one
7
+ * language, and three things open one: a `locale` field on an object, a record
8
+ * keyed by `s.locale()`, and a locale segment in a segmented key. Once this can
9
+ * be answered from the schema and the source alone, everything downstream falls
10
+ * out of it — the Studio's locale filter, deep links, `<html lang>`, and knowing
11
+ * what to translate from and into.
12
+ *
13
+ * One implementation, so the Studio, the server and the validation worker cannot
14
+ * disagree about what language a piece of content is in.
15
+ *
16
+ * `null` means no scope governs the path — which is the answer for most content
17
+ * in most projects, and for every project that has not declared any languages.
18
+ */
19
+ export declare function localeAt(path: SourcePath, snapshot: SchemaSourceSnapshot): string | null;
@@ -1,4 +1,4 @@
1
- import { type Json, type ModuleFilePath, type SerializedSchema, type SourcePath, type ValidationError } from "@valbuild/core";
1
+ import { type Json, type ModuleFilePath, type SerializedSchema, type SourcePath, type ValidationError, type ValidationFix } from "@valbuild/core";
2
2
  export type SchemaSourceSnapshot = {
3
3
  schemas: Record<ModuleFilePath, SerializedSchema>;
4
4
  sources: Record<ModuleFilePath, Json>;
@@ -28,12 +28,36 @@ export declare function getKeyOfRecordAt(sourcePath: SourcePath, snapshot: Schem
28
28
  source: unknown;
29
29
  schema: SerializedSchema | undefined;
30
30
  };
31
+ /**
32
+ * The languages the project declares, from its settings module.
33
+ *
34
+ * Empty where there is no settings module, no `locales` section, or nothing in
35
+ * it — all of which mean the same thing: this project has not said it is
36
+ * translated.
37
+ */
38
+ export declare function declaredLocales(snapshot: SchemaSourceSnapshot): string[];
31
39
  export type ResolvedFix = {
32
40
  status: "resolved";
33
41
  } | {
34
42
  status: "remaining";
35
43
  error: ValidationError;
36
44
  };
45
+ /**
46
+ * The fixes {@link resolveSchemaSourceFixForError} answers for.
47
+ *
48
+ * A marker error carrying one of these is NOT a message: it says "ask the whole
49
+ * project about this" and carries the value to ask with, because the schema
50
+ * that raised it can only see its own module. Left unresolved, what a user sees
51
+ * is the marker's own placeholder text — "should typically be processed by Val
52
+ * internally … version mismatch" — which is alarming and says nothing.
53
+ *
54
+ * A list rather than a check per call site: every caller that resolves some of
55
+ * these has to resolve all of them, and the two that leaked did so by adding a
56
+ * fix here and not to a condition somewhere else.
57
+ */
58
+ export declare const SCHEMA_SOURCE_FIXES: ValidationFix[];
59
+ /** Whether an error is a marker only the whole project can answer. */
60
+ export declare function isSchemaSourceFixError(error: ValidationError): boolean;
37
61
  /**
38
62
  * The record a `keyof:check-keys` error resolves against, or null for any other
39
63
  * error — including a malformed marker, which
@@ -46,20 +70,20 @@ export type ResolvedFix = {
46
70
  */
47
71
  export declare function keyOfRecordPath(error: ValidationError): SourcePath | null;
48
72
  /**
49
- * Resolves a single `keyof:check-keys` or `router:check-route` error against
50
- * the given schema/source snapshot. Returns:
73
+ * Resolves a single marker error (see {@link SCHEMA_SOURCE_FIXES}) against the
74
+ * given schema/source snapshot. Returns:
51
75
  * - null if the error is not one of these fixes (caller should pass through)
52
- * - { status: "resolved" } if the referenced key/route is valid (drop error)
76
+ * - { status: "resolved" } if the reference is valid (drop error)
53
77
  * - { status: "remaining"; error } if invalid (rewritten error to surface)
54
78
  */
55
79
  export declare function resolveSchemaSourceFixForError(error: ValidationError, snapshot: SchemaSourceSnapshot): ResolvedFix | null;
56
80
  /**
57
81
  * Apply schema/source fix resolution across a map of validation errors.
58
82
  *
59
- * Errors with `keyof:check-keys` or `router:check-route` are resolved against
60
- * the in-memory schema/source snapshot — valid references drop the error,
61
- * invalid ones are rewritten with a "did you mean…" message and have their
62
- * `fixes` cleared (so downstream code reports them as plain validation errors).
83
+ * Errors carrying one of {@link SCHEMA_SOURCE_FIXES} are resolved against the
84
+ * in-memory schema/source snapshot — valid references drop the error, invalid
85
+ * ones are rewritten with a "did you mean…" message and have their `fixes`
86
+ * cleared (so downstream code reports them as plain validation errors).
63
87
  * All other errors pass through untouched.
64
88
  *
65
89
  * Source paths with no remaining errors are removed from the result.
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { type SerializedSchema as SerializedSchemaT, type SerializedStringSchema as SerializedStringSchemaT, type SerializedLiteralSchema as SerializedLiteralSchemaT, type SerializedBooleanSchema as SerializedBooleanSchemaT, type SerializedNumberSchema as SerializedNumberSchemaT, type SerializedObjectSchema as SerializedObjectSchemaT, type SerializedArraySchema as SerializedArraySchemaT, type SerializedDiscriminatedUnionSchema as SerializedDiscriminatedUnionSchemaT, type SerializedEnumSchema as SerializedEnumSchemaT, type SerializedRichTextSchema as SerializedRichTextSchemaT, type SerializedRichTextOptions as SerializedRichTextOptionsT, type SerializedRecordSchema as SerializedRecordSchemaT, type SerializedKeyOfSchema as SerializedKeyOfSchemaT, type SerializedRouteSchema as SerializedRouteSchemaT, type SerializedFileSchema as SerializedFileSchemaT, type SerializedDateSchema as SerializedDateSchemaT, type SerializedDateTimeSchema as SerializedDateTimeSchemaT, type SerializedColorSchema as SerializedColorSchemaT, type SerializedCodeSchema as SerializedCodeSchemaT, type SerializedImageSchema as SerializedImageSchemaT, type SerializedSettingsSchema as SerializedSettingsSchemaT } from "@valbuild/core";
2
+ import { type SerializedSchema as SerializedSchemaT, type SerializedStringSchema as SerializedStringSchemaT, type SerializedLiteralSchema as SerializedLiteralSchemaT, type SerializedBooleanSchema as SerializedBooleanSchemaT, type SerializedNumberSchema as SerializedNumberSchemaT, type SerializedObjectSchema as SerializedObjectSchemaT, type SerializedArraySchema as SerializedArraySchemaT, type SerializedDiscriminatedUnionSchema as SerializedDiscriminatedUnionSchemaT, type SerializedEnumSchema as SerializedEnumSchemaT, type SerializedRichTextSchema as SerializedRichTextSchemaT, type SerializedRichTextOptions as SerializedRichTextOptionsT, type SerializedRecordSchema as SerializedRecordSchemaT, type SerializedKeyOfSchema as SerializedKeyOfSchemaT, type SerializedLocaleSchema as SerializedLocaleSchemaT, type SerializedRouteSchema as SerializedRouteSchemaT, type SerializedFileSchema as SerializedFileSchemaT, type SerializedDateSchema as SerializedDateSchemaT, type SerializedDateTimeSchema as SerializedDateTimeSchemaT, type SerializedColorSchema as SerializedColorSchemaT, type SerializedCodeSchema as SerializedCodeSchemaT, type SerializedImageSchema as SerializedImageSchemaT, type SerializedSettingsSchema as SerializedSettingsSchemaT } from "@valbuild/core";
3
3
  export declare const SerializedStringSchema: z.ZodType<SerializedStringSchemaT>;
4
4
  export declare const SerializedLiteralSchema: z.ZodType<SerializedLiteralSchemaT>;
5
5
  export declare const SerializedBooleanSchema: z.ZodType<SerializedBooleanSchemaT>;
@@ -73,6 +73,7 @@ export declare const CodeOptions: z.ZodObject<{
73
73
  }>>;
74
74
  }, z.core.$strip>;
75
75
  export declare const SerializedCodeSchema: z.ZodType<SerializedCodeSchemaT>;
76
+ export declare const SerializedLocaleSchema: z.ZodType<SerializedLocaleSchemaT>;
76
77
  export declare const SerializedRouteSchema: z.ZodType<SerializedRouteSchemaT>;
77
78
  export declare const SerializedSettingsSchema: z.ZodType<SerializedSettingsSchemaT>;
78
79
  export declare const SerializedSchema: z.ZodType<SerializedSchemaT>;