@jarenjs/linq 0.49.2 → 0.56.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/ARCHITECTURE.md +217 -0
- package/README.md +559 -17
- package/docs/APP-PEN.md +1143 -0
- package/docs/CONTRACT-PEN.md +1217 -0
- package/docs/DB-CLIENT.md +814 -0
- package/docs/FLOW-PEN.md +1026 -0
- package/docs/FORMS-PEN.md +940 -0
- package/docs/JSLT-PEN.md +955 -0
- package/docs/LINQ-FORMAT.md +771 -383
- package/docs/MIGRATION-PEN.md +781 -0
- package/docs/MODEL-PEN.md +1083 -0
- package/docs/QUERY-PEN.md +1636 -0
- package/docs/SCHEMA-PEN.md +1218 -0
- package/package.json +57 -4
- package/src/app/action.js +255 -0
- package/src/app/capture.js +63 -0
- package/src/app/define.js +260 -0
- package/src/app/index.js +20 -0
- package/src/app/patch.js +277 -0
- package/src/app/sub.js +106 -0
- package/src/async.js +329 -75
- package/src/capture-root.js +82 -0
- package/src/concurrency.js +9 -4
- package/src/contract/define.js +269 -0
- package/src/contract/http.js +247 -0
- package/src/contract/index.js +23 -0
- package/src/contract/operation.js +342 -0
- package/src/db/handle.js +86 -0
- package/src/db/include.js +316 -0
- package/src/db/index.js +19 -0
- package/src/db/live.js +43 -0
- package/src/db/membership.js +37 -0
- package/src/db/open.js +82 -0
- package/src/document.js +143 -13
- package/src/effect.js +65 -0
- package/src/errors.js +69 -6
- package/src/expression.js +437 -36
- package/src/flow/capture.js +33 -0
- package/src/flow/dag.js +302 -0
- package/src/flow/fsm.js +328 -0
- package/src/flow/index.js +22 -0
- package/src/forms/index.js +43 -0
- package/src/forms/rules.js +170 -0
- package/src/forms/submit.js +177 -0
- package/src/index.js +4 -2
- package/src/jslt/body.js +226 -0
- package/src/jslt/index.js +18 -0
- package/src/jslt/rules.js +207 -0
- package/src/json-boundary.js +90 -0
- package/src/migration/define.js +323 -0
- package/src/migration/index.js +15 -0
- package/src/migration/steps.js +248 -0
- package/src/model/collection.js +171 -0
- package/src/model/define.js +125 -0
- package/src/model/entity.js +307 -0
- package/src/model/index.js +47 -0
- package/src/model/relation.js +85 -0
- package/src/provider.js +137 -20
- package/src/schema/brand.js +31 -0
- package/src/schema/builders.js +526 -0
- package/src/schema/check.js +29 -0
- package/src/schema/emit.js +394 -0
- package/src/schema/factories.js +239 -0
- package/src/schema/index.js +37 -0
- package/src/schema-of.js +24 -0
- package/src/sequence.js +233 -103
- package/src/sources.js +10 -3
- package/types/app.d.ts +293 -0
- package/types/contract.d.ts +371 -0
- package/types/db.d.ts +188 -0
- package/types/flow.d.ts +285 -0
- package/types/forms.d.ts +253 -0
- package/types/index.d.ts +231 -26
- package/types/jslt.d.ts +193 -0
- package/types/migration.d.ts +201 -0
- package/types/model.d.ts +493 -0
- package/types/schema.d.ts +494 -0
package/docs/JSLT-PEN.md
ADDED
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
# The Jaren JSLT pen
|
|
2
|
+
|
|
3
|
+
> `./jslt` — `$jslt` 0.1 stylesheets: the envelope and its rules, whose
|
|
4
|
+
> bodies are captured over the matched value. **Read it when** you are
|
|
5
|
+
> transforming one document into another
|
|
6
|
+
|
|
7
|
+
Version 0.1. The key words MUST, MUST NOT, SHOULD and MAY are to be
|
|
8
|
+
interpreted as described in RFC 2119. This document is a **guide** — read
|
|
9
|
+
it in order and you can write the format — whose one normative section is
|
|
10
|
+
[§2 The mapping table](#2-the-mapping-table); the rules every pen keeps, the shared refusal table, the
|
|
11
|
+
index of the other pens and every pen's mapping table collected in one
|
|
12
|
+
place are the normative reference,
|
|
13
|
+
[LINQ-FORMAT.md](LINQ-FORMAT.md).
|
|
14
|
+
|
|
15
|
+
## 1. What it writes
|
|
16
|
+
|
|
17
|
+
You have one shape of JSON and you need another one — a record reshaped
|
|
18
|
+
for an API, a document rendered as a view, a value converted everywhere it
|
|
19
|
+
appears in a tree. A stylesheet is the declarative way to say that, and
|
|
20
|
+
writing one by hand means writing JSONPath strings and operator objects
|
|
21
|
+
into JSON literals with nothing checking either. This pen lets you write
|
|
22
|
+
each rule's body as a JavaScript arrow function and hands you the document
|
|
23
|
+
it recorded.
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
import { stylesheet, rule, body, apply, op } from '@jarenjs/linq/jslt';
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
writes `$jslt` 0.1 stylesheets ([JSLT-FORMAT](../../json/docs/JSLT-FORMAT.md)):
|
|
30
|
+
the envelope and its rules, whose bodies are callbacks captured over `$` —
|
|
31
|
+
the matched value — through the chain's recording proxy, with the two
|
|
32
|
+
externals the engine binds on every dispatch (`root`, `path`) and the
|
|
33
|
+
parameters a body declares. The document is what `compileJsltStylesheet`
|
|
34
|
+
takes unchanged; the grammar it validates under is `jaren-jslt`
|
|
35
|
+
(`packages/json/schemas/jaren-jslt.schema.json`, and its draft-07 twin);
|
|
36
|
+
the engine is `@jarenjs/json`'s stylesheet dispatcher.
|
|
37
|
+
|
|
38
|
+
The pen imports no engine and judges nothing the compiler judges — path
|
|
39
|
+
syntax (`JT0003`), the body's operators (`JT0007`), a `schema` match's
|
|
40
|
+
hook (`JT0006`), the depth guard (`JT2001`) — with one exception it can
|
|
41
|
+
see earlier: the `[]` idiom of JSLT-FORMAT §6.3.
|
|
42
|
+
|
|
43
|
+
**The running example.** §3 is one publisher's catalogue, seen eight ways:
|
|
44
|
+
summarised, walked as a book of chapters, repriced in another currency,
|
|
45
|
+
rendered twice under two modes, appraised through a registered operator,
|
|
46
|
+
sorted by an importer's log records, rendered as a view, and matched by a
|
|
47
|
+
schema builder. Each rule set stands alone — that is what the gate runs —
|
|
48
|
+
but they are all over the same kind of document, so a member you meet in
|
|
49
|
+
§3.1 means the same thing in §3.8.
|
|
50
|
+
|
|
51
|
+
**A stylesheet is recursive, and a two-rule example hides it.** The whole
|
|
52
|
+
point of a template language is that a rule's body dispatches BACK into
|
|
53
|
+
the rule set for its children, so the shape a reader should have in mind
|
|
54
|
+
is a walk, not a mapping:
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
import { stylesheet, rule, apply } from '@jarenjs/linq/jslt';
|
|
58
|
+
|
|
59
|
+
export const outline = stylesheet([
|
|
60
|
+
rule('$', (v) => ({ name: v.name, children: [apply(v.children.all())] })),
|
|
61
|
+
rule('$..children[*]', (v) => ({ name: v.name, children: [apply(v.children.all())] })),
|
|
62
|
+
]);
|
|
63
|
+
// the second rule applies to its own children, so one rule walks a tree
|
|
64
|
+
// of any depth: { name: '$.name', children: [ { $apply: '$.children[*]' } ] }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Both rules emit the same body; the second one reaching its own matches is
|
|
68
|
+
what makes the stylesheet a recursive walk. §3.2 shows the same shape with
|
|
69
|
+
two different bodies, which is what a real transform looks like, and §3.4
|
|
70
|
+
shows the same walk split across two modes.
|
|
71
|
+
|
|
72
|
+
### 1.1 How a JavaScript callback becomes a document
|
|
73
|
+
|
|
74
|
+
This is the one thing about this pen a reader cannot get from
|
|
75
|
+
JSLT-FORMAT, and it is stated here once. The flow pen
|
|
76
|
+
([FLOW-PEN.md](FLOW-PEN.md) §5), the migration pen
|
|
77
|
+
([MIGRATION-PEN.md](MIGRATION-PEN.md) §4.3) and the app pen
|
|
78
|
+
([APP-PEN.md](APP-PEN.md)) capture through the same machine and link
|
|
79
|
+
this section rather than restate it.
|
|
80
|
+
|
|
81
|
+
A body is not parsed and it is not serialized. It is **run once, at build
|
|
82
|
+
time, against a recording proxy** — the same proxy the chain's `select()`
|
|
83
|
+
records — and the document is what the recording left behind. Five
|
|
84
|
+
consequences, in the order a writer meets them:
|
|
85
|
+
|
|
86
|
+
1. **A member read records a path.** `v.author.name` is not a value; it
|
|
87
|
+
is a proxy that answers another proxy for every member read and
|
|
88
|
+
remembers the walk. When the callback returns, that walk has become the
|
|
89
|
+
string `"$.author.name"`. `v.chapters.all()` records the wildcard
|
|
90
|
+
segment (`"$.chapters[*]"`), `v.get('sub title')` records a bracket
|
|
91
|
+
segment (`"$['sub title']"`), and an operator method records the
|
|
92
|
+
operator (`v.price.mul(1.21)` → `{ "$mul": ["$.price", 1.21] }`). The
|
|
93
|
+
operators are the chain's own and are documented once, in
|
|
94
|
+
[QUERY-PEN.md](QUERY-PEN.md) §4.
|
|
95
|
+
2. **The callback runs exactly once, with no data — so JavaScript's own
|
|
96
|
+
logic is not the document's.** There is no input value to branch on,
|
|
97
|
+
and the ways that goes wrong divide in two. `>`, `<`, `+` and template
|
|
98
|
+
interpolation throw a plain `TypeError` ("Cannot convert object to
|
|
99
|
+
primitive value"): loud, uncoded, and impossible to miss. But `!`,
|
|
100
|
+
`&&`, `||`, the ternary, `in`, `typeof` and `Object.keys` all evaluate
|
|
101
|
+
against the PROXY and silently write the wrong document —
|
|
102
|
+
`() => ({ live: !v.deleted })` writes `{ "live": false }`,
|
|
103
|
+
`() => ({ both: v.a && v.b })` keeps only the right operand
|
|
104
|
+
(`{ "both": "$.b" }`), and `v.flag ? 'a' : 'b'` is always `'a'`. A
|
|
105
|
+
condition belongs in the document: the expression surface's `.and()`,
|
|
106
|
+
`.or()`, `.not()`, an operator (`op('$if', …)`), or a second rule with
|
|
107
|
+
a narrower `match`. [QUERY-PEN.md](QUERY-PEN.md) §3 states this once
|
|
108
|
+
for every capture in the suite.
|
|
109
|
+
3. **The externals argument is a closed world.** `body()`'s second
|
|
110
|
+
argument answers exactly the names the engine will bind: `root` and
|
|
111
|
+
`path` always (JSLT-FORMAT §8.2), plus each name the body declared in
|
|
112
|
+
`{ externals: [...] }` (§8.1). Any other name is `JL0104` at build
|
|
113
|
+
time, where the fix can be named — see §4.3. That is the whole reason
|
|
114
|
+
the argument is a proxy and not an object: an object would answer
|
|
115
|
+
`undefined` and the mistake would surface at run time as a missing
|
|
116
|
+
binding.
|
|
117
|
+
4. **A returned literal is a CONSTRUCTOR, not a constant.** The chain
|
|
118
|
+
folds a pure data tree into one `$const` ([QUERY-PEN.md](QUERY-PEN.md)
|
|
119
|
+
§3, expression capture); a body does not. `() => ({ level: 'unknown' })`
|
|
120
|
+
writes `{ "level": "unknown" }` — the format's own object constructor,
|
|
121
|
+
JSLT-FORMAT Appendix A.6's spelling — because that is what a
|
|
122
|
+
stylesheet author reads and edits.
|
|
123
|
+
The pen passes `fold: false` to the shared capture for exactly this.
|
|
124
|
+
The cost is that a string is now ambiguous with a path, so a string
|
|
125
|
+
value that starts with `$` is escaped `$$` on the way out
|
|
126
|
+
(`() => '$x'` writes `"$$x"`); the engine unescapes it.
|
|
127
|
+
5. **The document is a value.** What comes back is plain, deep-frozen
|
|
128
|
+
JSON — never the caller's object, never a proxy. Round-tripping it
|
|
129
|
+
through `JSON.stringify` and `JSON.parse` deep-equals it, two builds
|
|
130
|
+
of one spelling are one document, and an array the caller passed in
|
|
131
|
+
stays unfrozen. Captures
|
|
132
|
+
also nest: a `body()` built inside another body's callback is its own
|
|
133
|
+
document, and embedding it in the outer literal makes it DATA there
|
|
134
|
+
(the `$$` escape says so).
|
|
135
|
+
|
|
136
|
+
The machine underneath is `captureQuery` in
|
|
137
|
+
`packages/linq/src/capture-root.js` — a value rooted at `$` plus a list
|
|
138
|
+
of named externals — and `body()` is the JSLT pen's entry point into it
|
|
139
|
+
(`packages/linq/src/jslt/body.js`). The migration pen calls `body()`
|
|
140
|
+
itself, so a `jslt` migration step's externals are `root` and `path` too.
|
|
141
|
+
The flow and app pens call `captureQuery` directly with an EMPTY external
|
|
142
|
+
list, because their engines bind nothing; [FLOW-PEN.md](FLOW-PEN.md) §5
|
|
143
|
+
states what that changes.
|
|
144
|
+
|
|
145
|
+
## 2. The mapping table
|
|
146
|
+
|
|
147
|
+
Five exported names — `stylesheet`, `rule`, `body`, `apply` and `op` —
|
|
148
|
+
and seventeen rows, because every member and option a call takes earns
|
|
149
|
+
one of its own (`match`'s three forms, `mode`, `priority`, `unmatched`,
|
|
150
|
+
`modes`, and the two things that can stand where a body is taken). The
|
|
151
|
+
completeness gate in `test/linq/pen-docs.test.js` asserts that every
|
|
152
|
+
exported callable name appears somewhere in this section; rows
|
|
153
|
+
legitimately outnumber names here, which is why that gate is
|
|
154
|
+
one-directional, and this pen is its clearest case.
|
|
155
|
+
|
|
156
|
+
| Method | Emits | Type reading | Status |
|
|
157
|
+
|---|---|---|---|
|
|
158
|
+
| `stylesheet(rules, { unmatched?, modes? })` | `{ $jslt: '0.1', unmatched?, modes?, rules }` — the envelope (§2.1), in that member order; the bare-array form is the rules array itself | `Stylesheet<In, Out>`: the FIRST rule's phantoms, or the author's (`stylesheet<In, Out>(…)`) | native; a non-array, another option, a rule that is not an object, a rule without `body`, a rule that is not JSON `JL0101` |
|
|
159
|
+
| `unmatched` | `unmatched: 'share' \| 'fresh' \| 'error'` (§5) | `Disposition` | native; another value `JL0101` |
|
|
160
|
+
| `modes` | `modes: { name: { unmatched } }` (§2.1) | — | native; another member, a mode that is not `{ unmatched }`, a map whose prototype a `__proto__:` literal replaced `JL0101` |
|
|
161
|
+
| `rule(match, body, { mode?, priority? })` | `{ mode?, match?, priority?, body }` — the rule object (§2.2), in that member order | `Rule<In, Out>` | native; a non-object options, another option `JL0101` |
|
|
162
|
+
| `match` as a JSONPath string | `match: '$..price'` (§3.1) | the honest top | native |
|
|
163
|
+
| `match` as `{ path?, schema? }` | the object; `schema` a schema-pen builder's document, or a schema verbatim | a builder types the body's value (`Infer<>`) | native; `{}` is `JL0102`; another member, a non-string `path`, a `schema` that is not JSON `JL0101` |
|
|
164
|
+
| `match` `null` or absent | no `match` member — the unconditional rule (default priority −1, §4) | the honest top | native |
|
|
165
|
+
| `mode` | `mode: 'toc'` (§7) | `string` | native; a non-string `JL0101` |
|
|
166
|
+
| `priority` | `priority: 2` (§4) | `number` | native; a non-finite number, or `-0`, `JL0101` |
|
|
167
|
+
| `body(fn, { externals? })` | the captured query document — `fn(v, x)` with `v` at `$`, `x.root`/`x.path` (§8.2) and the declared parameters as `$name` externals (§8.1); a returned literal is a constructor, a string starting `$` is escaped `$$` | `BodyDocument<In, Out>`: `In` from the annotated `v` (`(v: Expr<Book>) => …`), `Out` the unwrapped return; a declared parameter is `UnknownExpr` until `x` is annotated (`x: Externals<{ rate: number }>`) | native; a non-callback, a non-object options, another option, a non-array or non-identifier external `JL0101`; an undeclared external `JL0104`; `root`/`path` declared `JL0104` |
|
|
168
|
+
| a callback where a body is taken | `body(fn)` with no parameters | as above; the match's builder types `v` | native |
|
|
169
|
+
| a query document where a body is taken | the document, verbatim (a `body()` result, or by hand) | a `body()` document carries its phantoms; a hand-written one is `unknown` | native; not JSON `JL0101` |
|
|
170
|
+
| `apply(selector)` | `{ $apply: selector }` — the rule's own mode (§6.2); the selector an expression (`v.chapters.all()`), a path string verbatim (`'$.chapters[*]'`), or data (`[1, 2]` embeds as `$const`) | `UnknownExpr` — a dispatch to other rules | native; no selector `JL0101`; outside `body()` `JL0102` |
|
|
171
|
+
| `apply(selector, mode)` | `{ $apply: [selector, mode] }` — the argument-list form (§6.2) | `UnknownExpr` | native; a non-string mode `JL0101` |
|
|
172
|
+
| `[apply(…)]` as a member value | `[{ $apply: … }]` — the `[]` idiom (§6.3) | `unknown[]` | native |
|
|
173
|
+
| `apply(…)` as a bare member value | — | — | refused (`JL0102`): the engine fails at run time on the second child (`JQ2001`) |
|
|
174
|
+
| `op(name, operands)` | `{ [name]: operands }` — a registered operator (§13), spelled without judging it; one operand or a list | `UnknownExpr` | native; the engine's `JQ0002` decides; a name without `$` `JL0101`; outside any capture `JL0005` |
|
|
175
|
+
|
|
176
|
+
Three rules the table implies, spelled out:
|
|
177
|
+
|
|
178
|
+
- **A body's literal is a constructor, not a constant.** §1.1 point 4 is
|
|
179
|
+
where the reason lives; the consequence for the table is that a `body`
|
|
180
|
+
cell never shows a `$const` unless the value reached the document as
|
|
181
|
+
DATA (an `apply()` selector that is plain data, an `op()` operand that
|
|
182
|
+
is an object).
|
|
183
|
+
- **`root` and `path` need no declaration; a parameter needs one.** The
|
|
184
|
+
engine binds the two on every dispatch (§8.2) and shadows any binding
|
|
185
|
+
of the same name, so declaring them is the mistake and is refused. A
|
|
186
|
+
parameter is `body(fn, { externals: ['rate'] })`, and
|
|
187
|
+
`transform.externals` lists exactly the declared names the body used
|
|
188
|
+
(§8.3).
|
|
189
|
+
- **The pen judges nothing the compiler judges.** A path that does not
|
|
190
|
+
parse, an operator no registry answers, a `schema` match compiled
|
|
191
|
+
without a hook, a self-applying loop: each is the engine's own error
|
|
192
|
+
(`JT0003`, `JT0007`/`JQ0002`, `JT0006`, `JT2001`), unwrapped. The one
|
|
193
|
+
refusal the pen adds is the one the engine would only raise at RUN
|
|
194
|
+
time.
|
|
195
|
+
|
|
196
|
+
## 3. Worked examples
|
|
197
|
+
|
|
198
|
+
Every `js` fence below exports exactly one stylesheet (or rule list), and
|
|
199
|
+
the `json` fence that follows it is what the pen emits — executed by
|
|
200
|
+
`test/linq/pen-docs.test.js`, which imports each fence from the workspace
|
|
201
|
+
and compares its one export to the JSON beside it. The seven fixtures of
|
|
202
|
+
JSLT-FORMAT Appendix A are all rebuilt through the pen and held BYTE-equal
|
|
203
|
+
to the format doc's own fences by `test/linq/jslt-pen.test.js`; three of
|
|
204
|
+
them are here, and every document below also validates under both
|
|
205
|
+
published grammars.
|
|
206
|
+
|
|
207
|
+
### 3.1 A rule at the root, with a `$const`-free body
|
|
208
|
+
|
|
209
|
+
The plainest thing the pen does: one rule matching the whole document,
|
|
210
|
+
and a body of member reads and operators. Nothing here is data, so
|
|
211
|
+
nothing here is `$const`.
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
import { stylesheet, rule } from '@jarenjs/linq/jslt';
|
|
215
|
+
|
|
216
|
+
export const summary = stylesheet([
|
|
217
|
+
rule('$', (v) => ({
|
|
218
|
+
isbn: v.isbn,
|
|
219
|
+
author: v.author.name.upper(),
|
|
220
|
+
pages: v.chapters.all().pages.sum(),
|
|
221
|
+
chapters: v.chapters.all().count(),
|
|
222
|
+
})),
|
|
223
|
+
]);
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{ "$jslt": "0.1",
|
|
228
|
+
"rules": [
|
|
229
|
+
{ "match": "$",
|
|
230
|
+
"body": { "isbn": "$.isbn",
|
|
231
|
+
"author": { "$upper": "$.author.name" },
|
|
232
|
+
"pages": { "$sum": "$.chapters[*].pages" },
|
|
233
|
+
"chapters": { "$count": "$.chapters[*]" } } }
|
|
234
|
+
] }
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Over `{ isbn: '978-1', author: { name: 'ada' }, chapters: [{ pages: 3 }, { pages: 4 }] }`
|
|
238
|
+
the compiled transform answers
|
|
239
|
+
`{ isbn: '978-1', author: 'ADA', pages: 7, chapters: 2 }`. Note
|
|
240
|
+
`v.chapters.all().count()`, not `v.chapters.count()`: the first counts the
|
|
241
|
+
ITEMS the wildcard yields, the second counts the one value `$.chapters` is.
|
|
242
|
+
That distinction is the chain's, not this pen's, and QUERY-PEN §4 is
|
|
243
|
+
where it is stated.
|
|
244
|
+
|
|
245
|
+
### 3.2 An `apply()` splice, and the `[]` idiom
|
|
246
|
+
|
|
247
|
+
JSLT-FORMAT Appendix A.3 — the book example, done right. The brackets
|
|
248
|
+
around the `apply()` are not decoration: they are what splices the
|
|
249
|
+
dispatched sequence into one array.
|
|
250
|
+
|
|
251
|
+
```js
|
|
252
|
+
import { stylesheet, rule, apply } from '@jarenjs/linq/jslt';
|
|
253
|
+
|
|
254
|
+
export const book = stylesheet([
|
|
255
|
+
rule({ schema: { type: 'object', required: ['isbn'] } },
|
|
256
|
+
(v) => ({ title: v.title, children: [apply(v.chapters.all())] })),
|
|
257
|
+
rule({ schema: { type: 'object', required: ['heading'] } },
|
|
258
|
+
(v) => ({ name: v.heading })),
|
|
259
|
+
]);
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
```json
|
|
263
|
+
{ "$jslt": "0.1",
|
|
264
|
+
"rules": [
|
|
265
|
+
{ "match": { "schema": { "type": "object", "required": ["isbn"] } },
|
|
266
|
+
"body": { "title": "$.title",
|
|
267
|
+
"children": [ { "$apply": "$.chapters[*]" } ] } },
|
|
268
|
+
{ "match": { "schema": { "type": "object", "required": ["heading"] } },
|
|
269
|
+
"body": { "name": "$.heading" } }
|
|
270
|
+
] }
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Drop the brackets — `children: apply(v.chapters.all())` — and the pen
|
|
274
|
+
refuses with `JL0102` before anything runs, because the engine would
|
|
275
|
+
accept it and then fail on the second chapter. §6.1 shows both spellings
|
|
276
|
+
side by side with the run-time error the refusal prevents.
|
|
277
|
+
|
|
278
|
+
### 3.3 A body captured directly: `root`, `path` and a declared parameter
|
|
279
|
+
|
|
280
|
+
Appendix A.7, in the bare-array form (a rules array is a stylesheet too).
|
|
281
|
+
This is `body()` used on its own, which is what the reader reaches for
|
|
282
|
+
when a body is shared between rules or built conditionally in JavaScript.
|
|
283
|
+
|
|
284
|
+
```js
|
|
285
|
+
import { rule, body } from '@jarenjs/linq/jslt';
|
|
286
|
+
|
|
287
|
+
export const priced = [
|
|
288
|
+
rule('$..price', body(
|
|
289
|
+
(v, x) => ({ amount: v.mul(x.rate), currency: x.root.currency, at: x.path }),
|
|
290
|
+
{ externals: ['rate'] })),
|
|
291
|
+
];
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
```json
|
|
295
|
+
[ { "match": "$..price",
|
|
296
|
+
"body": { "amount": { "$mul": ["$", "$rate"] },
|
|
297
|
+
"currency": "$root.currency",
|
|
298
|
+
"at": "$path" } } ]
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Three different things become three different spellings: `v` is the
|
|
302
|
+
matched value, so it records `"$"`; `x.root` is the input document root,
|
|
303
|
+
so it records `"$root.…"`; `x.path` is the matched value's normalized
|
|
304
|
+
path, a STRING the engine supplies per dispatch. Only `rate` needed
|
|
305
|
+
declaring, and the compiled transform then reports
|
|
306
|
+
`transform.externals` as `['rate']` — a caller who omits it gets
|
|
307
|
+
`JT2004` wrapping `JQ2006` at run time, which is the engine's business,
|
|
308
|
+
not the pen's.
|
|
309
|
+
|
|
310
|
+
### 3.4 Two modes, and the `modes` member
|
|
311
|
+
|
|
312
|
+
Appendix A.4 with dispositions added: the same sections rendered twice,
|
|
313
|
+
once as a table of contents and once as body copy, and each mode saying
|
|
314
|
+
what happens when nothing matches (§5).
|
|
315
|
+
|
|
316
|
+
```js
|
|
317
|
+
import { stylesheet, rule, apply } from '@jarenjs/linq/jslt';
|
|
318
|
+
|
|
319
|
+
export const guide = stylesheet([
|
|
320
|
+
rule('$', (v) => ({
|
|
321
|
+
toc: [apply(v.sections.all(), 'toc')],
|
|
322
|
+
body: [apply(v.sections.all(), 'render')],
|
|
323
|
+
})),
|
|
324
|
+
rule('$.sections[*]', (v) => ({ ref: v.id, label: v.heading }), { mode: 'toc' }),
|
|
325
|
+
rule('$.sections[*]', (v) => ({ anchor: v.id, heading: v.heading, text: v.text }), { mode: 'render' }),
|
|
326
|
+
], { unmatched: 'share', modes: { toc: { unmatched: 'error' }, render: { unmatched: 'fresh' } } });
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
```json
|
|
330
|
+
{ "$jslt": "0.1",
|
|
331
|
+
"unmatched": "share",
|
|
332
|
+
"modes": { "toc": { "unmatched": "error" }, "render": { "unmatched": "fresh" } },
|
|
333
|
+
"rules": [
|
|
334
|
+
{ "match": "$",
|
|
335
|
+
"body": { "toc": [ { "$apply": ["$.sections[*]", "toc"] } ],
|
|
336
|
+
"body": [ { "$apply": ["$.sections[*]", "render"] } ] } },
|
|
337
|
+
{ "mode": "toc", "match": "$.sections[*]",
|
|
338
|
+
"body": { "ref": "$.id", "label": "$.heading" } },
|
|
339
|
+
{ "mode": "render", "match": "$.sections[*]",
|
|
340
|
+
"body": { "anchor": "$.id", "heading": "$.heading", "text": "$.text" } }
|
|
341
|
+
] }
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
The envelope's member order is fixed (`$jslt`, `unmatched`, `modes`,
|
|
345
|
+
`rules`) whatever order the options were written in, and a rule's is
|
|
346
|
+
`mode`, `match`, `priority`, `body`. The `mode` argument of `apply()` is
|
|
347
|
+
a literal string, never an expression — dynamic mode selection is a
|
|
348
|
+
non-goal of 0.1 (§6.2) — and passing one records `JL0101` naming that.
|
|
349
|
+
|
|
350
|
+
### 3.5 `op()` — an operator the chain cannot reach
|
|
351
|
+
|
|
352
|
+
`op()` exists because a registry may carry operators the chain has no
|
|
353
|
+
method for. `$npv` is one: `@jarenjs/json`'s `financePack` registers it,
|
|
354
|
+
and no chain builder spells it.
|
|
355
|
+
|
|
356
|
+
```js
|
|
357
|
+
import { stylesheet, rule, op } from '@jarenjs/linq/jslt';
|
|
358
|
+
|
|
359
|
+
export const appraisal = stylesheet([
|
|
360
|
+
rule('$.titles[*]', (v) => ({
|
|
361
|
+
name: v.name,
|
|
362
|
+
npv: op('$npv', [v.rate, v.cashflows.all()]),
|
|
363
|
+
})),
|
|
364
|
+
]);
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
```json
|
|
368
|
+
{ "$jslt": "0.1",
|
|
369
|
+
"rules": [
|
|
370
|
+
{ "match": "$.titles[*]",
|
|
371
|
+
"body": { "name": "$.name",
|
|
372
|
+
"npv": { "$npv": ["$.rate", "$.cashflows[*]"] } } }
|
|
373
|
+
] }
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
The pen writes the name it is given and stops there. Compiled through
|
|
377
|
+
`createJsltRegistry().use(financePack)` this runs; compiled through the
|
|
378
|
+
default `compileJsltStylesheet` it is `JT0007` wrapping `JQ0002`,
|
|
379
|
+
"unknown operator". That is deliberate: LINQ-FORMAT §1.1 rule 1 forbids
|
|
380
|
+
the pen from carrying its own opinion of which operators exist, because
|
|
381
|
+
the registry is the host's to choose. `op()` also works inside a chain
|
|
382
|
+
callback — it lifts into whatever capture is in progress — and outside
|
|
383
|
+
every capture it is the chain's `JL0005`.
|
|
384
|
+
|
|
385
|
+
### 3.6 A constructor literal, and the `$$` escape
|
|
386
|
+
|
|
387
|
+
Appendix A.6's dispatch with an explicit fallback. Every body here
|
|
388
|
+
returns a JavaScript object literal, and every one of them lands in the
|
|
389
|
+
document as an object constructor rather than a `$const` — including the
|
|
390
|
+
string that starts with `$`, which is escaped so the engine reads it as
|
|
391
|
+
data.
|
|
392
|
+
|
|
393
|
+
```js
|
|
394
|
+
import { stylesheet, rule } from '@jarenjs/linq/jslt';
|
|
395
|
+
|
|
396
|
+
export const levels = stylesheet([
|
|
397
|
+
rule({ schema: { type: 'object', required: ['error'] } }, (v) => ({ level: 'fatal', message: v.error })),
|
|
398
|
+
rule({ schema: { type: 'object', required: ['info'] } }, (v) => ({ level: 'note', message: v.info })),
|
|
399
|
+
rule(null, () => ({ level: 'unknown', hint: '$path is a literal here' })),
|
|
400
|
+
], { unmatched: 'error' });
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
```json
|
|
404
|
+
{ "$jslt": "0.1",
|
|
405
|
+
"unmatched": "error",
|
|
406
|
+
"rules": [
|
|
407
|
+
{ "match": { "schema": { "type": "object", "required": ["error"] } },
|
|
408
|
+
"body": { "level": "fatal", "message": "$.error" } },
|
|
409
|
+
{ "match": { "schema": { "type": "object", "required": ["info"] } },
|
|
410
|
+
"body": { "level": "note", "message": "$.info" } },
|
|
411
|
+
{ "body": { "level": "unknown", "hint": "$$path is a literal here" } }
|
|
412
|
+
] }
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Read the third rule closely. `'fatal'`, `'note'` and `'unknown'` are
|
|
416
|
+
data, and they are spelled as themselves because no path can be confused
|
|
417
|
+
with them. `'$path is a literal here'` starts with `$`, which in a
|
|
418
|
+
document means an expression, so the pen wrote `"$$path is a literal
|
|
419
|
+
here"` — and the engine unescapes it back to the string the callback
|
|
420
|
+
returned. The rule for a reader is short: **an object literal in a body
|
|
421
|
+
is always spelled as the format's own constructor, and a string in it is
|
|
422
|
+
data**, escaped where it has to be. There is no spelling of a body that
|
|
423
|
+
turns an object literal into an expression by accident; to write an
|
|
424
|
+
expression you call one (a member read, an operator, `apply()`, `op()`).
|
|
425
|
+
|
|
426
|
+
The `rule(null, …)` in third place is the unconditional rule, whose
|
|
427
|
+
default priority is −1 (§4) — it is what makes `unmatched: 'error'` safe
|
|
428
|
+
to declare, since nothing can reach the built-in rule.
|
|
429
|
+
|
|
430
|
+
### 3.7 A stylesheet whose output is a view
|
|
431
|
+
|
|
432
|
+
A `jaren-vnode` tree is `[tag, props, children]` arrays, so a stylesheet
|
|
433
|
+
that writes one is an ordinary stylesheet whose bodies return arrays. This
|
|
434
|
+
is how an app document's view is written — [APP-PEN.md](APP-PEN.md) links
|
|
435
|
+
here for it — and how a `jslt` node in a dataflow renders
|
|
436
|
+
([FLOW-PEN.md](FLOW-PEN.md) §3.6).
|
|
437
|
+
|
|
438
|
+
```js
|
|
439
|
+
import { stylesheet, rule, apply } from '@jarenjs/linq/jslt';
|
|
440
|
+
|
|
441
|
+
export const catalogue = stylesheet([
|
|
442
|
+
rule('$', (v) => ['ul', { class: 'catalogue' }, [apply(v.books.all())]]),
|
|
443
|
+
rule('$.books[*]', (v) => ['li', { 'data-isbn': v.isbn }, v.title]),
|
|
444
|
+
]);
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
```json
|
|
448
|
+
{ "$jslt": "0.1",
|
|
449
|
+
"rules": [
|
|
450
|
+
{ "match": "$",
|
|
451
|
+
"body": ["ul", { "class": "catalogue" }, [ { "$apply": "$.books[*]" } ]] },
|
|
452
|
+
{ "match": "$.books[*]",
|
|
453
|
+
"body": ["li", { "data-isbn": "$.isbn" }, "$.title"] }
|
|
454
|
+
] }
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
The `[]` idiom is doing its usual work in the children position, and for
|
|
458
|
+
once the brackets look like what they are: an array constructor holding
|
|
459
|
+
one spliced sequence. Over two books this renders
|
|
460
|
+
`['ul', { class: 'catalogue' }, [['li', { 'data-isbn': '978-1' }, 'Ada'], ['li', { 'data-isbn': '978-2' }, 'Grace']]]`.
|
|
461
|
+
|
|
462
|
+
### 3.8 An `.open()` match, and the honest top
|
|
463
|
+
|
|
464
|
+
A `schema` match that is a schema-pen builder types the body's value, so
|
|
465
|
+
`v.title` is `string` and `v.titel` does not compile. `.open()` changes
|
|
466
|
+
that reading and it is worth seeing why.
|
|
467
|
+
|
|
468
|
+
```js
|
|
469
|
+
import * as s from '@jarenjs/linq/schema';
|
|
470
|
+
import { stylesheet, rule } from '@jarenjs/linq/jslt';
|
|
471
|
+
|
|
472
|
+
const Book = s.object({ isbn: s.string(), title: s.string() }).open();
|
|
473
|
+
|
|
474
|
+
export const listing = stylesheet([
|
|
475
|
+
rule({ schema: Book }, (v) => ({ title: v.title, isbn: v.isbn, extra: v.get('subtitle') })),
|
|
476
|
+
]);
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
```json
|
|
480
|
+
{ "$jslt": "0.1",
|
|
481
|
+
"rules": [
|
|
482
|
+
{ "match": { "schema": { "type": "object",
|
|
483
|
+
"properties": { "isbn": { "type": "string" },
|
|
484
|
+
"title": { "type": "string" } },
|
|
485
|
+
"required": ["isbn", "title"] } },
|
|
486
|
+
"body": { "title": "$.title", "isbn": "$.isbn", "extra": "$['subtitle']" } }
|
|
487
|
+
] }
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
`.open()` removes `additionalProperties: false`, which is why the emitted
|
|
491
|
+
schema carries no such member. The type follows emit's reading of the
|
|
492
|
+
EMITTED document (LINQ-FORMAT §1.1 rule 4), so `Infer<typeof Book>`
|
|
493
|
+
becomes `{ isbn: string; title: string } & { [key: string]: unknown }` —
|
|
494
|
+
and that index signature is what a reader has to see coming, because on
|
|
495
|
+
the chain it changes EVERY member:
|
|
496
|
+
|
|
497
|
+
| | closed `s.object({ isbn, title })` | the same builder `.open()` |
|
|
498
|
+
|---|---|---|
|
|
499
|
+
| `Infer<>` | `{ isbn: string; title: string }` | the same, plus `{ [key: string]: unknown }` |
|
|
500
|
+
| `v.title` in the body | `string` | `unknown` |
|
|
501
|
+
| `v.get('subtitle')` | `unknown`, and nothing says the member exists | `unknown`, and the shape says it may |
|
|
502
|
+
| the emitted document | identical apart from `additionalProperties: false` | — |
|
|
503
|
+
|
|
504
|
+
The chain reads a member through the same lookup for both, and an index
|
|
505
|
+
signature answers `unknown` for every key — so opening the object buys
|
|
506
|
+
the honest statement that the shape has more in it than the schema
|
|
507
|
+
declares, and pays for it with the declared members' types. Both bodies
|
|
508
|
+
above emit exactly the same paths; only the compiler's opinion of them
|
|
509
|
+
differs. Where the members matter more than the openness, keep the
|
|
510
|
+
builder closed and reach the undeclared ones with `get()`; the pen never
|
|
511
|
+
blocks it.
|
|
512
|
+
|
|
513
|
+
## 4. Refusals
|
|
514
|
+
|
|
515
|
+
The JSLT pen raises these three `LinqBuildError` codes and no others —
|
|
516
|
+
`test/linq/pen-docs.test.js` holds this list equal, in both directions,
|
|
517
|
+
to the codes `packages/linq/src/jslt/` throws. The full condition each
|
|
518
|
+
code states across every pen is the binder's,
|
|
519
|
+
[LINQ-FORMAT.md](LINQ-FORMAT.md) §1.3.
|
|
520
|
+
|
|
521
|
+
| Code | What this pen raises it for |
|
|
522
|
+
|---|---|
|
|
523
|
+
| `JL0101` | a value this pen cannot spell, an option it does not know, or a name → value map it cannot read |
|
|
524
|
+
| `JL0102` | a construct the format cannot carry: an `apply()` as a bare object member, a `match` of `{}`, an `apply()` outside a body |
|
|
525
|
+
| `JL0104` | an external a captured body did not declare, or `root`/`path` declared as one |
|
|
526
|
+
|
|
527
|
+
`packages/linq/src/jslt/` carries **28 throw sites** — 24 `JL0101`, 3
|
|
528
|
+
`JL0102` and 1 `JL0104` — and they collapse to the conditions below. Two
|
|
529
|
+
further conditions reach a caller through this pen without being thrown
|
|
530
|
+
in its directory: the JSON boundary (`requireJson`, `requireNameMap`) and
|
|
531
|
+
the shared capture's undeclared-external check
|
|
532
|
+
(`packages/linq/src/capture-root.js`), which is where the second
|
|
533
|
+
`JL0104` comes from.
|
|
534
|
+
|
|
535
|
+
Every message below is the one the pen raised when the spelling beside it
|
|
536
|
+
was run, with the code prefix (`JL0101: `) removed. `docPath`, where the
|
|
537
|
+
refusal carries one, is the JSON pointer of the node being assembled and
|
|
538
|
+
is appended to the message text as well (`… at /modes/m`); the rows name
|
|
539
|
+
it where it exists, and its absence in a row is not an oversight but the
|
|
540
|
+
pen's state today (§4.4).
|
|
541
|
+
|
|
542
|
+
### 4.1 `JL0101` — the value, the option and the map
|
|
543
|
+
|
|
544
|
+
**`body()`** — five conditions, all at the door, before the callback runs.
|
|
545
|
+
|
|
546
|
+
| The spelling that trips it | The message | The spelling that works |
|
|
547
|
+
|---|---|---|
|
|
548
|
+
| `body(42)`, `body('$')` | `body() takes a callback (value, x) => …, got 42` | a callback; a document goes straight into `rule()` |
|
|
549
|
+
| `body(fn, 'rate')` | `body() options are { externals?: string[] }, got a string` | `{ externals: ['rate'] }` |
|
|
550
|
+
| `body(fn, { params: ['rate'] })` | `body() does not take 'params'` | `externals` — the format's word (§8.1) |
|
|
551
|
+
| `body(fn, { externals: 'rate' })` | `body() externals is an array of parameter names, got a string` | an array, even for one name |
|
|
552
|
+
| `body(fn, { externals: ['not a name'] })` | `body() externals are identifiers ('rate'), got a string` | an identifier: `[A-Za-z_][A-Za-z0-9_]*` |
|
|
553
|
+
|
|
554
|
+
**`apply()` and `op()`** — three conditions.
|
|
555
|
+
|
|
556
|
+
| The spelling that trips it | The message | The spelling that works |
|
|
557
|
+
|---|---|---|
|
|
558
|
+
| `body(() => [apply()])` | `apply() takes a selector: a path (v.chapters.all(), '$.chapters[*]') or an expression` | a selector |
|
|
559
|
+
| `body((v) => [apply(v, v.mode)])` | `apply() takes the target mode as a literal string (a mode is not an expression, JSLT-FORMAT §6.2), got an expression` | `apply(v, 'toc')` |
|
|
560
|
+
| `body(() => op('npv', []))` | `op() takes an operator name starting with '$' ('$npv'), got a string` | `op('$npv', …)` |
|
|
561
|
+
|
|
562
|
+
**`rule()`** — ten conditions, four of them about `match`.
|
|
563
|
+
|
|
564
|
+
| The spelling that trips it | The message | The spelling that works |
|
|
565
|
+
|---|---|---|
|
|
566
|
+
| `rule(42, fn)` | `rule() match is a JSONPath string or { path?, schema? }, got 42` — `docPath` `/match` | a path string, the object form, or `null` |
|
|
567
|
+
| `rule({ paths: '$' }, fn)` | `rule() match takes 'path' and/or 'schema', not 'paths' (JSLT-FORMAT §3.1)` — `docPath` `/match/paths` | `path`, `schema`, or both |
|
|
568
|
+
| `rule({ path: 42 }, fn)` | `rule() match.path is an RFC 9535 query string, got 42` — `docPath` `/match/path` | a query string |
|
|
569
|
+
| `rule({ schema: new Date(0) }, fn)` | `rule() match.schema received a Date instance, which is not JSON — a document carries null, booleans, finite numbers (never -0), strings, arrays and plain objects, and nothing else` | a schema-pen builder, or a JSON schema |
|
|
570
|
+
| `rule('$')` | `rule() takes a body: a callback (value, x) => …, body(…), or a query document` — `docPath` `/body` | one of the three |
|
|
571
|
+
| `rule('$', { $const: Symbol('x') })` | `rule() body received a Object instance, which is not JSON — a document carries null, booleans, finite numbers (never -0), strings, arrays and plain objects, and nothing else` | a JSON document |
|
|
572
|
+
| `rule('$', fn, 'toc')` | `rule() options are { mode?, priority? }, got a string` | `{ mode: 'toc' }` |
|
|
573
|
+
| `rule('$', fn, { modes: 'x' })` | `rule() does not take 'modes' (JSLT-FORMAT §2.2)` | `mode` on a rule; `modes` on the stylesheet |
|
|
574
|
+
| `rule('$', fn, { mode: 1 })` | `rule() mode is a string naming the rule's mode, got 1` — `docPath` `/mode` | a string; `''` is the unnamed mode |
|
|
575
|
+
| `rule('$', fn, { priority: 'high' })`, `{ priority: NaN }`, `{ priority: -0 }` | `rule() priority is a finite JSON number, got a string` — `docPath` `/priority` | a finite number |
|
|
576
|
+
|
|
577
|
+
**`stylesheet()`** — eleven conditions.
|
|
578
|
+
|
|
579
|
+
| The spelling that trips it | The message | The spelling that works |
|
|
580
|
+
|---|---|---|
|
|
581
|
+
| `stylesheet({ rules: [] })` | `stylesheet() takes an array of rules, got a Object instance` — `docPath` `/rules` | the rules array itself |
|
|
582
|
+
| `stylesheet([], 'fresh')` | `stylesheet() options are { unmatched?, modes? }, got a string` | `{ unmatched: 'fresh' }` |
|
|
583
|
+
| `stylesheet([], { version: '0.1' })` | `stylesheet() does not take 'version' (JSLT-FORMAT §2.1)` | the pen writes `$jslt` itself |
|
|
584
|
+
| `stylesheet([], { unmatched: 'copy' })` | `unmatched is one of 'share', 'fresh' or 'error' (JSLT-FORMAT §5), got 'copy'` — `docPath` `/unmatched` | one of the three |
|
|
585
|
+
| `stylesheet([], { modes: 'x' })` | `stylesheet() modes is { name: { unmatched } }, got a string` — `docPath` `/modes` | a name → `{ unmatched }` map |
|
|
586
|
+
| `stylesheet([], { modes: { __proto__: { unmatched: 'error' } } })` | `stylesheet() modes received a map whose prototype was replaced: a '__proto__:' key in an object literal sets the prototype instead of adding a member, so that member is not there to emit — spell it { ['__proto__']: … }, which is an own key` — `docPath` `/modes` | `{ ['__proto__']: … }` |
|
|
587
|
+
| `stylesheet([], { modes: { m: {} } })`, `{ m: { unmatched: 'share', extra: 1 } }` | `stylesheet() mode 'm' is { unmatched } and nothing else (JSLT-FORMAT §2.1)` — `docPath` `/modes/m` | exactly `{ unmatched }` |
|
|
588
|
+
| `stylesheet([], { modes: { m: { unmatched: 'x' } } })` | `modes/m/unmatched is one of 'share', 'fresh' or 'error' (JSLT-FORMAT §5), got 'x'` — `docPath` `/modes/m/unmatched` | one of the three |
|
|
589
|
+
| `stylesheet(['$'])` | `stylesheet() rule 0 is an object — rule(match, body) — got a string` — `docPath` `/rules/0` | `rule(match, body)` |
|
|
590
|
+
| `stylesheet([{ match: '$' }])` | `stylesheet() rule 0 has no body (JSLT-FORMAT §2.2, the compiler's JT0002)` — `docPath` `/rules/0/body` | give the rule a body |
|
|
591
|
+
| a hand-written rule that is not JSON | `stylesheet() rule 0 received a … instance, which is not JSON — …` — `docPath` `/rules/0` | a JSON rule document |
|
|
592
|
+
|
|
593
|
+
The `modes` row that names `__proto__` is not a curiosity: it is
|
|
594
|
+
LINQ-FORMAT §1.1 rule 5 applied at this pen's one name → value map, and
|
|
595
|
+
the gate that proves it is the same one that proves it for the schema,
|
|
596
|
+
model, contract, flow, app and forms pens.
|
|
597
|
+
|
|
598
|
+
Two refusals a body can raise that are the CHAIN's rather than this
|
|
599
|
+
pen's, listed because a reader who hits one will look here first:
|
|
600
|
+
`rule('$', () => 1n)` is `JL0005` ("a captured expression cannot embed a
|
|
601
|
+
bigint value") and `op('$npv', [])` outside any capture is `JL0005` ("an
|
|
602
|
+
operator expression can only be lifted inside a capture callback — no
|
|
603
|
+
capture is in progress to bind it to"). Both are documented in
|
|
604
|
+
[QUERY-PEN.md](QUERY-PEN.md) §9.
|
|
605
|
+
|
|
606
|
+
### 4.2 `JL0102` — the construct the format cannot carry
|
|
607
|
+
|
|
608
|
+
Three conditions, and every one of them is the pen seeing something the
|
|
609
|
+
engine would only object to later, or not at all.
|
|
610
|
+
|
|
611
|
+
| The spelling that trips it | The message | The spelling that works |
|
|
612
|
+
|---|---|---|
|
|
613
|
+
| `body((v) => ({ children: apply(v.chapters.all()) }))` | `an object member takes exactly one value — 'children' holds an apply(), which yields a SEQUENCE and fails at run time on the second child (JQ2001); wrap the apply in [] (JSLT-FORMAT §6.3: children: [apply(…)])` — `docPath` `/children` | `children: [apply(v.chapters.all())]` |
|
|
614
|
+
| `rule({}, fn)` | `rule() match {} would match nothing — write no match for the unconditional rule (JSLT-FORMAT §3.1, the compiler's JT0003)` — `docPath` `/match` | `rule(null, fn)` |
|
|
615
|
+
| `apply('$')` outside a body | `apply() spells $apply, which exists only inside a rule body (JSLT-FORMAT §6.1) — call it inside body()` | call it inside `body()` or a `rule()` callback |
|
|
616
|
+
|
|
617
|
+
The first is this pen's whole reason for existing beyond typing, and §6.1
|
|
618
|
+
tells the story with the run-time error beside it. The walk that finds it
|
|
619
|
+
descends the literal the callback returned — plain objects and arrays,
|
|
620
|
+
any depth — so `body((v) => ({ a: { b: [{ c: apply(v.x) }] } }))` refuses
|
|
621
|
+
with `docPath` `/a/b/0/c`, naming the exact member. §6.1 also names the
|
|
622
|
+
one place the walk does not reach.
|
|
623
|
+
|
|
624
|
+
The second is `JT0003` seen early. `{}` is a legal JSON object and an
|
|
625
|
+
illegal `match`, and the compiler would say so — but a stylesheet is
|
|
626
|
+
often assembled from a variable, and `rule(someMatch, fn)` with an empty
|
|
627
|
+
object is the shape that mistake takes.
|
|
628
|
+
|
|
629
|
+
The third is availability: `$apply` is injected into body compilation and
|
|
630
|
+
exists nowhere else (§6.1), so an `apply()` outside a capture has no
|
|
631
|
+
document to belong to.
|
|
632
|
+
|
|
633
|
+
### 4.3 `JL0104` — the closed world of a body
|
|
634
|
+
|
|
635
|
+
A body may name `root`, `path` and its declared parameters. Nothing else,
|
|
636
|
+
because the engine binds nothing else — JSLT-FORMAT §8.1 and §8.2 are the
|
|
637
|
+
whole vocabulary, and a name outside it would compile and then fail at
|
|
638
|
+
run time as an unbound reference (`JQ2006`, wrapped as `JT2004`).
|
|
639
|
+
|
|
640
|
+
| The spelling that trips it | The message | The spelling that works |
|
|
641
|
+
|---|---|---|
|
|
642
|
+
| `body((v, x) => v.mul(x.rate))` | `a body() rule cannot bind 'rate' — its query evaluates with exactly 2 externals, 'root' and 'path'; anything else has nothing to bind to — a stylesheet parameter is declared first: body(fn, { externals: ['rate'] })` | declare it: `body(fn, { externals: ['rate'] })` |
|
|
643
|
+
| `body((v, x) => x.limit, { externals: ['rate'] })` | `a body() rule cannot bind 'limit' — its query evaluates with exactly 3 externals, 'root' and 'path' and 'rate'; anything else has nothing to bind to — a stylesheet parameter is declared first: body(fn, { externals: ['limit'] })` | add it: `{ externals: ['rate', 'limit'] }` |
|
|
644
|
+
| `body(fn, { externals: ['root'] })`, `['path']` | `'root' is engine-bound on every dispatch (JSLT-FORMAT §8.2) — it needs no declaration and is always present on the externals argument` | drop the declaration; read `x.root` directly |
|
|
645
|
+
|
|
646
|
+
The second message's advice names the ONE name that was missing, which is
|
|
647
|
+
the fix in a body that declared nothing else; a body that already
|
|
648
|
+
declares parameters adds to the list rather than replacing it, as the
|
|
649
|
+
"spelling that works" column says.
|
|
650
|
+
|
|
651
|
+
The third row is the interesting one. Declaring `root` is not harmless
|
|
652
|
+
and then ignored: §8.2 makes the two names reserved, and the engine
|
|
653
|
+
shadows any stylesheet parameter of the same name, so a body that
|
|
654
|
+
declared `root` and passed a value for it would silently read the
|
|
655
|
+
document root instead. Refusing the declaration is what keeps that from
|
|
656
|
+
being a run-time surprise.
|
|
657
|
+
|
|
658
|
+
### 4.4 What carries a `docPath` and what does not
|
|
659
|
+
|
|
660
|
+
Where the pen knows the pointer of the node it is assembling, the
|
|
661
|
+
refusal carries it: every `match`, `mode`, `priority`, `modes` and
|
|
662
|
+
`rules` row above. Where the value crossed the JSON boundary
|
|
663
|
+
(`requireJson`) the pointer is not passed through, so
|
|
664
|
+
`rule({ schema: new Date(0) }, fn)` names the method in its message and
|
|
665
|
+
carries no `docPath` — the message is still unambiguous, and the
|
|
666
|
+
convention across the family is that a row states the pointer only when
|
|
667
|
+
one exists.
|
|
668
|
+
|
|
669
|
+
## 5. The types
|
|
670
|
+
|
|
671
|
+
The declarations are `packages/linq/types/jslt.d.ts` (193 lines), and
|
|
672
|
+
every claim below is pinned at compile level in
|
|
673
|
+
`test/consumer/linq-jslt.ts` with a runtime twin in
|
|
674
|
+
`test/linq/jslt-pen.test.js`. This subpath exports **no builder class, no
|
|
675
|
+
constant and no type guard** — the three kinds the mapping table excludes
|
|
676
|
+
(LINQ-FORMAT's D5 reading) are empty here, so §2 names the whole runtime
|
|
677
|
+
surface and this section is about the compile-time one. The shapes below
|
|
678
|
+
are §3.2's book and its chapters, declared.
|
|
679
|
+
|
|
680
|
+
```ts
|
|
681
|
+
import { stylesheet, rule, body, apply } from '@jarenjs/linq/jslt';
|
|
682
|
+
import type { Externals, Input, Output, Rule, Stylesheet, BodyDocument } from '@jarenjs/linq/jslt';
|
|
683
|
+
import type { Expr } from '@jarenjs/linq';
|
|
684
|
+
import * as s from '@jarenjs/linq/schema';
|
|
685
|
+
|
|
686
|
+
interface Chapter { heading: string }
|
|
687
|
+
interface Book { isbn: string; title: string; chapters: Chapter[] }
|
|
688
|
+
interface Item { sku: string; price: number }
|
|
689
|
+
|
|
690
|
+
const BookSchema = s.object({ isbn: s.string(), title: s.string() });
|
|
691
|
+
|
|
692
|
+
const chapter = rule({ schema: s.object({ heading: s.string() }) }, (v) => ({ name: v.heading }));
|
|
693
|
+
// ^ Rule<{ heading: string }, { name: string }> — the builder types v
|
|
694
|
+
const book = body((v: Expr<Book>) => ({ title: v.title, children: [apply(v.chapters.all())] }));
|
|
695
|
+
// ^ BodyDocument<Book, { title: string; children: unknown[] }> — a dispatch is unknown
|
|
696
|
+
const priced = body(
|
|
697
|
+
(v: Expr<Item>, x: Externals<{ rate: number }>) => ({ amount: v.price.mul(x.rate) }),
|
|
698
|
+
{ externals: ['rate'] }); // x.limit does not compile
|
|
699
|
+
|
|
700
|
+
const sheet = stylesheet([rule(null, book), chapter]);
|
|
701
|
+
type Out = Output<typeof sheet>; // the FIRST rule's: { title: string; children: unknown[] }
|
|
702
|
+
const typed = stylesheet<Book, { title: string; children: { name: string }[] }>([rule(null, book), chapter]);
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### 5.1 The phantoms, and where each side comes from
|
|
706
|
+
|
|
707
|
+
`BodyDocument<In, Out>`, `Rule<In, Out>` and `Stylesheet<In, Out>` all
|
|
708
|
+
carry two phantom members — `__in` and `__out`, declared and never
|
|
709
|
+
present at runtime — and `Input<>`/`Output<>` read them back. The
|
|
710
|
+
question a reader has is where each side gets its value.
|
|
711
|
+
|
|
712
|
+
| | `In` — what the body was written over | `Out` — what it produces |
|
|
713
|
+
|---|---|---|
|
|
714
|
+
| `body(fn)` | the annotation on the first argument (`(v: Expr<Book>) => …`); `unknown` without one | `Unwrap<>` of the callback's return |
|
|
715
|
+
| `rule(match, fn)` | the `schema` match's `Infer<>` when it is a schema-pen builder; the first argument's annotation otherwise; `unknown` for a path match alone | `Unwrap<>` of the callback's return |
|
|
716
|
+
| `rule(match, bodyDoc)` | the body document's `In`, or the match's when the body is untyped | the body document's `Out` |
|
|
717
|
+
| `rule(match, json)` | `unknown` | `unknown` |
|
|
718
|
+
| `stylesheet(rules)` | the FIRST rule's `In` | the FIRST rule's `Out` |
|
|
719
|
+
| `stylesheet<In, Out>(rules)` | the author's | the author's |
|
|
720
|
+
|
|
721
|
+
Two consequences worth stating rather than discovering:
|
|
722
|
+
|
|
723
|
+
- **Write the root rule first.** A stylesheet's phantoms are its first
|
|
724
|
+
rule's, which is a convention rather than an inference — Appendix A
|
|
725
|
+
writes the root rule first in every fixture, and this pen types on that
|
|
726
|
+
assumption. A stylesheet whose first rule is a leaf rule types as that
|
|
727
|
+
leaf.
|
|
728
|
+
- **Every `apply()` is `unknown`, and it has to be.** A dispatch lands on
|
|
729
|
+
whichever rule wins at run time, over a value the type system never
|
|
730
|
+
sees; `[apply(…)]` therefore unwraps to `unknown[]`, and
|
|
731
|
+
`apply(…).count()` to `number` (the operator is typed, its input is
|
|
732
|
+
not). Where the author knows the answer — a chapter always renders as
|
|
733
|
+
`{ name }` — the author states it with `stylesheet<In, Out>(…)`, and
|
|
734
|
+
the pin file holds that annotation as the way to say so.
|
|
735
|
+
|
|
736
|
+
The built-in rule's rebuilds (`share`/`fresh` around an unmatched
|
|
737
|
+
container, §5) are not typed at all: a stylesheet whose root is unmatched
|
|
738
|
+
is `Stylesheet<unknown, unknown>` unless annotated.
|
|
739
|
+
|
|
740
|
+
### 5.2 `Externals<X, Root>` — a second argument that answers by name
|
|
741
|
+
|
|
742
|
+
`Externals` is the type of `body()`'s second parameter and it is built
|
|
743
|
+
from two halves: `root` and `path`, always present, and a mapped type
|
|
744
|
+
over the declared names.
|
|
745
|
+
|
|
746
|
+
```ts
|
|
747
|
+
// root types by the second parameter of Externals<>; path is always a string expression
|
|
748
|
+
void body((v: Expr<Item>, x: Externals<{}, Book>) => ({ isbn: x.root.isbn.upper(), me: v.sku }));
|
|
749
|
+
// a declared name types through the annotation; the declaration and the annotation must agree
|
|
750
|
+
const p = body((v: Expr<Item>, x: Externals<{ rate: number }>) => v.price.mul(x.rate), { externals: ['rate'] });
|
|
751
|
+
// @ts-expect-error — the annotation names 'rate' and the declaration says 'limit'
|
|
752
|
+
void body((v: Expr<Item>, x: Externals<{ rate: number }>) => x.rate, { externals: ['limit'] });
|
|
753
|
+
// @ts-expect-error — a typed parameter takes its own kind
|
|
754
|
+
void body((v: Expr<Item>, x: Externals<{ rate: number }>) => x.rate.upper(), { externals: ['rate'] });
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
Without an annotation a declared name is still reachable and is
|
|
758
|
+
`UnknownExpr` — the honest top — so `{ externals: ['rate'] }` alone buys
|
|
759
|
+
the runtime check without the type. Undeclared names are a compile error
|
|
760
|
+
whether or not `x` is annotated, which is the same shape the chain's
|
|
761
|
+
`params()` has and the reason the pin file checks both directions.
|
|
762
|
+
|
|
763
|
+
### 5.3 What the pins hold
|
|
764
|
+
|
|
765
|
+
`test/consumer/linq-jslt.ts` (117 lines) is the compile-level record, and
|
|
766
|
+
it is worth knowing what it asserts because a claim not in it is a claim
|
|
767
|
+
this document should not make. It pins, with `Equals<A, B>` — identity in
|
|
768
|
+
both directions, never assignability — the `In`/`Out` of an annotated
|
|
769
|
+
body, an unannotated body, a scalar body, a schema-matched rule, an
|
|
770
|
+
annotated callback rule, a path-only rule, a rule built from a body
|
|
771
|
+
document, and a stylesheet in both its inferred and annotated forms. It
|
|
772
|
+
pins eleven negatives with `@ts-expect-error`, each of which FAILS the
|
|
773
|
+
build if it ever starts compiling: an undeclared external, an annotation
|
|
774
|
+
that disagrees with its declaration, an external used at the wrong kind,
|
|
775
|
+
a misspelled member on an annotated value, a misspelled member under a
|
|
776
|
+
schema match, a non-string mode, a non-number priority, an unknown
|
|
777
|
+
disposition, an operator name without `$`, an expression where a mode
|
|
778
|
+
string belongs, and a stylesheet asserted at an output its rule
|
|
779
|
+
contradicts.
|
|
780
|
+
|
|
781
|
+
## 6. What it cannot spell
|
|
782
|
+
|
|
783
|
+
Four things, and only one of them is a limit of the FORMAT. The other
|
|
784
|
+
three are the pen refusing a spelling that would compile and then behave
|
|
785
|
+
wrongly, or a route the pen does not offer.
|
|
786
|
+
|
|
787
|
+
### 6.1 The `[]` idiom — the refusal, and the run-time failure it prevents
|
|
788
|
+
|
|
789
|
+
This is the trap JSLT-FORMAT §6.3 names as "the one trap every author
|
|
790
|
+
hits", and it is the only place the pen refuses something the compiler
|
|
791
|
+
accepts.
|
|
792
|
+
|
|
793
|
+
An `$apply` yields a SEQUENCE. An object member holds exactly one value
|
|
794
|
+
(QUERY-FORMAT §3.1). So:
|
|
795
|
+
|
|
796
|
+
```js
|
|
797
|
+
// refused at build time — JL0102, docPath /children
|
|
798
|
+
rule('$', (v) => ({ title: v.title, children: apply(v.chapters.all()) }))
|
|
799
|
+
|
|
800
|
+
// the spelling that works — the brackets splice the sequence into one array
|
|
801
|
+
rule('$', (v) => ({ title: v.title, children: [apply(v.chapters.all())] }))
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
Write the refused spelling by hand — as a JSON rule document, which the
|
|
805
|
+
pen carries verbatim — and the engine compiles it happily. Over a book
|
|
806
|
+
with ONE chapter it even works, answering
|
|
807
|
+
`{ title: 'T', children: 1 }`; over a book with two it throws
|
|
808
|
+
`JT2004` wrapping `JQ2001`, "member 'children' evaluated to 2 items; an
|
|
809
|
+
object member takes exactly one". That is the failure mode this refusal
|
|
810
|
+
exists for: a transform that passes its first test and breaks on real
|
|
811
|
+
data.
|
|
812
|
+
|
|
813
|
+
**The check runs over the emitted document, not over what the callback
|
|
814
|
+
returned, and the difference matters.** An object literal handed to an
|
|
815
|
+
OPERATOR is lowered while the callback is still running —
|
|
816
|
+
`op('$if', [v.flag, { children: apply(v.chapters.all()) }, null])` — so by
|
|
817
|
+
the time the callback returns there is no marker left to find, only the
|
|
818
|
+
node it left behind in the document. Walking the document catches both
|
|
819
|
+
routes with one rule, and it reads member position exactly as the capture
|
|
820
|
+
wrote it: a map constructor has no `$`-prefixed key, an operator or FLWOR
|
|
821
|
+
phrase does, and `$map` — the phrase a `$`-keyed data object is spelled
|
|
822
|
+
as — carries its members as `[key, value]` pairs. So all four of these are
|
|
823
|
+
`JL0102`, at the pointer of the offending member:
|
|
824
|
+
|
|
825
|
+
| The spelling | `docPath` |
|
|
826
|
+
|---|---|
|
|
827
|
+
| `({ children: apply(v.x) })` | `/children` |
|
|
828
|
+
| `({ a: { b: [{ c: apply(v.x) }] } })` | `/a/b/0/c` |
|
|
829
|
+
| `({ out: op('$if', [v.flag, { children: apply(v.x) }, null]) })` | `/out/$if/1/children` |
|
|
830
|
+
| `({ $weird: apply(v.x) })` | `/$map/0/1` |
|
|
831
|
+
|
|
832
|
+
An `apply()` in an OPERAND position is not a member and stays legal:
|
|
833
|
+
`apply(v.items.all()).count()` is `{ "$count": { "$apply": … } }`, and
|
|
834
|
+
`[apply(…)]` is the idiom itself. A hand-written body is untouched by any
|
|
835
|
+
of this — the pen judges what it captured, and a rule document handed to
|
|
836
|
+
`rule()` rides verbatim (§2).
|
|
837
|
+
|
|
838
|
+
### 6.2 A `match` of `{}`
|
|
839
|
+
|
|
840
|
+
The format has two ways to say "match everything": a path of `'$'`
|
|
841
|
+
(positional, the document root only) and no `match` at all (the
|
|
842
|
+
unconditional rule, which reaches every dispatched value). It has no way
|
|
843
|
+
to say it with an empty object, and the compiler's `JT0003` is what would
|
|
844
|
+
otherwise say so. `rule(null, fn)` is the spelling; `rule('$', fn)` is
|
|
845
|
+
the other one and means something different.
|
|
846
|
+
|
|
847
|
+
### 6.3 `apply()` outside a body
|
|
848
|
+
|
|
849
|
+
`$apply` is not part of the query language. The stylesheet compiler
|
|
850
|
+
injects it into each body compile (§6.1), and `compileJsonQuery` rejects
|
|
851
|
+
it as `JQ0002`. So there is no way to build a fragment containing an
|
|
852
|
+
`apply()` outside a `body()` and splice it in later — the marker the pen
|
|
853
|
+
uses to find bare applies is created by the capture and only lives inside
|
|
854
|
+
one. Build the fragment as a callback and pass it where a body is taken,
|
|
855
|
+
or write the `{ $apply: … }` document by hand and accept that the `[]`
|
|
856
|
+
check will not see it.
|
|
857
|
+
|
|
858
|
+
### 6.4 `from()` has no extensions pass-through — the one that surprises
|
|
859
|
+
|
|
860
|
+
This is not a limit of the stylesheet format at all, and it is here
|
|
861
|
+
because it is where readers of this document look for it. `op()` lifts an
|
|
862
|
+
operator into whatever capture is in progress, so it works in a chain
|
|
863
|
+
callback as well as in a body:
|
|
864
|
+
|
|
865
|
+
```js
|
|
866
|
+
from([{ rate: 0.1, cashflows: [-100, 60, 60] }])
|
|
867
|
+
.select((r) => ({ v: op('$npv', [r.rate, r.cashflows.all()]) }))
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
The DOCUMENT this records is correct and identical to the one a body
|
|
871
|
+
would record. But the chain's in-memory evaluation carries no operator
|
|
872
|
+
registry, so `explain()` and `toArray()` both raise the engine's
|
|
873
|
+
`JQ0002` — "unknown operator '$npv'" — with `docPath` `/$return/v`. A
|
|
874
|
+
registered operator is reachable from a chain only where the chain runs
|
|
875
|
+
against a provider that was given the registry, or from a stylesheet
|
|
876
|
+
compiled through one (`createJsltRegistry().use(financePack)`, §3.5).
|
|
877
|
+
`test/linq/jslt-pen.test.js` pins both directions.
|
|
878
|
+
|
|
879
|
+
### 6.5 Everything else is the compiler's, by design
|
|
880
|
+
|
|
881
|
+
For completeness, because the boundary is what LINQ-FORMAT §1.1 rule 1
|
|
882
|
+
asks each pen document to state plainly. The pen does NOT check: whether
|
|
883
|
+
a `match` path parses (`JT0003`); whether a body's operators exist
|
|
884
|
+
(`JT0007` wrapping `JQ0002`); whether a `schema` match can be compiled
|
|
885
|
+
without the type-test hook (`JT0006`); whether a rule set recurses
|
|
886
|
+
without bottoming out (`JT2001`, the depth guard); whether an `$apply`
|
|
887
|
+
argument list has the right arity (`JQ0003` inside `JT0007`); or whether
|
|
888
|
+
two rules of equal priority conflict (§4 decides by document order and
|
|
889
|
+
never errors). Every one of those is built through the pen and asserted
|
|
890
|
+
at the engine's own code in `test/linq/jslt-pen.test.js` — the pen's
|
|
891
|
+
non-judgement is itself gated.
|
|
892
|
+
|
|
893
|
+
### 6.6 When not to reach for this pen
|
|
894
|
+
|
|
895
|
+
- **The stylesheet is data.** A `$jslt` document loaded from a file,
|
|
896
|
+
authored by a model ([@jarenjs/ai](../../ai/README.md)'s authoring
|
|
897
|
+
profile writes them) or edited in the studio is a value;
|
|
898
|
+
`compileJsltStylesheet` takes it directly and nothing here has to be in
|
|
899
|
+
the path.
|
|
900
|
+
- **You are transforming one value, once, in JavaScript.** A stylesheet
|
|
901
|
+
is a document because it has to travel — into a migration step, into a
|
|
902
|
+
flow node, into an app's view, into a database. Code that runs in one
|
|
903
|
+
process and stays there should be a function: it can branch, loop and
|
|
904
|
+
call libraries, and §1.1's five consequences are five ways a callback
|
|
905
|
+
that looks like ordinary JavaScript is not.
|
|
906
|
+
- **The transform needs a condition JavaScript would express better.** A
|
|
907
|
+
body cannot use `if`, `&&`, `||` or a ternary — §1.1 rule 2 says what
|
|
908
|
+
they do instead, and it is silent and wrong. A transform whose shape is
|
|
909
|
+
mostly conditional is a set of rules with narrower `match` paths, or it
|
|
910
|
+
is not a stylesheet.
|
|
911
|
+
- **You need an operator the query language does not have.** `op()`
|
|
912
|
+
reaches a REGISTERED operator, so the escape exists — but registering
|
|
913
|
+
one is a decision about the engine everywhere it runs, not a local
|
|
914
|
+
convenience, and a transform that needs three of them is a program.
|
|
915
|
+
- **The output is not JSON.** A stylesheet's result is a JSON value. Text,
|
|
916
|
+
bytes and streams are somebody else's job — a `jaren-vnode` tree
|
|
917
|
+
(§3.7) is JSON and renders to HTML downstream, which is the pattern to
|
|
918
|
+
copy rather than the exception to it.
|
|
919
|
+
|
|
920
|
+
## 7. Cost
|
|
921
|
+
|
|
922
|
+
`@jarenjs/linq/jslt` builds to **<!--fact:bundle.jslt-->19,124<!--/fact--> bytes** as a minified,
|
|
923
|
+
tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
|
|
924
|
+
measures and `npm run test:tree-shaking` reports, published rounded
|
|
925
|
+
(<!--fact:bundle.jslt.kb-->19<!--/fact--> kB) beside the other nine subpath prices in
|
|
926
|
+
[docs/CONSUMING.md](../../../docs/CONSUMING.md).
|
|
927
|
+
|
|
928
|
+
The probe is a gate, not a report. Building a stylesheet as a consumer
|
|
929
|
+
would, it asserts that the bundle carries:
|
|
930
|
+
|
|
931
|
+
- **no engine** — not one byte of `@jarenjs/json`, `@jarenjs/validate`,
|
|
932
|
+
`@jarenjs/emit`, `@jarenjs/db`, `@jarenjs/formats` or `@jarenjs/refs`,
|
|
933
|
+
which is what makes the "the pen imports no engine" rule a measurement
|
|
934
|
+
rather than a promise;
|
|
935
|
+
- **no chain module** — none of `sequence.js`, `document.js`, `async.js`,
|
|
936
|
+
`concurrency.js`, `provider.js`, `sources.js` or `schema-of.js`;
|
|
937
|
+
- **of the schema pen, only `brand.js`** — the builder brand, which
|
|
938
|
+
`rule()` needs to tell a `schema` match's builder from a hand-written
|
|
939
|
+
schema;
|
|
940
|
+
- **a ceiling** of 20,000 bytes; and the other direction, that neither the
|
|
941
|
+
chain's bundle nor the schema pen's carries a byte of
|
|
942
|
+
`packages/linq/src/jslt/`.
|
|
943
|
+
|
|
944
|
+
What it does carry is the recording proxy (`expression.js`), the shared
|
|
945
|
+
root capture (`capture-root.js`), the JSON boundary
|
|
946
|
+
(`json-boundary.js`), and this pen's own three files — 414 lines of
|
|
947
|
+
source. That makes it the SMALLEST of the nine pen bundles, and the
|
|
948
|
+
reason is that a stylesheet is mostly bodies, and a body is the shared
|
|
949
|
+
machine every pen already pays for.
|
|
950
|
+
|
|
951
|
+
Two figures worth reading beside it: `./migration` (<!--fact:bundle.migration-->23,599<!--/fact--> bytes)
|
|
952
|
+
carries this pen's `body()` and pays for it, which is why the two prices
|
|
953
|
+
sit so close; and `./flow` (<!--fact:bundle.flow-->19,181<!--/fact--> bytes) is within 60 bytes of this one
|
|
954
|
+
despite writing two formats, because it shares the same capture and adds
|
|
955
|
+
almost nothing but member checks and their messages.
|