read-excel-file 9.0.2 → 9.0.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/CHANGELOG.md +57 -41
- package/README.md +78 -56
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,51 +1,67 @@
|
|
|
1
1
|
9.0.0 / 18.04.2026
|
|
2
2
|
==================
|
|
3
3
|
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
*
|
|
4
|
+
* If you were using `parseData()` function:
|
|
5
|
+
* Rewrote the code of the `parseData()` function.
|
|
6
|
+
* The result of `parseData()` function is now `{ errors, objects }`. If there're no errors, `errors` will be `undefined`. Otherwise, `errors` will be a non-empty array and `objects` will be `undefined`.
|
|
7
|
+
* Previously the result of `parseData()` function was `[{ errors, object }, ...]`, i.e. the `errors` were split between each particular data row. Now the `errors` are combined for all data rows. The rationale is that it's simpler to handle the result of the function this way.
|
|
8
|
+
* Re-added `row: number` property to the `error` object.
|
|
9
|
+
* In a `schema`, a nested object could be declared as: `{ required: true/false, schema: { ... } }`. This is still true but the `required` flag is now only allowed to be either `undefined` or `false`, so `true` value is not allowed. The reason is quite simple. If a nested object as a whole is marked as `required: true`, and then it happens to be empty, a `"required"` error should be returned for it. But that error would also have to include a `column` title, and a nested object simply can't be pinned down to a single column in a sheet because it is by definition spread over multiple columns. So instead of marking a nested object as a whole with `required: true`, mark the specific required properties of it.
|
|
9
10
|
|
|
10
11
|
8.0.0 / 11.03.2026
|
|
11
12
|
==================
|
|
12
13
|
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
* Removed `
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* `
|
|
46
|
-
* `
|
|
47
|
-
* `
|
|
48
|
-
|
|
14
|
+
* If you were using the default exported function:
|
|
15
|
+
* Renamed the default exported function to a named exported function `readSheet`.
|
|
16
|
+
* Old: `import readExcelFile from "read-excel-file/browser"`
|
|
17
|
+
* New: `import { readSheet } from "read-excel-file/browser"`
|
|
18
|
+
* And same for other exports like `"read-excel-file/node"`, etc.
|
|
19
|
+
* The default exported function now returns a different kind of result. Specifically, now it returns all available sheets — an array of objects: `[{ sheet: "Sheet 1", data: [['a1','b1','c1'],['a2','b2','c2']] }, ...]`.
|
|
20
|
+
* The default exported function used to return sheet names when passed `getSheets: true` parameter. Now, instead of that, the default exported function just returns all available sheets, from which one could get the sheet names.
|
|
21
|
+
|
|
22
|
+
* If you were using `readSheetNames()` function:
|
|
23
|
+
* Removed exported function `readSheetNames()`. Use the default exported function instead. The default exported function now returns all sheets.
|
|
24
|
+
|
|
25
|
+
* If you were using `parseExcelDate()` function:
|
|
26
|
+
* Removed exported function `parseExcelDate()` because there seems to be no need to have it exported.
|
|
27
|
+
|
|
28
|
+
* If you were using `schema` parameter:
|
|
29
|
+
* Removed `schema` parameter. Instead, use exported function `parseData(data, schema)` to map data to an array of objects.
|
|
30
|
+
* Old: `import readXlsxFile from "read-excel-file"` and then `const { rows, errors } = await readXlsxFile(..., { schema })`
|
|
31
|
+
* New: `import { readSheet, parseData } from "read-excel-file/browser"` and then `const result = parseData(await readSheet(...), schema)`
|
|
32
|
+
* The `result` of the function is an array where each element represents a "data row" and has shape `{ object, errors }`.
|
|
33
|
+
* Depending on whether there were any errors when parsing a given "data row", either `object` or `errors` property will be `undefined`.
|
|
34
|
+
* The `errors` don't have a `row` property anymore because it could be derived from "data row" number.
|
|
35
|
+
* In version `9.x`, the `row` property has been re-added, so consider migrating straight to `9.x`.
|
|
36
|
+
* In version `9.x`, the returned result of `parseData()` has been changed back to `{ errors, objects }`, so consider migrating straight to `9.x`. In that case, if there're no errors, `errors` will be `undefined`; otherwise, `errors` will be a non-empty array and `objects` will be `undefined`.
|
|
37
|
+
* Renamed some `schema`-related parameters:
|
|
38
|
+
* `schemaPropertyValueForMissingColumn` → `propertyValueWhenColumnIsMissing`
|
|
39
|
+
* `schemaPropertyValueForMissingValue` → `propertyValueWhenCellIsEmpty`
|
|
40
|
+
* `schemaPropertyShouldSkipRequiredValidationForMissingColumn` → (removed)
|
|
41
|
+
* `getEmptyObjectValue` → `transformEmptyObject`
|
|
42
|
+
* The leading `.` character is now removed from the `path` parameter.
|
|
43
|
+
* `getEmptyArrayValue` → `transformEmptyArray`
|
|
44
|
+
* The leading `.` character is now removed from the `path` parameter.
|
|
45
|
+
* Previously, when using a `schema` to parse comma-separated values, it used to ignore any commas that're surrounded by quotes, similar to how it's done in `.csv` files. Now it no longer does that.
|
|
46
|
+
* Previously, when using a `schema` to parse comma-separated values, it used to allow empty-string elements. Now it no longer does that and such empty-string elements will now result in an error with properties: `{ error: "invalid", reason: "syntax" }`.
|
|
47
|
+
* Previously, when using a `schema` to parse `type: Date` properties, it used to support both `Date` objects and numeric timestamps as the input data for the property value. In the latter case, it simply force-converted those numeric timestamps to corresponding `Date` objects. Now `parseData()` function no longer does that, and demands the input data for `type: Date` schema properties to only be `Date` objects, i.e. it shifts the responsibility to interpret date cell values correctly onto `readSheet()` and `readExcelFile()` functions. And I'd personally assume that in any real-world (i.e. non-contrived) scenario those functions would interpret date cell values correctly, so I personally don't consider this a "breaking change". Still, formally, it is a "breaking change" and therefore should be mentioned. So if, for some strange reason, those two functions happen to not recognize a date cell value correctly, `parseData()` function will return an error for such cell: `"not_a_date"`.
|
|
48
|
+
* Previously, when using a `schema` to parse sheet data, and a given row of data was completely empty, it didn't run any `required` property validations. Now it no longer does that and it will run all `required` property validations regardless of whether it's a completely empty row of data or not.
|
|
49
|
+
|
|
50
|
+
* If you were using `transformData` parameter:
|
|
51
|
+
* Removed `transformData` parameter because the `schema` parameter was extracted into a separate function called `parseData()`. Now, if required, a developer could transform the `data` manually and then pass it to `parseData()` function.
|
|
52
|
+
|
|
53
|
+
* If you were using `isColumnOriented` parameter:
|
|
54
|
+
* Removed `isColumnOriented` parameter because it seemed to be of no use.
|
|
55
|
+
|
|
56
|
+
* If you were using `ignoreEmptyRows` parameter:
|
|
57
|
+
* Removed `ignoreEmptyRows` parameter. Empty rows somewhere in the middle of a sheet are not ignored now. That doesn't concern empty rows at the end of a sheet though — those're still ignored.
|
|
58
|
+
|
|
59
|
+
* If you were using TypeScript:
|
|
60
|
+
* Renamed some of the exported types:
|
|
61
|
+
* `Type` → `ParseDataCustomType`
|
|
62
|
+
* `Error` or `SchemaParseCellValueError` → `ParseDataError`
|
|
63
|
+
* `CellValueRequiredError` → `ParseDataValueRequiredError`
|
|
64
|
+
* `ParsedObjectsResult` → `ParseDataResult`
|
|
49
65
|
|
|
50
66
|
7.0.1 / 04.03.2026
|
|
51
67
|
==================
|
package/README.md
CHANGED
|
@@ -24,42 +24,57 @@ Also check out [`write-excel-file`](https://www.npmjs.com/package/write-excel-fi
|
|
|
24
24
|
|
|
25
25
|
######
|
|
26
26
|
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
* Removed `
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* `
|
|
60
|
-
* `
|
|
61
|
-
* `
|
|
62
|
-
|
|
27
|
+
* If you were using the default exported function:
|
|
28
|
+
* Renamed the default exported function to a named exported function `readSheet`.
|
|
29
|
+
* Old: `import readExcelFile from "read-excel-file/browser"`
|
|
30
|
+
* New: `import { readSheet } from "read-excel-file/browser"`
|
|
31
|
+
* And same for other exports like `"read-excel-file/node"`, etc.
|
|
32
|
+
* The default exported function now returns a different kind of result. Specifically, now it returns all available sheets — an array of objects: `[{ sheet: "Sheet 1", data: [['a1','b1','c1'],['a2','b2','c2']] }, ...]`.
|
|
33
|
+
* The default exported function used to return sheet names when passed `getSheets: true` parameter. Now, instead of that, the default exported function just returns all available sheets, from which one could get the sheet names.
|
|
34
|
+
|
|
35
|
+
* If you were using `readSheetNames()` function:
|
|
36
|
+
* Removed exported function `readSheetNames()`. Use the default exported function instead. The default exported function now returns all sheets.
|
|
37
|
+
|
|
38
|
+
* If you were using `parseExcelDate()` function:
|
|
39
|
+
* Removed exported function `parseExcelDate()` because there seems to be no need to have it exported.
|
|
40
|
+
|
|
41
|
+
* If you were using `schema` parameter:
|
|
42
|
+
* Removed `schema` parameter. Instead, use exported function `parseData(data, schema)` to map data to an array of objects.
|
|
43
|
+
* Old: `import readXlsxFile from "read-excel-file"` and then `const { rows, errors } = await readXlsxFile(..., { schema })`
|
|
44
|
+
* New: `import { readSheet, parseData } from "read-excel-file/browser"` and then `const result = parseData(await readSheet(...), schema)`
|
|
45
|
+
* The `result` of the function is an array where each element represents a "data row" and has shape `{ object, errors }`.
|
|
46
|
+
* Depending on whether there were any errors when parsing a given "data row", either `object` or `errors` property will be `undefined`.
|
|
47
|
+
* The `errors` don't have a `row` property anymore because it could be derived from "data row" number.
|
|
48
|
+
* In version `9.x`, the `row` property has been re-added, so consider migrating straight to `9.x`.
|
|
49
|
+
* In version `9.x`, the returned result of `parseData()` has been changed back to `{ errors, objects }`, so consider migrating straight to `9.x`. In that case, if there're no errors, `errors` will be `undefined`; otherwise, `errors` will be a non-empty array and `objects` will be `undefined`.
|
|
50
|
+
* Renamed some `schema`-related parameters:
|
|
51
|
+
* `schemaPropertyValueForMissingColumn` → `propertyValueWhenColumnIsMissing`
|
|
52
|
+
* `schemaPropertyValueForMissingValue` → `propertyValueWhenCellIsEmpty`
|
|
53
|
+
* `schemaPropertyShouldSkipRequiredValidationForMissingColumn` → (removed)
|
|
54
|
+
* `getEmptyObjectValue` → `transformEmptyObject`
|
|
55
|
+
* The leading `.` character is now removed from the `path` parameter.
|
|
56
|
+
* `getEmptyArrayValue` → `transformEmptyArray`
|
|
57
|
+
* The leading `.` character is now removed from the `path` parameter.
|
|
58
|
+
* Previously, when using a `schema` to parse comma-separated values, it used to ignore any commas that're surrounded by quotes, similar to how it's done in `.csv` files. Now it no longer does that.
|
|
59
|
+
* Previously, when using a `schema` to parse comma-separated values, it used to allow empty-string elements. Now it no longer does that and such empty-string elements will now result in an error with properties: `{ error: "invalid", reason: "syntax" }`.
|
|
60
|
+
* Previously, when using a `schema` to parse `type: Date` properties, it used to support both `Date` objects and numeric timestamps as the input data for the property value. In the latter case, it simply force-converted those numeric timestamps to corresponding `Date` objects. Now `parseData()` function no longer does that, and demands the input data for `type: Date` schema properties to only be `Date` objects, i.e. it shifts the responsibility to interpret date cell values correctly onto `readSheet()` and `readExcelFile()` functions. And I'd personally assume that in any real-world (i.e. non-contrived) scenario those functions would interpret date cell values correctly, so I personally don't consider this a "breaking change". Still, formally, it is a "breaking change" and therefore should be mentioned. So if, for some strange reason, those two functions happen to not recognize a date cell value correctly, `parseData()` function will return an error for such cell: `"not_a_date"`.
|
|
61
|
+
* Previously, when using a `schema` to parse sheet data, and a given row of data was completely empty, it didn't run any `required` property validations. Now it no longer does that and it will run all `required` property validations regardless of whether it's a completely empty row of data or not.
|
|
62
|
+
|
|
63
|
+
* If you were using `transformData` parameter:
|
|
64
|
+
* Removed `transformData` parameter because the `schema` parameter was extracted into a separate function called `parseData()`. Now, if required, a developer could transform the `data` manually and then pass it to `parseData()` function.
|
|
65
|
+
|
|
66
|
+
* If you were using `isColumnOriented` parameter:
|
|
67
|
+
* Removed `isColumnOriented` parameter because it seemed to be of no use.
|
|
68
|
+
|
|
69
|
+
* If you were using `ignoreEmptyRows` parameter:
|
|
70
|
+
* Removed `ignoreEmptyRows` parameter. Empty rows somewhere in the middle of a sheet are not ignored now. That doesn't concern empty rows at the end of a sheet though — those're still ignored.
|
|
71
|
+
|
|
72
|
+
* If you were using TypeScript:
|
|
73
|
+
* Renamed some of the exported types:
|
|
74
|
+
* `Type` → `ParseDataCustomType`
|
|
75
|
+
* `Error` or `SchemaParseCellValueError` → `ParseDataError`
|
|
76
|
+
* `CellValueRequiredError` → `ParseDataValueRequiredError`
|
|
77
|
+
* `ParsedObjectsResult` → `ParseDataResult`
|
|
63
78
|
</details>
|
|
64
79
|
|
|
65
80
|
<details>
|
|
@@ -67,11 +82,12 @@ Also check out [`write-excel-file`](https://www.npmjs.com/package/write-excel-fi
|
|
|
67
82
|
|
|
68
83
|
######
|
|
69
84
|
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
|
|
74
|
-
*
|
|
85
|
+
* If you were using `parseData()` function:
|
|
86
|
+
* Rewrote the code of the `parseData()` function.
|
|
87
|
+
* The result of `parseData()` function is now `{ errors, objects }`. If there're no errors, `errors` will be `undefined`. Otherwise, `errors` will be a non-empty array and `objects` will be `undefined`.
|
|
88
|
+
* Previously the result of `parseData()` function was `[{ errors, object }, ...]`, i.e. the `errors` were split between each particular data row. Now the `errors` are combined for all data rows. The rationale is that it's simpler to handle the result of the function this way.
|
|
89
|
+
* Re-added `row: number` property to the `error` object.
|
|
90
|
+
* In a `schema`, a nested object could be declared as: `{ required: true/false, schema: { ... } }`. This is still true but the `required` flag is now only allowed to be either `undefined` or `false`, so `true` value is not allowed. The reason is quite simple. If a nested object as a whole is marked as `required: true`, and then it happens to be empty, a `"required"` error should be returned for it. But that error would also have to include a `column` title, and a nested object simply can't be pinned down to a single column in a sheet because it is by definition spread over multiple columns. So instead of marking a nested object as a whole with `required: true`, mark the specific required properties of it.
|
|
75
91
|
</details>
|
|
76
92
|
|
|
77
93
|
## Install
|
|
@@ -84,19 +100,21 @@ Alternatively, it could be included on a web page [directly](#cdn) via a `<scrip
|
|
|
84
100
|
|
|
85
101
|
## Use
|
|
86
102
|
|
|
87
|
-
If your `.xlsx` file only has a single "sheet", or if you only
|
|
103
|
+
If your `.xlsx` file only has a single "sheet", or if you only need to read a single "sheet", or if you don't care what a "sheet" is, use `readSheet()` function.
|
|
104
|
+
|
|
105
|
+
For example, consider the following `.xlsx` file:
|
|
88
106
|
|
|
89
107
|
| Name | Date of Birth | Married | Kids |
|
|
90
108
|
| ---------- | ------------- | ------- | ---- |
|
|
91
109
|
| John Smith | 1/1/1995 | TRUE | 3 |
|
|
92
110
|
| Kate Brown | 3/1/2010 | FALSE | 0 |
|
|
93
111
|
|
|
112
|
+
Here's how to read it using `readSheet()` function:
|
|
113
|
+
|
|
94
114
|
```js
|
|
95
115
|
import { readSheet } from 'read-excel-file/node'
|
|
96
116
|
|
|
97
|
-
await readSheet(file)
|
|
98
|
-
|
|
99
|
-
// Returns
|
|
117
|
+
await readSheet(file) ===
|
|
100
118
|
[
|
|
101
119
|
['Name', 'Date of Birth', 'Married', 'Kids'],
|
|
102
120
|
['John Smith', 1995-01-01T00:00:00.000Z, true, 3],
|
|
@@ -104,20 +122,18 @@ await readSheet(file)
|
|
|
104
122
|
]
|
|
105
123
|
```
|
|
106
124
|
|
|
107
|
-
|
|
125
|
+
The result is an array of rows. Each row is an array of values — `string`, `number`, `boolean` or `Date`.
|
|
108
126
|
|
|
109
127
|
<!-- It's same as the default exported function shown above with the only difference that it returns just `data` instead of `[{ name: 'Sheet1', data }]`, so it's just a bit simpler to use. It has an optional second argument — `sheet` — which could be a sheet number (starting from `1`) or a sheet name. By default, it reads the first sheet. -->
|
|
110
128
|
|
|
111
|
-
|
|
129
|
+
It also has an optional second argument — `sheet` — which could be a sheet number (starting from `1`) or a sheet name. By default, it reads the first sheet.
|
|
112
130
|
|
|
113
|
-
But if you need to read all "sheets"
|
|
131
|
+
But if you need to read all available "sheets" in a file, use the default exported function:
|
|
114
132
|
|
|
115
133
|
```js
|
|
116
134
|
import readExcelFile from 'read-excel-file/node'
|
|
117
135
|
|
|
118
|
-
await readExcelFile(file)
|
|
119
|
-
|
|
120
|
-
// Returns
|
|
136
|
+
await readExcelFile(file) ===
|
|
121
137
|
[{
|
|
122
138
|
sheet: 'Sheet1',
|
|
123
139
|
data: [
|
|
@@ -131,19 +147,21 @@ await readExcelFile(file)
|
|
|
131
147
|
}]
|
|
132
148
|
```
|
|
133
149
|
|
|
134
|
-
|
|
150
|
+
The result is a non-empty array of "sheets". Each "sheet" is an object with properties:
|
|
135
151
|
* `sheet` — Sheet name.
|
|
136
152
|
* Example: `"Sheet1"`
|
|
137
153
|
* `data` — Sheet data. An array of rows. Each row is an array of values — `string`, `number`, `boolean` or `Date`.
|
|
138
154
|
* Example: `[ ['Name','Age'], ['John Smith',30], ['Kate Brown',15] ]`
|
|
139
155
|
|
|
140
|
-
##
|
|
156
|
+
## Import
|
|
141
157
|
|
|
142
158
|
This package provides a separate `import` path for each different environment, as described below.
|
|
143
159
|
|
|
144
160
|
### Browser
|
|
145
161
|
|
|
146
|
-
|
|
162
|
+
`read-excel-file/browser`
|
|
163
|
+
|
|
164
|
+
It can read from a [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File), a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) or an [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer).
|
|
147
165
|
|
|
148
166
|
Example: User chooses a file and the web application reads it.
|
|
149
167
|
|
|
@@ -180,7 +198,7 @@ const data = await readSheet(blob)
|
|
|
180
198
|
|
|
181
199
|
######
|
|
182
200
|
|
|
183
|
-
All exports of `read-excel-file` already use a [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) under the hood when reading `.xlsx` file contents. This is in order to avoid freezing the UI when reading large files. So using an additional Web Worker on top of that isn't really necessary. Still, for those who require it, this example shows how a user chooses a file and the web application reads it in a [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers).
|
|
201
|
+
All exports of `read-excel-file` already use a [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) under the hood when reading `.xlsx` file contents. This is in order to avoid freezing the UI when reading large files. So using an additional Web Worker on top of that isn't really necessary. Still, for those who require it, this example shows how a user chooses a file and the web application reads it in a [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) using `read-excel-file/web-worker` import path.
|
|
184
202
|
|
|
185
203
|
```js
|
|
186
204
|
// Step 1: Initialize Web Worker.
|
|
@@ -219,7 +237,9 @@ onmessage = async function(event) {
|
|
|
219
237
|
|
|
220
238
|
### Node.js
|
|
221
239
|
|
|
222
|
-
|
|
240
|
+
`read-excel-file/node`
|
|
241
|
+
|
|
242
|
+
It can read from a file path, a [`Stream`](https://nodejs.org/api/stream.html), a [`Buffer`](https://nodejs.org/api/buffer.html) or a [`Blob`](https://developer.mozilla.org/docs/Web/API/Blob).
|
|
223
243
|
|
|
224
244
|
Example 1: Read from a file path.
|
|
225
245
|
|
|
@@ -239,6 +259,8 @@ const data = await readSheet(fs.createReadStream('/path/to/file'))
|
|
|
239
259
|
|
|
240
260
|
### Universal
|
|
241
261
|
|
|
262
|
+
`read-excel-file/universal`
|
|
263
|
+
|
|
242
264
|
This one works both in a web browser and Node.js. It can only read from a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) or an [`ArrayBuffer`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer), which could be a bit less convenient for general use.
|
|
243
265
|
|
|
244
266
|
```js
|