• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

discoveryjs / json-ext / 9551388080

17 Jun 2024 04:35PM UTC coverage: 99.663% (+0.7%) from 98.939%
9551388080

push

github

lahmatiy
Rework transpile and bundle

387 of 391 branches covered (98.98%)

Branch coverage included in aggregate %.

1390 of 1392 relevant lines covered (99.86%)

26702.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.03
/src/stringify-stream.js
1
import { Readable } from 'node:stream';
1✔
2
import {
1✔
3
    normalizeReplacer,
1✔
4
    normalizeSpace,
1✔
5
    replaceValue,
1✔
6
    getTypeAsync,
1✔
7
    type
1✔
8
} from './utils.js';
1✔
9

1✔
10
const noop = () => {};
1✔
11
const {
1✔
12
    PRIMITIVE,
1✔
13
    OBJECT,
1✔
14
    ARRAY,
1✔
15
    PROMISE,
1✔
16
    STRING_STREAM,
1✔
17
    OBJECT_STREAM
1✔
18
} = type;
1✔
19

1✔
20
// TODO: Remove when drop support for Node.js 10
1✔
21
// Node.js 10 has no well-formed JSON.stringify()
1✔
22
// https://github.com/tc39/proposal-well-formed-stringify
1✔
23
// Adopted code from https://bugs.chromium.org/p/v8/issues/detail?id=7782#c12
1✔
24
const wellformedStringStringify = JSON.stringify('\ud800') === '"\\ud800"'
1✔
25
    ? JSON.stringify
1✔
26
    : s => JSON.stringify(s).replace(
1!
27
        /\p{Surrogate}/gu,
×
28
        m => `\\u${m.charCodeAt(0).toString(16)}`
×
29
    );
1✔
30

1✔
31
function push() {
27✔
32
    this.push(this._stack.value);
27✔
33
    this.popStack();
27✔
34
}
27✔
35

1✔
36
function pushPrimitive(value) {
1,030✔
37
    switch (typeof value) {
1,030✔
38
        case 'string':
1,030✔
39
            this.push(this.encodeString(value));
159✔
40
            break;
159✔
41

1,030✔
42
        case 'number':
1,030✔
43
            this.push(Number.isFinite(value) ? this.encodeNumber(value) : 'null');
623✔
44
            break;
623✔
45

1,030✔
46
        case 'boolean':
1,030✔
47
            this.push(value ? 'true' : 'false');
92✔
48
            break;
92✔
49

1,030✔
50
        case 'undefined':
1,030✔
51
        case 'object': // typeof null === 'object'
1,030✔
52
            this.push('null');
155✔
53
            break;
155✔
54

1,030✔
55
        default:
1,030✔
56
            this.destroy(new TypeError(`Do not know how to serialize a ${value.constructor && value.constructor.name || typeof value}`));
1!
57
    }
1,030✔
58
}
1,030✔
59

1✔
60
function processObjectEntry(key) {
519✔
61
    const current = this._stack;
519✔
62

519✔
63
    if (!current.first) {
519✔
64
        current.first = true;
277✔
65
    } else {
519✔
66
        this.push(',');
242✔
67
    }
242✔
68

519✔
69
    if (this.space) {
519✔
70
        this.push(`\n${this.space.repeat(this._depth)}${this.encodeString(key)}: `);
270✔
71
    } else {
519✔
72
        this.push(this.encodeString(key) + ':');
249✔
73
    }
249✔
74
}
519✔
75

1✔
76
function processObject() {
1,009✔
77
    const current = this._stack;
1,009✔
78

1,009✔
79
    // when no keys left, remove obj from stack
1,009✔
80
    if (current.index === current.keys.length) {
1,009✔
81
        if (this.space && current.first) {
346✔
82
            this.push(`\n${this.space.repeat(this._depth - 1)}}`);
138✔
83
        } else {
346✔
84
            this.push('}');
208✔
85
        }
208✔
86

346✔
87
        this.popStack();
346✔
88
        return;
346✔
89
    }
346✔
90

1,009✔
91
    const key = current.keys[current.index];
1,009✔
92

1,009✔
93
    this.processValue(current.value, key, current.value[key], processObjectEntry);
1,009✔
94
    current.index++;
1,009✔
95
}
1,009✔
96

