codekingpro/portable-devtools
114k
1const Parser = require('jsonparse')
2const { Minipass } = require('minipass')
3
4class JSONStreamError extends Error {
5 constructor (err, caller) {
6 super(err.message)
7 Error.captureStackTrace(this, caller || this.constructor)
8 }
9
10 get name () {
11 return 'JSONStreamError'
12 }
13}
14
15const check = (x, y) =>
16 typeof x === 'string' ? String(y) === x
17 : x && typeof x.test === 'function' ? x.test(y)
18 : typeof x === 'boolean' || typeof x === 'object' ? x
19 : typeof x === 'function' ? x(y)
20 : false
21
22class JSONStream extends Minipass {
23 #count = 0
24 #ending = false
25 #footer = null
26 #header = null
27 #map = null
28 #onTokenOriginal
29 #parser
30 #path = null
31 #root = null
32
33 constructor (opts) {
34 super({
35 ...opts,
36 objectMode: true,
37 })
38
39 const parser = this.#parser = new Parser()
40 parser.onValue = value => this.#onValue(value)
41 this.#onTokenOriginal = parser.onToken
42 parser.onToken = (token, value) => this.#onToken(token, value)
43 parser.onError = er => this.#onError(er)
44
45 this.#path = typeof opts.path === 'string'
46 ? opts.path.split('.').map(e =>
47 e === '$*' ? { emitKey: true }
48 : e === '*' ? true
49 : e === '' ? { recurse: true }
50 : e)
51 : Array.isArray(opts.path) && opts.path.length ? opts.path
52 : null
53
54 if (typeof opts.map === 'function') {
55 this.#map = opts.map
56 }
57 }
58
59 #setHeaderFooter (key, value) {
60 // header has not been emitted yet
61 if (this.#header !== false) {
62 this.#header = this.#header || {}
63 this.#header[key] = value
64 }
65
66 // footer has not been emitted yet but header has
67 if (this.#footer !== false && this.#header === false) {
68 this.#footer = this.#footer || {}
69 this.#footer[key] = value
70 }
71 }
72
73 #onError (er) {
74 // error will always happen during a write() call.
75 const caller = this.#ending ? this.end : this.write
76 this.#ending = false
77 return this.emit('error', new JSONStreamError(er, caller))
78 }
79
80 #onToken (token, value) {
81 const parser = this.#parser
82 this.#onTokenOriginal.call(this.#parser, token, value)
83 if (parser.stack.length === 0) {
84 if (this.#root) {
85 const root = this.#root
86 if (!this.#path) {
87 super.write(root)
88 }
89 this.#root = null
90 this.#count = 0
91 }
92 }
93 }
94
95 #onValue (value) {
96 const parser = this.#parser
97 // the LAST onValue encountered is the root object.
98 // just overwrite it each time.
99 this.#root = value
100
101 if (!this.#path) {
102 return
103 }
104
105 let i = 0 // iterates on path
106 let j = 0 // iterates on stack
107 let emitKey = false
108 while (i < this.#path.length) {
109 const key = this.#path[i]
110 j++
111
112 if (key && !key.recurse) {
113 const c = (j === parser.stack.length) ? parser : parser.stack[j]
114 if (!c) {
115 return
116 }
117 if (!check(key, c.key)) {
118 this.#setHeaderFooter(c.key, value)
119 return
120 }
121 emitKey = !!key.emitKey
122 i++
123 } else {
124 i++
125 if (i >= this.#path.length) {
126 return
127 }
128 const nextKey = this.#path[i]
129 if (!nextKey) {
130 return
131 }
132 while (true) {
133 const c = (j === parser.stack.length) ? parser : parser.stack[j]
134 if (!c) {
135 return
136 }
137 if (check(nextKey, c.key)) {
138 i++
139 if (!Object.isFrozen(parser.stack[j])) {
140 parser.stack[j].value = null
141 }
142 break
143 } else {
144 this.#setHeaderFooter(c.key, value)
145 }
146 j++
147 }
148 }
149 }
150
151 // emit header
152 if (this.#header) {
153 const header = this.#header
154 this.#header = false
155 this.emit('header', header)
156 }
157 if (j !== parser.stack.length) {
158 return
159 }
160
161 this.#count++
162 const actualPath = parser.stack.slice(1)
163 .map(e => e.key).concat([parser.key])
164 if (value !== null && value !== undefined) {
165 const data = this.#map ? this.#map(value, actualPath) : value
166 if (data !== null && data !== undefined) {
167 const emit = emitKey ? { value: data } : data
168 if (emitKey) {
169 emit.key = parser.key
170 }
171 super.write(emit)
172 }
173 }
174
175 if (parser.value) {
176 delete parser.value[parser.key]
177 }
178
179 for (const k of parser.stack) {
180 k.value = null
181 }
182 }
183
184 write (chunk, encoding) {
185 if (typeof chunk === 'string') {
186 chunk = Buffer.from(chunk, encoding)
187 } else if (!Buffer.isBuffer(chunk)) {
188 return this.emit('error', new TypeError(
189 'Can only parse JSON from string or buffer input'))
190 }
191 this.#parser.write(chunk)
192 return this.flowing
193 }
194
195 end (chunk, encoding) {
196 this.#ending = true
197 if (chunk) {
198 this.write(chunk, encoding)
199 }
200
201 const h = this.#header
202 this.#header = null
203 const f = this.#footer
204 this.#footer = null
205 if (h) {
206 this.emit('header', h)
207 }
208 if (f) {
209 this.emit('footer', f)
210 }
211 return super.end()
212 }
213
214 static get JSONStreamError () {
215 return JSONStreamError
216 }
217
218 static parse (path, map) {
219 return new JSONStream({ path, map })
220 }
221}
222
223module.exports = JSONStream
224 