@localess/schema 4.0.0 → 4.0.1-dev.20260915135536

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/SKILL.md CHANGED
@@ -1,203 +1,203 @@
1
- ---
2
- name: localess-schema
3
- description: Programmatic Localess schema definitions in TypeScript, with pure type inference of content types (no codegen). Use when defining ROOT/NODE/ENUM schemas in code, deriving content types from those definitions, or validating/exporting a schema config before pushing it to a Localess space via `@localess/cli`.
4
- ---
5
-
6
- # @localess/schema
7
-
8
- Define Localess schemas (ROOT content types, NODE nested components, ENUM
9
- option sets) in TypeScript and derive content types from them by pure type
10
- inference — no build step, no generated `.d.ts` file. Zero external
11
- dependencies: its only dependency is `@localess/model`, the shared,
12
- dependency-free domain-model package (ADR 009).
13
-
14
- `@localess/cli`'s `schema pull`/`push`/`diff`/`validate` commands sync
15
- definitions written with this package to and from a Localess space. See that
16
- package's docs for the sync workflow; this package only defines, validates,
17
- infers, and exports — it does no I/O.
18
-
19
- ## Quick start
20
-
21
- ```ts
22
- import { defineConfig, defineEnum, defineSchema } from '@localess/schema';
23
- import type { InferContentData } from '@localess/schema';
24
-
25
- const ButtonType = defineEnum({
26
- id: 'ButtonType',
27
- displayName: 'Button Type',
28
- values: [
29
- { name: 'Primary', value: 'primary' },
30
- { name: 'Secondary', value: 'secondary' },
31
- ],
32
- });
33
-
34
- const Button = defineSchema({
35
- id: 'Button',
36
- type: 'NODE',
37
- displayName: 'Button',
38
- previewField: 'label',
39
- fields: [
40
- { name: 'label', kind: 'TEXT', required: true, translatable: true, maxLength: 50 },
41
- { name: 'type', kind: 'OPTION', source: ButtonType }, // by-value ref, normalized to 'ButtonType'
42
- { name: 'icon', kind: 'ASSET', fileTypes: ['IMAGE'] },
43
- ],
44
- });
45
-
46
- const Page = defineSchema({
47
- id: 'Page',
48
- type: 'ROOT',
49
- fields: [
50
- { name: 'title', kind: 'TEXT', required: true },
51
- { name: 'blocks', kind: 'SCHEMAS', schemas: [Button] },
52
- ],
53
- });
54
-
55
- export const config = defineConfig({ schemas: [Page, Button, ButtonType] });
56
-
57
- // Content = { _id: string; _schema: 'Page'; title: string; blocks?: ButtonContent[] }
58
- export type Content = InferContentData<typeof config>;
59
- ```
60
-
61
- ## API reference
62
-
63
- | Export | Purpose |
64
- |---|---|
65
- | `defineEnum(definition)` | Define an ENUM schema. Identity function; injects `type: 'ENUM'`. |
66
- | `defineSchema(definition)` | Define a ROOT or NODE schema. Normalizes by-value refs (`source`, `schemas`) to id strings. Throws on duplicate field names. |
67
- | `defineField(field)` | Define a single field, narrowed by `kind`. Optional; catches a stray property from the wrong kind at the call site, unlike a bare field literal. Identity function. |
68
- | `defineConfig({ schemas })` | Register the full schema list — the unit the CLI loads and inference resolves against. Throws on duplicate schema ids. |
69
- | `validate(config)` | Non-throwing `{ ok, issues }` — ID/name patterns, reserved names, length limits, reference resolution. |
70
- | `toSchemaExport(config)` | Pure mapping to the wire format (`SchemaExport[]`) the Localess API accepts/returns. |
71
- | `InferContentData<C>` | Union of every ROOT schema's content type in config `C`. |
72
- | `InferContent<S, C>` | Content type of one schema definition `S`, resolved against config `C`. |
73
- | `InferEnum<E>` | Literal union of an enum definition's values. |
74
-
75
- ### Exported types
76
-
77
- Everything below is `export type` — importable with `import type { ... } from '@localess/schema'`.
78
-
79
- | Group | Types |
80
- |---|---|
81
- | Authoring (from `define.ts`) | `EnumDefinition` (result of `defineEnum`), `ComponentDefinition` (result of `defineSchema`), `SchemaDefinition` (`ComponentDefinition \| EnumDefinition`), `LocalessSchemaConfig` (`{ schemas: readonly SchemaDefinition[] }` — what `validate`/`toSchemaExport` accept), `SchemaFieldInput` (a field as authored: `source`/`schemas` accept by-value refs), `EnumDefinitionInput`, `ComponentDefinitionInput` |
82
- | Inference (from `infer.ts`) | `InferContentData`, `InferContent`, `InferEnum` |
83
- | Validation (from `validate.ts`) | `ValidationIssue` (`{ severity: 'error' \| 'warning'; code; path; message }`), `ValidationResult` (`{ ok; issues }`) |
84
- | Wire model (re-exported from `@localess/model`) | `SchemaType`, `SchemaFieldKind`, `AssetFileType`, `SchemaEnumValue`, `SchemaFieldBase`, `SchemaField`, the 18 per-kind interfaces (`SchemaFieldText`, `SchemaFieldTextarea`, `SchemaFieldRichText`, `SchemaFieldMarkdown`, `SchemaFieldNumber`, `SchemaFieldColor`, `SchemaFieldDate`, `SchemaFieldDateTime`, `SchemaFieldBoolean`, `SchemaFieldOption`, `SchemaFieldOptions`, `SchemaFieldLink`, `SchemaFieldReference`, `SchemaFieldReferences`, `SchemaFieldAsset`, `SchemaFieldAssets`, `SchemaFieldSchema`, `SchemaFieldSchemas`), `SchemaComponentExport`, `SchemaEnumExport`, `SchemaExport` |
85
- | Content values (re-exported from `@localess/model`) | `ContentAsset`, `ContentLink`, `ContentReference`, `ContentRichText` |
86
-
87
- ## `defineField`
88
-
89
- Optional. Wraps a single field so TypeScript catches a stray property from
90
- the wrong `kind` at the call site — something a bare field literal inside
91
- `defineSchema({ fields: [...] })` cannot do:
92
-
93
- ```ts
94
- import { defineField } from '@localess/schema';
95
-
96
- defineField({ name: 'amount', kind: 'NUMBER', maxLength: 5 });
97
- // ^ compile error: maxLength is not valid on a NUMBER field
98
-
99
- defineField({ name: 'amount', kind: 'NUMBER', minValue: 0 }); // OK
100
- ```
101
-
102
- `defineSchema`'s `fields` array accepts raw literals and `defineField(...)`
103
- results interchangeably — by-value ref normalization (`source`, `schemas`)
104
- still happens exclusively in `defineSchema`, regardless of a field's origin.
105
-
106
- ## By-value references
107
-
108
- `OPTION`/`OPTIONS` fields accept an enum definition (or a string id) in
109
- `source`; `SCHEMA`/`SCHEMAS` fields accept schema definitions (or string ids)
110
- in `schemas`. Both are normalized to string ids at runtime by `defineSchema`,
111
- matching the wire format exactly — but the *type* keeps the literal id, so
112
- `InferContent` resolves `OPTION` fields to the referenced enum's literal
113
- value union instead of plain `string`, and `SCHEMA`/`SCHEMAS` fields to the
114
- allowed schemas' content types (or every `NODE` schema in the config when the
115
- `schemas` list is omitted).
116
-
117
- Fallbacks when a reference can't be resolved against the config:
118
-
119
- - `OPTION`/`OPTIONS` whose `source` id is not in the config → `string` (or
120
- `string[]`), mirroring the CLI's `type generate` behavior.
121
- - `InferEnum` of an enum with no `values` → `string`.
122
- - Unrestricted `SCHEMA`/`SCHEMAS` (no `schemas` list) in a config with no
123
- `NODE` schemas → `{ _id: string; _schema: string }` (or an array of it).
124
- - `InferContentData` of a config with no `ROOT` schema → `never`.
125
-
126
- ## Field kinds
127
-
128
- Every field carries `name`, `kind`, and the base optional properties
129
- (`displayName`, `required`, `description`, `defaultValue`, `translatable`).
130
- `required: true` fields are non-optional keys on the inferred content type;
131
- everything else is optional.
132
-
133
- | Kind | Extra properties | Inferred type |
134
- |---|---|---|
135
- | `TEXT`, `TEXTAREA`, `MARKDOWN` | `minLength?`, `maxLength?` | `string` |
136
- | `RICH_TEXT` | `minLength?`, `maxLength?` | `ContentRichText` |
137
- | `NUMBER` | `minValue?`, `maxValue?` | `number` |
138
- | `COLOR`, `DATE`, `DATETIME` | — | `string` |
139
- | `BOOLEAN` | — | `boolean` |
140
- | `OPTION` | `source` (required; enum id or `defineEnum` result) | literal union of the referenced enum's values |
141
- | `OPTIONS` | `source` (required), `minValues?`, `maxValues?` | that union, as an array |
142
- | `LINK` | — | `ContentLink` |
143
- | `REFERENCE` / `REFERENCES` | `path?` | `ContentReference` / `ContentReference[]` |
144
- | `ASSET` / `ASSETS` | `fileTypes?: AssetFileType[]`, `fileType?: AssetFileType` | `ContentAsset` / `ContentAsset[]` |
145
- | `SCHEMA` / `SCHEMAS` | `schemas?` (allowed ids or `defineSchema` results; unrestricted when absent) | allowed schemas' content type / array of it |
146
-
147
- `AssetFileType` is `'ANY' | 'IMAGE' | 'VIDEO' | 'TEXT' | 'AUDIO' | 'APPLICATION'`.
148
-
149
- `ContentAsset`, `ContentLink`, `ContentReference`, and `ContentRichText` are
150
- re-exported from `@localess/model`, the shared domain-model package (see
151
- ADR 009).
152
-
153
- ## `validate(config)`
154
-
155
- Non-throwing. Returns `{ ok, issues }`; `ok` is `false` only when at least
156
- one `error`-severity issue is present (the `ValidationIssue` type allows
157
- `'warning'`, but every rule currently implemented reports `error`). Every
158
- schema in the config is checked, not just the first. Rules and their `code`:
159
-
160
- | Code | Rule |
161
- |---|---|
162
- | `schema/invalid-id` | Schema id must match `/^[a-zA-Z][a-zA-Z0-9]+$/` and be 2-50 characters. |
163
- | `schema/reserved-id` | Schema id must not be (case-insensitively) `Translations`, `Links`, `ContentMetadata`, `ContentReference`, `ContentRichText`, `ContentLink`, `ContentData`, `ContentAsset`, or `Content`. |
164
- | `schema/display-name-too-long` | Schema `displayName` ≤ 50 characters. |
165
- | `schema/description-too-long` | Schema `description` ≤ 250 characters. |
166
- | `schema/invalid-label` | Each label 2-50 characters, no spaces. |
167
- | `enum/invalid-value-name` | ENUM value `name` 1-50 characters. |
168
- | `enum/invalid-value` | ENUM `value` 1-50 characters matching `/^[a-zA-Z]$\|^[a-zA-Z][a-zA-Z0-9-_]*[a-zA-Z0-9]$/`. |
169
- | `schema/unknown-preview-field` | `previewField` must name a field of the same schema. |
170
- | `field/invalid-name` | Field name must match `/^[a-z][a-zA-Z0-9_]*[a-zA-Z0-9]$/` (camelCase), be 2-30 characters, and not contain `_i18n_`. |
171
- | `field/reserved-name` | Field name must not be (case-insensitively) `_id` or `_schema`. |
172
- | `field/display-name-too-long` | Field `displayName` ≤ 30 characters. |
173
- | `field/description-too-long` | Field `description` ≤ 250 characters. |
174
- | `field/default-value-too-long` | Field `defaultValue` ≤ 250 characters. |
175
- | `field/unresolved-source` | `OPTION`/`OPTIONS` `source` must be the id of a schema in the config. |
176
- | `field/source-not-enum` | ...and that schema must be an `ENUM`. |
177
- | `field/unresolved-schema-ref` | Every `SCHEMA`/`SCHEMAS` `schemas` entry must be the id of a schema in the config. |
178
- | `field/schema-ref-is-enum` | ...and that schema must be `ROOT` or `NODE`, not `ENUM`. |
179
-
180
- `validate` does not check for stray properties from the wrong field kind —
181
- use `defineField` for that (below). Duplicate field names and duplicate
182
- schema ids are not validation issues either: `defineSchema`/`defineConfig`
183
- throw on them as programming errors.
184
-
185
- ## `toSchemaExport(config)`
186
-
187
- Pure mapping to `SchemaExport[]`, preserving schema order. Strips
188
- `undefined`-valued keys from each schema and from the items of its array
189
- properties (`fields`, `values`), so the result round-trips through JSON
190
- unchanged. Performs no I/O.
191
-
192
- ## Known limitation (mitigated by `defineField`)
193
-
194
- TypeScript's excess-property check doesn't apply to object literals inside
195
- an array passed through a `const`-inferred generic parameter (only to
196
- literals checked directly against a declared type). A stray property from
197
- the wrong field kind — e.g. `maxLength` on a `NUMBER` field — inside a bare
198
- field literal in `defineSchema({ fields: [...] })` is therefore not flagged
199
- at the call site. Wrap the field in `defineField(...)` instead to get that
200
- check while keeping full literal-type preservation (see "`defineField`"
201
- above). Missing required properties (e.g. omitting `source` on `OPTION`) are
202
- still caught either way. `validate()` and the Localess backend's schema
203
- validation don't check for this either today.
1
+ ---
2
+ name: localess-schema
3
+ description: Programmatic Localess schema definitions in TypeScript, with pure type inference of content types (no codegen). Use when defining ROOT/NODE/ENUM schemas in code, deriving content types from those definitions, or validating/exporting a schema config before pushing it to a Localess space via `@localess/cli`.
4
+ ---
5
+
6
+ # @localess/schema
7
+
8
+ Define Localess schemas (ROOT content types, NODE nested components, ENUM
9
+ option sets) in TypeScript and derive content types from them by pure type
10
+ inference — no build step, no generated `.d.ts` file. Zero external
11
+ dependencies: its only dependency is `@localess/model`, the shared,
12
+ dependency-free domain-model package (ADR 009).
13
+
14
+ `@localess/cli`'s `schema pull`/`push`/`diff`/`validate` commands sync
15
+ definitions written with this package to and from a Localess space. See that
16
+ package's docs for the sync workflow; this package only defines, validates,
17
+ infers, and exports — it does no I/O.
18
+
19
+ ## Quick start
20
+
21
+ ```ts
22
+ import { defineConfig, defineEnum, defineSchema } from '@localess/schema';
23
+ import type { InferContentData } from '@localess/schema';
24
+
25
+ const ButtonType = defineEnum({
26
+ id: 'ButtonType',
27
+ displayName: 'Button Type',
28
+ values: [
29
+ { name: 'Primary', value: 'primary' },
30
+ { name: 'Secondary', value: 'secondary' },
31
+ ],
32
+ });
33
+
34
+ const Button = defineSchema({
35
+ id: 'Button',
36
+ type: 'NODE',
37
+ displayName: 'Button',
38
+ previewField: 'label',
39
+ fields: [
40
+ { name: 'label', kind: 'TEXT', required: true, translatable: true, maxLength: 50 },
41
+ { name: 'type', kind: 'OPTION', source: ButtonType }, // by-value ref, normalized to 'ButtonType'
42
+ { name: 'icon', kind: 'ASSET', fileTypes: ['IMAGE'] },
43
+ ],
44
+ });
45
+
46
+ const Page = defineSchema({
47
+ id: 'Page',
48
+ type: 'ROOT',
49
+ fields: [
50
+ { name: 'title', kind: 'TEXT', required: true },
51
+ { name: 'blocks', kind: 'SCHEMAS', schemas: [Button] },
52
+ ],
53
+ });
54
+
55
+ export const config = defineConfig({ schemas: [Page, Button, ButtonType] });
56
+
57
+ // Content = { _id: string; _schema: 'Page'; title: string; blocks?: ButtonContent[] }
58
+ export type Content = InferContentData<typeof config>;
59
+ ```
60
+
61
+ ## API reference
62
+
63
+ | Export | Purpose |
64
+ |---|---|
65
+ | `defineEnum(definition)` | Define an ENUM schema. Identity function; injects `type: 'ENUM'`. |
66
+ | `defineSchema(definition)` | Define a ROOT or NODE schema. Normalizes by-value refs (`source`, `schemas`) to id strings. Throws on duplicate field names. `previewField` is restricted to the names of the schema's own `fields` (falls back to `string` when `fields` is omitted). |
67
+ | `defineField(field)` | Define a single field, narrowed by `kind`. Optional; catches a stray property from the wrong kind at the call site, unlike a bare field literal. Identity function. |
68
+ | `defineConfig({ schemas })` | Register the full schema list — the unit the CLI loads and inference resolves against. Throws on duplicate schema ids. |
69
+ | `validate(config)` | Non-throwing `{ ok, issues }` — ID/name patterns, reserved names, length limits, reference resolution. |
70
+ | `toSchemaExport(config)` | Pure mapping to the wire format (`SchemaExport[]`) the Localess API accepts/returns. |
71
+ | `InferContentData<C>` | Union of every ROOT schema's content type in config `C`. |
72
+ | `InferContent<S, C>` | Content type of one schema definition `S`, resolved against config `C`. |
73
+ | `InferEnum<E>` | Literal union of an enum definition's values. |
74
+
75
+ ### Exported types
76
+
77
+ Everything below is `export type` — importable with `import type { ... } from '@localess/schema'`.
78
+
79
+ | Group | Types |
80
+ |---|---|
81
+ | Authoring (from `define.ts`) | `EnumDefinition` (result of `defineEnum`), `ComponentDefinition` (result of `defineSchema`), `SchemaDefinition` (`ComponentDefinition \| EnumDefinition`), `LocalessSchemaConfig` (`{ schemas: readonly SchemaDefinition[] }` — what `validate`/`toSchemaExport` accept), `SchemaFieldInput` (a field as authored: `source`/`schemas` accept by-value refs), `EnumDefinitionInput`, `ComponentDefinitionInput` |
82
+ | Inference (from `infer.ts`) | `InferContentData`, `InferContent`, `InferEnum` |
83
+ | Validation (from `validate.ts`) | `ValidationIssue` (`{ severity: 'error' \| 'warning'; code; path; message }`), `ValidationResult` (`{ ok; issues }`) |
84
+ | Wire model (re-exported from `@localess/model`) | `SchemaType`, `SchemaFieldKind`, `AssetFileType`, `SchemaEnumValue`, `SchemaFieldBase`, `SchemaField`, the 18 per-kind interfaces (`SchemaFieldText`, `SchemaFieldTextarea`, `SchemaFieldRichText`, `SchemaFieldMarkdown`, `SchemaFieldNumber`, `SchemaFieldColor`, `SchemaFieldDate`, `SchemaFieldDateTime`, `SchemaFieldBoolean`, `SchemaFieldOption`, `SchemaFieldOptions`, `SchemaFieldLink`, `SchemaFieldReference`, `SchemaFieldReferences`, `SchemaFieldAsset`, `SchemaFieldAssets`, `SchemaFieldSchema`, `SchemaFieldSchemas`), `SchemaComponentExport`, `SchemaEnumExport`, `SchemaExport` |
85
+ | Content values (re-exported from `@localess/model`) | `ContentAsset`, `ContentLink`, `ContentReference`, `ContentRichText` |
86
+
87
+ ## `defineField`
88
+
89
+ Optional. Wraps a single field so TypeScript catches a stray property from
90
+ the wrong `kind` at the call site — something a bare field literal inside
91
+ `defineSchema({ fields: [...] })` cannot do:
92
+
93
+ ```ts
94
+ import { defineField } from '@localess/schema';
95
+
96
+ defineField({ name: 'amount', kind: 'NUMBER', maxLength: 5 });
97
+ // ^ compile error: maxLength is not valid on a NUMBER field
98
+
99
+ defineField({ name: 'amount', kind: 'NUMBER', minValue: 0 }); // OK
100
+ ```
101
+
102
+ `defineSchema`'s `fields` array accepts raw literals and `defineField(...)`
103
+ results interchangeably — by-value ref normalization (`source`, `schemas`)
104
+ still happens exclusively in `defineSchema`, regardless of a field's origin.
105
+
106
+ ## By-value references
107
+
108
+ `OPTION`/`OPTIONS` fields accept an enum definition (or a string id) in
109
+ `source`; `SCHEMA`/`SCHEMAS` fields accept schema definitions (or string ids)
110
+ in `schemas`. Both are normalized to string ids at runtime by `defineSchema`,
111
+ matching the wire format exactly — but the *type* keeps the literal id, so
112
+ `InferContent` resolves `OPTION` fields to the referenced enum's literal
113
+ value union instead of plain `string`, and `SCHEMA`/`SCHEMAS` fields to the
114
+ allowed schemas' content types (or every `NODE` schema in the config when the
115
+ `schemas` list is omitted).
116
+
117
+ Fallbacks when a reference can't be resolved against the config:
118
+
119
+ - `OPTION`/`OPTIONS` whose `source` id is not in the config → `string` (or
120
+ `string[]`), mirroring the CLI's `type generate` behavior.
121
+ - `InferEnum` of an enum with no `values` → `string`.
122
+ - Unrestricted `SCHEMA`/`SCHEMAS` (no `schemas` list) in a config with no
123
+ `NODE` schemas → `{ _id: string; _schema: string }` (or an array of it).
124
+ - `InferContentData` of a config with no `ROOT` schema → `never`.
125
+
126
+ ## Field kinds
127
+
128
+ Every field carries `name`, `kind`, and the base optional properties
129
+ (`displayName`, `required`, `description`, `defaultValue`, `translatable`).
130
+ `required: true` fields are non-optional keys on the inferred content type;
131
+ everything else is optional.
132
+
133
+ | Kind | Extra properties | Inferred type |
134
+ |---|---|---|
135
+ | `TEXT`, `TEXTAREA`, `MARKDOWN` | `minLength?`, `maxLength?` | `string` |
136
+ | `RICH_TEXT` | `minLength?`, `maxLength?` | `ContentRichText` |
137
+ | `NUMBER` | `minValue?`, `maxValue?` | `number` |
138
+ | `COLOR`, `DATE`, `DATETIME` | — | `string` |
139
+ | `BOOLEAN` | — | `boolean` |
140
+ | `OPTION` | `source` (required; enum id or `defineEnum` result) | literal union of the referenced enum's values |
141
+ | `OPTIONS` | `source` (required), `minValues?`, `maxValues?` | that union, as an array |
142
+ | `LINK` | — | `ContentLink` |
143
+ | `REFERENCE` / `REFERENCES` | `path?` | `ContentReference` / `ContentReference[]` |
144
+ | `ASSET` / `ASSETS` | `fileTypes?: AssetFileType[]`, `fileType?: AssetFileType` | `ContentAsset` / `ContentAsset[]` |
145
+ | `SCHEMA` / `SCHEMAS` | `schemas?` (allowed ids or `defineSchema` results; unrestricted when absent) | allowed schemas' content type / array of it |
146
+
147
+ `AssetFileType` is `'ANY' | 'IMAGE' | 'VIDEO' | 'TEXT' | 'AUDIO' | 'APPLICATION'`.
148
+
149
+ `ContentAsset`, `ContentLink`, `ContentReference`, and `ContentRichText` are
150
+ re-exported from `@localess/model`, the shared domain-model package (see
151
+ ADR 009).
152
+
153
+ ## `validate(config)`
154
+
155
+ Non-throwing. Returns `{ ok, issues }`; `ok` is `false` only when at least
156
+ one `error`-severity issue is present (the `ValidationIssue` type allows
157
+ `'warning'`, but every rule currently implemented reports `error`). Every
158
+ schema in the config is checked, not just the first. Rules and their `code`:
159
+
160
+ | Code | Rule |
161
+ |---|---|
162
+ | `schema/invalid-id` | Schema id must match `/^[a-zA-Z][a-zA-Z0-9]+$/` and be 2-50 characters. |
163
+ | `schema/reserved-id` | Schema id must not be (case-insensitively) `Translations`, `Links`, `ContentMetadata`, `ContentReference`, `ContentRichText`, `ContentLink`, `ContentData`, `ContentAsset`, or `Content`. |
164
+ | `schema/display-name-too-long` | Schema `displayName` ≤ 50 characters. |
165
+ | `schema/description-too-long` | Schema `description` ≤ 250 characters. |
166
+ | `schema/invalid-label` | Each label 2-50 characters, no spaces. |
167
+ | `enum/invalid-value-name` | ENUM value `name` 1-50 characters. |
168
+ | `enum/invalid-value` | ENUM `value` 1-50 characters matching `/^[a-zA-Z]$\|^[a-zA-Z][a-zA-Z0-9-_]*[a-zA-Z0-9]$/`. |
169
+ | `schema/unknown-preview-field` | `previewField` must name a field of the same schema. |
170
+ | `field/invalid-name` | Field name must match `/^[a-z][a-zA-Z0-9_]*[a-zA-Z0-9]$/` (camelCase), be 2-30 characters, and not contain `_i18n_`. |
171
+ | `field/reserved-name` | Field name must not be (case-insensitively) `_id` or `_schema`. |
172
+ | `field/display-name-too-long` | Field `displayName` ≤ 30 characters. |
173
+ | `field/description-too-long` | Field `description` ≤ 250 characters. |
174
+ | `field/default-value-too-long` | Field `defaultValue` ≤ 250 characters. |
175
+ | `field/unresolved-source` | `OPTION`/`OPTIONS` `source` must be the id of a schema in the config. |
176
+ | `field/source-not-enum` | ...and that schema must be an `ENUM`. |
177
+ | `field/unresolved-schema-ref` | Every `SCHEMA`/`SCHEMAS` `schemas` entry must be the id of a schema in the config. |
178
+ | `field/schema-ref-is-enum` | ...and that schema must be `ROOT` or `NODE`, not `ENUM`. |
179
+
180
+ `validate` does not check for stray properties from the wrong field kind —
181
+ use `defineField` for that (below). Duplicate field names and duplicate
182
+ schema ids are not validation issues either: `defineSchema`/`defineConfig`
183
+ throw on them as programming errors.
184
+
185
+ ## `toSchemaExport(config)`
186
+
187
+ Pure mapping to `SchemaExport[]`, preserving schema order. Strips
188
+ `undefined`-valued keys from each schema and from the items of its array
189
+ properties (`fields`, `values`), so the result round-trips through JSON
190
+ unchanged. Performs no I/O.
191
+
192
+ ## Known limitation (mitigated by `defineField`)
193
+
194
+ TypeScript's excess-property check doesn't apply to object literals inside
195
+ an array passed through a `const`-inferred generic parameter (only to
196
+ literals checked directly against a declared type). A stray property from
197
+ the wrong field kind — e.g. `maxLength` on a `NUMBER` field — inside a bare
198
+ field literal in `defineSchema({ fields: [...] })` is therefore not flagged
199
+ at the call site. Wrap the field in `defineField(...)` instead to get that
200
+ check while keeping full literal-type preservation (see "`defineField`"
201
+ above). Missing required properties (e.g. omitting `source` on `OPTION`) are
202
+ still caught either way. `validate()` and the Localess backend's schema
203
+ validation don't check for this either today.
package/dist/define.d.ts CHANGED
@@ -37,9 +37,9 @@ type FieldInputOf<F> = F extends {
37
37
  * against a declared type), so a stray property from a different kind (e.g. `maxLength` on a
38
38
  * `NUMBER` field) inside `defineSchema({ fields: [...] })` will not be flagged at the call site.