1✔
97
function processArrayItem(index) {
683✔
98
    if (index !== 0) {
683✔
99
        this.push(',');
416✔
100
    }
416✔
101

683✔
102
    if (this.space) {
683✔
103
        this.push(`\n${this.space.repeat(this._depth)}`);
378✔
104
    }
378✔
105
}
683✔
106

1✔
107
function processArray() {
920✔
108
    const current = this._stack;
920✔
109

920✔
110
    if (current.index === current.value.length) {
920✔
111
        if (this.space && current.index > 0) {
285✔
112
            this.push(`\n${this.space.repeat(this._depth - 1)}]`);
138✔
113
        } else {
285✔
114
            this.push(']');
147✔
115
        }
147✔
116

285✔
117
        this.popStack();
285✔
118
        return;
285✔
119
    }
285✔
120

920✔
121
    this.processValue(current.value, current.index, current.value[current.index], processArrayItem);
920✔
122
    current.index++;
920✔
123
}
920✔
124

1✔
125
function createStreamReader(fn) {
2✔
126
    return function() {
2✔
127
        const current = this._stack;
157✔
128
        const data = current.value.read(this._readSize);
157✔
129

157✔
130
        if (data !== null) {
157✔
131
            current.first = false;
63✔
132
            fn.call(this, data, current);
63✔
133
        } else {
157✔
134
            if ((current.first && !current.value._readableState.reading) || current.ended) {
94✔
135
                this.popStack();
41✔
136
            } else {
94✔
137
                current.first = true;
53✔
138
                current.awaiting = true;
53✔
139
            }
53✔
140
        }
94✔
141
    };
2✔
142
}
2✔
143

1✔
144
const processReadableObject = createStreamReader(function(data, current) {
1✔
145
    this.processValue(current.value, current.index, data, processArrayItem);
48✔
146
    current.index++;
48✔
147
});
1✔
148

1✔
149
const processReadableString = createStreamReader(function(data) {
1✔
150
    this.push(data);
15✔
151
});
1✔
152

