parse-nested-form-data 1.0.0 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  <div align="center">
2
2
  <h1>parse-nested-form-data</h1>
3
3
 
4
- <p>A tiny node module for parsing FormData by name into objects and arrays</p>
4
+ <p>Parse FormData into nested objects and arrays, based on the field names</p>
5
5
  </div>
6
6
 
7
7
  ---
@@ -10,6 +10,7 @@
10
10
  [![Build Status][build-badge]][build]
11
11
  [![Code Coverage][coverage-badge]][coverage]
12
12
  [![version][version-badge]][package]
13
+ [![Size][size-badge]][size]
13
14
  [![downloads][downloads-badge]][npmtrends]
14
15
  [![MIT License][license-badge]][license]
15
16
  [![All Contributors][all-contributors-badge]](#contributors-)
@@ -17,33 +18,53 @@
17
18
  [![Code of Conduct][coc-badge]][coc]
18
19
  <!-- prettier-ignore-end -->
19
20
 
20
- ## The problem
21
-
22
- 1. You use Forms to upload data to your server (e.g. with remix-run)
23
- 2. You want a way to upload objects and arrays with booleans and numbers
24
- 3. You do not want to transfer the data per hand
21
+ A form sends flat name-value pairs. This library turns them into nested objects
22
+ and arrays, with numbers, booleans and `null`. The field names tell it how.
23
+
24
+ ```html
25
+ <form method="post">
26
+ <input name="user.name" value="Ada" />
27
+ <input name="+user.age" value="36" />
28
+ <input name="&user.admin" type="checkbox" checked />
29
+ <input name="tags[]" value="math" />
30
+ <input name="tags[]" value="code" />
31
+ </form>
32
+ ```
25
33
 
26
- ## This solution
34
+ ```ts
35
+ parseFormData(formData)
36
+ // {
37
+ // user: {name: 'Ada', age: 36, admin: true},
38
+ // tags: ['math', 'code'],
39
+ // }
40
+ ```
27
41
 
28
- A parse functions that uses the name prop to tells the parser how to create an
29
- object out of the values. You can tell the parser to do not parse empty values.
42
+ It's under 2 kB (minified and brotli-compressed), has no dependencies and works
43
+ in all modern JavaScript runtimes (see
44
+ [Supported runtimes](#supported-runtimes)).
30
45
 
31
46
  <!-- START doctoc generated TOC please keep comment here to allow auto update -->
32
47
  <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
33
48
 
34
- - [Installation](#installation)
35
- - [Name](#name)
36
- - [Boolean](#boolean)
37
- - [Number](#number)
49
+ - [Install](#install)
50
+ - [Usage](#usage)
51
+ - [Field names](#field-names)
52
+ - [Numbers](#numbers)
53
+ - [Booleans](#booleans)
38
54
  - [Null](#null)
39
- - [Array](#array)
40
- - [Object](#object)
41
- - [Basic Usage](#basic-usage)
42
- - [Transform values](#transform-values)
43
- - [Advanced usage](#advanced-usage)
44
- - [Options](#options)
45
- - [removeEmptyString: `[boolean]`](#removeemptystring-boolean)
46
- - [transformEntry: `(entry: [path: string, value: string | File], defaultTransform: DefaultTransform) => [string, string | JsonLeafValue]`](#transformentry-entry-path-string-value-string--file-defaulttransform-defaulttransform--string-string--jsonleafvalue)
55
+ - [Arrays](#arrays)
56
+ - [Objects](#objects)
57
+ - [Files](#files)
58
+ - [API](#api)
59
+ - [parseFormData(formData, options?)](#parseformdataformdata-options)
60
+ - [formData](#formdata)
61
+ - [options](#options)
62
+ - [removeEmptyString](#removeemptystring)
63
+ - [depth](#depth)
64
+ - [transformEntry](#transformentry)
65
+ - [Errors](#errors)
66
+ - [Security](#security)
67
+ - [Supported runtimes](#supported-runtimes)
47
68
  - [Issues](#issues)
48
69
  - [🐛 Bugs](#-bugs)
49
70
  - [💡 Feature Requests](#-feature-requests)
@@ -53,257 +74,377 @@ object out of the values. You can tell the parser to do not parse empty values.
53
74
 
54
75
  <!-- END doctoc generated TOC please keep comment here to allow auto update -->
55
76
 
56
- ## Installation
57
-
58
- This module is distributed via [npm][npm] which is bundled with [node][node] and
59
- should be installed as one of your project's `dependencies`:
77
+ ## Install
60
78
 
61
79
  ```sh
62
80
  npm install parse-nested-form-data
63
81
  ```
64
82
 
65
- ## Name
83
+ ## Usage
84
+
85
+ ```ts
86
+ import {parseFormData} from 'parse-nested-form-data'
87
+
88
+ const data = parseFormData(await request.formData())
89
+ ```
66
90
 
67
- The name prop is used to tell the parser how to create an object out of the
68
- values. The name prop is a string that can contain the following characters:
91
+ `parseFormData` reads each entry, puts its value at the place its name tells,
92
+ and returns a new object. The input stays unchanged.
69
93
 
70
- - `.`: to create a nested object
71
- - `[]`: to create an array (pushes value)
72
- - `[$order]`: to create an array (sets value at index and squashes array)
73
- - `&`: to transform the value to a boolean
74
- - `-`: to transform the value to null
75
- - `+`: to transform the value to a number
94
+ ## Field names
76
95
 
77
- ### Boolean
96
+ | In the name | Meaning | Name and value | Result |
97
+ | ---------------- | ------------------------------------ | -------------- | --------------- |
98
+ | `.key` | A key in an object | `a.b` = `x` | `{a: {b: 'x'}}` |
99
+ | `[key]` | A key in an object, same as `.key` | `a[b]` = `x` | `{a: {b: 'x'}}` |
100
+ | `[]` | Add the value to the end of an array | `a[]` = `x` | `{a: ['x']}` |
101
+ | `[0]`, `[1]`, … | Put the value at this array position | `a[0]` = `x` | `{a: ['x']}` |
102
+ | `+` at the start | Change the value to a number | `+a` = `36` | `{a: 36}` |
103
+ | `&` at the start | Change the value to a boolean | `&a` = `on` | `{a: true}` |
104
+ | `-` at the start | Change the value to `null` | `-a` = `x` | `{a: null}` |
78
105
 
79
- If the name prop starts with a `&` the value will be transformed to a boolean.
106
+ You can combine them, for example `+orders[0].items[].price`.
80
107
 
81
- True if the value has the following values:
108
+ In this README, the name without its `+`, `&` or `-` is the **path**. Each
109
+ `key`, `.key`, `[key]`, `[]` and `[0]` in it is a **path part**: `a.b[0]` has
110
+ three path parts.
82
111
 
83
- - `true`
84
- - `1`
85
- - `on`
112
+ ### Numbers
113
+
114
+ A name that starts with `+` changes the value with `Number(value)`. A value that
115
+ isn't a number gives `NaN`. An empty string gives `0`.
86
116
 
87
117
  ```ts
88
118
  const formData = new FormData()
89
- formData.append('&isTrue', 'true')
90
- formData.append('&isFalse', 'false')
91
- formData.append('&isTrue1', '1')
92
- formData.append('&isFalse1', '0')
93
- formData.append('&isTrue2', 'on')
94
- formData.append('&isFalse2', 'off')
119
+ formData.append('+a', '1')
120
+ formData.append('+b', '1.5')
121
+ formData.append('+c', 'x')
95
122
  parseFormData(formData)
96
- // {
97
- // isTrue: true,
98
- // isFalse: false,
99
- // isTrue1: true,
100
- // isFalse1: false,
101
- // isTrue2: true,
102
- // isFalse2: false,
103
- // }
123
+ // {a: 1, b: 1.5, c: NaN}
104
124
  ```
105
125
 
106
- ### Number
126
+ ### Booleans
107
127
 
108
- If the name prop starts with a `+` the value will be transformed to a number.
128
+ A name that starts with `&` changes the value to a boolean. The value `on`,
129
+ `true` or any number other than `0` gives `true`. All other values give `false`.
109
130
 
110
131
  ```ts
111
132
  const formData = new FormData()
112
- formData.append('+number', '1')
113
- formData.append('+number2', '1.1')
114
- formData.append('+number3', 'a')
133
+ formData.append('&a', 'on')
134
+ formData.append('&b', 'true')
135
+ formData.append('&c', '1')
136
+ formData.append('&d', 'off')
137
+ formData.append('&e', 'false')
138
+ formData.append('&f', '0')
115
139
  parseFormData(formData)
116
- // {
117
- // number: 1,
118
- // number2: 1.1,
119
- // number3: NaN,
120
- // }
140
+ // {a: true, b: true, c: true, d: false, e: false, f: false}
121
141
  ```
122
142
 
143
+ A checkbox sends `on` when it's checked. When it isn't checked, the browser
144
+ sends nothing, so the key is missing from the result.
145
+
123
146
  ### Null
124
147
 
125
- If the name prop starts with a `-` the value will be transformed to null.
148
+ A name that starts with `-` changes the value to `null`, whatever the value is.
126
149
 
127
150
  ```ts
128
151
  const formData = new FormData()
129
- formData.append('-null', 'null')
130
- formData.append('-ignored', 'ignored')
152
+ formData.append('-a', 'x')
131
153
  parseFormData(formData)
132
- // {
133
- // null: null,
134
- // ignored: null,
135
- // }
154
+ // {a: null}
136
155
  ```
137
156
 
138
- ### Array
157
+ ### Arrays
139
158
 
140
- If a path of the name prop ends with `[]` the value will be pushed to an array.
141
- If the path of the name prop ends with `[$order]` the value will be set at the
142
- index of the order. Cannot be mixed with `[]`.
159
+ `[]` adds the value to the end of the array, in the order of the entries:
143
160
 
144
161
  ```ts
145
162
  const formData = new FormData()
146
- formData.append('array[0]', '1')
147
- formData.append('array[1]', '2')
148
- formData.append('array[2]', '3')
149
- formData.append('array[3]', '4')
163
+ formData.append('a[]', '1')
164
+ formData.append('a[]', '2')
150
165
  parseFormData(formData)
151
- // {
152
- // array: ['1', '2', '3', '4'],
153
- // }
166
+ // {a: ['1', '2']}
154
167
  ```
155
168
 
169
+ `[0]`, `[1]`, … put the value at that position. The order of the entries doesn't
170
+ matter. Gaps are removed, so `a[1]` and `a[5]` give an array of two values:
171
+
156
172
  ```ts
157
173
  const formData = new FormData()
158
- formData.append('array[]', '1')
159
- formData.append('array[]', '2')
160
- formData.append('array[]', '3')
174
+ formData.append('a[5]', 'second')
175
+ formData.append('a[1]', 'first')
161
176
  parseFormData(formData)
162
- // {
163
- // array: ['1', '2', '3'],
164
- // }
177
+ // {a: ['first', 'second']}
165
178
  ```
166
179
 
167
- Also works with nested objects:
180
+ One array can't use both `[]` and `[0]`. That throws a `MixedArrayError`.
181
+
182
+ The largest position is `4294967294`, the largest index a JavaScript array can
183
+ hold. A larger number throws an `InvalidPathError`.
184
+
185
+ Arrays can hold objects and other arrays:
168
186
 
169
187
  ```ts
170
188
  const formData = new FormData()
171
- formData.append('array[0].a', '1')
172
- formData.append('array[1].a', '2')
173
- formData.append('array[2].a', '3')
189
+ formData.append('a[0].b', '1')
190
+ formData.append('a[1].b', '2')
191
+ formData.append('c[0][]', '3')
192
+ formData.append('c[0][]', '4')
193
+ formData.append('c[1][]', '5')
174
194
  parseFormData(formData)
175
- // {
176
- // array: [{a: '1'}, {a: '2'}, {a: '3'}],
177
- // }
195
+ // {a: [{b: '1'}, {b: '2'}], c: [['3', '4'], ['5']]}
178
196
  ```
179
197
 
180
- Also works with nested arrays:
198
+ ### Objects
199
+
200
+ `.key` and `[key]` mean the same thing, so `a.b` and `a[b]` are the same key.
201
+ Only digits in brackets (`[0]`) make an array. You can mix both forms with each
202
+ other and with arrays:
181
203
 
182
204
  ```ts
183
205
  const formData = new FormData()
184
- formData.append('array[0][]', '1')
185
- formData.append('array[0][]', '2')
186
- formData.append('array[1][]', '3')
187
- formData.append('array[1][]', '4')
206
+ formData.append('a[b]', '1')
207
+ formData.append('a.c[0].d', '2')
208
+ formData.append('a[c][1][d]', '3')
188
209
  parseFormData(formData)
189
- // {
190
- // array: [['1', '2'], ['3', '4']],
191
- // }
210
+ // {a: {b: '1', c: [{d: '2'}, {d: '3'}]}}
192
211
  ```
193
212
 
194
- ### Object
195
-
196
- If the name prop contains a `.` the value will be nested in an object.
213
+ The text in brackets is the key exactly as written. It can contain `.`, `[` and
214
+ `]`, if the brackets inside are balanced:
197
215
 
198
216
  ```ts
199
217
  const formData = new FormData()
200
- formData.append('object.a', '1')
201
- formData.append('object.b', '2')
218
+ formData.append('a[b.c]', '1')
219
+ formData.append('a[d[e]]', '2')
202
220
  parseFormData(formData)
203
- // {
204
- // object: {
205
- // a: '1',
206
- // b: '2',
207
- // },
208
- // }
221
+ // {a: {'b.c': '1', 'd[e]': '2'}}
209
222
  ```
210
223
 
211
- ## Basic Usage
224
+ Brackets in the wrong place don't cause an error:
225
+
226
+ - A `[` without a matching `]` stays in the key. `a[[0]` is the key `[0` in `a`.
227
+ `a[0` is the key `a[0`.
228
+ - A `]` without a matching `[` stays in the key. `a]b` is the key `a]b`.
229
+ - Text after `]` starts the next key. `a[0]b` is the same as `a[0].b`.
230
+ - Empty keys are skipped. `a..b` and `a.[b]` are the same as `a.b`, and `[a]` is
231
+ the same as `a`.
212
232
 
213
- ### Transform values
233
+ ### Files
234
+
235
+ `File` values stay as they are. This includes native files and files from a
236
+ polyfill such as `@remix-run/web-file`. A file is always the end of a path:
214
237
 
215
238
  ```ts
216
239
  const formData = new FormData()
217
- formData.append('+a', '1')
218
- formData.append('&b', 'true')
219
- formData.append('-c', 'null')
220
- formData.append('d', 'foo')
221
- parseFormData(formData, defaultTransform)
222
- // => {a: 1, b: true, c: null, d: 'foo'}
240
+ formData.append('avatar', new File(['...'], 'avatar.png'))
241
+ parseFormData(formData)
242
+ // {avatar: File}
243
+
244
+ formData.append('avatar.name', 'me.png')
245
+ parseFormData(formData)
246
+ // throws DuplicateKeyError('avatar')
223
247
  ```
224
248
 
225
- ## Advanced usage
249
+ ## API
250
+
251
+ ### parseFormData(formData, options?)
252
+
253
+ Returns a new object with the parsed values.
226
254
 
227
- Complex examples:
255
+ #### formData
256
+
257
+ Type: `Iterable<[string, string | File]>`
258
+
259
+ A `FormData` object, or any other list of `[name, value]` pairs, for example
260
+ `[['a.b', 'x']]` or a `URLSearchParams` object.
261
+
262
+ #### options
263
+
264
+ Type: `object`
265
+
266
+ ##### removeEmptyString
267
+
268
+ Type: `boolean`\
269
+ Default: `false`
270
+
271
+ Skip all entries whose value is an empty string (`''`).
228
272
 
229
273
  ```ts
230
274
  const formData = new FormData()
231
- formData.append('a[].b.c', '1')
232
- formData.append('a[].b.d', '2')
233
- formData.append('a[]', '3')
234
- formData.append('b[0]', '4')
235
- formData.append('+b[100]', '5')
236
- parseFormData(formData)
237
- // {
238
- // a: [
239
- // {
240
- // b: {
241
- // c: '1',
242
- // d: '2',
243
- // },
244
- // },
245
- // '3',
246
- // ],
247
- // b: ['4', 5],
248
- // }
275
+ formData.append('a', '')
276
+ formData.append('b', 'x')
277
+ parseFormData(formData, {removeEmptyString: true})
278
+ // {b: 'x'}
249
279
  ```
250
280
 
251
- ### Options
252
-
253
- The second argument is an option object with following properties:
281
+ ##### depth
254
282
 
255
- ### removeEmptyString: `[boolean]`
283
+ Type: `number`\
284
+ Default: `Infinity`
256
285
 
257
- _Default: `false`_
258
-
259
- If `true` empty strings will be removed from the result.
286
+ The maximum number of path parts in one path. All parts count, so `a.b[0]` and
287
+ `a[b][]` both have three. A longer path throws a [`DepthLimitError`](#errors).
288
+ The value must be a positive integer or `Infinity`. Other values throw a
289
+ `TypeError`.
260
290
 
261
291
  ```ts
262
292
  const formData = new FormData()
263
- formData.append('foo[1].foo', '')
264
- formData.append('foo[1].bar', 'test2')
265
- formData.append('foo[0].foo', 'test3')
266
- formData.append('foo[0].baz', '4')
293
+ formData.append('user.address.street', 'Main St')
294
+ parseFormData(formData, {depth: 3})
295
+ // {user: {address: {street: 'Main St'}}}
267
296
 
268
- parseFormData(formData, {removeEmptyString: true})
269
- // {foo: [{foo: 'test3', baz: '4'}, {bar: 'test2'}]}
297
+ formData.append('user.address.geo.lat', '52.5')
298
+ parseFormData(formData, {depth: 3})
299
+ // throws DepthLimitError('user.address.geo.lat')
270
300
  ```
271
301
 
272
- ### transformEntry: `(entry: [path: string, value: string | File], defaultTransform: DefaultTransform) => [string, string | JsonLeafValue]`
302
+ The parser can handle any depth. But a field like `a.a.a…` with thousands of
303
+ parts makes an object that crashes code that walks it recursively, for example
304
+ `JSON.stringify`, validators or deep clones. If the input comes from untrusted
305
+ clients, set a limit. `depth: 32` is enough for almost every form. Version 2.0
306
+ will change the default to a smaller number.
307
+
308
+ The depth is checked after `transformEntry`. An entry that `removeEmptyString`
309
+ skips is never checked.
310
+
311
+ ##### transformEntry
312
+
313
+ Type:
314
+ `(entry: [path: string, value: string | File], defaultTransform) => {path: string, value: string | number | boolean | null | File}`\
315
+ Default:
316
+ the built-in transform for `+`, `&` and `-`
317
+
318
+ Changes each entry before it's parsed. It gets the entry as `[name, value]` and
319
+ the built-in transform. It must return the path and the value for the result.
320
+ Call `defaultTransform(entry)` to keep `+`, `&` and `-` working:
273
321
 
274
322
  ```ts
275
323
  const formData = new FormData()
276
- formData.append('a', 'a')
277
- formData.append('b', 'b')
324
+ formData.append('a', 'x')
325
+ formData.append('+b', '1')
278
326
  parseFormData(formData, {
279
- transformEntry: ([path, value], defaultTransform) => {
327
+ transformEntry: (entry, defaultTransform) => {
328
+ const {path, value} = defaultTransform(entry)
280
329
  return {
281
330
  path,
282
- value:
283
- typeof value === 'string'
284
- ? value.toUpperCase()
285
- : defaultTransform(value),
331
+ value: typeof value === 'string' ? value.toUpperCase() : value,
286
332
  }
287
333
  },
288
334
  })
289
- // => {a: 'A', b: 'B'}
335
+ // {a: 'X', b: 1}
336
+ ```
337
+
338
+ ## Errors
339
+
340
+ `parseFormData` stops at the first error. All error classes are exported:
341
+
342
+ ```ts
343
+ import {
344
+ DepthLimitError,
345
+ DuplicateKeyError,
346
+ ForbiddenKeyError,
347
+ InvalidPathError,
348
+ MixedArrayError,
349
+ } from 'parse-nested-form-data'
290
350
  ```
291
351
 
352
+ | Error | Thrown when | Example names |
353
+ | ------------------- | ------------------------------------------------------------------------ | ---------------------------------------- |
354
+ | `DuplicateKeyError` | Two entries set the same path, or use a path part in two different ways. | `a` and `a`; `a` and `a.b`; `a.b`, `a[]` |
355
+ | `MixedArrayError` | One array uses both `[]` and `[0]`. | `a[]` and `a[0]` |
356
+ | `ForbiddenKeyError` | A path part is `__proto__`, `constructor` or `prototype`. | `__proto__.x`; `a[constructor]` |
357
+ | `DepthLimitError` | A path has more path parts than the `depth` option allows. | `a.b.c` with `depth: 2` |
358
+ | `InvalidPathError` | An array position is larger than `4294967294`. | `a[4294967295]` |
359
+ | `TypeError` | The `depth` option isn't a positive integer or `Infinity`. | `depth: 0` |
360
+
361
+ Each library error has a `key` property. It's the path up to the path part that
362
+ caused the error. For example, `a.b` = `y` and then `a.b[0]` = `x` throw
363
+ `DuplicateKeyError('a.b')`. In `ForbiddenKeyError`, `DepthLimitError` and
364
+ `InvalidPathError`, a `key` longer than 100 characters is cut to 100 characters
365
+ plus `...`.
366
+
367
+ `key` is the path after `transformEntry`, not the field name. So it doesn't have
368
+ the `+`, `&` or `-`: `+a.b.c` with `depth: 2` throws `DepthLimitError('a.b.c')`.
369
+ A custom `transformEntry` can change the path completely. If you use `key` to
370
+ find the form field, undo your own transform first.
371
+
372
+ The entries are parsed in order, so the error of an earlier entry comes first.
373
+ In one entry, the depth is checked first: a path that's too deep throws
374
+ `DepthLimitError`, even if it has `__proto__` in it.
375
+
376
+ ```ts
377
+ try {
378
+ parseFormData(formData)
379
+ } catch (error) {
380
+ if (error instanceof DuplicateKeyError) {
381
+ return badRequest(`The field ${error.key} is used more than once`)
382
+ }
383
+ throw error
384
+ }
385
+ ```
386
+
387
+ ## Security
388
+
389
+ `__proto__`, `constructor` and `prototype` could change `Object.prototype`. So a
390
+ path part with one of these names throws a `ForbiddenKeyError`, in every
391
+ position, with or without brackets:
392
+
393
+ ```ts
394
+ const formData = new FormData()
395
+ formData.append('user.__proto__.isAdmin', 'true')
396
+ parseFormData(formData)
397
+ // throws ForbiddenKeyError('user.__proto__')
398
+ ```
399
+
400
+ Other names that exist on `Object.prototype` at parse time (`toString`,
401
+ `hasOwnProperty`, …, and names that a newer runtime or other code adds) would
402
+ hide a built-in method of the result. So the parser removes every entry with
403
+ such a name in its path. It parses the entry like any other entry first, so the
404
+ entry causes the same errors as any other name. Because of this, an error
405
+ doesn't tell the client which properties exist on the server's
406
+ `Object.prototype`.
407
+
408
+ ```ts
409
+ const formData = new FormData()
410
+ formData.append('user.name', 'Ada')
411
+ formData.append('user.toString', 'x')
412
+ formData.append('settings.valueOf', 'x')
413
+ parseFormData(formData)
414
+ // {user: {name: 'Ada'}}
415
+ ```
416
+
417
+ For input from untrusted clients, also set the [`depth`](#depth) option.
418
+
419
+ ## Supported runtimes
420
+
421
+ The library uses only ES2020 and no runtime APIs. It works in:
422
+
423
+ - browsers
424
+ - Node.js (CI tests versions 22, 24 and 26)
425
+ - Deno
426
+ - Bun
427
+ - Cloudflare Workers
428
+ - Vercel (Edge and Node.js runtimes)
429
+
430
+ The package has ESM, CommonJS and UMD builds. In a `<script>` tag, the UMD build
431
+ sets the global `parseNestedFormData`.
432
+
292
433
  ## Issues
293
434
 
294
- _Looking to contribute? Look for the [Good First Issue][good-first-issue]
295
- label._
435
+ _Looking to contribute? Look for the [Good First Issue][good-first-issue] label,
436
+ and see [CONTRIBUTING.md](CONTRIBUTING.md) for how to set up the project._
296
437
 
297
438
  ### 🐛 Bugs
298
439
 
299
- Please file an issue for bugs, missing documentation, or unexpected behavior.
440
+ Please open an issue for bugs, missing documentation or unexpected behavior.
300
441
 
301
442
  [**See Bugs**][bugs]
302
443
 
303
444
  ### 💡 Feature Requests
304
445
 
305
- Please file an issue to suggest new features. Vote on feature requests by adding
306
- a 👍. This helps maintainers prioritize what to work on.
446
+ Please open an issue to suggest new features. Vote on feature requests with a
447
+ 👍. This helps maintainers decide what to work on.
307
448
 
308
449
  [**See Feature Requests**][requests]
309
450
 
@@ -337,22 +478,22 @@ Special thanks to Kent C. Dodds and his [match-sorter][match-sorter] package
337
478
  where most of the setup is from.
338
479
 
339
480
  <!-- prettier-ignore-start -->
340
- [npm]: https://www.npmjs.com
341
- [node]: https://nodejs.org
342
- [build-badge]: https://img.shields.io/github/workflow/status/milamer/parse-nested-form-data/validate?logo=github&style=flat-square
481
+ [build-badge]: https://img.shields.io/github/actions/workflow/status/milamer/parse-nested-form-data/validate.yml?branch=main&logo=github&style=flat-square
343
482
  [build]: https://github.com/milamer/parse-nested-form-data/actions?query=workflow%3Avalidate
344
483
  [coverage-badge]: https://img.shields.io/codecov/c/github/milamer/parse-nested-form-data.svg?style=flat-square
345
484
  [coverage]: https://codecov.io/github/milamer/parse-nested-form-data
346
485
  [version-badge]: https://img.shields.io/npm/v/parse-nested-form-data.svg?style=flat-square
347
486
  [package]: https://www.npmjs.com/package/parse-nested-form-data
487
+ [size-badge]: https://img.shields.io/badge/min%2Bbrotli-%3C%202%20kB-brightgreen?style=flat-square
488
+ [size]: https://github.com/milamer/parse-nested-form-data/blob/main/.size-limit.json
348
489
  [downloads-badge]: https://img.shields.io/npm/dm/parse-nested-form-data.svg?style=flat-square
349
490
  [npmtrends]: https://www.npmtrends.com/parse-nested-form-data
350
491
  [license-badge]: https://img.shields.io/npm/l/parse-nested-form-data.svg?style=flat-square
351
- [license]: https://github.com/milamer/parse-nested-form-data/blob/master/LICENSE
492
+ [license]: https://github.com/milamer/parse-nested-form-data/blob/main/LICENSE
352
493
  [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
353
494
  [prs]: http://makeapullrequest.com
354
495
  [coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square
355
- [coc]: https://github.com/milamer/parse-nested-form-data/blob/master/CODE_OF_CONDUCT.md
496
+ [coc]: https://github.com/milamer/parse-nested-form-data/blob/main/CODE_OF_CONDUCT.md
356
497
  [emojis]: https://github.com/all-contributors/all-contributors#emoji-key
357
498
  [all-contributors]: https://github.com/all-contributors/all-contributors
358
499
  [all-contributors-badge]: https://img.shields.io/github/all-contributors/milamer/parse-nested-form-data?color=orange&style=flat-square