@jarenjs/emit 0.56.0 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -3
- package/package.json +4 -4
- package/src/cli.js +17 -0
- package/src/model.js +7 -3
package/README.md
CHANGED
|
@@ -216,6 +216,10 @@ jaren-emit --schema <file|dir> --out <dir> [options]
|
|
|
216
216
|
generated types did not, which is the failure mode that makes generated code
|
|
217
217
|
untrustworthy in the first place.
|
|
218
218
|
|
|
219
|
+
Outside bundle mode, schema names must produce distinct output filenames.
|
|
220
|
+
Names such as `user-account.json` and `user_account.json` both become
|
|
221
|
+
`UserAccount.d.ts`; the CLI refuses that collision with exit 2 before writing.
|
|
222
|
+
|
|
219
223
|
`--bundle` compiles every schema into **one name space**: a `$defs.Id` that
|
|
220
224
|
two schemas both declare comes out as `Id` and `Id2`, deterministically in
|
|
221
225
|
sorted-file order, instead of two colliding declarations. The programmatic
|
|
@@ -228,9 +232,9 @@ equivalent is the `reserved` option of `compileEmitModel`.
|
|
|
228
232
|
| `type: 'string' \| 'number' \| 'integer' \| 'boolean' \| 'null'` | the primitive (`integer` → `number`) |
|
|
229
233
|
| `const` / `enum` | a literal / a union of literals |
|
|
230
234
|
| `properties` + `required` | interface members, optional when not required |
|
|
231
|
-
| `additionalProperties` / `patternProperties` | an index signature, widened to cover the declared members |
|
|
235
|
+
| `additionalProperties` / `patternProperties` | an index signature combining their value types, widened to cover the declared members |
|
|
232
236
|
| *omitted* `additionalProperties` | `[key: string]: unknown` — the object is **open**, see below |
|
|
233
|
-
| `additionalProperties: false` | a closed interface
|
|
237
|
+
| `additionalProperties: false` | a closed interface unless `patternProperties` supplies an index signature; with no members or patterns, `Record<string, never>` (an empty interface would let a primitive through) |
|
|
234
238
|
| `items` | `Array<T>` |
|
|
235
239
|
| `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
240
|
| `$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 |
|
|
@@ -245,7 +249,10 @@ equivalent is the `reserved` option of `compileEmitModel`.
|
|
|
245
249
|
A JSON Schema object accepts members it never declared. That is the default,
|
|
246
250
|
and it is easy to forget when reading a schema that lists four properties and
|
|
247
251
|
looks like a struct. So an interface generated from one carries an index
|
|
248
|
-
signature
|
|
252
|
+
signature. `additionalProperties: false` removes it unless `patternProperties`
|
|
253
|
+
still permits matching names. When patterns and typed additional properties
|
|
254
|
+
coexist, the index includes both value types; TypeScript cannot restrict each
|
|
255
|
+
type to the names that JSON Schema assigns it.
|
|
249
256
|
|
|
250
257
|
This costs something real: with an index signature TypeScript stops flagging a
|
|
251
258
|
misspelled property, because the misspelling is a legal member. The trade is
|
|
@@ -292,6 +299,55 @@ theirs. If your schemas are documents — published, shared with other
|
|
|
292
299
|
languages, or fed to an LLM's structured-output mode — this is the one that
|
|
293
300
|
fits.
|
|
294
301
|
|
|
302
|
+
## Exports
|
|
303
|
+
|
|
304
|
+
Every subpath a consumer can import, derived from the manifest by
|
|
305
|
+
`npm run docs:derive` (`npm run docs:check` fails when the two drift):
|
|
306
|
+
|
|
307
|
+
<!--fact:exports.emit-->
|
|
308
|
+
| Import | Kind | Declarations |
|
|
309
|
+
|---|---|---|
|
|
310
|
+
| `@jarenjs/emit` | JavaScript | declared |
|
|
311
|
+
| `@jarenjs/emit/model` | JavaScript | declared |
|
|
312
|
+
| `@jarenjs/emit/typescript` | JavaScript | declared |
|
|
313
|
+
| `@jarenjs/emit/markdown` | JavaScript | declared |
|
|
314
|
+
| `@jarenjs/emit/schemas/jaren-emit-model.schema.json` | schema | — |
|
|
315
|
+
| `@jarenjs/emit/package.json` | metadata | — |
|
|
316
|
+
<!--/fact-->
|
|
317
|
+
|
|
318
|
+
What each family is for, and what it costs:
|
|
319
|
+
|
|
320
|
+
- **The root** re-exports the three routes below, so `import { emitTypeScript }
|
|
321
|
+
from '@jarenjs/emit'` is the whole package. Reach for a subpath when a
|
|
322
|
+
bundle should carry one target only.
|
|
323
|
+
- **`./model`** is the published intermediate stage
|
|
324
|
+
([EMIT-FORMAT](docs/EMIT-FORMAT.md)): `compileEmitModel(schema, options)`
|
|
325
|
+
turns a JSON Schema into a `jaren-emit` model document — every type
|
|
326
|
+
resolved, every union normalized, every constraint the target cannot
|
|
327
|
+
express listed — and `EMIT_MODEL_VERSION` names the document version it
|
|
328
|
+
writes. It is the route a *new* target starts from: a stylesheet over
|
|
329
|
+
the model is a generator, and the model is what the TypeScript and
|
|
330
|
+
Markdown routes both read. It imports the validator's schema
|
|
331
|
+
normalizer, so it is the heaviest of the three by itself.
|
|
332
|
+
- **`./typescript`**: `emitTypeScript(schema, options)` is schema → model →
|
|
333
|
+
declaration text in one call; `renderTypeScript(model, options)` renders a
|
|
334
|
+
model you already compiled (the CLI compiles once and renders per target
|
|
335
|
+
this way); `TYPESCRIPT_STYLESHEET` is the JTLT document doing the
|
|
336
|
+
rendering, exported so a host can extend it rather than fork it.
|
|
337
|
+
- **`./markdown`**: `emitMarkdown`, `renderMarkdown` and
|
|
338
|
+
`MARKDOWN_STYLESHEET` — the same three shapes, emitting a reference
|
|
339
|
+
document instead of a declaration file.
|
|
340
|
+
- **`./schemas/*`** is the JSON Schema of the model document itself
|
|
341
|
+
(`jaren-emit-model.schema.json`): validate a model you stored or wrote
|
|
342
|
+
by hand against it before rendering, or hand it to an editor for
|
|
343
|
+
completion. It is JSON, not code — no declaration rides with it.
|
|
344
|
+
- **`./package.json`** is metadata: the version a tool may read, nothing
|
|
345
|
+
a program imports.
|
|
346
|
+
|
|
347
|
+
The `jaren-emit` CLI (`bin`) is not an export — it is the command the
|
|
348
|
+
package installs, and it uses `./typescript` and `./markdown` exactly as a
|
|
349
|
+
program would.
|
|
350
|
+
|
|
295
351
|
## Development
|
|
296
352
|
|
|
297
353
|
Tests live in `test/emit/` at the repository root (`npm run test:emit`).
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/emit",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.67.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
"prepack": "npm run build:types"
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
|
-
"@jarenjs/core": "^0.
|
|
72
|
-
"@jarenjs/json": "^0.
|
|
73
|
-
"@jarenjs/validate": "^0.
|
|
71
|
+
"@jarenjs/core": "^0.67.0",
|
|
72
|
+
"@jarenjs/json": "^0.67.0",
|
|
73
|
+
"@jarenjs/validate": "^0.67.0"
|
|
74
74
|
}
|
|
75
75
|
}
|
package/src/cli.js
CHANGED
|
@@ -143,6 +143,23 @@ function main() {
|
|
|
143
143
|
return;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
// Validate the whole output plan before writing: different schema names
|
|
147
|
+
// can collapse onto the same PascalCase filename.
|
|
148
|
+
if (options.bundle === null) {
|
|
149
|
+
const destinations = new Map();
|
|
150
|
+
for (const file of files) {
|
|
151
|
+
const out = path.join(options.out, nameFromFile(file) + target.extension);
|
|
152
|
+
const destination = process.platform === 'win32' ? out.toLowerCase() : out;
|
|
153
|
+
const previous = destinations.get(destination);
|
|
154
|
+
if (previous !== undefined) {
|
|
155
|
+
console.error(`output collision: '${previous}' and '${file}' both write '${out}'`);
|
|
156
|
+
process.exit(2);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
destinations.set(destination, file);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
146
163
|
// Only pass normalize options when at least one is on: a null here is what
|
|
147
164
|
// tells the model to emit a single declaration per type rather than a pair.
|
|
148
165
|
const normalizeOptions = options.defaults || options.coerce
|
package/src/model.js
CHANGED
|
@@ -851,11 +851,15 @@ function objectShape(node, ctx, hint) {
|
|
|
851
851
|
// unsound type for a consumer who prefers excess-property checking.
|
|
852
852
|
index = T.unknown();
|
|
853
853
|
}
|
|
854
|
-
|
|
854
|
+
// Matching names are governed by their pattern schemas, not by
|
|
855
|
+
// additionalProperties. A typed additional index must therefore include
|
|
856
|
+
// those values too; an unknown index already covers every pattern.
|
|
857
|
+
if (index?.kind !== 'unknown' && isJsonObject(node.patternProperties)) {
|
|
855
858
|
const patterns = Object.getOwnPropertyNames(node.patternProperties);
|
|
856
859
|
if (patterns.length > 0) {
|
|
857
|
-
|
|
858
|
-
typeOf(node.patternProperties[p], ctx, `${hint}Pattern${i + 1}`))
|
|
860
|
+
const patternTypes = patterns.map((p, i) =>
|
|
861
|
+
typeOf(node.patternProperties[p], ctx, `${hint}Pattern${i + 1}`));
|
|
862
|
+
index = unionOf(index === null ? patternTypes : [index, ...patternTypes]);
|
|
859
863
|
}
|
|
860
864
|
}
|
|
861
865
|
|