39
39
  * Missing required properties (e.g. omitting `source` on `OPTION`) are still caught, since that is
40
- * ordinary structural assignability, not a freshness check. Sanity documents the same limitation
41
- * for their unwrapped array fields; the alternative is a per-field wrapper function, which this
42
- * package deliberately avoids (see ADR 008).
40
+ * ordinary structural assignability, not a freshness check. Wrap a field in `defineField(...)` to
41
+ * get the excess-property check at the call site; raw literals and `defineField` results are
42
+ * accepted interchangeably (see ADR 008).
43
43
  */
44
44
  export type SchemaFieldInput = FieldInputOf<SchemaField>;
45
45
  /** Authoring input for defineEnum: type is injected by the helper. */
@@ -100,8 +100,21 @@ type DefinedComponent<TId extends string, TType extends 'ROOT' | 'NODE', TFields
100
100
  * Define an ENUM schema. Identity function apart from injecting `type: 'ENUM'`;
101
101
  * exists to preserve literal types for inference.
102
102
  *
103
+ * - `id` — unique schema id; also what `OPTION`/`OPTIONS` fields reference via `source`
104
+ * - `values` — the fixed option set; each `{ name, value }` becomes one selectable option, and
105
+ * every `value` across the config becomes part of `InferEnum`'s literal union
106
+ *
103
107
  * @param definition the enum definition (id, values, optional display metadata)
