@localess/schema 4.0.0-dev.20260905071322

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 ADDED
@@ -0,0 +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.
@@ -0,0 +1,164 @@
1
+ import { SchemaComponentExport, SchemaEnumExport, SchemaEnumValue, SchemaField, SchemaFieldKind } from './models';
2
+ type Prettify<T> = {
3
+ [K in keyof T]: T[K];
4
+ } & {};
5
+ /**
6
+ * Deep-readonly view of a wire type. Definitions created with `const` type parameters carry
7
+ * readonly arrays/tuples, which the mutable wire types would reject — every authoring-facing
8
+ * type is therefore expressed through this.
9
+ */
10
+ type DeepReadonly<T> = T extends readonly (infer U)[] ? readonly DeepReadonly<U>[] : T extends object ? {
11
+ readonly [K in keyof T]: DeepReadonly<T[K]>;
12
+ } : T;
13
+ /** Result of defineEnum — readonly view of the wire SchemaEnumExport. */
14
+ export type EnumDefinition = DeepReadonly<SchemaEnumExport>;
15
+ /** Result of defineSchema — readonly view of the wire SchemaComponentExport. */
16
+ export type ComponentDefinition = DeepReadonly<SchemaComponentExport>;
17
+ /** Any schema definition. */
18
+ export type SchemaDefinition = ComponentDefinition | EnumDefinition;
19
+ /** A registry of schema definitions — the unit the CLI loads and inference resolves against. */
20
+ export interface LocalessSchemaConfig {
21
+ schemas: readonly SchemaDefinition[];
22
+ }
23
+ type FieldInputOf<F> = F extends {
24
+ kind: 'OPTION' | 'OPTIONS';
25
+ } ? Omit<DeepReadonly<F>, 'source'> & {
26
+ readonly source: string | EnumDefinition;
27
+ } : F extends {
28
+ kind: 'SCHEMA' | 'SCHEMAS';
29
+ } ? Omit<DeepReadonly<F>, 'schemas'> & {
30
+ readonly schemas?: readonly (string | ComponentDefinition)[];
31
+ } : DeepReadonly<F>;
32
+ /**
33
+ * A field as authored: OPTION/OPTIONS source and SCHEMA/SCHEMAS schemas accept by-value refs.
34
+ *
35
+ * Known limitation: TypeScript's excess-property check does not apply to object literals inside
36
+ * an array passed through a `const`-inferred generic parameter (only to literals checked directly
37
+ * against a declared type), so a stray property from a different kind (e.g. `maxLength` on a
38
+ * `NUMBER` field) inside `defineSchema({ fields: [...] })` will not be flagged at the call site.
39
+ * Missing required properties (e.g. omitting `source` on `OPTION`) are still caught, since that is
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
+ */
44
+ export type SchemaFieldInput = FieldInputOf<SchemaField>;
45
+ /** Authoring input for defineEnum: type is injected by the helper. */
46
+ export interface EnumDefinitionInput {
47
+ id: string;
48
+ displayName?: string;
49
+ description?: string;
50
+ labels?: readonly string[];
51
+ values?: readonly SchemaEnumValue[];
52
+ }
53
+ /** Authoring input for defineSchema. */
54
+ export interface ComponentDefinitionInput {
55
+ id: string;
56
+ type: 'ROOT' | 'NODE';
57
+ displayName?: string;
58
+ description?: string;
59
+ labels?: readonly string[];
60
+ previewField?: string;
61
+ fields?: readonly SchemaFieldInput[];
62
+ }
63
+ type NormalizeRef<S> = S extends {
64
+ readonly id: infer Id extends string;
65
+ } ? Id : S;
66
+ type NormalizeRefs<A> = A extends readonly unknown[] ? {
67
+ -readonly [K in keyof A]: NormalizeRef<A[K]>;
68
+ } : A;
69
+ type NormalizeField<F> = Prettify<Omit<F, 'source' | 'schemas'> & (F extends {
70
+ source: infer S;
71
+ } ? {
72
+ source: NormalizeRef<S>;
73
+ } : unknown) & (F extends {
74
+ schemas: infer A;
75
+ } ? {
76
+ schemas: NormalizeRefs<A>;
77
+ } : unknown)>;
78
+ type DefinedEnum<TId extends string, TValues> = Prettify<{
79
+ id: TId;
80
+ type: 'ENUM';
81
+ displayName?: string;
82
+ description?: string;
83
+ labels?: readonly string[];
84
+ } & (TValues extends readonly unknown[] ? {
85
+ values: TValues;
86
+ } : unknown)>;
87
+ type DefinedComponent<TId extends string, TType extends 'ROOT' | 'NODE', TFields> = Prettify<{
88
+ id: TId;
89
+ type: TType;
90
+ displayName?: string;
91
+ description?: string;
92
+ labels?: readonly string[];
93
+ previewField?: string;
94
+ } & (TFields extends readonly unknown[] ? {
95
+ fields: {
96
+ -readonly [K in keyof TFields]: NormalizeField<TFields[K]>;
97
+ };
98
+ } : unknown)>;
99
+ /**
100
+ * Define an ENUM schema. Identity function apart from injecting `type: 'ENUM'`;
101
+ * exists to preserve literal types for inference.
102
+ *
103
+ * @param definition the enum definition (id, values, optional display metadata)
104
+ * @returns the definition with `type: 'ENUM'`, literal types preserved
105
+ */
106
+ export declare function defineEnum<const TId extends string, const TValues extends readonly SchemaEnumValue[] | undefined = undefined>(definition: {
107
+ id: TId;
108
+ displayName?: string;
109
+ description?: string;
110
+ labels?: readonly string[];
111
+ values?: TValues;
112
+ }): DefinedEnum<TId, TValues>;
113
+ /**
114
+ * Define a single field, narrowing it to the extras valid for its `kind` and catching a stray
115
+ * property from the wrong kind at the call site (e.g. `maxLength` on a `NUMBER` field) — something
116
+ * a bare field literal inside `defineSchema({ fields: [...] })` cannot do. Optional: `defineSchema`
117
+ * accepts raw field literals and `defineField(...)` results interchangeably in the same `fields`
118
+ * array. A near-identity function like `defineEnum`/`defineSchema` — by-value ref normalization
119
+ * (`source`, `schemas`) still happens exclusively in `defineSchema`, applied uniformly regardless
120
+ * of a field's origin. See `docs/decisions/008-schema-package.md`.
121
+ *
122
+ * @param field the field definition; `kind` selects which extra properties are allowed
123
+ * @returns the field unchanged, with `name`/`kind`/extras narrowed to their literal types
124
+ */
125
+ export declare function defineField<const TKind extends SchemaFieldKind, const TName extends string, const TField extends Omit<Extract<SchemaFieldInput, {
126
+ kind: TKind;
127
+ }>, 'name' | 'kind'> = Omit<Extract<SchemaFieldInput, {
128
+ kind: TKind;
129
+ }>, 'name' | 'kind'>>(field: {
130
+ kind: TKind;
131
+ name: TName;
132
+ } & TField): Prettify<{
133
+ name: TName;
134
+ kind: TKind;
135
+ } & TField>;
136
+ /**
137
+ * Define a ROOT (content type) or NODE (nested component) schema.
138
+ * Normalizes by-value references (enum in `source`, components in `schemas`) to their id strings;
139
+ * the returned type keeps those ids as literals for inference.
140
+ *
141
+ * @param definition the schema definition; `type` selects ROOT or NODE
142
+ * @returns the definition with references normalized to id strings, literal types preserved
143
+ * @throws Error on duplicate field names — a programming error, not a validation concern
144
+ */
145
+ export declare function defineSchema<const TId extends string, const TType extends 'ROOT' | 'NODE', const TFields extends readonly SchemaFieldInput[] | undefined = undefined>(definition: {
146
+ id: TId;
147
+ type: TType;
148
+ displayName?: string;
149
+ description?: string;
150
+ labels?: readonly string[];
151
+ previewField?: string;
152
+ fields?: TFields;
153
+ }): DefinedComponent<TId, TType, TFields>;
154
+ /**
155
+ * Register schemas into a config — the unit the CLI pushes and type inference resolves against.
156
+ *
157
+ * @param config object with the full list of schema definitions
158
+ * @returns the config unchanged, literal types preserved
159
+ * @throws Error on duplicate schema ids
160
+ */
161
+ export declare function defineConfig<const T extends {
162
+ schemas: readonly SchemaDefinition[];
163
+ }>(config: T): T;
164
+ export {};
@@ -0,0 +1,7 @@
1
+ import { LocalessSchemaConfig } from './define';
2
+ import { SchemaExport } from './models';
3
+ /**
4
+ * Convert a schema config into the Localess wire format (SchemaExport[]) accepted by
5
+ * the push endpoint and produced by the pull endpoint. Pure data transformation.
6
+ */
7
+ export declare function toSchemaExport(config: LocalessSchemaConfig): SchemaExport[];
@@ -0,0 +1,8 @@
1
+ export type { ComponentDefinition, ComponentDefinitionInput, EnumDefinition, EnumDefinitionInput, LocalessSchemaConfig, SchemaDefinition, SchemaFieldInput, } from './define';
2
+ export { defineConfig, defineEnum, defineField, defineSchema } from './define';
3
+ export { toSchemaExport } from './export';
4
+ export type { InferContent, InferContentData, InferEnum } from './infer';
5
+ export type { AssetFileType, SchemaComponentExport, SchemaEnumExport, SchemaEnumValue, SchemaExport, SchemaField, SchemaFieldAsset, SchemaFieldAssets, SchemaFieldBase, SchemaFieldBoolean, SchemaFieldColor, SchemaFieldDate, SchemaFieldDateTime, SchemaFieldKind, SchemaFieldLink, SchemaFieldMarkdown, SchemaFieldNumber, SchemaFieldOption, SchemaFieldOptions, SchemaFieldReference, SchemaFieldReferences, SchemaFieldRichText, SchemaFieldSchema, SchemaFieldSchemas, SchemaFieldText, SchemaFieldTextarea, SchemaType, } from './models';
6
+ export type { ValidationIssue, ValidationResult } from './validate';
7
+ export { validate } from './validate';
8
+ export type { ContentAsset, ContentLink, ContentReference, ContentRichText } from '@localess/model';
package/dist/index.js ADDED
@@ -0,0 +1,162 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/define.ts
3
+ /**
4
+ * Define an ENUM schema. Identity function apart from injecting `type: 'ENUM'`;
5
+ * exists to preserve literal types for inference.
6
+ *
7
+ * @param definition the enum definition (id, values, optional display metadata)
8
+ * @returns the definition with `type: 'ENUM'`, literal types preserved
9
+ */
10
+ function defineEnum(definition) {
11
+ return {
12
+ ...definition,
13
+ type: "ENUM"
14
+ };
15
+ }
16
+ /**
17
+ * Define a single field, narrowing it to the extras valid for its `kind` and catching a stray
18
+ * property from the wrong kind at the call site (e.g. `maxLength` on a `NUMBER` field) — something
19
+ * a bare field literal inside `defineSchema({ fields: [...] })` cannot do. Optional: `defineSchema`
20
+ * accepts raw field literals and `defineField(...)` results interchangeably in the same `fields`
21
+ * array. A near-identity function like `defineEnum`/`defineSchema` — by-value ref normalization
22
+ * (`source`, `schemas`) still happens exclusively in `defineSchema`, applied uniformly regardless
23
+ * of a field's origin. See `docs/decisions/008-schema-package.md`.
24
+ *
25
+ * @param field the field definition; `kind` selects which extra properties are allowed
26
+ * @returns the field unchanged, with `name`/`kind`/extras narrowed to their literal types
27
+ */
28
+ function defineField(field) {
29
+ return field;
30
+ }
31
+ /**
32
+ * Define a ROOT (content type) or NODE (nested component) schema.
33
+ * Normalizes by-value references (enum in `source`, components in `schemas`) to their id strings;
34
+ * the returned type keeps those ids as literals for inference.
35
+ *
36
+ * @param definition the schema definition; `type` selects ROOT or NODE
37
+ * @returns the definition with references normalized to id strings, literal types preserved
38
+ * @throws Error on duplicate field names — a programming error, not a validation concern
39
+ */
40
+ function defineSchema(definition) {
41
+ const seen = /* @__PURE__ */ new Set();
42
+ const fields = definition.fields?.map((field) => {
43
+ if (seen.has(field.name)) throw new Error(`[localess/schema] Duplicate field name '${field.name}' in schema '${definition.id}'`);
44
+ seen.add(field.name);
45
+ const out = { ...field };
46
+ if ("source" in field && typeof field.source === "object" && field.source !== null) out.source = field.source.id;
47
+ if ("schemas" in field && Array.isArray(field.schemas)) out.schemas = field.schemas.map((ref) => typeof ref === "object" && ref !== null ? ref.id : ref);
48
+ return out;
49
+ });
50
+ return {
51
+ ...definition,
52
+ ...fields ? { fields } : {}
53
+ };
54
+ }
55
+ /**
56
+ * Register schemas into a config — the unit the CLI pushes and type inference resolves against.
57
+ *
58
+ * @param config object with the full list of schema definitions
59
+ * @returns the config unchanged, literal types preserved
60
+ * @throws Error on duplicate schema ids
61
+ */
62
+ function defineConfig(config) {
63
+ const seen = /* @__PURE__ */ new Set();
64
+ for (const schema of config.schemas) {
65
+ if (seen.has(schema.id)) throw new Error(`[localess/schema] Duplicate schema id '${schema.id}' in config`);
66
+ seen.add(schema.id);
67
+ }
68
+ return config;
69
+ }
70
+ //#endregion
71
+ //#region src/export.ts
72
+ /**
73
+ * Convert a schema config into the Localess wire format (SchemaExport[]) accepted by
74
+ * the push endpoint and produced by the pull endpoint. Pure data transformation.
75
+ */
76
+ function toSchemaExport(config) {
77
+ return config.schemas.map((schema) => stripUndefined({ ...schema }));
78
+ }
79
+ function stripUndefined(value) {
80
+ for (const key of Object.keys(value)) if (value[key] === void 0) delete value[key];
81
+ else if (Array.isArray(value[key])) value[key] = value[key].map((item) => item !== null && typeof item === "object" ? stripUndefined({ ...item }) : item);
82
+ return value;
83
+ }
84
+ //#endregion
85
+ //#region src/validate.ts
86
+ var SCHEMA_ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9]+$/;
87
+ var FIELD_NAME_PATTERN = /^[a-z][a-zA-Z0-9_]*[a-zA-Z0-9]$/;
88
+ var ENUM_VALUE_PATTERN = /^[a-zA-Z]$|^[a-zA-Z][a-zA-Z0-9-_]*[a-zA-Z0-9]$/;
89
+ var RESERVED_SCHEMA_IDS = [
90
+ "Translations",
91
+ "Links",
92
+ "ContentMetadata",
93
+ "ContentReference",
94
+ "ContentRichText",
95
+ "ContentLink",
96
+ "ContentData",
97
+ "ContentAsset",
98
+ "Content"
99
+ ];
100
+ var RESERVED_FIELD_NAMES = ["_id", "_schema"];
101
+ /**
102
+ * Validate a schema config against the Localess authoring rules (ID/name patterns, reserved
103
+ * names, length limits, reference resolution). Non-throwing; `ok` is false only when at least
104
+ * one error-severity issue is present — warnings alone don't fail validation.
105
+ */
106
+ function validate(config) {
107
+ const issues = [];
108
+ const error = (code, path, message) => issues.push({
109
+ severity: "error",
110
+ code,
111
+ path,
112
+ message
113
+ });
114
+ const byId = /* @__PURE__ */ new Map();
115
+ for (const schema of config.schemas) byId.set(schema.id, schema);
116
+ for (const schema of config.schemas) {
117
+ const path = schema.id;
118
+ if (schema.id.length < 2 || schema.id.length > 50 || !SCHEMA_ID_PATTERN.test(schema.id)) error("schema/invalid-id", path, `Schema id '${schema.id}' must match ${SCHEMA_ID_PATTERN} and be 2-50 characters`);
119
+ if (RESERVED_SCHEMA_IDS.some((it) => it.toLowerCase() === schema.id.toLowerCase())) error("schema/reserved-id", path, `Schema id '${schema.id}' is reserved`);
120
+ if (schema.displayName && schema.displayName.length > 50) error("schema/display-name-too-long", path, "displayName exceeds 50 characters");
121
+ if (schema.description && schema.description.length > 250) error("schema/description-too-long", path, "description exceeds 250 characters");
122
+ for (const label of schema.labels ?? []) if (label.length < 2 || label.length > 50 || label.includes(" ")) error("schema/invalid-label", path, `Label '${label}' must be 2-50 characters without spaces`);
123
+ if (schema.type === "ENUM") {
124
+ for (const value of schema.values ?? []) {
125
+ if (value.name.length < 1 || value.name.length > 50) error("enum/invalid-value-name", `${path}.${value.name}`, "Enum value name must be 1-50 characters");
126
+ if (value.value.length < 1 || value.value.length > 50 || !ENUM_VALUE_PATTERN.test(value.value)) error("enum/invalid-value", `${path}.${value.name}`, `Enum value '${value.value}' is invalid`);
127
+ }
128
+ continue;
129
+ }
130
+ const fieldNames = new Set((schema.fields ?? []).map((it) => it.name));
131
+ if (schema.previewField && !fieldNames.has(schema.previewField)) error("schema/unknown-preview-field", path, `previewField '${schema.previewField}' does not exist on '${schema.id}'`);
132
+ for (const field of schema.fields ?? []) {
133
+ const fieldPath = `${path}.${field.name}`;
134
+ if (field.name.length < 2 || field.name.length > 30 || !FIELD_NAME_PATTERN.test(field.name) || field.name.includes("_i18n_")) error("field/invalid-name", fieldPath, `Field name '${field.name}' must be camelCase, 2-30 characters, without '_i18n_'`);
135
+ if (RESERVED_FIELD_NAMES.some((it) => it.toLowerCase() === field.name.toLowerCase())) error("field/reserved-name", fieldPath, `Field name '${field.name}' is reserved`);
136
+ if (field.displayName && field.displayName.length > 30) error("field/display-name-too-long", fieldPath, "Field displayName exceeds 30 characters");
137
+ if (field.description && field.description.length > 250) error("field/description-too-long", fieldPath, "Field description exceeds 250 characters");
138
+ if (field.defaultValue && field.defaultValue.length > 250) error("field/default-value-too-long", fieldPath, "Field defaultValue exceeds 250 characters");
139
+ if (field.kind === "OPTION" || field.kind === "OPTIONS") {
140
+ const source = byId.get(field.source);
141
+ if (!source) error("field/unresolved-source", fieldPath, `source '${field.source}' does not resolve to a schema in the config`);
142
+ else if (source.type !== "ENUM") error("field/source-not-enum", fieldPath, `source '${field.source}' must be an ENUM schema`);
143
+ }
144
+ if (field.kind === "SCHEMA" || field.kind === "SCHEMAS") for (const ref of field.schemas ?? []) {
145
+ const target = byId.get(ref);
146
+ if (!target) error("field/unresolved-schema-ref", fieldPath, `schemas ref '${ref}' does not resolve to a schema in the config`);
147
+ else if (target.type === "ENUM") error("field/schema-ref-is-enum", fieldPath, `schemas ref '${ref}' must be a ROOT or NODE schema, not an ENUM`);
148
+ }
149
+ }
150
+ }
151
+ return {
152
+ ok: !issues.some((it) => it.severity === "error"),
153
+ issues
154
+ };
155
+ }
156
+ //#endregion
157
+ exports.defineConfig = defineConfig;
158
+ exports.defineEnum = defineEnum;
159
+ exports.defineField = defineField;
160
+ exports.defineSchema = defineSchema;
161
+ exports.toSchemaExport = toSchemaExport;
162
+ exports.validate = validate;
package/dist/index.mjs ADDED
@@ -0,0 +1,156 @@
1
+ //#region src/define.ts
2
+ /**
3
+ * Define an ENUM schema. Identity function apart from injecting `type: 'ENUM'`;
4
+ * exists to preserve literal types for inference.
5
+ *
6
+ * @param definition the enum definition (id, values, optional display metadata)
7
+ * @returns the definition with `type: 'ENUM'`, literal types preserved
8
+ */
9
+ function defineEnum(definition) {
10
+ return {
11
+ ...definition,
12
+ type: "ENUM"
13
+ };
14
+ }
15
+ /**
16
+ * Define a single field, narrowing it to the extras valid for its `kind` and catching a stray
17
+ * property from the wrong kind at the call site (e.g. `maxLength` on a `NUMBER` field) — something
18
+ * a bare field literal inside `defineSchema({ fields: [...] })` cannot do. Optional: `defineSchema`
19
+ * accepts raw field literals and `defineField(...)` results interchangeably in the same `fields`
20
+ * array. A near-identity function like `defineEnum`/`defineSchema` — by-value ref normalization
21
+ * (`source`, `schemas`) still happens exclusively in `defineSchema`, applied uniformly regardless
22
+ * of a field's origin. See `docs/decisions/008-schema-package.md`.
23
+ *
24
+ * @param field the field definition; `kind` selects which extra properties are allowed
25
+ * @returns the field unchanged, with `name`/`kind`/extras narrowed to their literal types
26
+ */
27
+ function defineField(field) {
28
+ return field;
29
+ }
30
+ /**
31
+ * Define a ROOT (content type) or NODE (nested component) schema.
32
+ * Normalizes by-value references (enum in `source`, components in `schemas`) to their id strings;
33
+ * the returned type keeps those ids as literals for inference.
34
+ *
35
+ * @param definition the schema definition; `type` selects ROOT or NODE
36
+ * @returns the definition with references normalized to id strings, literal types preserved
37
+ * @throws Error on duplicate field names — a programming error, not a validation concern
38
+ */
39
+ function defineSchema(definition) {
40
+ const seen = /* @__PURE__ */ new Set();
41
+ const fields = definition.fields?.map((field) => {
42
+ if (seen.has(field.name)) throw new Error(`[localess/schema] Duplicate field name '${field.name}' in schema '${definition.id}'`);
43
+ seen.add(field.name);
44
+ const out = { ...field };
45
+ if ("source" in field && typeof field.source === "object" && field.source !== null) out.source = field.source.id;
46
+ if ("schemas" in field && Array.isArray(field.schemas)) out.schemas = field.schemas.map((ref) => typeof ref === "object" && ref !== null ? ref.id : ref);
47
+ return out;
48
+ });
49
+ return {
50
+ ...definition,
51
+ ...fields ? { fields } : {}
52
+ };
53
+ }
54
+ /**
55
+ * Register schemas into a config — the unit the CLI pushes and type inference resolves against.
56
+ *
57
+ * @param config object with the full list of schema definitions
58
+ * @returns the config unchanged, literal types preserved
59
+ * @throws Error on duplicate schema ids
60
+ */
61
+ function defineConfig(config) {
62
+ const seen = /* @__PURE__ */ new Set();
63
+ for (const schema of config.schemas) {
64
+ if (seen.has(schema.id)) throw new Error(`[localess/schema] Duplicate schema id '${schema.id}' in config`);
65
+ seen.add(schema.id);
66
+ }
67
+ return config;
68
+ }
69
+ //#endregion
70
+ //#region src/export.ts
71
+ /**
72
+ * Convert a schema config into the Localess wire format (SchemaExport[]) accepted by
73
+ * the push endpoint and produced by the pull endpoint. Pure data transformation.
74
+ */
75
+ function toSchemaExport(config) {
76
+ return config.schemas.map((schema) => stripUndefined({ ...schema }));
77
+ }
78
+ function stripUndefined(value) {
79
+ for (const key of Object.keys(value)) if (value[key] === void 0) delete value[key];
80
+ else if (Array.isArray(value[key])) value[key] = value[key].map((item) => item !== null && typeof item === "object" ? stripUndefined({ ...item }) : item);
81
+ return value;
82
+ }
83
+ //#endregion
84
+ //#region src/validate.ts
85
+ var SCHEMA_ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9]+$/;
86
+ var FIELD_NAME_PATTERN = /^[a-z][a-zA-Z0-9_]*[a-zA-Z0-9]$/;
87
+ var ENUM_VALUE_PATTERN = /^[a-zA-Z]$|^[a-zA-Z][a-zA-Z0-9-_]*[a-zA-Z0-9]$/;
88
+ var RESERVED_SCHEMA_IDS = [
89
+ "Translations",
90
+ "Links",
91
+ "ContentMetadata",
92
+ "ContentReference",
93
+ "ContentRichText",
94
+ "ContentLink",
95
+ "ContentData",
96
+ "ContentAsset",
97
+ "Content"
98
+ ];
99
+ var RESERVED_FIELD_NAMES = ["_id", "_schema"];
100
+ /**
101
+ * Validate a schema config against the Localess authoring rules (ID/name patterns, reserved
102
+ * names, length limits, reference resolution). Non-throwing; `ok` is false only when at least
103
+ * one error-severity issue is present — warnings alone don't fail validation.
104
+ */
105
+ function validate(config) {
106
+ const issues = [];
107
+ const error = (code, path, message) => issues.push({
108
+ severity: "error",
109
+ code,
110
+ path,
111
+ message
112
+ });
113
+ const byId = /* @__PURE__ */ new Map();
114
+ for (const schema of config.schemas) byId.set(schema.id, schema);
115
+ for (const schema of config.schemas) {
116
+ const path = schema.id;
117
+ if (schema.id.length < 2 || schema.id.length > 50 || !SCHEMA_ID_PATTERN.test(schema.id)) error("schema/invalid-id", path, `Schema id '${schema.id}' must match ${SCHEMA_ID_PATTERN} and be 2-50 characters`);
118
+ if (RESERVED_SCHEMA_IDS.some((it) => it.toLowerCase() === schema.id.toLowerCase())) error("schema/reserved-id", path, `Schema id '${schema.id}' is reserved`);
119
+ if (schema.displayName && schema.displayName.length > 50) error("schema/display-name-too-long", path, "displayName exceeds 50 characters");
120
+ if (schema.description && schema.description.length > 250) error("schema/description-too-long", path, "description exceeds 250 characters");
121
+ for (const label of schema.labels ?? []) if (label.length < 2 || label.length > 50 || label.includes(" ")) error("schema/invalid-label", path, `Label '${label}' must be 2-50 characters without spaces`);
122
+ if (schema.type === "ENUM") {
123
+ for (const value of schema.values ?? []) {
124
+ if (value.name.length < 1 || value.name.length > 50) error("enum/invalid-value-name", `${path}.${value.name}`, "Enum value name must be 1-50 characters");
125
+ if (value.value.length < 1 || value.value.length > 50 || !ENUM_VALUE_PATTERN.test(value.value)) error("enum/invalid-value", `${path}.${value.name}`, `Enum value '${value.value}' is invalid`);
126
+ }
127
+ continue;
128
+ }
129
+ const fieldNames = new Set((schema.fields ?? []).map((it) => it.name));
130
+ if (schema.previewField && !fieldNames.has(schema.previewField)) error("schema/unknown-preview-field", path, `previewField '${schema.previewField}' does not exist on '${schema.id}'`);
131
+ for (const field of schema.fields ?? []) {
132
+ const fieldPath = `${path}.${field.name}`;
133
+ if (field.name.length < 2 || field.name.length > 30 || !FIELD_NAME_PATTERN.test(field.name) || field.name.includes("_i18n_")) error("field/invalid-name", fieldPath, `Field name '${field.name}' must be camelCase, 2-30 characters, without '_i18n_'`);
134
+ if (RESERVED_FIELD_NAMES.some((it) => it.toLowerCase() === field.name.toLowerCase())) error("field/reserved-name", fieldPath, `Field name '${field.name}' is reserved`);
135
+ if (field.displayName && field.displayName.length > 30) error("field/display-name-too-long", fieldPath, "Field displayName exceeds 30 characters");
136
+ if (field.description && field.description.length > 250) error("field/description-too-long", fieldPath, "Field description exceeds 250 characters");
137
+ if (field.defaultValue && field.defaultValue.length > 250) error("field/default-value-too-long", fieldPath, "Field defaultValue exceeds 250 characters");
138
+ if (field.kind === "OPTION" || field.kind === "OPTIONS") {
139
+ const source = byId.get(field.source);
140
+ if (!source) error("field/unresolved-source", fieldPath, `source '${field.source}' does not resolve to a schema in the config`);
141
+ else if (source.type !== "ENUM") error("field/source-not-enum", fieldPath, `source '${field.source}' must be an ENUM schema`);
142
+ }
143
+ if (field.kind === "SCHEMA" || field.kind === "SCHEMAS") for (const ref of field.schemas ?? []) {
144
+ const target = byId.get(ref);
145
+ if (!target) error("field/unresolved-schema-ref", fieldPath, `schemas ref '${ref}' does not resolve to a schema in the config`);
146
+ else if (target.type === "ENUM") error("field/schema-ref-is-enum", fieldPath, `schemas ref '${ref}' must be a ROOT or NODE schema, not an ENUM`);
147
+ }
148
+ }
149
+ }
150
+ return {
151
+ ok: !issues.some((it) => it.severity === "error"),
152
+ issues
153
+ };
154
+ }
155
+ //#endregion
156
+ export { defineConfig, defineEnum, defineField, defineSchema, toSchemaExport, validate };
@@ -0,0 +1,103 @@
1
+ import { ContentAsset, ContentLink, ContentReference, ContentRichText } from '@localess/model';
2
+ import { SchemaEnumValue } from './models';
3
+ type Prettify<T> = {
4
+ [K in keyof T]: T[K];
5
+ } & {};
6
+ type SchemasOf<C> = C extends {
7
+ schemas: readonly (infer D)[];
8
+ } ? D : never;
9
+ type FindById<C, Id> = Extract<SchemasOf<C>, {
10
+ id: Id;
11
+ }>;
12
+ type NodeIds<C> = [Extract<SchemasOf<C>, {
13
+ type: 'NODE';
14
+ }>] extends [never] ? never : Extract<SchemasOf<C>, {
15
+ type: 'NODE';
16
+ }> extends {
17
+ id: infer Id extends string;
18
+ } ? Id : never;
19
+ type EnumValuesUnion<E> = E extends {
20
+ values: readonly SchemaEnumValue[];
21
+ } ? E extends {
22
+ values: readonly {
23
+ value: infer V extends string;
24
+ }[];
25
+ } ? V : string : string;
26
+ /** Literal union of an ENUM definition's values (falls back to `string` when values are absent). */
27
+ export type InferEnum<E> = EnumValuesUnion<E>;
28
+ type ResolveEnum<Id extends string, C> = [FindById<C, Id>] extends [never] ? string : EnumValuesUnion<FindById<C, Id>>;
29
+ type AllowedIds<F, C> = F extends {
30
+ schemas: readonly (infer Id extends string)[];
31
+ } ? Id : NodeIds<C>;
32
+ type ResolveSchemaContent<F, C> = [AllowedIds<F, C>] extends [never] ? {
33
+ _id: string;
34
+ _schema: string;
35
+ } : ContentByIds<AllowedIds<F, C>, C>;
36
+ type ContentByIds<Id extends string, C> = Id extends unknown ? InferContent<FindById<C, Id>, C> : never;
37
+ type FieldValue<F, C> = F extends {
38
+ kind: 'TEXT' | 'TEXTAREA' | 'MARKDOWN' | 'COLOR' | 'DATE' | 'DATETIME';
39
+ } ? string : F extends {
40
+ kind: 'NUMBER';
41
+ } ? number : F extends {
42
+ kind: 'BOOLEAN';
43
+ } ? boolean : F extends {
44
+ kind: 'RICH_TEXT';
45
+ } ? ContentRichText : F extends {
46
+ kind: 'LINK';
47
+ } ? ContentLink : F extends {
48
+ kind: 'ASSET';
49
+ } ? ContentAsset : F extends {
50
+ kind: 'ASSETS';
51
+ } ? ContentAsset[] : F extends {
52
+ kind: 'REFERENCE';
53
+ } ? ContentReference : F extends {
54
+ kind: 'REFERENCES';
55
+ } ? ContentReference[] : F extends {
56
+ kind: 'OPTION';
57
+ source: infer S extends string;
58
+ } ? ResolveEnum<S, C> : F extends {
59
+ kind: 'OPTIONS';
60
+ source: infer S extends string;
61
+ } ? ResolveEnum<S, C>[] : F extends {
62
+ kind: 'SCHEMA';
63
+ } ? ResolveSchemaContent<F, C> : F extends {
64
+ kind: 'SCHEMAS';
65
+ } ? ResolveSchemaContent<F, C>[] : unknown;
66
+ type FieldsOf<S> = S extends {
67
+ fields: readonly (infer F)[];
68
+ } ? F : never;
69
+ type RequiredFieldNames<S> = FieldsOf<S> extends infer F ? (F extends {
70
+ required: true;
71
+ name: infer N extends string;
72
+ } ? N : never) : never;
73
+ type OptionalFieldNames<S> = FieldsOf<S> extends infer F ? (F extends {
74
+ required: true;
75
+ } ? never : F extends {
76
+ name: infer N extends string;
77
+ } ? N : never) : never;
78
+ type FieldByName<S, N> = Extract<FieldsOf<S>, {
79
+ name: N;
80
+ }>;
81
+ type FieldsObject<S, C> = Prettify<{
82
+ [N in RequiredFieldNames<S>]: FieldValue<FieldByName<S, N>, C>;
83
+ } & {
84
+ [N in OptionalFieldNames<S>]?: FieldValue<FieldByName<S, N>, C>;
85
+ }>;
86
+ /** Content type of a single ROOT/NODE definition, resolved against config C. */
87
+ export type InferContent<S, C> = S extends {
88
+ type: 'ROOT' | 'NODE';
89
+ id: infer Id extends string;
90
+ } ? Prettify<{
91
+ _id: string;
92
+ _schema: Id;
93
+ } & FieldsObject<S, C>> : never;
94
+ type RootIds<C> = [Extract<SchemasOf<C>, {
95
+ type: 'ROOT';
96
+ }>] extends [never] ? never : Extract<SchemasOf<C>, {
97
+ type: 'ROOT';
98
+ }> extends {
99
+ id: infer Id extends string;
100
+ } ? Id : never;
101
+ /** Union of the content types of every ROOT schema in config C. */
102
+ export type InferContentData<C> = ContentByIds<RootIds<C>, C>;
103
+ export {};
@@ -0,0 +1 @@
1
+ export * from '@localess/model';
@@ -0,0 +1,17 @@
1
+ import { LocalessSchemaConfig } from './define';
2
+ export interface ValidationIssue {
3
+ severity: 'error' | 'warning';
4
+ code: string;
5
+ path: string;
6
+ message: string;
7
+ }
8
+ export interface ValidationResult {
9
+ ok: boolean;
10
+ issues: ValidationIssue[];
11
+ }
12
+ /**
13
+ * Validate a schema config against the Localess authoring rules (ID/name patterns, reserved
14
+ * names, length limits, reference resolution). Non-throwing; `ok` is false only when at least
15
+ * one error-severity issue is present — warnings alone don't fail validation.
16
+ */
17
+ export declare function validate(config: LocalessSchemaConfig): ValidationResult;
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@localess/schema",
3
+ "version": "4.0.0-dev.20260905071322",
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.0-dev.20260905071322"
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
+ }