@valbuild/core 0.146.2 → 0.147.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,32 @@
1
1
  # @valbuild/core
2
2
 
3
+ ## 0.147.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#816](https://github.com/valbuild/val/pull/816) [`2f09cd5`](https://github.com/valbuild/val/commit/2f09cd5e2f4fc37f06917899f24f9ad2ee8d3a8b) Thanks [@freekh](https://github.com/freekh)! - Routers can give each route parameter a schema, and a URL can say which language a page is in.
8
+
9
+ - **`s.router(router, params, item)`** takes a schema per route parameter, by the name the route file gives it (`$slug`, `[slug]`, `{-$locale}`). Each key's parameters are validated against them, and a name the route does not have is a schema error.
10
+
11
+ ```ts
12
+ s.router(tanstackRouter, { locale: urlLocale, slug: s.string() }, item);
13
+ ```
14
+
15
+ - **`s.enum(...).locales({ ... })`** says which of the project's languages each value means, so `/nb/…` can mean `nb-NO`. The tags are checked against `locales.available` in the settings module, like `s.locale()`. On a nullable enum, a second argument names the language of a URL that leaves the segment out, which only a router parameter may use:
16
+
17
+ ```ts
18
+ const urlLocale = s
19
+ .enum("nb")
20
+ .nullable()
21
+ .locales({ nb: "nb-NO" }, { null: "en-US" });
22
+ ```
23
+
24
+ A router with a locale parameter makes each page a locale scope, so the Studio and validation know which language every page is in.
25
+
26
+ - **A route's language segment can no longer be forgotten.** In a project that declares `locales.available`, a page route with a `locale` or `lang` segment (`{-$locale}`, `[lang]`) whose module gives that parameter no schema is reported, saying what to add. Any schema answers it, so a `$lang` that is not a language is silenced with its own, e.g. `{ lang: s.enum("typescript", "python") }`. It is an ordinary validation error: the Studio still opens and `val validate` lists it, but a project that already has such a route will see it after upgrading, and it blocks publishing until the parameter has a schema.
27
+ - **The Studio's new-page form offers a language as a list.** A route parameter whose schema is a locale is picked from the project's languages, each named in its own language, with "no prefix" where the URL may leave the segment out. Other enum parameters get a list of their values, and every parameter's description is shown under it. The URL row previews what the choice puts there.
28
+ - **TanStack optional segments (`{-$param}`)** are now understood: route validation, `useValRoute` and the Studio's new-page form accept keys with and without the segment.
29
+
3
30
  ## 0.146.2
4
31
 
5
32
  ### Patch Changes
