@maroonedog/luq 2.2.0 → 2.3.1
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 +47 -602
- package/dist/builder/compile-declarations.d.ts +7 -1
- package/dist/builder/compile-declarations.js +18 -8
- package/dist/builder/compile-declarations.mjs +18 -8
- package/dist/builder/create-builder.js +2 -3
- package/dist/builder/create-builder.mjs +2 -3
- package/dist/builder/create-field-builder.js +13 -1
- package/dist/builder/create-field-builder.mjs +13 -1
- package/dist/builder/declared-calls-store.d.ts +9 -0
- package/dist/builder/declared-calls-store.js +19 -0
- package/dist/builder/declared-calls-store.mjs +15 -0
- package/dist/builder/field-declared-calls.types.d.ts +6 -0
- package/dist/builder/field-declared-calls.types.js +2 -0
- package/dist/builder/field-declared-calls.types.mjs +1 -0
- package/dist/builder/field-entry.types.d.ts +9 -3
- package/dist/builder/field-options.types.d.ts +26 -0
- package/dist/chain/bundle-paths.types.d.ts +9 -12
- package/dist/chain/chain-node-store.d.ts +5 -0
- package/dist/chain/chain-node-store.js +15 -0
- package/dist/chain/chain-node-store.mjs +11 -0
- package/dist/chain/collect-field-rules.d.ts +13 -2
- package/dist/chain/collect-field-rules.js +10 -3
- package/dist/chain/collect-field-rules.mjs +10 -3
- package/dist/chain/create-chain-node.d.ts +10 -0
- package/dist/chain/create-chain-node.js +19 -9
- package/dist/chain/create-chain-node.mjs +19 -9
- package/dist/chain/create-field-slots.js +9 -1
- package/dist/chain/create-field-slots.mjs +9 -1
- package/dist/chain/declaration-recorder.port.d.ts +31 -0
- package/dist/chain/declaration-recorder.port.js +17 -0
- package/dist/chain/declaration-recorder.port.mjs +13 -0
- package/dist/chain/declared-call.types.d.ts +15 -0
- package/dist/chain/declared-call.types.js +2 -0
- package/dist/chain/declared-call.types.mjs +1 -0
- package/dist/chain/index.d.ts +3 -1
- package/dist/chain/slot-type-guard.d.ts +10 -0
- package/dist/chain/slot-type-guard.js +37 -0
- package/dist/chain/slot-type-guard.mjs +34 -0
- package/dist/compile/compile-array-node.js +1 -0
- package/dist/compile/compile-array-node.mjs +1 -0
- package/dist/compile/compile-field.d.ts +1 -0
- package/dist/compile/compile-field.js +8 -6
- package/dist/compile/compile-field.mjs +8 -6
- package/dist/compile/compile-schema.js +4 -0
- package/dist/compile/compile-schema.mjs +4 -0
- package/dist/compile/group-array-fields.d.ts +1 -0
- package/dist/compile/split-rules-by-kind.js +10 -14
- package/dist/compile/split-rules-by-kind.mjs +10 -14
- package/dist/compile/validation-plan.types.d.ts +19 -6
- package/dist/core/type-erasure.d.ts +36 -30
- package/dist/core/type-erasure.js +36 -30
- package/dist/core/type-erasure.mjs +36 -30
- package/dist/json-schema/build-from-schema.js +8 -1
- package/dist/json-schema/build-from-schema.mjs +8 -1
- package/dist/json-schema/create-structural-context.js +3 -0
- package/dist/json-schema/create-structural-context.mjs +3 -0
- package/dist/json-schema/declare-additional-properties.d.ts +7 -7
- package/dist/json-schema/declare-additional-properties.js +7 -7
- package/dist/json-schema/declare-additional-properties.mjs +7 -7
- package/dist/json-schema/declare-object-keywords.js +4 -4
- package/dist/json-schema/declare-object-keywords.mjs +4 -4
- package/dist/json-schema/follow-json-pointer.d.ts +7 -6
- package/dist/json-schema/follow-json-pointer.js +24 -24
- package/dist/json-schema/follow-json-pointer.mjs +24 -24
- package/dist/json-schema/ref-resolution-error.js +3 -3
- package/dist/json-schema/ref-resolution-error.mjs +3 -3
- package/dist/json-schema/schema-registry.js +12 -11
- package/dist/json-schema/schema-registry.mjs +12 -11
- package/dist/json-schema/uri-reference.js +12 -12
- package/dist/json-schema/uri-reference.mjs +12 -12
- package/dist/path/create-value-writer.js +12 -12
- package/dist/path/create-value-writer.mjs +12 -12
- package/dist/path/reserved-segment.d.ts +16 -16
- package/dist/path/reserved-segment.js +17 -21
- package/dist/path/reserved-segment.mjs +17 -21
- package/dist/plugins/index.generated.js +2 -2
- package/dist/plugins/index.generated.mjs +2 -2
- package/dist/plugins/manifest.generated.js +2 -2
- package/dist/plugins/manifest.generated.mjs +2 -2
- package/dist/plugins/object/object.d.ts +6 -0
- package/dist/plugins/object/object.js +9 -11
- package/dist/plugins/object/object.mjs +10 -12
- package/dist/plugins/object-additional-properties/select-additional-keys.d.ts +8 -8
- package/dist/plugins/object-additional-properties/select-additional-keys.js +16 -16
- package/dist/plugins/object-additional-properties/select-additional-keys.mjs +16 -16
- package/dist/plugins/stitch/stitch.d.ts +10 -14
- package/dist/plugins/stitch-with/stitch-with.d.ts +1 -1
- package/dist/plugins/stitch-with/stitch-with.js +22 -24
- package/dist/plugins/stitch-with/stitch-with.mjs +22 -24
- package/dist/plugins/string-min/string-min.js +6 -8
- package/dist/plugins/string-min/string-min.mjs +6 -8
- package/dist/presets/presets.d.ts +10 -11
- package/dist/presets/presets.js +22 -23
- package/dist/presets/presets.mjs +22 -23
- package/dist/runtime/create-field-validator.js +4 -6
- package/dist/runtime/create-field-validator.mjs +4 -6
- package/dist/runtime/create-validator.js +11 -11
- package/dist/runtime/create-validator.mjs +11 -11
- package/dist/runtime/output-writer.js +5 -1
- package/dist/runtime/output-writer.mjs +5 -1
- package/dist/runtime/run-array-node.js +6 -6
- package/dist/runtime/run-array-node.mjs +6 -6
- package/dist/runtime/run-field.js +18 -19
- package/dist/runtime/run-field.mjs +18 -19
- package/dist/standard-schema/assemble-json-schema.d.ts +4 -0
- package/dist/standard-schema/assemble-json-schema.js +95 -0
- package/dist/standard-schema/assemble-json-schema.mjs +92 -0
- package/dist/standard-schema/declaration-recorder.d.ts +6 -0
- package/dist/standard-schema/declaration-recorder.js +30 -0
- package/dist/standard-schema/declaration-recorder.mjs +27 -0
- package/dist/standard-schema/declarations-unavailable-error.d.ts +4 -0
- package/dist/standard-schema/declarations-unavailable-error.js +32 -0
- package/dist/standard-schema/declarations-unavailable-error.mjs +28 -0
- package/dist/standard-schema/emit-field-schema.d.ts +9 -0
- package/dist/standard-schema/emit-field-schema.js +68 -0
- package/dist/standard-schema/emit-field-schema.mjs +65 -0
- package/dist/standard-schema/index.d.ts +5 -0
- package/dist/standard-schema/index.js +9 -1
- package/dist/standard-schema/index.mjs +4 -0
- package/dist/standard-schema/json-schema-target.d.ts +6 -0
- package/dist/standard-schema/json-schema-target.js +44 -0
- package/dist/standard-schema/json-schema-target.mjs +39 -0
- package/dist/standard-schema/plugin-keyword-map.d.ts +3 -0
- package/dist/standard-schema/plugin-keyword-map.js +93 -0
- package/dist/standard-schema/plugin-keyword-map.mjs +90 -0
- package/dist/standard-schema/split-issue-path.d.ts +6 -4
- package/dist/standard-schema/split-issue-path.js +15 -13
- package/dist/standard-schema/split-issue-path.mjs +15 -13
- package/dist/standard-schema/standard-schema.types.d.ts +8 -7
- package/dist/standard-schema/standard-schema.types.js +6 -6
- package/dist/standard-schema/standard-schema.types.mjs +6 -6
- package/dist/standard-schema/to-standard-json-schema.d.ts +19 -0
- package/dist/standard-schema/to-standard-json-schema.js +36 -0
- package/dist/standard-schema/to-standard-json-schema.mjs +33 -0
- package/dist/standard-schema/to-standard-schema.d.ts +16 -15
- package/dist/standard-schema/to-standard-schema.js +15 -22
- package/dist/standard-schema/to-standard-schema.mjs +15 -22
- package/dist/standard-schema/unrepresentable-rule-error.d.ts +15 -0
- package/dist/standard-schema/unrepresentable-rule-error.js +43 -0
- package/dist/standard-schema/unrepresentable-rule-error.mjs +38 -0
- package/dist/types/index.d.ts +12 -12
- package/dist/types/index.js +7 -7
- package/dist/types/index.mjs +7 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
**A TypeScript validation library where the wrong rule does not compile.**
|
|
10
10
|
|
|
11
|
+
[Documentation](https://luq.dev) · [Getting started](https://luq.dev/docs/getting-started) · [Plugins](https://luq.dev/plugins) · [JSON Schema](https://luq.dev/json-schema)
|
|
12
|
+
|
|
11
13
|
</div>
|
|
12
14
|
|
|
15
|
+
## Why this exists
|
|
16
|
+
|
|
13
17
|
Your types were probably not written by you. `openapi-typescript` generates
|
|
14
18
|
them from a spec you do not own. Prisma and Drizzle generate them from the
|
|
15
19
|
schema of record. protobuf and GraphQL codegen generate them for services in
|
|
@@ -17,20 +21,19 @@ four languages at once. Increasingly, a model generates the code that uses them.
|
|
|
17
21
|
|
|
18
22
|
A validator whose schema is the source of truth assumes you are the one who
|
|
19
23
|
decides the shape. When that assumption holds, it is the better arrangement and
|
|
20
|
-
this
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
this page says so again below. When it does not, you end up maintaining a second
|
|
25
|
+
description of a shape you did not choose. You can have the compiler check the
|
|
26
|
+
copy against the original — zod's `satisfies z.ZodType<Order>` does exactly that
|
|
27
|
+
— but you still author it, update it, and remember to write the check.
|
|
24
28
|
|
|
25
29
|
Luq runs the other way. It takes the type you already have and lets you declare
|
|
26
|
-
rules against its field paths.
|
|
27
|
-
|
|
28
|
-
the
|
|
29
|
-
fires.
|
|
30
|
+
rules against its field paths. **Your type definitions do not change**: not
|
|
31
|
+
re-authored as a schema, not replaced by an inferred one, not moved. `.for<Order>()`
|
|
32
|
+
takes the `Order` you already have, exactly as it is.
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
What makes those declarations worth writing is that the compiler checks them
|
|
35
|
+
against the type: a rule that does not apply to the field it is written on is a
|
|
36
|
+
compile error, not a rule that quietly never fires.
|
|
34
37
|
|
|
35
38
|
| Mistake | Result |
|
|
36
39
|
|---|---|
|
|
@@ -41,194 +44,20 @@ an array wildcard gets a red squiggle, not a validator that passes everything.
|
|
|
41
44
|
| A JSON Schema keyword bound to a chain method that does not exist | compile error |
|
|
42
45
|
| A documented example drifting from the API | fails CI |
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
Every number on this page was measured on this repository, and the file it came
|
|
48
|
-
from is named next to it. Where a measurement is worse than 1.x — and some are —
|
|
49
|
-
it is written down as worse.
|
|
50
|
-
|
|
51
|
-
## Install
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
npm install @maroonedog/luq
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
## Quick start
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
import { Builder } from "@maroonedog/luq";
|
|
61
|
-
import { requiredPlugin } from "@maroonedog/luq/plugins/required";
|
|
62
|
-
import { stringMinPlugin } from "@maroonedog/luq/plugins/stringMin";
|
|
63
|
-
import { stringEmailPlugin } from "@maroonedog/luq/plugins/stringEmail";
|
|
64
|
-
import { numberMinPlugin } from "@maroonedog/luq/plugins/numberMin";
|
|
65
|
-
|
|
66
|
-
type User = {
|
|
67
|
-
name: string;
|
|
68
|
-
age: number;
|
|
69
|
-
email: string;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const userValidator = Builder()
|
|
73
|
-
.use(requiredPlugin)
|
|
74
|
-
.use(stringMinPlugin)
|
|
75
|
-
.use(stringEmailPlugin)
|
|
76
|
-
.use(numberMinPlugin)
|
|
77
|
-
.for<User>()
|
|
78
|
-
.v("name", (b) => b.string.required().min(3))
|
|
79
|
-
.v("age", (b) => b.number.required().min(18))
|
|
80
|
-
.v("email", (b) => b.string.required().email())
|
|
81
|
-
.build();
|
|
82
|
-
|
|
83
|
-
const result = userValidator.validate({
|
|
84
|
-
name: "Jo",
|
|
85
|
-
age: 25,
|
|
86
|
-
email: "jo@example.com",
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
if (result.valid) {
|
|
90
|
-
// `data` exists only on this branch — no cast, no non-null assertion.
|
|
91
|
-
console.error(result.data.name);
|
|
92
|
-
} else {
|
|
93
|
-
for (const issue of result.issues) {
|
|
94
|
-
console.error(`${issue.path}: ${issue.message} (${issue.code})`);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
`build()` returns an **object**, not a function. It has four members:
|
|
100
|
-
|
|
101
|
-
| Member | What it gives back |
|
|
102
|
-
|---|---|
|
|
103
|
-
| `validate(value, options?)` | `ValidationResult<T>` holding the **original** value |
|
|
104
|
-
| `parse(value, options?)` | `ValidationResult<TParsed>` holding the value **after transforms** |
|
|
105
|
-
| `pick(path)` | a single-field validator for one declared path |
|
|
106
|
-
| `pickAll(paths)` | a validator returning exactly those paths, keyed by the path string |
|
|
107
|
-
|
|
108
|
-
A `ValidationResult<T>` is a discriminated union on `valid`: the success branch
|
|
109
|
-
carries `data`, both branches carry `issues`, and each issue is
|
|
110
|
-
`{ path, code, message, severity }`.
|
|
111
|
-
|
|
112
|
-
> Every code block on this page is extracted and typechecked against the built
|
|
113
|
-
> package by `npm run check:docs`. It exists because documentation drifts from
|
|
114
|
-
> the API it documents unless something compiles it — 1.x's quick start had drifted
|
|
115
|
-
> in three places at once (a `build()` result called as a function, a `result.issues`
|
|
116
|
-
> member that did not exist, and a subpath missing from the exports map), and each
|
|
117
|
-
> was the readable kind of mistake that nobody reads.
|
|
118
|
-
|
|
119
|
-
## It patches onto the types you already have
|
|
120
|
-
|
|
121
|
-
This is the practical consequence of being type-first, and it is the main reason
|
|
122
|
-
to reach for Luq: **your type definitions do not change.** Not re-authored as a
|
|
123
|
-
schema, not replaced by an inferred one, not moved. `.for<Order>()` takes the
|
|
124
|
-
`Order` you already have, exactly as it is, and every rule is declared against
|
|
125
|
-
it.
|
|
126
|
-
|
|
127
|
-
So Luq asks you to describe the **rules**, not the shape — the shape is already
|
|
128
|
-
written down. Adopting a schema-first validator on an existing codebase means
|
|
129
|
-
producing a second description of the same shape for every type you cover, and
|
|
130
|
-
then keeping the two in agreement; that is per-type work whether you author the
|
|
131
|
-
schema alongside the type or switch the type to be inferred from it. Luq skips
|
|
132
|
-
that step because it never needs the second description.
|
|
133
|
-
|
|
134
|
-
Which is what makes adoption a patch rather than a migration:
|
|
135
|
-
|
|
136
|
-
**Declare only the fields you care about.** A path you did not declare is not
|
|
137
|
-
validated, not required, and not read. There is no "unknown key" behaviour to
|
|
138
|
-
opt out of, so a partly-covered type is a normal state and not a half-finished
|
|
139
|
-
one.
|
|
140
|
-
|
|
141
|
-
```ts
|
|
142
|
-
import { Builder } from "@maroonedog/luq";
|
|
143
|
-
import { requiredPlugin } from "@maroonedog/luq/plugins/required";
|
|
144
|
-
import { stringMinPlugin } from "@maroonedog/luq/plugins/stringMin";
|
|
145
|
-
|
|
146
|
-
type Order = {
|
|
147
|
-
id: string;
|
|
148
|
-
customerNote: string;
|
|
149
|
-
legacyBlob: unknown;
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
// One field of three.
|
|
153
|
-
const orderValidator = Builder()
|
|
154
|
-
.use(requiredPlugin)
|
|
155
|
-
.use(stringMinPlugin)
|
|
156
|
-
.for<Order>()
|
|
157
|
-
.v("id", (b) => b.string.required().min(3))
|
|
158
|
-
.build();
|
|
159
|
-
|
|
160
|
-
// `customerNote` and `legacyBlob` are never read, so anything goes there —
|
|
161
|
-
// including being absent.
|
|
162
|
-
console.error(orderValidator.validate({ id: "abc" } as Order).valid); // true
|
|
163
|
-
console.error(orderValidator.validate({ id: "ab" } as Order).valid); // false
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
**Validate one field at a time.** `pick(path)` gives back a validator for a
|
|
167
|
-
single declared path, which is what a form needs on blur. It takes the field's
|
|
168
|
-
own value, and optionally its siblings for cross-field rules.
|
|
169
|
-
|
|
170
|
-
```ts
|
|
171
|
-
import { Builder } from "@maroonedog/luq";
|
|
172
|
-
import { requiredPlugin } from "@maroonedog/luq/plugins/required";
|
|
173
|
-
import { stringMinPlugin } from "@maroonedog/luq/plugins/stringMin";
|
|
174
|
-
|
|
175
|
-
type Order = { id: string; customerNote: string };
|
|
176
|
-
|
|
177
|
-
const id = Builder()
|
|
178
|
-
.use(requiredPlugin)
|
|
179
|
-
.use(stringMinPlugin)
|
|
180
|
-
.for<Order>()
|
|
181
|
-
.v("id", (b) => b.string.required().min(3))
|
|
182
|
-
.build()
|
|
183
|
-
.pick("id");
|
|
184
|
-
|
|
185
|
-
console.error(id.validate("ab").valid); // false
|
|
186
|
-
console.error(id.validate("abc").valid); // true
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
`pickAll(["a", "b"])` does the same for a named subset and hands back exactly
|
|
190
|
-
those paths, keyed by the strings you asked for.
|
|
191
|
-
|
|
192
|
-
**Keep what you already have.** Luq implements Standard Schema v1, so a Luq
|
|
193
|
-
validator and a zod schema are interchangeable at any boundary that accepts one.
|
|
194
|
-
Adding Luq to one route does not commit the next one, and does not remove zod
|
|
195
|
-
from the routes it is already in.
|
|
196
|
-
|
|
197
|
-
None of this needs a migration step, because there is nothing global to migrate:
|
|
198
|
-
no registry, no plugin installation, no shared configuration object. A validator
|
|
199
|
-
is a value in a module, declared against a type that was there before it.
|
|
200
|
-
|
|
201
|
-
**And when you want the opposite, ask for it: `.strict()`.** Partial coverage is
|
|
202
|
-
the default because that is what makes a patch possible, but a builder that
|
|
203
|
-
declares `.strict()` will not compile until every leaf path of `T` is declared —
|
|
204
|
-
and the error names the ones you missed:
|
|
205
|
-
|
|
206
|
-
```ts
|
|
207
|
-
import { Builder } from "@maroonedog/luq";
|
|
208
|
-
import { requiredPlugin } from "@maroonedog/luq/plugins/required";
|
|
209
|
-
|
|
210
|
-
type Order = { id: string; customerNote: string; nested: { deep: number } };
|
|
211
|
-
|
|
212
|
-
export const incomplete = Builder()
|
|
213
|
-
.use(requiredPlugin)
|
|
214
|
-
.for<Order>()
|
|
215
|
-
.v("id", (b) => b.string.required())
|
|
216
|
-
.strict()
|
|
217
|
-
// @ts-expect-error strict() returned
|
|
218
|
-
// MissingFieldsError<"customerNote" | "nested.deep">, which has no build().
|
|
219
|
-
.build();
|
|
220
|
-
```
|
|
47
|
+
That matters most when the code calling this library is generated rather than
|
|
48
|
+
typed by hand. A generator that picks the wrong rule, misspells a path or drops
|
|
49
|
+
an array wildcard gets a red squiggle, not a validator that passes everything.
|
|
221
50
|
|
|
222
|
-
|
|
223
|
-
(`tags[*]`) and a field inside an array of objects (`items[*].sku`) are each
|
|
224
|
-
required in their own right. It has no run-time effect at all — the obligation
|
|
225
|
-
is discharged by the compiler.
|
|
51
|
+
Two consequences worth knowing before you read further:
|
|
226
52
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
53
|
+
- **Adoption is a patch, not a migration.** A path you did not declare is not
|
|
54
|
+
validated, not required, and not read, so a partly-covered type is a normal
|
|
55
|
+
state rather than a half-finished one. There is nothing global to migrate: no
|
|
56
|
+
registry, no plugin installation, no shared configuration object.
|
|
57
|
+
- **Every rule you can call is a plugin you imported by name**, so the bundle
|
|
58
|
+
contains what you used and nothing else — an unimported plugin's method does
|
|
59
|
+
not even typecheck. What each configuration costs is measured on every build:
|
|
60
|
+
[luq.dev/benchmarks](https://luq.dev/benchmarks).
|
|
232
61
|
|
|
233
62
|
## When schema-first is the right answer
|
|
234
63
|
|
|
@@ -260,421 +89,37 @@ If you are starting from nothing and you will own the shape, use zod. It is
|
|
|
260
89
|
mature, it is everywhere, and every question you will have is already answered
|
|
261
90
|
somewhere.
|
|
262
91
|
|
|
263
|
-
##
|
|
264
|
-
|
|
265
|
-
A path is a string literal that TypeScript resolves against your type. Nested
|
|
266
|
-
fields use dots; array elements use `[*]`, and the issue you get back names the
|
|
267
|
-
**real index**.
|
|
268
|
-
|
|
269
|
-
```ts
|
|
270
|
-
import { Builder } from "@maroonedog/luq";
|
|
271
|
-
import { requiredPlugin } from "@maroonedog/luq/plugins/required";
|
|
272
|
-
import { stringMinPlugin } from "@maroonedog/luq/plugins/stringMin";
|
|
273
|
-
|
|
274
|
-
type Order = {
|
|
275
|
-
customer: { name: string };
|
|
276
|
-
items: { productId: string }[];
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
const orderValidator = Builder()
|
|
280
|
-
.use(requiredPlugin)
|
|
281
|
-
.use(stringMinPlugin)
|
|
282
|
-
.for<Order>()
|
|
283
|
-
.v("customer.name", (b) => b.string.required().min(2))
|
|
284
|
-
.v("items[*].productId", (b) => b.string.required().min(5))
|
|
285
|
-
.build();
|
|
286
|
-
|
|
287
|
-
const rejected = orderValidator.validate({
|
|
288
|
-
customer: { name: "Acme" },
|
|
289
|
-
items: [{ productId: "PROD-1" }, { productId: "X" }],
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
// -> ["items[1].productId"], never "items[*].productId"
|
|
293
|
-
export const failedPaths = rejected.issues.map((issue) => issue.path);
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
A path that does not exist on the type is a compile error, not a silent no-op.
|
|
297
|
-
So is choosing a slot the field's type cannot be: `b.number` on a `string`
|
|
298
|
-
field fails to compile.
|
|
299
|
-
|
|
300
|
-
**Cross-field rules read those paths back with their types intact.** `stitch`
|
|
301
|
-
takes the paths it needs and hands them over as a bundle keyed by the path
|
|
302
|
-
string — each one typed from your type, nested paths included. There is no
|
|
303
|
-
`unknown` to narrow and no cast to write.
|
|
304
|
-
|
|
305
|
-
```ts
|
|
306
|
-
import { Builder } from "@maroonedog/luq";
|
|
307
|
-
import { requiredPlugin } from "@maroonedog/luq/plugins/required";
|
|
308
|
-
import { numberMinPlugin } from "@maroonedog/luq/plugins/numberMin";
|
|
309
|
-
import { stitchPlugin } from "@maroonedog/luq/plugins/stitch";
|
|
310
|
-
|
|
311
|
-
type Booking = { seats: number; venue: { capacity: number } };
|
|
312
|
-
|
|
313
|
-
const bookingValidator = Builder()
|
|
314
|
-
.use(requiredPlugin)
|
|
315
|
-
.use(numberMinPlugin)
|
|
316
|
-
.use(stitchPlugin)
|
|
317
|
-
.for<Booking>()
|
|
318
|
-
.v("venue.capacity", (b) => b.number.required().min(1))
|
|
319
|
-
.v("seats", (b) =>
|
|
320
|
-
b.number.required().stitch(["venue.capacity"], (fieldValues, value) => ({
|
|
321
|
-
// fieldValues["venue.capacity"] is number, and value is number.
|
|
322
|
-
valid: value <= fieldValues["venue.capacity"],
|
|
323
|
-
message: "seats must fit the venue",
|
|
324
|
-
}))
|
|
325
|
-
)
|
|
326
|
-
.build();
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
Asking for a path the type does not have is a compile error, and so is using a
|
|
330
|
-
bundled value at the wrong type. 1.x passed this bundle as
|
|
331
|
-
`Record<string, unknown>`, which meant every cross-field rule opened with a
|
|
332
|
-
cast; the paths were already declared, so the types were always knowable.
|
|
333
|
-
|
|
334
|
-
## Plugins are imports
|
|
335
|
-
|
|
336
|
-
There is no plugin registry to populate and no barrel you have to pay for.
|
|
337
|
-
`.use()` puts a plugin in the builder's bag, and the bag decides which methods
|
|
338
|
-
exist on which slots — so an unimported plugin is not merely absent at runtime,
|
|
339
|
-
its method does not typecheck.
|
|
340
|
-
|
|
341
|
-
```ts
|
|
342
|
-
import { Builder } from "@maroonedog/luq";
|
|
343
|
-
import { requiredPlugin } from "@maroonedog/luq/plugins/required";
|
|
344
|
-
|
|
345
|
-
type Draft = { title: string };
|
|
346
|
-
|
|
347
|
-
const draftValidator = Builder()
|
|
348
|
-
.use(requiredPlugin)
|
|
349
|
-
.for<Draft>()
|
|
350
|
-
// `.min(3)` is not offered here: stringMinPlugin was never `use`d.
|
|
351
|
-
.v("title", (b) => b.string.required())
|
|
352
|
-
.build();
|
|
353
|
-
|
|
354
|
-
export const isTitled = draftValidator.validate({ title: "x" }).valid;
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
78 plugin objects ship across 77 subpaths, plus one deprecated alias kept from
|
|
358
|
-
1.x. The complete table — subpath, symbol, chain method, slots — is generated
|
|
359
|
-
from the built package: **[docs/guide/plugin-reference.md](docs/guide/plugin-reference.md)**.
|
|
360
|
-
|
|
361
|
-
A convenience barrel exists at `@maroonedog/luq/plugins`. It is measurably free
|
|
362
|
-
when a bundler can tree-shake (three plugins via the barrel gzip to 7,986 B
|
|
363
|
-
against 7,987 B via three subpaths — 0.01%), but the per-plugin subpaths are the
|
|
364
|
-
supported route.
|
|
365
|
-
|
|
366
|
-
## JSON Schema
|
|
367
|
-
|
|
368
|
-
```ts
|
|
369
|
-
import { fromJsonSchema } from "@maroonedog/luq/plugins/jsonSchemaFullFeature";
|
|
370
|
-
|
|
371
|
-
type Account = { email: string; age?: number };
|
|
372
|
-
|
|
373
|
-
const accountValidator = fromJsonSchema<Account>({
|
|
374
|
-
type: "object",
|
|
375
|
-
properties: {
|
|
376
|
-
email: { type: "string", format: "email" },
|
|
377
|
-
age: { type: "number", minimum: 18 },
|
|
378
|
-
},
|
|
379
|
-
required: ["email"],
|
|
380
|
-
});
|
|
92
|
+
## Install
|
|
381
93
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}).valid;
|
|
94
|
+
```bash
|
|
95
|
+
npm install @maroonedog/luq
|
|
385
96
|
```
|
|
386
97
|
|
|
387
|
-
|
|
388
|
-
[JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite)
|
|
389
|
-
(required tests only, skipped cases counted as **failures**):
|
|
390
|
-
|
|
391
|
-
**929 / 929 = 100.00%.** Read that against the floor, not against zero: a
|
|
392
|
-
validator that returned `true` unconditionally scores 551 / 929 = 59.31% on
|
|
393
|
-
this corpus. The skip list is empty, and its `cause` union is `never`, so
|
|
394
|
-
excluding a case again means adding a name to a type. Full breakdown, including
|
|
395
|
-
what closed each cause and what is still bounded:
|
|
396
|
-
[docs/json-schema-conformance.md](docs/json-schema-conformance.md).
|
|
397
|
-
|
|
398
|
-
External `$ref` resolves against a map of documents **you** already have —
|
|
399
|
-
`jsonSchemaFullFeature(document, { externalDocuments })`. Luq never fetches, so
|
|
400
|
-
a URI written in a schema cannot make the process open a socket, conversion
|
|
401
|
-
stays synchronous, and nothing is evaluated.
|
|
402
|
-
|
|
403
|
-
`jsonSchemaFullFeature` bundles 49 plugins so one import covers a whole
|
|
404
|
-
document. `@maroonedog/luq/plugins/jsonSchema` adds a chain method instead, so a
|
|
405
|
-
single declared field can be constrained by a document; it takes the plugin bag
|
|
406
|
-
explicitly. It is billed as the tree-shakeable half, and measured below, it is
|
|
407
|
-
not — use it for the chain method, not to save bytes.
|
|
408
|
-
|
|
409
|
-
## Standard Schema
|
|
410
|
-
|
|
411
|
-
Luq implements [Standard Schema v1](https://standardschema.dev). Anything that
|
|
412
|
-
accepts a Standard Schema — tRPC, TanStack Form, Hono, t3-env — accepts a Luq
|
|
413
|
-
validator wherever it accepts a zod schema.
|
|
414
|
-
|
|
415
|
-
```ts
|
|
416
|
-
import { Builder } from "@maroonedog/luq";
|
|
417
|
-
import { requiredPlugin } from "@maroonedog/luq/plugins/required";
|
|
418
|
-
import { stringMinPlugin } from "@maroonedog/luq/plugins/stringMin";
|
|
419
|
-
import { toStandardSchema } from "@maroonedog/luq/standard-schema";
|
|
420
|
-
|
|
421
|
-
type Account = { handle: string };
|
|
422
|
-
|
|
423
|
-
const standard = toStandardSchema(
|
|
424
|
-
Builder()
|
|
425
|
-
.use(requiredPlugin)
|
|
426
|
-
.use(stringMinPlugin)
|
|
427
|
-
.for<Account>()
|
|
428
|
-
.v("handle", (b) => b.string.required().min(2))
|
|
429
|
-
.build()
|
|
430
|
-
);
|
|
431
|
-
|
|
432
|
-
// `standard` is still the Validator — `validate`, `parse`, `pick` and `pickAll`
|
|
433
|
-
// are all there — and it now also satisfies Standard Schema v1, so it can be
|
|
434
|
-
// handed to tRPC, TanStack Form, Hono or t3-env unchanged.
|
|
435
|
-
const outcome = standard["~standard"].validate({ handle: "j" });
|
|
436
|
-
|
|
437
|
-
if (outcome.issues === undefined) {
|
|
438
|
-
console.error(outcome.value.handle);
|
|
439
|
-
} else {
|
|
440
|
-
for (const issue of outcome.issues) {
|
|
441
|
-
console.error(issue.message, issue.path);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
```
|
|
98
|
+
Zero runtime dependencies. TypeScript 5.0 or later.
|
|
445
99
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
- `validate` calls Luq's `parse()`, not `validate()`. The spec's success result
|
|
449
|
-
is `{ value: Output }`, and `Output` is the value *after* validation — so a
|
|
450
|
-
`transform` has to be applied, and only `parse()` applies it.
|
|
451
|
-
- It collects every issue rather than stopping at the first. The consumer of
|
|
452
|
-
this seam is a form, and returning one issue at a time produces a UI where
|
|
453
|
-
fixing an error reveals the next one. Callers who want the fast path use the
|
|
454
|
-
`Validator` directly.
|
|
455
|
-
- `InferInput` is the type you wrote in `.for<T>()`, not a type inferred back
|
|
456
|
-
out of a schema value.
|
|
457
|
-
|
|
458
|
-
It is a subpath, not part of `build()`. Measured on the 2.0.0 core (7,420 B) and
|
|
459
|
-
carrying `~standard` on every validator adds 312 B — 4.2% charged to everyone,
|
|
460
|
-
including the people who never pass a validator to tRPC. Importing the subpath
|
|
461
|
-
costs those 312 B only when you import it, and nothing when you don't.
|
|
462
|
-
|
|
463
|
-
## Your own rules
|
|
464
|
-
|
|
465
|
-
```ts
|
|
466
|
-
import { definePlugin, check, PASS, fail } from "@maroonedog/luq/plugin-kit";
|
|
467
|
-
import type { Unchanged } from "@maroonedog/luq/plugin-kit";
|
|
468
|
-
|
|
469
|
-
export const productCodePlugin = definePlugin<{
|
|
470
|
-
args: readonly [];
|
|
471
|
-
out: Unchanged;
|
|
472
|
-
context: { readonly prefix: string };
|
|
473
|
-
}>()({
|
|
474
|
-
name: "productCode",
|
|
475
|
-
method: "productCode",
|
|
476
|
-
slots: ["string"] as const,
|
|
477
|
-
build: (ctx) =>
|
|
478
|
-
check({
|
|
479
|
-
code: ctx.code,
|
|
480
|
-
messageFactory: ctx.messageFactory,
|
|
481
|
-
severity: ctx.severity,
|
|
482
|
-
run: (value) =>
|
|
483
|
-
typeof value === "string" && value.startsWith("PROD-")
|
|
484
|
-
? PASS
|
|
485
|
-
: fail({ actual: value }),
|
|
486
|
-
describe: (_detail, messageContext) =>
|
|
487
|
-
`${messageContext.path} must start with PROD-`,
|
|
488
|
-
buildMessageContext: () => ({ prefix: "PROD-" }),
|
|
489
|
-
}),
|
|
490
|
-
});
|
|
491
|
-
```
|
|
100
|
+
## Documentation
|
|
492
101
|
|
|
493
|
-
|
|
494
|
-
return and what the chain's type becomes afterwards. Eleven argument markers and
|
|
495
|
-
four output markers make up the vocabulary — see
|
|
496
|
-
**[docs/guide/writing-a-plugin.md](docs/guide/writing-a-plugin.md)**.
|
|
497
|
-
|
|
498
|
-
## Measured numbers
|
|
499
|
-
|
|
500
|
-
### Bundle size
|
|
501
|
-
|
|
502
|
-
esbuild 0.25.5, `bundle + minify + esm + es2020 + platform:neutral + treeShaking`,
|
|
503
|
-
then `zlib.gzipSync` — the same options 1.x's own `bundle-size-comparison` used,
|
|
504
|
-
so the two columns are comparable. Recorded in
|
|
505
|
-
[config/size-budget.json](config/size-budget.json) and re-measured by
|
|
506
|
-
`npm run check:size` on every build.
|
|
507
|
-
|
|
508
|
-
| Entry | gzip | 1.x, same method |
|
|
509
|
-
|---|---:|---:|
|
|
510
|
-
<!-- generated:bundle-size -->
|
|
511
|
-
| `Builder` only, zero plugins | **7,954 B** | 17,423 B |
|
|
512
|
-
| + 6 plugins (1.x's "simple" set) | **8,879 B** | 19,562 B |
|
|
513
|
-
| all 77 plugins | **25,991 B** | — |
|
|
514
|
-
<!-- /generated:bundle-size -->
|
|
515
|
-
|
|
516
|
-
Three more entries were measured the same way on 2026-09-07 but are **not** in
|
|
517
|
-
`config/size-budget.json`, so nothing re-measures them and they can go stale
|
|
518
|
-
without anything noticing. They are kept because the JSON Schema claim needs
|
|
519
|
-
evidence, and marked because a figure nobody checks is worth less than one that
|
|
520
|
-
is checked:
|
|
521
|
-
|
|
522
|
-
| Entry (measured once, not gated) | gzip | 1.x, same method |
|
|
523
|
-
|---|---:|---:|
|
|
524
|
-
| core + `jsonSchema`, plugin alone (not usable) | 18,992 B | — |
|
|
525
|
-
| core + `jsonSchema` + a working 49-plugin bag | 21,371 B | 26.06–29.08 KB |
|
|
526
|
-
| core + `jsonSchemaFullFeature` | 21,383 B | 31.75–32.31 KB |
|
|
527
|
-
|
|
528
|
-
1.x published "tree-shakeable, 19–23KB gzipped". Measured the same way, its
|
|
529
|
-
core was 17.4 KB **before any plugin was imported** — 89.1% of its "simple"
|
|
530
|
-
figure. Here the core is <!-- generated:bundle-core-share -->30.6% of the all-plugins build (7,954 of 25,991 B)<!-- /generated:bundle-core-share -->,
|
|
531
|
-
and adding a plugin costs 129–224 B of gzip. Both figures are in the table above;
|
|
532
|
-
the difference is where the bytes sit, not which README is right.
|
|
533
|
-
|
|
534
|
-
Two lines that are **not** wins:
|
|
535
|
-
|
|
536
|
-
- "all 77 plugins at 25,607 B" is larger than the 23,015 B 1.x published for its
|
|
537
|
-
`complex` case. The two are not comparable — 1.x's figure was one schema's
|
|
538
|
-
plugin set, not its whole catalogue — so it is not counted either way here.
|
|
539
|
-
- The 18,992 B for `jsonSchema` measures the plugin **without a bag**, which is
|
|
540
|
-
not a configuration you can actually validate with. Supplying a working bag
|
|
541
|
-
costs 21,371 B — 12 bytes, 0.06%, **less** than just importing
|
|
542
|
-
`jsonSchemaFullFeature`. The tree-shakeable JSON Schema route saves nothing
|
|
543
|
-
worth having today; use `jsonSchemaFullFeature` unless you need the chain
|
|
544
|
-
method for one field.
|
|
545
|
-
|
|
546
|
-
### Speed
|
|
547
|
-
|
|
548
|
-
`npm run bench:record`, recorded verbatim in
|
|
549
|
-
[config/perf-baseline.json](config/perf-baseline.json). Machine: AMD Ryzen 7
|
|
550
|
-
5825U, 16 logical cores, Node v23.11.0, Windows. Subject is `src/` transpiled by
|
|
551
|
-
ts-node, not the bundle. `abortEarly: true`, input accepted, so no rule is
|
|
552
|
-
skipped. Every subject rotates over a pool of at least four distinct values —
|
|
553
|
-
one frozen input let V8 delete a subject outright, which is the artefact
|
|
554
|
-
described below. Each figure is the median of the fastest half of 9 samples; the
|
|
555
|
-
spread quoted alongside is the full range over that figure, and on these ten it
|
|
556
|
-
is <!-- generated:perf-spread -->1.2–12.3%<!-- /generated:perf-spread -->.
|
|
557
|
-
|
|
558
|
-
| Shape | `validate` ops/sec | `parse` ops/sec |
|
|
559
|
-
|---|---:|---:|
|
|
560
|
-
<!-- generated:perf-throughput -->
|
|
561
|
-
| 1 field, 1 check | 5,581,199 | 5,593,550 |
|
|
562
|
-
| 3 fields, 6 plugins | 2,444,308 | 1,933,180 |
|
|
563
|
-
| nested, depth 2–3 | 1,722,770 | 1,701,094 |
|
|
564
|
-
| array of 50 elements | 90,590 | 90,013 |
|
|
565
|
-
| JSON Schema document | 312,630 | 309,502 |
|
|
566
|
-
<!-- /generated:perf-throughput -->
|
|
567
|
-
|
|
568
|
-
**This rewrite is slower than 1.x on flat and nested shapes.** Measured side by
|
|
569
|
-
side, in one process on one machine, 1.x source against this source, sample by
|
|
570
|
-
sample interleaved so a drift in the machine hits both halves of every ratio:
|
|
571
|
-
|
|
572
|
-
| Shape | 1.x | this | ratio |
|
|
573
|
-
|---|---:|---:|---:|
|
|
574
|
-
<!-- generated:perf-legacy -->
|
|
575
|
-
| 1 field | 26,568,111 | 5,463,953 | **×0.21** |
|
|
576
|
-
| 3 fields | 3,082,290 | 2,434,509 | **×0.79** |
|
|
577
|
-
| nested | 2,203,633 | 1,842,200 | **×0.83** |
|
|
578
|
-
| array of 50 | 19,610 | 84,707 | ×4.30 |
|
|
579
|
-
| JSON Schema | 146,283 | 306,778 | ×2.10 |
|
|
580
|
-
<!-- /generated:perf-legacy -->
|
|
581
|
-
|
|
582
|
-
1.x carried a directory of specialised fast paths that this implementation has
|
|
583
|
-
no equivalent of. The comparison was checked for the ways it could be wrong: 1.x
|
|
584
|
-
demonstrably rejects bad values on all five shapes, so it is not winning by
|
|
585
|
-
doing less work, and both halves are asserted to accept the accepted pool and
|
|
586
|
-
reject the rejected pool before either is timed.
|
|
587
|
-
|
|
588
|
-
Also worth stating plainly: **neither figure 1.x's README published reproduces
|
|
589
|
-
here.** It claimed 1.2M ops/sec simple and 43K complex; on this machine 1.x
|
|
590
|
-
itself does <!-- generated:perf-legacy-simple -->3.08M<!-- /generated:perf-legacy-simple -->
|
|
591
|
-
on the shape rebuilt from its own "simple" benchmark source, and "complex" has
|
|
592
|
-
no reproducible definition to measure.
|
|
593
|
-
|
|
594
|
-
`build()` costs 14–662 µs depending on shape, against sub-microsecond
|
|
595
|
-
`validate()` calls — so one `build()` pays for itself after 35–100 `validate()`
|
|
596
|
-
calls on four of the five shapes, and after 2 calls on the 50-element array
|
|
597
|
-
(where `validate()` itself costs ~33 µs).
|
|
598
|
-
|
|
599
|
-
CI does not gate on any absolute number. It gates on the ratio between Luq and a
|
|
600
|
-
hand-written validator measured in the same process, so the runner's speed
|
|
601
|
-
cancels out. Fifteen pairings are gated: five shapes × `validate` on accepted
|
|
602
|
-
input, `parse` on accepted input, and `validate` on rejected input — the
|
|
603
|
-
rejected path is a different program under `abortEarly` (early exit, issue
|
|
604
|
-
construction, path strings) and was previously not measured at all.
|
|
605
|
-
|
|
606
|
-
The gate's resolution is recorded rather than assumed. Slowing every shape's
|
|
607
|
-
validator by a fixed factor and re-running (`gateSensitivity` in
|
|
608
|
-
config/perf-baseline.json, one run per level): **+35% is caught** on 14 of the
|
|
609
|
-
15 pairings, +25% on 3, and **+15% is missed** on all 15. So the gate sees
|
|
610
|
-
roughly a third-slower regression and does not see a sixth-slower one.
|
|
611
|
-
|
|
612
|
-
The reference implementations are held to two conditions of their own, both
|
|
613
|
-
asserted before any timing. `bench/assert-reference-agreement.ts` requires the
|
|
614
|
-
hand-written reference and Luq to agree on every value in both pools, which is
|
|
615
|
-
what stops the denominator drifting into a cheaper check than the one Luq
|
|
616
|
-
performs — the email, UUID and date-time references were rewritten to the
|
|
617
|
-
plugins' own semantics after this was added, and the rejected pool carries one
|
|
618
|
-
value per known difference so reverting any of them fails the assertion.
|
|
619
|
-
`bench/measure-reference-work.ts` requires each reference to be slower than 0.95
|
|
620
|
-
of an empty loop over the same pool, which is how the deleted-subject artefact
|
|
621
|
-
is caught: when V8 removes the work the ratio sits at 1.00 or above, and the ten
|
|
622
|
-
figures recorded here span 0.01–0.86.
|
|
623
|
-
|
|
624
|
-
### CSP-safe
|
|
625
|
-
|
|
626
|
-
No `eval`, no `new Function`. Checked mechanically over all 762 emitted `.js`
|
|
627
|
-
and `.mjs` files by `npm run check:no-dynamic-code`, and over `src/` by the
|
|
628
|
-
public-API smoke test: **0 occurrences**.
|
|
629
|
-
|
|
630
|
-
The check is there because the claim is easy to make and easy to stop being true
|
|
631
|
-
— 1.x's README made it while `src/types/array-type-analysis.ts` still carried a
|
|
632
|
-
live `new Function`. This is the first release where a script enforces it on
|
|
633
|
-
every build rather than a sentence asserting it.
|
|
634
|
-
|
|
635
|
-
### Package
|
|
636
|
-
|
|
637
|
-
86 keys in `exports`, every one resolving to files that exist: 8 fixed keys
|
|
638
|
-
(`.`, `./package.json`, `./result`, `./plugin-kit`, `./field-rule`, `./async`,
|
|
639
|
-
`./plugins`, `./standard-schema`) and 78 under `./plugins/` — 77 plugins plus one
|
|
640
|
-
deprecated alias.
|
|
641
|
-
`npm pack --dry-run`: 1,197 files, 376.9 kB packed, 1.4 MB unpacked —
|
|
642
|
-
`LICENSE`, `README.md`, `package.json` and `dist/` (398 `.d.ts` + 398 `.js` +
|
|
643
|
-
398 `.mjs`), with nothing from `src/`, `test/`, `scripts/`, `bench/` or `docs/`,
|
|
644
|
-
no raw `.ts` and no source maps. A scratch consumer typechecks **every one of
|
|
645
|
-
the 86 keys** against the published declarations under **both** `node16` and
|
|
646
|
-
`bundler` resolution, and an unpublished subpath is proven to fail.
|
|
647
|
-
|
|
648
|
-
1.x's `createPluginRegistry` / `useField` / `createFieldRule` are published at
|
|
649
|
-
`@maroonedog/luq/field-rule`; `useField` is a free function now. See
|
|
650
|
-
[docs/migration/breaking-changes.md](docs/migration/breaking-changes.md#15-createpluginregistry-usefield-and-createfieldrule-moved-to-maroonedogluqfield-rule).
|
|
102
|
+
### **[luq.dev](https://luq.dev)**
|
|
651
103
|
|
|
652
|
-
|
|
104
|
+
| | |
|
|
105
|
+
|---|---|
|
|
106
|
+
| [Getting started](https://luq.dev/docs/getting-started) | the builder, defaults, `normalize`, reading a result |
|
|
107
|
+
| [Core concepts](https://luq.dev/docs/core-concepts) | field paths, slots, presence, transforms |
|
|
108
|
+
| [Plugins](https://luq.dev/plugins) | every subpath, symbol, chain method and slot |
|
|
109
|
+
| [JSON Schema](https://luq.dev/json-schema) | reading a document in, writing one back out, measured Draft-07 conformance |
|
|
110
|
+
| [Standard Schema](https://luq.dev/standard-schema) | tRPC, TanStack Form, Hono, react-hook-form — and what does not cross that boundary |
|
|
111
|
+
| [Benchmarks](https://luq.dev/benchmarks) | bundle size and throughput, with the method |
|
|
112
|
+
| [Luq or zod?](https://luq.dev/luq-or-zod) | when schema-first is the better answer |
|
|
653
113
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
- **[Field paths](docs/guide/field-paths.md)** — what a path may be, and what
|
|
657
|
-
changed from 1.x
|
|
658
|
-
- **[Presence and conditionals](docs/guide/presence-and-conditionals.md)** —
|
|
659
|
-
`required` / `optional` / `nullable` / `requiredIf`, the order rules run
|
|
660
|
-
in, and the cross-field rules (`compareField` / `stitch` / `stitchWith`)
|
|
661
|
-
- **[JSON Schema](docs/guide/json-schema.md)** — the two front doors, and the
|
|
662
|
-
keywords that are not supported
|
|
663
|
-
- **[Writing a plugin](docs/guide/writing-a-plugin.md)** — markers, `out`,
|
|
664
|
-
message factories, and the isolation rule
|
|
665
|
-
- **[Plugin reference](docs/guide/plugin-reference.md)** — generated table of
|
|
666
|
-
every subpath, method and slot
|
|
667
|
-
- **[Breaking changes from 1.x](docs/migration/breaking-changes.md)** — every
|
|
668
|
-
incompatibility with the fix beside it
|
|
669
|
-
- **[Draft-07 conformance](docs/json-schema-conformance.md)** — the 100% and
|
|
670
|
-
what closed each of the ten causes that used to fail
|
|
114
|
+
The same documentation ships in `docs/` inside this repository, so the copy at
|
|
115
|
+
any tag describes that release.
|
|
671
116
|
|
|
672
117
|
## Status, and how this gets changed
|
|
673
118
|
|
|
674
|
-
The 2.x API is stable and
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
119
|
+
The 2.x API is stable and gated, but the production track record is still short.
|
|
120
|
+
Breaking changes happen in a major and nowhere else, an API being removed is
|
|
121
|
+
deprecated one major ahead, and each major ships with the codemod needed to
|
|
122
|
+
cross it.
|
|
678
123
|
|
|
679
124
|
- **[CONTRIBUTING.md](CONTRIBUTING.md)** — `npm run verify` is the whole
|
|
680
125
|
contract; the gates and what each one refuses
|