ata-validator 1.59.0 → 1.61.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 +107 -4
- package/bin/ata.js +84 -0
- package/build.d.ts +29 -0
- package/build.mjs +2 -0
- package/compiled.d.ts +3 -1
- package/index.browser.mjs +2 -2
- package/index.bundle.mjs +2 -2
- package/index.d.ts +9 -0
- package/index.edge.mjs +2 -2
- package/index.js +48 -0
- package/index.mjs +1 -1
- package/index.node.mjs +1 -1
- package/lib/aot-build.js +73 -6
- package/lib/aot-impl.js +28 -27
- package/lib/bun-target.js +612 -0
- package/lib/codegen-engine.js +9 -3
- package/lib/combined-runtime.js +1 -1
- package/lib/compiled.js +6 -0
- package/lib/formats.js +1 -1
- package/lib/js-compiler-errors.js +45 -10
- package/lib/js-compiler.js +92 -13
- package/lib/public-api.js +1 -0
- package/lib/standard-schema.js +145 -0
- package/lib/tier0.js +1 -1
- package/lib/validator-core.js +44 -138
- package/lib/verify.js +321 -0
- package/lib/version.js +1 -1
- package/lite.js +1 -0
- package/lite.mjs +1 -1
- package/package.json +10 -9
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// `ata build --target bun`: the schema as a TypeScript type, written beside
|
|
3
|
+
// the plain module for executables built with `bun build --compile --aot`.
|
|
4
|
+
//
|
|
5
|
+
// The type carries the shape of the schema (the JSON kind, properties and
|
|
6
|
+
// which are required, items and fixed tuples, literals, local definitions as
|
|
7
|
+
// named types) and nothing a TypeScript type cannot say. The module written
|
|
8
|
+
// with it answers every check through the plain module, so the answers and
|
|
9
|
+
// the errors are the module's; the type is for the program that imports it.
|
|
10
|
+
// `parse(text)` declares the type where it parses (`const d: T =
|
|
11
|
+
// JSON.parse(text)`), the form the compiler's documentation gives for a parse
|
|
12
|
+
// that makes objects with the layout of `T`; measured on the pull request
|
|
13
|
+
// build of 2026-10-25, that changed nothing on a ten-item order (parse 564 to
|
|
14
|
+
// 574 ns against 570 to 578 for a plain parse and the module, reading the
|
|
15
|
+
// object after it 593 to 595 against 587 to 591), so the file is a type for
|
|
16
|
+
// the program and nothing faster.
|
|
17
|
+
//
|
|
18
|
+
// With `tags: true`, the type is written with Bun's `Schema` tags
|
|
19
|
+
// (`Schema.Int`, `Schema.MinLength<n>`, `Schema.Strict`, ...) and the schema
|
|
20
|
+
// is split exactly in two: the keywords the tagged type carries, and a
|
|
21
|
+
// residual schema with every other keyword, kept inside the structure needed
|
|
22
|
+
// to reach it, for a checker that reads the tags. Bun's `validate<T>` and
|
|
23
|
+
// `JSON.parse<T>` did that in the compiler's pull request until 2026-10-25,
|
|
24
|
+
// when they left it to return as a change of their own; nothing in ata
|
|
25
|
+
// writes the tagged form until then. The split keeps the groups that see
|
|
26
|
+
// each other whole in the residual: `if`/`then`/`else`; `unevaluatedProperties`
|
|
27
|
+
// and `unevaluatedItems` with every sibling of theirs; `additionalProperties`
|
|
28
|
+
// with `patternProperties`; the `contains` family; `anyOf` whenever one
|
|
29
|
+
// branch has a residual, since a document may satisfy one branch's type and
|
|
30
|
+
// another's residual and neither branch.
|
|
31
|
+
//
|
|
32
|
+
// A type is built as six sets, one per JSON kind, each a list of alternatives
|
|
33
|
+
// of that kind. A union adds the lists; an intersection pairs them kind by
|
|
34
|
+
// kind, so `{ foo: string } & unknown[]` is never written: an object rule and
|
|
35
|
+
// an array rule meet in no value.
|
|
36
|
+
|
|
37
|
+
// Annotations: no effect on a verdict, carried by nothing here.
|
|
38
|
+
const IGNORED = new Set([
|
|
39
|
+
'$schema', '$id', '$comment', '$anchor', '$vocabulary', 'title', 'description',
|
|
40
|
+
'examples', 'default', 'deprecated', 'readOnly', 'writeOnly', '$defs', 'definitions',
|
|
41
|
+
'contentMediaType', 'contentEncoding', 'contentSchema'
|
|
42
|
+
])
|
|
43
|
+
const STRING_KEYS = ['minLength', 'maxLength', 'pattern']
|
|
44
|
+
const NUMBER_KEYS = ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf']
|
|
45
|
+
const OBJECT_KEYS = ['properties', 'required', 'additionalProperties']
|
|
46
|
+
const ARRAY_KEYS = ['items', 'prefixItems', 'minItems', 'maxItems']
|
|
47
|
+
const CONSTRAINT_KEYS = [...STRING_KEYS, ...NUMBER_KEYS, ...OBJECT_KEYS, ...ARRAY_KEYS]
|
|
48
|
+
// Keywords the type reads. Everything else at a node goes to the residual.
|
|
49
|
+
const TYPED = new Set(['type', 'enum', 'const', 'anyOf', 'allOf', '$ref', ...CONSTRAINT_KEYS])
|
|
50
|
+
// Keywords that take the whole node with them.
|
|
51
|
+
const WHOLE_NODE = new Set(['unevaluatedProperties', 'unevaluatedItems', '$dynamicRef', '$dynamicAnchor', '$recursiveRef', '$recursiveAnchor'])
|
|
52
|
+
const KINDS = ['string', 'number', 'boolean', 'null', 'object', 'array']
|
|
53
|
+
// Formats ata decides with one regular expression and nothing else: the type
|
|
54
|
+
// carries them as that expression, so Bun's check and ata's are the same
|
|
55
|
+
// test. The others (email, date-time, uri, hostname, ipv6, uuid, date, time)
|
|
56
|
+
// are hand-written passes and stay in the residual.
|
|
57
|
+
function formatPattern (name) {
|
|
58
|
+
const f = require('./formats')
|
|
59
|
+
if (name === 'ipv4') return f.IPV4.source
|
|
60
|
+
if (name === 'duration') return f.DURATION_RE ? f.DURATION_RE.source : null
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
const UNIVERSAL = { string: 'string', number: 'number', boolean: 'boolean', null: 'null', object: 'Record<string, unknown>', array: 'unknown[]' }
|
|
64
|
+
|
|
65
|
+
class Declined extends Error {
|
|
66
|
+
constructor (reason, pointer) {
|
|
67
|
+
super(`${reason} at ${pointer}`)
|
|
68
|
+
this.reason = reason
|
|
69
|
+
this.pointer = pointer
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function fail (reason, ptr) { throw new Declined(reason, ptr) }
|
|
74
|
+
|
|
75
|
+
// The six sets. `alias` is a named type standing for a whole definition; it
|
|
76
|
+
// joins a union as a name and declines an intersection, whose members it
|
|
77
|
+
// would have to be distributed over.
|
|
78
|
+
function empty () { return { string: [], number: [], boolean: [], null: [], object: [], array: [], alias: [] } }
|
|
79
|
+
function universal () { const t = empty(); for (const k of KINDS) t[k].push(UNIVERSAL[k]); return t }
|
|
80
|
+
function single (kind, alt) { const t = empty(); t[kind].push(alt); return t }
|
|
81
|
+
function union (a, b) { const t = empty(); for (const k of [...KINDS, 'alias']) t[k] = dedupe([...a[k], ...b[k]]); return t }
|
|
82
|
+
function dedupe (xs) { return xs.filter((x, i) => xs.indexOf(x) === i) }
|
|
83
|
+
function isUniversal (t) { return t.alias.length === 0 && KINDS.every((k) => t[k].length === 1 && t[k][0] === UNIVERSAL[k]) }
|
|
84
|
+
|
|
85
|
+
// Intersection, or null where the two cannot be written as one type; the
|
|
86
|
+
// caller then keeps one side in the residual.
|
|
87
|
+
function intersect (a, b) {
|
|
88
|
+
if (a.alias.length || b.alias.length) return null
|
|
89
|
+
const t = empty()
|
|
90
|
+
for (const k of KINDS) {
|
|
91
|
+
for (const x of a[k]) {
|
|
92
|
+
for (const y of b[k]) {
|
|
93
|
+
const z = meet(k, x, y)
|
|
94
|
+
if (z === undefined) return null
|
|
95
|
+
if (z !== null) t[k].push(z)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
t[k] = dedupe(t[k])
|
|
99
|
+
}
|
|
100
|
+
return t
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Two alternatives of one kind: the alternative for both, null when no value
|
|
104
|
+
// satisfies both, undefined when the pair cannot be written.
|
|
105
|
+
function meet (kind, x, y) {
|
|
106
|
+
if (x === UNIVERSAL[kind]) return y
|
|
107
|
+
if (y === UNIVERSAL[kind]) return x
|
|
108
|
+
if (x === y) return x
|
|
109
|
+
const xl = isLiteral(x), yl = isLiteral(y)
|
|
110
|
+
if (xl && yl) return null
|
|
111
|
+
if (xl || yl) return undefined
|
|
112
|
+
switch (kind) {
|
|
113
|
+
case 'string': return `${x} & ${y.replace(/^string & /, '')}`
|
|
114
|
+
case 'number': return `${x} & ${y.replace(/^number & /, '')}`
|
|
115
|
+
case 'object':
|
|
116
|
+
// Strict on one member of an intersection would have to see the
|
|
117
|
+
// others' properties; whether Bun does is not pinned down.
|
|
118
|
+
if (/Schema\.Strict\b/.test(x) || /Schema\.Strict\b/.test(y)) return undefined
|
|
119
|
+
return `${x} & ${y}`
|
|
120
|
+
default: return undefined
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Whether an element type needs parentheses before `[]`: a union, an
|
|
125
|
+
// intersection or an `&` tag at its top level. An object literal, a tuple, a
|
|
126
|
+
// name or a literal does not.
|
|
127
|
+
function needsParens (t) {
|
|
128
|
+
let depth = 0
|
|
129
|
+
let q = null
|
|
130
|
+
for (let i = 0; i < t.length; i++) {
|
|
131
|
+
const c = t[i]
|
|
132
|
+
if (q) { if (c === '\\') i++; else if (c === q) q = null; continue }
|
|
133
|
+
if (c === '"' || c === "'") q = c
|
|
134
|
+
else if (c === '{' || c === '[' || c === '<' || c === '(') depth++
|
|
135
|
+
else if (c === '}' || c === ']' || c === '>' || c === ')') depth--
|
|
136
|
+
else if (depth === 0 && (c === '|' || c === '&')) return true
|
|
137
|
+
}
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isLiteral (alt) { return /^(".*"|-?[0-9.eE+-]+|true|false|null)$/.test(alt) }
|
|
142
|
+
|
|
143
|
+
function render (t) {
|
|
144
|
+
const alts = [...KINDS.flatMap((k) => t[k]), ...t.alias]
|
|
145
|
+
if (alts.length === 0) return 'never'
|
|
146
|
+
if (isUniversal(t)) return 'unknown'
|
|
147
|
+
if (alts.length === 1) return alts[0]
|
|
148
|
+
return alts.map((a) => /[&|]/.test(a) ? `(${a})` : a).join(' | ')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function literalOf (v) {
|
|
152
|
+
if (typeof v === 'string') return single('string', JSON.stringify(v))
|
|
153
|
+
if (typeof v === 'number' && Number.isFinite(v)) return single('number', String(v))
|
|
154
|
+
if (typeof v === 'boolean') return single('boolean', String(v))
|
|
155
|
+
if (v === null) return single('null', 'null')
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function finite (v) { return typeof v === 'number' && Number.isFinite(v) }
|
|
160
|
+
|
|
161
|
+
// Bun's Pattern runs the expression without the unicode flag; JSON Schema's
|
|
162
|
+
// patterns are read with it. The two agree on what they accept unless an atom
|
|
163
|
+
// that can match half of a surrogate pair is counted: `^.{2}$` accepts one
|
|
164
|
+
// astral character without the flag and refuses it with it. The expressions
|
|
165
|
+
// carried are the ones where no such atom is counted: ASCII only, no escapes
|
|
166
|
+
// whose meaning depends on the flag, and `.`, a negated class, `\W`, `\S`
|
|
167
|
+
// and `\D` only under a greedy `+` or `*`. Returns null when the pattern is
|
|
168
|
+
// carried, else what rules it out.
|
|
169
|
+
function patternDiffers (p) {
|
|
170
|
+
if (!/^[\x20-\x7e]*$/.test(p)) return 'with a character outside ASCII'
|
|
171
|
+
for (let i = 0; i < p.length; i++) {
|
|
172
|
+
const c = p[i]
|
|
173
|
+
if (c === '\\') {
|
|
174
|
+
const n = p[i + 1]
|
|
175
|
+
if (n === undefined) return 'ending in a backslash'
|
|
176
|
+
if ('pPuxbBk'.includes(n)) return `with \\${n}`
|
|
177
|
+
if ('WSD'.includes(n) && !greedyUnbounded(p, i + 2)) return `with \\${n} outside a greedy + or *`
|
|
178
|
+
i++
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
if (c === '.') { if (!greedyUnbounded(p, i + 1)) return 'with . outside a greedy + or *'; continue }
|
|
182
|
+
if (c === '[') {
|
|
183
|
+
const negated = p[i + 1] === '^'
|
|
184
|
+
let j = i + 1 + (negated ? 1 : 0)
|
|
185
|
+
if (p[j] === ']') j++
|
|
186
|
+
for (; j < p.length && p[j] !== ']'; j++) {
|
|
187
|
+
if (p[j] === '\\') {
|
|
188
|
+
const n = p[j + 1]
|
|
189
|
+
if ('pPuxWSDkbB'.includes(n || '')) return `with \\${n} in a class`
|
|
190
|
+
j++
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (j >= p.length) return 'with an unclosed class'
|
|
194
|
+
if (negated && !greedyUnbounded(p, j + 1)) return 'with a negated class outside a greedy + or *'
|
|
195
|
+
i = j
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return null
|
|
199
|
+
}
|
|
200
|
+
function greedyUnbounded (p, i) { return (p[i] === '+' || p[i] === '*') && p[i + 1] !== '?' }
|
|
201
|
+
|
|
202
|
+
// Names a definition must not take: they would shadow what the generated
|
|
203
|
+
// file refers to.
|
|
204
|
+
const RESERVED = new Set(['Schema', 'Record', 'Array', 'ReadonlyArray', 'Partial', 'Readonly', 'Pick', 'Omit', 'Required', 'Promise', 'Date', 'Object', 'String', 'Number', 'Boolean', 'Function', 'Symbol', 'JSON', 'Bun', 'ReturnType'])
|
|
205
|
+
|
|
206
|
+
// `name` must be an identifier TypeScript accepts.
|
|
207
|
+
function typeName (key, taken) {
|
|
208
|
+
let base = key.replace(/[^A-Za-z0-9_$]/g, '_').replace(/^([0-9])/, '_$1') || 'Def'
|
|
209
|
+
base = base.replace(/^([a-z])/, (m) => m.toUpperCase())
|
|
210
|
+
if (RESERVED.has(base)) base += '_'
|
|
211
|
+
let name = base
|
|
212
|
+
for (let i = 2; taken.has(name); i++) name = base + i
|
|
213
|
+
taken.add(name)
|
|
214
|
+
return name
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isSchema (s) { return s === true || s === false || (typeof s === 'object' && s !== null && !Array.isArray(s)) }
|
|
218
|
+
|
|
219
|
+
// The type and the residual for `schema`. Returns { name, type, aliases,
|
|
220
|
+
// residual, carried }: `type` the source of the root type, `aliases` the
|
|
221
|
+
// [name, source] definitions it refers to, `residual` the schema of what the
|
|
222
|
+
// type does not check (null when the type checks everything), `carried` the
|
|
223
|
+
// keywords the type took, by pointer. `opts.aliasPrefix` goes in front of
|
|
224
|
+
// every definition's name, for a file that holds several schemas. Throws
|
|
225
|
+
// Declined for a schema the split cannot be made for.
|
|
226
|
+
function schemaToBunType (root, opts = {}) {
|
|
227
|
+
const taken = new Set()
|
|
228
|
+
const name = typeName(opts.name || 'Data', taken)
|
|
229
|
+
// Whether formats are asserted, as a Validator does by default; a build
|
|
230
|
+
// that treats them as annotations (the official suite's reading) keeps
|
|
231
|
+
// every format out of the type.
|
|
232
|
+
const assertFormat = opts.assertFormat !== false
|
|
233
|
+
// Whether the type carries rules as Bun's `Schema` tags, with the residual
|
|
234
|
+
// split. Without them the type says the shape only and `residual` is null:
|
|
235
|
+
// the plain module checks everything.
|
|
236
|
+
const tags = opts.tags === true
|
|
237
|
+
function tag (t, s, ptr) { if (!tags) return t; carried.push(ptr); return `${t} & Schema.${s}` }
|
|
238
|
+
const aliases = []
|
|
239
|
+
const aliasByPointer = new Map()
|
|
240
|
+
const defResiduals = new Map() // pointer -> residual schema or null
|
|
241
|
+
const carried = []
|
|
242
|
+
let depth = 0
|
|
243
|
+
// Whether a definition still being built, or the root, has a residual is
|
|
244
|
+
// not known while a reference to it is met. The reference is first taken as
|
|
245
|
+
// having none; when the definition turns out to have one, it is built a
|
|
246
|
+
// second time with the reference kept, so that a cycle through a residual
|
|
247
|
+
// stays a cycle.
|
|
248
|
+
let rootHasResidual = false
|
|
249
|
+
let rootRefSeen = false
|
|
250
|
+
// In draft-07 and before, keywords beside $ref are ignored.
|
|
251
|
+
const refOverrides = typeof root.$schema === 'string' && /draft-0[467]\b/.test(root.$schema)
|
|
252
|
+
const restKey = (key) => `__rest__${key}`
|
|
253
|
+
|
|
254
|
+
function resolveRef (ref, ptr) {
|
|
255
|
+
if (ref === '#') return { pointer: '#', schema: root, key: 'Root' }
|
|
256
|
+
const m = /^#\/(\$defs|definitions)\/([^/]+)$/.exec(ref)
|
|
257
|
+
if (!m) return fail(`$ref ${JSON.stringify(ref)} is not a local definition`, ptr)
|
|
258
|
+
const key = decodeURIComponent(m[2].replace(/~1/g, '/').replace(/~0/g, '~'))
|
|
259
|
+
const defs = root[m[1]]
|
|
260
|
+
if (!defs || typeof defs !== 'object' || !Object.prototype.hasOwnProperty.call(defs, key)) return fail(`$ref ${JSON.stringify(ref)} does not resolve`, ptr)
|
|
261
|
+
return { pointer: `#/${m[1]}/${m[2]}`, schema: defs[key], key }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// The alias for a definition; its type and residual are built once.
|
|
265
|
+
function alias (target) {
|
|
266
|
+
if (aliasByPointer.has(target.pointer)) return aliasByPointer.get(target.pointer)
|
|
267
|
+
const n = target.pointer === '#' ? name : typeName((opts.aliasPrefix || '') + target.key, taken)
|
|
268
|
+
aliasByPointer.set(target.pointer, n)
|
|
269
|
+
if (target.pointer !== '#') {
|
|
270
|
+
defResiduals.set(target.pointer, null)
|
|
271
|
+
const r = conv(target.schema, target.pointer)
|
|
272
|
+
aliases.push([n, render(r.t)])
|
|
273
|
+
if (r.res !== null) {
|
|
274
|
+
defResiduals.set(target.pointer, r.res)
|
|
275
|
+
defResiduals.set(target.pointer, conv(target.schema, target.pointer).res)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return n
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function stringAlt (s, ptr, res) {
|
|
282
|
+
let t = 'string'
|
|
283
|
+
if (tags && typeof s.format === 'string' && assertFormat) {
|
|
284
|
+
const src = formatPattern(s.format)
|
|
285
|
+
if (src !== null && patternDiffers(src) === null) { t = tag(t, `Pattern<${JSON.stringify(src)}>`, `${ptr}/format`); delete res.format }
|
|
286
|
+
}
|
|
287
|
+
if (s.minLength !== undefined && finite(s.minLength)) t = tag(t, `MinLength<${s.minLength}>`, `${ptr}/minLength`); else if (s.minLength !== undefined) res.minLength = s.minLength
|
|
288
|
+
if (s.maxLength !== undefined && finite(s.maxLength)) t = tag(t, `MaxLength<${s.maxLength}>`, `${ptr}/maxLength`); else if (s.maxLength !== undefined) res.maxLength = s.maxLength
|
|
289
|
+
if (s.pattern !== undefined) {
|
|
290
|
+
if (typeof s.pattern === 'string' && patternDiffers(s.pattern) === null) t = tag(t, `Pattern<${JSON.stringify(s.pattern)}>`, `${ptr}/pattern`); else res.pattern = s.pattern
|
|
291
|
+
}
|
|
292
|
+
return t
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function numberAlt (s, ptr, res, integer) {
|
|
296
|
+
let t = integer ? tag('number', 'Int', `${ptr}/type`) : 'number'
|
|
297
|
+
if (s.minimum !== undefined && finite(s.minimum)) t = tag(t, `Min<${s.minimum}>`, `${ptr}/minimum`); else if (s.minimum !== undefined) res.minimum = s.minimum
|
|
298
|
+
if (s.maximum !== undefined && finite(s.maximum)) t = tag(t, `Max<${s.maximum}>`, `${ptr}/maximum`); else if (s.maximum !== undefined) res.maximum = s.maximum
|
|
299
|
+
// The draft-04 boolean forms, and anything else odd, stay in the residual.
|
|
300
|
+
if (s.exclusiveMinimum !== undefined && finite(s.exclusiveMinimum)) t = tag(t, `Above<${s.exclusiveMinimum}>`, `${ptr}/exclusiveMinimum`); else if (s.exclusiveMinimum !== undefined) res.exclusiveMinimum = s.exclusiveMinimum
|
|
301
|
+
if (s.exclusiveMaximum !== undefined && finite(s.exclusiveMaximum)) t = tag(t, `Below<${s.exclusiveMaximum}>`, `${ptr}/exclusiveMaximum`); else if (s.exclusiveMaximum !== undefined) res.exclusiveMaximum = s.exclusiveMaximum
|
|
302
|
+
// Floating-point multiples are decided differently by every engine.
|
|
303
|
+
if (s.multipleOf !== undefined && Number.isInteger(s.multipleOf) && s.multipleOf >= 1) t = tag(t, `MultipleOf<${s.multipleOf}>`, `${ptr}/multipleOf`); else if (s.multipleOf !== undefined) res.multipleOf = s.multipleOf
|
|
304
|
+
return t
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// The object alternative. Property subschemas' residuals go under
|
|
308
|
+
// res.properties; when additionalProperties cannot be carried it goes to the
|
|
309
|
+
// residual with every declared property name, since it applies to the keys
|
|
310
|
+
// not among them.
|
|
311
|
+
function objectAlt (s, ptr, res, keys) {
|
|
312
|
+
const props = s.properties
|
|
313
|
+
const required = s.required
|
|
314
|
+
const propsOk = props === undefined || (typeof props === 'object' && props !== null && !Array.isArray(props))
|
|
315
|
+
const reqOk = required === undefined || (Array.isArray(required) && required.every((k) => typeof k === 'string'))
|
|
316
|
+
if (!propsOk || !reqOk) {
|
|
317
|
+
if (props !== undefined) res.properties = props
|
|
318
|
+
if (required !== undefined) res.required = required
|
|
319
|
+
if (s.additionalProperties !== undefined) res.additionalProperties = s.additionalProperties
|
|
320
|
+
return UNIVERSAL.object
|
|
321
|
+
}
|
|
322
|
+
const names = props ? Object.keys(props) : []
|
|
323
|
+
if (names.includes('__proto__')) return fail('a property named __proto__ is not carried', `${ptr}/properties`)
|
|
324
|
+
const req = new Set(required || [])
|
|
325
|
+
const members = []
|
|
326
|
+
const propRes = {}
|
|
327
|
+
for (const k of names) {
|
|
328
|
+
const r = conv(props[k], `${ptr}/properties/${k}`)
|
|
329
|
+
members.push(`${JSON.stringify(k)}${req.has(k) ? '' : '?'}: ${render(r.t)}`)
|
|
330
|
+
if (r.res !== null) propRes[k] = r.res
|
|
331
|
+
}
|
|
332
|
+
for (const k of req) if (!names.includes(k)) members.push(`${JSON.stringify(k)}: unknown`)
|
|
333
|
+
if (props !== undefined) carried.push(`${ptr}/properties`)
|
|
334
|
+
if (required !== undefined) carried.push(`${ptr}/required`)
|
|
335
|
+
const ap = s.additionalProperties
|
|
336
|
+
const apCarried = ap === undefined || ap === true || (ap === false && !keys.includes('patternProperties'))
|
|
337
|
+
let t = members.length ? `{ ${members.join('; ')} }` : UNIVERSAL.object
|
|
338
|
+
if (apCarried) {
|
|
339
|
+
if (ap === false && tags) t = tag(members.length ? t : '{}', 'Strict', `${ptr}/additionalProperties`)
|
|
340
|
+
if (Object.keys(propRes).length) res.properties = propRes
|
|
341
|
+
} else {
|
|
342
|
+
// The residual needs the declared names to know which keys are additional.
|
|
343
|
+
res.additionalProperties = ap
|
|
344
|
+
const all = {}
|
|
345
|
+
for (const k of names) all[k] = Object.prototype.hasOwnProperty.call(propRes, k) ? propRes[k] : true
|
|
346
|
+
if (names.length) res.properties = all
|
|
347
|
+
}
|
|
348
|
+
return t
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function arrayAlt (s, ptr, res) {
|
|
352
|
+
let t
|
|
353
|
+
if (Array.isArray(s.items)) {
|
|
354
|
+
// The draft-07 tuple form; its keywords stay together in the residual.
|
|
355
|
+
for (const k of [...ARRAY_KEYS, 'additionalItems']) if (s[k] !== undefined) res[k] = s[k]
|
|
356
|
+
return UNIVERSAL.array
|
|
357
|
+
}
|
|
358
|
+
if (s.prefixItems !== undefined) {
|
|
359
|
+
// Bun checks a tuple of fixed length only; the schema allows a shorter
|
|
360
|
+
// array unless minItems says otherwise, and a longer one unless items is
|
|
361
|
+
// false. The tuple is carried when both agree with it; otherwise the
|
|
362
|
+
// array keywords stay together in the residual.
|
|
363
|
+
const fixed = Array.isArray(s.prefixItems) && s.items === false && s.minItems === s.prefixItems.length && (s.maxItems === undefined || s.maxItems >= s.prefixItems.length)
|
|
364
|
+
if (!fixed) {
|
|
365
|
+
for (const k of ARRAY_KEYS) if (s[k] !== undefined) res[k] = s[k]
|
|
366
|
+
return UNIVERSAL.array
|
|
367
|
+
}
|
|
368
|
+
const parts = []
|
|
369
|
+
const prefRes = []
|
|
370
|
+
let anyRes = false
|
|
371
|
+
s.prefixItems.forEach((p, i) => { const r = conv(p, `${ptr}/prefixItems/${i}`); parts.push(render(r.t)); prefRes.push(r.res === null ? true : r.res); if (r.res !== null) anyRes = true })
|
|
372
|
+
if (anyRes) res.prefixItems = prefRes
|
|
373
|
+
carried.push(`${ptr}/prefixItems`, `${ptr}/items`, `${ptr}/minItems`)
|
|
374
|
+
if (s.maxItems !== undefined) carried.push(`${ptr}/maxItems`)
|
|
375
|
+
return `[${parts.join(', ')}]`
|
|
376
|
+
}
|
|
377
|
+
if (s.items === undefined || s.items === true) t = UNIVERSAL.array
|
|
378
|
+
else if (s.items === false) { t = '[]'; carried.push(`${ptr}/items`) } else {
|
|
379
|
+
const r = conv(s.items, `${ptr}/items`)
|
|
380
|
+
const it = render(r.t)
|
|
381
|
+
t = needsParens(it) ? `(${it})[]` : `${it}[]`
|
|
382
|
+
if (r.res !== null) res.items = r.res
|
|
383
|
+
carried.push(`${ptr}/items`)
|
|
384
|
+
}
|
|
385
|
+
if (s.minItems !== undefined && finite(s.minItems)) t = tag(t, `MinItems<${s.minItems}>`, `${ptr}/minItems`); else if (s.minItems !== undefined) res.minItems = s.minItems
|
|
386
|
+
if (s.maxItems !== undefined && finite(s.maxItems)) t = tag(t, `MaxItems<${s.maxItems}>`, `${ptr}/maxItems`); else if (s.maxItems !== undefined) res.maxItems = s.maxItems
|
|
387
|
+
return t
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function hasAny (s, keys) { return keys.some((k) => s[k] !== undefined) }
|
|
391
|
+
function keysOf (kind) { return kind === 'string' ? STRING_KEYS : kind === 'number' || kind === 'integer' ? NUMBER_KEYS : kind === 'object' ? OBJECT_KEYS : kind === 'array' ? ARRAY_KEYS : [] }
|
|
392
|
+
|
|
393
|
+
// The alternatives for one named kind, with the rules of that kind.
|
|
394
|
+
function kindAlt (kind, s, ptr, res, keys) {
|
|
395
|
+
switch (kind) {
|
|
396
|
+
case 'string': return single('string', stringAlt(s, ptr, res))
|
|
397
|
+
case 'number': return single('number', numberAlt(s, ptr, res, false))
|
|
398
|
+
case 'integer': return single('number', numberAlt(s, ptr, res, true))
|
|
399
|
+
case 'boolean': return single('boolean', 'boolean')
|
|
400
|
+
case 'null': return single('null', 'null')
|
|
401
|
+
case 'object': return single('object', objectAlt(s, ptr, res, keys))
|
|
402
|
+
case 'array': return single('array', arrayAlt(s, ptr, res))
|
|
403
|
+
default: return null
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Everything at this node into the residual, the type `unknown`.
|
|
408
|
+
function whole (s) { const res = {}; for (const k of Object.keys(s)) if (!IGNORED.has(k)) res[k] = s[k]; return { t: universal(), res: Object.keys(res).length ? res : null } }
|
|
409
|
+
|
|
410
|
+
function conv (s, ptr) {
|
|
411
|
+
if (s === true) return { t: universal(), res: null }
|
|
412
|
+
if (s === false) return { t: empty(), res: null }
|
|
413
|
+
if (!isSchema(s)) return fail('a schema must be an object or a boolean', ptr)
|
|
414
|
+
if (++depth > 200) return fail('the schema nests too deep', ptr)
|
|
415
|
+
try {
|
|
416
|
+
const keys = Object.keys(s).filter((k) => !IGNORED.has(k))
|
|
417
|
+
if (keys.length === 0) return { t: universal(), res: null }
|
|
418
|
+
for (const k of keys) if (WHOLE_NODE.has(k)) return k.startsWith('$') ? fail(`keyword ${JSON.stringify(k)} is not carried`, ptr) : whole(s)
|
|
419
|
+
const res = {}
|
|
420
|
+
for (const k of keys) if (!TYPED.has(k)) res[k] = s[k]
|
|
421
|
+
let t = universal()
|
|
422
|
+
|
|
423
|
+
if (s.$ref !== undefined) {
|
|
424
|
+
if (typeof s.$ref !== 'string') return fail('$ref must be a string', ptr)
|
|
425
|
+
const target = resolveRef(s.$ref, ptr)
|
|
426
|
+
const n = alias(target)
|
|
427
|
+
const aliasT = empty(); aliasT.alias.push(n)
|
|
428
|
+
carried.push(`${ptr}/$ref`)
|
|
429
|
+
if (target.pointer === '#') rootRefSeen = true
|
|
430
|
+
const defRes = target.pointer === '#' ? (rootHasResidual ? { $ref: '#' } : null) : defResiduals.get(target.pointer)
|
|
431
|
+
// The residual's reference goes to the definition's residual, kept
|
|
432
|
+
// under its own name beside the original definitions.
|
|
433
|
+
if (defRes !== null && defRes !== undefined) res.$ref = target.pointer === '#' ? '#' : `#/${target.pointer.split('/')[1]}/${restKey(target.key)}`
|
|
434
|
+
if (refOverrides) { for (const k of keys) if (k !== '$ref') delete res[k] }
|
|
435
|
+
if (keys.length === 1 || refOverrides) return { t: aliasT, res: Object.keys(res).length ? res : null }
|
|
436
|
+
// Siblings beside $ref are conjunctive. The alias cannot be
|
|
437
|
+
// intersected, so the siblings' rules stay in the residual and the
|
|
438
|
+
// type is the alias.
|
|
439
|
+
for (const k of keys) if (k !== '$ref' && TYPED.has(k)) res[k] = s[k]
|
|
440
|
+
return { t: aliasT, res }
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (s.const !== undefined || s.enum !== undefined) {
|
|
444
|
+
let lit = null
|
|
445
|
+
if (s.const !== undefined && s.enum === undefined) lit = literalOf(s.const)
|
|
446
|
+
else if (s.enum !== undefined && s.const === undefined && Array.isArray(s.enum) && s.enum.length > 0) {
|
|
447
|
+
lit = empty()
|
|
448
|
+
for (const v of s.enum) { const l = literalOf(v); if (l === null) { lit = null; break } lit = union(lit, l) }
|
|
449
|
+
}
|
|
450
|
+
if (lit === null) {
|
|
451
|
+
if (s.const !== undefined) res.const = s.const
|
|
452
|
+
if (s.enum !== undefined) res.enum = s.enum
|
|
453
|
+
} else {
|
|
454
|
+
t = lit
|
|
455
|
+
carried.push(s.const !== undefined ? `${ptr}/const` : `${ptr}/enum`)
|
|
456
|
+
}
|
|
457
|
+
// The literal says everything about the value; the other rules at this
|
|
458
|
+
// node would have to be checked against it, which the residual does.
|
|
459
|
+
for (const k of ['type', ...CONSTRAINT_KEYS]) if (s[k] !== undefined) res[k] = s[k]
|
|
460
|
+
return finish(t, res, s, ptr)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (s.type !== undefined) {
|
|
464
|
+
const types = Array.isArray(s.type) ? s.type : [s.type]
|
|
465
|
+
const known = types.every((k) => KINDS.includes(k) || k === 'integer')
|
|
466
|
+
if (types.length === 0 || !known) {
|
|
467
|
+
for (const k of ['type', ...CONSTRAINT_KEYS]) if (s[k] !== undefined) res[k] = s[k]
|
|
468
|
+
return finish(universal(), res, s, ptr)
|
|
469
|
+
}
|
|
470
|
+
carried.push(`${ptr}/type`)
|
|
471
|
+
t = empty()
|
|
472
|
+
for (const k of types) t = union(t, kindAlt(k, s, ptr, res, keys))
|
|
473
|
+
// A rule for a kind the type does not name binds nothing; it is kept
|
|
474
|
+
// in the residual rather than dropped, so nothing is ever lost.
|
|
475
|
+
const allowed = new Set(types.flatMap(keysOf))
|
|
476
|
+
for (const k of CONSTRAINT_KEYS) if (s[k] !== undefined && !allowed.has(k)) res[k] = s[k]
|
|
477
|
+
} else if (hasAny(s, CONSTRAINT_KEYS) || (tags && typeof s.format === 'string' && assertFormat && formatPattern(s.format) !== null)) {
|
|
478
|
+
// A rule without a type binds only the values of its kind: {minLength:
|
|
479
|
+
// 1} accepts every number, object and array. The six kinds, each with
|
|
480
|
+
// the rules of its own kind, say exactly that.
|
|
481
|
+
t = empty()
|
|
482
|
+
for (const k of KINDS) t = union(t, hasAny(s, keysOf(k)) || (tags && k === 'string' && typeof s.format === 'string' && assertFormat && formatPattern(s.format) !== null) ? kindAlt(k, s, ptr, res, keys) : single(k, UNIVERSAL[k]))
|
|
483
|
+
}
|
|
484
|
+
return finish(t, res, s, ptr)
|
|
485
|
+
} finally {
|
|
486
|
+
depth--
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// anyOf and allOf on top of the node's own type, then the residual.
|
|
491
|
+
function finish (t, res, s, ptr) {
|
|
492
|
+
if (s.allOf !== undefined) {
|
|
493
|
+
if (Array.isArray(s.allOf) && s.allOf.length > 0) {
|
|
494
|
+
const rest = []
|
|
495
|
+
s.allOf.forEach((b, i) => {
|
|
496
|
+
const r = conv(b, `${ptr}/allOf/${i}`)
|
|
497
|
+
const m = intersect(t, r.t)
|
|
498
|
+
if (m === null) { rest.push(b) } else { t = m; if (r.res !== null) rest.push(r.res) }
|
|
499
|
+
})
|
|
500
|
+
if (rest.length) res.allOf = rest
|
|
501
|
+
carried.push(`${ptr}/allOf`)
|
|
502
|
+
} else res.allOf = s.allOf
|
|
503
|
+
}
|
|
504
|
+
if (s.anyOf !== undefined) {
|
|
505
|
+
if (Array.isArray(s.anyOf) && s.anyOf.length > 0) {
|
|
506
|
+
let u = empty()
|
|
507
|
+
let clean = true
|
|
508
|
+
for (let i = 0; i < s.anyOf.length; i++) {
|
|
509
|
+
const r = conv(s.anyOf[i], `${ptr}/anyOf/${i}`)
|
|
510
|
+
// Without tags the type is a shape and the union of the branches'
|
|
511
|
+
// shapes is it; with them a branch's residual breaks the split.
|
|
512
|
+
if (tags && r.res !== null) { clean = false; break }
|
|
513
|
+
u = union(u, r.t)
|
|
514
|
+
}
|
|
515
|
+
const m = clean ? intersect(t, u) : null
|
|
516
|
+
if (m !== null) { t = m; carried.push(`${ptr}/anyOf`) } else res.anyOf = s.anyOf
|
|
517
|
+
} else res.anyOf = s.anyOf
|
|
518
|
+
}
|
|
519
|
+
return { t, res: Object.keys(res).length ? res : null }
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
aliasByPointer.set('#', name)
|
|
523
|
+
let r = conv(root, '#')
|
|
524
|
+
if (tags && r.res !== null && rootRefSeen) { rootHasResidual = true; r = conv(root, '#') }
|
|
525
|
+
let residual = tags ? r.res : null
|
|
526
|
+
if (residual !== null) {
|
|
527
|
+
// Subschemas the residual copies as they are may refer to any definition,
|
|
528
|
+
// so the definitions travel whole; the residuals of definitions the split
|
|
529
|
+
// refers to travel beside them under their own names.
|
|
530
|
+
const text = JSON.stringify(residual)
|
|
531
|
+
if (/"\$ref":"#"/.test(text) && !rootHasResidual) return fail('a rule the type cannot carry refers to the root', '#')
|
|
532
|
+
for (const bucket of ['$defs', 'definitions']) {
|
|
533
|
+
if (root[bucket] && typeof root[bucket] === 'object' && text.includes(`"#/${bucket}/`)) residual[bucket] = { ...root[bucket] }
|
|
534
|
+
}
|
|
535
|
+
for (const [pointer, v] of defResiduals.entries()) {
|
|
536
|
+
if (v === null) continue
|
|
537
|
+
const m = /^#\/(\$defs|definitions)\/(.+)$/.exec(pointer)
|
|
538
|
+
residual[m[1]] = residual[m[1]] || {}
|
|
539
|
+
residual[m[1]][restKey(decodeURIComponent(m[2].replace(/~1/g, '/').replace(/~0/g, '~')))] = v
|
|
540
|
+
}
|
|
541
|
+
if (typeof root.$schema === 'string') residual = { $schema: root.$schema, ...residual }
|
|
542
|
+
}
|
|
543
|
+
if (isUniversal(r.t)) return fail('the type carries nothing of the schema', '#')
|
|
544
|
+
return { name, type: render(r.t), aliases, residual, carried }
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// The TypeScript module `ata build --target bun` writes beside the plain one:
|
|
548
|
+
// the type, and the module's functions with the type on them. `parse`
|
|
549
|
+
// declares the type where it parses, which is what the compiler reads to make
|
|
550
|
+
// the objects with that layout; the module then checks the content.
|
|
551
|
+
function toBunModule ({ schemaFile, moduleImport, typeName: tn, typeSource, aliases }) {
|
|
552
|
+
const lines = []
|
|
553
|
+
lines.push(`// Generated by ata build --target bun from ${schemaFile}. Do not edit.`)
|
|
554
|
+
lines.push('//')
|
|
555
|
+
lines.push('// For executables built with `bun build --compile --aot`: the schema as a type')
|
|
556
|
+
lines.push(`// for the program, with the plain module (${moduleImport}) answering every`)
|
|
557
|
+
lines.push('// check, so the answers and the errors are the module\'s. `parse(text)` declares')
|
|
558
|
+
lines.push('// the type where it parses and the module checks the content; it is not faster')
|
|
559
|
+
lines.push('// than a plain parse and the module. Everywhere else, including plain `bun run`,')
|
|
560
|
+
lines.push('// the plain module does the same without this file.')
|
|
561
|
+
lines.push(`import * as _m from ${JSON.stringify(moduleImport)}`)
|
|
562
|
+
lines.push('')
|
|
563
|
+
for (const [n, src] of aliases) lines.push(`type ${n} = ${src}`)
|
|
564
|
+
lines.push(`export type ${tn} = ${typeSource}`)
|
|
565
|
+
lines.push('')
|
|
566
|
+
lines.push(`export function isValid (d: unknown): d is ${tn} { return _m.isValid(d) }`)
|
|
567
|
+
lines.push('export function validate (d: unknown) { return _m.validate(d) }')
|
|
568
|
+
lines.push('// The parsed document when the text is JSON and the document is valid, else null.')
|
|
569
|
+
lines.push(`export function parse (text: string): ${tn} | null {`)
|
|
570
|
+
lines.push(' try {')
|
|
571
|
+
lines.push(` const d: ${tn} = JSON.parse(text)`)
|
|
572
|
+
lines.push(' return _m.isValid(d) ? d : null')
|
|
573
|
+
lines.push(' } catch { return null }')
|
|
574
|
+
lines.push('}')
|
|
575
|
+
lines.push('')
|
|
576
|
+
return lines.join('\n')
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Writes the Bun target for one compiled module: the `.bun.ts` beside it.
|
|
580
|
+
// Returns what was written; throws Declined when no type can be written (a
|
|
581
|
+
// remote $ref, a dynamic reference, a schema the type says nothing about).
|
|
582
|
+
function writeBunTarget ({ schema, input, output, typeName: tn }) {
|
|
583
|
+
const fs = require('fs')
|
|
584
|
+
const path = require('path')
|
|
585
|
+
const mapped = schemaToBunType(schema, { name: tn })
|
|
586
|
+
const ext = path.extname(output)
|
|
587
|
+
const bunPath = output.slice(0, output.length - ext.length) + '.bun.ts'
|
|
588
|
+
fs.writeFileSync(bunPath, toBunModule({
|
|
589
|
+
schemaFile: path.relative(process.cwd(), input) || input,
|
|
590
|
+
moduleImport: './' + path.basename(output),
|
|
591
|
+
typeName: mapped.name,
|
|
592
|
+
typeSource: mapped.type,
|
|
593
|
+
aliases: mapped.aliases,
|
|
594
|
+
}))
|
|
595
|
+
return { bunPath, definitions: mapped.aliases.length, carried: mapped.carried.length }
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// The validation keywords in a residual of the tagged split, for a report.
|
|
599
|
+
function countKeywords (s) {
|
|
600
|
+
if (typeof s !== 'object' || s === null) return 0
|
|
601
|
+
if (Array.isArray(s)) return s.reduce((n, x) => n + countKeywords(x), 0)
|
|
602
|
+
let n = 0
|
|
603
|
+
for (const k of Object.keys(s)) {
|
|
604
|
+
if (IGNORED.has(k) && k !== '$defs' && k !== 'definitions') continue
|
|
605
|
+
if (['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas'].includes(k)) { for (const v of Object.values(s[k] || {})) n += countKeywords(v); continue }
|
|
606
|
+
if (['items', 'prefixItems', 'allOf', 'anyOf', 'oneOf', 'not', 'if', 'then', 'else', 'contains', 'additionalProperties', 'propertyNames', 'unevaluatedProperties', 'unevaluatedItems'].includes(k)) { n += (typeof s[k] === 'object' ? countKeywords(s[k]) : 0) + (['items', 'prefixItems', 'allOf', 'anyOf', 'properties'].includes(k) ? 0 : 1); continue }
|
|
607
|
+
n++
|
|
608
|
+
}
|
|
609
|
+
return n
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
module.exports = { schemaToBunType, toBunModule, writeBunTarget, countKeywords, Declined }
|
package/lib/codegen-engine.js
CHANGED
|
@@ -325,8 +325,14 @@ function installCodegenPaths (ctx) {
|
|
|
325
325
|
// second time and built a second rejection around it.
|
|
326
326
|
ctx.rejectBase = onReject;
|
|
327
327
|
// The rich combined function, for a validator whose errors are being read
|
|
328
|
-
// with richErrors on. See _ensureCompiled.
|
|
329
|
-
|
|
328
|
+
// with richErrors on. See _ensureCompiled. The builders read the document
|
|
329
|
+
// validate() has already passed through the preprocess pass (defaults,
|
|
330
|
+
// coercion, removal edit it in place), so they hold with one too: a schema
|
|
331
|
+
// with a single `default`, which ata fills without being asked, used to
|
|
332
|
+
// keep its errors on the enrich-afterwards path, 666 against 313 ns for
|
|
333
|
+
// one `required` error on a four-field user. Only the one-pass swap below,
|
|
334
|
+
// which replaces validate() itself, needs the document unprocessed.
|
|
335
|
+
{
|
|
330
336
|
ctx.richBuilder = richBuilder;
|
|
331
337
|
// The plain combined function stands in where no rich one is built
|
|
332
338
|
// (richErrors off, or a schema source to frame errors from): its errors
|
|
@@ -343,7 +349,7 @@ function installCodegenPaths (ctx) {
|
|
|
343
349
|
ctx.isCombined = (fn) => fn !== null && fn === _safeCombined;
|
|
344
350
|
// validate() as the combined function in its result shape: built and
|
|
345
351
|
// probed here, null when it declines or throws on the probe.
|
|
346
|
-
ctx.onePassOf = (empty, fallback) => {
|
|
352
|
+
ctx.onePassOf = preprocess ? null : (empty, fallback) => {
|
|
347
353
|
// A collapsed branch error is built in its final plain shape here
|
|
348
354
|
// (no ordering key in the runtime shape); its branch errors are put
|
|
349
355
|
// in order with the rest.
|
package/lib/combined-runtime.js
CHANGED
|
@@ -114,7 +114,7 @@ function srX (S, e, srt) {
|
|
|
114
114
|
// the standalone modules carry). The prototype test itself stays inline in
|
|
115
115
|
// the generated code, read once per object: a shared helper reading the
|
|
116
116
|
// prototype of every object it was asked about was megamorphic.
|
|
117
|
-
const _PN = ({}).__proto__ === Object.prototype // eslint-disable-line no-proto
|
|
117
|
+
const _PN = typeof Bun === 'undefined' && ({}).__proto__ === Object.prototype // eslint-disable-line no-proto -- JavaScriptCore goes through Object.getPrototypeOf
|
|
118
118
|
const _OP = Object.prototype
|
|
119
119
|
const _gp = Object.getPrototypeOf
|
|
120
120
|
const _hop = Object.prototype.hasOwnProperty
|