@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 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, with no index signature; with no members at all, `Record<string, never>` (an empty interface would let a primitive through) |
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, and only `additionalProperties: false` removes it.
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.56.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.56.0",
72
- "@jarenjs/json": "^0.56.0",
73
- "@jarenjs/validate": "^0.56.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
- else if (isJsonObject(node.patternProperties)) {
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
- index = unionOf(patterns.map((p, i) =>
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