@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.
- package/ARCHITECTURE.md +97 -0
- package/README.md +300 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/markdown.d.ts +102 -0
- package/dist/types/model.d.ts +170 -0
- package/dist/types/typescript.d.ts +156 -0
- package/docs/EMIT-FORMAT.md +273 -0
- package/package.json +75 -0
- package/schemas/jaren-emit-model.schema.json +171 -0
- package/src/cli.js +191 -0
- package/src/index.js +21 -0
- package/src/markdown.js +105 -0
- package/src/model.js +993 -0
- package/src/typescript.js +276 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stylesheet. Every rule dispatches on a node's `kind`, so the model's
|
|
3
|
+
* vocabulary is the only coupling between the two stages.
|
|
4
|
+
*/
|
|
5
|
+
export declare const TYPESCRIPT_STYLESHEET: {
|
|
6
|
+
$jtlt: string;
|
|
7
|
+
output: string;
|
|
8
|
+
rules: ({
|
|
9
|
+
match: string;
|
|
10
|
+
body: {
|
|
11
|
+
$apply: string[];
|
|
12
|
+
}[][];
|
|
13
|
+
priority?: undefined;
|
|
14
|
+
mode?: undefined;
|
|
15
|
+
} | {
|
|
16
|
+
match: {
|
|
17
|
+
schema: {
|
|
18
|
+
type: string;
|
|
19
|
+
properties: {
|
|
20
|
+
kind: {
|
|
21
|
+
const: string;
|
|
22
|
+
};
|
|
23
|
+
type: {
|
|
24
|
+
properties: {
|
|
25
|
+
kind: {
|
|
26
|
+
const: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
required: string[];
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
mode: string;
|
|
35
|
+
priority: number;
|
|
36
|
+
body: (string | {
|
|
37
|
+
$apply: string[];
|
|
38
|
+
}[] | {
|
|
39
|
+
$raw: string;
|
|
40
|
+
})[];
|
|
41
|
+
} | {
|
|
42
|
+
match: {
|
|
43
|
+
schema: {
|
|
44
|
+
type: string;
|
|
45
|
+
properties: {
|
|
46
|
+
kind: {
|
|
47
|
+
const: any;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
required: string[];
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
mode: string;
|
|
54
|
+
priority: number;
|
|
55
|
+
body: (string | {
|
|
56
|
+
$apply: string[];
|
|
57
|
+
}[] | {
|
|
58
|
+
$raw: string;
|
|
59
|
+
})[];
|
|
60
|
+
} | {
|
|
61
|
+
priority?: undefined;
|
|
62
|
+
mode: string;
|
|
63
|
+
body: (string | {
|
|
64
|
+
$apply: string[];
|
|
65
|
+
}[] | {
|
|
66
|
+
$raw: string;
|
|
67
|
+
$if?: undefined;
|
|
68
|
+
} | {
|
|
69
|
+
$raw?: undefined;
|
|
70
|
+
$if: (string | {
|
|
71
|
+
$not: string;
|
|
72
|
+
})[];
|
|
73
|
+
})[];
|
|
74
|
+
match?: undefined;
|
|
75
|
+
} | {
|
|
76
|
+
priority?: undefined;
|
|
77
|
+
match: {
|
|
78
|
+
schema: {
|
|
79
|
+
type: string;
|
|
80
|
+
properties: {
|
|
81
|
+
kind: {
|
|
82
|
+
const: any;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
required: string[];
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
mode: string;
|
|
89
|
+
body: {
|
|
90
|
+
$raw: string;
|
|
91
|
+
}[];
|
|
92
|
+
} | {
|
|
93
|
+
priority?: undefined;
|
|
94
|
+
match: {
|
|
95
|
+
schema: {
|
|
96
|
+
type: string;
|
|
97
|
+
properties: {
|
|
98
|
+
kind: {
|
|
99
|
+
const: any;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
required: string[];
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
mode: string;
|
|
106
|
+
body: {
|
|
107
|
+
$json: string;
|
|
108
|
+
}[];
|
|
109
|
+
} | {
|
|
110
|
+
priority?: undefined;
|
|
111
|
+
match: {
|
|
112
|
+
schema: {
|
|
113
|
+
type: string;
|
|
114
|
+
properties: {
|
|
115
|
+
kind: {
|
|
116
|
+
const: any;
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
required: string[];
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
mode: string;
|
|
123
|
+
body: (string | {
|
|
124
|
+
$apply: string[];
|
|
125
|
+
}[])[];
|
|
126
|
+
})[];
|
|
127
|
+
};
|
|
128
|
+
export type EmitModel = import('./model.js').EmitModel;
|
|
129
|
+
export type EmitModelOptions = import('./model.js').EmitModelOptions;
|
|
130
|
+
export type RenderTypeScriptOptions = {
|
|
131
|
+
/**
|
|
132
|
+
* - Emit the do-not-edit header
|
|
133
|
+
*/
|
|
134
|
+
banner?: boolean;
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Render a type model as TypeScript declarations.
|
|
138
|
+
* @param {EmitModel} model - A type model from `compileEmitModel`
|
|
139
|
+
* @param {RenderTypeScriptOptions} [options]
|
|
140
|
+
* @returns {string} TypeScript source
|
|
141
|
+
*/
|
|
142
|
+
export declare function renderTypeScript(model: EmitModel, options?: RenderTypeScriptOptions): string;
|
|
143
|
+
/**
|
|
144
|
+
* Compile a JSON Schema straight to TypeScript declarations — the one-call
|
|
145
|
+
* form of {@link compileEmitModel} followed by {@link renderTypeScript}.
|
|
146
|
+
* @param {object|boolean} schema - The schema to emit
|
|
147
|
+
* @param {EmitModelOptions & RenderTypeScriptOptions} [options] - Model and
|
|
148
|
+
* rendering options — `normalize` and `variantSuffix` included, so the
|
|
149
|
+
* programmatic route can do everything the CLI flags can
|
|
150
|
+
* @returns {string} TypeScript source
|
|
151
|
+
* @example
|
|
152
|
+
* emitTypeScript({ type: 'object', properties: { id: { type: 'string' } } },
|
|
153
|
+
* { name: 'User' });
|
|
154
|
+
* // export interface User { id?: string; }
|
|
155
|
+
*/
|
|
156
|
+
export declare function emitTypeScript(schema: object | boolean, options?: EmitModelOptions & RenderTypeScriptOptions): string;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# The Jaren emit type model — format 0.1
|
|
2
|
+
|
|
3
|
+
The contract between a schema analysis and an artifact emitter.
|
|
4
|
+
|
|
5
|
+
This document is normative for producers and consumers of the model; the
|
|
6
|
+
machine-checkable form is
|
|
7
|
+
[`jaren-emit-model.schema.json`](../schemas/jaren-emit-model.schema.json), and
|
|
8
|
+
the package's test suite validates every model it produces against it.
|
|
9
|
+
|
|
10
|
+
## 1. Why the model is a separate, published stage
|
|
11
|
+
|
|
12
|
+
A JSON Schema graph cannot be walked top-to-bottom by a template:
|
|
13
|
+
|
|
14
|
+
- `$ref` points sideways, and may point in a cycle;
|
|
15
|
+
- a subschema may be anonymous, while a declaration needs a name;
|
|
16
|
+
- `allOf` is intersection, `anyOf`/`oneOf` are unions, and `if`/`then` is
|
|
17
|
+
neither;
|
|
18
|
+
- roughly half the vocabulary constrains a value without narrowing its type.
|
|
19
|
+
|
|
20
|
+
The model is the result of resolving all four. It is a **flat, ordered list of
|
|
21
|
+
named declarations** in which every reference is by name — so a cyclic schema
|
|
22
|
+
produces an acyclic model — and every constraint that a type cannot carry is
|
|
23
|
+
recorded rather than dropped.
|
|
24
|
+
|
|
25
|
+
Publishing it, rather than keeping it as an emitter's private state, is what
|
|
26
|
+
makes a third-party emitter a first-class citizen: the bundled TypeScript and
|
|
27
|
+
Markdown emitters read exactly this and nothing else.
|
|
28
|
+
|
|
29
|
+
## 2. The document
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{ "$emit": "0.1",
|
|
33
|
+
"source": "user.json",
|
|
34
|
+
"root": "User",
|
|
35
|
+
"declarations": [] }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `$emit` (REQUIRED) — the format version, the string `"0.1"`.
|
|
39
|
+
- `declarations` (REQUIRED) — the declarations, in **emission order**.
|
|
40
|
+
- `source` (OPTIONAL) — where the model came from; emitters may put it in a
|
|
41
|
+
generated banner. `null` when unknown.
|
|
42
|
+
- `root` (OPTIONAL) — the declaration corresponding to the source schema's
|
|
43
|
+
root. `null` for a bundle with no single root.
|
|
44
|
+
|
|
45
|
+
## 3. Declarations
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{ "kind": "declaration",
|
|
49
|
+
"name": "User",
|
|
50
|
+
"type": { "kind": "object", "members": [] },
|
|
51
|
+
"constraints": [],
|
|
52
|
+
"doc": ["A user"] }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`name` is unique across the document and is a safe identifier: it matches
|
|
56
|
+
`[A-Za-z_$][A-Za-z0-9_$]*`. A producer derives it from the schema's own
|
|
57
|
+
vocabulary where one exists (a `$defs` key, a `$ref`'s last pointer token) and
|
|
58
|
+
otherwise from the path that reached the node. **Names are deterministic**: the
|
|
59
|
+
same input document produces the same names in the same order, because a
|
|
60
|
+
generated artifact that churns between runs cannot be reviewed in a diff.
|
|
61
|
+
|
|
62
|
+
## 4. Members
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{ "kind": "member",
|
|
66
|
+
"name": "id",
|
|
67
|
+
"type": { "kind": "primitive", "primitive": "string" },
|
|
68
|
+
"required": true,
|
|
69
|
+
"default": 0,
|
|
70
|
+
"constraints": [],
|
|
71
|
+
"doc": [] }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`name` is the property name **verbatim** — it is data, not an identifier, and
|
|
75
|
+
an emitter quotes it if its target language requires that. `default` is
|
|
76
|
+
present only when the source schema declares one. Members appear in the order
|
|
77
|
+
the schema declared them.
|
|
78
|
+
|
|
79
|
+
### 4.1 The extension seam
|
|
80
|
+
|
|
81
|
+
Unknown keywords normally vanish from the model — only the listed
|
|
82
|
+
constraint groups are carried. A producer MAY be told to preserve
|
|
83
|
+
named extension keywords instead:
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
compileEmitModel(schema, { extensions: ['x-entity'] })
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Every property member whose schema declares a listed keyword gains
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{ "kind": "member", "name": "posts",
|
|
93
|
+
"extensions": { "x-entity": { "relation": { "to": "Post" } } }, … }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
with the value copied **verbatim and uninterpreted** — the compiler
|
|
97
|
+
learns "keep these keywords", never what they mean. Keywords are read
|
|
98
|
+
from the property schema node itself: a vocabulary that wants to ride
|
|
99
|
+
this seam declares its members inline, not behind `$ref`. `extensions`
|
|
100
|
+
is absent when no listed keyword is present, and the option is absent
|
|
101
|
+
by default, so models without it are byte-identical to before.
|
|
102
|
+
|
|
103
|
+
This is the contract for vocabulary-aware artifacts: a downstream
|
|
104
|
+
package post-processes the MODEL DOCUMENT (replace member types,
|
|
105
|
+
adjust `required`, add declarations) and hands the result back to any
|
|
106
|
+
renderer. `@jarenjs/db` renders entity-aware TypeScript from
|
|
107
|
+
`x-entity` this way, and `x-form` is the obvious second customer —
|
|
108
|
+
the seam serves any vocabulary, which is why it lives here and not in
|
|
109
|
+
either consumer.
|
|
110
|
+
|
|
111
|
+
## 5. Type references
|
|
112
|
+
|
|
113
|
+
Every type reference carries a `kind`, so an emitter dispatches on shape
|
|
114
|
+
rather than on position. That is what lets a stylesheet match with a schema
|
|
115
|
+
match and stay correct under re-dispatch.
|
|
116
|
+
|
|
117
|
+
| `kind` | members | meaning |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `unknown` | — | nothing is known; the honest top type |
|
|
120
|
+
| `never` | — | nothing satisfies this (`false` schema) |
|
|
121
|
+
| `primitive` | `primitive` | one of `string`, `number`, `boolean`, `null` |
|
|
122
|
+
| `literal` | `value` | exactly this JSON value |
|
|
123
|
+
| `ref` | `ref` | the declaration with that `name` |
|
|
124
|
+
| `array` | `items` | a homogeneous list |
|
|
125
|
+
| `tuple` | `items`, `rest?` | positional items, optionally followed by a rest type |
|
|
126
|
+
| `optional` | `item` | a tuple position that may be absent; only valid inside `tuple.items`, after every non-optional position |
|
|
127
|
+
| `record` | `value` | a map from string keys to `value` |
|
|
128
|
+
| `object` | `members`, `index?` | declared members, plus an optional index signature |
|
|
129
|
+
| `union` | `options` (≥ 2) | any one of |
|
|
130
|
+
| `intersection` | `parts` (≥ 2) | all of |
|
|
131
|
+
|
|
132
|
+
A producer MUST collapse the degenerate forms: a union or intersection of one
|
|
133
|
+
is that one type, and a union containing `unknown` is `unknown`. A consumer is
|
|
134
|
+
therefore entitled to assume `options` and `parts` have at least two entries.
|
|
135
|
+
A tuple with no positional items MUST also collapse: with a `rest` type it is
|
|
136
|
+
an `array` of that type, and without one it is the empty tuple — a consumer
|
|
137
|
+
never has to print a rest with nothing in front of it.
|
|
138
|
+
|
|
139
|
+
**Tuples mirror what the schema enforces, not what it suggests.** JSON Schema
|
|
140
|
+
`prefixItems` constrains the positions that exist; `minItems` says how many
|
|
141
|
+
must; an omitted rest schema leaves the array open. A producer therefore
|
|
142
|
+
marks only the first `minItems` positions non-optional and emits an `unknown`
|
|
143
|
+
rest for an omitted one — a closed, all-required tuple is emitted only when
|
|
144
|
+
the schema actually closes it.
|
|
145
|
+
|
|
146
|
+
**`index` covers the declared members.** When an object has both members and
|
|
147
|
+
an index signature, the index type is widened to include every member's type,
|
|
148
|
+
because TypeScript — and any other target with the same rule — rejects an
|
|
149
|
+
index signature that does not.
|
|
150
|
+
|
|
151
|
+
## 6. Constraints and documentation
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{ "constraints": [{ "keyword": "minLength", "value": 3 }],
|
|
155
|
+
"doc": ["Display name",
|
|
156
|
+
"Schema constraints this type cannot express: minLength=3"] }
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`constraints` is the **structured** record of everything the source schema
|
|
160
|
+
asserted that the type does not enforce. `doc` is the same information already
|
|
161
|
+
flattened to lines, together with the schema's `description`, so an emitter
|
|
162
|
+
prints lines rather than deciding what belongs in a comment.
|
|
163
|
+
|
|
164
|
+
Both are always present; an empty array means there is nothing to say, and an
|
|
165
|
+
emitter MUST then produce no comment at all. That absence is deliberate: it
|
|
166
|
+
lets a template express "only when present" by dispatching a path that yields
|
|
167
|
+
the empty sequence, without a conditional.
|
|
168
|
+
|
|
169
|
+
**A producer MUST NOT drop a constraint silently.** The whole reason a
|
|
170
|
+
generated type is trustworthy is that what it cannot say, it says it cannot
|
|
171
|
+
say.
|
|
172
|
+
|
|
173
|
+
## 7. Accepted and normalized variants
|
|
174
|
+
|
|
175
|
+
`@jarenjs/validate/normalize` makes a contract's input and output shapes
|
|
176
|
+
differ: a defaulted member is optional for the caller and present afterwards,
|
|
177
|
+
and a coerced member accepts its transport form on the way in. A model
|
|
178
|
+
compiled with normalization options carries **both**, so a typed contract
|
|
179
|
+
boundary can name them separately.
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{ "$emit": "0.1", "variants": true, "declarations": [
|
|
183
|
+
{ "kind": "declaration", "name": "Config", "variant": "normalized", "…": "…" },
|
|
184
|
+
{ "kind": "declaration", "name": "ConfigInput", "variant": "accepted",
|
|
185
|
+
"variantOf": "Config", "…": "…" } ] }
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
- `variants` on the document is `true` exactly when variant pairs were
|
|
189
|
+
derived. Without normalization options there are no `variant` members at
|
|
190
|
+
all, and each declaration is simply the schema's shape.
|
|
191
|
+
- The **normalized** declaration keeps the plain name. It is the shape *after*
|
|
192
|
+
normalizing, which is what the rest of a program handles.
|
|
193
|
+
- The **accepted** declaration takes a suffix (`Input` by default) and names
|
|
194
|
+
its counterpart in `variantOf`.
|
|
195
|
+
|
|
196
|
+
**A twin is emitted only when the type actually differs.** A producer computes
|
|
197
|
+
that bottom-up — a type differs if anything it contains differs — so a schema
|
|
198
|
+
with one defaulted field does not double every declaration in the document.
|
|
199
|
+
Everything unaffected is referenced by its single shared name from both sides.
|
|
200
|
+
|
|
201
|
+
**Two normalizations produce a difference, and only two.** `useDefaults` moves
|
|
202
|
+
a member from optional to required across the boundary. `coerceTypes` widens
|
|
203
|
+
the accepted side to the types the normalizer will convert *from*. `trimStrings`
|
|
204
|
+
is string-to-string and `removeAdditional` removes members no type declared, so
|
|
205
|
+
neither justifies a second declaration.
|
|
206
|
+
|
|
207
|
+
A producer MUST derive the accepted side from the **same** switch resolution
|
|
208
|
+
the normalizer uses, including predicate options. A variant that disagrees
|
|
209
|
+
with the normalizer is worse than no variant: it is a type that certifies an
|
|
210
|
+
input the normalizer will not accept.
|
|
211
|
+
|
|
212
|
+
The same rule extends to *where* normalization runs. `compileNormalizer`
|
|
213
|
+
deliberately does not descend `anyOf`/`oneOf` — which branch applies is only
|
|
214
|
+
known after validating — so inside a union branch no default materializes and
|
|
215
|
+
no coercion applies. A producer MUST NOT let variant semantics leak into
|
|
216
|
+
union branches: a branch references the schema's **as-declared** reading.
|
|
217
|
+
When a referenced type differs under normalization, that reading is its own
|
|
218
|
+
`Plain`-suffixed declaration, shared by both sides of the pair; when it does
|
|
219
|
+
not differ, the branch shares the single plain-named declaration.
|
|
220
|
+
|
|
221
|
+
Three consequences worth stating because each was once wrong:
|
|
222
|
+
|
|
223
|
+
- a **`required` member with an enabled default** is still optional on the
|
|
224
|
+
accepted side — the normalizer materializes it before validation runs;
|
|
225
|
+
- a **`const`/`enum` with an enabled coercion** widens its accepted side by
|
|
226
|
+
the source primitives that can actually reach a member of the literal set
|
|
227
|
+
(an integer enum admits `string`; a string enum of words admits nothing
|
|
228
|
+
extra, because no number ever becomes `"admin"`);
|
|
229
|
+
- coercion widens only nodes with a **single string-valued `type`**, because
|
|
230
|
+
that is the only place `coerceToType` runs.
|
|
231
|
+
|
|
232
|
+
## 8. Determinism
|
|
233
|
+
|
|
234
|
+
Two compilations of the same input MUST produce byte-identical models.
|
|
235
|
+
Concretely, a producer:
|
|
236
|
+
|
|
237
|
+
- keeps schema declaration order for members and `$defs`;
|
|
238
|
+
- keeps discovery order for declarations;
|
|
239
|
+
- derives names from the document, never from a counter that depends on
|
|
240
|
+
traversal timing;
|
|
241
|
+
- does not serialize a `Set` or `Map` whose order depends on insertion history
|
|
242
|
+
across merges.
|
|
243
|
+
|
|
244
|
+
## 9. What the format deliberately does not model
|
|
245
|
+
|
|
246
|
+
- **`if`/`then`/`else` and `not`.** Neither has a sound type-level reading —
|
|
247
|
+
the first is a conditional type in principle and unreadable in practice, the
|
|
248
|
+
second has no equivalent at all. A producer ignores them for the type and
|
|
249
|
+
**records them as dropped constraints** (§6), rather than inventing a union
|
|
250
|
+
that would be wrong in one direction or the other. The same recording
|
|
251
|
+
applies to the other silently-widening keywords: integer-ness (`type:
|
|
252
|
+
"integer"` emits as `number`), `dependentSchemas`/`dependencies`,
|
|
253
|
+
constraining `unevaluated*` values, and the key restrictions of
|
|
254
|
+
`patternProperties` (the index signature carries their value types, but no
|
|
255
|
+
type restricts which keys a pattern admits).
|
|
256
|
+
- **Type inference from applicators.** `properties` or `items` on a node with
|
|
257
|
+
no `type` does not make it an object or an array — the validator accepts a
|
|
258
|
+
primitive without reading either. A producer emits the described container
|
|
259
|
+
shape as one union arm and the remaining JSON kinds beside it.
|
|
260
|
+
- **Cross-document `$ref`.** A model compiles the document it was given.
|
|
261
|
+
Following a ref into another document would mean owning a resolution scope,
|
|
262
|
+
which is `@jarenjs/validate`'s job, not this format's. Within the document,
|
|
263
|
+
the resolved forms are exactly the ones `compileNormalizer` resolves — `#`,
|
|
264
|
+
`#/pointer` and plain `#anchor` (outside embedded `$id` resources) — and a
|
|
265
|
+
`$ref`'s siblings compose with its target as an intersection, per 2019-09+.
|
|
266
|
+
That composition is unconditional, like the normalizer's: a draft-07
|
|
267
|
+
document in which a validator lets `$ref` shadow its siblings should not
|
|
268
|
+
put constraining siblings there — they are dead keywords to that validator,
|
|
269
|
+
and this producer takes them at their word.
|
|
270
|
+
- **`removeAdditional` and `trimStrings` as type differences.** Neither
|
|
271
|
+
changes a *declared* type: trimming is string-to-string, and stripping
|
|
272
|
+
removes members the type never declared. Only `useDefaults` and
|
|
273
|
+
`coerceTypes` earn a variant (§7).
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jarenjs/emit",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.34.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./dist/types/index.d.ts",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"bin": {
|
|
10
|
+
"jaren-emit": "./src/cli.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist/types/",
|
|
14
|
+
"src/",
|
|
15
|
+
"schemas/",
|
|
16
|
+
"docs/",
|
|
17
|
+
"ARCHITECTURE.md"
|
|
18
|
+
],
|
|
19
|
+
"description": "Build-time artifacts from JSON documents: JSON Schema to TypeScript declarations and beyond, via JTLT stylesheets",
|
|
20
|
+
"author": "joham",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/jklarenbeek/jarenjs.git",
|
|
24
|
+
"directory": "packages/emit"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=24"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public",
|
|
32
|
+
"registry": "https://registry.npmjs.org/"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"jaren",
|
|
36
|
+
"json-schema",
|
|
37
|
+
"typescript",
|
|
38
|
+
"codegen",
|
|
39
|
+
"code-generation",
|
|
40
|
+
"types",
|
|
41
|
+
"dts",
|
|
42
|
+
"json-schema-to-typescript",
|
|
43
|
+
"jtlt"
|
|
44
|
+
],
|
|
45
|
+
"exports": {
|
|
46
|
+
".": {
|
|
47
|
+
"types": "./dist/types/index.d.ts",
|
|
48
|
+
"default": "./src/index.js"
|
|
49
|
+
},
|
|
50
|
+
"./model": {
|
|
51
|
+
"types": "./dist/types/model.d.ts",
|
|
52
|
+
"default": "./src/model.js"
|
|
53
|
+
},
|
|
54
|
+
"./typescript": {
|
|
55
|
+
"types": "./dist/types/typescript.d.ts",
|
|
56
|
+
"default": "./src/typescript.js"
|
|
57
|
+
},
|
|
58
|
+
"./markdown": {
|
|
59
|
+
"types": "./dist/types/markdown.d.ts",
|
|
60
|
+
"default": "./src/markdown.js"
|
|
61
|
+
},
|
|
62
|
+
"./schemas/*": "./schemas/*",
|
|
63
|
+
"./package.json": "./package.json"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "npm run build:types",
|
|
67
|
+
"build:types": "tsc -p tsconfig.json",
|
|
68
|
+
"prepack": "npm run build:types"
|
|
69
|
+
},
|
|
70
|
+
"dependencies": {
|
|
71
|
+
"@jarenjs/core": "^0.34.0",
|
|
72
|
+
"@jarenjs/json": "^0.34.0",
|
|
73
|
+
"@jarenjs/validate": "^0.34.0"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/jklarenbeek/jarenjs/schemas/jaren-emit-model.schema.json",
|
|
4
|
+
"title": "Jaren emit type model",
|
|
5
|
+
"description": "The intermediate between a JSON Schema graph and a generated artifact. Published so that a third-party emitter targets the same contract the bundled ones do.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["$emit", "declarations"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"$emit": {
|
|
11
|
+
"description": "The model format version.",
|
|
12
|
+
"const": "0.1"
|
|
13
|
+
},
|
|
14
|
+
"source": {
|
|
15
|
+
"description": "Where the model came from, recorded for the generated banner.",
|
|
16
|
+
"type": ["string", "null"]
|
|
17
|
+
},
|
|
18
|
+
"root": {
|
|
19
|
+
"description": "The declaration name that corresponds to the schema's root.",
|
|
20
|
+
"type": ["string", "null"]
|
|
21
|
+
},
|
|
22
|
+
"variants": {
|
|
23
|
+
"description": "Present and true when the model carries accepted/normalized variant pairs, i.e. it was compiled with normalization options.",
|
|
24
|
+
"const": true
|
|
25
|
+
},
|
|
26
|
+
"declarations": {
|
|
27
|
+
"description": "Named declarations in emission order. Every reference is by name, so the list is flat even when the source schema was cyclic.",
|
|
28
|
+
"type": "array",
|
|
29
|
+
"items": { "$ref": "#/$defs/declaration" }
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"$defs": {
|
|
33
|
+
"declaration": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"required": ["kind", "name", "type", "constraints", "doc"],
|
|
36
|
+
"additionalProperties": false,
|
|
37
|
+
"properties": {
|
|
38
|
+
"kind": { "const": "declaration" },
|
|
39
|
+
"variant": {
|
|
40
|
+
"description": "Which side of normalization this declaration describes. Absent when the model was compiled without normalization options, in which case the single declaration is both.",
|
|
41
|
+
"enum": ["accepted", "normalized"]
|
|
42
|
+
},
|
|
43
|
+
"variantOf": {
|
|
44
|
+
"description": "For an accepted variant, the name of its normalized counterpart.",
|
|
45
|
+
"type": "string"
|
|
46
|
+
},
|
|
47
|
+
"name": {
|
|
48
|
+
"description": "A unique, identifier-safe name.",
|
|
49
|
+
"type": "string",
|
|
50
|
+
"minLength": 1
|
|
51
|
+
},
|
|
52
|
+
"type": { "$ref": "#/$defs/type" },
|
|
53
|
+
"constraints": { "$ref": "#/$defs/constraints" },
|
|
54
|
+
"doc": { "$ref": "#/$defs/doc" }
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"member": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"required": ["kind", "name", "type", "required", "constraints", "doc"],
|
|
60
|
+
"additionalProperties": false,
|
|
61
|
+
"properties": {
|
|
62
|
+
"kind": { "const": "member" },
|
|
63
|
+
"name": { "type": "string" },
|
|
64
|
+
"type": { "$ref": "#/$defs/type" },
|
|
65
|
+
"required": { "type": "boolean" },
|
|
66
|
+
"default": { "description": "The schema default, when it declares one." },
|
|
67
|
+
"constraints": { "$ref": "#/$defs/constraints" },
|
|
68
|
+
"doc": { "$ref": "#/$defs/doc" }
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"doc": {
|
|
72
|
+
"description": "Documentation lines: the description, then a summary of the constraints the type cannot express. Empty means the node has nothing to say, and an emitter should print no comment at all.",
|
|
73
|
+
"type": "array",
|
|
74
|
+
"items": { "type": "string" }
|
|
75
|
+
},
|
|
76
|
+
"constraints": {
|
|
77
|
+
"description": "Keywords the source schema states that a type cannot carry. Recorded rather than dropped: a reader of the generated artifact must be able to learn that the constraint exists and is not enforced by the type.",
|
|
78
|
+
"type": "array",
|
|
79
|
+
"items": {
|
|
80
|
+
"type": "object",
|
|
81
|
+
"required": ["keyword"],
|
|
82
|
+
"additionalProperties": false,
|
|
83
|
+
"properties": {
|
|
84
|
+
"keyword": { "type": "string" },
|
|
85
|
+
"value": {}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"type": {
|
|
90
|
+
"description": "A type reference. Every form carries a `kind`, so an emitter dispatches on shape rather than on position.",
|
|
91
|
+
"type": "object",
|
|
92
|
+
"required": ["kind"],
|
|
93
|
+
"properties": {
|
|
94
|
+
"kind": {
|
|
95
|
+
"enum": ["unknown", "never", "primitive", "literal", "ref",
|
|
96
|
+
"array", "tuple", "optional", "record", "union", "intersection", "object"]
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"allOf": [
|
|
100
|
+
{
|
|
101
|
+
"if": { "properties": { "kind": { "const": "primitive" } }, "required": ["kind"] },
|
|
102
|
+
"then": {
|
|
103
|
+
"required": ["primitive"],
|
|
104
|
+
"properties": { "primitive": { "enum": ["string", "number", "boolean", "null"] } }
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"if": { "properties": { "kind": { "const": "literal" } }, "required": ["kind"] },
|
|
109
|
+
"then": { "required": ["value"] }
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"if": { "properties": { "kind": { "const": "ref" } }, "required": ["kind"] },
|
|
113
|
+
"then": { "required": ["ref"], "properties": { "ref": { "type": "string" } } }
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"if": { "properties": { "kind": { "const": "array" } }, "required": ["kind"] },
|
|
117
|
+
"then": { "required": ["items"], "properties": { "items": { "$ref": "#/$defs/type" } } }
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"if": { "properties": { "kind": { "const": "record" } }, "required": ["kind"] },
|
|
121
|
+
"then": { "required": ["value"], "properties": { "value": { "$ref": "#/$defs/type" } } }
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"if": { "properties": { "kind": { "const": "tuple" } }, "required": ["kind"] },
|
|
125
|
+
"then": {
|
|
126
|
+
"required": ["items"],
|
|
127
|
+
"properties": {
|
|
128
|
+
"items": { "type": "array", "items": { "$ref": "#/$defs/type" } },
|
|
129
|
+
"rest": { "$ref": "#/$defs/type" }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"if": { "properties": { "kind": { "const": "optional" } }, "required": ["kind"] },
|
|
135
|
+
"then": {
|
|
136
|
+
"required": ["item"],
|
|
137
|
+
"properties": { "item": { "$ref": "#/$defs/type" } }
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"if": { "properties": { "kind": { "const": "union" } }, "required": ["kind"] },
|
|
142
|
+
"then": {
|
|
143
|
+
"required": ["options"],
|
|
144
|
+
"properties": {
|
|
145
|
+
"options": { "type": "array", "minItems": 2, "items": { "$ref": "#/$defs/type" } }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"if": { "properties": { "kind": { "const": "intersection" } }, "required": ["kind"] },
|
|
151
|
+
"then": {
|
|
152
|
+
"required": ["parts"],
|
|
153
|
+
"properties": {
|
|
154
|
+
"parts": { "type": "array", "minItems": 2, "items": { "$ref": "#/$defs/type" } }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"if": { "properties": { "kind": { "const": "object" } }, "required": ["kind"] },
|
|
160
|
+
"then": {
|
|
161
|
+
"required": ["members"],
|
|
162
|
+
"properties": {
|
|
163
|
+
"members": { "type": "array", "items": { "$ref": "#/$defs/member" } },
|
|
164
|
+
"index": { "$ref": "#/$defs/type" }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|