104
108
  * @returns the definition with `type: 'ENUM'`, literal types preserved
109
+ *
110
+ * @example
111
+ * const Status = defineEnum({
112
+ * id: 'Status',
113
+ * values: [
114
+ * { name: 'Draft', value: 'draft' },
115
+ * { name: 'Published', value: 'published' },
116
+ * ],
117
+ * });
105
118
  */
106
119
  export declare function defineEnum<const TId extends string, const TValues extends readonly SchemaEnumValue[] | undefined = undefined>(definition: {
107
120
  id: TId;
@@ -119,8 +132,29 @@ export declare function defineEnum<const TId extends string, const TValues exten
119
132
  * (`source`, `schemas`) still happens exclusively in `defineSchema`, applied uniformly regardless
120
133
  * of a field's origin. See `docs/decisions/008-schema-package.md`.
121
134
  *
135
+ * Every `kind` accepts `name` (required), plus `displayName?`, `required?`, `description?`,
136
+ * `defaultValue?`, `translatable?`. Kind-specific extras:
137
+ * - `TEXT` / `TEXTAREA` / `RICH_TEXT` / `MARKDOWN` — `minLength?`, `maxLength?`
138
+ * - `NUMBER` — `minValue?`, `maxValue?`
139
+ * - `COLOR` / `DATE` / `DATETIME` / `BOOLEAN` / `LINK` — no extras
140
+ * - `OPTION` — `source` (required: an ENUM definition from `defineEnum`, or its id)
141
+ * - `OPTIONS` — `source` (required, same as `OPTION`), `minValues?`, `maxValues?`
142
+ * - `REFERENCE` / `REFERENCES` — `path?`
143
+ * - `ASSET` / `ASSETS` — `fileTypes?`, `fileType?`
144
+ * - `SCHEMA` / `SCHEMAS` — `schemas?` (allowed definitions from `defineSchema`, or their ids;
145
+ * every `NODE` schema in the config is allowed when omitted)
146
+ *
147
+ * Full field-kind reference, including the type each kind infers to: `docs/schema.md`.
148
+ *
122
149
  * @param field the field definition; `kind` selects which extra properties are allowed
123
150
  * @returns the field unchanged, with `name`/`kind`/extras narrowed to their literal types
151
+ *
152
+ * @example
153
+ * defineField({ name: 'title', kind: 'TEXT', required: true, maxLength: 100 });
154
+ * @example
155
+ * defineField({ name: 'status', kind: 'OPTION', source: Status }); // Status = defineEnum(...)
156
+ * @example
157
+ * defineField({ name: 'blocks', kind: 'SCHEMAS', schemas: [Button] }); // Button = defineSchema(...)
124
158
  */
125
159
  export declare function defineField<const TKind extends SchemaFieldKind, const TName extends string, const TField extends Omit<Extract<SchemaFieldInput, {
126
160
  kind: TKind;
@@ -133,14 +167,36 @@ export declare function defineField<const TKind extends SchemaFieldKind, const T
133
167
  name: TName;
134
168
  kind: TKind;
135
169
  } & TField>;
170
+ /** Union of a schema's own field names; falls back to plain `string` when `fields` is omitted. */
171
+ type FieldNameOf<TFields> = TFields extends readonly {
172
+ name: infer N extends string;
173
+ }[] ? N : string;
136
174
  /**
137
175
  * Define a ROOT (content type) or NODE (nested component) schema.
138
176
  * Normalizes by-value references (enum in `source`, components in `schemas`) to their id strings;
139
177
  * the returned type keeps those ids as literals for inference.
140
178
  *
179
+ * - `id` — unique schema id; also the `_schema` value on its inferred content type, and what
180
+ * `SCHEMA`/`SCHEMAS` fields reference via `schemas`
181
+ * - `type` — `'ROOT'` for a fetchable content type, `'NODE'` for a nested component only reachable
182
+ * through another schema's `SCHEMA`/`SCHEMAS` field
183
+ * - `previewField` — name of one of this schema's own fields, shown as its preview label in the
184
+ * Localess editor; restricted to the field names in `fields` (falls back to plain `string`
185
+ * when `fields` is omitted)
186
+ * - `fields` — ordered list of `defineField(...)` results and/or raw field literals; see
187
+ * `defineField` for the per-kind property reference
188
+ *
141
189
  * @param definition the schema definition; `type` selects ROOT or NODE
142
190
  * @returns the definition with references normalized to id strings, literal types preserved
143
191
  * @throws Error on duplicate field names — a programming error, not a validation concern
192
+ *
193
+ * @example
194
+ * const Button = defineSchema({
195
+ * id: 'Button',
196
+ * type: 'NODE',
197
+ * previewField: 'label',
198
+ * fields: [defineField({ name: 'label', kind: 'TEXT', required: true })],
199
+ * });
144
200
  */
145
201
  export declare function defineSchema<const TId extends string, const TType extends 'ROOT' | 'NODE', const TFields extends readonly SchemaFieldInput[] | undefined = undefined>(definition: {
146
202
  id: TId;
@@ -148,7 +204,7 @@ export declare function defineSchema<const TId extends string, const TType exten
148
204
  displayName?: string;
149
205
  description?: string;
150
206
  labels?: readonly string[];
151
- previewField?: string;
207
+ previewField?: FieldNameOf<TFields>;
152
208
  fields?: TFields;
153
209
  }): DefinedComponent<TId, TType, TFields>;
154
210
  /**
package/dist/index.js CHANGED
@@ -4,8 +4,21 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
4
  * Define an ENUM schema. Identity function apart from injecting `type: 'ENUM'`;
5
5
  * exists to preserve literal types for inference.
6
6
  *
7
+ * - `id` — unique schema id; also what `OPTION`/`OPTIONS` fields reference via `source`
8
+ * - `values` — the fixed option set; each `{ name, value }` becomes one selectable option, and
9
+ * every `value` across the config becomes part of `InferEnum`'s literal union
10
+ *
7
11
  * @param definition the enum definition (id, values, optional display metadata)
8
12
  * @returns the definition with `type: 'ENUM'`, literal types preserved
13
+ *
14
+ * @example
15
+ * const Status = defineEnum({
16
+ * id: 'Status',
17
+ * values: [
18
+ * { name: 'Draft', value: 'draft' },
19
+ * { name: 'Published', value: 'published' },
20
+ * ],
21
+ * });
9
22
  */
10
23
  function defineEnum(definition) {
11
24
  return {
@@ -22,8 +35,29 @@ function defineEnum(definition) {
22
35
  * (`source`, `schemas`) still happens exclusively in `defineSchema`, applied uniformly regardless
23
36
  * of a field's origin. See `docs/decisions/008-schema-package.md`.
24
37
  *
38
+ * Every `kind` accepts `name` (required), plus `displayName?`, `required?`, `description?`,
39
+ * `defaultValue?`, `translatable?`. Kind-specific extras:
40
+ * - `TEXT` / `TEXTAREA` / `RICH_TEXT` / `MARKDOWN` — `minLength?`, `maxLength?`
41
+ * - `NUMBER` — `minValue?`, `maxValue?`
42
+ * - `COLOR` / `DATE` / `DATETIME` / `BOOLEAN` / `LINK` — no extras
43
+ * - `OPTION` — `source` (required: an ENUM definition from `defineEnum`, or its id)
44
+ * - `OPTIONS` — `source` (required, same as `OPTION`), `minValues?`, `maxValues?`
45
+ * - `REFERENCE` / `REFERENCES` — `path?`
46
+ * - `ASSET` / `ASSETS` — `fileTypes?`, `fileType?`
47
+ * - `SCHEMA` / `SCHEMAS` — `schemas?` (allowed definitions from `defineSchema`, or their ids;
48
+ * every `NODE` schema in the config is allowed when omitted)
49
+ *
50
+ * Full field-kind reference, including the type each kind infers to: `docs/schema.md`.
51
+ *
25
52
  * @param field the field definition; `kind` selects which extra properties are allowed
26
53
  * @returns the field unchanged, with `name`/`kind`/extras narrowed to their literal types
54
+ *
55
+ * @example
56
+ * defineField({ name: 'title', kind: 'TEXT', required: true, maxLength: 100 });
57
+ * @example
58
+ * defineField({ name: 'status', kind: 'OPTION', source: Status }); // Status = defineEnum(...)
59
+ * @example
60
+ * defineField({ name: 'blocks', kind: 'SCHEMAS', schemas: [Button] }); // Button = defineSchema(...)
27
61
  */
28
62
  function defineField(field) {
29
63
  return field;
@@ -33,9 +67,27 @@ function defineField(field) {
33
67
  * Normalizes by-value references (enum in `source`, components in `schemas`) to their id strings;
34
68
  * the returned type keeps those ids as literals for inference.
35
69
  *
70
+ * - `id` — unique schema id; also the `_schema` value on its inferred content type, and what
71
+ * `SCHEMA`/`SCHEMAS` fields reference via `schemas`
72
+ * - `type` — `'ROOT'` for a fetchable content type, `'NODE'` for a nested component only reachable
73
+ * through another schema's `SCHEMA`/`SCHEMAS` field
74
+ * - `previewField` — name of one of this schema's own fields, shown as its preview label in the
75
+ * Localess editor; restricted to the field names in `fields` (falls back to plain `string`
76
+ * when `fields` is omitted)
77
+ * - `fields` — ordered list of `defineField(...)` results and/or raw field literals; see
78
+ * `defineField` for the per-kind property reference
79
+ *
36
80
  * @param definition the schema definition; `type` selects ROOT or NODE
37
81
  * @returns the definition with references normalized to id strings, literal types preserved
38
82
  * @throws Error on duplicate field names — a programming error, not a validation concern
83
+ *
84
+ * @example
85
+ * const Button = defineSchema({
86
+ * id: 'Button',
87
+ * type: 'NODE',
88
+ * previewField: 'label',
89
+ * fields: [defineField({ name: 'label', kind: 'TEXT', required: true })],
90
+ * });
39
91
  */
40
92
  function defineSchema(definition) {
41
93
  const seen = /* @__PURE__ */ new Set();
package/dist/index.mjs CHANGED
@@ -3,8 +3,21 @@
3
3
  * Define an ENUM schema. Identity function apart from injecting `type: 'ENUM'`;
4
4
  * exists to preserve literal types for inference.
5
5
  *
6
+ * - `id` — unique schema id; also what `OPTION`/`OPTIONS` fields reference via `source`
7
+ * - `values` — the fixed option set; each `{ name, value }` becomes one selectable option, and
8
+ * every `value` across the config becomes part of `InferEnum`'s literal union
9
+ *
6
10
  * @param definition the enum definition (id, values, optional display metadata)
7
11
  * @returns the definition with `type: 'ENUM'`, literal types preserved
12
+ *
13
+ * @example
14
+ * const Status = defineEnum({
15
+ * id: 'Status',
16
+ * values: [
17
+ * { name: 'Draft', value: 'draft' },
18
+ * { name: 'Published', value: 'published' },
19
+ * ],
20
+ * });
8
21
  */
9
22
  function defineEnum(definition) {
10
23
  return {
@@ -21,8 +34,29 @@ function defineEnum(definition) {
21
34
  * (`source`, `schemas`) still happens exclusively in `defineSchema`, applied uniformly regardless
22
35
  * of a field's origin. See `docs/decisions/008-schema-package.md`.
23
36
  *
37
+ * Every `kind` accepts `name` (required), plus `displayName?`, `required?`, `description?`,
38
+ * `defaultValue?`, `translatable?`. Kind-specific extras:
39
+ * - `TEXT` / `TEXTAREA` / `RICH_TEXT` / `MARKDOWN` — `minLength?`, `maxLength?`
40
+ * - `NUMBER` — `minValue?`, `maxValue?`
41
+ * - `COLOR` / `DATE` / `DATETIME` / `BOOLEAN` / `LINK` — no extras
42
+ * - `OPTION` — `source` (required: an ENUM definition from `defineEnum`, or its id)
43
+ * - `OPTIONS` — `source` (required, same as `OPTION`), `minValues?`, `maxValues?`
44
+ * - `REFERENCE` / `REFERENCES` — `path?`
45
+ * - `ASSET` / `ASSETS` — `fileTypes?`, `fileType?`
46
+ * - `SCHEMA` / `SCHEMAS` — `schemas?` (allowed definitions from `defineSchema`, or their ids;
47
+ * every `NODE` schema in the config is allowed when omitted)
48
+ *
49
+ * Full field-kind reference, including the type each kind infers to: `docs/schema.md`.
50
+ *
24
51
  * @param field the field definition; `kind` selects which extra properties are allowed
25
52
  * @returns the field unchanged, with `name`/`kind`/extras narrowed to their literal types
53
+ *
54
+ * @example
55
+ * defineField({ name: 'title', kind: 'TEXT', required: true, maxLength: 100 });
56
+ * @example
57
+ * defineField({ name: 'status', kind: 'OPTION', source: Status }); // Status = defineEnum(...)
58
+ * @example
59
+ * defineField({ name: 'blocks', kind: 'SCHEMAS', schemas: [Button] }); // Button = defineSchema(...)
26
60
  */
27
61
  function defineField(field) {
28
62
  return field;
@@ -32,9 +66,27 @@ function defineField(field) {
32
66
  * Normalizes by-value references (enum in `source`, components in `schemas`) to their id strings;
33
67
  * the returned type keeps those ids as literals for inference.
34
68
  *
69
+ * - `id` — unique schema id; also the `_schema` value on its inferred content type, and what
70
+ * `SCHEMA`/`SCHEMAS` fields reference via `schemas`
71
+ * - `type` — `'ROOT'` for a fetchable content type, `'NODE'` for a nested component only reachable
72
+ * through another schema's `SCHEMA`/`SCHEMAS` field
73
+ * - `previewField` — name of one of this schema's own fields, shown as its preview label in the
74
+ * Localess editor; restricted to the field names in `fields` (falls back to plain `string`
75
+ * when `fields` is omitted)
76
+ * - `fields` — ordered list of `defineField(...)` results and/or raw field literals; see
77
+ * `defineField` for the per-kind property reference
78
+ *
35
79
  * @param definition the schema definition; `type` selects ROOT or NODE
36
80
  * @returns the definition with references normalized to id strings, literal types preserved
37
81
  * @throws Error on duplicate field names — a programming error, not a validation concern
82
+ *
83
+ * @example
84
+ * const Button = defineSchema({
85
+ * id: 'Button',
86
+ * type: 'NODE',
87
+ * previewField: 'label',
88
+ * fields: [defineField({ name: 'label', kind: 'TEXT', required: true })],
89
+ * });
38
90
  */
39
91
  function defineSchema(definition) {
40
92
  const seen = /* @__PURE__ */ new Set();
package/dist/infer.d.ts CHANGED
@@ -9,11 +9,9 @@ type SchemasOf<C> = C extends {
9
9
  type FindById<C, Id> = Extract<SchemasOf<C>, {
10
10
  id: Id;
11
11
  }>;
12
- type NodeIds<C> = [
13
- Extract<SchemasOf<C>, {
14
- type: 'NODE';
15
- }>
16
- ] extends [never] ? never : Extract<SchemasOf<C>, {
12
+ type NodeIds<C> = [Extract<SchemasOf<C>, {
13
+ type: 'NODE';
14
+ }>] extends [never] ? never : Extract<SchemasOf<C>, {
17
15
  type: 'NODE';
18
16
  }> extends {
19
17
  id: infer Id extends string;
@@ -93,11 +91,9 @@ export type InferContent<S, C> = S extends {
93
91
  _id: string;
94
92
  _schema: Id;
95
93
  } & FieldsObject<S, C>> : never;
96
- type RootIds<C> = [
97
- Extract<SchemasOf<C>, {
98
- type: 'ROOT';
99
- }>
100
- ] extends [never] ? never : Extract<SchemasOf<C>, {
94
+ type RootIds<C> = [Extract<SchemasOf<C>, {
95
+ type: 'ROOT';
96
+ }>] extends [never] ? never : Extract<SchemasOf<C>, {
101
97
  type: 'ROOT';
102
98
  }> extends {
103
99
  id: infer Id extends string;
package/package.json CHANGED
@@ -1,57 +1,57 @@
1
- {
2
- "name": "@localess/schema",
3
- "version": "4.0.0",
4
- "description": "Programmatic schema definitions for Localess with TypeScript content type inference.",
5
- "keywords": [
6
- "localess",
7
- "sdk",
8
- "schema",
9
- "cms",
10
- "javascript",
11
- "typescript"
12
- ],
13
- "author": "Lessify",
14
- "homepage": "https://github.com/Lessify/localess-js",
15
- "sideEffects": false,
16
- "files": [
17
- "dist",
18
- "SKILL.md"
19
- ],
20
- "main": "dist/index.js",
21
- "module": "dist/index.mjs",
22
- "types": "dist/index.d.ts",
23
- "exports": {
24
- ".": {
25
- "types": "./dist/index.d.ts",
26
- "import": "./dist/index.mjs",
27
- "require": "./dist/index.js"
28
- }
29
- },
30
- "repository": {
31
- "type": "git",
32
- "url": "git+https://github.com/Lessify/localess-js.git",
33
- "directory": "packages/schema"
34
- },
35
- "bugs": {
36
- "url": "https://github.com/Lessify/localess-js/issues"
37
- },
38
- "scripts": {
39
- "build": "vite build",
40
- "test": "vitest run",
41
- "test:watch": "vitest",
42
- "test:coverage": "vitest run --coverage"
43
- },
44
- "license": "MIT",
45
- "dependencies": {
46
- "@localess/model": "*"
47
- },
48
- "devDependencies": {
49
- "@types/node": "^24",
50
- "typescript": "^5.9.3",
51
- "vite": "^8.0.16",
52
- "vite-plugin-dts": "^5.0.0"
53
- },
54
- "engines": {
55
- "node": ">= 24.0.0"
56
- }
57
- }
1
+ {
2
+ "name": "@localess/schema",
3
+ "version": "4.0.1-dev.20260915135536",
4
+ "description": "Programmatic schema definitions for Localess with TypeScript content type inference.",
5
+ "keywords": [
6
+ "localess",
7
+ "sdk",
8
+ "schema",
9
+ "cms",
10
+ "javascript",
11
+ "typescript"
12
+ ],
13
+ "author": "Lessify",
14
+ "homepage": "https://github.com/Lessify/localess-js",
15
+ "sideEffects": false,
16
+ "files": [
17
+ "dist",
18
+ "SKILL.md"
19
+ ],
20
+ "main": "dist/index.js",
21
+ "module": "dist/index.mjs",
22
+ "types": "dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.mjs",
27
+ "require": "./dist/index.js"
28
+ }
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/Lessify/localess-js.git",
33
+ "directory": "packages/schema"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/Lessify/localess-js/issues"
37
+ },
38
+ "scripts": {
39
+ "build": "vite build",
40
+ "test": "vitest run",
41
+ "test:watch": "vitest",
42
+ "test:coverage": "vitest run --coverage"
43
+ },
44
+ "license": "MIT",
45
+ "dependencies": {
46
+ "@localess/model": "4.0.1-dev.20260915135536"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^24",
50
+ "typescript": "^5.9.3",
51
+ "vite": "^8.0.16",
52
+ "vite-plugin-dts": "^5.0.0"
53
+ },
54
+ "engines": {
55
+ "node": ">= 24.0.0"
56
+ }
57
+ }