diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 495ba52..05cbfa7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,16 +10,15 @@ jobs: fail-fast: false matrix: node-version: - - 16 - - 14 - - 12 + - 22 + - 18 os: - ubuntu-latest - macos-latest - windows-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm install diff --git a/index.js b/index.js index e6cfe10..aa0d9e3 100644 --- a/index.js +++ b/index.js @@ -1,32 +1,27 @@ -import {Buffer} from 'node:buffer'; -import {promises as fsPromises} from 'node:fs'; -import {promisify} from 'node:util'; +import fsPromises from 'node:fs/promises'; import path from 'node:path'; -import fs from 'graceful-fs'; -import FileType from 'file-type'; +import {fileTypeFromBuffer} from 'file-type'; import {globby} from 'globby'; import pPipe from 'p-pipe'; -import replaceExt from 'replace-ext'; -import junk from 'junk'; +import changeFileExtension from 'change-file-extension'; +import {isNotJunk} from 'junk'; import convertToUnixPath from 'slash'; - -const readFile = promisify(fs.readFile); -const writeFile = promisify(fs.writeFile); +import {assertUint8Array} from 'uint8array-extras'; +import {isBrowser} from 'environment'; +import ow from 'ow'; const handleFile = async (sourcePath, {destination, plugins = []}) => { - if (plugins && !Array.isArray(plugins)) { - throw new TypeError('The `plugins` option should be an `Array`'); - } + ow(plugins, ow.optional.array.message('The `plugins` option should be an `Array`')); - let data = await readFile(sourcePath); + let data = await fsPromises.readFile(sourcePath); data = await (plugins.length > 0 ? pPipe(...plugins)(data) : data); - const {ext} = await FileType.fromBuffer(data) || {ext: path.extname(sourcePath)}; + const {ext} = await fileTypeFromBuffer(data) ?? {ext: path.extname(sourcePath)}; let destinationPath = destination ? path.join(destination, path.basename(sourcePath)) : undefined; - destinationPath = ext === 'webp' ? replaceExt(destinationPath, '.webp') : destinationPath; + destinationPath = ext === 'webp' ? changeFileExtension(destinationPath, 'webp') : destinationPath; const returnValue = { - data, + data: new Uint8Array(data), sourcePath, destinationPath, }; @@ -36,22 +31,24 @@ const handleFile = async (sourcePath, {destination, plugins = []}) => { } await fsPromises.mkdir(path.dirname(returnValue.destinationPath), {recursive: true}); - await writeFile(returnValue.destinationPath, returnValue.data); + await fsPromises.writeFile(returnValue.destinationPath, returnValue.data); return returnValue; }; export default async function imagemin(input, {glob = true, ...options} = {}) { - if (!Array.isArray(input)) { - throw new TypeError(`Expected an \`Array\`, got \`${typeof input}\``); + if (isBrowser) { + throw new Error('This package does not work in the browser.'); } + ow(input, ow.array); + const unixFilePaths = input.map(path => convertToUnixPath(path)); const filePaths = glob ? await globby(unixFilePaths, {onlyFiles: true}) : input; return Promise.all( filePaths - .filter(filePath => junk.not(path.basename(filePath))) + .filter(filePath => isNotJunk(path.basename(filePath))) .map(async filePath => { try { return await handleFile(filePath, options); @@ -63,14 +60,17 @@ export default async function imagemin(input, {glob = true, ...options} = {}) { ); } -imagemin.buffer = async (input, {plugins = []} = {}) => { - if (!Buffer.isBuffer(input)) { - throw new TypeError(`Expected a \`Buffer\`, got \`${typeof input}\``); +imagemin.buffer = async (data, {plugins = []} = {}) => { + if (isBrowser) { + throw new Error('This package does not work in the browser.'); } + assertUint8Array(data); + if (plugins.length === 0) { - return input; + return new Uint8Array(data); } - return pPipe(...plugins)(input); + // The `new Uint8Array` can be removed if all plugins are changed to return `Uint8Array` instead of `Buffer`. + return new Uint8Array(await pPipe(...plugins)(data)); }; diff --git a/package.json b/package.json index 2925a63..996dbd1 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,11 @@ "description": "Minify images seamlessly", "license": "MIT", "repository": "imagemin/imagemin", + "funding": "https://github.com/sponsors/sindresorhus", "type": "module", "exports": "./index.js", "engines": { - "node": ">=12" + "node": ">=18" }, "scripts": { "test": "xo && ava" @@ -27,22 +28,23 @@ "svg" ], "dependencies": { - "file-type": "^16.5.3", - "globby": "^12.0.0", - "graceful-fs": "^4.2.8", - "junk": "^3.1.0", + "change-file-extension": "^0.1.1", + "environment": "^1.0.0", + "file-type": "^19.0.0", + "globby": "^14.0.1", + "junk": "^4.0.1", + "ow": "^2.0.0", "p-pipe": "^4.0.0", - "replace-ext": "^2.0.0", - "slash": "^3.0.0" + "slash": "^5.1.0", + "uint8array-extras": "^1.1.0" }, "devDependencies": { - "ava": "^3.15.0", - "del": "^6.0.0", + "ava": "^6.1.2", + "del": "^7.1.0", "imagemin-jpegtran": "^7.0.0", - "imagemin-svgo": "^9.0.0", - "imagemin-webp": "^6.0.0", - "is-jpg": "^2.0.0", - "tempy": "^1.0.1", - "xo": "^0.43.0" + "imagemin-svgo": "^10.0.1", + "imagemin-webp": "^8.0.0", + "tempy": "^3.1.0", + "xo": "^0.58.0" } } diff --git a/readme.md b/readme.md index 6411502..e536df9 100644 --- a/readme.md +++ b/readme.md @@ -21,19 +21,6 @@

-
- -
- Doppler -
- All your environment variables, in one place -
- Stop struggling with scattered API keys, hacking together home-brewed tools, -
- and avoiding access controls. Keep your team and servers in sync with Doppler. -
-
-
Strapi @@ -52,8 +39,8 @@ ## Install -``` -$ npm install imagemin +```sh +npm install imagemin ``` ## Usage @@ -74,14 +61,14 @@ const files = await imagemin(['images/*.{jpg,png}'], { }); console.log(files); -//=> [{data: , destinationPath: 'build/images/foo.jpg'}, …] +//=> [{data: , destinationPath: 'build/images/foo.jpg'}, …] ``` ## API ### imagemin(input, options?) -Returns `Promise` in the format `{data: Buffer, sourcePath: string, destinationPath: string}`. +Returns `Promise` in the format `{data: Uint8Array, sourcePath: string, destinationPath: string}`. #### input @@ -103,7 +90,7 @@ Set the destination folder to where your files will be written. If no destinatio Type: `Array` -[Plugins](https://www.npmjs.com/browse/keyword/imageminplugin) to use. +The [plugins](https://www.npmjs.com/browse/keyword/imageminplugin) to use. ##### glob @@ -112,15 +99,15 @@ Default: `true` Enable globbing when matching file paths. -### imagemin.buffer(buffer, options?) +### imagemin.buffer(data, options?) -Returns `Promise`. +Returns `Promise`. -#### buffer +#### data -Type: `Buffer` +Type: `Uint8Array` -Buffer to optimize. +The image data to optimize. #### options @@ -135,6 +122,4 @@ Type: `Array` ## Related - [imagemin-cli](https://github.com/imagemin/imagemin-cli) - CLI for this module -- [imagemin-app](https://github.com/imagemin/imagemin-app) - GUI app for this module - [gulp-imagemin](https://github.com/sindresorhus/gulp-imagemin) - Gulp plugin -- [grunt-contrib-imagemin](https://github.com/gruntjs/grunt-contrib-imagemin) - Grunt plugin diff --git a/test.js b/test.js index fbc9c3e..0c3980f 100644 --- a/test.js +++ b/test.js @@ -1,13 +1,13 @@ import fs, {promises as fsPromises} from 'node:fs'; import path from 'node:path'; import {fileURLToPath} from 'node:url'; -import del from 'del'; +import {deleteAsync} from 'del'; import imageminJpegtran from 'imagemin-jpegtran'; import imageminWebp from 'imagemin-webp'; import imageminSvgo from 'imagemin-svgo'; -import isJpg from 'is-jpg'; -import tempy from 'tempy'; +import {temporaryDirectory, temporaryFile} from 'tempy'; import test from 'ava'; +import {fileTypeFromBuffer} from 'file-type'; import imagemin from './index.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -20,17 +20,22 @@ test('optimize a file', async t => { t.is(files[0].destinationPath, undefined); t.true(files[0].data.length < buffer.length); - t.true(isJpg(files[0].data)); + + const fileType = await fileTypeFromBuffer(files[0].data); + t.is(fileType?.ext, 'jpg'); }); test('optimize a buffer', async t => { const buffer = await fsPromises.readFile(path.join(__dirname, 'fixture.jpg')); + const data = await imagemin.buffer(buffer, { plugins: [imageminJpegtran()], }); t.true(data.length < buffer.length); - t.true(isJpg(data)); + + const fileType = await fileTypeFromBuffer(data); + t.is(fileType?.ext, 'jpg'); }); test('output error on corrupt images', async t => { @@ -40,8 +45,8 @@ test('output error on corrupt images', async t => { }); test('throw on wrong input', async t => { - await t.throwsAsync(imagemin('foo'), {message: /Expected an `Array`, got `string`/}); - await t.throwsAsync(imagemin.buffer('foo'), {message: /Expected a `Buffer`, got `string`/}); + await t.throwsAsync(imagemin('foo'), {message: /Expected argument to be of type `array`/}); + await t.throwsAsync(imagemin.buffer('foo'), {message: /Expected `Uint8Array`, got `string`/}); }); test('return original file if no plugins are defined', async t => { @@ -49,16 +54,20 @@ test('return original file if no plugins are defined', async t => { const files = await imagemin(['fixture.jpg']); t.is(files[0].destinationPath, undefined); - t.deepEqual(files[0].data, buffer); - t.true(isJpg(files[0].data)); + t.deepEqual(files[0].data, new Uint8Array(buffer)); + + const fileType = await fileTypeFromBuffer(files[0].data); + t.is(fileType?.ext, 'jpg'); }); test('return original buffer if no plugins are defined', async t => { const buffer = await fsPromises.readFile(path.join(__dirname, 'fixture.jpg')); const data = await imagemin.buffer(buffer); - t.deepEqual(data, buffer); - t.true(isJpg(data)); + t.deepEqual(data, new Uint8Array(buffer)); + + const fileType = await fileTypeFromBuffer(data); + t.is(fileType?.ext, 'jpg'); }); // TODO: Fix the test. @@ -85,8 +94,8 @@ test.failing('return processed buffer even it is a bad optimization', async t => }); test('output at the specified location', async t => { - const temporary = tempy.directory(); - const destinationTemporary = tempy.directory(); + const temporary = temporaryDirectory(); + const destinationTemporary = temporaryDirectory(); const buffer = await fsPromises.readFile(path.join(__dirname, 'fixture.jpg')); await fsPromises.mkdir(temporary, {recursive: true}); @@ -100,12 +109,12 @@ test('output at the specified location', async t => { t.true(fs.existsSync(files[0].destinationPath)); t.true(fs.existsSync(files[1].destinationPath)); - await del([temporary, destinationTemporary], {force: true}); + await deleteAsync([temporary, destinationTemporary], {force: true}); }); test('output at the specified location when input paths contain Windows path delimiter', async t => { - const temporary = tempy.directory(); - const destinationTemporary = tempy.directory(); + const temporary = temporaryDirectory(); + const destinationTemporary = temporaryDirectory(); const buffer = await fsPromises.readFile(path.join(__dirname, 'fixture.jpg')); await fsPromises.mkdir(temporary, {recursive: true}); @@ -120,34 +129,34 @@ test('output at the specified location when input paths contain Windows path del t.true(fs.existsSync(files[0].destinationPath)); - await del([temporary, destinationTemporary], {force: true}); + await deleteAsync([temporary, destinationTemporary], {force: true}); }); test('set webp ext', async t => { - const temporary = tempy.file(); + const temporary = temporaryFile(); const files = await imagemin(['fixture.jpg'], { destination: temporary, plugins: [imageminWebp()], }); t.is(path.extname(files[0].destinationPath), '.webp'); - await del(temporary, {force: true}); + await deleteAsync(temporary, {force: true}); }); test('set svg ext', async t => { - const temporary = tempy.file(); + const temporary = temporaryFile(); const files = await imagemin(['fixture.svg'], { destination: temporary, plugins: [imageminSvgo()], }); t.is(path.extname(files[0].destinationPath), '.svg'); - await del(temporary, {force: true}); + await deleteAsync(temporary, {force: true}); }); test('ignores junk files', async t => { - const temporary = tempy.directory(); - const destinationTemporary = tempy.directory(); + const temporary = temporaryDirectory(); + const destinationTemporary = temporaryDirectory(); const buffer = await fsPromises.readFile(path.join(__dirname, 'fixture.jpg')); await fsPromises.mkdir(temporary, {recursive: true}); @@ -164,7 +173,7 @@ test('ignores junk files', async t => { t.false(fs.existsSync(path.join(destinationTemporary, '.DS_Store'))); t.false(fs.existsSync(path.join(destinationTemporary, 'Thumbs.db'))); - await del([temporary, destinationTemporary], {force: true}); + await deleteAsync([temporary, destinationTemporary], {force: true}); }); test('glob option', async t => { @@ -173,5 +182,6 @@ test('glob option', async t => { plugins: [imageminJpegtran()], }); - t.true(isJpg(files[0].data)); + const fileType = await fileTypeFromBuffer(files[0].data); + t.is(fileType?.ext, 'jpg'); });