qs 6.4.1 → 6.5.3

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/.editorconfig CHANGED
@@ -13,8 +13,10 @@ quote_type = single
13
13
  [test/*]
14
14
  max_line_length = off
15
15
 
16
- [*.md]
16
+ [LICENSE.md]
17
17
  indent_size = off
18
+
19
+ [*.md]
18
20
  max_line_length = off
19
21
 
20
22
  [*.json]
@@ -37,8 +39,5 @@ indent_style = off
37
39
  indent = off
38
40
  max_line_length = off
39
41
 
40
- [dist/*]
41
- max_line_length = off
42
-
43
42
  [.nycrc]
44
43
  indent_style = tab
package/.eslintrc CHANGED
@@ -8,7 +8,7 @@
8
8
  ],
9
9
 
10
10
  "rules": {
11
- "complexity": [2, 29],
11
+ "complexity": 0,
12
12
  "consistent-return": 1,
13
13
  "func-name-matching": 0,
14
14
  "id-length": [2, { "min": 1, "max": 25, "properties": "never" }],
package/CHANGELOG.md CHANGED
@@ -1,24 +1,53 @@
1
- ## **6.4.1**
1
+ ## **6.5.3**
2
2
  - [Fix] `parse`: ignore `__proto__` keys (#428)
3
- - [Fix] fix for an impossible situation: when the formatter is called with a non-string value
4
- - [Fix] use `safer-buffer` instead of `Buffer` constructor
5
- - [Fix] `utils.merge`: avoid a crash with a null target and an array source
6
3
  - [Fix]` `utils.merge`: avoid a crash with a null target and a truthy non-array source
4
+ - [Fix] correctly parse nested arrays
7
5
  - [Fix] `stringify`: fix a crash with `strictNullHandling` and a custom `filter`/`serializeDate` (#279)
8
6
  - [Fix] `utils`: `merge`: fix crash when `source` is a truthy primitive & no options are provided
9
7
  - [Fix] when `parseArrays` is false, properly handle keys ending in `[]`
10
- - [Robustness] `stringify`: avoid relying on a global `undefined` (#427)
8
+ - [Fix] fix for an impossible situation: when the formatter is called with a non-string value
9
+ - [Fix] `utils.merge`: avoid a crash with a null target and an array source
10
+ - [Refactor] `utils`: reduce observable [[Get]]s
11
11
  - [Refactor] use cached `Array.isArray`
12
12
  - [Refactor] `stringify`: Avoid arr = arr.concat(...), push to the existing instance (#269)
13
+ - [Refactor] `parse`: only need to reassign the var once
14
+ - [Robustness] `stringify`: avoid relying on a global `undefined` (#427)
13
15
  - [readme] remove travis badge; add github actions/codecov badges; update URLs
16
+ - [Docs] Clean up license text so it’s properly detected as BSD-3-Clause
14
17
  - [Docs] Clarify the need for "arrayLimit" option
15
18
  - [meta] fix README.md (#399)
16
- - [meta] Clean up license text so it’s properly detected as BSD-3-Clause
17
19
  - [meta] add FUNDING.yml
18
20
  - [actions] backport actions from main
21
+ - [Tests] always use `String(x)` over `x.toString()`
19
22
  - [Tests] remove nonexistent tape option
20
23
  - [Dev Deps] backport from main
21
24
 
25
+ ## **6.5.2**
26
+ - [Fix] use `safer-buffer` instead of `Buffer` constructor
27
+ - [Refactor] utils: `module.exports` one thing, instead of mutating `exports` (#230)
28
+ - [Dev Deps] update `browserify`, `eslint`, `iconv-lite`, `safer-buffer`, `tape`, `browserify`
29
+
30
+ ## **6.5.1**
31
+ - [Fix] Fix parsing & compacting very deep objects (#224)
32
+ - [Refactor] name utils functions
33
+ - [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `tape`
34
+ - [Tests] up to `node` `v8.4`; use `nvm install-latest-npm` so newer npm doesn’t break older node
35
+ - [Tests] Use precise dist for Node.js 0.6 runtime (#225)
36
+ - [Tests] make 0.6 required, now that it’s passing
37
+ - [Tests] on `node` `v8.2`; fix npm on node 0.6
38
+
39
+ ## **6.5.0**
40
+ - [New] add `utils.assign`
41
+ - [New] pass default encoder/decoder to custom encoder/decoder functions (#206)
42
+ - [New] `parse`/`stringify`: add `ignoreQueryPrefix`/`addQueryPrefix` options, respectively (#213)
43
+ - [Fix] Handle stringifying empty objects with addQueryPrefix (#217)
44
+ - [Fix] do not mutate `options` argument (#207)
45
+ - [Refactor] `parse`: cache index to reuse in else statement (#182)
46
+ - [Docs] add various badges to readme (#208)
47
+ - [Dev Deps] update `eslint`, `browserify`, `iconv-lite`, `tape`
48
+ - [Tests] up to `node` `v8.1`, `v7.10`, `v6.11`; npm v4.6 breaks on node < v1; npm v5+ breaks on node < v4
49
+ - [Tests] add `editorconfig-tools`
50
+
22
51
  ## **6.4.0**
23
52
  - [New] `qs.stringify`: add `encodeValuesOnly` option
24
53
  - [Fix] follow `allowPrototypes` option during merge (#201, #201)
@@ -28,6 +57,13 @@
28
57
  - [Tests] up to `node` `v7.7`, `v6.10`,` v4.8`; disable osx builds since they block linux builds
29
58
  - [eslint] reduce warnings
30
59
 
60
+ ## **6.3.2**
61
+ - [Fix] follow `allowPrototypes` option during merge (#201, #200)
62
+ - [Dev Deps] update `eslint`
63
+ - [Fix] chmod a-x
64
+ - [Fix] support keys starting with brackets (#202, #200)
65
+ - [Tests] up to `node` `v7.7`, `v6.10`,` v4.8`; disable osx builds since they block linux builds
66
+
31
67
  ## **6.3.1**
32
68
  - [Fix] ensure that `allowPrototypes: false` does not ever shadow Object.prototype properties (thanks, @snyk!)
33
69
  - [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `browserify`, `iconv-lite`, `qs-iconv`, `tape`
@@ -54,6 +90,12 @@
54
90
  - [Tests] skip Object.create tests when null objects are not available
55
91
  - [Tests] Turn on eslint for test files (#175)
56
92
 
93
+ ## **6.2.3**
94
+ - [Fix] follow `allowPrototypes` option during merge (#201, #200)
95
+ - [Fix] chmod a-x
96
+ - [Fix] support keys starting with brackets (#202, #200)
97
+ - [Tests] up to `node` `v7.7`, `v6.10`,` v4.8`; disable osx builds since they block linux builds
98
+
57
99
  ## **6.2.2**
58
100
  - [Fix] ensure that `allowPrototypes: false` does not ever shadow Object.prototype properties
59
101
 
@@ -69,6 +111,12 @@
69
111
  - [New] add "encoder" and "decoder" options, for custom param encoding/decoding (#160)
70
112
  - [Fix] fix compacting of nested sparse arrays (#150)
71
113
 
114
+ ## **6.1.2
115
+ - [Fix] follow `allowPrototypes` option during merge (#201, #200)
116
+ - [Fix] chmod a-x
117
+ - [Fix] support keys starting with brackets (#202, #200)
118
+ - [Tests] up to `node` `v7.7`, `v6.10`,` v4.8`; disable osx builds since they block linux builds
119
+
72
120
  ## **6.1.1**
73
121
  - [Fix] ensure that `allowPrototypes: false` does not ever shadow Object.prototype properties
74
122
 
@@ -77,6 +125,12 @@
77
125
  - [Fix] "sort" option should work at a depth of 3 or more (#151)
78
126
  - [Fix] Restore `dist` directory; will be removed in v7 (#148)
79
127
 
128
+ ## **6.0.4**
129
+ - [Fix] follow `allowPrototypes` option during merge (#201, #200)
130
+ - [Fix] chmod a-x
131
+ - [Fix] support keys starting with brackets (#202, #200)
132
+ - [Tests] up to `node` `v7.7`, `v6.10`,` v4.8`; disable osx builds since they block linux builds
133
+
80
134
  ## **6.0.3**
81
135
  - [Fix] ensure that `allowPrototypes: false` does not ever shadow Object.prototype properties
82
136
  - [Fix] Restore `dist` directory; will be removed in v7 (#148)
package/README.md CHANGED
@@ -11,8 +11,6 @@
11
11
 
12
12
  A querystring parsing and stringifying library with some added security.
13
13
 
14
- [![Build Status](https://api.travis-ci.org/ljharb/qs.svg)](http://travis-ci.org/ljharb/qs)
15
-
16
14
  Lead Maintainer: [Jordan Harband](https://github.com/ljharb)
17
15
 
18
16
  The **qs** module was originally created and maintained by [TJ Holowaychuk](https://github.com/visionmedia/node-querystring).
@@ -42,9 +40,9 @@ For example, the string `'foo[bar]=baz'` converts to:
42
40
 
43
41
  ```javascript
44
42
  assert.deepEqual(qs.parse('foo[bar]=baz'), {
45
- foo: {
46
- bar: 'baz'
47
- }
43
+ foo: {
44
+ bar: 'baz'
45
+ }
48
46
  });
49
47
  ```
50
48
 
@@ -66,7 +64,7 @@ URI encoded strings work too:
66
64
 
67
65
  ```javascript
68
66
  assert.deepEqual(qs.parse('a%5Bb%5D=c'), {
69
- a: { b: 'c' }
67
+ a: { b: 'c' }
70
68
  });
71
69
  ```
72
70
 
@@ -74,11 +72,11 @@ You can also nest your objects, like `'foo[bar][baz]=foobarbaz'`:
74
72
 
75
73
  ```javascript
76
74
  assert.deepEqual(qs.parse('foo[bar][baz]=foobarbaz'), {
77
- foo: {
78
- bar: {
79
- baz: 'foobarbaz'
75
+ foo: {
76
+ bar: {
77
+ baz: 'foobarbaz'
78
+ }
80
79
  }
81
- }
82
80
  });
83
81
  ```
84
82
 
@@ -87,19 +85,19 @@ By default, when nesting objects **qs** will only parse up to 5 children deep. T
87
85
 
88
86
  ```javascript
89
87
  var expected = {
90
- a: {
91
- b: {
92
- c: {
93
- d: {
94
- e: {
95
- f: {
96
- '[g][h][i]': 'j'
88
+ a: {
89
+ b: {
90
+ c: {
91
+ d: {
92
+ e: {
93
+ f: {
94
+ '[g][h][i]': 'j'
95
+ }
96
+ }
97
+ }
97
98
  }
98
- }
99
99
  }
100
- }
101
100
  }
102
- }
103
101
  };
104
102
  var string = 'a[b][c][d][e][f][g][h][i]=j';
105
103
  assert.deepEqual(qs.parse(string), expected);
@@ -121,6 +119,13 @@ var limited = qs.parse('a=b&c=d', { parameterLimit: 1 });
121
119
  assert.deepEqual(limited, { a: 'b' });
122
120
  ```
123
121
 
122
+ To bypass the leading question mark, use `ignoreQueryPrefix`:
123
+
124
+ ```javascript
125
+ var prefixed = qs.parse('?a=b&c=d', { ignoreQueryPrefix: true });
126
+ assert.deepEqual(prefixed, { a: 'b', c: 'd' });
127
+ ```
128
+
124
129
  An optional delimiter can also be passed:
125
130
 
126
131
  ```javascript
@@ -236,10 +241,10 @@ assert.equal(unencoded, 'a[b]=c');
236
241
 
237
242
  Encoding can be disabled for keys by setting the `encodeValuesOnly` option to `true`:
238
243
  ```javascript
239
- var encodedValues = qs.stringify(
240
- { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
241
- { encodeValuesOnly: true }
242
- )
244
+ var encodedValues = qs.stringify(
245
+ { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
246
+ { encodeValuesOnly: true }
247
+ );
243
248
  assert.equal(encodedValues,'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h');
244
249
  ```
245
250
 
@@ -247,8 +252,8 @@ This encoding can also be replaced by a custom encoding method set as `encoder`
247
252
 
248
253
  ```javascript
249
254
  var encoded = qs.stringify({ a: { b: 'c' } }, { encoder: function (str) {
250
- // Passed in values `a`, `b`, `c`
251
- return // Return encoded string
255
+ // Passed in values `a`, `b`, `c`
256
+ return // Return encoded string
252
257
  }})
253
258
  ```
254
259
 
@@ -258,8 +263,8 @@ Analogue to the `encoder` there is a `decoder` option for `parse` to override de
258
263
 
259
264
  ```javascript
260
265
  var decoded = qs.parse('x=z', { decoder: function (str) {
261
- // Passed in values `x`, `z`
262
- return // Return decoded string
266
+ // Passed in values `x`, `z`
267
+ return // Return decoded string
263
268
  }})
264
269
  ```
265
270
 
@@ -350,6 +355,12 @@ Properties that are set to `undefined` will be omitted entirely:
350
355
  assert.equal(qs.stringify({ a: null, b: undefined }), 'a=');
351
356
  ```
352
357
 
358
+ The query string may optionally be prepended with a question mark:
359
+
360
+ ```javascript
361
+ assert.equal(qs.stringify({ a: 'b', c: 'd' }, { addQueryPrefix: true }), '?a=b&c=d');
362
+ ```
363
+
353
364
  The delimiter may be overridden with stringify as well:
354
365
 
355
366
  ```javascript
@@ -371,7 +382,7 @@ You may use the `sort` option to affect the order of parameter keys:
371
382
 
372
383
  ```javascript
373
384
  function alphabeticalSort(a, b) {
374
- return a.localeCompare(b);
385
+ return a.localeCompare(b);
375
386
  }
376
387
  assert.equal(qs.stringify({ a: 'c', z: 'y', b : 'f' }, { sort: alphabeticalSort }), 'a=c&b=f&z=y');
377
388
  ```
@@ -382,17 +393,17 @@ pass an array, it will be used to select properties and array indices for string
382
393
 
383
394
  ```javascript
384
395
  function filterFunc(prefix, value) {
385
- if (prefix == 'b') {
386
- // Return an `undefined` value to omit a property.
387
- return;
388
- }
389
- if (prefix == 'e[f]') {
390
- return value.getTime();
391
- }
392
- if (prefix == 'e[g][0]') {
393
- return value * 2;
394
- }
395
- return value;
396
+ if (prefix == 'b') {
397
+ // Return an `undefined` value to omit a property.
398
+ return;
399
+ }
400
+ if (prefix == 'e[f]') {
401
+ return value.getTime();
402
+ }
403
+ if (prefix == 'e[g][0]') {
404
+ return value * 2;
405
+ }
406
+ return value;
396
407
  }
397
408
  qs.stringify({ a: 'b', c: 'd', e: { f: new Date(123), g: [2] } }, { filter: filterFunc });
398
409
  // 'a=b&c=d&e[f]=123&e[g][0]=4'
package/component.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "qs",
3
- "repository": "hapijs/qs",
3
+ "repository": "ljharb/qs",
4
4
  "description": "query-string parser / stringifier with nesting support",
5
- "version": "6.4.1",
5
+ "version": "6.5.3",
6
6
  "keywords": ["querystring", "query", "parser"],
7
7
  "main": "lib/index.js",
8
8
  "scripts": [
package/dist/qs.js CHANGED
@@ -52,19 +52,23 @@ var defaults = {
52
52
 
53
53
  var parseValues = function parseQueryStringValues(str, options) {
54
54
  var obj = {};
55
- var parts = str.split(options.delimiter, options.parameterLimit === Infinity ? undefined : options.parameterLimit);
55
+ var cleanStr = options.ignoreQueryPrefix ? str.replace(/^\?/, '') : str;
56
+ var limit = options.parameterLimit === Infinity ? undefined : options.parameterLimit;
57
+ var parts = cleanStr.split(options.delimiter, limit);
56
58
 
57
59
  for (var i = 0; i < parts.length; ++i) {
58
60
  var part = parts[i];
59
- var pos = part.indexOf(']=') === -1 ? part.indexOf('=') : part.indexOf(']=') + 1;
61
+
62
+ var bracketEqualsPos = part.indexOf(']=');
63
+ var pos = bracketEqualsPos === -1 ? part.indexOf('=') : bracketEqualsPos + 1;
60
64
 
61
65
  var key, val;
62
66
  if (pos === -1) {
63
- key = options.decoder(part);
67
+ key = options.decoder(part, defaults.decoder);
64
68
  val = options.strictNullHandling ? null : '';
65
69
  } else {
66
- key = options.decoder(part.slice(0, pos));
67
- val = options.decoder(part.slice(pos + 1));
70
+ key = options.decoder(part.slice(0, pos), defaults.decoder);
71
+ val = options.decoder(part.slice(pos + 1), defaults.decoder);
68
72
  }
69
73
  if (has.call(obj, key)) {
70
74
  obj[key] = [].concat(obj[key]).concat(val);
@@ -76,38 +80,39 @@ var parseValues = function parseQueryStringValues(str, options) {
76
80
  return obj;
77
81
  };
78
82
 
79
- var parseObject = function parseObjectRecursive(chain, val, options) {
80
- if (!chain.length) {
81
- return val;
82
- }
83
+ var parseObject = function (chain, val, options) {
84
+ var leaf = val;
83
85
 
84
- var root = chain.shift();
86
+ for (var i = chain.length - 1; i >= 0; --i) {
87
+ var obj;
88
+ var root = chain[i];
85
89
 
86
- var obj;
87
- if (root === '[]' && options.parseArrays) {
88
- obj = [];
89
- obj = obj.concat(parseObject(chain, val, options));
90
- } else {
91
- obj = options.plainObjects ? Object.create(null) : {};
92
- var cleanRoot = root.charAt(0) === '[' && root.charAt(root.length - 1) === ']' ? root.slice(1, -1) : root;
93
- var index = parseInt(cleanRoot, 10);
94
- if (!options.parseArrays && cleanRoot === '') {
95
- obj = { 0: val };
96
- } else if (
97
- !isNaN(index)
98
- && root !== cleanRoot
99
- && String(index) === cleanRoot
100
- && index >= 0
101
- && (options.parseArrays && index <= options.arrayLimit)
102
- ) {
103
- obj = [];
104
- obj[index] = parseObject(chain, val, options);
105
- } else if (cleanRoot !== '__proto__') {
106
- obj[cleanRoot] = parseObject(chain, val, options);
90
+ if (root === '[]' && options.parseArrays) {
91
+ obj = [].concat(leaf);
92
+ } else {
93
+ obj = options.plainObjects ? Object.create(null) : {};
94
+ var cleanRoot = root.charAt(0) === '[' && root.charAt(root.length - 1) === ']' ? root.slice(1, -1) : root;
95
+ var index = parseInt(cleanRoot, 10);
96
+ if (!options.parseArrays && cleanRoot === '') {
97
+ obj = { 0: leaf };
98
+ } else if (
99
+ !isNaN(index)
100
+ && root !== cleanRoot
101
+ && String(index) === cleanRoot
102
+ && index >= 0
103
+ && (options.parseArrays && index <= options.arrayLimit)
104
+ ) {
105
+ obj = [];
106
+ obj[index] = leaf;
107
+ } else if (cleanRoot !== '__proto__') {
108
+ obj[cleanRoot] = leaf;
109
+ }
107
110
  }
111
+
112
+ leaf = obj;
108
113
  }
109
114
 
110
- return obj;
115
+ return leaf;
111
116
  };
112
117
 
113
118
  var parseKeys = function parseQueryStringKeys(givenKey, val, options) {
@@ -132,7 +137,8 @@ var parseKeys = function parseQueryStringKeys(givenKey, val, options) {
132
137
 
133
138
  var keys = [];
134
139
  if (parent) {
135
- // If we aren't using plain objects, optionally prefix keys that would overwrite object prototype properties
140
+ // If we aren't using plain objects, optionally prefix keys
141
+ // that would overwrite object prototype properties
136
142
  if (!options.plainObjects && has.call(Object.prototype, parent)) {
137
143
  if (!options.allowPrototypes) {
138
144
  return;
@@ -165,12 +171,13 @@ var parseKeys = function parseQueryStringKeys(givenKey, val, options) {
165
171
  };
166
172
 
167
173
  module.exports = function (str, opts) {
168
- var options = opts || {};
174
+ var options = opts ? utils.assign({}, opts) : {};
169
175
 
170
176
  if (options.decoder !== null && options.decoder !== undefined && typeof options.decoder !== 'function') {
171
177
  throw new TypeError('Decoder has to be a function.');
172
178
  }
173
179
 
180
+ options.ignoreQueryPrefix = options.ignoreQueryPrefix === true;
174
181
  options.delimiter = typeof options.delimiter === 'string' || utils.isRegExp(options.delimiter) ? options.delimiter : defaults.delimiter;
175
182
  options.depth = typeof options.depth === 'number' ? options.depth : defaults.depth;
176
183
  options.arrayLimit = typeof options.arrayLimit === 'number' ? options.arrayLimit : defaults.arrayLimit;
@@ -262,7 +269,7 @@ var stringify = function stringify(
262
269
 
263
270
  if (obj === null) {
264
271
  if (strictNullHandling) {
265
- return encoder && !encodeValuesOnly ? encoder(prefix) : prefix;
272
+ return encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder) : prefix;
266
273
  }
267
274
 
268
275
  obj = '';
@@ -270,8 +277,8 @@ var stringify = function stringify(
270
277
 
271
278
  if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean' || utils.isBuffer(obj)) {
272
279
  if (encoder) {
273
- var keyValue = encodeValuesOnly ? prefix : encoder(prefix);
274
- return [formatter(keyValue) + '=' + formatter(encoder(obj))];
280
+ var keyValue = encodeValuesOnly ? prefix : encoder(prefix, defaults.encoder);
281
+ return [formatter(keyValue) + '=' + formatter(encoder(obj, defaults.encoder))];
275
282
  }
276
283
  return [formatter(prefix) + '=' + formatter(String(obj))];
277
284
  }
@@ -335,7 +342,7 @@ var stringify = function stringify(
335
342
 
336
343
  module.exports = function (object, opts) {
337
344
  var obj = object;
338
- var options = opts || {};
345
+ var options = opts ? utils.assign({}, opts) : {};
339
346
 
340
347
  if (options.encoder !== null && typeof options.encoder !== 'undefined' && typeof options.encoder !== 'function') {
341
348
  throw new TypeError('Encoder has to be a function.');
@@ -414,7 +421,10 @@ module.exports = function (object, opts) {
414
421
  ));
415
422
  }
416
423
 
417
- return keys.join(delimiter);
424
+ var joined = keys.join(delimiter);
425
+ var prefix = options.addQueryPrefix === true ? '?' : '';
426
+
427
+ return joined.length > 0 ? prefix + joined : '';
418
428
  };
419
429
 
420
430
  },{"./formats":1,"./utils":5}],5:[function(require,module,exports){
@@ -431,7 +441,30 @@ var hexTable = (function () {
431
441
  return array;
432
442
  }());
433
443
 
434
- exports.arrayToObject = function (source, options) {
444
+ var compactQueue = function compactQueue(queue) {
445
+ var obj;
446
+
447
+ while (queue.length) {
448
+ var item = queue.pop();
449
+ obj = item.obj[item.prop];
450
+
451
+ if (Array.isArray(obj)) {
452
+ var compacted = [];
453
+
454
+ for (var j = 0; j < obj.length; ++j) {
455
+ if (typeof obj[j] !== 'undefined') {
456
+ compacted.push(obj[j]);
457
+ }
458
+ }
459
+
460
+ item.obj[item.prop] = compacted;
461
+ }
462
+ }
463
+
464
+ return obj;
465
+ };
466
+
467
+ var arrayToObject = function arrayToObject(source, options) {
435
468
  var obj = options && options.plainObjects ? Object.create(null) : {};
436
469
  for (var i = 0; i < source.length; ++i) {
437
470
  if (typeof source[i] !== 'undefined') {
@@ -442,7 +475,7 @@ exports.arrayToObject = function (source, options) {
442
475
  return obj;
443
476
  };
444
477
 
445
- exports.merge = function (target, source, options) {
478
+ var merge = function merge(target, source, options) {
446
479
  if (!source) {
447
480
  return target;
448
481
  }
@@ -467,14 +500,15 @@ exports.merge = function (target, source, options) {
467
500
 
468
501
  var mergeTarget = target;
469
502
  if (Array.isArray(target) && !Array.isArray(source)) {
470
- mergeTarget = exports.arrayToObject(target, options);
503
+ mergeTarget = arrayToObject(target, options);
471
504
  }
472
505
 
473
506
  if (Array.isArray(target) && Array.isArray(source)) {
474
507
  source.forEach(function (item, i) {
475
508
  if (has.call(target, i)) {
476
- if (target[i] && typeof target[i] === 'object') {
477
- target[i] = exports.merge(target[i], item, options);
509
+ var targetItem = target[i];
510
+ if (targetItem && typeof targetItem === 'object' && item && typeof item === 'object') {
511
+ target[i] = merge(targetItem, item, options);
478
512
  } else {
479
513
  target.push(item);
480
514
  }
@@ -488,8 +522,8 @@ exports.merge = function (target, source, options) {
488
522
  return Object.keys(source).reduce(function (acc, key) {
489
523
  var value = source[key];
490
524
 
491
- if (Object.prototype.hasOwnProperty.call(acc, key)) {
492
- acc[key] = exports.merge(acc[key], value, options);
525
+ if (has.call(acc, key)) {
526
+ acc[key] = merge(acc[key], value, options);
493
527
  } else {
494
528
  acc[key] = value;
495
529
  }
@@ -497,7 +531,14 @@ exports.merge = function (target, source, options) {
497
531
  }, mergeTarget);
498
532
  };
499
533
 
500
- exports.decode = function (str) {
534
+ var assign = function assignSingleSource(target, source) {
535
+ return Object.keys(source).reduce(function (acc, key) {
536
+ acc[key] = source[key];
537
+ return acc;
538
+ }, target);
539
+ };
540
+
541
+ var decode = function (str) {
501
542
  try {
502
543
  return decodeURIComponent(str.replace(/\+/g, ' '));
503
544
  } catch (e) {
@@ -505,7 +546,7 @@ exports.decode = function (str) {
505
546
  }
506
547
  };
507
548
 
508
- exports.encode = function (str) {
549
+ var encode = function encode(str) {
509
550
  // This code was originally written by Brian White (mscdex) for the io.js core querystring library.
510
551
  // It has been adapted here for stricter adherence to RFC 3986
511
552
  if (str.length === 0) {
@@ -558,46 +599,33 @@ exports.encode = function (str) {
558
599
  return out;
559
600
  };
560
601
 
561
- exports.compact = function (obj, references) {
562
- if (typeof obj !== 'object' || obj === null) {
563
- return obj;
564
- }
602
+ var compact = function compact(value) {
603
+ var queue = [{ obj: { o: value }, prop: 'o' }];
604
+ var refs = [];
565
605
 
566
- var refs = references || [];
567
- var lookup = refs.indexOf(obj);
568
- if (lookup !== -1) {
569
- return refs[lookup];
570
- }
606
+ for (var i = 0; i < queue.length; ++i) {
607
+ var item = queue[i];
608
+ var obj = item.obj[item.prop];
571
609
 
572
- refs.push(obj);
573
-
574
- if (Array.isArray(obj)) {
575
- var compacted = [];
576
-
577
- for (var i = 0; i < obj.length; ++i) {
578
- if (obj[i] && typeof obj[i] === 'object') {
579
- compacted.push(exports.compact(obj[i], refs));
580
- } else if (typeof obj[i] !== 'undefined') {
581
- compacted.push(obj[i]);
610
+ var keys = Object.keys(obj);
611
+ for (var j = 0; j < keys.length; ++j) {
612
+ var key = keys[j];
613
+ var val = obj[key];
614
+ if (typeof val === 'object' && val !== null && refs.indexOf(val) === -1) {
615
+ queue.push({ obj: obj, prop: key });
616
+ refs.push(val);
582
617
  }
583
618
  }
584
-
585
- return compacted;
586
619
  }
587
620
 
588
- var keys = Object.keys(obj);
589
- keys.forEach(function (key) {
590
- obj[key] = exports.compact(obj[key], refs);
591
- });
592
-
593
- return obj;
621
+ return compactQueue(queue);
594
622
  };
595
623
 
596
- exports.isRegExp = function (obj) {
624
+ var isRegExp = function isRegExp(obj) {
597
625
  return Object.prototype.toString.call(obj) === '[object RegExp]';
598
626
  };
599
627
 
600
- exports.isBuffer = function (obj) {
628
+ var isBuffer = function isBuffer(obj) {
601
629
  if (obj === null || typeof obj === 'undefined') {
602
630
  return false;
603
631
  }
@@ -605,5 +633,16 @@ exports.isBuffer = function (obj) {
605
633
  return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj));
606
634
  };
607
635
 
636
+ module.exports = {
637
+ arrayToObject: arrayToObject,
638
+ assign: assign,
639
+ compact: compact,
640
+ decode: decode,
641
+ encode: encode,
642
+ isBuffer: isBuffer,
643
+ isRegExp: isRegExp,
644
+ merge: merge
645
+ };
646
+
608
647
  },{}]},{},[2])(2)
609
648
  });
package/lib/parse.js CHANGED
@@ -18,19 +18,23 @@ var defaults = {
18
18
 
19
19
  var parseValues = function parseQueryStringValues(str, options) {
20
20
  var obj = {};
21
- var parts = str.split(options.delimiter, options.parameterLimit === Infinity ? undefined : options.parameterLimit);
21
+ var cleanStr = options.ignoreQueryPrefix ? str.replace(/^\?/, '') : str;
22
+ var limit = options.parameterLimit === Infinity ? undefined : options.parameterLimit;
23
+ var parts = cleanStr.split(options.delimiter, limit);
22
24
 
23
25
  for (var i = 0; i < parts.length; ++i) {
24
26
  var part = parts[i];
25
- var pos = part.indexOf(']=') === -1 ? part.indexOf('=') : part.indexOf(']=') + 1;
27
+
28
+ var bracketEqualsPos = part.indexOf(']=');
29
+ var pos = bracketEqualsPos === -1 ? part.indexOf('=') : bracketEqualsPos + 1;
26
30
 
27
31
  var key, val;
28
32
  if (pos === -1) {
29
- key = options.decoder(part);
33
+ key = options.decoder(part, defaults.decoder);
30
34
  val = options.strictNullHandling ? null : '';
31
35
  } else {
32
- key = options.decoder(part.slice(0, pos));
33
- val = options.decoder(part.slice(pos + 1));
36
+ key = options.decoder(part.slice(0, pos), defaults.decoder);
37
+ val = options.decoder(part.slice(pos + 1), defaults.decoder);
34
38
  }
35
39
  if (has.call(obj, key)) {
36
40
  obj[key] = [].concat(obj[key]).concat(val);
@@ -42,38 +46,39 @@ var parseValues = function parseQueryStringValues(str, options) {
42
46
  return obj;
43
47
  };
44
48
 
45
- var parseObject = function parseObjectRecursive(chain, val, options) {
46
- if (!chain.length) {
47
- return val;
48
- }
49
+ var parseObject = function (chain, val, options) {
50
+ var leaf = val;
49
51
 
50
- var root = chain.shift();
51
-
52
- var obj;
53
- if (root === '[]' && options.parseArrays) {
54
- obj = [];
55
- obj = obj.concat(parseObject(chain, val, options));
56
- } else {
57
- obj = options.plainObjects ? Object.create(null) : {};
58
- var cleanRoot = root.charAt(0) === '[' && root.charAt(root.length - 1) === ']' ? root.slice(1, -1) : root;
59
- var index = parseInt(cleanRoot, 10);
60
- if (!options.parseArrays && cleanRoot === '') {
61
- obj = { 0: val };
62
- } else if (
63
- !isNaN(index)
64
- && root !== cleanRoot
65
- && String(index) === cleanRoot
66
- && index >= 0
67
- && (options.parseArrays && index <= options.arrayLimit)
68
- ) {
69
- obj = [];
70
- obj[index] = parseObject(chain, val, options);
71
- } else if (cleanRoot !== '__proto__') {
72
- obj[cleanRoot] = parseObject(chain, val, options);
52
+ for (var i = chain.length - 1; i >= 0; --i) {
53
+ var obj;
54
+ var root = chain[i];
55
+
56
+ if (root === '[]' && options.parseArrays) {
57
+ obj = [].concat(leaf);
58
+ } else {
59
+ obj = options.plainObjects ? Object.create(null) : {};
60
+ var cleanRoot = root.charAt(0) === '[' && root.charAt(root.length - 1) === ']' ? root.slice(1, -1) : root;
61
+ var index = parseInt(cleanRoot, 10);
62
+ if (!options.parseArrays && cleanRoot === '') {
63
+ obj = { 0: leaf };
64
+ } else if (
65
+ !isNaN(index)
66
+ && root !== cleanRoot
67
+ && String(index) === cleanRoot
68
+ && index >= 0
69
+ && (options.parseArrays && index <= options.arrayLimit)
70
+ ) {
71
+ obj = [];
72
+ obj[index] = leaf;
73
+ } else if (cleanRoot !== '__proto__') {
74
+ obj[cleanRoot] = leaf;
75
+ }
73
76
  }
77
+
78
+ leaf = obj;
74
79
  }
75
80
 
76
- return obj;
81
+ return leaf;
77
82
  };
78
83
 
79
84
  var parseKeys = function parseQueryStringKeys(givenKey, val, options) {
@@ -98,7 +103,8 @@ var parseKeys = function parseQueryStringKeys(givenKey, val, options) {
98
103
 
99
104
  var keys = [];
100
105
  if (parent) {
101
- // If we aren't using plain objects, optionally prefix keys that would overwrite object prototype properties
106
+ // If we aren't using plain objects, optionally prefix keys
107
+ // that would overwrite object prototype properties
102
108
  if (!options.plainObjects && has.call(Object.prototype, parent)) {
103
109
  if (!options.allowPrototypes) {
104
110
  return;
@@ -131,12 +137,13 @@ var parseKeys = function parseQueryStringKeys(givenKey, val, options) {
131
137
  };
132
138
 
133
139
  module.exports = function (str, opts) {
134
- var options = opts || {};
140
+ var options = opts ? utils.assign({}, opts) : {};
135
141
 
136
142
  if (options.decoder !== null && options.decoder !== undefined && typeof options.decoder !== 'function') {
137
143
  throw new TypeError('Decoder has to be a function.');
138
144
  }
139
145
 
146
+ options.ignoreQueryPrefix = options.ignoreQueryPrefix === true;
140
147
  options.delimiter = typeof options.delimiter === 'string' || utils.isRegExp(options.delimiter) ? options.delimiter : defaults.delimiter;
141
148
  options.depth = typeof options.depth === 'number' ? options.depth : defaults.depth;
142
149
  options.arrayLimit = typeof options.arrayLimit === 'number' ? options.arrayLimit : defaults.arrayLimit;
package/lib/stringify.js CHANGED
@@ -58,7 +58,7 @@ var stringify = function stringify(
58
58
 
59
59
  if (obj === null) {
60
60
  if (strictNullHandling) {
61
- return encoder && !encodeValuesOnly ? encoder(prefix) : prefix;
61
+ return encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder) : prefix;
62
62
  }
63
63
 
64
64
  obj = '';
@@ -66,8 +66,8 @@ var stringify = function stringify(
66
66
 
67
67
  if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean' || utils.isBuffer(obj)) {
68
68
  if (encoder) {
69
- var keyValue = encodeValuesOnly ? prefix : encoder(prefix);
70
- return [formatter(keyValue) + '=' + formatter(encoder(obj))];
69
+ var keyValue = encodeValuesOnly ? prefix : encoder(prefix, defaults.encoder);
70
+ return [formatter(keyValue) + '=' + formatter(encoder(obj, defaults.encoder))];
71
71
  }
72
72
  return [formatter(prefix) + '=' + formatter(String(obj))];
73
73
  }
@@ -131,7 +131,7 @@ var stringify = function stringify(
131
131
 
132
132
  module.exports = function (object, opts) {
133
133
  var obj = object;
134
- var options = opts || {};
134
+ var options = opts ? utils.assign({}, opts) : {};
135
135
 
136
136
  if (options.encoder !== null && typeof options.encoder !== 'undefined' && typeof options.encoder !== 'function') {
137
137
  throw new TypeError('Encoder has to be a function.');
@@ -210,5 +210,8 @@ module.exports = function (object, opts) {
210
210
  ));
211
211
  }
212
212
 
213
- return keys.join(delimiter);
213
+ var joined = keys.join(delimiter);
214
+ var prefix = options.addQueryPrefix === true ? '?' : '';
215
+
216
+ return joined.length > 0 ? prefix + joined : '';
214
217
  };
package/lib/utils.js CHANGED
@@ -11,7 +11,30 @@ var hexTable = (function () {
11
11
  return array;
12
12
  }());
13
13
 
14
- exports.arrayToObject = function (source, options) {
14
+ var compactQueue = function compactQueue(queue) {
15
+ var obj;
16
+
17
+ while (queue.length) {
18
+ var item = queue.pop();
19
+ obj = item.obj[item.prop];
20
+
21
+ if (Array.isArray(obj)) {
22
+ var compacted = [];
23
+
24
+ for (var j = 0; j < obj.length; ++j) {
25
+ if (typeof obj[j] !== 'undefined') {
26
+ compacted.push(obj[j]);
27
+ }
28
+ }
29
+
30
+ item.obj[item.prop] = compacted;
31
+ }
32
+ }
33
+
34
+ return obj;
35
+ };
36
+
37
+ var arrayToObject = function arrayToObject(source, options) {
15
38
  var obj = options && options.plainObjects ? Object.create(null) : {};
16
39
  for (var i = 0; i < source.length; ++i) {
17
40
  if (typeof source[i] !== 'undefined') {
@@ -22,7 +45,7 @@ exports.arrayToObject = function (source, options) {
22
45
  return obj;
23
46
  };
24
47
 
25
- exports.merge = function (target, source, options) {
48
+ var merge = function merge(target, source, options) {
26
49
  if (!source) {
27
50
  return target;
28
51
  }
@@ -47,14 +70,15 @@ exports.merge = function (target, source, options) {
47
70
 
48
71
  var mergeTarget = target;
49
72
  if (Array.isArray(target) && !Array.isArray(source)) {
50
- mergeTarget = exports.arrayToObject(target, options);
73
+ mergeTarget = arrayToObject(target, options);
51
74
  }
52
75
 
53
76
  if (Array.isArray(target) && Array.isArray(source)) {
54
77
  source.forEach(function (item, i) {
55
78
  if (has.call(target, i)) {
56
- if (target[i] && typeof target[i] === 'object') {
57
- target[i] = exports.merge(target[i], item, options);
79
+ var targetItem = target[i];
80
+ if (targetItem && typeof targetItem === 'object' && item && typeof item === 'object') {
81
+ target[i] = merge(targetItem, item, options);
58
82
  } else {
59
83
  target.push(item);
60
84
  }
@@ -68,8 +92,8 @@ exports.merge = function (target, source, options) {
68
92
  return Object.keys(source).reduce(function (acc, key) {
69
93
  var value = source[key];
70
94
 
71
- if (Object.prototype.hasOwnProperty.call(acc, key)) {
72
- acc[key] = exports.merge(acc[key], value, options);
95
+ if (has.call(acc, key)) {
96
+ acc[key] = merge(acc[key], value, options);
73
97
  } else {
74
98
  acc[key] = value;
75
99
  }
@@ -77,7 +101,14 @@ exports.merge = function (target, source, options) {
77
101
  }, mergeTarget);
78
102
  };
79
103
 
80
- exports.decode = function (str) {
104
+ var assign = function assignSingleSource(target, source) {
105
+ return Object.keys(source).reduce(function (acc, key) {
106
+ acc[key] = source[key];
107
+ return acc;
108
+ }, target);
109
+ };
110
+
111
+ var decode = function (str) {
81
112
  try {
82
113
  return decodeURIComponent(str.replace(/\+/g, ' '));
83
114
  } catch (e) {
@@ -85,7 +116,7 @@ exports.decode = function (str) {
85
116
  }
86
117
  };
87
118
 
88
- exports.encode = function (str) {
119
+ var encode = function encode(str) {
89
120
  // This code was originally written by Brian White (mscdex) for the io.js core querystring library.
90
121
  // It has been adapted here for stricter adherence to RFC 3986
91
122
  if (str.length === 0) {
@@ -138,49 +169,47 @@ exports.encode = function (str) {
138
169
  return out;
139
170
  };
140
171
 
141
- exports.compact = function (obj, references) {
142
- if (typeof obj !== 'object' || obj === null) {
143
- return obj;
144
- }
145
-
146
- var refs = references || [];
147
- var lookup = refs.indexOf(obj);
148
- if (lookup !== -1) {
149
- return refs[lookup];
150
- }
151
-
152
- refs.push(obj);
153
-
154
- if (Array.isArray(obj)) {
155
- var compacted = [];
156
-
157
- for (var i = 0; i < obj.length; ++i) {
158
- if (obj[i] && typeof obj[i] === 'object') {
159
- compacted.push(exports.compact(obj[i], refs));
160
- } else if (typeof obj[i] !== 'undefined') {
161
- compacted.push(obj[i]);
172
+ var compact = function compact(value) {
173
+ var queue = [{ obj: { o: value }, prop: 'o' }];
174
+ var refs = [];
175
+
176
+ for (var i = 0; i < queue.length; ++i) {
177
+ var item = queue[i];
178
+ var obj = item.obj[item.prop];
179
+
180
+ var keys = Object.keys(obj);
181
+ for (var j = 0; j < keys.length; ++j) {
182
+ var key = keys[j];
183
+ var val = obj[key];
184
+ if (typeof val === 'object' && val !== null && refs.indexOf(val) === -1) {
185
+ queue.push({ obj: obj, prop: key });
186
+ refs.push(val);
162
187
  }
163
188
  }
164
-
165
- return compacted;
166
189
  }
167
190
 
168
- var keys = Object.keys(obj);
169
- keys.forEach(function (key) {
170
- obj[key] = exports.compact(obj[key], refs);
171
- });
172
-
173
- return obj;
191
+ return compactQueue(queue);
174
192
  };
175
193
 
176
- exports.isRegExp = function (obj) {
194
+ var isRegExp = function isRegExp(obj) {
177
195
  return Object.prototype.toString.call(obj) === '[object RegExp]';
178
196
  };
179
197
 
180
- exports.isBuffer = function (obj) {
198
+ var isBuffer = function isBuffer(obj) {
181
199
  if (obj === null || typeof obj === 'undefined') {
182
200
  return false;
183
201
  }
184
202
 
185
203
  return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj));
186
204
  };
205
+
206
+ module.exports = {
207
+ arrayToObject: arrayToObject,
208
+ assign: assign,
209
+ compact: compact,
210
+ decode: decode,
211
+ encode: encode,
212
+ isBuffer: isBuffer,
213
+ isRegExp: isRegExp,
214
+ merge: merge
215
+ };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "qs",
3
3
  "description": "A querystring parser that supports nesting and arrays, with a depth limit",
4
4
  "homepage": "https://github.com/ljharb/qs",
5
- "version": "6.4.1",
5
+ "version": "6.5.3",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/ljharb/qs.git"
package/test/parse.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  var test = require('tape');
4
4
  var qs = require('../');
5
+ var utils = require('../lib/utils');
5
6
  var iconv = require('iconv-lite');
6
7
  var SaferBuffer = require('safer-buffer').Buffer;
7
8
 
@@ -236,6 +237,14 @@ test('parse()', function (t) {
236
237
  st.end();
237
238
  });
238
239
 
240
+ t.test('parses jquery-param strings', function (st) {
241
+ // readable = 'filter[0][]=int1&filter[0][]==&filter[0][]=77&filter[]=and&filter[2][]=int2&filter[2][]==&filter[2][]=8'
242
+ var encoded = 'filter%5B0%5D%5B%5D=int1&filter%5B0%5D%5B%5D=%3D&filter%5B0%5D%5B%5D=77&filter%5B%5D=and&filter%5B2%5D%5B%5D=int2&filter%5B2%5D%5B%5D=%3D&filter%5B2%5D%5B%5D=8';
243
+ var expected = { filter: [['int1', '=', '77'], 'and', ['int2', '=', '8']] };
244
+ st.deepEqual(qs.parse(encoded), expected);
245
+ st.end();
246
+ });
247
+
239
248
  t.test('continues parsing when no parent is found', function (st) {
240
249
  st.deepEqual(qs.parse('[]=&a=b'), { 0: '', a: 'b' });
241
250
  st.deepEqual(qs.parse('[]&a=b', { strictNullHandling: true }), { 0: null, a: 'b' });
@@ -312,6 +321,13 @@ test('parse()', function (t) {
312
321
  st.end();
313
322
  });
314
323
 
324
+ t.test('allows for query string prefix', function (st) {
325
+ st.deepEqual(qs.parse('?foo=bar', { ignoreQueryPrefix: true }), { foo: 'bar' });
326
+ st.deepEqual(qs.parse('foo=bar', { ignoreQueryPrefix: true }), { foo: 'bar' });
327
+ st.deepEqual(qs.parse('?foo=bar', { ignoreQueryPrefix: false }), { '?foo': 'bar' });
328
+ st.end();
329
+ });
330
+
315
331
  t.test('parses an object', function (st) {
316
332
  var input = {
317
333
  'user[name]': { 'pop[bob]': 3 },
@@ -396,6 +412,33 @@ test('parse()', function (t) {
396
412
  st.end();
397
413
  });
398
414
 
415
+ t.test('does not crash when parsing deep objects', function (st) {
416
+ var parsed;
417
+ var str = 'foo';
418
+
419
+ for (var i = 0; i < 5000; i++) {
420
+ str += '[p]';
421
+ }
422
+
423
+ str += '=bar';
424
+
425
+ st.doesNotThrow(function () {
426
+ parsed = qs.parse(str, { depth: 5000 });
427
+ });
428
+
429
+ st.equal('foo' in parsed, true, 'parsed has "foo" property');
430
+
431
+ var depth = 0;
432
+ var ref = parsed.foo;
433
+ while ((ref = ref.p)) {
434
+ depth += 1;
435
+ }
436
+
437
+ st.equal(depth, 5000, 'parsed is 5000 properties deep');
438
+
439
+ st.end();
440
+ });
441
+
399
442
  t.test('parses null objects correctly', { skip: !Object.create }, function (st) {
400
443
  var a = Object.create(null);
401
444
  a.b = 'c';
@@ -572,16 +615,35 @@ test('parse()', function (t) {
572
615
  result.push(parseInt(parts[1], 16));
573
616
  parts = reg.exec(str);
574
617
  }
575
- return iconv.decode(SaferBuffer.from(result), 'shift_jis').toString();
618
+ return String(iconv.decode(SaferBuffer.from(result), 'shift_jis'));
576
619
  }
577
620
  }), { 県: '大阪府' });
578
621
  st.end();
579
622
  });
580
623
 
624
+ t.test('receives the default decoder as a second argument', function (st) {
625
+ st.plan(1);
626
+ qs.parse('a', {
627
+ decoder: function (str, defaultDecoder) {
628
+ st.equal(defaultDecoder, utils.decode);
629
+ }
630
+ });
631
+ st.end();
632
+ });
633
+
581
634
  t.test('throws error with wrong decoder', function (st) {
582
635
  st['throws'](function () {
583
636
  qs.parse({}, { decoder: 'string' });
584
637
  }, new TypeError('Decoder has to be a function.'));
585
638
  st.end();
586
639
  });
640
+
641
+ t.test('does not mutate the options argument', function (st) {
642
+ var options = {};
643
+ qs.parse('a[b]=true', options);
644
+ st.deepEqual(options, {});
645
+ st.end();
646
+ });
647
+
648
+ t.end();
587
649
  });
package/test/stringify.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  var test = require('tape');
4
4
  var qs = require('../');
5
+ var utils = require('../lib/utils');
5
6
  var iconv = require('iconv-lite');
6
7
  var SaferBuffer = require('safer-buffer').Buffer;
7
8
 
@@ -18,6 +19,32 @@ test('stringify()', function (t) {
18
19
  st.end();
19
20
  });
20
21
 
22
+ t.test('stringifies falsy values', function (st) {
23
+ st.equal(qs.stringify(undefined), '');
24
+ st.equal(qs.stringify(null), '');
25
+ st.equal(qs.stringify(null, { strictNullHandling: true }), '');
26
+ st.equal(qs.stringify(false), '');
27
+ st.equal(qs.stringify(0), '');
28
+ st.end();
29
+ });
30
+
31
+ t.test('adds query prefix', function (st) {
32
+ st.equal(qs.stringify({ a: 'b' }, { addQueryPrefix: true }), '?a=b');
33
+ st.end();
34
+ });
35
+
36
+ t.test('with query prefix, outputs blank string given an empty object', function (st) {
37
+ st.equal(qs.stringify({}, { addQueryPrefix: true }), '');
38
+ st.end();
39
+ });
40
+
41
+ t.test('stringifies nested falsy values', function (st) {
42
+ st.equal(qs.stringify({ a: { b: { c: null } } }), 'a%5Bb%5D%5Bc%5D=');
43
+ st.equal(qs.stringify({ a: { b: { c: null } } }, { strictNullHandling: true }), 'a%5Bb%5D%5Bc%5D');
44
+ st.equal(qs.stringify({ a: { b: { c: false } } }), 'a%5Bb%5D%5Bc%5D=false');
45
+ st.end();
46
+ });
47
+
21
48
  t.test('stringifies a nested object', function (st) {
22
49
  st.equal(qs.stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c');
23
50
  st.equal(qs.stringify({ a: { b: { c: { d: 'e' } } } }), 'a%5Bb%5D%5Bc%5D%5Bd%5D=e');
@@ -453,6 +480,16 @@ test('stringify()', function (t) {
453
480
  st.end();
454
481
  });
455
482
 
483
+ t.test('receives the default encoder as a second argument', function (st) {
484
+ st.plan(2);
485
+ qs.stringify({ a: 1 }, {
486
+ encoder: function (str, defaultEncoder) {
487
+ st.equal(defaultEncoder, utils.encode);
488
+ }
489
+ });
490
+ st.end();
491
+ });
492
+
456
493
  t.test('throws error with wrong encoder', function (st) {
457
494
  st['throws'](function () {
458
495
  qs.stringify({}, { encoder: 'string' });
@@ -574,6 +611,13 @@ test('stringify()', function (t) {
574
611
  st.end();
575
612
  });
576
613
 
614
+ t.test('does not mutate the options argument', function (st) {
615
+ var options = {};
616
+ qs.stringify({}, options);
617
+ st.deepEqual(options, {});
618
+ st.end();
619
+ });
620
+
577
621
  t.test('strictNullHandling works with custom filter', function (st) {
578
622
  var filter = function (prefix, value) {
579
623
  return value;
package/test/utils.js CHANGED
@@ -25,5 +25,41 @@ test('merge()', function (t) {
25
25
  var noOptionsNonObjectSource = utils.merge({ foo: 'baz' }, 'bar');
26
26
  t.deepEqual(noOptionsNonObjectSource, { foo: 'baz', bar: true });
27
27
 
28
+ t.test(
29
+ 'avoids invoking array setters unnecessarily',
30
+ { skip: typeof Object.defineProperty !== 'function' },
31
+ function (st) {
32
+ var setCount = 0;
33
+ var getCount = 0;
34
+ var observed = [];
35
+ Object.defineProperty(observed, 0, {
36
+ get: function () {
37
+ getCount += 1;
38
+ return { bar: 'baz' };
39
+ },
40
+ set: function () { setCount += 1; }
41
+ });
42
+ utils.merge(observed, [null]);
43
+ st.equal(setCount, 0);
44
+ st.equal(getCount, 1);
45
+ observed[0] = observed[0]; // eslint-disable-line no-self-assign
46
+ st.equal(setCount, 1);
47
+ st.equal(getCount, 2);
48
+ st.end();
49
+ }
50
+ );
51
+
52
+ t.end();
53
+ });
54
+
55
+ test('assign()', function (t) {
56
+ var target = { a: 1, b: 2 };
57
+ var source = { b: 3, c: 4 };
58
+ var result = utils.assign(target, source);
59
+
60
+ t.equal(result, target, 'returns the target');
61
+ t.deepEqual(target, { a: 1, b: 3, c: 4 }, 'target and source are merged');
62
+ t.deepEqual(source, { b: 3, c: 4 }, 'source is untouched');
63
+
28
64
  t.end();
29
65
  });