@jarenjs/emit 0.34.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.
@@ -0,0 +1,97 @@
1
+ # @jarenjs/emit — architecture
2
+
3
+ Two stages, one published contract between them.
4
+
5
+ ```mermaid
6
+ flowchart LR
7
+ A["schema"] --> B["model.js"]
8
+ B --> C["TYPE MODEL"]
9
+ C --> D["typescript.js<br/>markdown.js"]
10
+ D --> E["artifact"]
11
+ N1["stage 1: the flattening<br/>no template can do"]
12
+ N2["a published format,<br/>not private state"]
13
+ N3["stage 2: one stylesheet<br/>per target language"]
14
+ B -.- N1
15
+ C -.- N2
16
+ D -.- N3
17
+ class N1,N2,N3 note
18
+ ```
19
+
20
+ ## Stage 1 — `model.js`
21
+
22
+ A single recursive descent over the schema, producing a flat declaration list.
23
+ The decisions that live here, and nowhere else:
24
+
25
+ - **Naming.** `$defs` keys and `$ref` pointer tokens are the names a reader
26
+ already uses, so they win. Anything else is derived from the path that
27
+ reached it (`UserAddressStreet`), PascalCased and de-collided by suffix.
28
+ - **Cycles.** A node reached while it is still being built is hoisted to a
29
+ declaration and referenced by name. That is what turns a cyclic graph into
30
+ an acyclic list; the frame already building it finishes the declaration.
31
+ - **Composition.** `allOf` → intersection, `anyOf`/`oneOf` → union, `const`/
32
+ `enum` → literals. Degenerate unions collapse, `never` drops out, and a
33
+ union containing `unknown` becomes `unknown`. A `$ref`'s 2019-09+ siblings
34
+ intersect with the target; the reference itself resolves through the
35
+ helpers `@jarenjs/validate/normalize` exports (`#`, `#/pointer`, plain
36
+ `#anchor`, embedded-`$id` scope boundary), so a ref means here exactly what
37
+ it means to the runtime normalizer.
38
+ - **Honest widening.** Every keyword in `DROPPED_CONSTRAINTS` is recorded on
39
+ the node it came from and flattened into `doc` lines — including
40
+ integer-ness, `not`, active conditionals, dependent/unevaluated keywords
41
+ and `patternProperties` key restrictions. Nothing is discarded. The
42
+ directional half of the rule: the type may be wider than the schema (and
43
+ says where), never narrower. That is why applicators imply no container
44
+ (`properties` without `type` unions the object arm with every other JSON
45
+ kind), why tuples take their required count from `minItems` and stay open
46
+ unless the schema closes them, and why a closed empty object emits
47
+ `Record<string, never>` rather than TypeScript's primitive-swallowing
48
+ empty interface.
49
+ - **The plain universe.** When variants are derived, `anyOf`/`oneOf`
50
+ branches compile in a context with normalization inert, because
51
+ `compileNormalizer` does not descend union branches. A referenced type
52
+ that differs under normalization gets a `Plain`-suffixed declaration for
53
+ its as-declared reading, shared by both passes; everything else shares the
54
+ main declaration. `normalizationChangesType` walks exactly the keywords
55
+ `compileNormalizer` walks — the twin analysis answering differently from
56
+ the runtime is the defect class this package exists to rule out.
57
+ - **Determinism.** Declaration order is discovery order, member order is
58
+ schema order, and names never depend on a traversal counter. Boolean
59
+ schemas memoize by value — every `true` is the same schema — except that
60
+ the `$defs` loop forces one declaration per def name, so every importable
61
+ name exists. The `reserved` option seeds the taken-name list, which is how
62
+ the CLI's `--bundle` keeps independently compiled models in one name space.
63
+
64
+ Absent information is **omitted**, not set to `null` — a missing `rest`,
65
+ `index` or `default` is simply not there. Stage 2 relies on that: a path that
66
+ yields the empty sequence dispatches nothing, which is how a template says
67
+ "only when present" without a conditional.
68
+
69
+ ## Stage 2 — the stylesheets
70
+
71
+ Each emitter is a JTLT document plus a thin `render` function. Three things
72
+ about them are worth knowing before editing one:
73
+
74
+ 1. **Rules match by SCHEMA, not by path.** `$apply` on the current node
75
+ dispatches *location-less*, so a path match like `$..[?@.kind == 'union']`
76
+ silently never fires for a re-dispatched node. `isKind()` builds the schema
77
+ match instead, which is location-independent — and shape is what the rules
78
+ mean anyway. This is the single easiest way to break these files.
79
+ 2. **`$if` is an expression segment.** It interpolates a value; it cannot
80
+ contain a segment list. Conditional *rendering* is done by dispatching a
81
+ path that may be empty, never by `$if`.
82
+ 3. **Separators without a position variable.** JTLT has no `position()`, so a
83
+ separated list dispatches `$.options[0]` bare and `$.options[1:]` through a
84
+ mode that prints its own separator first. Ordinary RFC 9535 selectors, no
85
+ help needed from the model.
86
+
87
+ `markdown.js` exists to keep stage 1 honest: it is as unlike TypeScript as a
88
+ target gets, and it required no model change. If a future target does require
89
+ one, that is the signal the model is shaped around a language rather than
90
+ around schemas.
91
+
92
+ ## Verification
93
+
94
+ `test/emit/agreement.test.js` is the reason to trust any of this — the cyclic
95
+ check against `@jarenjs/validate` described in the
96
+ [README](./README.md#the-cyclic-verification). It is itself checked by
97
+ breaking the generator on purpose and confirming the suite fails.
package/README.md ADDED
@@ -0,0 +1,300 @@
1
+ # @jarenjs/emit
2
+
3
+ Build-time artifacts from JSON documents. Point it at your JSON Schemas, get
4
+ TypeScript declarations — or Markdown reference docs, or whatever a stylesheet
5
+ emits next.
6
+
7
+ ```bash
8
+ npx jaren-emit --schema ./schemas --out ./src/types
9
+ ```
10
+
11
+ ```javascript
12
+ import { emitTypeScript } from '@jarenjs/emit';
13
+
14
+ emitTypeScript({
15
+ type: 'object',
16
+ description: 'A user',
17
+ properties: {
18
+ id: { type: 'string', format: 'uuid' },
19
+ role: { enum: ['admin', 'user'] },
20
+ },
21
+ required: ['id'],
22
+ }, { name: 'User' });
23
+ ```
24
+
25
+ ```typescript
26
+ /**
27
+ * A user
28
+ */
29
+ export interface User {
30
+ /**
31
+ * Schema constraints this type cannot express: format="uuid"
32
+ */
33
+ id: string;
34
+ role?: "admin" | "user";
35
+ }
36
+ ```
37
+
38
+ ## Why this exists
39
+
40
+ A schema-first codebase has a gap where a Zod codebase has `z.infer`: the
41
+ schema knows the shape, and TypeScript does not. The usual answers are to
42
+ assert the type by hand — which can silently certify something the schema
43
+ never said — or to reach for a type-level library.
44
+
45
+ The observation this package is built on is smaller than either: **a JSON
46
+ Schema is JSON, TypeScript is text, and [JTLT](../json/docs/JTLT-FORMAT.md) is
47
+ JSON-to-text.** Generating a declaration file is a stylesheet, not a new
48
+ engine. That is also why it is called `emit` and not `infer` — swap the
49
+ stylesheet and the same machinery emits documentation, DDL, or anything else
50
+ a target language needs.
51
+
52
+ ## The cyclic verification
53
+
54
+ This is the part that matters, and the part a standalone
55
+ schema-to-TypeScript tool structurally cannot do.
56
+
57
+ One schema goes two ways, and the two answers have to correspond:
58
+
59
+ ```mermaid
60
+ flowchart TD
61
+ S["JSON Schema"]
62
+ T["TypeScript type"]
63
+ V["compiled validator"]
64
+ C["the SAME instances —<br/>and the two answers must correspond"]
65
+ S -->|"@jarenjs/emit"| T
66
+ S -->|"@jarenjs/validate"| V
67
+ T --> C
68
+ V --> C
69
+ N["owning both sides is what<br/>makes this testable"]
70
+ C -.- N
71
+ classDef answer fill:#dcfce7,stroke:#16a34a
72
+ class C answer
73
+ class N note
74
+ ```
75
+
76
+ A generator that is merely *plausible* is worthless. It will emit a type that
77
+ says one thing while the validator enforces another, and you find out in
78
+ production — data that passed validation and violates its own declared type,
79
+ or a type so wide it certifies anything.
80
+
81
+ Because Jaren owns **both sides**, that can be tested rather than trusted.
82
+ The suite in `test/emit/agreement.test.js` takes a corpus of schemas plus
83
+ instances, generates the declarations, writes a probe file that assigns every
84
+ instance to its generated type, and runs `tsc` over it. Then it runs the
85
+ compiled validator over the same instances and requires three relationships to
86
+ hold:
87
+
88
+ | Instance | Validator | Generated type | What a failure would mean |
89
+ | --- | --- | --- | --- |
90
+ | valid | accepts | **must** type-check | the type is **narrower** than the schema — it rejects data your service accepts |
91
+ | structurally invalid | rejects | **must not** type-check | the type is **wider** than the schema — it certifies data your service rejects |
92
+ | constraint-invalid | rejects | **does** type-check | the documented widening — see below |
93
+
94
+ The third row is the honest one. TypeScript cannot express `minLength`,
95
+ `pattern` or `format`, so a value can be type-correct and schema-invalid.
96
+ Rather than hide that, the corpus asserts it, and the generator writes the
97
+ dropped constraint into the generated file:
98
+
99
+ ```typescript
100
+ /**
101
+ * Schema constraints this type cannot express: minLength=3
102
+ */
103
+ id: string;
104
+ ```
105
+
106
+ The type says `string`. The comment says the schema also demands a minimum
107
+ length that the type does not enforce. A reader learns both. **Widening
108
+ silently would be a lie of omission**, and it is the single most common way a
109
+ generated type misleads the person reading it.
110
+
111
+ The rig is checked against itself, too: deliberately breaking the generator so
112
+ it emits `unknown` everywhere makes the "not wider than the schema" assertion
113
+ fail. A verification suite that passes on a broken generator proves nothing.
114
+
115
+ ## Accepted and normalized types
116
+
117
+ `@jarenjs/validate/normalize` makes a contract's input and output shapes
118
+ differ — a defaulted member is optional for the caller and present afterwards,
119
+ and a coerced member arrives in its transport form. Pass the same options to
120
+ the generator and it names both sides:
121
+
122
+ ```bash
123
+ jaren-emit --schema ./schemas --out ./types --defaults --coerce
124
+ ```
125
+
126
+ ```typescript
127
+ export interface Config {
128
+ host: string; // present after normalizing
129
+ port: number;
130
+ name: string;
131
+ }
132
+
133
+ /**
134
+ * Accepted input for Config: the shape before normalization, where defaulted
135
+ * members may be absent and coercible values may still be in their transport form.
136
+ */
137
+ export interface ConfigInput {
138
+ host?: string | number | boolean; // optional, and widened to what
139
+ port?: number | string; // the normalizer will convert FROM
140
+ name: string | number | boolean;
141
+ }
142
+ ```
143
+
144
+ That pair is exactly what a `Contract<Input, Output>` boundary wants: the
145
+ handler signature takes `ConfigInput`, the rest of the program handles
146
+ `Config`, and the normalizer is the transition between them.
147
+
148
+ **A twin appears only where the type actually differs.** The generator works
149
+ that out bottom-up, so a schema with one defaulted field does not double every
150
+ declaration; everything unaffected keeps a single shared name on both sides.
151
+
152
+ **Only two normalizations produce a difference.** `useDefaults` moves a member
153
+ across the optional boundary and `coerceTypes` widens what the input accepts.
154
+ `trimStrings` is string-to-string, and `removeAdditional` removes members no
155
+ type ever declared — neither earns a second declaration.
156
+
157
+ The switch resolution, including per-field predicates, is **imported from the
158
+ normalizer rather than reimplemented**. Two copies of that rule would drift,
159
+ and a variant that disagrees with the normalizer is worse than no variant: it
160
+ is a type certifying an input the normalizer will not take. The agreement
161
+ suite checks the pair the same way it checks everything else — a raw input
162
+ must satisfy `ConfigInput` and not `Config`, and normalizing it at runtime
163
+ must produce something the `Config` side describes.
164
+
165
+ **The variants stop where the normalizer stops.** `compileNormalizer` does
166
+ not descend `anyOf`/`oneOf` — which branch applies is only known after
167
+ validating — so inside a union branch no default materializes and no coercion
168
+ runs, and the generated pair says the same: branch members keep their
169
+ declared optionality and their declared scalar types on *both* sides. When a
170
+ branch references a type that does differ elsewhere, the branch points at a
171
+ `Plain`-suffixed declaration carrying the schema's as-declared reading.
172
+ Literals get the same treatment in the other direction: an integer `enum`
173
+ under `--coerce` accepts its transport string on the input side (`1 | 2 |
174
+ string`), because the normalizer coerces `"2"` to `2` before the enum check
175
+ runs.
176
+
177
+ ## Two stages, and why
178
+
179
+ ```
180
+ schema ──▶ compileEmitModel ──▶ TYPE MODEL ──▶ stylesheet ──▶ artifact
181
+ ```
182
+
183
+ The templating is the easy half. The hard half is that a schema graph is not
184
+ shaped like a declaration file: `$ref`s point sideways and in cycles,
185
+ subschemas nest anonymously, `allOf` means intersection while `anyOf` means
186
+ union, and half the vocabulary has no type-level meaning. **Stage one** does
187
+ that flattening once — resolving refs, breaking cycles by name, deriving
188
+ identifiers, widening honestly — into a flat list of named declarations.
189
+
190
+ **Stage two** is a JTLT stylesheet per target. TypeScript and Markdown ship;
191
+ both read the same model, and neither has privileged access to it.
192
+
193
+ The [type model](./docs/EMIT-FORMAT.md) is a **published format** with its own
194
+ [JSON Schema](./schemas/jaren-emit-model.schema.json) — and the test suite
195
+ validates every model the package produces against it, so "published format"
196
+ stays true rather than becoming decoration. A third-party emitter targeting
197
+ the model is exactly as capable as the bundled ones.
198
+
199
+ Markdown exists mainly as evidence for that claim: it is as unlike TypeScript
200
+ as a target gets, and adding it required **no change to stage one**. If the
201
+ model had secretly been the TypeScript printer's private state, writing it
202
+ would have forced one.
203
+
204
+ ## CLI
205
+
206
+ ```bash
207
+ jaren-emit --schema <file|dir> --out <dir> [options]
208
+
209
+ --target <name> typescript (default) or markdown
210
+ --name <Name> root declaration name for a single schema
211
+ --bundle <file> one output file instead of one per schema
212
+ --check write nothing; exit 1 if any output is out of date
213
+ ```
214
+
215
+ `--check` is the CI guard: it fails the build when a schema changed and the
216
+ generated types did not, which is the failure mode that makes generated code
217
+ untrustworthy in the first place.
218
+
219
+ `--bundle` compiles every schema into **one name space**: a `$defs.Id` that
220
+ two schemas both declare comes out as `Id` and `Id2`, deterministically in
221
+ sorted-file order, instead of two colliding declarations. The programmatic
222
+ equivalent is the `reserved` option of `compileEmitModel`.
223
+
224
+ ## What it maps
225
+
226
+ | JSON Schema | TypeScript |
227
+ | --- | --- |
228
+ | `type: 'string' \| 'number' \| 'integer' \| 'boolean' \| 'null'` | the primitive (`integer` → `number`) |
229
+ | `const` / `enum` | a literal / a union of literals |
230
+ | `properties` + `required` | interface members, optional when not required |
231
+ | `additionalProperties` / `patternProperties` | an index signature, widened to cover the declared members |
232
+ | *omitted* `additionalProperties` | `[key: string]: unknown` — the object is **open**, see below |
233
+ | `additionalProperties: false` | a closed interface, with no index signature; with no members at all, `Record<string, never>` (an empty interface would let a primitive through) |
234
+ | `items` | `Array<T>` |
235
+ | `prefixItems`, array-form `items` | a tuple: the first `minItems` positions required, the rest optional, and an **open** rest (`...Array<unknown>`) unless `items: false`/`additionalItems: false` closes it — JSON Schema accepts shorter and longer arrays, so the type does too |
236
+ | `$ref` (same document, including cycles) | a reference to the named declaration — `#/pointer` and plain `#anchor` forms, resolving exactly as `compileNormalizer` resolves them; a root `$ref` aliases its target, and 2019-09+ siblings intersect with it |
237
+ | `allOf` | an intersection |
238
+ | `anyOf`, `oneOf` | a union |
239
+ | `properties`/`items` with **no `type`** | the container shape as one union arm, plus the other JSON kinds — applicators do not imply a container, and the validator accepts a primitive without reading them |
240
+ | `description` | a doc comment |
241
+ | `default` (with `--defaults`) | optional on the accepted side — even when `required` lists it, since the normalizer materializes it before validation — and present on the normalized side |
242
+
243
+ ### Objects are open unless the schema closes them
244
+
245
+ A JSON Schema object accepts members it never declared. That is the default,
246
+ and it is easy to forget when reading a schema that lists four properties and
247
+ looks like a struct. So an interface generated from one carries an index
248
+ signature, and only `additionalProperties: false` removes it.
249
+
250
+ This costs something real: with an index signature TypeScript stops flagging a
251
+ misspelled property, because the misspelling is a legal member. The trade is
252
+ deliberate. A type that is **narrower** than its schema rejects a document your
253
+ service accepts — the caller is told their payload is wrong by the very
254
+ artifact that promised to describe it, and no amount of local convenience is
255
+ worth a generated type that lies in that direction. If you want the tighter
256
+ type, say so in the schema with `additionalProperties: false` and get it
257
+ honestly, or pass `openObjects: 'closed'` and own the divergence.
258
+
259
+ Both directions are pinned by the agreement corpus: an open schema's extra
260
+ member must type-check, and a closed schema's must not.
261
+
262
+ **Widened, with the constraint recorded**: `minLength`, `maxLength`,
263
+ `pattern`, `format`, `minimum`, `maximum`, `exclusiveMinimum`,
264
+ `exclusiveMaximum`, `multipleOf`, `minItems`, `maxItems`, `uniqueItems`,
265
+ `contains`, `minProperties`, `maxProperties`, `propertyNames`,
266
+ `dependentRequired`, `dependentSchemas`/`dependencies`, `not`,
267
+ `if`/`then`/`else`, constraining `unevaluatedProperties`/`unevaluatedItems`,
268
+ integer-ness (`type: "integer"` emits as `number`), the key restrictions of
269
+ `patternProperties`, and the Jaren extension keywords. Nothing on this list
270
+ narrows a type, and nothing on it disappears silently: each is written into
271
+ the generated file's doc comment.
272
+
273
+ **Not mapped**: `if`/`then`/`else` and `not` contribute nothing to the type —
274
+ they have no sound type-level equivalent, so they are recorded (see above)
275
+ rather than guessed at. Cross-document `$ref`s are not followed — a model
276
+ compiles the document it was handed — and an unresolvable reference
277
+ contributes nothing, leaving the node honestly wider.
278
+
279
+ ## Honest comparison
280
+
281
+ [`json-schema-to-ts`](https://github.com/ThomasAribart/json-schema-to-ts)
282
+ computes types *in the type system* from schema literals. This generates
283
+ *source*. Different mechanism, different trade:
284
+
285
+ - **Theirs** needs no build step and stays exact as you edit a literal.
286
+ - **Ours** works on schemas that live in `.json` files, produces declarations
287
+ a human can read and review in a diff, emits non-TypeScript targets, and can
288
+ be checked against the validator that will actually run.
289
+
290
+ If your schemas are TypeScript literals and you want zero build steps, use
291
+ theirs. If your schemas are documents — published, shared with other
292
+ languages, or fed to an LLM's structured-output mode — this is the one that
293
+ fits.
294
+
295
+ ## Development
296
+
297
+ Tests live in `test/emit/` at the repository root (`npm run test:emit`).
298
+ `agreement.test.js` is the cyclic verification and needs the workspace's
299
+ TypeScript. The internals are described in [ARCHITECTURE.md](./ARCHITECTURE.md);
300
+ the model contract is in [EMIT-FORMAT.md](./docs/EMIT-FORMAT.md).
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,3 @@
1
+ export { compileEmitModel, EMIT_MODEL_VERSION } from './model.js';
2
+ export { emitTypeScript, renderTypeScript, TYPESCRIPT_STYLESHEET } from './typescript.js';
3
+ export { emitMarkdown, renderMarkdown, MARKDOWN_STYLESHEET } from './markdown.js';
@@ -0,0 +1,102 @@
1
+ /** Reference documentation for a type model. */
2
+ export declare const MARKDOWN_STYLESHEET: {
3
+ $jtlt: string;
4
+ output: string;
5
+ rules: ({
6
+ match: string;
7
+ body: {
8
+ $apply: string[];
9
+ }[][];
10
+ priority?: undefined;
11
+ mode?: undefined;
12
+ } | {
13
+ match: {
14
+ schema: {
15
+ type: string;
16
+ properties: {
17
+ kind: {
18
+ const: any;
19
+ };
20
+ };
21
+ required: string[];
22
+ };
23
+ };
24
+ mode: string;
25
+ body: (string | {
26
+ $apply: string[];
27
+ }[] | {
28
+ $raw: string;
29
+ })[];
30
+ priority?: undefined;
31
+ } | {
32
+ match: {
33
+ schema: {
34
+ type: string;
35
+ properties: {
36
+ kind: {
37
+ const: any;
38
+ };
39
+ };
40
+ required: string[];
41
+ };
42
+ };
43
+ mode: string;
44
+ priority: number;
45
+ body: (string | {
46
+ $apply: string[];
47
+ }[])[];
48
+ } | {
49
+ mode: string;
50
+ priority: number;
51
+ body: (string | {
52
+ $apply: string[];
53
+ }[])[];
54
+ match?: undefined;
55
+ } | {
56
+ priority?: undefined;
57
+ mode: string;
58
+ body: (string | {
59
+ $apply: string[];
60
+ }[] | {
61
+ $raw: string;
62
+ })[];
63
+ match?: undefined;
64
+ } | {
65
+ priority?: undefined;
66
+ mode: string;
67
+ body: {
68
+ $if: string[];
69
+ }[];
70
+ match?: undefined;
71
+ } | {
72
+ priority?: undefined;
73
+ match: {
74
+ schema: {
75
+ type: string;
76
+ properties: {
77
+ kind: {
78
+ const: any;
79
+ };
80
+ };
81
+ required: string[];
82
+ };
83
+ };
84
+ mode: string;
85
+ body: {
86
+ $json: string;
87
+ }[];
88
+ })[];
89
+ };
90
+ /**
91
+ * Render a type model as Markdown reference documentation.
92
+ * @param {import('./model.js').EmitModel} model - A type model from {@link compileEmitModel}
93
+ * @returns {string} Markdown
94
+ */
95
+ export declare function renderMarkdown(model: import('./model.js').EmitModel): string;
96
+ /**
97
+ * Compile a JSON Schema straight to Markdown reference documentation.
98
+ * @param {object|boolean} schema - The schema to document
99
+ * @param {import('./model.js').EmitModelOptions} [options] - Model options
100
+ * @returns {string} Markdown
101
+ */
102
+ export declare function emitMarkdown(schema: object | boolean, options?: import('./model.js').EmitModelOptions): string;
@@ -0,0 +1,170 @@
1
+ /** The model format version this module produces and consumes. */
2
+ export declare const EMIT_MODEL_VERSION = "0.1";
3
+ export type NormalizeOptions = import('@jarenjs/validate/normalize').NormalizeOptions;
4
+ export type EmitConstraint = {
5
+ /**
6
+ * - The schema keyword
7
+ */
8
+ keyword: string;
9
+ /**
10
+ * - The keyword's value in the source schema
11
+ */
12
+ value?: any;
13
+ };
14
+ export type EmitTypeRef = {
15
+ kind: 'unknown' | 'never' | 'primitive' | 'literal' | 'ref' | 'array' | 'tuple' | 'optional' | 'record' | 'union' | 'intersection' | 'object';
16
+ /**
17
+ * - For `primitive`
18
+ */
19
+ primitive?: 'string' | 'number' | 'boolean' | 'null';
20
+ /**
21
+ * - The JSON value of a `literal`, or the value type of a `record`
22
+ */
23
+ value?: any;
24
+ /**
25
+ * - For `ref`: the referenced declaration name
26
+ */
27
+ ref?: string;
28
+ /**
29
+ * - `array` item type, or `tuple` positional items
30
+ */
31
+ items?: EmitTypeRef | EmitTypeRef[];
32
+ /**
33
+ * - For `tuple`: the rest type, when the tuple is open
34
+ */
35
+ rest?: EmitTypeRef;
36
+ /**
37
+ * - For `optional`: the wrapped tuple element
38
+ */
39
+ item?: EmitTypeRef;
40
+ /**
41
+ * - For `union` (at least two)
42
+ */
43
+ options?: EmitTypeRef[];
44
+ /**
45
+ * - For `intersection` (at least two)
46
+ */
47
+ parts?: EmitTypeRef[];
48
+ /**
49
+ * - For `object`
50
+ */
51
+ members?: EmitMember[];
52
+ /**
53
+ * - For `object`: the index-signature value type
54
+ */
55
+ index?: EmitTypeRef;
56
+ };
57
+ export type EmitMember = {
58
+ kind: 'member';
59
+ /**
60
+ * - The property name, verbatim
61
+ */
62
+ name: string;
63
+ type: EmitTypeRef;
64
+ required: boolean;
65
+ /**
66
+ * - The schema default, when it declares one
67
+ */
68
+ default?: any;
69
+ constraints: EmitConstraint[];
70
+ doc: string[];
71
+ };
72
+ export type EmitDeclaration = {
73
+ kind: 'declaration';
74
+ /**
75
+ * - Unique, identifier-safe
76
+ */
77
+ name: string;
78
+ type: EmitTypeRef;
79
+ constraints: EmitConstraint[];
80
+ doc: string[];
81
+ /**
82
+ * - Which side of normalization this declaration describes
83
+ */
84
+ variant?: 'accepted' | 'normalized';
85
+ /**
86
+ * - For an accepted variant, its normalized counterpart
87
+ */
88
+ variantOf?: string;
89
+ };
90
+ export type EmitModel = {
91
+ /**
92
+ * - The model format version
93
+ */
94
+ $emit: string;
95
+ /**
96
+ * - Where the model came from
97
+ */
98
+ source: string | null;
99
+ /**
100
+ * - The declaration name of the schema's root
101
+ */
102
+ root: string | null;
103
+ /**
104
+ * - Present when accepted/normalized pairs were derived
105
+ */
106
+ variants?: true;
107
+ declarations: EmitDeclaration[];
108
+ };
109
+ export type EmitModelOptions = {
110
+ /**
111
+ * - The name for the root declaration
112
+ */
113
+ name?: string;
114
+ /**
115
+ * - A source identifier recorded in the model
116
+ */
117
+ source?: string;
118
+ /**
119
+ * - How to treat an object
120
+ * whose `additionalProperties` is omitted. JSON Schema says such an object
121
+ * is open, so the default emits an index signature; `'closed'` opts into the
122
+ * tighter type, which regains excess-property checking at the cost of
123
+ * rejecting documents the schema accepts.
124
+ */
125
+ openObjects?: 'open' | 'closed';
126
+ /**
127
+ * - When set, derive
128
+ * accepted/normalized variant pairs with exactly these `compileNormalizer`
129
+ * options
130
+ */
131
+ normalize?: NormalizeOptions | null;
132
+ /**
133
+ * - The suffix for accepted-variant
134
+ * declaration names
135
+ */
136
+ variantSuffix?: string;
137
+ /**
138
+ * - Declaration names already taken outside
139
+ * this model. Bundling concatenates models into one file, so each model
140
+ * must be able to avoid the names its predecessors used.
141
+ */
142
+ reserved?: string[];
143
+ /**
144
+ * - Extension keyword names (typically
145
+ * `x-*`) to PRESERVE: when a property schema carries one of these, the
146
+ * member node gains `extensions: { '<keyword>': value }` with the value
147
+ * copied verbatim. The compiler itself never interprets them — a
148
+ * downstream consumer of the model does. Keywords are read from the
149
+ * property node itself (a vocabulary declares them inline, not through
150
+ * `$ref`). Absent by default, so existing models are byte-identical.
151
+ */
152
+ extensions?: string[];
153
+ };
154
+ /**
155
+ * Compile one or more JSON Schemas into a type model.
156
+ *
157
+ * The model is a plain JSON document. It is the contract every emitter reads,
158
+ * and it is published as a schema so a third-party emitter can target it too.
159
+ * @param {object|boolean} schema - The root schema
160
+ * @param {EmitModelOptions} [options] - Compile options
161
+ * @returns {EmitModel} The type model document
162
+ * @example
163
+ * const model = compileEmitModel({
164
+ * $defs: { Id: { type: 'string' } },
165
+ * type: 'object',
166
+ * properties: { id: { $ref: '#/$defs/Id' } },
167
+ * required: ['id'],
168
+ * }, { name: 'User' });
169
+ */
170
+ export declare function compileEmitModel(schema: object | boolean, options?: EmitModelOptions): EmitModel;