1✔
153
class JsonStringifyStream extends Readable {
1✔
154
    constructor(value, replacer, space) {
1✔
155
        super({
524✔
156
            autoDestroy: true
524✔
157
        });
524✔
158

524✔
159
        this.getKeys = Object.keys;
524✔
160
        this.replacer = normalizeReplacer(replacer);
524✔
161

524✔
162
        if (Array.isArray(this.replacer)) {
524✔
163
            const allowlist = this.replacer;
4✔
164

4✔
165
            this.getKeys = () => allowlist;
4✔
166
            this.replacer = null;
4✔
167
        }
4✔
168

524✔
169
        this.space = normalizeSpace(space);
524✔
170
        this._depth = 0;
524✔
171

524✔
172
        this.error = null;
524✔
173
        this._processing = false;
524✔
174
        this._ended = false;
524✔
175

524✔
176
        this._readSize = 0;
524✔
177
        this._buffer = '';
524✔
178

524✔
179
        this._stack = null;
524✔
180
        this._visited = new WeakSet();
524✔
181

524✔
182
        this.pushStack({
524✔
183
            handler: () => {
524✔
184
                this.popStack();
524✔
185
                this.processValue({ '': value }, '', value, noop);
524✔
186
            }
524✔
187
        });
524✔
188
    }
524✔
189

1✔
190
    encodeString(value) {
1✔
191
        if (/[^\x20-\uD799]|[\x22\x5c]/.test(value)) {
678✔
192
            return wellformedStringStringify(value);
14✔
193
        }
14✔
194

664✔
195
        return '"' + value + '"';
664✔
196
    }
678✔
197

1✔
198
    encodeNumber(value) {
1✔
199
        return value;
560✔
200
    }
560✔
201

1✔
202
    processValue(holder, key, value, callback) {
1✔
203
        value = replaceValue(holder, key, value, this.replacer);
1,898✔
204

1,898✔
205
        let type = getTypeAsync(value);
1,898✔
206

1,898✔
207
        switch (type) {
1,898✔
208
            case PRIMITIVE:
1,898✔
209
                if (callback !== processObjectEntry || value !== undefined) {
1,174✔
210
                    callback.call(this, key);
1,030✔
211
                    pushPrimitive.call(this, value);
1,030✔
212
                }
1,030✔
213
                break;
1,174✔
214

1,898✔
215
            case OBJECT:
1,898✔
216
                callback.call(this, key);
357✔
217

357✔
218
                // check for circular structure
357✔
219
                if (this._visited.has(value)) {
357✔
220
                    return this.destroy(new TypeError('Converting circular structure to JSON'));
4✔
221
                }
4✔
222

353✔
223
                this._visited.add(value);
353✔
224
                this._depth++;
353✔
225
                this.push('{');
353✔
226
                this.pushStack({
353✔
227
                    handler: processObject,
353✔
228
                    value,
353✔
229
                    index: 0,
353✔
230
                    first: false,
353✔
231
                    keys: this.getKeys(value)
353✔
232
                });
353✔
233
                break;
353✔
234

1,898✔
235
            case ARRAY:
1,898✔
236
                callback.call(this, key);
291✔
237

291✔
238
                // check for circular structure
291✔
239
                if (this._visited.has(value)) {
291✔
240
                    return this.destroy(new TypeError('Converting circular structure to JSON'));
2✔
241
                }
2✔
242

289✔
243
                this._visited.add(value);
289✔
244

289✔
245
                this.push('[');
289✔
246
                this.pushStack({
289✔
247
                    handler: processArray,
289✔
248
                    value,
289✔
249
                    index: 0
289✔
250
                });
289✔
251
                this._depth++;
289✔
252
                break;
289✔
253

1,898✔
254
            case PROMISE:
1,898✔
255
                this.pushStack({
29✔
256
                    handler: noop,
29✔
257
                    awaiting: true
29✔
258
                });
29✔
259

29✔
260
                Promise.resolve(value)
29✔
261
                    .then(resolved => {
29✔
262
                        this.popStack();
28✔
263
                        this.processValue(holder, key, resolved, callback);
28✔
264
                        this.processStack();
28✔
265
                    })
29✔
266
                    .catch(error => {
29✔
267
                        this.destroy(error);
1✔
268
                    });
29✔
269
                break;
29✔
270

1,898✔
271
            case STRING_STREAM:
1,898✔
272
            case OBJECT_STREAM:
1,898✔
273
                callback.call(this, key);
46✔
274

46✔
275
                // TODO: Remove when drop support for Node.js 10
46✔
276
                // Used `_readableState.endEmitted` as fallback, since Node.js 10 has no `readableEnded` getter
46✔
277
                if (value.readableEnded || value._readableState.endEmitted) {
46✔
278
                    return this.destroy(new Error('Readable Stream has ended before it was serialized. All stream data have been lost'));
1✔
279
                }
1✔
280

45✔
281
                if (value.readableFlowing) {
46✔
282
                    return this.destroy(new Error('Readable Stream is in flowing mode, data may have been lost. Trying to pause stream.'));
1✔
283
                }
1✔
284

46✔
285
                if (type === OBJECT_STREAM) {
46✔
286
                    this.push('[');
30✔
287
                    this.pushStack({
30✔
288
                        handler: push,
30✔
289
                        value: this.space ? '\n' + this.space.repeat(this._depth) + ']' : ']'
30✔
290
                    });
30✔
291
                    this._depth++;
30✔
292
                }
30✔
293

44✔
294
                const self = this.pushStack({
44✔
295
                    handler: type === OBJECT_STREAM ? processReadableObject : processReadableString,
46✔
296
                    value,
46✔
297
                    index: 0,
46✔
298
                    first: false,
46✔
299
                    ended: false,
46✔
300
                    awaiting: !value.readable || value.readableLength === 0
46✔
301
                });
46✔
302
                const continueProcessing = () => {
46✔
303
                    if (self.awaiting) {
146✔
304
                        self.awaiting = false;
97✔
305
                        this.processStack();
97✔
306
                    }
97✔
307
                };
46✔
308

46✔
309
                value.once('error', error => this.destroy(error));
46✔
310
                value.once('end', () => {
46✔
311
                    self.ended = true;
43✔
312
                    continueProcessing();
43✔
313
                });
46✔
314
                value.on('readable', continueProcessing);
46✔
315
                break;
46✔
316
        }
1,898✔
317
    }
1,898✔
318

1✔
319
    pushStack(node) {
1✔
320
        node.prev = this._stack;
1,269✔
321
        return this._stack = node;
1,269✔
322
    }
1,269✔
323

1✔
324
    popStack() {
1✔
325
        const { handler, value } = this._stack;
1,251✔
326

1,251✔
327
        if (handler === processObject || handler === processArray || handler === processReadableObject) {
1,251✔
328
            this._visited.delete(value);
658✔
329
            this._depth--;
658✔
330
        }
658✔
331

1,251✔
332
        this._stack = this._stack.prev;
1,251✔
333
    }
1,251✔
334

1✔
335
    processStack() {
1✔
336
        if (this._processing || this._ended) {
650✔
337
            return;
2✔
338
        }
2✔
339

650✔
340
        try {
650✔
341
            this._processing = true;
650✔
342

650✔
343
            while (this._stack !== null && !this._stack.awaiting) {
650✔
344
                this._stack.handler.call(this);
2,637✔
345

2,637✔
346
                if (!this._processing) {
2,637✔
347
                    return;
9✔
348
                }
9✔
349
            }
2,637✔
350

638✔
351
            this._processing = false;
638✔
352
        } catch (error) {
650✔
353
            this.destroy(error);
1✔
354
            return;
1✔
355
        }
1✔
356

650✔
357
        if (this._stack === null && !this._ended) {
650✔
358
            this._finish();
512✔
359
            this.push(null);
512✔
360
        }
512✔
361
    }
650✔
362

1✔
363
    push(data) {
1✔
364
        if (data !== null) {
4,441✔
365
            this._buffer += data;
3,929✔
366

3,929✔
367
            // check buffer overflow
3,929✔
368
            if (this._buffer.length < this._readSize) {
3,929✔
369
                return;
3,928✔
370
            }
3,928✔
371

3,929✔
372
            // flush buffer
3,929✔
373
            data = this._buffer;
3,929✔
374
            this._buffer = '';
3,929✔
375
            this._processing = false;
3,929✔
376
        }
3,929✔
377

4,441✔
378
        super.push(data);
4,441✔
379
    }
4,441✔
380

1✔
381
    _read(size) {
1✔
382
        // start processing
525✔
383
        this._readSize = size || this.readableHighWaterMark;
525!
384
        this.processStack();
525✔
385
    }
525✔
386

1✔
387
    _finish() {
1✔
388
        this._ended = true;
1,036✔
389
        this._processing = false;
1,036✔
390
        this._stack = null;
1,036✔
391
        this._visited = null;
1,036✔
392

1,036✔
393
        if (this._buffer && this._buffer.length) {
1,036✔
394
            super.push(this._buffer); // flush buffer
520✔
395
        }
520✔
396

1,036✔
397
        this._buffer = '';
1,036✔
398
    }
1,036✔
399

1✔
400
    _destroy(error, cb) {
1✔
401
        this.error = this.error || error;
524✔
402
        this._finish();
524✔
403
        cb(error);
524✔
404
    }
524✔
405
}
1✔
406

1✔
407
export function stringifyStream(value, replacer, space) {
1✔
408
    return new JsonStringifyStream(value, replacer, space);
524✔
409
};
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc