@jarenjs/emit 0.67.0 → 0.72.2
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 +15 -7
- package/dist/types/model.d.ts +4 -0
- package/docs/EMIT-FORMAT.md +15 -2
- package/package.json +4 -4
- package/schemas/jaren-emit-model.schema.json +1 -0
- package/src/cli.js +21 -7
- package/src/markdown.js +8 -0
- package/src/model.js +73 -35
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ emitTypeScript({
|
|
|
19
19
|
role: { enum: ['admin', 'user'] },
|
|
20
20
|
},
|
|
21
21
|
required: ['id'],
|
|
22
|
-
}, { name: 'User' });
|
|
22
|
+
}, { name: 'User', banner: false });
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
```typescript
|
|
@@ -32,6 +32,7 @@ export interface User {
|
|
|
32
32
|
*/
|
|
33
33
|
id: string;
|
|
34
34
|
role?: "admin" | "user";
|
|
35
|
+
[key: string]: unknown;
|
|
35
36
|
}
|
|
36
37
|
```
|
|
37
38
|
|
|
@@ -80,10 +81,10 @@ or a type so wide it certifies anything.
|
|
|
80
81
|
|
|
81
82
|
Because Jaren owns **both sides**, that can be tested rather than trusted.
|
|
82
83
|
The suite in `test/emit/agreement.test.js` takes a corpus of schemas plus
|
|
83
|
-
instances,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
hold:
|
|
84
|
+
instances, checks the generated declaration fixture, and runs the compiled
|
|
85
|
+
validator over those same instances. The companion `npm run test:types` gate
|
|
86
|
+
runs `tsc` over assignments to that fixture. Together the gates require three
|
|
87
|
+
relationships to hold:
|
|
87
88
|
|
|
88
89
|
| Instance | Validator | Generated type | What a failure would mean |
|
|
89
90
|
| --- | --- | --- | --- |
|
|
@@ -106,7 +107,10 @@ dropped constraint into the generated file:
|
|
|
106
107
|
The type says `string`. The comment says the schema also demands a minimum
|
|
107
108
|
length that the type does not enforce. A reader learns both. **Widening
|
|
108
109
|
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
|
+
generated type misleads the person reading it. Anonymous constrained nodes gain
|
|
111
|
+
stable named declarations so their constraints have a documentation location;
|
|
112
|
+
objects with documented members are named as well. Markdown emits those member
|
|
113
|
+
notes after the declaration's table.
|
|
110
114
|
|
|
111
115
|
The rig is checked against itself, too: deliberately breaking the generator so
|
|
112
116
|
it emits `unknown` everywhere makes the "not wider than the schema" assertion
|
|
@@ -146,7 +150,7 @@ handler signature takes `ConfigInput`, the rest of the program handles
|
|
|
146
150
|
`Config`, and the normalizer is the transition between them.
|
|
147
151
|
|
|
148
152
|
**A twin appears only where the type actually differs.** The generator works
|
|
149
|
-
that out
|
|
153
|
+
that out across the reference graph, including cycles, so a schema with one defaulted field does not double every
|
|
150
154
|
declaration; everything unaffected keeps a single shared name on both sides.
|
|
151
155
|
|
|
152
156
|
**Only two normalizations produce a difference.** `useDefaults` moves a member
|
|
@@ -212,6 +216,10 @@ jaren-emit --schema <file|dir> --out <dir> [options]
|
|
|
212
216
|
--check write nothing; exit 1 if any output is out of date
|
|
213
217
|
```
|
|
214
218
|
|
|
219
|
+
Options requiring a value reject a missing value before writing. Input and
|
|
220
|
+
filesystem errors are concise diagnostics with exit status 2; output drift under
|
|
221
|
+
`--check` uses status 1.
|
|
222
|
+
|
|
215
223
|
`--check` is the CI guard: it fails the build when a schema changed and the
|
|
216
224
|
generated types did not, which is the failure mode that makes generated code
|
|
217
225
|
untrustworthy in the first place.
|
package/dist/types/model.d.ts
CHANGED
|
@@ -68,6 +68,10 @@ export type EmitMember = {
|
|
|
68
68
|
default?: any;
|
|
69
69
|
constraints: EmitConstraint[];
|
|
70
70
|
doc: string[];
|
|
71
|
+
/**
|
|
72
|
+
* - Selected extension annotations carried from the property schema
|
|
73
|
+
*/
|
|
74
|
+
extensions?: Record<string, unknown>;
|
|
71
75
|
};
|
|
72
76
|
export type EmitDeclaration = {
|
|
73
77
|
kind: 'declaration';
|
package/docs/EMIT-FORMAT.md
CHANGED
|
@@ -74,7 +74,9 @@ generated artifact that churns between runs cannot be reviewed in a diff.
|
|
|
74
74
|
`name` is the property name **verbatim** — it is data, not an identifier, and
|
|
75
75
|
an emitter quotes it if its target language requires that. `default` is
|
|
76
76
|
present only when the source schema declares one. Members appear in the order
|
|
77
|
-
the schema declared them.
|
|
77
|
+
the schema declared them. Names present only in `required` are appended in that
|
|
78
|
+
array's order with type `unknown`; the `properties` map is not required to
|
|
79
|
+
establish member presence.
|
|
78
80
|
|
|
79
81
|
### 4.1 The extension seam
|
|
80
82
|
|
|
@@ -194,7 +196,7 @@ boundary can name them separately.
|
|
|
194
196
|
its counterpart in `variantOf`.
|
|
195
197
|
|
|
196
198
|
**A twin is emitted only when the type actually differs.** A producer computes
|
|
197
|
-
that
|
|
199
|
+
that across reference cycles — a type differs if anything it contains differs — so a schema
|
|
198
200
|
with one defaulted field does not double every declaration in the document.
|
|
199
201
|
Everything unaffected is referenced by its single shared name from both sides.
|
|
200
202
|
|
|
@@ -229,6 +231,17 @@ Three consequences worth stating because each was once wrong:
|
|
|
229
231
|
- coercion widens only nodes with a **single string-valued `type`**, because
|
|
230
232
|
that is the only place `coerceToType` runs.
|
|
231
233
|
|
|
234
|
+
Anonymous nodes carrying unrepresentable constraints are promoted to stable,
|
|
235
|
+
named declarations so their documentation survives rendering. An anonymous
|
|
236
|
+
object with documented members is named for the same reason. This can add
|
|
237
|
+
helper declarations without changing the structural type. The Markdown emitter
|
|
238
|
+
prints member documentation after each member table.
|
|
239
|
+
|
|
240
|
+
`nullable: true` extends an explicitly declared type with `null`, matching the
|
|
241
|
+
validator's extension. Literal `const`/`enum` values still satisfy the declared
|
|
242
|
+
type and compose with sibling applicators; an incompatible literal cannot erase
|
|
243
|
+
a type constraint.
|
|
244
|
+
|
|
232
245
|
## 8. Determinism
|
|
233
246
|
|
|
234
247
|
Two compilations of the same input MUST produce byte-identical models.
|
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.72.2",
|
|
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.72.2",
|
|
72
|
+
"@jarenjs/json": "^0.72.2",
|
|
73
|
+
"@jarenjs/validate": "^0.72.2"
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"type": { "$ref": "#/$defs/type" },
|
|
65
65
|
"required": { "type": "boolean" },
|
|
66
66
|
"default": { "description": "The schema default, when it declares one." },
|
|
67
|
+
"extensions": { "type": "object", "description": "Selected property extension annotations, carried verbatim." },
|
|
67
68
|
"constraints": { "$ref": "#/$defs/constraints" },
|
|
68
69
|
"doc": { "$ref": "#/$defs/doc" }
|
|
69
70
|
}
|
package/src/cli.js
CHANGED
|
@@ -51,16 +51,24 @@ function parseArgs(argv) {
|
|
|
51
51
|
defaults: false, coerce: false, suffix: 'Input',
|
|
52
52
|
};
|
|
53
53
|
for (let i = 2; i < argv.length; i++) {
|
|
54
|
+
const value = () => {
|
|
55
|
+
const option = argv[i];
|
|
56
|
+
const next = argv[i + 1];
|
|
57
|
+
if (next === undefined || next.startsWith('-'))
|
|
58
|
+
throw new Error(`${option} requires a value`);
|
|
59
|
+
i++;
|
|
60
|
+
return next;
|
|
61
|
+
};
|
|
54
62
|
switch (argv[i]) {
|
|
55
|
-
case '--schema': options.schema =
|
|
56
|
-
case '--out': options.out =
|
|
57
|
-
case '--target': options.target =
|
|
58
|
-
case '--name': options.name =
|
|
59
|
-
case '--bundle': options.bundle =
|
|
63
|
+
case '--schema': options.schema = value(); break;
|
|
64
|
+
case '--out': options.out = value(); break;
|
|
65
|
+
case '--target': options.target = value(); break;
|
|
66
|
+
case '--name': options.name = value(); break;
|
|
67
|
+
case '--bundle': options.bundle = value(); break;
|
|
60
68
|
case '--check': options.check = true; break;
|
|
61
69
|
case '--defaults': options.defaults = true; break;
|
|
62
70
|
case '--coerce': options.coerce = true; break;
|
|
63
|
-
case '--suffix': options.suffix =
|
|
71
|
+
case '--suffix': options.suffix = value(); break;
|
|
64
72
|
case '--help': case '-h': options.help = true; break;
|
|
65
73
|
default:
|
|
66
74
|
throw new Error(`unknown option: ${argv[i]}`);
|
|
@@ -203,6 +211,12 @@ function main() {
|
|
|
203
211
|
}
|
|
204
212
|
}
|
|
205
213
|
|
|
206
|
-
|
|
214
|
+
try {
|
|
215
|
+
main();
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
219
|
+
process.exitCode = 2;
|
|
220
|
+
}
|
|
207
221
|
|
|
208
222
|
//#endregion
|
package/src/markdown.js
CHANGED
|
@@ -43,6 +43,7 @@ export const MARKDOWN_STYLESHEET = {
|
|
|
43
43
|
'| Member | Type | Required |\n| --- | --- | --- |\n',
|
|
44
44
|
[{ $apply: ['$.members[*]', 'row'] }],
|
|
45
45
|
'\n',
|
|
46
|
+
[{ $apply: ['$.members[?@.doc[0]]', 'memberdoc'] }],
|
|
46
47
|
],
|
|
47
48
|
},
|
|
48
49
|
{
|
|
@@ -55,6 +56,13 @@ export const MARKDOWN_STYLESHEET = {
|
|
|
55
56
|
[{ $apply: ['$.required', 'yesno'] }], ' |\n'],
|
|
56
57
|
},
|
|
57
58
|
{ mode: 'yesno', body: [{ $if: ['$', 'yes', 'no'] }] },
|
|
59
|
+
{
|
|
60
|
+
mode: 'memberdoc',
|
|
61
|
+
body: [
|
|
62
|
+
'**`', { $raw: '$.name' }, '`**\n\n',
|
|
63
|
+
[{ $apply: ['$.doc[*]', 'docline'] }],
|
|
64
|
+
],
|
|
65
|
+
},
|
|
58
66
|
|
|
59
67
|
// --- the same type vocabulary, printed as prose ---------------------
|
|
60
68
|
{ match: isKind('primitive'), mode: 'type', body: [{ $raw: '$.primitive' }] },
|
package/src/model.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
// a private intermediate: a third-party stylesheet targets it, and `emit`'s
|
|
29
29
|
// own TypeScript and Markdown emitters have no privileged access.
|
|
30
30
|
|
|
31
|
-
import { isJsonObject } from '@jarenjs/core/object';
|
|
31
|
+
import { isJsonObject, setObjectMember } from '@jarenjs/core/object';
|
|
32
32
|
import {
|
|
33
33
|
NUMERIC_CONSTRAINTS, STRING_CONSTRAINTS,
|
|
34
34
|
ARRAY_CONSTRAINTS, OBJECT_CONSTRAINTS,
|
|
@@ -79,6 +79,7 @@ export const EMIT_MODEL_VERSION = '0.1';
|
|
|
79
79
|
* @property {any} [default] - The schema default, when it declares one
|
|
80
80
|
* @property {EmitConstraint[]} constraints
|
|
81
81
|
* @property {string[]} doc
|
|
82
|
+
* @property {Record<string, unknown>} [extensions] - Selected extension annotations carried from the property schema
|
|
82
83
|
*/
|
|
83
84
|
|
|
84
85
|
/**
|
|
@@ -337,7 +338,7 @@ function unionOf(types) {
|
|
|
337
338
|
* analysis answering differently from the runtime is precisely the defect the
|
|
338
339
|
* variants exist to rule out.
|
|
339
340
|
*
|
|
340
|
-
*
|
|
341
|
+
* Follows reference reachability, because a type differs if anything it
|
|
341
342
|
* contains differs. A node reached while it is still being analyzed is a
|
|
342
343
|
* cycle, and a cycle alone introduces no difference, so it answers `false`.
|
|
343
344
|
* @param {any} node - The schema node
|
|
@@ -411,7 +412,10 @@ function normalizationChangesType(node, ctx) {
|
|
|
411
412
|
}
|
|
412
413
|
|
|
413
414
|
ctx.analyzing.delete(node);
|
|
414
|
-
|
|
415
|
+
// A false answer reached through a backedge may still depend on a node
|
|
416
|
+
// whose coercible member has not been visited. Only a completed outer
|
|
417
|
+
// traversal proves false; a discovered difference is final immediately.
|
|
418
|
+
if (differs || ctx.analyzing.size === 0) memo.set(node, differs);
|
|
415
419
|
return differs;
|
|
416
420
|
}
|
|
417
421
|
|
|
@@ -509,9 +513,10 @@ function reserveName(ctx, preferred) {
|
|
|
509
513
|
* @param {any} node - The schema node
|
|
510
514
|
* @param {object} ctx - The compile context
|
|
511
515
|
* @param {string} hint - A name to use if this node has to be hoisted
|
|
516
|
+
* @param {boolean} [documented=false] - The owning member already carries this node's own documentation
|
|
512
517
|
* @returns {object} A type ref
|
|
513
518
|
*/
|
|
514
|
-
function typeOf(node, ctx, hint) {
|
|
519
|
+
function typeOf(node, ctx, hint, documented = false) {
|
|
515
520
|
if (node === true || node === undefined) return T.unknown();
|
|
516
521
|
if (node === false) return T.never();
|
|
517
522
|
if (!isJsonObject(node)) return T.unknown();
|
|
@@ -529,6 +534,15 @@ function typeOf(node, ctx, hint) {
|
|
|
529
534
|
return T.ref(name);
|
|
530
535
|
}
|
|
531
536
|
|
|
537
|
+
// Anonymous constraints need a declaration's documentation slot. Object
|
|
538
|
+
// member documentation also needs the declaration printer: an inline
|
|
539
|
+
// object type has no member comment surface in either bundled emitter.
|
|
540
|
+
const documentedMembers = isJsonObject(node.properties)
|
|
541
|
+
&& Object.values(node.properties).some((member) => isJsonObject(member)
|
|
542
|
+
&& (droppedConstraints(member).length > 0
|
|
543
|
+
|| (typeof member.description === 'string' && member.description.length > 0)));
|
|
544
|
+
if ((!documented && droppedConstraints(node).length > 0) || documentedMembers)
|
|
545
|
+
return declare(node, ctx, hint);
|
|
532
546
|
return shapeOf(node, ctx, hint);
|
|
533
547
|
}
|
|
534
548
|
|
|
@@ -671,6 +685,24 @@ function coercionCanProduce(source, type, value) {
|
|
|
671
685
|
}
|
|
672
686
|
}
|
|
673
687
|
|
|
688
|
+
/** Whether a literal satisfies the explicitly declared JSON type. */
|
|
689
|
+
function literalMatchesType(value, node) {
|
|
690
|
+
if (node.type === undefined) return true;
|
|
691
|
+
if (value === null && node.nullable === true) return true;
|
|
692
|
+
const types = Array.isArray(node.type) ? node.type : [node.type];
|
|
693
|
+
return types.some((type) => {
|
|
694
|
+
switch (type) {
|
|
695
|
+
case 'null': return value === null;
|
|
696
|
+
case 'object': return isJsonObject(value);
|
|
697
|
+
case 'array': return Array.isArray(value);
|
|
698
|
+
case 'integer': return typeof value === 'number' && Number.isInteger(value);
|
|
699
|
+
case 'number': return typeof value === 'number' && Number.isFinite(value);
|
|
700
|
+
case 'string': case 'boolean': return typeof value === type;
|
|
701
|
+
default: return true;
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
674
706
|
/** The structural shape of a schema node. */
|
|
675
707
|
function shapeOf(node, ctx, hint) {
|
|
676
708
|
const parts = [];
|
|
@@ -689,41 +721,38 @@ function shapeOf(node, ctx, hint) {
|
|
|
689
721
|
parts.push(declare(target.node, ctx, target.name ?? hint));
|
|
690
722
|
}
|
|
691
723
|
|
|
692
|
-
//
|
|
693
|
-
//
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
if (
|
|
697
|
-
const values = node.const !== undefined ? [node.const] : node.enum
|
|
698
|
-
|
|
724
|
+
// Literals still satisfy the node's type and every sibling applicator.
|
|
725
|
+
// Filter the type here instead of emitting redundant `"a" & string` arms.
|
|
726
|
+
const hasLiterals = node.const !== undefined || Array.isArray(node.enum);
|
|
727
|
+
let containerLiteral = false;
|
|
728
|
+
if (hasLiterals) {
|
|
729
|
+
const values = (node.const !== undefined ? [node.const] : node.enum)
|
|
730
|
+
.filter((value) => literalMatchesType(value, node));
|
|
731
|
+
const literals = values.map((value) => T.literal(value));
|
|
732
|
+
containerLiteral = values.some((value) => isJsonObject(value) || Array.isArray(value));
|
|
699
733
|
for (const source of coercionSources(node, values, ctx))
|
|
700
734
|
literals.push(T.primitive(PRIMITIVES[source]));
|
|
701
735
|
parts.push(unionOf(literals));
|
|
702
736
|
}
|
|
703
|
-
else {
|
|
704
|
-
// allOf is intersection.
|
|
705
|
-
if (Array.isArray(node.allOf) && node.allOf.length > 0) {
|
|
706
|
-
const branches = node.allOf.map((b, i) => typeOf(b, ctx, `${hint}Part${i + 1}`));
|
|
707
|
-
const usable = branches.filter((t) => t.kind !== 'unknown');
|
|
708
|
-
if (usable.length === 1) parts.push(usable[0]);
|
|
709
|
-
else if (usable.length > 1) parts.push(T.intersection(usable));
|
|
710
|
-
}
|
|
711
737
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
738
|
+
if (Array.isArray(node.allOf) && node.allOf.length > 0) {
|
|
739
|
+
const branches = node.allOf.map((branch, i) => typeOf(branch, ctx, `${hint}Part${i + 1}`));
|
|
740
|
+
const usable = branches.filter((type) => type.kind !== 'unknown');
|
|
741
|
+
if (usable.length === 1) parts.push(usable[0]);
|
|
742
|
+
else if (usable.length > 1) parts.push(T.intersection(usable));
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// The normalizer does not descend union branches, so both variants use
|
|
746
|
+
// the plain schema reading of those branches.
|
|
747
|
+
for (const key of ['anyOf', 'oneOf']) {
|
|
748
|
+
if (Array.isArray(node[key]) && node[key].length > 0) {
|
|
749
|
+
const branchCtx = ctx.normalize === null ? ctx : (ctx.plain ??= createPlainContext(ctx));
|
|
750
|
+
parts.push(unionOf(node[key].map((branch, i) =>
|
|
751
|
+
typeOf(branch, branchCtx, `${hint}${toIdentifier(key)}${i + 1}`))));
|
|
725
752
|
}
|
|
753
|
+
}
|
|
726
754
|
|
|
755
|
+
if (!hasLiterals || containerLiteral) {
|
|
727
756
|
const own = ownShape(node, ctx, hint);
|
|
728
757
|
if (own !== null) parts.push(own);
|
|
729
758
|
}
|
|
@@ -742,7 +771,8 @@ function ownShape(node, ctx, hint) {
|
|
|
742
771
|
|
|
743
772
|
const hasObjectKeywords = node.properties !== undefined
|
|
744
773
|
|| node.patternProperties !== undefined
|
|
745
|
-
|| node.additionalProperties !== undefined
|
|
774
|
+
|| node.additionalProperties !== undefined
|
|
775
|
+
|| Array.isArray(node.required);
|
|
746
776
|
const hasArrayKeywords = node.items !== undefined || node.prefixItems !== undefined;
|
|
747
777
|
|
|
748
778
|
// No `type`: the applicator keywords describe the container cases, but
|
|
@@ -786,6 +816,7 @@ function ownShape(node, ctx, hint) {
|
|
|
786
816
|
}
|
|
787
817
|
else alternatives.push(T.unknown());
|
|
788
818
|
}
|
|
819
|
+
if (node.nullable === true) alternatives.push(T.primitive('null'));
|
|
789
820
|
return unionOf(alternatives);
|
|
790
821
|
}
|
|
791
822
|
|
|
@@ -812,7 +843,7 @@ function objectShape(node, ctx, hint) {
|
|
|
812
843
|
const member = {
|
|
813
844
|
kind: 'member',
|
|
814
845
|
name: key,
|
|
815
|
-
type: typeOf(sub, ctx, `${hint}${toIdentifier(key)}
|
|
846
|
+
type: typeOf(sub, ctx, `${hint}${toIdentifier(key)}`, true),
|
|
816
847
|
required: defaulted
|
|
817
848
|
? ctx.variant !== 'accepted'
|
|
818
849
|
: declaredRequired,
|
|
@@ -828,7 +859,7 @@ function objectShape(node, ctx, hint) {
|
|
|
828
859
|
for (const keyword of declaredExtensions) {
|
|
829
860
|
if (subNode[keyword] === undefined) continue;
|
|
830
861
|
if (carried === null) carried = {};
|
|
831
|
-
carried
|
|
862
|
+
setObjectMember(carried, keyword, subNode[keyword]);
|
|
832
863
|
}
|
|
833
864
|
if (carried !== null) member.extensions = carried;
|
|
834
865
|
}
|
|
@@ -836,6 +867,13 @@ function objectShape(node, ctx, hint) {
|
|
|
836
867
|
}
|
|
837
868
|
}
|
|
838
869
|
|
|
870
|
+
// `required` owns presence independently of `properties`. A required
|
|
871
|
+
// undeclared member has no narrower named schema to infer here.
|
|
872
|
+
for (const key of required) {
|
|
873
|
+
if (members.some((member) => member.name === key)) continue;
|
|
874
|
+
members.push({ kind: 'member', name: key, type: T.unknown(), required: true, constraints: [], doc: [] });
|
|
875
|
+
}
|
|
876
|
+
|
|
839
877
|
// An index signature comes from additionalProperties or patternProperties.
|
|
840
878
|
// `additionalProperties: false` is the closed case and adds nothing; `true`
|
|
841
879
|
// or a schema opens the object up.
|