@@ -22,3 +22,12 @@ export declare function getSourcePathFromRoute(pathname: string, schemas: Record
22
22
  * which is which.
23
23
  */
24
24
  export declare function isPageRouter(routerId: string): boolean;
25
+ /**
26
+ * The route pattern a page router's module serves, or `null` for a router
27
+ * whose keys are not routes of this site.
28
+ *
29
+ * By router id, for the callers that hold a SERIALIZED schema — which carries
30
+ * the id and not the router. A schema instance asks its router directly
31
+ * (`ValRouter.getRoutePattern`), and both end at the same parser.
32
+ */
33
+ export declare function routePatternOf(routerId: string, moduleFilePath: ModuleFilePath): string[] | null;
@@ -93,8 +93,8 @@ export { deserializeSchema } from "./schema/deserialize.js";
93
93
  export { type PreviewItem, type ItemPreviewInput, type RecordPreview, type ArrayPreview, type ReifiedPreview, type PreviewScope, previewScope, } from "./preview.js";
94
94
  export { type InlineRender, type FieldRender, type SliderRender, type NumberRender, isInlineRender, } from "./render.js";
95
95
  export type { ValRouter, ExternalPageRouter, RouteValidationError, } from "./router.js";
96
- export { parseNextJsRoutePattern, parseTanStackRoutePattern, tanStackSegmentsOfRoutePath, validateUrlAgainstPattern, } from "./router.js";
97
- export { getSourcePathFromRoute, isPageRouter } from "./getSourcePathFromRoute.js";
96
+ export { parseNextJsRoutePattern, parseTanStackRoutePattern, tanStackSegmentsOfRoutePath, validateUrlAgainstPattern, matchRoutePattern, } from "./router.js";
97
+ export { getSourcePathFromRoute, isPageRouter, routePatternOf, } from "./getSourcePathFromRoute.js";
98
98
  /**
99
99
  * Which URL schemes an external page key may carry.
100
100
  *
@@ -113,11 +113,12 @@ export declare const DEFAULT_APP_HOST = "https://admin.val.build";
113
113
  * Where Val's own immutable build artifacts are served from: the content
114
114
  * service's `/v1/static`.
115
115
  *
116
- * Only one kind of thing lives here so far: the WebAssembly binary of the
117
- * bundler the Studio builds a site with. It cannot travel with the Studio's
118
- * own bundle -- 10.9 MB of binary, and `/api/val/static` is a base64 record of
119
- * TEXT inside the server bundle, which both corrupts it and would carry it
120
- * into every project that installs `@valbuild/ui`.
116
+ * Only one kind of thing lives here so far: the WebAssembly the Studio loads
117
+ * -- the bundler it builds a site with, and the WebP encoder it uses where the
118
+ * canvas cannot write WebP. None of it can travel with the Studio's own bundle:
119
+ * `/api/val/static` is a base64 record of TEXT inside the server bundle, which
120
+ * both corrupts a binary and would carry it (10.9 MB of bundler) into every
121
+ * project that installs `@valbuild/ui`.
121
122
  *
122
123
  * Everything under this path is addressed by the SHA-256 of its own bytes, so
123
124
  * an upload is idempotent, a URL is immutable, and the artifact a build asks
@@ -129,11 +130,11 @@ export declare const DEFAULT_APP_HOST = "https://admin.val.build";
129
130
  * availability and one more domain to every customer's egress allowlist.
130
131
  *
131
132
  * A FIXED LITERAL, and deliberately not derived from a project's configured
132
- * content host (`VAL_CONTENT_URL`). Deriving it would make serving this binary
133
+ * content host (`VAL_CONTENT_URL`). Deriving it would make serving these binaries
133
134
  * an obligation of every content service anybody points Val at, self-hosted
134
- * ones included. A deployment that must fetch it from somewhere else sets
135
- * `globalThis.__VAL_ROLLDOWN_WASM_URL__` instead (see
136
- * `packages/ui/build/rolldownWasm.ts`).
135
+ * ones included. A deployment that must fetch them from somewhere else sets
136
+ * one global per binary instead -- `globalThis.__VAL_ROLLDOWN_WASM_URL__` and
137
+ * the others listed in `packages/ui/build/staticWasm.ts`.
137
138
  */
138
139
  export declare const DEFAULT_STATIC_HOST = "https://content.val.build/v1/static";
139
140
  declare const Internal: {
@@ -393,6 +393,25 @@ export type InitSchema = {
393
393
  * });
394
394
  * ```
395
395
  *
396
+ * @example Route parameters, by the names the route file gives them
397
+ * ```typescript
398
+ * import { s, c, tanstackRouter } from "../val.config";
399
+ * // `/about` is English and `/nb/about` Norwegian: `{-$locale}` is an
400
+ * // optional segment, and `{ null }` says what a URL without it is in.
401
+ * const locale = s
402
+ * .enum("nb")
403
+ * .nullable()
404
+ * .locales({ nb: "nb-NO" }, { null: "en-US" });
405
+ * export default c.define(
406
+ * "/src/routes/{-$locale}.$slug.val.ts",
407
+ * s.router(tanstackRouter, { locale, slug: s.string() }, s.object({ title: s.string() })),
408
+ * {
409
+ * "/about": { title: "About us" },
410
+ * "/nb/about": { title: "Om oss" },
411
+ * },
412
+ * );
413
+ * ```
414
+ *
396
415
  * @param router - The router configuration (e.g., nextAppRouter)
397
416
  * @param schema - The schema for each route item
398
417
  * @returns A RecordSchema configured as a router
@@ -33,6 +33,37 @@ export type RouteValidationError = {
33
33
  expectedPath: string | null;
34
34
  };
35
35
  };
36
+ /**
37
+ * The value of each parameter a URL gives a route pattern, or `null` when the
38
+ * URL is not one of that pattern's.
39
+ *
40
+ * A parameter that an OPTIONAL segment left out is `null` rather than absent,
41
+ * so a caller can tell "this route has no such parameter" from "this URL did
42
+ * not use it". A catch-all's segments are joined with `/`, which is how they
43
+ * appear in the key; an optional catch-all with nothing to catch is `null`.
44
+ *
45
+ * The pattern vocabulary is the one both parsers produce: `[name]` is a
46
+ * segment, `[[name]]` an optional one, `[...name]` a catch-all and
47
+ * `[[...name]]` an optional catch-all; anything else is literal.
48
+ *
49
+ * Optional segments are what make this a search rather than a walk: in
50
+ * `/[[locale]]/blog/[slug]` the URL `/blog/hello` only matches once `blog`
51
+ * has been tried as the locale and given back. A present segment is tried
52
+ * first, so `/nb/blog/hello` reads as the locale `nb` — the reading the router
53
+ * itself makes.
54
+ */
55
+ export declare function matchRoutePattern(urlPath: string, routePattern: string[]): Record<string, string | null> | null;
56
+ /**
57
+ * The parameters a route pattern has, by name, and whether a URL may leave
58
+ * each one out.
59
+ */
60
+ export declare function routeParamsOfPattern(routePattern: string[]): {
61
+ name: string;
62
+ optional: boolean;
63
+ }[];
64
+ /**
65
+ * Whether a URL is one of a route pattern's, and the pattern to show when not.
66
+ */
36
67
  export declare function validateUrlAgainstPattern(urlPath: string, routePattern: string[]): {
37
68
  isValid: boolean;
38
69
  expectedPath?: string;
@@ -61,6 +92,7 @@ export declare function parseNextJsRoutePattern(moduleFilePath: string): string[
61
92
  * - `.` and `/` both separate segments (`posts.$postId.val.ts` and
62
93
  * `posts/$postId.val.ts` are the same route)
63
94
  * - `$param` is a dynamic segment, `$` on its own is a splat
95
+ * - `{-$param}` is an optional segment, written `[[param]]`
64
96
  * - `index` is the directory's own route and contributes no segment
65
97
  * - `route` is a layout for the directory, likewise
66
98
  * - `(group)` folders and `_pathless` layout segments are not in the URL
@@ -75,6 +107,7 @@ export declare function parseNextJsRoutePattern(moduleFilePath: string): string[
75
107
  * - /routes/posts/$postId.val.ts -> ["posts", "[postId]"]
76
108
  * - /src/routes/index.val.ts -> []
77
109
  * - /src/routes/files.$.val.ts -> ["files", "[..._splat]"]
110
+ * - /src/routes/{-$locale}.about.val.ts -> ["[[locale]]", "about"]
78
111
  * - /src/routes/(app)/_layout.about.val.ts -> ["about"]
79
112
  */
80
113
  export declare function parseTanStackRoutePattern(moduleFilePath: string): string[];
@@ -105,5 +138,14 @@ export interface ValRouter {
105
138
  * than URLs, which is every other one.
106
139
  */
107
140
  getUrlSchemePolicy?(): ExternalUrlSchemePolicy;
141
+ /**
142
+ * The route pattern a module of this router serves, in the `[param]`
143
+ * vocabulary — see {@link matchRoutePattern}.
144
+ *
145
+ * What lets `s.router(router, { locale: …, slug: … }, item)` find a key's
146
+ * parameters by NAME. Absent on a router whose keys are not paths of this
147
+ * site, which therefore has no parameters to give a schema to.
148
+ */
149
+ getRoutePattern?(moduleFilePath: ModuleFilePath): string[];
108
150
  validate(moduleFilePath: ModuleFilePath, urlPaths: string[]): RouteValidationError[];
109
151
  }
@@ -11,6 +11,16 @@ export type SerializedEnumSchema = {
11
11
  * the editor's dropdown offers them in.
12
12
  */
13
13
  values: string[];
14
+ /**
15
+ * Set by `.locales(...)`: the project language each value MEANS, by value.
16
+ * Its presence is what makes this enum a locale — see `EnumSchema.locales`.
17
+ */
18
+ locales?: Record<string, string>;
19
+ /**
20
+ * Set by `.locales(..., { null: tag })`: the language of a URL that leaves
21
+ * this route parameter out. Only meaningful on a router parameter.
22
+ */
23
+ nullLocale?: string;
14
24
  opt: boolean;
15
25
  customValidate?: boolean;
16
26
  readonly?: boolean;
@@ -34,6 +44,10 @@ export declare class EnumSchema<Src extends string | null> extends Schema<Src> {
34
44
  private readonly isHidden;
35
45
  private readonly description?;
36
46
  private readonly previewInput;
47
+ /** Set by `.locales(...)`: the language each value means. */
48
+ private readonly localeMap;
49
+ /** Set by `.locales(..., { null })`: the language of a missing segment. */
50
+ private readonly nullLocale;
37
51
  /**
38
52
  * Type-only marker: as a record key, this declares the key set.
39
53
  *
@@ -43,7 +57,11 @@ export declare class EnumSchema<Src extends string | null> extends Schema<Src> {
43
57
  * cannot be a record key at all. See `LocaleSchema`.
44
58
  */
45
59
  readonly __declaresRecordKeys: true;
46
- constructor(values: readonly string[], opt?: boolean, customValidateFunctions?: CustomValidateFunction<Src>[], isReadonly?: boolean, isHidden?: boolean, description?: string | undefined, previewInput?: ItemPreviewInput<Src> | null);
60
+ constructor(values: readonly string[], opt?: boolean, customValidateFunctions?: CustomValidateFunction<Src>[], isReadonly?: boolean, isHidden?: boolean, description?: string | undefined, previewInput?: ItemPreviewInput<Src> | null,
61
+ /** Set by `.locales(...)`: the language each value means. */
62
+ localeMap?: Readonly<Record<string, string>> | null,
63
+ /** Set by `.locales(..., { null })`: the language of a missing segment. */
64
+ nullLocale?: string | null);
47
65
  /**
48
66
  * Describe this field.
49
67
  *
@@ -85,10 +103,108 @@ export declare class EnumSchema<Src extends string | null> extends Schema<Src> {
85
103
  */
86
104
  validate(validationFunction: (src: Src) => false | string): EnumSchema<Src>;
87
105
  protected executeValidate(path: SourcePath, src: Src): ValidationErrors;
106
+ /**
107
+ * Validate the value a URL gives a route parameter — see
108
+ * `s.router(router, params, item)`.
109
+ *
110
+ * Not `executeValidate`, because one thing is right here that is wrong on a
111
+ * field: `{ null: tag }`. A URL that leaves out an optional segment has a
112
+ * language someone chose; a field that holds `null` has none, and Val does
113
+ * not guess one for it.
114
+ */
115
+ protected validateRouteParam(path: SourcePath, src: Src): ValidationErrors;
116
+ private validateAs;
117
+ /**
118
+ * The tag `value` is mapped to, or `null` where the mapping does not name it.
119
+ *
120
+ * An OWN property, not `in`: a mapping that came back from JSON is a plain
121
+ * object, which inherits `toString` and its siblings — and an enum value may
122
+ * be spelled like one of them, which would read as mapped when it is not.
123
+ */
124
+ private localeTagOf;
125
+ /**
126
+ * The cross-module half of the check: is `tag` one of the project's
127
+ * languages? The same deferred error `s.locale()` reports, resolved by
128
+ * `resolveSchemaSourceFixes` against `locales.available` — so a mapped
129
+ * value and a written-out tag are checked by one implementation.
130
+ */
131
+ private checkLocale;
132
+ /**
133
+ * What is wrong with the mapping itself, whatever the value is.
134
+ *
135
+ * The compiler checks all of this for a schema written in TypeScript except
136
+ * the spelling and the uniqueness of the tags; the rest is here for a schema
137
+ * that came back from JSON.
138
+ */
139
+ protected localeMappingErrors(): ValidationError[];
140
+ private localeMappingErrorsMemo?;
141
+ private computeLocaleMappingErrors;
88
142
  protected executeAssert(path: SourcePath, src: unknown): SchemaAssertResult<Src>;
89
143
  nullable(): EnumSchema<Src | null>;
90
144
  readonly(isReadonly?: boolean): EnumSchema<Src>;
91
145
  hidden(isHidden?: boolean): EnumSchema<Src>;
146
+ /**
147
+ * Say which of the project's languages each value MEANS — which makes this
148
+ * enum a locale, the way `s.locale()` is one.
149
+ *
150
+ * For a value that is not a language tag itself: most often a route
151
+ * parameter, where a site writes `/nb/…` and means `nb-NO`. The values are
152
+ * what is stored (and what is in the URL); the tags on the right are what
153
+ * they mean, and those are checked against `locales.available` in the
154
+ * settings module exactly as an `s.locale()` value is.
155
+ *
156
+ * On a nullable enum it takes an optional second argument, `{ null: tag }`:
157
+ * the language of a URL that leaves the segment out, which is how a site
158
+ * serves its default language without a prefix. That is ONLY for a router
159
+ * parameter, where leaving it out of a nullable one is reported on every URL
160
+ * without the segment. On a field `null` means nobody chose a language, and
161
+ * saying which one it "really" is would file content under a language
162
+ * nobody picked, so there it is a schema error.
163
+ *
164
+ * One spelling per language: two values (or a value and `null`) that mean
165
+ * the same tag would give one page two URLs in that language.
166
+ *
167
+ * @example // a route parameter: `/about` is English, `/nb/about` Norwegian
168
+ * import { tanstackRouter } from "../val.config";
169
+ * const urlLocale = s
170
+ * .enum("nb")
171
+ * .nullable()
172
+ * .locales({ nb: "nb-NO" }, { null: "en-US" });
173
+ * const schema = s.router(
174
+ * tanstackRouter,
175
+ * { locale: urlLocale },
176
+ * s.object({ title: s.string() }),
177
+ * );
178
+ * export default c.define("/src/routes/{-$locale}.about.val.ts", schema, {
179
+ * "/about": { title: "About us" },
180
+ * "/nb/about": { title: "Om oss" },
181
+ * });
182
+ *
183
+ * @example // a field that stores a short code
184
+ * const schema = s.object({
185
+ * language: s.enum("en", "nb").locales({ en: "en-US", nb: "nb-NO" }),
186
+ * title: s.string(),
187
+ * });
188
+ * export default c.define("/example.val.ts", schema, {
189
+ * language: "nb",
190
+ * title: "Vinterjakke",
191
+ * });
192
+ */
193
+ locales(locales: {
194
+ readonly [Value in NonNullable<Src>]: string;
195
+ }, ...whenNull: null extends Src ? [whenNull?: {
196
+ null: string;
197
+ }] : []): EnumSchema<Src>;
198
+ /**
199
+ * The language of a URL that leaves this parameter out, where one was given.
200
+ * For `RecordSchema`, which checks that the route can leave it out at all.
201
+ */
202
+ protected routeParamNullLocale(): string | null;
203
+ /**
204
+ * Whether `.locales(...)` made this enum a language. If so, an object with
205
+ * it as a field is in that language, the same as one with `s.locale()`.
206
+ */
207
+ protected isLocaleField(): boolean;
92
208
  protected executeCustomValidateAt(path: SourcePath, src: Src): ValidationError[];
93
209
  /**
94
210
  * How this VALUE is shown where a preview of it is needed — a row in a
@@ -97,6 +97,11 @@ export declare abstract class Schema<Src extends SelectorSource> {
97
97
  * and the reason it is validated rather than typed.
98
98
  */
99
99
  protected opensLocaleScope(): "field" | "key" | null;
100
+ /**
101
+ * What to call this node in a message about the scope it opens: "object" for
102
+ * one with a locale field, "locale-keyed record" for a record keyed by one.
103
+ */
104
+ protected localeScopeName(): string;
100
105
  /**
101
106
  * The child schemas a locale scope reaches, by the path segment that reaches
102
107
  * them.
@@ -59,6 +59,11 @@ export type SerializedRecordSchema = {
59
59
  * wide default, which the Studio knows on its own.
60
60
  */
61
61
  routerSchemes?: string[];
62
+ /**
63
+ * A router's parameter schemas, by the name the route file gives each one.
64
+ * See `s.router(router, params, item)`.
65
+ */
66
+ params?: Record<string, SerializedSchema>;
62
67
  customValidate?: boolean;
63
68
  mediaType?: MediaCollectionType;
64
69
  accept?: string;
@@ -116,28 +121,36 @@ RO extends boolean = false> extends Schema<Src> {
116
121
  /**
117
122
  * Set by `.external(label)`; the entries live behind that adapter.
118
123
  *
119
- * LAST on purpose: every other `new RecordSchema(...,
120
- this.externalLabel,
121
- )` in the codebase
122
- * passes its arguments positionally, so a new parameter anywhere else
123
- * silently shifts `previewInput` and `renderInput` at each of those call
124
- * sites.
124
+ * Appended rather than inserted, and so is everything after it: every
125
+ * `new RecordSchema(...)` in the codebase passes its arguments
126
+ * positionally, so a new parameter anywhere else silently shifts
127
+ * `previewInput` and `renderInput` at each of those call sites.
125
128
  */
126
129
  private readonly externalLabel;
130
+ /**
131
+ * Set by `s.router(router, { name: schema }, item)`: a schema for each
132
+ * route parameter, by the name the route file gives it. See
133
+ * `getRouteParamValidations`.
134
+ */
135
+ private readonly routeParams;
127
136
  constructor(item: T, opt?: boolean, customValidateFunctions?: CustomValidateFunction<Src>[], currentRouter?: ValRouter | null, keySchema?: Schema<string> | null, mediaOptions?: MediaOptions | undefined, isReadonly?: boolean, isHidden?: boolean, description?: string | undefined,
128
137
  /** When true, entry values are lazily loaded {@link JsonSource} thunks. */
129
138
  isJsonValues?: boolean, previewInput?: ItemPreviewInput<Src> | null, renderInput?: FieldRender | null,
130
139
  /**
131
140
  * Set by `.external(label)`; the entries live behind that adapter.
132
141
  *
133
- * LAST on purpose: every other `new RecordSchema(...,
134
- this.externalLabel,
135
- )` in the codebase
136
- * passes its arguments positionally, so a new parameter anywhere else
137
- * silently shifts `previewInput` and `renderInput` at each of those call
138
- * sites.
142
+ * Appended rather than inserted, and so is everything after it: every
143
+ * `new RecordSchema(...)` in the codebase passes its arguments
144
+ * positionally, so a new parameter anywhere else silently shifts
145
+ * `previewInput` and `renderInput` at each of those call sites.
146
+ */
147
+ externalLabel?: string | null,
148
+ /**
149
+ * Set by `s.router(router, { name: schema }, item)`: a schema for each
150
+ * route parameter, by the name the route file gives it. See
151
+ * `getRouteParamValidations`.
139
152
  */
140
- externalLabel?: string | null);
153
+ routeParams?: Readonly<Record<string, Schema<string | null>>> | null);
141
154
  /**
142
155
  * Describe this field.
143
156
  *
@@ -212,6 +225,7 @@ RO extends boolean = false> extends Schema<Src> {
212
225
  private declaredKeys;
213
226
  private declaredKeySet;
214
227
  protected opensLocaleScope(): "field" | "key" | null;
228
+ protected localeScopeName(): string;
215
229
  protected localeScopeChildren(): {
216
230
  key: string;
217
231
  schema: Schema<SelectorSource>;
@@ -364,6 +378,38 @@ RO extends boolean = false> extends Schema<Src> {
364
378
  */
365
379
  private validateExternal;
366
380
  private getRouterValidations;
381
+ /**
382
+ * Validate what each key gives its route parameters, against the schema
383
+ * `s.router(router, { name: schema }, item)` gave that parameter.
384
+ *
385
+ * The parameters are bound by NAME — the name the route file gives them
386
+ * (`$slug`, `[slug]`, `{-$locale}`) — rather than by position, so renaming
387
+ * or adding a segment in the route file cannot silently hand one
388
+ * parameter's schema to another. A name the route does not have is
389
+ * therefore a schema error rather than a parameter that is never checked.
390
+ *
391
+ * Keys that do not match the route at all are left to the router's own
392
+ * validation, which says what the route expects; there is nothing here to
393
+ * give a parameter.
394
+ */
395
+ private getRouteParamValidations;
396
+ /**
397
+ * A marker for each `locale` or `lang` segment of this page's route that no
398
+ * route parameter schema describes.
399
+ *
400
+ * A router only checks a key's language when its module says which
401
+ * parameter is the language — `s.router(router, { locale: urlLocale, … },
402
+ * item)` — and that is written again in every route module. Forgetting it in
403
+ * one is silent: `/xx/blog/hello` validates and the page is in no language.
404
+ *
405
+ * A marker (`router:check-locale-param`) rather than an error, because
406
+ * whether it is a mistake depends on whether the project declares languages
407
+ * at all, which is in the settings module; `resolveSchemaSourceFixes`
408
+ * answers it. Any schema for the parameter is an answer — `{ lang:
409
+ * s.enum("typescript", "python") }` is how a `$lang` that is not a language
410
+ * says so.
411
+ */
412
+ private getUndescribedLocaleParamMarkers;
367
413
  protected executeCustomValidateAt(path: SourcePath, src: Src): ValidationError[];
368
414
  protected executeSerialize(): SerializedRecordSchema;
369
415
  /**
@@ -2,5 +2,13 @@ import { Schema, SelectorOfSchema } from "./index.js";
2
2
  import { ValRouter } from "../router.js";
3
3
  import { SelectorSource } from "../selector/index.js";
4
4
  import { RecordSchema } from "./record.js";
5
+ /**
6
+ * A schema for each of a route's parameters, by the name the route file gives
7
+ * it: `$slug` / `[slug]` is `slug`, `{-$locale}` is `locale`.
8
+ */
9
+ export type RouteParamSchemas = {
10
+ readonly [name: string]: Schema<string | null>;
11
+ };
5
12
  export declare function router<K extends Schema<string>, T extends Schema<SelectorSource>>(router: ValRouter, key: K, item: T): RecordSchema<T, K, Record<SelectorOfSchema<K>, SelectorOfSchema<T>>>;
13
+ export declare function router<T extends Schema<SelectorSource>>(router: ValRouter, params: RouteParamSchemas, item: T): RecordSchema<T, Schema<string>, Record<string, SelectorOfSchema<T>>>;
6
14
  export declare function router<T extends Schema<SelectorSource>>(router: ValRouter, item: T): RecordSchema<T, Schema<string>, Record<string, SelectorOfSchema<T>>>;
@@ -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", "video:add-metadata", "video:upload-remote", "video:download-remote", "videos:add-metadata", "videos:check-remote", "videos:upload-remote", "keyof:check-keys", "image:check-gallery", "file:check-gallery", "video:check-gallery", "router:check-route", "locale:check-locale", "images:check-unique-folder", "files:check-unique-folder", "images:check-all-files", "files:check-all-files", "videos:check-unique-folder", "videos:check-all-files", "jsonValues:extract-entry", "record:fill-keys", "external:upload", "view:check-module"];
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", "video:add-metadata", "video:upload-remote", "video:download-remote", "videos:add-metadata", "videos:check-remote", "videos:upload-remote", "keyof:check-keys", "image:check-gallery", "file:check-gallery", "video:check-gallery", "router:check-route", "locale:check-locale", "router:check-locale-param", "images:check-unique-folder", "files:check-unique-folder", "images:check-all-files", "files:check-all-files", "videos:check-unique-folder", "videos:check-all-files", "jsonValues:extract-entry", "record:fill-keys", "external:upload", "view:check-module"];
2
2
  export type ValidationFix = (typeof ValidationFix)[number];