@maroonedog/luq 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +364 -57
- package/dist/builder/builder-surface.types.d.ts +1 -0
- package/dist/builder/create-builder.js +9 -0
- package/dist/builder/create-builder.mjs +9 -0
- package/dist/builder/field-builder.types.d.ts +13 -0
- package/dist/chain/bundle-paths.types.d.ts +26 -0
- package/dist/chain/bundle-paths.types.js +2 -0
- package/dist/chain/bundle-paths.types.mjs +1 -0
- package/dist/chain/chain-method.types.d.ts +8 -3
- package/dist/chain/create-chain-node.js +22 -1
- package/dist/chain/create-chain-node.mjs +22 -1
- package/dist/chain/resolve-args.types.d.ts +4 -2
- package/dist/compile/compile-array-node.d.ts +0 -7
- package/dist/compile/compile-array-node.js +4 -0
- package/dist/compile/compile-array-node.mjs +4 -0
- package/dist/compile/compile-field.js +10 -1
- package/dist/compile/compile-field.mjs +10 -1
- package/dist/compile/resolve-conditional-presence.js +1 -0
- package/dist/compile/resolve-conditional-presence.mjs +1 -0
- package/dist/compile/resolve-presence.js +4 -0
- package/dist/compile/resolve-presence.mjs +4 -0
- package/dist/compile/split-rules-by-kind.js +31 -7
- package/dist/compile/split-rules-by-kind.mjs +31 -7
- package/dist/compile/validation-plan.types.d.ts +33 -0
- package/dist/json-schema/build-from-schema.js +2 -3
- package/dist/json-schema/build-from-schema.mjs +2 -3
- package/dist/json-schema/collect-definitions.d.ts +30 -0
- package/dist/json-schema/collect-definitions.js +47 -3
- package/dist/json-schema/collect-definitions.mjs +46 -4
- package/dist/json-schema/collect-sub-schema-rules.d.ts +12 -1
- package/dist/json-schema/collect-sub-schema-rules.js +37 -7
- package/dist/json-schema/collect-sub-schema-rules.mjs +38 -8
- package/dist/json-schema/create-structural-context.d.ts +13 -3
- package/dist/json-schema/create-structural-context.js +72 -6
- package/dist/json-schema/create-structural-context.mjs +72 -7
- package/dist/json-schema/declare-additional-properties.d.ts +19 -0
- package/dist/json-schema/declare-additional-properties.js +42 -0
- package/dist/json-schema/declare-additional-properties.mjs +37 -0
- package/dist/json-schema/declare-object-keywords.d.ts +0 -2
- package/dist/json-schema/declare-object-keywords.js +6 -13
- package/dist/json-schema/declare-object-keywords.mjs +7 -13
- package/dist/json-schema/declare-presence.d.ts +9 -3
- package/dist/json-schema/declare-presence.js +12 -6
- package/dist/json-schema/declare-presence.mjs +12 -6
- package/dist/json-schema/extensions/json-schema/index.d.ts +1 -0
- package/dist/json-schema/extensions/json-schema/json-schema.d.ts +17 -2
- package/dist/json-schema/extensions/json-schema/json-schema.js +15 -7
- package/dist/json-schema/extensions/json-schema/json-schema.mjs +16 -8
- package/dist/json-schema/extensions/json-schema-full-feature/index.d.ts +1 -0
- package/dist/json-schema/extensions/json-schema-full-feature/json-schema-full-feature.d.ts +2 -1
- package/dist/json-schema/extensions/json-schema-full-feature/json-schema-full-feature.js +3 -1
- package/dist/json-schema/extensions/json-schema-full-feature/json-schema-full-feature.mjs +3 -1
- package/dist/json-schema/flatten-schema.js +1 -1
- package/dist/json-schema/flatten-schema.mjs +1 -1
- package/dist/json-schema/follow-json-pointer.d.ts +23 -0
- package/dist/json-schema/follow-json-pointer.js +87 -0
- package/dist/json-schema/follow-json-pointer.mjs +82 -0
- package/dist/json-schema/index.d.ts +7 -2
- package/dist/json-schema/index.js +10 -2
- package/dist/json-schema/index.mjs +5 -2
- package/dist/json-schema/keyword-map-object.d.ts +1 -1
- package/dist/json-schema/ref-resolution-error.d.ts +4 -0
- package/dist/json-schema/ref-resolution-error.js +20 -0
- package/dist/json-schema/ref-resolution-error.mjs +16 -0
- package/dist/json-schema/ref-scope.d.ts +18 -0
- package/dist/json-schema/ref-scope.js +37 -0
- package/dist/json-schema/ref-scope.mjs +33 -0
- package/dist/json-schema/resolve-ref.d.ts +14 -9
- package/dist/json-schema/resolve-ref.js +89 -59
- package/dist/json-schema/resolve-ref.mjs +86 -55
- package/dist/json-schema/schema-registry.d.ts +30 -0
- package/dist/json-schema/schema-registry.js +142 -0
- package/dist/json-schema/schema-registry.mjs +139 -0
- package/dist/json-schema/schema-to-declarations.js +2 -1
- package/dist/json-schema/schema-to-declarations.mjs +2 -1
- package/dist/json-schema/structural-expansion.types.d.ts +8 -1
- package/dist/json-schema/uri-reference.d.ts +29 -0
- package/dist/json-schema/uri-reference.js +118 -0
- package/dist/json-schema/uri-reference.mjs +111 -0
- package/dist/path/create-value-writer.js +27 -2
- package/dist/path/create-value-writer.mjs +27 -2
- package/dist/path/reserved-segment.d.ts +22 -4
- package/dist/path/reserved-segment.js +27 -7
- package/dist/path/reserved-segment.mjs +27 -7
- package/dist/plugin-kit/compiled-rule.d.ts +13 -0
- package/dist/plugin-kit/create-rule.d.ts +11 -0
- package/dist/plugin-kit/create-rule.js +1 -0
- package/dist/plugin-kit/create-rule.mjs +1 -0
- package/dist/plugin-kit/marker.types.d.ts +18 -0
- package/dist/plugin-kit/plugin-definition.d.ts +15 -0
- package/dist/plugin-kit/plugin-definition.js +1 -0
- package/dist/plugin-kit/plugin-definition.mjs +1 -0
- package/dist/plugins/index.generated.d.ts +1 -0
- package/dist/plugins/index.generated.js +4 -2
- package/dist/plugins/index.generated.mjs +1 -0
- package/dist/plugins/manifest.generated.js +1 -0
- package/dist/plugins/manifest.generated.mjs +1 -0
- package/dist/plugins/object-additional-properties/object-additional-properties-schema.d.ts +1 -1
- package/dist/plugins/object-additional-properties/object-additional-properties-schema.js +4 -2
- package/dist/plugins/object-additional-properties/object-additional-properties-schema.mjs +4 -2
- package/dist/plugins/object-additional-properties/object-additional-properties.d.ts +1 -1
- package/dist/plugins/object-additional-properties/object-additional-properties.js +4 -2
- package/dist/plugins/object-additional-properties/object-additional-properties.mjs +4 -2
- package/dist/plugins/object-additional-properties/select-additional-keys.d.ts +15 -0
- package/dist/plugins/object-additional-properties/select-additional-keys.js +51 -0
- package/dist/plugins/object-additional-properties/select-additional-keys.mjs +47 -0
- package/dist/plugins/stitch/stitch.d.ts +28 -8
- package/dist/plugins/stitch-with/index.d.ts +2 -0
- package/dist/plugins/stitch-with/index.js +5 -0
- package/dist/plugins/stitch-with/index.mjs +1 -0
- package/dist/plugins/stitch-with/stitch-with.d.ts +12 -0
- package/dist/plugins/stitch-with/stitch-with.js +94 -0
- package/dist/plugins/stitch-with/stitch-with.mjs +91 -0
- package/dist/plugins/stitchWith.d.ts +1 -0
- package/dist/plugins/stitchWith.js +2 -0
- package/dist/plugins/stitchWith.mjs +1 -0
- package/dist/plugins/string-exact-length/string-exact-length.js +2 -2
- package/dist/plugins/string-exact-length/string-exact-length.mjs +3 -3
- package/dist/plugins/string-max/string-max.js +2 -2
- package/dist/plugins/string-max/string-max.mjs +3 -3
- package/dist/plugins/string-min/string-min.js +21 -3
- package/dist/plugins/string-min/string-min.mjs +21 -3
- package/dist/presets/index.d.ts +1 -0
- package/dist/presets/index.js +9 -0
- package/dist/presets/index.mjs +1 -0
- package/dist/presets/presets.d.ts +165 -0
- package/dist/presets/presets.js +80 -0
- package/dist/presets/presets.mjs +77 -0
- package/dist/runtime/create-field-validator.js +24 -8
- package/dist/runtime/create-field-validator.mjs +24 -8
- package/dist/runtime/create-validator.js +38 -8
- package/dist/runtime/create-validator.mjs +38 -8
- package/dist/runtime/decide-presence.js +2 -1
- package/dist/runtime/decide-presence.mjs +2 -1
- package/dist/runtime/field-rule-context.d.ts +32 -0
- package/dist/runtime/field-rule-context.js +48 -0
- package/dist/runtime/field-rule-context.mjs +44 -0
- package/dist/runtime/index-stack.d.ts +29 -4
- package/dist/runtime/index-stack.js +76 -15
- package/dist/runtime/index-stack.mjs +76 -15
- package/dist/runtime/run-array-node.js +22 -10
- package/dist/runtime/run-array-node.mjs +22 -10
- package/dist/runtime/run-field.js +47 -17
- package/dist/runtime/run-field.mjs +47 -17
- package/dist/runtime/run-plan.js +5 -1
- package/dist/runtime/run-plan.mjs +5 -1
- package/dist/standard-schema/index.d.ts +6 -0
- package/dist/standard-schema/index.js +7 -0
- package/dist/standard-schema/index.mjs +2 -0
- package/dist/standard-schema/split-issue-path.d.ts +9 -0
- package/dist/standard-schema/split-issue-path.js +66 -0
- package/dist/standard-schema/split-issue-path.mjs +63 -0
- package/dist/standard-schema/standard-schema.types.d.ts +46 -0
- package/dist/standard-schema/standard-schema.types.js +13 -0
- package/dist/standard-schema/standard-schema.types.mjs +12 -0
- package/dist/standard-schema/to-standard-schema.d.ts +41 -0
- package/dist/standard-schema/to-standard-schema.js +60 -0
- package/dist/standard-schema/to-standard-schema.mjs +57 -0
- package/dist/standard-schema.d.ts +1 -0
- package/dist/standard-schema.js +2 -0
- package/dist/standard-schema.mjs +1 -0
- package/dist/types/index.d.ts +24 -0
- package/dist/types/index.js +18 -0
- package/dist/types/index.mjs +17 -0
- package/package.json +42 -19
package/README.md
CHANGED
|
@@ -6,22 +6,52 @@
|
|
|
6
6
|
[](https://www.npmjs.com/package/@maroonedog/luq)
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
8
|
|
|
9
|
-
**A TypeScript validation library
|
|
9
|
+
**A TypeScript validation library where the wrong rule does not compile.**
|
|
10
10
|
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
Your types were probably not written by you. `openapi-typescript` generates
|
|
14
|
+
them from a spec you do not own. Prisma and Drizzle generate them from the
|
|
15
|
+
schema of record. protobuf and GraphQL codegen generate them for services in
|
|
16
|
+
four languages at once. Increasingly, a model generates the code that uses them.
|
|
17
|
+
|
|
18
|
+
A validator whose schema is the source of truth assumes you are the one who
|
|
19
|
+
decides the shape. When that assumption holds, it is the better arrangement and
|
|
20
|
+
this README will say so again below. When it does not, you end up maintaining a
|
|
21
|
+
second description of a shape you did not choose. You can have the compiler check
|
|
22
|
+
the copy against the original — zod's `satisfies z.ZodType<Order>` does exactly
|
|
23
|
+
that — but you still author it, update it, and remember to write the check.
|
|
24
|
+
|
|
25
|
+
Luq runs the other way. It takes the type you already have and lets you declare
|
|
26
|
+
rules against its field paths. What makes those declarations worth writing is
|
|
27
|
+
that the compiler checks them against the type: a rule that does not apply to
|
|
28
|
+
the field it is written on is a compile error, not a rule that quietly never
|
|
29
|
+
fires.
|
|
30
|
+
|
|
31
|
+
That matters most when the code calling this library is generated rather than
|
|
32
|
+
typed by hand. A generator that picks the wrong rule, misspells a path or drops
|
|
33
|
+
an array wildcard gets a red squiggle, not a validator that passes everything.
|
|
34
|
+
|
|
35
|
+
| Mistake | Result |
|
|
36
|
+
|---|---|
|
|
37
|
+
| A slot unrelated to the field's type (`b.string` on a `number`) | compile error |
|
|
38
|
+
| A missing `[*]` (`"items.name"`) | compile error |
|
|
39
|
+
| Descending into a built-in (`"when.getTime"` on a `Date`) | compile error |
|
|
40
|
+
| A method that does not exist inside an element sub-chain | compile error |
|
|
41
|
+
| A JSON Schema keyword bound to a chain method that does not exist | compile error |
|
|
42
|
+
| A documented example drifting from the API | fails CI |
|
|
43
|
+
|
|
44
|
+
Every rule you can call is a plugin you imported by name, so the bundle contains
|
|
45
|
+
what you used and nothing else.
|
|
16
46
|
|
|
17
|
-
Every number on this page was measured on this repository
|
|
18
|
-
is
|
|
19
|
-
|
|
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.
|
|
20
50
|
|
|
21
51
|
## Install
|
|
22
52
|
|
|
23
53
|
```bash
|
|
24
|
-
npm install @maroonedog/luq
|
|
54
|
+
npm install @maroonedog/luq
|
|
25
55
|
```
|
|
26
56
|
|
|
27
57
|
## Quick start
|
|
@@ -80,10 +110,155 @@ carries `data`, both branches carry `issues`, and each issue is
|
|
|
80
110
|
`{ path, code, message, severity }`.
|
|
81
111
|
|
|
82
112
|
> Every code block on this page is extracted and typechecked against the built
|
|
83
|
-
> package by `npm run check:docs`.
|
|
84
|
-
>
|
|
85
|
-
>
|
|
86
|
-
>
|
|
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
|
+
```
|
|
221
|
+
|
|
222
|
+
It counts leaves, so an optional property, a `Date`, an array's elements
|
|
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.
|
|
226
|
+
|
|
227
|
+
So the choice between "cover one field" and "cover everything" is a single call,
|
|
228
|
+
made per builder, and reported at compile time with the missing names rather than
|
|
229
|
+
at run time as a value that quietly passed. What `.strict()` does **not** cover is
|
|
230
|
+
properties that are not in the type; rejecting those is a run-time rule and
|
|
231
|
+
belongs to `additionalProperties(false)`.
|
|
232
|
+
|
|
233
|
+
## When schema-first is the right answer
|
|
234
|
+
|
|
235
|
+
Worth stating plainly, because it is a real tension and not a debating point:
|
|
236
|
+
**if the schema genuinely is your single source of truth, schema-first is the
|
|
237
|
+
coherent arrangement, and zod, valibot or TypeBox are the right tools.** You
|
|
238
|
+
write one artefact, your types come out of it, and there is nothing to keep in
|
|
239
|
+
step. That is a better position than Luq's, and Luq cannot give it to you.
|
|
240
|
+
|
|
241
|
+
Luq is for the case where that artefact already exists somewhere else and is not
|
|
242
|
+
yours to move — an OpenAPI document you consume, a Prisma schema, a `.proto`
|
|
243
|
+
shared with three other services, a type someone generated last week. There, the
|
|
244
|
+
schema-first arrangement asks you to author a *second* source of truth, and the
|
|
245
|
+
question stops being which library is nicer and becomes which copy is right.
|
|
246
|
+
|
|
247
|
+
Two things follow that are easy to miss:
|
|
248
|
+
|
|
249
|
+
- **Luq contains both directions.** `fromJsonSchema(document)` is schema-first —
|
|
250
|
+
the document decides, and Luq builds the rules from it. That is not a
|
|
251
|
+
contradiction to be argued away; it is the same principle applied to a
|
|
252
|
+
different upstream. What Luq declines to do is make you *hand-write* the second
|
|
253
|
+
copy.
|
|
254
|
+
- **You do not have to pick a side per project, only per boundary.** Standard
|
|
255
|
+
Schema means a zod schema and a Luq validator are interchangeable where they
|
|
256
|
+
meet, so "the schema is the truth here, the type is the truth there" is a
|
|
257
|
+
workable arrangement rather than an unresolved argument.
|
|
258
|
+
|
|
259
|
+
If you are starting from nothing and you will own the shape, use zod. It is
|
|
260
|
+
mature, it is everywhere, and every question you will have is already answered
|
|
261
|
+
somewhere.
|
|
87
262
|
|
|
88
263
|
## Field paths
|
|
89
264
|
|
|
@@ -122,6 +297,40 @@ A path that does not exist on the type is a compile error, not a silent no-op.
|
|
|
122
297
|
So is choosing a slot the field's type cannot be: `b.number` on a `string`
|
|
123
298
|
field fails to compile.
|
|
124
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
|
+
|
|
125
334
|
## Plugins are imports
|
|
126
335
|
|
|
127
336
|
There is no plugin registry to populate and no barrel you have to pay for.
|
|
@@ -145,7 +354,7 @@ const draftValidator = Builder()
|
|
|
145
354
|
export const isTitled = draftValidator.validate({ title: "x" }).valid;
|
|
146
355
|
```
|
|
147
356
|
|
|
148
|
-
|
|
357
|
+
78 plugin objects ship across 77 subpaths, plus one deprecated alias kept from
|
|
149
358
|
1.x. The complete table — subpath, symbol, chain method, slots — is generated
|
|
150
359
|
from the built package: **[docs/guide/plugin-reference.md](docs/guide/plugin-reference.md)**.
|
|
151
360
|
|
|
@@ -179,17 +388,78 @@ Measured Draft-07 conformance against the official
|
|
|
179
388
|
[JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite)
|
|
180
389
|
(required tests only, skipped cases counted as **failures**):
|
|
181
390
|
|
|
182
|
-
**
|
|
183
|
-
|
|
184
|
-
|
|
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:
|
|
185
396
|
[docs/json-schema-conformance.md](docs/json-schema-conformance.md).
|
|
186
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
|
+
|
|
187
403
|
`jsonSchemaFullFeature` bundles 49 plugins so one import covers a whole
|
|
188
404
|
document. `@maroonedog/luq/plugins/jsonSchema` adds a chain method instead, so a
|
|
189
405
|
single declared field can be constrained by a document; it takes the plugin bag
|
|
190
406
|
explicitly. It is billed as the tree-shakeable half, and measured below, it is
|
|
191
407
|
not — use it for the chain method, not to save bytes.
|
|
192
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
|
+
```
|
|
445
|
+
|
|
446
|
+
Three decisions the spec leaves open, made explicit here:
|
|
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
|
+
|
|
193
463
|
## Your own rules
|
|
194
464
|
|
|
195
465
|
```ts
|
|
@@ -237,21 +507,33 @@ so the two columns are comparable. Recorded in
|
|
|
237
507
|
|
|
238
508
|
| Entry | gzip | 1.x, same method |
|
|
239
509
|
|---|---:|---:|
|
|
240
|
-
|
|
241
|
-
|
|
|
242
|
-
|
|
|
243
|
-
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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.
|
|
251
533
|
|
|
252
534
|
Two lines that are **not** wins:
|
|
253
535
|
|
|
254
|
-
- "all
|
|
536
|
+
- "all 77 plugins at 25,607 B" is larger than the 23,015 B 1.x published for its
|
|
255
537
|
`complex` case. The two are not comparable — 1.x's figure was one schema's
|
|
256
538
|
plugin set, not its whole catalogue — so it is not counted either way here.
|
|
257
539
|
- The 18,992 B for `jsonSchema` measures the plugin **without a bag**, which is
|
|
@@ -271,15 +553,17 @@ skipped. Every subject rotates over a pool of at least four distinct values —
|
|
|
271
553
|
one frozen input let V8 delete a subject outright, which is the artefact
|
|
272
554
|
described below. Each figure is the median of the fastest half of 9 samples; the
|
|
273
555
|
spread quoted alongside is the full range over that figure, and on these ten it
|
|
274
|
-
is 2
|
|
556
|
+
is <!-- generated:perf-spread -->1.2–12.3%<!-- /generated:perf-spread -->.
|
|
275
557
|
|
|
276
558
|
| Shape | `validate` ops/sec | `parse` ops/sec |
|
|
277
559
|
|---|---:|---:|
|
|
278
|
-
|
|
279
|
-
|
|
|
280
|
-
|
|
|
281
|
-
|
|
|
282
|
-
|
|
|
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 -->
|
|
283
567
|
|
|
284
568
|
**This rewrite is slower than 1.x on flat and nested shapes.** Measured side by
|
|
285
569
|
side, in one process on one machine, 1.x source against this source, sample by
|
|
@@ -287,11 +571,13 @@ sample interleaved so a drift in the machine hits both halves of every ratio:
|
|
|
287
571
|
|
|
288
572
|
| Shape | 1.x | this | ratio |
|
|
289
573
|
|---|---:|---:|---:|
|
|
290
|
-
|
|
291
|
-
|
|
|
292
|
-
|
|
|
293
|
-
|
|
|
294
|
-
|
|
|
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 -->
|
|
295
581
|
|
|
296
582
|
1.x carried a directory of specialised fast paths that this implementation has
|
|
297
583
|
no equivalent of. The comparison was checked for the ways it could be wrong: 1.x
|
|
@@ -301,8 +587,9 @@ reject the rejected pool before either is timed.
|
|
|
301
587
|
|
|
302
588
|
Also worth stating plainly: **neither figure 1.x's README published reproduces
|
|
303
589
|
here.** It claimed 1.2M ops/sec simple and 43K complex; on this machine 1.x
|
|
304
|
-
itself does 3.
|
|
305
|
-
|
|
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.
|
|
306
593
|
|
|
307
594
|
`build()` costs 14–662 µs depending on shape, against sub-microsecond
|
|
308
595
|
`validate()` calls — so one `build()` pays for itself after 35–100 `validate()`
|
|
@@ -338,20 +625,24 @@ figures recorded here span 0.01–0.86.
|
|
|
338
625
|
|
|
339
626
|
No `eval`, no `new Function`. Checked mechanically over all 762 emitted `.js`
|
|
340
627
|
and `.mjs` files by `npm run check:no-dynamic-code`, and over `src/` by the
|
|
341
|
-
public-API smoke test: **0 occurrences**.
|
|
342
|
-
|
|
343
|
-
|
|
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.
|
|
344
634
|
|
|
345
635
|
### Package
|
|
346
636
|
|
|
347
|
-
|
|
637
|
+
86 keys in `exports`, every one resolving to files that exist: 8 fixed keys
|
|
348
638
|
(`.`, `./package.json`, `./result`, `./plugin-kit`, `./field-rule`, `./async`,
|
|
349
|
-
`./plugins`) and
|
|
350
|
-
|
|
351
|
-
`
|
|
352
|
-
|
|
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/`,
|
|
353
644
|
no raw `.ts` and no source maps. A scratch consumer typechecks **every one of
|
|
354
|
-
the
|
|
645
|
+
the 86 keys** against the published declarations under **both** `node16` and
|
|
355
646
|
`bundler` resolution, and an unpublished subpath is proven to fail.
|
|
356
647
|
|
|
357
648
|
1.x's `createPluginRegistry` / `useField` / `createFieldRule` are published at
|
|
@@ -365,7 +656,8 @@ the 84 keys** against the published declarations under **both** `node16` and
|
|
|
365
656
|
- **[Field paths](docs/guide/field-paths.md)** — what a path may be, and what
|
|
366
657
|
changed from 1.x
|
|
367
658
|
- **[Presence and conditionals](docs/guide/presence-and-conditionals.md)** —
|
|
368
|
-
`required` / `optional` / `nullable` / `requiredIf
|
|
659
|
+
`required` / `optional` / `nullable` / `requiredIf`, the order rules run
|
|
660
|
+
in, and the cross-field rules (`compareField` / `stitch` / `stitchWith`)
|
|
369
661
|
- **[JSON Schema](docs/guide/json-schema.md)** — the two front doors, and the
|
|
370
662
|
keywords that are not supported
|
|
371
663
|
- **[Writing a plugin](docs/guide/writing-a-plugin.md)** — markers, `out`,
|
|
@@ -374,15 +666,30 @@ the 84 keys** against the published declarations under **both** `node16` and
|
|
|
374
666
|
every subpath, method and slot
|
|
375
667
|
- **[Breaking changes from 1.x](docs/migration/breaking-changes.md)** — every
|
|
376
668
|
incompatibility with the fix beside it
|
|
377
|
-
- **[Draft-07 conformance](docs/json-schema-conformance.md)** — the
|
|
378
|
-
|
|
669
|
+
- **[Draft-07 conformance](docs/json-schema-conformance.md)** — the 100% and
|
|
670
|
+
what closed each of the ten causes that used to fail
|
|
671
|
+
|
|
672
|
+
## Status, and how this gets changed
|
|
673
|
+
|
|
674
|
+
The 2.x API is stable and the surface below is gated, but the production track
|
|
675
|
+
record is still short. Breaking changes happen in a major and nowhere else, an
|
|
676
|
+
API being removed is deprecated one major ahead, and each major ships with the
|
|
677
|
+
codemod needed to cross it.
|
|
678
|
+
|
|
679
|
+
- **[CONTRIBUTING.md](CONTRIBUTING.md)** — `npm run verify` is the whole
|
|
680
|
+
contract; the gates and what each one refuses
|
|
681
|
+
- **[SECURITY.md](SECURITY.md)** — reporting, zero runtime dependencies, the
|
|
682
|
+
prototype-pollution and SSRF positions, and what is *not* protected against
|
|
683
|
+
- **[docs/RELEASING.md](docs/RELEASING.md)** — the release steps, the versioning
|
|
684
|
+
policy, what each CI workflow watches, and what is still decided by hand
|
|
379
685
|
|
|
380
686
|
## About the "universal platform" goal
|
|
381
687
|
|
|
382
|
-
1.x
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
688
|
+
1.x described a `.luq` DSL that would generate validators for other languages,
|
|
689
|
+
against dated milestones. Those dates have passed and none of it shipped, so the
|
|
690
|
+
plan has been withdrawn rather than moved: no part of it is in this package, and
|
|
691
|
+
this release makes no claim about when any of it will exist. What is in the box
|
|
692
|
+
is the TypeScript validation library described above.
|
|
386
693
|
|
|
387
694
|
## License
|
|
388
695
|
|
|
@@ -34,6 +34,7 @@ export interface FieldBuilderSurface {
|
|
|
34
34
|
/** The erased twin of Builder<B>. `use` mutates and returns the receiver. */
|
|
35
35
|
export interface BuilderSurface {
|
|
36
36
|
use(plugin: AnyPlugin): BuilderSurface;
|
|
37
|
+
useAll(plugins: PluginBag): BuilderSurface;
|
|
37
38
|
withConfig(config: GlobalConfig): BuilderSurface;
|
|
38
39
|
for(): FieldBuilderSurface;
|
|
39
40
|
}
|
|
@@ -50,6 +50,15 @@ function createBuilderSurface() {
|
|
|
50
50
|
registerPlugin(registration.plugins, plugin);
|
|
51
51
|
return surface;
|
|
52
52
|
},
|
|
53
|
+
useAll(plugins) {
|
|
54
|
+
// 順序は Object.values の列挙順。first-wins なので、同じ名前が二度
|
|
55
|
+
// 来ても最初のものが残る — プリセットが既に登録したものを黙って
|
|
56
|
+
// 置き換えることはない。
|
|
57
|
+
for (const plugin of Object.values(plugins)) {
|
|
58
|
+
registerPlugin(registration.plugins, plugin);
|
|
59
|
+
}
|
|
60
|
+
return surface;
|
|
61
|
+
},
|
|
53
62
|
withConfig(config) {
|
|
54
63
|
registration.config = Object.assign({}, registration.config, config);
|
|
55
64
|
return surface;
|
|
@@ -44,6 +44,15 @@ export function createBuilderSurface() {
|
|
|
44
44
|
registerPlugin(registration.plugins, plugin);
|
|
45
45
|
return surface;
|
|
46
46
|
},
|
|
47
|
+
useAll(plugins) {
|
|
48
|
+
// 順序は Object.values の列挙順。first-wins なので、同じ名前が二度
|
|
49
|
+
// 来ても最初のものが残る — プリセットが既に登録したものを黙って
|
|
50
|
+
// 置き換えることはない。
|
|
51
|
+
for (const plugin of Object.values(plugins)) {
|
|
52
|
+
registerPlugin(registration.plugins, plugin);
|
|
53
|
+
}
|
|
54
|
+
return surface;
|
|
55
|
+
},
|
|
47
56
|
withConfig(config) {
|
|
48
57
|
registration.config = Object.assign({}, registration.config, config);
|
|
49
58
|
return surface;
|
|
@@ -26,6 +26,19 @@ export interface FieldBuilder<T extends object, B extends PluginBag, TDeclared e
|
|
|
26
26
|
}
|
|
27
27
|
export interface Builder<B extends PluginBag = Record<never, never>> {
|
|
28
28
|
use<P extends AnyPlugin>(plugin: P): Builder<B & BagEntry<P>>;
|
|
29
|
+
/**
|
|
30
|
+
* A whole SET of plugins at once — a preset, or any object of them.
|
|
31
|
+
*
|
|
32
|
+
* A bag is a name -> plugin map, so a preset is that value and nothing more;
|
|
33
|
+
* there is no registry and no preset type to learn. `use()` one at a time
|
|
34
|
+
* still works and still costs only what it names, which is the point of the
|
|
35
|
+
* subpaths — this is for the case where writing fifteen `use()` lines is the
|
|
36
|
+
* thing standing between you and the validator.
|
|
37
|
+
*
|
|
38
|
+
* Duplicates follow the same rule as `use()`: FIRST WINS, so a preset cannot
|
|
39
|
+
* quietly replace a plugin you already registered.
|
|
40
|
+
*/
|
|
41
|
+
useAll<Bag extends PluginBag>(plugins: Bag): Builder<B & Bag>;
|
|
29
42
|
/**
|
|
30
43
|
* The per-builder override of the process-wide GlobalConfig. Merged over
|
|
31
44
|
* getGlobalConfig() once, at build(), and handed to every plugin as
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FieldPath } from "../path/field-path.types";
|
|
2
|
+
import type { ValueAtPath } from "../path/value-at-path.types";
|
|
3
|
+
import type { AnyChain } from "./field-chain.types";
|
|
4
|
+
import type { FieldSlots } from "./field-slots.types";
|
|
5
|
+
import type { PluginBag } from "./plugin-bag.types";
|
|
6
|
+
/** 別名 -> ルートのパス。パスはルートに実在するものしか書けない。 */
|
|
7
|
+
export type BundlePaths<TRoot> = Readonly<Record<string, FieldPath<TRoot> & string>>;
|
|
8
|
+
/** 対応表から組み上がる束。別名がキーで、値はそのパスの値。 */
|
|
9
|
+
export type BundleOf<TRoot, M extends BundlePaths<TRoot>> = {
|
|
10
|
+
readonly [A in keyof M & string]: ValueAtPath<TRoot, M[A]>;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* 束ひとつに対するサブチェーン。
|
|
14
|
+
*
|
|
15
|
+
* stitch の核は「複数のフィールドを **1つの判定** にまとめる」ことなので、
|
|
16
|
+
* 主体は束そのものであって別名ごとではない。別名ごとにルールを並べる形も
|
|
17
|
+
* 書けるが、それは `total === price * quantity` のような判定が書けず、
|
|
18
|
+
* stitch ではなくなる。
|
|
19
|
+
*
|
|
20
|
+
* 主体が束なので、束の中を `.v()` 的に見るのではなく、`b.object` などの
|
|
21
|
+
* スロットがそのまま開く。型は BundleOf なので、束のメンバーは補完も効くし
|
|
22
|
+
* 取り違えればコンパイルエラーになる — stitch が
|
|
23
|
+
* `Readonly<Record<string, unknown>>` を渡していたのに対する、ここの一点だけ
|
|
24
|
+
* が違いである。
|
|
25
|
+
*/
|
|
26
|
+
export type BundleChain<TRoot, M extends BundlePaths<TRoot>, B extends PluginBag> = (b: FieldSlots<BundleOf<TRoot, M>, B, BundleOf<TRoot, M>>) => AnyChain;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|