@localess/schema 4.0.1-dev.20260915143617 → 4.0.1-dev.20260915190418
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/README.md +208 -0
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
<br/>
|
|
2
|
+
<br/>
|
|
3
|
+
<img src="https://github.com/Lessify/localess/wiki/img/logo-adaptive.svg" alt="logo">
|
|
4
|
+
<br/>
|
|
5
|
+
<br/>
|
|
6
|
+
|
|
7
|
+
----
|
|
8
|
+
|
|
9
|
+
# @localess/schema
|
|
10
|
+
|
|
11
|
+
Define your [Localess](https://github.com/Lessify/localess) content schemas **in TypeScript**, keep them in version control, and get the content types of every schema inferred for free.
|
|
12
|
+
|
|
13
|
+
Instead of clicking a content model together in the Studio UI and then hand-writing matching interfaces that drift, you write the schema once. `@localess/cli` pushes it to your space, and `InferContent` derives the exact shape `getContentBySlug()` will return — including nested blocks and enum unions.
|
|
14
|
+
|
|
15
|
+
**Zero external dependencies** — `@localess/model` is the only entry in `dependencies`. See [ADR 008](../../docs/decisions/008-schema-package.md).
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- Node.js >= 24.0.0
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# npm
|
|
25
|
+
npm install @localess/schema
|
|
26
|
+
npm install --save-dev @localess/cli
|
|
27
|
+
|
|
28
|
+
# yarn
|
|
29
|
+
yarn add @localess/schema && yarn add -D @localess/cli
|
|
30
|
+
|
|
31
|
+
# pnpm
|
|
32
|
+
pnpm add @localess/schema && pnpm add -D @localess/cli
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Defining a schema
|
|
38
|
+
|
|
39
|
+
Straight from the [schema playground](../../playgrounds/schema). An enum, a reusable block, and a root document that nests it:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// schemas/button-type.ts
|
|
43
|
+
import { defineEnum } from '@localess/schema';
|
|
44
|
+
|
|
45
|
+
export const ButtonType = defineEnum({
|
|
46
|
+
id: 'ButtonType',
|
|
47
|
+
displayName: 'Button Type',
|
|
48
|
+
description: 'It will define the visual part of a button.',
|
|
49
|
+
labels: ['button'],
|
|
50
|
+
values: [
|
|
51
|
+
{ name: 'Primary', value: 'primary' },
|
|
52
|
+
{ name: 'Secondary', value: 'secondary' },
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
// schemas/button.ts
|
|
59
|
+
import { defineField, defineSchema } from '@localess/schema';
|
|
60
|
+
import { ButtonType } from './button-type';
|
|
61
|
+
|
|
62
|
+
export const Button = defineSchema({
|
|
63
|
+
id: 'Button',
|
|
64
|
+
type: 'NODE',
|
|
65
|
+
displayName: 'Button',
|
|
66
|
+
description: 'A button',
|
|
67
|
+
previewField: 'label',
|
|
68
|
+
fields: [
|
|
69
|
+
defineField({
|
|
70
|
+
name: 'label',
|
|
71
|
+
kind: 'TEXT',
|
|
72
|
+
displayName: 'Label',
|
|
73
|
+
required: true,
|
|
74
|
+
translatable: true,
|
|
75
|
+
defaultValue: 'CTA',
|
|
76
|
+
minLength: 3,
|
|
77
|
+
maxLength: 30,
|
|
78
|
+
}),
|
|
79
|
+
defineField({ name: 'type', kind: 'OPTION', displayName: 'Type', required: true, source: ButtonType }),
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// schemas/page.ts
|
|
86
|
+
import { defineField, defineSchema } from '@localess/schema';
|
|
87
|
+
import { Button } from './button';
|
|
88
|
+
|
|
89
|
+
export const Page = defineSchema({
|
|
90
|
+
id: 'Page',
|
|
91
|
+
type: 'ROOT',
|
|
92
|
+
displayName: 'Page',
|
|
93
|
+
fields: [
|
|
94
|
+
defineField({ name: 'title', kind: 'TEXT', displayName: 'Title', translatable: true }),
|
|
95
|
+
defineField({ name: 'description', kind: 'TEXTAREA', displayName: 'Description', translatable: true }),
|
|
96
|
+
defineField({ name: 'buttons', kind: 'SCHEMAS', displayName: 'Buttons', schemas: [Button] }),
|
|
97
|
+
defineField({ name: 'content', kind: 'RICH_TEXT', displayName: 'Content', translatable: true }),
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Collect them into a config — this is the file the CLI reads:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
// schemas/index.ts
|
|
106
|
+
import { defineConfig } from '@localess/schema';
|
|
107
|
+
import { Button } from './button';
|
|
108
|
+
import { ButtonType } from './button-type';
|
|
109
|
+
import { Page } from './page';
|
|
110
|
+
|
|
111
|
+
export const config = defineConfig({ schemas: [Button, ButtonType, Page] });
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`type: 'ROOT'` marks a schema that can be a document of its own; `type: 'NODE'` is a block only usable inside another schema.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Inferring content types
|
|
119
|
+
|
|
120
|
+
This is the payoff — no hand-written interfaces:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import type { InferContent, InferContentData } from '@localess/schema';
|
|
124
|
+
import { Page } from './schemas/page';
|
|
125
|
+
import { config } from './schemas';
|
|
126
|
+
|
|
127
|
+
/** Unions the content type of every ROOT schema — type a page-fetching function with this. */
|
|
128
|
+
export type ContentData = InferContentData<typeof config>;
|
|
129
|
+
|
|
130
|
+
/** Resolves a single schema, `SCHEMAS`/`SCHEMA` references included. */
|
|
131
|
+
export type PageContent = InferContent<typeof Page, typeof config>;
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`PageContent` now has `title: string`, `buttons: ButtonContent[]`, and `type: 'primary' | 'secondary'` on each button — the enum narrowed to a literal union, resolved through the nesting. Change `minLength` in the schema and nothing breaks; rename a field and every call site fails to compile.
|
|
135
|
+
|
|
136
|
+
Use it with the client:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const content = await client.getContentBySlug<PageContent>('home', { locale: 'en' });
|
|
140
|
+
content.data.buttons[0].type; // 'primary' | 'secondary'
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`InferEnum<typeof ButtonType>` gives you the value union on its own.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Validating before you push
|
|
148
|
+
|
|
149
|
+
`validate(config)` checks ids, field constraints and cross-schema references locally — no network:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { validate } from '@localess/schema';
|
|
153
|
+
import { config } from './schemas';
|
|
154
|
+
|
|
155
|
+
const result = validate(config);
|
|
156
|
+
// → { ok: true, issues: [] }
|
|
157
|
+
|
|
158
|
+
if (!result.ok) {
|
|
159
|
+
for (const issue of result.issues) {
|
|
160
|
+
console.error(`${issue.severity} ${issue.code} at ${issue.path}: ${issue.message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Each issue carries `severity` (`'error' | 'warning'`), a stable `code` such as `schema/invalid-id`, the `path` to the offending schema or field, and a human-readable `message`.
|
|
166
|
+
|
|
167
|
+
`toSchemaExport(config)` produces the wire format the API accepts, if you'd rather push it yourself than use the CLI.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## The CLI workflow
|
|
172
|
+
|
|
173
|
+
[`@localess/cli`](../cli) drives the round trip. From the playground's `package.json`:
|
|
174
|
+
|
|
175
|
+
```jsonc
|
|
176
|
+
{
|
|
177
|
+
"scripts": {
|
|
178
|
+
"schema:validate": "localess schema validate ./src/schemas/index.ts",
|
|
179
|
+
"schema:diff": "localess schema diff ./src/schemas/index.ts",
|
|
180
|
+
"schema:push": "localess schema push ./src/schemas/index.ts",
|
|
181
|
+
"schema:pull": "localess schema pull --path ./src/schemas/pulled",
|
|
182
|
+
"localess:types": "localess type generate --path ./src/localess.types.ts"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`diff` shows what a push would change before it changes it. `pull` goes the other way, generating these definitions from a space you already modelled in the UI — useful for adopting schemas-as-code on an existing project.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Field kinds
|
|
192
|
+
|
|
193
|
+
`TEXT`, `TEXTAREA`, `MARKDOWN`, `RICH_TEXT`, `NUMBER`, `COLOR`, `DATE`, `DATETIME`, `BOOLEAN`, `OPTION`, `OPTIONS`, `LINK`, `REFERENCE`, `REFERENCES`, `ASSET`, `ASSETS`, `SCHEMA`, `SCHEMAS`.
|
|
194
|
+
|
|
195
|
+
Each kind accepts its own options — `minLength`/`maxLength` on text, `source` on `OPTION`, `schemas` on `SCHEMA`/`SCHEMAS`, `fileTypes` on assets — and `defineField` is typed per kind, so an option that doesn't belong is a compile error rather than a runtime surprise.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Related
|
|
200
|
+
|
|
201
|
+
- [`@localess/cli`](../cli) — push, pull, diff, validate, and type generation
|
|
202
|
+
- [`@localess/model`](../model) — the wire types this package emits
|
|
203
|
+
- [docs/schema.md](../../docs/schema.md) — full reference for every field kind and inference rule
|
|
204
|
+
- [CONTRIBUTING.md](./CONTRIBUTING.md) — adding a field kind
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
See the [Localess](https://github.com/Lessify/localess) repository.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@localess/schema",
|
|
3
|
-
"version": "4.0.1-dev.
|
|
3
|
+
"version": "4.0.1-dev.20260915190418",
|
|
4
4
|
"description": "Programmatic schema definitions for Localess with TypeScript content type inference.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"localess",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"license": "MIT",
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@localess/model": "4.0.1-dev.
|
|
46
|
+
"@localess/model": "4.0.1-dev.20260915190418"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/node": "^24",
|