diff --git a/.travis.yml b/.travis.yml index 274308b2..033d60af 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,8 @@ language: node_js node_js: - - '13' - - '12' - - '10' - - '10.13.0' + - '16.0.0' + - '14.13.1' + - '12.20.0' os: - linux - osx diff --git a/README.md b/README.md index f46c3adb..f8b7dbf2 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ Multer adds a `body` object and a `file` or `files` object to the `request` obje Basic usage example: ```javascript -const multer = require('multer') -const express = require('express') +import multer from 'multer' +import express from 'express' const app = express() const upload = multer() @@ -49,8 +49,8 @@ app.post('/cool-profile', cpUpload, (req, res, next) => { In case you need to handle a text-only multipart form, you can use the `.none()` method, example: ```javascript -const multer = require('multer') -const express = require('express') +import multer from 'multer' +import express from 'express' const app = express() const upload = multer() diff --git a/index.js b/index.js index 76acc2ad..a6bcf85c 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ -const createFileFilter = require('./lib/file-filter') -const createMiddleware = require('./lib/middleware') +import bytes from 'bytes' -const bytes = require('bytes') +import createFileFilter from './lib/file-filter.js' +import createMiddleware from './lib/middleware.js' const kLimits = Symbol('limits') @@ -60,7 +60,7 @@ class Multer { } } -function multer (options = {}) { +export default function multer (options = {}) { if (options === null) throw new TypeError('Expected object for argument "options", got null') if (typeof options !== 'object') throw new TypeError(`Expected object for argument "options", got ${typeof options}`) @@ -70,5 +70,3 @@ function multer (options = {}) { return new Multer(options) } - -module.exports = multer diff --git a/lib/error.js b/lib/error.js index f25a5af0..92b6171d 100644 --- a/lib/error.js +++ b/lib/error.js @@ -8,7 +8,7 @@ const errorMessages = new Map([ ['LIMIT_UNEXPECTED_FILE', 'Unexpected file field'] ]) -class MulterError extends Error { +export default class MulterError extends Error { constructor (code, optionalField) { super(errorMessages.get(code)) @@ -19,5 +19,3 @@ class MulterError extends Error { Error.captureStackTrace(this, this.constructor) } } - -module.exports = MulterError diff --git a/lib/file-appender.js b/lib/file-appender.js index 6ad55474..55eaa50b 100644 --- a/lib/file-appender.js +++ b/lib/file-appender.js @@ -1,10 +1,10 @@ -function createFileAppender (strategy, req, fields) { +export default function createFileAppender (strategy, req, fields) { switch (strategy) { case 'NONE': break case 'VALUE': req.file = null; break case 'ARRAY': req.files = []; break case 'OBJECT': req.files = Object.create(null); break - // istanbul ignore next + /* c8 ignore next */ default: throw new Error(`Unknown file strategy: ${strategy}`) } @@ -22,5 +22,3 @@ function createFileAppender (strategy, req, fields) { } } } - -module.exports = createFileAppender diff --git a/lib/file-filter.js b/lib/file-filter.js index 92b0239f..f364735a 100644 --- a/lib/file-filter.js +++ b/lib/file-filter.js @@ -1,6 +1,6 @@ -const MulterError = require('./error') +import MulterError from './error.js' -function createFileFilter (fields) { +export default function createFileFilter (fields) { const filesLeft = new Map() for (const field of fields) { @@ -25,5 +25,3 @@ function createFileFilter (fields) { filesLeft.set(file.fieldName, left - 1) } } - -module.exports = createFileFilter diff --git a/lib/middleware.js b/lib/middleware.js index 7c818b5d..f19611bf 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -1,9 +1,10 @@ -const is = require('type-is') -const fs = require('fs') -const appendField = require('append-field') +import fs from 'node:fs' -const createFileAppender = require('./file-appender') -const readBody = require('./read-body') +import appendField from 'append-field' +import is from 'type-is' + +import createFileAppender from './file-appender.js' +import readBody from './read-body.js' async function handleRequest (setup, req) { const options = setup() @@ -25,11 +26,9 @@ async function handleRequest (setup, req) { } } -function createMiddleware (setup) { - return function multerMiddleware (req, res, next) { +export default function createMiddleware (setup) { + return function multerMiddleware (req, _, next) { if (!is(req, ['multipart'])) return next() handleRequest(setup, req).then(next, next) } } - -module.exports = createMiddleware diff --git a/lib/read-body.js b/lib/read-body.js index 740458d4..f2bad972 100644 --- a/lib/read-body.js +++ b/lib/read-body.js @@ -1,14 +1,16 @@ -const path = require('path') -const pify = require('pify') -const temp = require('fs-temp') -const Busboy = require('busboy') -const FileType = require('stream-file-type') -const hasOwnProperty = require('has-own-property') +import { extname } from 'node:path' +import { pipeline } from 'node:stream/promises' +import { promisify } from 'node:util' -const pump = pify(require('pump')) -const onFinished = pify(require('on-finished')) +import Busboy from 'busboy' +import { createWriteStream } from 'fs-temp' +import hasOwnProperty from 'has-own-property' +import _onFinished from 'on-finished' +import FileType from 'stream-file-type' -const MulterError = require('./error') +import MulterError from './error.js' + +const onFinished = promisify(_onFinished) function drainStream (stream) { stream.on('readable', stream.read.bind(stream)) @@ -19,7 +21,8 @@ function collectFields (busboy, limits) { const result = [] busboy.on('field', (fieldname, value, fieldnameTruncated, valueTruncated) => { - // istanbul ignore next: Currently not implemented (https://github.com/mscdex/busboy/issues/6) + // Currently not implemented (https://github.com/mscdex/busboy/issues/6) + /* c8 ignore next */ if (fieldnameTruncated) return reject(new MulterError('LIMIT_FIELD_KEY')) if (valueTruncated) return reject(new MulterError('LIMIT_FIELD_VALUE', fieldname)) @@ -58,7 +61,7 @@ function collectFiles (busboy, limits, fileFilter) { fieldName: fieldname, originalName: filename, clientReportedMimeType: mimetype, - clientReportedFileExtension: path.extname(filename || '') + clientReportedFileExtension: extname(filename || '') } try { @@ -67,11 +70,11 @@ function collectFiles (busboy, limits, fileFilter) { return reject(err) } - const target = temp.createWriteStream() + const target = createWriteStream() const detector = new FileType() const fileClosed = new Promise((resolve) => target.on('close', resolve)) - const promise = pump(fileStream, detector, target) + const promise = pipeline(fileStream, detector, target) .then(async () => { await fileClosed file.path = target.path @@ -92,7 +95,7 @@ function collectFiles (busboy, limits, fileFilter) { }) } -async function readBody (req, limits, fileFilter) { +export default async function readBody (req, limits, fileFilter) { const busboy = new Busboy({ headers: req.headers, limits: limits }) const fields = collectFields(busboy, limits) @@ -119,10 +122,8 @@ async function readBody (req, limits, fileFilter) { busboy.removeAllListeners() // Wait for request to close, finish, or error - await onFinished(req).catch(/* istanbul ignore next: Already handled by req.on('error', _) */ () => {}) + await onFinished(req).catch(/* c8 ignore next: Already handled by req.on('error', _) */ () => {}) throw err } } - -module.exports = readBody diff --git a/package.json b/package.json index 3d3b61e4..25a8676c 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ ], "license": "MIT", "repository": "expressjs/multer", + "type": "module", + "exports": "./index.js", "keywords": [ "form", "post", @@ -19,31 +21,28 @@ "middleware" ], "dependencies": { - "append-field": "^1.0.0", + "append-field": "^2.0.0", "busboy": "^0.3.1", "bytes": "^3.1.0", - "fs-temp": "^1.1.1", - "has-own-property": "^1.0.0", + "fs-temp": "^2.0.0", + "has-own-property": "^2.0.0", "on-finished": "^2.3.0", - "pify": "^5.0.0", - "pump": "^3.0.0", - "stream-file-type": "^0.5.0", + "stream-file-type": "^0.6.1", "type-is": "^1.6.18" }, "devDependencies": { - "assert-rejects": "^1.0.0", + "c8": "^7.7.3", "express": "^4.16.4", - "form-data": "^3.0.0", - "get-stream": "^5.1.0", + "form-data": "^4.0.0", + "get-stream": "^6.0.1", "hasha": "^5.2.0", - "mocha": "^7.1.0", - "nyc": "^15.0.0", + "mocha": "^9.0.3", "recursive-nullify": "^1.0.0", - "standard": "^14.3.3", + "standard": "^16.0.3", "testdata-w3c-json-form": "^1.0.0" }, "engines": { - "node": ">=10.13" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "files": [ "LICENSE", @@ -51,6 +50,6 @@ "lib/" ], "scripts": { - "test": "standard && nyc --check-coverage --statements 100 mocha" + "test": "standard && c8 --check-coverage --statements 100 mocha" } } diff --git a/test/_util.js b/test/_util.js index 73fc083c..8cbc8fbb 100644 --- a/test/_util.js +++ b/test/_util.js @@ -1,11 +1,12 @@ -const fs = require('fs') -const path = require('path') -const pify = require('pify') -const hasha = require('hasha') -const assert = require('assert') -const stream = require('stream') +import assert from 'node:assert' +import fs from 'node:fs' +import stream from 'node:stream' +import { promisify } from 'node:util' -const onFinished = pify(require('on-finished')) +import hasha from 'hasha' +import _onFinished from 'on-finished' + +const onFinished = promisify(_onFinished) const files = new Map([ ['empty', { @@ -50,15 +51,15 @@ const files = new Map([ }] ]) -exports.file = function file (name) { - return fs.createReadStream(path.join(__dirname, 'files', name + files.get(name).extension)) +export function file (name) { + return fs.createReadStream(new URL(`files/${name}${files.get(name).extension}`, import.meta.url)) } -exports.knownFileLength = function knownFileLength (name) { +export function knownFileLength (name) { return files.get(name).size } -exports.assertFile = async (file, fieldName, fileName) => { +export async function assertFile (file, fieldName, fileName) { if (!files.has(fileName)) { throw new Error(`No file named "${fileName}"`) } @@ -80,15 +81,15 @@ exports.assertFile = async (file, fieldName, fileName) => { assert.strictEqual(hash, expected.hash) } -exports.assertFiles = (files) => { - return Promise.all(files.map((args) => exports.assertFile(args[0], args[1], args[2]))) +export async function assertFiles (files) { + await Promise.all(files.map((args) => assertFile(args[0], args[1], args[2]))) } function getLength (form) { - return pify(form.getLength).call(form) + return promisify(form.getLength).call(form) } -exports.submitForm = async (multer, form) => { +export async function submitForm (multer, form) { const length = await getLength(form) const req = new stream.PassThrough() @@ -101,7 +102,7 @@ exports.submitForm = async (multer, form) => { 'content-length': length } - await pify(multer)(req, null) + await promisify(multer)(req, null) await onFinished(req) return req diff --git a/test/aborted-requests.js b/test/aborted-requests.js index 9da01299..5a75972b 100644 --- a/test/aborted-requests.js +++ b/test/aborted-requests.js @@ -1,15 +1,16 @@ /* eslint-env mocha */ -const util = require('./_util') -const multer = require('../') +import assert from 'node:assert' +import { PassThrough } from 'node:stream' +import { promisify } from 'node:util' -const assertRejects = require('assert-rejects') -const FormData = require('form-data') -const PassThrough = require('stream').PassThrough -const pify = require('pify') +import FormData from 'form-data' + +import * as util from './_util.js' +import multer from '../index.js' function getLength (form) { - return pify(form.getLength).call(form) + return promisify(form.getLength).call(form) } function createAbortStream (maxBytes, aborter) { @@ -50,9 +51,9 @@ describe('Aborted requests', () => { 'content-length': length } - const result = pify(parser)(form.pipe(req), null) + const result = promisify(parser)(form.pipe(req), null) - return assertRejects(result, err => err.code === 'CLIENT_ABORTED') + return assert.rejects(result, err => err.code === 'CLIENT_ABORTED') }) it('should handle clients erroring the request', async () => { @@ -69,8 +70,8 @@ describe('Aborted requests', () => { 'content-length': length } - const result = pify(parser)(form.pipe(req), null) + const result = promisify(parser)(form.pipe(req), null) - return assertRejects(result, err => err.message === 'TEST_ERROR') + return assert.rejects(result, err => err.message === 'TEST_ERROR') }) }) diff --git a/test/body.js b/test/body.js index 9f507445..bd25a61c 100644 --- a/test/body.js +++ b/test/body.js @@ -1,15 +1,16 @@ /* eslint-env mocha */ -const assert = require('assert') -const stream = require('stream') -const hasOwnProperty = require('has-own-property') +import assert from 'node:assert' +import stream from 'node:stream' +import { promisify } from 'node:util' -const pify = require('pify') -const util = require('./_util') -const multer = require('../') -const FormData = require('form-data') -const testData = require('testdata-w3c-json-form') -const recursiveNullify = require('recursive-nullify') +import FormData from 'form-data' +import hasOwnProperty from 'has-own-property' +import recursiveNullify from 'recursive-nullify' +import testData from 'testdata-w3c-json-form' + +import * as util from './_util.js' +import multer from '../index.js' describe('body', () => { let parser @@ -69,7 +70,7 @@ describe('body', () => { 'content-length': 11 } - await pify(parser)(req, null) + await promisify(parser)(req, null) assert.strictEqual(hasOwnProperty(req, 'body'), false) assert.strictEqual(hasOwnProperty(req, 'files'), false) @@ -85,7 +86,7 @@ describe('body', () => { 'content-length': 11 } - await pify(parser)(req, null) + await promisify(parser)(req, null) assert.strictEqual(hasOwnProperty(req, 'body'), false) assert.strictEqual(hasOwnProperty(req, 'files'), false) diff --git a/test/error-handling.js b/test/error-handling.js index 2ca8350a..c4bd2ebe 100644 --- a/test/error-handling.js +++ b/test/error-handling.js @@ -1,13 +1,13 @@ /* eslint-env mocha */ -const assert = require('assert') -const assertRejects = require('assert-rejects') -const FormData = require('form-data') -const pify = require('pify') -const stream = require('stream') +import assert from 'node:assert' +import stream from 'node:stream' +import { promisify } from 'node:util' -const util = require('./_util') -const multer = require('../') +import FormData from 'form-data' + +import * as util from './_util.js' +import multer from '../index.js' function withLimits (limits, fields) { return multer({ limits: limits }).fields(fields) @@ -50,7 +50,7 @@ describe('Error Handling', () => { form.append('tiny', util.file('tiny')) form.append('small', util.file('small')) - await assertRejects( + await assert.rejects( util.submitForm(parser, form), hasCodeAndField('LIMIT_FILE_SIZE', 'small') ) @@ -66,7 +66,7 @@ describe('Error Handling', () => { form.append('small', util.file('small')) form.append('small', util.file('small')) - await assertRejects( + await assert.rejects( util.submitForm(parser, form), hasCode('LIMIT_FILE_COUNT') ) @@ -80,7 +80,7 @@ describe('Error Handling', () => { form.append('small', util.file('small')) - await assertRejects( + await assert.rejects( util.submitForm(parser, form), hasCode('LIMIT_FIELD_KEY') ) @@ -93,7 +93,7 @@ describe('Error Handling', () => { form.append('ok', 'SMILE') form.append('blowup', 'BOOM!') - await assertRejects( + await assert.rejects( util.submitForm(parser, form), hasCode('LIMIT_FIELD_KEY') ) @@ -106,7 +106,7 @@ describe('Error Handling', () => { form.append('field0', 'This is okay') form.append('field1', 'This will make the parser explode') - await assertRejects( + await assert.rejects( util.submitForm(parser, form), hasCodeAndField('LIMIT_FIELD_VALUE', 'field1') ) @@ -119,7 +119,7 @@ describe('Error Handling', () => { form.append('field0', 'BOOM!') form.append('field1', 'BOOM!') - await assertRejects( + await assert.rejects( util.submitForm(parser, form), hasCode('LIMIT_FIELD_COUNT') ) @@ -133,7 +133,7 @@ describe('Error Handling', () => { form.append('small', util.file('small')) - await assertRejects( + await assert.rejects( util.submitForm(parser, form), hasCodeAndField('LIMIT_UNEXPECTED_FILE', 'small') ) @@ -151,8 +151,8 @@ describe('Error Handling', () => { req.end(body) - await assertRejects( - pify(upload)(req, null), + await assert.rejects( + promisify(upload)(req, null), hasMessage('Multipart: Boundary not found') ) }) @@ -176,8 +176,8 @@ describe('Error Handling', () => { req.end(body) - await assertRejects( - pify(upload)(req, null), + await assert.rejects( + promisify(upload)(req, null), hasMessage('Unexpected end of multipart data') ) }) @@ -191,7 +191,7 @@ describe('Error Handling', () => { form.append('small', util.file('small')) form.append('small', util.file('small')) - await assertRejects( + await assert.rejects( util.submitForm(parser, form), hasCode('LIMIT_FILE_SIZE') ) diff --git a/test/express-integration.js b/test/express-integration.js index 1222974c..22bd988f 100644 --- a/test/express-integration.js +++ b/test/express-integration.js @@ -1,16 +1,17 @@ /* eslint-env mocha */ -const assert = require('assert') +import assert from 'node:assert' +import { promisify } from 'node:util' -const multer = require('../') -const util = require('./_util') +import express from 'express' +import FormData from 'form-data' +import getStream from 'get-stream' +import _onFinished from 'on-finished' -const pify = require('pify') -const express = require('express') -const FormData = require('form-data') -const getStream = require('get-stream') +import * as util from './_util.js' +import multer from '../index.js' -const onFinished = pify(require('on-finished')) +const onFinished = promisify(_onFinished) const port = 34279 diff --git a/test/limits.js b/test/limits.js index ec6aa9cd..043108cf 100644 --- a/test/limits.js +++ b/test/limits.js @@ -1,10 +1,10 @@ /* eslint-env mocha */ -const assertRejects = require('assert-rejects') -const FormData = require('form-data') +import assert from 'node:assert' +import FormData from 'form-data' -const util = require('./_util') -const multer = require('../') +import * as util from './_util.js' +import multer from '../index.js' describe('limits', () => { it('should report limit errors', async () => { @@ -13,7 +13,7 @@ describe('limits', () => { form.append('file', util.file('large')) - await assertRejects( + await assert.rejects( util.submitForm(parser, form), (err) => err.code === 'LIMIT_FILE_SIZE' && err.field === 'file' ) diff --git a/test/misc.js b/test/misc.js index 87edd96b..5a4e8c08 100644 --- a/test/misc.js +++ b/test/misc.js @@ -1,10 +1,11 @@ /* eslint-env mocha */ -const assert = require('assert') +import assert from 'node:assert' +import { PassThrough, pipeline } from 'node:stream' +import FormData from 'form-data' -const util = require('./_util') -const multer = require('../') -const FormData = require('form-data') +import * as util from './_util.js' +import multer from '../index.js' describe('Misc', () => { it('should handle unicode filenames', async () => { @@ -27,10 +28,9 @@ describe('Misc', () => { const stream = util.file('small') // Don't let FormData figure out a filename - delete stream.fd - delete stream.path + const hidden = pipeline(stream, new PassThrough(), () => {}) - form.append('file', stream, { knownLength: util.knownFileLength('small') }) + form.append('file', hidden, { knownLength: util.knownFileLength('small') }) const req = await util.submitForm(parser, form) assert.strictEqual(req.file.originalName, undefined) diff --git a/test/upload-any.js b/test/upload-any.js index 27c51aab..ad954bbd 100644 --- a/test/upload-any.js +++ b/test/upload-any.js @@ -1,10 +1,10 @@ /* eslint-env mocha */ -const assert = require('assert') -const FormData = require('form-data') +import assert from 'node:assert' +import FormData from 'form-data' -const multer = require('../') -const util = require('./_util') +import * as util from './_util.js' +import multer from '../index.js' describe('upload.any', () => { let parser diff --git a/test/upload-array.js b/test/upload-array.js index 4095b050..170301b6 100644 --- a/test/upload-array.js +++ b/test/upload-array.js @@ -1,11 +1,10 @@ /* eslint-env mocha */ -const assert = require('assert') -const assertRejects = require('assert-rejects') -const FormData = require('form-data') +import assert from 'node:assert' +import FormData from 'form-data' -const multer = require('../') -const util = require('./_util') +import * as util from './_util.js' +import multer from '../index.js' describe('upload.array', () => { let parser @@ -55,7 +54,7 @@ describe('upload.array', () => { form.append('files', util.file('small')) form.append('files', util.file('small')) - await assertRejects( + await assert.rejects( util.submitForm(parser, form), (err) => err.code === 'LIMIT_FILE_COUNT' && err.field === 'files' ) @@ -67,7 +66,7 @@ describe('upload.array', () => { form.append('name', 'Multer') form.append('unexpected', util.file('small')) - await assertRejects( + await assert.rejects( util.submitForm(parser, form), (err) => err.code === 'LIMIT_UNEXPECTED_FILE' && err.field === 'unexpected' ) diff --git a/test/upload-fields.js b/test/upload-fields.js index 49d85ed2..b668193c 100644 --- a/test/upload-fields.js +++ b/test/upload-fields.js @@ -1,11 +1,10 @@ /* eslint-env mocha */ -const assert = require('assert') -const assertRejects = require('assert-rejects') -const FormData = require('form-data') +import assert from 'node:assert' +import FormData from 'form-data' -const multer = require('../') -const util = require('./_util') +import * as util from './_util.js' +import multer from '../index.js' describe('upload.fields', () => { let parser @@ -85,7 +84,7 @@ describe('upload.fields', () => { form.append('CA$|-|', util.file('small')) form.append('CA$|-|', util.file('small')) - await assertRejects( + await assert.rejects( util.submitForm(parser, form), (err) => err.code === 'LIMIT_FILE_COUNT' && err.field === 'CA$|-|' ) @@ -97,7 +96,7 @@ describe('upload.fields', () => { form.append('name', 'Multer') form.append('unexpected', util.file('small')) - await assertRejects( + await assert.rejects( util.submitForm(parser, form), (err) => err.code === 'LIMIT_UNEXPECTED_FILE' && err.field === 'unexpected' ) diff --git a/test/upload-none.js b/test/upload-none.js index 0452d3a5..c7aa13fe 100644 --- a/test/upload-none.js +++ b/test/upload-none.js @@ -1,11 +1,10 @@ /* eslint-env mocha */ -const assert = require('assert') -const assertRejects = require('assert-rejects') -const FormData = require('form-data') +import assert from 'node:assert' +import FormData from 'form-data' -const multer = require('../') -const util = require('./_util') +import * as util from './_util.js' +import multer from '../index.js' describe('upload.none', () => { let parser @@ -35,7 +34,7 @@ describe('upload.none', () => { form.append('name', 'Multer') form.append('file', util.file('small')) - await assertRejects( + await assert.rejects( util.submitForm(parser, form), (err) => err.code === 'LIMIT_UNEXPECTED_FILE' && err.field === 'file' ) @@ -48,7 +47,7 @@ describe('upload.none', () => { form.append('file', util.file('tiny')) form.append('file', util.file('tiny')) - await assertRejects( + await assert.rejects( util.submitForm(parser, form), (err) => err.code === 'LIMIT_UNEXPECTED_FILE' && err.field === 'file' ) diff --git a/test/upload-single.js b/test/upload-single.js index ddee87ab..b639cbfa 100644 --- a/test/upload-single.js +++ b/test/upload-single.js @@ -1,11 +1,10 @@ /* eslint-env mocha */ -const assert = require('assert') -const assertRejects = require('assert-rejects') -const FormData = require('form-data') +import assert from 'node:assert' +import FormData from 'form-data' -const multer = require('../') -const util = require('./_util') +import * as util from './_util.js' +import multer from '../index.js' describe('upload.single', () => { let parser @@ -33,7 +32,7 @@ describe('upload.single', () => { form.append('file', util.file('tiny')) form.append('file', util.file('tiny')) - await assertRejects( + await assert.rejects( util.submitForm(parser, form), (err) => err.code === 'LIMIT_FILE_COUNT' && err.field === 'file' ) @@ -45,7 +44,7 @@ describe('upload.single', () => { form.append('name', 'Multer') form.append('unexpected', util.file('tiny')) - await assertRejects( + await assert.rejects( util.submitForm(parser, form), (err) => err.code === 'LIMIT_UNEXPECTED_FILE' && err.field === 'unexpected' )