formidable
Advanced tools
Comparing version 2.0.0-dev.20200131.2 to 3.1.1-canary.20211030
@@ -0,19 +1,43 @@ | ||
# Changelog | ||
### Unreleased 3.1.1 | ||
* feat: handle top level json array, string and number | ||
### Unreleased 3.1 | ||
* feat: add firstValues, readBooleans helpers | ||
### Unreleased 3.0 | ||
* feat: remove options.multiples ([730](https://github.com/node-formidable/formidable/pull/730)) | ||
* use modern URLSearchParams https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams internally | ||
* files and fields values are always arrays | ||
* fields with [] in the name do not receive special treatment | ||
* remove unused qs and querystring dependency | ||
* feat: Use ES modules ([727](https://github.com/node-formidable/formidable/pull/727)) | ||
* options.enabledPlugins must contain the plugin themselves instead of the plugins names | ||
### Unreleased (`canary` & `dev` dist-tags) | ||
* Test only on Node.js >= v10. Support only Node LTS and latest ([#515](https://github.com/node-formidable/node-formidable/pull/515)) | ||
* stop using deprecated features ([#516](https://github.com/node-formidable/node-formidable/pull/516), [#472](https://github.com/node-formidable/node-formidable/issues/472), [#406](https://github.com/node-formidable/node-formidable/issues/406)) | ||
* throw error during data parsing ([#513](https://github.com/node-formidable/node-formidable/pull/513)) | ||
* Array support for fields and files ([#380](https://github.com/node-formidable/node-formidable/pull/380), [#340](https://github.com/node-formidable/node-formidable/pull/340), [#367](https://github.com/node-formidable/node-formidable/pull/367), [#33](https://github.com/node-formidable/node-formidable/issues/33), [#498](https://github.com/node-formidable/node-formidable/issues/498), [#280](https://github.com/node-formidable/node-formidable/issues/280), [#483](https://github.com/node-formidable/node-formidable/issues/483)) | ||
* feat: add options.filter ([#716](https://github.com/node-formidable/formidable/pull/716)) | ||
* feat: add code and httpCode to most errors ([#686](https://github.com/node-formidable/formidable/pull/686)) | ||
* rename: option.hash into option.hashAlgorithm ([#689](https://github.com/node-formidable/formidable/pull/689)) | ||
* rename: file.path into file.filepath ([#689](https://github.com/node-formidable/formidable/pull/689)) | ||
* rename: file.type into file.mimetype ([#689](https://github.com/node-formidable/formidable/pull/689)) | ||
* refactor: split file.name into file.newFilename and file.originalFilename ([#689](https://github.com/node-formidable/formidable/pull/689)) | ||
* feat: prevent directory traversal attacks by default ([#689](https://github.com/node-formidable/formidable/pull/689)) | ||
* meta: stop including test files in npm ([7003c](https://github.com/node-formidable/formidable/commit/7003cd6133f90c384081accb51743688d5e1f4be)) | ||
* fix: handle invalid filenames ([d0a34](https://github.com/node-formidable/formidable/commit/d0a3484b048b8c177e62d66aecb03f5928f7a857)) | ||
* feat: add fileWriteStreamHandler option | ||
* feat: add allowEmptyFiles and minFileSize options | ||
* feat: Array support for fields and files ([#380](https://github.com/node-formidable/node-formidable/pull/380), [#340](https://github.com/node-formidable/node-formidable/pull/340), [#367](https://github.com/node-formidable/node-formidable/pull/367), [#33](https://github.com/node-formidable/node-formidable/issues/33), [#498](https://github.com/node-formidable/node-formidable/issues/498), [#280](https://github.com/node-formidable/node-formidable/issues/280), [#483](https://github.com/node-formidable/node-formidable/issues/483)) | ||
* possible partial fix of [#386](https://github.com/node-formidable/node-formidable/pull/386) with #380 (need tests and better implementation) | ||
* use hasOwnProperty in check against files/fields ([#522](https://github.com/node-formidable/node-formidable/pull/522)) | ||
* do not promote `IncomingForm` and add `exports.default` ([#529](https://github.com/node-formidable/node-formidable/pull/529)) | ||
* Improve examples and tests ([#523](https://github.com/node-formidable/node-formidable/pull/523)) | ||
* First step of Code quality improvements ([#525](https://github.com/node-formidable/node-formidable/pull/525)) | ||
* refactor: use hasOwnProperty in check against files/fields ([#522](https://github.com/node-formidable/node-formidable/pull/522)) | ||
* meta: do not promote `IncomingForm` and add `exports.default` ([#529](https://github.com/node-formidable/node-formidable/pull/529)) | ||
* meta: Improve examples and tests ([#523](https://github.com/node-formidable/node-formidable/pull/523)) | ||
* refactor: First step of Code quality improvements ([#525](https://github.com/node-formidable/node-formidable/pull/525)) | ||
* chore(funding): remove patreon & add npm funding field ([#525](https://github.com/node-formidable/node-formidable/pull/532) | ||
* feat: use Modern Streams API ([#531](https://github.com/node-formidable/node-formidable/pull/531)) | ||
* fix: remove gently hijack and tests ([#539](https://github.com/node-formidable/node-formidable/pull/539)) | ||
* docs: Clarify supported hash algorithms ([#537](https://github.com/node-formidable/node-formidable/pull/537)) | ||
* feat: better tests, add Airbnb + Prettier ([#542](https://github.com/node-formidable/node-formidable/pull/542)) | ||
* fix(incomingForm): better detection of fields vs files | ||
* fix: resolves [#128](https://github.com/node-formidable/node-formidable/pull/128) | ||
* fix: urlencoded parsing to emit end [#543](https://github.com/node-formidable/node-formidable/pull/543), introduced in [#531](https://github.com/node-formidable/node-formidable/pull/531) | ||
@@ -24,5 +48,7 @@ * fix(tests): include multipart and qs parser unit tests, part of [#415](https://github.com/node-formidable/node-formidable/issues/415) | ||
* feat: introduce Plugins API, fix silent failing tests ([#545](https://github.com/node-formidable/node-formidable/pull/545), [#391](https://github.com/node-formidable/node-formidable/pull/391), [#407](https://github.com/node-formidable/node-formidable/pull/407), [#386](https://github.com/node-formidable/node-formidable/pull/386), [#374](https://github.com/node-formidable/node-formidable/pull/374), [#521](https://github.com/node-formidable/node-formidable/pull/521), [#267](https://github.com/node-formidable/node-formidable/pull/267)) | ||
* respect form hash option on incoming octect/stream requests ([#407](https://github.com/node-formidable/node-formidable/pull/407)) | ||
* fix: exposing file writable stream errors ([#520](https://github.com/node-formidable/node-formidable/pull/520), [#316](https://github.com/node-formidable/node-formidable/pull/316), [#469](https://github.com/node-formidable/node-formidable/pull/469), [#470](https://github.com/node-formidable/node-formidable/pull/470)) | ||
* feat: custom file (re)naming, thru options.filename ([#591](https://github.com/node-formidable/node-formidable/pull/591), [#84](https://github.com/node-formidable/node-formidable/issues/84), [#86](https://github.com/node-formidable/node-formidable/issues/86), [#94](https://github.com/node-formidable/node-formidable/issues/94), [#154](https://github.com/node-formidable/node-formidable/issues/154), [#158](https://github.com/node-formidable/node-formidable/issues/158), [#488](https://github.com/node-formidable/node-formidable/issues/488), [#595](https://github.com/node-formidable/node-formidable/issues/595)) | ||
### v1.2.1 (2018-03-20) | ||
@@ -83,1 +109,5 @@ | ||
* Fix file handle leak on error (OrangeDog) | ||
--- | ||
[First commit, #3270eb4b1f8b (May 4th, 2010)](https://github.com/node-formidable/formidable/commit/3270eb4b1f8bb667b8c12f64c36a4e7b854216d8) |
{ | ||
"name": "formidable", | ||
"version": "2.0.0-dev.20200131.2", | ||
"version": "3.1.1-canary.20211030", | ||
"license": "MIT", | ||
"description": "A node.js module for parsing form data, especially file uploads.", | ||
"homepage": "https://github.com/node-formidable/node-formidable", | ||
"homepage": "https://github.com/node-formidable/formidable", | ||
"funding": "https://ko-fi.com/tunnckoCore/commissions", | ||
"repository": "node-formidable/node-formidable", | ||
"repository": "node-formidable/formidable", | ||
"type": "module", | ||
"main": "./src/index.js", | ||
"files": [ | ||
"src", | ||
"test" | ||
"CHANGELOG.md", | ||
"LICENSE", | ||
"README.md" | ||
], | ||
"publishConfig": { | ||
"access": "public", | ||
"tag": "dev" | ||
"tag": "3.x" | ||
}, | ||
@@ -24,37 +27,44 @@ "scripts": { | ||
"lint:prepare": "eslint --cache --fix --quiet --format codeframe", | ||
"reinstall": "rm -rf node_modules yarn.lock && yarn", | ||
"pretest": "rm -rf test/tmp && mkdir test/tmp", | ||
"test": "node test/run.js", | ||
"pretest:ci": "yarn pretest", | ||
"test:ci": "nyc node test/run.js" | ||
"reinstall": "del-cli ./node_modules ./yarn.lock", | ||
"postreinstall": "yarn setup", | ||
"setup": "yarn", | ||
"pretest": "del-cli ./test/tmp && make-dir ./test/tmp", | ||
"test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --coverage", | ||
"pretest:ci": "yarn run pretest", | ||
"test:ci": "node --experimental-vm-modules node_modules/.bin/nyc jest --coverage" | ||
}, | ||
"dependencies": { | ||
"dezalgo": "^1.0.3", | ||
"once": "^1.4.0" | ||
"dezalgo": "1.0.3", | ||
"hexoid": "1.0.0", | ||
"once": "1.4.0" | ||
}, | ||
"devDependencies": { | ||
"@commitlint/cli": "^8.3.5", | ||
"@commitlint/config-conventional": "^8.3.4", | ||
"@tunnckocore/prettier-config": "^1.2.0", | ||
"eslint": "^6.8.0", | ||
"eslint-config-airbnb-base": "^14.0.0", | ||
"eslint-config-prettier": "^6.10.0", | ||
"eslint-plugin-import": "^2.20.0", | ||
"eslint-plugin-prettier": "^3.1.2", | ||
"husky": "^4.2.1", | ||
"jest": "^25.1.0", | ||
"koa": "^2.11.0", | ||
"lint-staged": "^10.0.6", | ||
"nyc": "^15.0.0", | ||
"prettier": "^1.19.1", | ||
"prettier-plugin-pkgjson": "^0.2.0", | ||
"request": "^2.88.0", | ||
"supertest": "^4.0.2", | ||
"urun": "^0.0.8", | ||
"utest": "^0.0.8" | ||
"@commitlint/cli": "8.3.5", | ||
"@commitlint/config-conventional": "8.3.4", | ||
"@tunnckocore/prettier-config": "1.3.8", | ||
"del-cli": "3.0.0", | ||
"eslint": "6.8.0", | ||
"eslint-config-airbnb-base": "14.1.0", | ||
"eslint-config-prettier": "6.11.0", | ||
"eslint-plugin-import": "2.20.2", | ||
"eslint-plugin-prettier": "3.1.3", | ||
"express": "4.17.1", | ||
"husky": "4.2.5", | ||
"jest": "27.2.4", | ||
"koa": "2.11.0", | ||
"lint-staged": "10.2.7", | ||
"make-dir-cli": "2.0.0", | ||
"nyc": "15.1.0", | ||
"prettier": "2.0.5", | ||
"prettier-plugin-pkgjson": "0.2.8", | ||
"request": "2.88.2", | ||
"supertest": "6.1.6" | ||
}, | ||
"jest": { | ||
"verbose": true | ||
}, | ||
"husky": { | ||
"hooks": { | ||
"pre-commit": "git status --porcelain && lint-staged", | ||
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS" | ||
"pre-commit": "git status --porcelain && yarn lint-staged", | ||
"commit-msg": "yarn commitlint -E HUSKY_GIT_PARAMS" | ||
} | ||
@@ -74,3 +84,20 @@ }, | ||
] | ||
} | ||
}, | ||
"renovate": { | ||
"extends": [ | ||
"@tunnckocore", | ||
":pinAllExceptPeerDependencies" | ||
] | ||
}, | ||
"packageManager": "yarn@1.22.11", | ||
"keywords": [ | ||
"multipart", | ||
"form", | ||
"data", | ||
"querystring", | ||
"www", | ||
"json", | ||
"ulpoad", | ||
"file" | ||
] | ||
} |
521
README.md
<p align="center"> | ||
<img alt="node formidable logo" src="./logo.png" /> | ||
<img alt="npm formidable package logo" src="https://raw.githubusercontent.com/node-formidable/formidable/master/logo.png" /> | ||
</p> | ||
# formidable [![npm version][npmv-img]][npmv-url] [![MIT license][license-img]][license-url] [![Libera Manifesto][libera-manifesto-img]][libera-manifesto-url] | ||
# formidable [![npm version][npmv-img]][npmv-url] [![MIT license][license-img]][license-url] [![Libera Manifesto][libera-manifesto-img]][libera-manifesto-url] [![Twitter][twitter-img]][twitter-url] | ||
@@ -10,10 +10,36 @@ > A Node.js module for parsing form data, especially file uploads. | ||
[![Code style][codestyle-img]][codestyle-url] | ||
[![build status][build-img]][build-url] | ||
[![codecoverage][codecov-img]][codecov-url] | ||
[![linux build status][linux-build-img]][build-url] | ||
[![windows build status][windows-build-img]][build-url] | ||
[![macos build status][macos-build-img]][build-url] | ||
If you have any _how-to_ kind of questions, please read the [Contributing | ||
Guide][contributing-url] and [Code of Conduct][code_of_conduct-url] | ||
documents.<br /> For bugs reports and feature requests, [please create an | ||
issue][open-issue-url] or ping [@tunnckoCore](https://twitter.com/tunnckoCore) | ||
at Twitter. | ||
[![Conventional Commits][ccommits-img]][ccommits-url] | ||
[![Minimum Required Nodejs][nodejs-img]][npmv-url] | ||
[![Tidelift Subcsription][tidelift-img]][tidelift-url] | ||
[![Buy me a Kofi][kofi-img]][kofi-url] | ||
[![Renovate App Status][renovateapp-img]][renovateapp-url] | ||
[![Make A Pull Request][prs-welcome-img]][prs-welcome-url] | ||
[![Twitter][twitter-img]][twitter-url] | ||
## Status: Maintained [![npm version][npmv-canary-img]][npmv-url] [![npm version][npmv-dev-img]][npmv-url] | ||
This project is [semantically versioned](https://semver.org) and available as | ||
part of the [Tidelift Subscription][tidelift-url] for professional grade | ||
assurances, enhanced support and security. | ||
[Learn more.](https://tidelift.com/subscription/pkg/npm-formidable?utm_source=npm-formidable&utm_medium=referral&utm_campaign=enterprise) | ||
_The maintainers of `formidable` and thousands of other packages are working | ||
with Tidelift to deliver commercial support and maintenance for the Open Source | ||
dependencies you use to build your applications. Save time, reduce risk, and | ||
improve code health, while paying the maintainers of the exact dependencies you | ||
use._ | ||
[![][npm-weekly-img]][npmv-url] [![][npm-monthly-img]][npmv-url] | ||
[![][npm-yearly-img]][npmv-url] [![][npm-alltime-img]][npmv-url] | ||
## Status: Maintained [![npm version][npmv-canary-img]][npmv-url] | ||
This module was initially developed by | ||
@@ -28,21 +54,19 @@ [**@felixge**](https://github.com/felixge) for | ||
are always welcome! :heart: Jump on | ||
[issue #412](https://github.com/felixge/node-formidable/issues/412) if you are | ||
interested. | ||
[issue #412](https://github.com/felixge/node-formidable/issues/412) which is | ||
closed, but if you are interested we can discuss it and add you after strict | ||
rules, like enabling Two-Factor Auth in your npm and GitHub accounts. | ||
_**Note:** Master is a "canary" branch - try it with `npm i formidable@canary`. | ||
Do not expect (for now) things from it to be inside the`latest`"dist-tag" in the | ||
Npm. The`formidable@latest`is the`v1.2.1` version and probably it will be the | ||
last`v1` release!_ | ||
_**Note:** The github `master` branch is a "canary" branch - try it with | ||
`npm i formidable@canary`. Do not expect (for now) things from it to be inside | ||
the`latest` "dist-tag" in the Npm. The`formidable@latest`is the`v1.2.1` version | ||
and probably it will be the last`v1` release!_ | ||
_**Note: v2 is coming soon!**_ | ||
You can try the | ||
[Plugins API](https://github.com/felixge/node-formidable/tree/plugins-api) | ||
([#545](https://github.com/felixge/node-formidable/pull/545)), which is | ||
available through `formidable@dev`. | ||
## Highlights | ||
- Fast (~900-2500 mb/sec), streaming multipart parser | ||
- Automatically writing file uploads to disk | ||
- [Fast (~900-2500 mb/sec)](#benchmarks) & streaming multipart parser | ||
- Automatically writing file uploads to disk (optional, see | ||
[`options.fileWriteStreamHandler`](#options)) | ||
- [Plugins API](#useplugin-plugin) - allowing custom parsers and plugins | ||
- Low memory footprint | ||
@@ -54,2 +78,6 @@ - Graceful error handling | ||
This project requires `Node.js >= 10.13`. Install it using | ||
[yarn](https://yarnpkg.com) or [npm](https://npmjs.com).<br /> _We highly | ||
recommend to use Yarn when you think to contribute to this project._ | ||
```sh | ||
@@ -59,2 +87,4 @@ npm install formidable | ||
npm install formidable@canary | ||
## 3.x | ||
npm install formidable@3.x | ||
``` | ||
@@ -70,51 +100,165 @@ | ||
This is a low-level package, and if you're using a high-level framework it may | ||
already be included. | ||
This is a low-level package, and if you're using a high-level framework it _may_ | ||
already be included. Check the examples below and the `examples/` folder. | ||
However, [Express v4](http://expressjs.com) does not include any multipart | ||
handling, nor does [body-parser](https://github.com/expressjs/body-parser). | ||
## Examples | ||
For `koa` there is [koa-better-body](https://ghub.now.sh/koa-better-body) which | ||
can handle ANY type of body / form-data - JSON, urlencoded, multpart and so on. | ||
A new major release is coming there too. | ||
For more examples look at the `examples/` directory. | ||
## Example | ||
### with Node.js http module | ||
Parse an incoming file upload. | ||
Parse an incoming file upload, with the | ||
[Node.js's built-in `http` module](https://nodejs.org/api/http.html). | ||
```js | ||
const http = require('http'); | ||
const util = require('util'); | ||
const formidable = require('formidable'); | ||
import http from 'http'; | ||
import formidable from 'formidable'; | ||
http | ||
.createServer((req, res) => { | ||
if (req.url === '/upload' && req.method.toLowerCase() === 'post') { | ||
// parse a file upload | ||
const form = formidable(); | ||
const server = http.createServer((req, res) => { | ||
if (req.url === '/api/upload' && req.method.toLowerCase() === 'post') { | ||
// parse a file upload | ||
const form = formidable({}); | ||
form.parse(req, (err, fields, files) => { | ||
res.writeHead(200, { 'content-type': 'text/plain' }); | ||
res.write('received upload:\n\n'); | ||
res.end(util.inspect({ fields: fields, files: files })); | ||
}); | ||
form.parse(req, (err, fields, files) => { | ||
if (err) { | ||
res.writeHead(err.httpCode || 400, { 'Content-Type': 'text/plain' }); | ||
res.end(String(err)); | ||
return; | ||
} | ||
res.writeHead(200, { 'Content-Type': 'application/json' }); | ||
res.end(JSON.stringify({ fields, files }, null, 2)); | ||
}); | ||
return; | ||
} | ||
// show a file upload form | ||
res.writeHead(200, { 'Content-Type': 'text/html' }); | ||
res.end(` | ||
<h2>With Node.js <code>"http"</code> module</h2> | ||
<form action="/api/upload" enctype="multipart/form-data" method="post"> | ||
<div>Text field title: <input type="text" name="title" /></div> | ||
<div>File: <input type="file" name="multipleFiles" multiple="multiple" /></div> | ||
<input type="submit" value="Upload" /> | ||
</form> | ||
`); | ||
}); | ||
server.listen(8080, () => { | ||
console.log('Server listening on http://localhost:8080/ ...'); | ||
}); | ||
``` | ||
### with Express.js | ||
There are multiple variants to do this, but Formidable just need Node.js Request | ||
stream, so something like the following example should work just fine, without | ||
any third-party [Express.js](https://ghub.now.sh/express) middleware. | ||
Or try the | ||
[examples/with-express.js](https://github.com/node-formidable/formidable/blob/master/examples/with-express.js) | ||
```js | ||
import express from 'express'; | ||
import formidable from 'formidable'; | ||
const app = express(); | ||
app.get('/', (req, res) => { | ||
res.send(` | ||
<h2>With <code>"express"</code> npm package</h2> | ||
<form action="/api/upload" enctype="multipart/form-data" method="post"> | ||
<div>Text field title: <input type="text" name="title" /></div> | ||
<div>File: <input type="file" name="someExpressFiles" multiple="multiple" /></div> | ||
<input type="submit" value="Upload" /> | ||
</form> | ||
`); | ||
}); | ||
app.post('/api/upload', (req, res, next) => { | ||
const form = formidable({}); | ||
form.parse(req, (err, fields, files) => { | ||
if (err) { | ||
next(err); | ||
return; | ||
} | ||
res.json({ fields, files }); | ||
}); | ||
}); | ||
// show a file upload form | ||
res.writeHead(200, { 'content-type': 'text/html' }); | ||
res.end(` | ||
<form action="/upload" enctype="multipart/form-data" method="post"> | ||
<input type="text" name="title" /><br/> | ||
<input type="file" name="upload" multiple="multiple" /><br/> | ||
<input type="submit" value="Upload" /> | ||
</form> | ||
`); | ||
}) | ||
.listen(8080, () => { | ||
console.log('Server listening on http://localhost:8080/ ...'); | ||
}); | ||
app.listen(3000, () => { | ||
console.log('Server listening on http://localhost:3000 ...'); | ||
}); | ||
``` | ||
### with Koa and Formidable | ||
Of course, with [Koa v1, v2 or future v3](https://ghub.now.sh/koa) the things | ||
are very similar. You can use `formidable` manually as shown below or through | ||
the [koa-better-body](https://ghub.now.sh/koa-better-body) package which is | ||
using `formidable` under the hood and support more features and different | ||
request bodies, check its documentation for more info. | ||
_Note: this example is assuming Koa v2. Be aware that you should pass `ctx.req` | ||
which is Node.js's Request, and **NOT** the `ctx.request` which is Koa's Request | ||
object - there is a difference._ | ||
```js | ||
import Koa from 'Koa'; | ||
import formidable from 'formidable'; | ||
const app = new Koa(); | ||
app.on('error', (err) => { | ||
console.error('server error', err); | ||
}); | ||
app.use(async (ctx, next) => { | ||
if (ctx.url === '/api/upload' && ctx.method.toLowerCase() === 'post') { | ||
const form = formidable({}); | ||
// not very elegant, but that's for now if you don't want to use `koa-better-body` | ||
// or other middlewares. | ||
await new Promise((resolve, reject) => { | ||
form.parse(ctx.req, (err, fields, files) => { | ||
if (err) { | ||
reject(err); | ||
return; | ||
} | ||
ctx.set('Content-Type', 'application/json'); | ||
ctx.status = 200; | ||
ctx.state = { fields, files }; | ||
ctx.body = JSON.stringify(ctx.state, null, 2); | ||
resolve(); | ||
}); | ||
}); | ||
await next(); | ||
return; | ||
} | ||
// show a file upload form | ||
ctx.set('Content-Type', 'text/html'); | ||
ctx.status = 200; | ||
ctx.body = ` | ||
<h2>With <code>"koa"</code> npm package</h2> | ||
<form action="/api/upload" enctype="multipart/form-data" method="post"> | ||
<div>Text field title: <input type="text" name="title" /></div> | ||
<div>File: <input type="file" name="koaFiles" multiple="multiple" /></div> | ||
<input type="submit" value="Upload" /> | ||
</form> | ||
`; | ||
}); | ||
app.use((ctx) => { | ||
console.log('The next middleware is called'); | ||
console.log('Results:', ctx.state); | ||
}); | ||
app.listen(3000, () => { | ||
console.log('Server listening on http://localhost:3000 ...'); | ||
}); | ||
``` | ||
## Benchmarks | ||
@@ -160,20 +304,8 @@ | ||
_Please pass [`options`](#options) to the function/constructor, not by passing | ||
assigning them to the instance `form`_ | ||
_Please pass [`options`](#options) to the function/constructor, not by assigning | ||
them to the instance `form`_ | ||
```js | ||
const formidable = require('formidable'); | ||
import formidable from 'formidable'; | ||
const form = formidable(options); | ||
// or | ||
const { formidable } = require('formidable'); | ||
const form = formidable(options); | ||
// or | ||
const { IncomingForm } = require('formidable'); | ||
const form = new IncomingForm(options); | ||
// or | ||
const { Formidable } = require('formidable'); | ||
const form = new Formidable(options); | ||
``` | ||
@@ -183,4 +315,4 @@ | ||
See it's defaults in [src/Formidable.js](./src/Formidable.js#L14-L22) (the | ||
`DEFAULT_OPTIONS` constant). | ||
See it's defaults in [src/Formidable.js DEFAULT_OPTIONS](./src/Formidable.js) | ||
(the `DEFAULT_OPTIONS` constant). | ||
@@ -190,25 +322,48 @@ - `options.encoding` **{string}** - default `'utf-8'`; sets encoding for | ||
- `options.uploadDir` **{string}** - default `os.tmpdir()`; the directory for | ||
placing file uploads in. You can move them later by using `fs.rename()` | ||
placing file uploads in. You can move them later by using `fs.rename()`. | ||
- `options.keepExtensions` **{boolean}** - default `false`; to include the | ||
extensions of the original files or not | ||
- `options.allowEmptyFiles` **{boolean}** - default `true`; allow upload empty | ||
files | ||
- `options.minFileSize` **{number}** - default `1` (1byte); the minium size of | ||
uploaded file. | ||
- `options.maxFileSize` **{number}** - default `200 * 1024 * 1024` (200mb); | ||
limit the size of uploaded file. | ||
- `options.maxFields` **{number}** - default `1000`; limit the number of fields | ||
that the Querystring parser will decode, set 0 for unlimited | ||
- `options.maxFields` **{number}** - default `1000`; limit the number of fields, set 0 for unlimited | ||
- `options.maxFieldsSize` **{number}** - default `20 * 1024 * 1024` (20mb); | ||
limit the amount of memory all fields together (except files) can allocate in | ||
bytes. | ||
- `options.hash` **{boolean}** - default `false`; include checksums calculated | ||
- `options.hashAlgorithm` **{string | false}** - default `false`; include checksums calculated | ||
for incoming files, set this to some hash algorithm, see | ||
[crypto.createHash](https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm_options) | ||
for available algorithms | ||
- `options.multiples` **{boolean}** - default `false`; when you call the | ||
`.parse` method, the `files` argument (of the callback) will contain arrays of | ||
files for inputs which submit multiple files using the HTML5 `multiple` | ||
attribute. Also, the `fields` argument will contain arrays of values for | ||
fields that have names ending with '[]'. | ||
- `options.fileWriteStreamHandler` **{function}** - default `null`, which by | ||
default writes to host machine file system every file parsed; The function | ||
should return an instance of a | ||
[Writable stream](https://nodejs.org/api/stream.html#stream_class_stream_writable) | ||
that will receive the uploaded file data. With this option, you can have any | ||
custom behavior regarding where the uploaded file data will be streamed for. | ||
If you are looking to write the file uploaded in other types of cloud storages | ||
(AWS S3, Azure blob storage, Google cloud storage) or private file storage, | ||
this is the option you're looking for. When this option is defined the default | ||
behavior of writing the file in the host machine file system is lost. | ||
- `options.filename` **{function}** - default `undefined` Use it to control | ||
newFilename. Must return a string. Will be joined with options.uploadDir. | ||
_**Note:** If this value is exceeded, an `'error'` event is emitted._ | ||
- `options.filter` **{function}** - default function that always returns true. | ||
Use it to filter files before they are uploaded. Must return a boolean. | ||
#### `options.filename` **{function}** function (name, ext, part, form) -> string | ||
where part can be decomposed as | ||
```js | ||
const { originalFilename, mimetype} = part; | ||
``` | ||
_**Note:** If this size of combined fields, or size of some file is exceeded, an | ||
`'error'` event is fired._ | ||
```js | ||
// The amount of bytes received for this form so far. | ||
@@ -223,2 +378,16 @@ form.bytesReceived; | ||
#### `options.filter` **{function}** function ({name, originalFilename, mimetype}) -> boolean | ||
**Note:** use an outside variable to cancel all uploads upon the first error | ||
```js | ||
const options = { | ||
filter: function ({name, originalFilename, mimetype}) { | ||
// keep only images | ||
return mimetype && mimetype.includes("image"); | ||
} | ||
}; | ||
``` | ||
### .parse(request, callback) | ||
@@ -230,6 +399,4 @@ | ||
```js | ||
const formidable = require('formidable'); | ||
const form = formidable({ uploadDir: __dirname }); | ||
const form = formidable({ multiples: true, uploadDir: __dirname }); | ||
form.parse(req, (err, fields, files) => { | ||
@@ -246,2 +413,37 @@ console.log('fields:', fields); | ||
About `uploadDir`, given the following directory structure | ||
``` | ||
project-name | ||
├── src | ||
│ └── server.js | ||
│ | ||
└── uploads | ||
└── image.jpg | ||
``` | ||
`__dirname` would be the same directory as the source file itself (src) | ||
```js | ||
`${__dirname}/../uploads` | ||
``` | ||
to put files in uploads. | ||
Omitting `__dirname` would make the path relative to the current working directory. This would be the same if server.js is launched from src but not project-name. | ||
`null` will use default which is `os.tmpdir()` | ||
Note: If the directory does not exist, the uploaded files are __silently discarded__. To make sure it exists: | ||
```js | ||
import {createNecessaryDirectoriesSync} from "filesac"; | ||
const uploadPath = `${__dirname}/../uploads`; | ||
createNecessaryDirectoriesSync(`${uploadPath}/x`); | ||
``` | ||
In the example below, we listen on couple of events and direct them to the | ||
@@ -258,8 +460,8 @@ `data` listener, so you can do whatever you choose there, based on whether its | ||
form.on('fileBegin', (filename, file) => { | ||
form.emit('data', { name: 'fileBegin', filename, value: file }); | ||
form.on('fileBegin', (formname, file) => { | ||
form.emit('data', { name: 'fileBegin', formname, value: file }); | ||
}); | ||
form.on('file', (filename, file) => { | ||
form.emit('data', { name: 'file', key: filename, value: file }); | ||
form.on('file', (formname, file) => { | ||
form.emit('data', { name: 'file', formname, value: file }); | ||
}); | ||
@@ -276,3 +478,3 @@ | ||
// If you want to customize whatever you want... | ||
form.on('data', ({ name, key, value, buffer, start, end, ...more }) => { | ||
form.on('data', ({ name, key, value, buffer, start, end, formname, ...more }) => { | ||
if (name === 'partBegin') { | ||
@@ -295,6 +497,6 @@ } | ||
if (name === 'file') { | ||
console.log('file:', key, value); | ||
console.log('file:', formname, value); | ||
} | ||
if (name === 'fileBegin') { | ||
console.log('fileBegin:', key, value); | ||
console.log('fileBegin:', formname, value); | ||
} | ||
@@ -325,4 +527,2 @@ }); | ||
```js | ||
const formidable = require('formidable'); | ||
const form = formidable({ keepExtensions: true }); | ||
@@ -352,7 +552,6 @@ | ||
```js | ||
const { Formidable } = require('formidable'); | ||
const form = new Formidable({ | ||
hash: 'sha1', | ||
enabledPlugins: ['octetstream', 'querystring', 'json'], | ||
import formidable, {octetstream, querystring, json} from "formidable"; | ||
const form = formidable({ | ||
hashAlgorithm: 'sha1', | ||
enabledPlugins: [octetstream, querystring, json], | ||
}); | ||
@@ -379,3 +578,3 @@ ``` | ||
form.onPart = (part) => { | ||
part.on('data', (buffer) { | ||
part.on('data', (buffer) => { | ||
// do whatever you want here | ||
@@ -392,7 +591,7 @@ }); | ||
form.onPart = function(part) { | ||
form.onPart = function (part) { | ||
// let formidable handle only non-file parts | ||
if (part.filename === '' || !part.mime) { | ||
if (part.originalFilename === '' || !part.mimetype) { | ||
// used internally, please do not override! | ||
form.handlePart(part); | ||
form._handlePart(part); | ||
} | ||
@@ -413,16 +612,20 @@ }; | ||
// case you are unhappy with the way formidable generates a temporary path for your files. | ||
file.path: string; | ||
file.filepath: string; | ||
// The name this file had according to the uploading client. | ||
file.name: string | null; | ||
file.originalFilename: string | null; | ||
// calculated based on options provided | ||
file.newFilename: string | null; | ||
// The mime type of this file, according to the uploading client. | ||
file.type: string | null; | ||
file.mimetype: string | null; | ||
// A Date object (or `null`) containing the time this file was last written to. | ||
// Mostly here for compatibility with the [W3C File API Draft](http://dev.w3.org/2006/webapi/FileAPI/). | ||
file.lastModifiedDate: Date | null; | ||
file.mtime: Date | null; | ||
// If `options.hash` calculation was set, you can read the hex digest out of this var. | ||
file.hash: string | 'sha1' | 'md5' | 'sha256' | null; | ||
file.hashAlgorithm: false | |'sha1' | 'md5' | 'sha256' | ||
// If `options.hashAlgorithm` calculation was set, you can read the hex digest out of this var (at the end it will be a string) | ||
file.hash: string | object | null; | ||
} | ||
@@ -442,3 +645,3 @@ ``` | ||
Emitted after each incoming chunk of data that has been parsed. Can be used to | ||
roll your own progress bar. | ||
roll your own progress bar. **Warning** Use this only for server side progress bar. On the client side better use `XMLHttpRequest` with `xhr.upload.onprogress =` | ||
@@ -464,3 +667,10 @@ ```js | ||
```js | ||
form.on('fileBegin', (name, file) => {}); | ||
form.on('fileBegin', (formName, file) => { | ||
// accessible here | ||
// formName the name in the form (<input name="thisname" type="file">) or http filename for octetstream | ||
// file.originalFilename http filename or null if there was a parsing error | ||
// file.newFilename generated hexoid or what options.filename returned | ||
// file.filepath default pathnme as per options.uploadDir and options.filename | ||
// file.filepath = CUSTOM_PATH // to change the final path | ||
}); | ||
``` | ||
@@ -474,3 +684,7 @@ | ||
```js | ||
form.on('file', (name, file) => {}); | ||
form.on('file', (formname, file) => { | ||
// same as fileBegin, except | ||
// it is too late to change file.filepath | ||
// file.hash is available if options.hash was used | ||
}); | ||
``` | ||
@@ -484,2 +698,4 @@ | ||
May have `error.httpCode` and `error.code` attached. | ||
```js | ||
@@ -509,2 +725,42 @@ form.on('error', (err) => {}); | ||
### Helpers | ||
#### firstValues | ||
Gets first values of fields, like pre 3.0.0 without multiples pass in a list of optional exceptions where arrays of strings is still wanted (`<select multiple>` for example) | ||
```js | ||
import { firstValues } from 'formidable/src/helpers/firstValues.js'; | ||
// ... | ||
form.parse(request, async (error, fieldsMultiple, files) => { | ||
if (error) { | ||
//... | ||
} | ||
const exceptions = ['thisshouldbeanarray']; | ||
const fieldsSingle = firstValues(form, fieldsMultiple, exceptions); | ||
// ... | ||
``` | ||
#### readBooleans | ||
Html form input type="checkbox" only send the value "on" if checked, | ||
convert it to booleans for each input that is expected to be sent as a checkbox, only use after firstValues or similar was called. | ||
```js | ||
import { firstValues } from 'formidable/src/helpers/firstValues.js'; | ||
import { readBooleans } from 'formidable/src/helpers/readBooleans.js'; | ||
// ... | ||
form.parse(request, async (error, fieldsMultiple, files) => { | ||
if (error) { | ||
//... | ||
} | ||
const fieldsSingle = firstValues(form, fieldsMultiple); | ||
const expectedBooleans = ['checkbox1', 'wantsNewsLetter', 'hasACar']; | ||
const fieldsWithBooleans = readBooleans(fieldsSingle, expectedBooleans); | ||
// ... | ||
``` | ||
## Ports & Credits | ||
@@ -522,4 +778,4 @@ | ||
button (pencil icon) and suggest a correction. If you would like to help us fix | ||
a bug or add a new feature, please check our | ||
[Contributing Guide](./CONTRIBUTING.md). Pull requests are welcome! | ||
a bug or add a new feature, please check our [Contributing | ||
Guide][contributing-url]. Pull requests are welcome! | ||
@@ -554,2 +810,3 @@ Thanks goes to these wonderful people | ||
<td align="center"><a href="https://github.com/dmolim"><img src="https://avatars2.githubusercontent.com/u/7090374?v=4" width="100px;" alt=""/><br /><sub><b>Dmitry Ivonin</b></sub></a><br /><a href="https://github.com/node-formidable/node-formidable/commits?author=dmolim" title="Documentation">📖</a></td> | ||
<td align="center"><a href="https://audiobox.fm"><img src="https://avatars1.githubusercontent.com/u/12844?v=4" width="100px;" alt=""/><br /><sub><b>Claudio Poli</b></sub></a><br /><a href="https://github.com/node-formidable/node-formidable/commits?author=masterkain" title="Code">💻</a></td> | ||
</tr> | ||
@@ -563,2 +820,9 @@ </table> | ||
From a [Felix blog post](https://felixge.de/2013/03/11/the-pull-request-hack/): | ||
- [Sven Lito](https://github.com/svnlto) for fixing bugs and merging patches | ||
- [egirshov](https://github.com/egirshov) for contributing many improvements to the node-formidable multipart parser | ||
- [Andrew Kelley](https://github.com/superjoe30) for also helping with fixing bugs and making improvements | ||
- [Mike Frey](https://github.com/mikefrey) for contributing JSON support | ||
## License | ||
@@ -573,6 +837,4 @@ | ||
[codestyle-img]: https://badgen.net/badge/code%20style/airbnb%20%2B%20prettier/ff5a5f?icon=airbnb&cache=300 | ||
[codecov-url]: https://codecov.io/gh/node-formidable/node-formidable | ||
[codecov-img]: https://badgen.net/codecov/c/github/node-formidable/node-formidable/master?icon=codecov | ||
[build-img]: https://badgen.net/github/checks/node-formidable/node-formidable?label=build&icon=github | ||
[build-url]: https://github.com/node-formidable/node-formidable/actions?query=workflow%3Anodejs | ||
[codecov-url]: https://codecov.io/gh/node-formidable/formidable | ||
[codecov-img]: https://badgen.net/codecov/c/github/node-formidable/formidable/master?icon=codecov | ||
[npmv-canary-img]: https://badgen.net/npm/v/formidable/canary?icon=npm | ||
@@ -583,3 +845,3 @@ [npmv-dev-img]: https://badgen.net/npm/v/formidable/dev?icon=npm | ||
[license-img]: https://badgen.net/npm/license/formidable | ||
[license-url]: https://github.com/node-formidable/node-formidable/blob/master/LICENSE | ||
[license-url]: https://github.com/node-formidable/formidable/blob/master/LICENSE | ||
[chat-img]: https://badgen.net/badge/chat/on%20gitter/46BC99?icon=gitter | ||
@@ -596,2 +858,27 @@ [chat-url]: https://gitter.im/node-formidable/Lobby | ||
[npm-weekly-img]: https://badgen.net/npm/dw/formidable?icon=npm&cache=300 | ||
[npm-monthly-img]: https://badgen.net/npm/dm/formidable?icon=npm&cache=300 | ||
[npm-yearly-img]: https://badgen.net/npm/dy/formidable?icon=npm&cache=300 | ||
[npm-alltime-img]: https://badgen.net/npm/dt/formidable?icon=npm&cache=300&label=total%20downloads | ||
[nodejs-img]: https://badgen.net/badge/node/>=%2010.13/green?cache=300 | ||
[ccommits-url]: https://conventionalcommits.org/ | ||
[ccommits-img]: https://badgen.net/badge/conventional%20commits/v1.0.0/green?cache=300 | ||
[contributing-url]: https://github.com/node-formidable/.github/blob/master/CONTRIBUTING.md | ||
[code_of_conduct-url]: https://github.com/node-formidable/.github/blob/master/CODE_OF_CONDUCT.md | ||
[open-issue-url]: https://github.com/node-formidable/formidable/issues/new | ||
[tidelift-url]: https://tidelift.com/subscription/pkg/npm-formidable?utm_source=npm-formidable&utm_medium=referral&utm_campaign=enterprise | ||
[tidelift-img]: https://badgen.net/badge/tidelift/subscription/4B5168?labelColor=F6914D | ||
[kofi-url]: https://ko-fi.com/tunnckoCore/commissions | ||
[kofi-img]: https://badgen.net/badge/ko-fi/support/29abe0c2?cache=300&icon=https://rawcdn.githack.com/tunnckoCore/badgen-icons/f8264c6414e0bec449dd86f2241d50a9b89a1203/icons/kofi.svg | ||
[linux-build-img]: https://badgen-net.charlike.now.sh/github/checks/node-formidable/formidable/master/ubuntu?cache=300&label=linux%20build&icon=github | ||
[macos-build-img]: https://badgen-net.charlike.now.sh/github/checks/node-formidable/formidable/master/macos?cache=300&label=macos%20build&icon=github | ||
[windows-build-img]: https://badgen-net.charlike.now.sh/github/checks/node-formidable/formidable/master/windows?cache=300&label=windows%20build&icon=github | ||
[build-url]: https://github.com/node-formidable/formidable/actions?query=workflow%3Anodejs | ||
<!-- prettier-ignore-end --> |
/* eslint-disable class-methods-use-this */ | ||
/* eslint-disable no-underscore-dangle */ | ||
'use strict'; | ||
import os from 'os'; | ||
import path from 'path'; | ||
import hexoid from 'hexoid'; | ||
import once from 'once'; | ||
import dezalgo from 'dezalgo'; | ||
import { EventEmitter } from 'events'; | ||
import { StringDecoder } from 'string_decoder'; | ||
import { octetstream, querystring, multipart, json } from './plugins/index.js'; | ||
import PersistentFile from './PersistentFile.js'; | ||
import VolatileFile from './VolatileFile.js'; | ||
import DummyParser from './parsers/Dummy.js'; | ||
import MultipartParser from './parsers/Multipart.js'; | ||
import * as errors from './FormidableError.js'; | ||
import FormidableError from './FormidableError.js'; | ||
const os = require('os'); | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const crypto = require('crypto'); | ||
const once = require('once'); | ||
const dezalgo = require('dezalgo'); | ||
const { EventEmitter } = require('events'); | ||
const { StringDecoder } = require('string_decoder'); | ||
const toHexoId = hexoid(25); | ||
const DEFAULT_OPTIONS = { | ||
@@ -19,13 +24,16 @@ maxFields: 1000, | ||
maxFileSize: 200 * 1024 * 1024, | ||
minFileSize: 1, | ||
allowEmptyFiles: true, | ||
keepExtensions: false, | ||
encoding: 'utf-8', | ||
hash: false, | ||
multiples: false, | ||
enabledPlugins: ['octetstream', 'querystring', 'multipart', 'json'], | ||
hashAlgorithm: false, | ||
uploadDir: os.tmpdir(), | ||
enabledPlugins: [octetstream, querystring, multipart, json], | ||
fileWriteStreamHandler: null, | ||
defaultInvalidName: 'invalid-name', | ||
filter() { | ||
return true; | ||
}, | ||
}; | ||
const File = require('./File'); | ||
const DummyParser = require('./parsers/Dummy'); | ||
const MultipartParser = require('./parsers/Multipart'); | ||
function hasOwnProp(obj, key) { | ||
@@ -38,15 +46,26 @@ return Object.prototype.hasOwnProperty.call(obj, key); | ||
super(); | ||
this.error = null; | ||
this.ended = false; | ||
this.options = { ...DEFAULT_OPTIONS, ...options }; | ||
this.uploadDir = this.uploadDir || os.tmpdir(); | ||
this.headers = null; | ||
this.type = null; | ||
const dir = path.resolve( | ||
this.options.uploadDir || this.options.uploaddir || os.tmpdir(), | ||
); | ||
this.bytesReceived = null; | ||
this.bytesExpected = null; | ||
this.uploaddir = dir; | ||
this.uploadDir = dir; | ||
this._parser = null; | ||
// initialize with null | ||
[ | ||
'error', | ||
'headers', | ||
'type', | ||
'bytesExpected', | ||
'bytesReceived', | ||
'_parser', | ||
].forEach((key) => { | ||
this[key] = null; | ||
}); | ||
this._setUpRename(); | ||
this._flushing = 0; | ||
@@ -58,17 +77,18 @@ this._fieldsSize = 0; | ||
const enabledPlugins = [] | ||
this.options.enabledPlugins = [] | ||
.concat(this.options.enabledPlugins) | ||
.filter(Boolean); | ||
if (enabledPlugins.length === 0) { | ||
throw new Error( | ||
if (this.options.enabledPlugins.length === 0) { | ||
throw new FormidableError( | ||
'expect at least 1 enabled builtin plugin, see options.enabledPlugins', | ||
errors.missingPlugin, | ||
); | ||
} | ||
this.options.enabledPlugins.forEach((pluginName) => { | ||
const plgName = pluginName.toLowerCase(); | ||
// eslint-disable-next-line import/no-dynamic-require, global-require | ||
this.use(require(path.join(__dirname, 'plugins', `${plgName}.js`))); | ||
this.options.enabledPlugins.forEach((plugin) => { | ||
this.use(plugin); | ||
}); | ||
this._setUpMaxFields(); | ||
} | ||
@@ -78,3 +98,6 @@ | ||
if (typeof plugin !== 'function') { | ||
throw new Error('.use: expect `plugin` to be a function'); | ||
throw new FormidableError( | ||
'.use: expect `plugin` to be a function', | ||
errors.pluginFunction, | ||
); | ||
} | ||
@@ -118,50 +141,29 @@ this._plugins.push(plugin.bind(this)); | ||
const callback = once(dezalgo(cb)); | ||
const fields = {}; | ||
this.fields = {}; | ||
let mockFields = ''; | ||
const files = {}; | ||
this.on('field', (name, value) => { | ||
// TODO: too much nesting | ||
if (this.options.multiples && name.slice(-2) === '[]') { | ||
const realName = name.slice(0, name.length - 2); | ||
if (hasOwnProp(fields, realName)) { | ||
if (!Array.isArray(fields[realName])) { | ||
fields[realName] = [fields[realName]]; | ||
} | ||
if (this.type === 'multipart' || this.type === 'urlencoded') { | ||
if (!hasOwnProp(this.fields, name)) { | ||
this.fields[name] = [value]; | ||
} else { | ||
fields[realName] = []; | ||
this.fields[name].push(value); | ||
} | ||
fields[realName].push(value); | ||
} else { | ||
fields[name] = value; | ||
this.fields[name] = value; | ||
} | ||
// if (name === 'simple') { | ||
// console.log('fields name!!', name); | ||
// console.log('fields value!!', value); | ||
// } | ||
}); | ||
this.on('file', (name, file) => { | ||
// TODO: too much nesting | ||
if (this.options.multiples) { | ||
if (hasOwnProp(files, name)) { | ||
if (!Array.isArray(files[name])) { | ||
files[name] = [files[name]]; | ||
} | ||
files[name].push(file); | ||
} else { | ||
files[name] = file; | ||
} | ||
if (!hasOwnProp(files, name)) { | ||
files[name] = [file]; | ||
} else { | ||
files[name] = file; | ||
files[name].push(file); | ||
} | ||
// console.log('files!!', files); | ||
// if (name === 'simple') { | ||
// console.log('files name!!', name); | ||
// console.log('files value!!', file); | ||
// } | ||
}); | ||
this.on('error', (err) => { | ||
callback(err, fields, files); | ||
callback(err, this.fields, files); | ||
}); | ||
this.on('end', () => { | ||
callback(null, fields, files); | ||
callback(null, this.fields, files); | ||
}); | ||
@@ -180,3 +182,3 @@ } | ||
this.emit('aborted'); | ||
this._error(new Error('Request aborted')); | ||
this._error(new FormidableError('Request aborted', errors.aborted)); | ||
}) | ||
@@ -209,3 +211,9 @@ .on('data', (buffer) => { | ||
if (!this._parser) { | ||
this._error(new Error('not parser found')); | ||
this._error( | ||
new FormidableError( | ||
'no parser found', | ||
errors.noParser, | ||
415, // Unsupported Media Type | ||
), | ||
); | ||
return; | ||
@@ -224,3 +232,5 @@ } | ||
if (!this._parser) { | ||
this._error(new Error('uninitialized parser')); | ||
this._error( | ||
new FormidableError('uninitialized parser', errors.uninitializedParser), | ||
); | ||
return null; | ||
@@ -253,8 +263,13 @@ } | ||
_handlePart(part) { | ||
if (part.filename && typeof part.filename !== 'string') { | ||
this._error(new Error(`the part.filename should be string when exists`)); | ||
if (part.originalFilename && typeof part.originalFilename !== 'string') { | ||
this._error( | ||
new FormidableError( | ||
`the part.originalFilename should be string when it exists`, | ||
errors.filenameNotString, | ||
), | ||
); | ||
return; | ||
} | ||
// This MUST check exactly for undefined. You can not change it to !part.filename. | ||
// This MUST check exactly for undefined. You can not change it to !part.originalFilename. | ||
@@ -267,5 +282,5 @@ // todo: uncomment when switch tests to Jest | ||
// and such thing because code style | ||
// ? NOTE(@tunnckocore): or even better, if there is no mime, then it's for sure a field | ||
// ? NOTE(@tunnckocore): filename is an empty string when a field? | ||
if (!part.mime) { | ||
// ? NOTE(@tunnckocore): or even better, if there is no mimetype, then it's for sure a field | ||
// ? NOTE(@tunnckocore): originalFilename is an empty string when a field? | ||
if (!part.mimetype) { | ||
let value = ''; | ||
@@ -280,4 +295,6 @@ const decoder = new StringDecoder( | ||
this._error( | ||
new Error( | ||
`options.maxFieldsSize exceeded, received ${this._fieldsSize} bytes of field data`, | ||
new FormidableError( | ||
`options.maxFieldsSize (${this.options.maxFieldsSize} bytes) exceeded, received ${this._fieldsSize} bytes of field data`, | ||
errors.maxFieldsSizeExceeded, | ||
413, // Payload Too Large | ||
), | ||
@@ -296,9 +313,15 @@ ); | ||
if (!this.options.filter(part)) { | ||
return; | ||
} | ||
this._flushing += 1; | ||
const file = new File({ | ||
path: this._uploadPath(part.filename), | ||
name: part.filename, | ||
type: part.mime, | ||
hash: this.options.hash, | ||
const newFilename = this._getNewName(part); | ||
const filepath = this._joinDirectoryName(newFilename); | ||
const file = this._newFile({ | ||
newFilename, | ||
filepath, | ||
originalFilename: part.originalFilename, | ||
mimetype: part.mimetype, | ||
}); | ||
@@ -315,6 +338,18 @@ file.on('error', (err) => { | ||
this._fileSize += buffer.length; | ||
if (this._fileSize < this.options.minFileSize) { | ||
this._error( | ||
new FormidableError( | ||
`options.minFileSize (${this.options.minFileSize} bytes) inferior, received ${this._fileSize} bytes of file data`, | ||
errors.smallerThanMinFileSize, | ||
400, | ||
), | ||
); | ||
return; | ||
} | ||
if (this._fileSize > this.options.maxFileSize) { | ||
this._error( | ||
new Error( | ||
`options.maxFileSize exceeded, received ${this._fileSize} bytes of file data`, | ||
new FormidableError( | ||
`options.maxFileSize (${this.options.maxFileSize} bytes) exceeded, received ${this._fileSize} bytes of file data`, | ||
errors.biggerThanMaxFileSize, | ||
413, | ||
), | ||
@@ -334,2 +369,13 @@ ); | ||
part.on('end', () => { | ||
if (!this.options.allowEmptyFiles && this._fileSize === 0) { | ||
this._error( | ||
new FormidableError( | ||
`options.allowEmptyFiles is false, file size should be greather than 0`, | ||
errors.noEmptyFiles, | ||
400, | ||
), | ||
); | ||
return; | ||
} | ||
file.end(() => { | ||
@@ -351,3 +397,9 @@ this._flushing -= 1; | ||
if (!this.headers['content-type']) { | ||
this._error(new Error('bad content-type header, no content-type')); | ||
this._error( | ||
new FormidableError( | ||
'bad content-type header, no content-type', | ||
errors.missingContentType, | ||
400, | ||
), | ||
); | ||
return; | ||
@@ -370,4 +422,6 @@ } | ||
// there is no other better way, except a handle through options | ||
const error = new Error( | ||
const error = new FormidableError( | ||
`plugin on index ${idx} failed with: ${err.message}`, | ||
errors.pluginFailed, | ||
500, | ||
); | ||
@@ -411,4 +465,3 @@ error.idx = idx; | ||
this.openedFiles.forEach((file) => { | ||
file._writeStream.destroy(); | ||
setTimeout(fs.unlink, 0, file.path, () => {}); | ||
file.destroy(); | ||
}); | ||
@@ -435,7 +488,25 @@ } | ||
_newFile({ filepath, originalFilename, mimetype, newFilename }) { | ||
return this.options.fileWriteStreamHandler | ||
? new VolatileFile({ | ||
newFilename, | ||
filepath, | ||
originalFilename, | ||
mimetype, | ||
createFileWriteStream: this.options.fileWriteStreamHandler, | ||
hashAlgorithm: this.options.hashAlgorithm, | ||
}) | ||
: new PersistentFile({ | ||
newFilename, | ||
filepath, | ||
originalFilename, | ||
mimetype, | ||
hashAlgorithm: this.options.hashAlgorithm, | ||
}); | ||
} | ||
_getFileName(headerValue) { | ||
// matches either a quoted-string or a token (RFC 2616 section 19.5.1) | ||
const m = headerValue.match( | ||
// eslint-disable-next-line no-useless-escape | ||
/\bfilename=("(.*?)"|([^\(\)<>@,;:\\"\/\[\]\?=\{\}\s\t/]+))($|;\s)/i, | ||
/\bfilename=("(.*?)"|([^()<>{}[\]@,;:"?=\s/\t]+))($|;\s)/i, | ||
); | ||
@@ -445,24 +516,87 @@ if (!m) return null; | ||
const match = m[2] || m[3] || ''; | ||
let filename = match.substr(match.lastIndexOf('\\') + 1); | ||
filename = filename.replace(/%22/g, '"'); | ||
filename = filename.replace(/&#([\d]{4});/g, (_, code) => | ||
let originalFilename = match.substr(match.lastIndexOf('\\') + 1); | ||
originalFilename = originalFilename.replace(/%22/g, '"'); | ||
originalFilename = originalFilename.replace(/&#([\d]{4});/g, (_, code) => | ||
String.fromCharCode(code), | ||
); | ||
return filename; | ||
return originalFilename; | ||
} | ||
_uploadPath(filename) { | ||
const buf = crypto.randomBytes(16); | ||
let name = `upload_${buf.toString('hex')}`; | ||
_getExtension(str) { | ||
if (!str) { | ||
return ''; | ||
} | ||
if (this.options.keepExtensions) { | ||
let ext = path.extname(filename); | ||
ext = ext.replace(/(\.[a-z0-9]+).*/i, '$1'); | ||
const basename = path.basename(str); | ||
const firstDot = basename.indexOf('.'); | ||
const lastDot = basename.lastIndexOf('.'); | ||
const extname = path.extname(basename).replace(/(\.[a-z0-9]+).*/i, '$1'); | ||
name += ext; | ||
if (firstDot === lastDot) { | ||
return extname; | ||
} | ||
return path.join(this.uploadDir, name); | ||
return basename.slice(firstDot, lastDot) + extname; | ||
} | ||
_joinDirectoryName(name) { | ||
const newPath = path.join(this.uploadDir, name); | ||
// prevent directory traversal attacks | ||
if (!newPath.startsWith(this.uploadDir)) { | ||
return path.join(this.uploadDir, this.options.defaultInvalidName); | ||
} | ||
return newPath; | ||
} | ||
_setUpRename() { | ||
const hasRename = typeof this.options.filename === 'function'; | ||
if (hasRename) { | ||
this._getNewName = (part) => { | ||
let ext = ''; | ||
let name = this.options.defaultInvalidName; | ||
if (part.originalFilename) { | ||
// can be null | ||
({ ext, name } = path.parse(part.originalFilename)); | ||
if (this.options.keepExtensions !== true) { | ||
ext = ''; | ||
} | ||
} | ||
return this.options.filename.call(this, name, ext, part, this); | ||
}; | ||
} else { | ||
this._getNewName = (part) => { | ||
const name = toHexoId(); | ||
if (part && this.options.keepExtensions) { | ||
const originalFilename = | ||
typeof part === 'string' ? part : part.originalFilename; | ||
return `${name}${this._getExtension(originalFilename)}`; | ||
} | ||
return name; | ||
}; | ||
} | ||
} | ||
_setUpMaxFields() { | ||
if (this.options.maxFields !== 0) { | ||
let fieldsCount = 0; | ||
this.on('field', () => { | ||
fieldsCount += 1; | ||
if (fieldsCount > this.options.maxFields) { | ||
this._error( | ||
new FormidableError( | ||
`options.maxFields (${this.options.maxFields}) exceeded`, | ||
errors.maxFieldsExceeded, | ||
413, | ||
), | ||
); | ||
} | ||
}); | ||
} | ||
} | ||
_maybeEnd() { | ||
@@ -480,3 +614,3 @@ // console.log('ended', this.ended); | ||
IncomingForm.DEFAULT_OPTIONS = DEFAULT_OPTIONS; | ||
module.exports = IncomingForm; | ||
export default IncomingForm; | ||
export { DEFAULT_OPTIONS }; |
@@ -1,8 +0,6 @@ | ||
'use strict'; | ||
import PersistentFile from './PersistentFile.js'; | ||
import VolatileFile from './VolatileFile.js'; | ||
import Formidable, { DEFAULT_OPTIONS } from './Formidable.js'; | ||
const File = require('./File'); | ||
const Formidable = require('./Formidable'); | ||
const plugins = require('./plugins/index'); | ||
const parsers = require('./parsers/index'); | ||
@@ -12,23 +10,24 @@ // make it available without requiring the `new` keyword | ||
const formidable = (...args) => new Formidable(...args); | ||
const {enabledPlugins} = DEFAULT_OPTIONS; | ||
module.exports = Object.assign(formidable, { | ||
File, | ||
export default formidable; | ||
export { | ||
PersistentFile as File, | ||
PersistentFile, | ||
VolatileFile, | ||
Formidable, | ||
// alias | ||
Formidable as IncomingForm, | ||
// as named | ||
formidable, | ||
// alias | ||
IncomingForm: Formidable, | ||
// parsers | ||
...parsers, | ||
parsers, | ||
// misc | ||
defaultOptions: Formidable.DEFAULT_OPTIONS, | ||
enabledPlugins: Formidable.DEFAULT_OPTIONS.enabledPlugins, | ||
DEFAULT_OPTIONS as defaultOptions, | ||
enabledPlugins, | ||
}; | ||
// plugins | ||
plugins: { | ||
...plugins, | ||
}, | ||
}); | ||
export * from './parsers/index.js'; | ||
export * from './plugins/index.js'; | ||
export * as errors from './FormidableError.js'; |
/* eslint-disable no-underscore-dangle */ | ||
'use strict'; | ||
import { Transform } from 'stream'; | ||
const { Transform } = require('stream'); | ||
class DummyParser extends Transform { | ||
@@ -21,2 +19,2 @@ constructor(incomingForm, options = {}) { | ||
module.exports = DummyParser; | ||
export default DummyParser; |
@@ -1,10 +0,8 @@ | ||
'use strict'; | ||
import JSONParser from './JSON.js'; | ||
import DummyParser from './Dummy.js'; | ||
import MultipartParser from './Multipart.js'; | ||
import OctetStreamParser from './OctetStream.js'; | ||
import QueryStringParser from './Querystring.js'; | ||
const JSONParser = require('./JSON'); | ||
const DummyParser = require('./Dummy'); | ||
const MultipartParser = require('./Multipart'); | ||
const OctetStreamParser = require('./OctetStream'); | ||
const QueryStringParser = require('./Querystring'); | ||
Object.assign(exports, { | ||
export { | ||
JSONParser, | ||
@@ -14,5 +12,5 @@ DummyParser, | ||
OctetStreamParser, | ||
OctetstreamParser: OctetStreamParser, | ||
OctetStreamParser as OctetstreamParser, | ||
QueryStringParser, | ||
QuerystringParser: QueryStringParser, | ||
}); | ||
QueryStringParser as QuerystringParser, | ||
}; |
/* eslint-disable no-underscore-dangle */ | ||
'use strict'; | ||
import { Transform } from 'stream'; | ||
const { Transform } = require('stream'); | ||
class JSONParser extends Transform { | ||
@@ -22,6 +20,3 @@ constructor(options = {}) { | ||
const fields = JSON.parse(this.chunks.join('')); | ||
Object.keys(fields).forEach((key) => { | ||
const value = fields[key]; | ||
this.push({ key, value }); | ||
}); | ||
this.push(fields); | ||
} catch (e) { | ||
@@ -36,2 +31,2 @@ callback(e); | ||
module.exports = JSONParser; | ||
export default JSONParser; |
@@ -6,6 +6,6 @@ /* eslint-disable no-fallthrough */ | ||
'use strict'; | ||
import { Transform } from 'stream'; | ||
import * as errors from '../FormidableError.js'; | ||
import FormidableError from '../FormidableError.js'; | ||
const { Transform } = require('stream'); | ||
let s = 0; | ||
@@ -43,6 +43,6 @@ const STATE = { | ||
exports.STATES = {}; | ||
export const STATES = {}; | ||
Object.keys(STATE).forEach((stateName) => { | ||
exports.STATES[stateName] = STATE[stateName]; | ||
STATES[stateName] = STATE[stateName]; | ||
}); | ||
@@ -64,3 +64,3 @@ | ||
_final(done) { | ||
_flush(done) { | ||
if ( | ||
@@ -75,4 +75,6 @@ (this.state === STATE.HEADER_FIELD_START && this.index === 0) || | ||
done( | ||
new Error( | ||
new FormidableError( | ||
`MultipartParser.end(): stream ended unexpectedly: ${this.explain()}`, | ||
errors.malformedMultipart, | ||
400, | ||
), | ||
@@ -115,3 +117,3 @@ ); | ||
const setMark = (name, idx) => { | ||
this[`${name}Mark`] = idx || i; | ||
this[`${name}Mark`] = typeof idx === 'number' ? idx : i; | ||
}; | ||
@@ -131,6 +133,6 @@ | ||
this._handleCallback(name, buffer, this[markSymbol], buffer.length); | ||
setMark(markSymbol, 0); | ||
setMark(name, 0); | ||
} else { | ||
this._handleCallback(name, buffer, this[markSymbol], i); | ||
clearMarkSymbol(markSymbol); | ||
clearMarkSymbol(name); | ||
} | ||
@@ -348,2 +350,2 @@ }; | ||
module.exports = Object.assign(MultipartParser, { STATES: exports.STATES }); | ||
export default Object.assign(MultipartParser, { STATES }); |
@@ -1,5 +0,3 @@ | ||
'use strict'; | ||
import { PassThrough } from 'stream'; | ||
const { PassThrough } = require('stream'); | ||
class OctetStreamParser extends PassThrough { | ||
@@ -12,2 +10,2 @@ constructor(options = {}) { | ||
module.exports = OctetStreamParser; | ||
export default OctetStreamParser; |
/* eslint-disable no-underscore-dangle */ | ||
'use strict'; | ||
import { Transform } from 'stream'; | ||
const { Transform } = require('stream'); | ||
const querystring = require('querystring'); | ||
// This is a buffering parser, not quite as nice as the multipart one. | ||
@@ -14,3 +11,2 @@ // If I find time I'll rewrite this to be fully streaming as well | ||
this.globalOptions = { ...options }; | ||
this.maxKeys = this.globalOptions.maxFields; | ||
this.buffer = ''; | ||
@@ -27,10 +23,7 @@ this.bufferLength = 0; | ||
_flush(callback) { | ||
const fields = querystring.parse(this.buffer, '&', '=', { | ||
maxKeys: this.maxKeys, | ||
}); | ||
// eslint-disable-next-line no-restricted-syntax, guard-for-in | ||
for (const key in fields) { | ||
const fields = new URLSearchParams(this.buffer); | ||
for (const [key, value] of fields) { | ||
this.push({ | ||
key, | ||
value: fields[key], | ||
value, | ||
}); | ||
@@ -43,2 +36,2 @@ } | ||
module.exports = QuerystringParser; | ||
export default QuerystringParser; |
@@ -1,13 +0,6 @@ | ||
'use strict'; | ||
import octetstream from './octetstream.js'; | ||
import querystring from './querystring.js'; | ||
import multipart from './multipart.js'; | ||
import json from './json.js'; | ||
const octetstream = require('./octetstream'); | ||
const querystring = require('./querystring'); | ||
const multipart = require('./multipart'); | ||
const json = require('./json'); | ||
Object.assign(exports, { | ||
octetstream, | ||
querystring, | ||
multipart, | ||
json, | ||
}); | ||
export { octetstream, querystring, multipart, json }; |
/* eslint-disable no-underscore-dangle */ | ||
'use strict'; | ||
import JSONParser from '../parsers/JSON.js'; | ||
const JSONParser = require('../parsers/JSON'); | ||
export const jsonType = 'json'; | ||
// the `options` is also available through the `this.options` / `formidable.options` | ||
module.exports = function plugin(formidable, options) { | ||
export default function plugin(formidable, options) { | ||
// the `this` context is always formidable, as the first argument of a plugin | ||
@@ -24,8 +23,8 @@ // but this allows us to customize/test each plugin | ||
function init(_self, _opts) { | ||
this.type = 'json'; | ||
this.type = jsonType; | ||
const parser = new JSONParser(this.options); | ||
parser.on('data', ({ key, value }) => { | ||
this.emit('field', key, value); | ||
parser.on('data', (fields) => { | ||
this.fields = fields; | ||
}); | ||
@@ -32,0 +31,0 @@ |
/* eslint-disable no-underscore-dangle */ | ||
'use strict'; | ||
import { Stream } from 'stream'; | ||
import MultipartParser from '../parsers/Multipart.js'; | ||
import * as errors from '../FormidableError.js'; | ||
import FormidableError from '../FormidableError.js'; | ||
const { Stream } = require('stream'); | ||
const MultipartParser = require('../parsers/Multipart'); | ||
export const multipartType = 'multipart'; | ||
// the `options` is also available through the `options` / `formidable.options` | ||
module.exports = function plugin(formidable, options) { | ||
export default function plugin(formidable, options) { | ||
// the `this` context is always formidable, as the first argument of a plugin | ||
@@ -16,3 +17,6 @@ // but this allows us to customize/test each plugin | ||
if (/multipart\/form-data/i.test(self.headers['content-type'])) { | ||
// NOTE: we (currently) support both multipart/form-data and multipart/related | ||
const multipart = /multipart/i.test(self.headers['content-type']); | ||
if (multipart) { | ||
const m = self.headers['content-type'].match( | ||
@@ -23,9 +27,13 @@ /boundary=(?:"([^"]+)"|([^;]+))/i, | ||
const initMultipart = createInitMultipart(m[1] || m[2]); | ||
initMultipart.call(self, self, options); | ||
initMultipart.call(self, self, options); // lgtm [js/superfluous-trailing-arguments] | ||
} else { | ||
const err = new Error('bad content-type header, no multipart boundary'); | ||
const err = new FormidableError( | ||
'bad content-type header, no multipart boundary', | ||
errors.missingMultipartBoundary, | ||
400, | ||
); | ||
self._error(err); | ||
} | ||
} | ||
}; | ||
} | ||
@@ -37,3 +45,3 @@ // Note that it's a good practice (but it's up to you) to use the `this.options` instead | ||
return function initMultipart() { | ||
this.type = 'multipart'; | ||
this.type = multipartType; | ||
@@ -54,6 +62,6 @@ const parser = new MultipartParser(this.options); | ||
part.name = null; | ||
part.filename = null; | ||
part.mime = null; | ||
part.originalFilename = null; | ||
part.mimetype = null; | ||
part.transferEncoding = 'binary'; | ||
part.transferEncoding = this.options.encoding; | ||
part.transferBuffer = ''; | ||
@@ -81,5 +89,5 @@ | ||
part.filename = this._getFileName(headerValue); | ||
part.originalFilename = this._getFileName(headerValue); | ||
} else if (headerField === 'content-type') { | ||
part.mime = headerValue; | ||
part.mimetype = headerValue; | ||
} else if (headerField === 'content-transfer-encoding') { | ||
@@ -95,3 +103,4 @@ part.transferEncoding = headerValue.toLowerCase(); | ||
case '7bit': | ||
case '8bit': { | ||
case '8bit': | ||
case 'utf-8': { | ||
const dataPropagation = (ctx) => { | ||
@@ -150,3 +159,9 @@ if (ctx.name === 'partData') { | ||
default: | ||
return this._error(new Error('unknown transfer-encoding')); | ||
return this._error( | ||
new FormidableError( | ||
'unknown transfer-encoding', | ||
errors.unknownTransferEncoding, | ||
501, | ||
), | ||
); | ||
} | ||
@@ -153,0 +168,0 @@ |
/* eslint-disable no-underscore-dangle */ | ||
'use strict'; | ||
import OctetStreamParser from '../parsers/OctetStream.js'; | ||
const File = require('../File'); | ||
const OctetStreamParser = require('../parsers/OctetStream'); | ||
export const octetStreamType = 'octet-stream'; | ||
// the `options` is also available through the `options` / `formidable.options` | ||
module.exports = function plugin(formidable, options) { | ||
export default function plugin(formidable, options) { | ||
// the `this` context is always formidable, as the first argument of a plugin | ||
@@ -21,3 +19,3 @@ // but this allows us to customize/test each plugin | ||
return self; | ||
}; | ||
} | ||
@@ -28,14 +26,20 @@ // Note that it's a good practice (but it's up to you) to use the `this.options` instead | ||
function init(_self, _opts) { | ||
this.type = 'octet-stream'; | ||
const filename = this.headers['x-file-name']; | ||
const mime = this.headers['content-type']; | ||
this.type = octetStreamType; | ||
const originalFilename = this.headers['x-file-name']; | ||
const mimetype = this.headers['content-type']; | ||
const file = new File({ | ||
path: this._uploadPath(filename), | ||
name: filename, | ||
type: mime, | ||
hash: this.options.hash, | ||
const thisPart = { | ||
originalFilename, | ||
mimetype, | ||
}; | ||
const newFilename = this._getNewName(thisPart); | ||
const filepath = this._joinDirectoryName(newFilename); | ||
const file = this._newFile({ | ||
newFilename, | ||
filepath, | ||
originalFilename, | ||
mimetype, | ||
}); | ||
this.emit('fileBegin', filename, file); | ||
this.emit('fileBegin', originalFilename, file); | ||
file.open(); | ||
@@ -42,0 +46,0 @@ this.openedFiles.push(file); |
/* eslint-disable no-underscore-dangle */ | ||
'use strict'; | ||
const QuerystringParser = require('../parsers/Querystring'); | ||
import QuerystringParser from '../parsers/Querystring.js'; | ||
export const querystringType = 'urlencoded'; | ||
// the `options` is also available through the `this.options` / `formidable.options` | ||
module.exports = function plugin(formidable, options) { | ||
export default function plugin(formidable, options) { | ||
// the `this` context is always formidable, as the first argument of a plugin | ||
@@ -26,3 +26,3 @@ // but this allows us to customize/test each plugin | ||
function init(_self, _opts) { | ||
this.type = 'urlencoded'; | ||
this.type = querystringType; | ||
@@ -29,0 +29,0 @@ const parser = new QuerystringParser(this.options); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
0
855
2
1
Yes
98748
3
20
23
1527
+ Addedhexoid@1.0.0
+ Addeddezalgo@1.0.3(transitive)
+ Addedhexoid@1.0.0(transitive)
- Removeddezalgo@1.0.4(transitive)
Updateddezalgo@1.0.3
Updatedonce@1.4.0