poof 2.1.1 → 3.0.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/LICENSE +21 -0
- package/README.md +165 -222
- package/dist/cli.mjs +842 -0
- package/dist/index.mjs +1854 -0
- package/dist/spawn-rm/worker.mjs +41 -0
- package/package.json +19 -57
- package/dist/index.js +0 -9
- package/lib/index.js +0 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Hiroki Osame <hiroki.osame@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,305 +1,248 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
[](https://david-dm.org/fatfisz/poof?path=packages/poof)
|
|
7
|
-
[](https://david-dm.org/fatfisz/poof?path=packages/poof#info=devDependencies)
|
|
8
|
-
|
|
9
|
-
Poof is a tool for creating data processing functions in a declarative way by utilising an upcoming JS feature - decorators.
|
|
10
|
-
|
|
11
|
-
It makes use of the awesome [validator](https://www.npmjs.com/package/validator) lib to provide assertion functions. Without it Poof probably wouldn't exist.
|
|
12
|
-
|
|
13
|
-
## Contents
|
|
14
|
-
|
|
15
|
-
- [Getting started](#getting-started)
|
|
16
|
-
- [Why use Poof?](#why-use-poof)
|
|
17
|
-
- [Example](#example)
|
|
18
|
-
- [Example explained](#example-explained)
|
|
19
|
-
- [The difference between poof and poof-cast](#the-difference-between-poof-and-poof-cast)
|
|
20
|
-
- [API](#api)
|
|
21
|
-
- [createProcessor(config)](#createprocessorconfig)
|
|
22
|
-
- [decorators](#decorators)
|
|
23
|
-
- [decorators.assert.method and decorators.assert.not.method](#decoratorsassertmethod-and-decoratorsassertnotmethod)
|
|
24
|
-
- [decorators.assert.hasType and decorators.assert.not.hasType](#decoratorsasserthastype-and-decoratorsassertnothastype)
|
|
25
|
-
- [decorators.assert.isInstanceOf and decorators.assert.not.isInstanceOf](#decoratorsassertisinstanceof-and-decoratorsassertnotisinstanceof)
|
|
26
|
-
- [decorators.assign](#decoratorsassign)
|
|
27
|
-
- [decorators.filter(predicate)](#decoratorsfilterpredicate)
|
|
28
|
-
- [decorators.from(key)](#decoratorsfromkey)
|
|
29
|
-
- [decorators.ignoreIf(predicate)](#decoratorsignoreifpredicate)
|
|
30
|
-
- [decorators.ignoreIfUndefined](#decoratorsignoreifundefined)
|
|
31
|
-
- [decorators.map(mapper)](#decoratorsmapmapper)
|
|
32
|
-
- [decorators.set(value)](#decoratorssetvalue)
|
|
33
|
-
- [decorators.transform(transformer)](#decoratorstransformtransformer)
|
|
34
|
-
- [Some questions you might have](#some-questions-you-might-have)
|
|
35
|
-
- [How can I use decorators in my code?](#how-can-i-use-decorators-in-my-code)
|
|
36
|
-
- [Why the explicit assignment decorator?](#why-the-explicit-assignment-decorator)
|
|
37
|
-
- [What about nested structures?](#what-about-nested-structures)
|
|
38
|
-
- [Why is the field error exception a separate package?](#why-is-the-field-error-exception-a-separate-package)
|
|
39
|
-
- [Be careful with decorators!](#be-careful-with-decorators)
|
|
40
|
-
- [Contributing](#contributing)
|
|
41
|
-
- [License](#license)
|
|
42
|
-
|
|
43
|
-
## Getting started
|
|
44
|
-
|
|
45
|
-
Install the `poof` package with this command:
|
|
46
|
-
```shell
|
|
47
|
-
npm install poof field-validation-error --save
|
|
48
|
-
```
|
|
49
|
-
and/or install the `poof-cast` package with this command:
|
|
50
|
-
```shell
|
|
51
|
-
npm install poof-cast field-validation-error -save
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
`field-validation-error` is a peer dependency of both `poof` packages.
|
|
55
|
-
|
|
56
|
-
### ES6 Data Structures
|
|
1
|
+
<h2 align="center">
|
|
2
|
+
<img width="240" src=".github/logo.png">
|
|
3
|
+
<br><br>
|
|
4
|
+
<a href="https://npm.im/poof"><img src="https://badgen.net/npm/v/poof"></a> <a href="https://npm.im/poof"><img src="https://badgen.net/npm/dm/poof"></a> <a href="https://packagephobia.now.sh/result?p=poof"><img src="https://packagephobia.now.sh/badge?p=poof"></a>
|
|
5
|
+
</h2>
|
|
57
6
|
|
|
58
|
-
|
|
59
|
-
Make sure you add a polyfill yourself if you want to support older browsers.
|
|
7
|
+
Ever `rm -rf`'d a large directory and sat there waiting for it to finish? With `poof` you no longer need to wait:
|
|
60
8
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
- it is especially useful for isomorphic websites - you can declare data validator once and use it both on the client-side and the on the server-side
|
|
65
|
-
- Poof processors are composable - you can pass the result from one processor to another, e.g. when you want to separate validation and transforming
|
|
9
|
+
```sh
|
|
10
|
+
$ poof ./large-file ./large-directory "**/globs"
|
|
11
|
+
```
|
|
66
12
|
|
|
67
|
-
|
|
13
|
+
### What's "poof"?
|
|
14
|
+
`poof` is a fast, non-blocking CLI alternative to `rm -rf`, designed for deleting large files and directories without waiting for cleanup to complete.
|
|
68
15
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
import { createProcessor, decorators } from 'poof-cast';
|
|
16
|
+
It works by quickly moving files and directories out of the way, then deleting them in the background.
|
|
17
|
+
This means large deletions finish instantly from your perspective, even when there's a lot of data to clean up.
|
|
72
18
|
|
|
73
|
-
|
|
74
|
-
@decorators.from('postId')
|
|
75
|
-
@decorators.assert.not.isNull('Missing post id')
|
|
76
|
-
@decorators.assert.isMongoId('Invalid id')
|
|
77
|
-
@decorators.transform(ObjectID)
|
|
78
|
-
@decorators.assign
|
|
79
|
-
_id() {},
|
|
19
|
+
On a large filesystem (4.7 GB, 190k files), `poof` returns control in ~0.6s while `rm -rf` takes ~28s and `rimraf` ~12s. Full benchmarks below.
|
|
80
20
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
});
|
|
21
|
+
### Features
|
|
22
|
+
* ⚡ **Immediate return**: deletion doesn't block your shell
|
|
23
|
+
* 🗂 **Move-then-delete**: fast, atomic rename before cleanup
|
|
24
|
+
* 🛡 **Built-in safeguards**: root protection and directory scoping
|
|
25
|
+
* 🖥 **Cross-platform**: macOS, Linux, and Windows
|
|
87
26
|
|
|
88
|
-
|
|
89
|
-
const result = processIdAndIndex(request.body);
|
|
27
|
+
## Install
|
|
90
28
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (error instanceof FieldValidationError) {
|
|
94
|
-
// Do something with error messages contained in `error.fields`
|
|
95
|
-
}
|
|
96
|
-
}
|
|
29
|
+
```sh
|
|
30
|
+
npm install -g poof
|
|
97
31
|
```
|
|
98
32
|
|
|
99
|
-
##
|
|
100
|
-
|
|
101
|
-
Poof library has two versions: [`poof`, and `poof-cast`](#the-difference-between-poof-and-poof-cast). Both of them have two exports: the [`createProcessor` function](#createprocessorconfig) and the [`decorators` object](#decorators).
|
|
33
|
+
## CLI Usage
|
|
102
34
|
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
// about it later.
|
|
107
|
-
import FieldValidationError from 'field-validation-error';
|
|
108
|
-
import { createProcessor, decorators } from 'poof-cast';
|
|
35
|
+
```sh
|
|
36
|
+
# Delete files or directories
|
|
37
|
+
poof node_modules dist
|
|
109
38
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const processIdAndIndex = createProcessor({
|
|
39
|
+
# Use glob patterns
|
|
40
|
+
poof "*.log" "temp-*"
|
|
113
41
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
@decorators.from('postId')
|
|
42
|
+
# Recursive match with ** (searches all subdirectories)
|
|
43
|
+
poof "**/node_modules" "**/dist"
|
|
117
44
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
45
|
+
# Verbose output
|
|
46
|
+
poof --verbose ./large-directory
|
|
47
|
+
```
|
|
121
48
|
|
|
122
|
-
|
|
123
|
-
@decorators.assert.isMongoId('Invalid id')
|
|
49
|
+
### Options
|
|
124
50
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
51
|
+
| Flag | Alias | Description |
|
|
52
|
+
| ------------- | ----- | ---------------------------------------------- |
|
|
53
|
+
| `--dry` | `-d` | Preview files without deleting |
|
|
54
|
+
| `--verbose` | `-v` | Log each file as it's deleted |
|
|
55
|
+
| `--dangerous` | | Allow deleting paths outside current directory |
|
|
56
|
+
| `--version` | | Show version |
|
|
57
|
+
| `--help` | | Show help |
|
|
128
58
|
|
|
129
|
-
|
|
130
|
-
// object, you have to do it explicitly with the `assign` decorator.
|
|
131
|
-
@decorators.assign
|
|
59
|
+
## File matching
|
|
132
60
|
|
|
133
|
-
|
|
134
|
-
_id() {},
|
|
61
|
+
`poof` accepts explicit paths or [glob patterns](https://en.wikipedia.org/wiki/Glob_%28programming%29) for flexible file matching.
|
|
135
62
|
|
|
63
|
+
> [!TIP]
|
|
64
|
+
> When using glob patterns, start with `--dry` to preview what will match before deleting.
|
|
136
65
|
|
|
137
|
-
|
|
138
|
-
@decorators.assert.not.isNull('Missing index')
|
|
66
|
+
### Quick refresher: shell quoting
|
|
139
67
|
|
|
140
|
-
|
|
141
|
-
|
|
68
|
+
How unquoted glob patterns are expanded depends on your shell's settings (`dotglob`, `globstar`, etc.).
|
|
69
|
+
To get consistent behavior, quote the pattern so `poof` handles the matching itself.
|
|
142
70
|
|
|
143
|
-
|
|
144
|
-
|
|
71
|
+
```sh
|
|
72
|
+
# The shell expands the glob before poof runs
|
|
73
|
+
$ poof **/node_modules
|
|
145
74
|
|
|
146
|
-
|
|
147
|
-
|
|
75
|
+
# Recommended: poof expands the glob
|
|
76
|
+
$ poof "**/node_modules"
|
|
77
|
+
```
|
|
148
78
|
|
|
149
|
-
|
|
150
|
-
// Note that here we initially get the value from the `index` property, so
|
|
151
|
-
// the `from` decorator is not needed.
|
|
152
|
-
index() {},
|
|
153
|
-
});
|
|
79
|
+
### Basic patterns
|
|
154
80
|
|
|
81
|
+
```sh
|
|
82
|
+
# Explicit paths
|
|
83
|
+
$ poof node_modules
|
|
155
84
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const result = processIdAndIndex(request.body);
|
|
85
|
+
# Multiple paths
|
|
86
|
+
$ poof dist coverage
|
|
159
87
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
// Do something with error messages contained in `error.fields`
|
|
164
|
-
}
|
|
165
|
-
}
|
|
88
|
+
# Wildcards in current directory
|
|
89
|
+
$ poof "*.log"
|
|
90
|
+
$ poof "temp-*"
|
|
166
91
|
```
|
|
167
92
|
|
|
168
|
-
|
|
169
|
-
```js
|
|
170
|
-
{
|
|
171
|
-
postId: '507f1f77bcf86cd799439011',
|
|
172
|
-
index: '12',
|
|
173
|
-
}
|
|
174
|
-
```
|
|
175
|
-
then `result` would be:
|
|
176
|
-
```js
|
|
177
|
-
{
|
|
178
|
-
_id: ObjectID('507f1f77bcf86cd799439011'),
|
|
179
|
-
index: 12,
|
|
180
|
-
}
|
|
181
|
-
```
|
|
93
|
+
### Recursive patterns
|
|
182
94
|
|
|
183
|
-
|
|
184
|
-
```js
|
|
185
|
-
{
|
|
186
|
-
index: '-1',
|
|
187
|
-
}
|
|
188
|
-
```
|
|
189
|
-
then you'd get a `FieldValidationError` with the `fields` property equal to:
|
|
190
|
-
```js
|
|
191
|
-
{
|
|
192
|
-
_id: 'Missing post id',
|
|
193
|
-
index: 'Invalid index',
|
|
194
|
-
}
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
## The difference between poof and poof-cast
|
|
95
|
+
Use `**` to match across nested directories:
|
|
198
96
|
|
|
199
|
-
|
|
97
|
+
```sh
|
|
98
|
+
# All node_modules in a monorepo
|
|
99
|
+
$ poof "**/node_modules"
|
|
200
100
|
|
|
201
|
-
|
|
101
|
+
# All .log files recursively
|
|
102
|
+
$ poof "**/*.log"
|
|
202
103
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
104
|
+
# Multiple patterns
|
|
105
|
+
$ poof "**/dist" "**/coverage" "**/*.tmp"
|
|
106
|
+
```
|
|
206
107
|
|
|
207
|
-
|
|
108
|
+
### Dotfiles (hidden files)
|
|
208
109
|
|
|
209
|
-
|
|
110
|
+
By default, glob wildcards (`*`, `**`) don't match dotfiles (files starting with `.`). This helps avoid accidentally deleting `.git`, `.env`, or other hidden files.
|
|
210
111
|
|
|
211
|
-
|
|
112
|
+
To target dotfiles, explicitly include the `.` in your pattern:
|
|
212
113
|
|
|
213
|
-
|
|
114
|
+
```sh
|
|
115
|
+
# Delete all dotfiles in current directory
|
|
116
|
+
$ poof ".*"
|
|
214
117
|
|
|
215
|
-
|
|
118
|
+
# Delete .cache directories (not inside hidden dirs)
|
|
119
|
+
$ poof "**/.cache"
|
|
216
120
|
|
|
217
|
-
|
|
121
|
+
# Delete a specific dotfile
|
|
122
|
+
$ poof .env.local
|
|
123
|
+
```
|
|
218
124
|
|
|
219
|
-
|
|
125
|
+
#### Searching inside hidden directories
|
|
220
126
|
|
|
221
|
-
|
|
127
|
+
`**/.cache` finds `.cache` directories in regular directories, but does **not** scan inside other hidden directories like `.git`. This is intentional—scanning hidden directories is slow and rarely useful.
|
|
222
128
|
|
|
223
|
-
|
|
129
|
+
To search inside hidden directories, start your pattern with `.*`:
|
|
224
130
|
|
|
225
|
-
|
|
131
|
+
```sh
|
|
132
|
+
# Find .cache inside any hidden directory
|
|
133
|
+
$ poof ".*/**/.cache"
|
|
134
|
+
```
|
|
226
135
|
|
|
227
|
-
|
|
136
|
+
Or use brace expansion to target specific directories:
|
|
228
137
|
|
|
229
|
-
|
|
138
|
+
```sh
|
|
139
|
+
# Search inside .config and src
|
|
140
|
+
$ poof "{.config,src}/**/.cache"
|
|
141
|
+
```
|
|
230
142
|
|
|
231
|
-
|
|
143
|
+
### Negation patterns
|
|
232
144
|
|
|
233
|
-
|
|
145
|
+
Extglob negations like `!(pattern)` match everything *except* the specified pattern:
|
|
234
146
|
|
|
235
|
-
|
|
147
|
+
```sh
|
|
148
|
+
# Delete everything except .gitkeep
|
|
149
|
+
$ poof "!(.gitkeep)"
|
|
150
|
+
```
|
|
236
151
|
|
|
237
|
-
|
|
152
|
+
> [!WARNING]
|
|
153
|
+
> Negations match dotfiles. `!(important.txt)` will match `.env`, `.git`, and other hidden files. Always use `--dry` first.
|
|
238
154
|
|
|
239
|
-
|
|
155
|
+
## Safety
|
|
240
156
|
|
|
241
|
-
|
|
157
|
+
`poof` includes guards to help prevent common accidents:
|
|
242
158
|
|
|
243
|
-
|
|
159
|
+
- **Root protection**: refuses to delete the filesystem root
|
|
160
|
+
- **Directory scoping**: won't delete paths outside `cwd` unless `--dangerous` is passed
|
|
161
|
+
- **Typo protection**: explicit paths that don't exist are reported as errors, so `poof ndoe_modoules` won't silently succeed
|
|
162
|
+
- **Script-friendly globs**: glob patterns with no matches exit silently, so `poof "*.log"` won't break your scripts
|
|
244
163
|
|
|
245
|
-
|
|
164
|
+
## How it works
|
|
246
165
|
|
|
247
|
-
|
|
166
|
+
Traditional `rm -rf` blocks until every file is unlinked.
|
|
167
|
+
For large directories, this means waiting on thousands of filesystem operations.
|
|
248
168
|
|
|
249
|
-
|
|
169
|
+
`poof` uses a different strategy:
|
|
250
170
|
|
|
251
|
-
|
|
171
|
+
1. Resolve glob patterns and validate paths
|
|
172
|
+
2. Spawn a detached cleanup process
|
|
173
|
+
3. Rename files to a temp directory (constant-time, atomic)
|
|
174
|
+
4. Stream renamed paths to the cleanup process as renames complete
|
|
175
|
+
5. Exit process to return your shell while cleanup process continues in the background
|
|
252
176
|
|
|
253
|
-
|
|
177
|
+
### Cross-device fallback
|
|
254
178
|
|
|
255
|
-
|
|
179
|
+
If the target is on a different filesystem (`EXDEV`), `poof` falls back to renaming in place with a hidden prefix (e.g., `.poof-uuid-large-directory`) and streams it directly to the cleaner.
|
|
256
180
|
|
|
257
|
-
|
|
181
|
+
## Benchmarks
|
|
258
182
|
|
|
259
|
-
|
|
183
|
+
Deleting `**/node_modules` directories from a synthetic fixture:
|
|
260
184
|
|
|
261
|
-
|
|
185
|
+
| Tool | Time | vs poof |
|
|
186
|
+
| -------- | ------- | ---------- |
|
|
187
|
+
| `poof` | 0.59s | — |
|
|
188
|
+
| `rimraf` | 12.32s | 21x slower |
|
|
189
|
+
| `rm -rf` | 27.82s | 48x slower |
|
|
262
190
|
|
|
263
|
-
|
|
191
|
+
Fixture: 4.7 GB, 190k files
|
|
264
192
|
|
|
265
|
-
|
|
193
|
+
Environment: macOS 15.2, Apple M2 Max, SSD
|
|
266
194
|
|
|
267
|
-
|
|
195
|
+
> [!NOTE]
|
|
196
|
+
> These benchmarks measure how long it takes to return control of your terminal, not actual deletion time. Background deletion continues after `poof` exits. `rm -rf` and `rimraf` measure actual deletion time.
|
|
268
197
|
|
|
269
|
-
|
|
198
|
+
## JS API
|
|
270
199
|
|
|
271
|
-
|
|
200
|
+
`poof` can also be used programmatically:
|
|
272
201
|
|
|
273
|
-
|
|
202
|
+
```ts
|
|
203
|
+
import poof from 'poof'
|
|
274
204
|
|
|
275
|
-
|
|
205
|
+
await poof('./large-directory')
|
|
206
|
+
await poof(['**/dist', 'coverage'])
|
|
276
207
|
|
|
277
|
-
|
|
208
|
+
const { deleted, errors } = await poof('./large-directory', { dry: true })
|
|
209
|
+
```
|
|
278
210
|
|
|
279
|
-
|
|
211
|
+
### Types
|
|
280
212
|
|
|
281
|
-
|
|
213
|
+
```ts
|
|
214
|
+
type Options = {
|
|
215
|
+
cwd?: string
|
|
216
|
+
dry?: boolean
|
|
217
|
+
dangerous?: boolean
|
|
218
|
+
}
|
|
282
219
|
|
|
283
|
-
|
|
220
|
+
type Failure = {
|
|
221
|
+
path: string
|
|
222
|
+
error: Error
|
|
223
|
+
}
|
|
284
224
|
|
|
285
|
-
|
|
225
|
+
type Result = {
|
|
226
|
+
deleted: string[]
|
|
227
|
+
errors: Failure[]
|
|
228
|
+
}
|
|
229
|
+
```
|
|
286
230
|
|
|
287
|
-
|
|
231
|
+
## Alternatives
|
|
288
232
|
|
|
289
|
-
|
|
233
|
+
Some tools provide fast, non-blocking removal by moving files to the system trash:
|
|
290
234
|
|
|
291
|
-
|
|
292
|
-
|
|
235
|
+
- [trash](https://formulae.brew.sh/formula/trash) (macOS)
|
|
236
|
+
- [trash-cli](https://github.com/sindresorhus/trash-cli) (cross-platform)
|
|
293
237
|
|
|
294
|
-
|
|
238
|
+
These are useful for recoverable deletes, but large directories can accumulate in the trash and consume disk space.
|
|
295
239
|
|
|
296
|
-
|
|
240
|
+
`poof` permanently deletes files, freeing space immediately.
|
|
297
241
|
|
|
298
|
-
##
|
|
242
|
+
## Requirements
|
|
299
243
|
|
|
300
|
-
|
|
301
|
-
Add unit tests for any new or changed functionality. Lint and test your code with `npm test`.
|
|
244
|
+
Node.js >= 20.19.6
|
|
302
245
|
|
|
303
246
|
## License
|
|
304
247
|
|
|
305
|
-
|
|
248
|
+
MIT
|