diff --git a/.github/security.md b/.github/security.md new file mode 100644 index 0000000..5358dc5 --- /dev/null +++ b/.github/security.md @@ -0,0 +1,3 @@ +# Security Policy + +To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 441975c..346585c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,10 +10,11 @@ jobs: fail-fast: false matrix: node-version: - - 16 + - 20 + - 18 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 cda7733..5ceae52 100644 --- a/index.js +++ b/index.js @@ -1,30 +1,27 @@ -import {createRequire} from 'node:module'; import path from 'node:path'; import process from 'node:process'; -import log from 'fancy-log'; -import PluginError from 'plugin-error'; -import through from 'through2-concurrent'; import prettyBytes from 'pretty-bytes'; import chalk from 'chalk'; import imagemin from 'imagemin'; import plur from 'plur'; - -const require = createRequire(import.meta.url); +import {gulpPlugin} from 'gulp-plugin-extras'; const PLUGIN_NAME = 'gulp-imagemin'; const defaultPlugins = ['gifsicle', 'mozjpeg', 'optipng', 'svgo']; -const loadPlugin = (plugin, ...args) => { +const loadPlugin = async (pluginName, ...arguments_) => { try { - return require(`imagemin-${plugin}`)(...args); - } catch { - log(`${PLUGIN_NAME}: Could not load default plugin \`${plugin}\``); + const {default: plugin} = await import(`imagemin-${pluginName}`); + return plugin(...arguments_); + } catch (error) { + console.log('er', error); + console.log(`${PLUGIN_NAME}: Could not load default plugin \`${pluginName}\``); } }; -const exposePlugin = plugin => (...args) => loadPlugin(plugin, ...args); +const exposePlugin = async plugin => (...arguments_) => loadPlugin(plugin, ...arguments_); -const getDefaultPlugins = () => defaultPlugins.flatMap(plugin => loadPlugin(plugin)); +const getDefaultPlugins = async () => Promise.all(defaultPlugins.flatMap(plugin => loadPlugin(plugin))); export default function gulpImagemin(plugins, options) { if (typeof plugins === 'object' && !Array.isArray(plugins)) { @@ -45,75 +42,58 @@ export default function gulpImagemin(plugins, options) { let totalSavedBytes = 0; let totalFiles = 0; - return through.obj({ - maxConcurrency: 8, - }, (file, encoding, callback) => { - if (file.isNull()) { - callback(null, file); - return; + return gulpPlugin('gulp-imagemin', async file => { + if (!validExtensions.has(path.extname(file.path).toLowerCase())) { + if (options.verbose) { + console.log(`${PLUGIN_NAME}: Skipping unsupported image ${chalk.blue(file.relative)}`); + } + + return file; } - if (file.isStream()) { - callback(new PluginError(PLUGIN_NAME, 'Streaming not supported')); - return; + if (Array.isArray(plugins)) { + plugins = await Promise.all(plugins); } - if (!validExtensions.has(path.extname(file.path).toLowerCase())) { - if (options.verbose) { - log(`${PLUGIN_NAME}: Skipping unsupported image ${chalk.blue(file.relative)}`); - } + const localPlugins = plugins ?? await getDefaultPlugins(); + const data = await imagemin.buffer(file.contents, {plugins: localPlugins}); + const originalSize = file.contents.length; + const optimizedSize = data.length; + const saved = originalSize - optimizedSize; + const percent = originalSize > 0 ? (saved / originalSize) * 100 : 0; + const savedMessage = `saved ${prettyBytes(saved)} - ${percent.toFixed(1).replace(/\.0$/, '')}%`; + const message = saved > 0 ? savedMessage : 'already optimized'; + + if (saved > 0) { + totalBytes += originalSize; + totalSavedBytes += saved; + totalFiles++; + } - callback(null, file); - return; + if (options.verbose) { + console.log(`${PLUGIN_NAME}:`, chalk.green('✔ ') + file.relative + chalk.gray(` (${message})`)); } - const localPlugins = plugins || getDefaultPlugins(); - - (async () => { - try { - const data = await imagemin.buffer(file.contents, { - plugins: localPlugins, - }); - const originalSize = file.contents.length; - const optimizedSize = data.length; - const saved = originalSize - optimizedSize; - const percent = originalSize > 0 ? (saved / originalSize) * 100 : 0; - const savedMessage = `saved ${prettyBytes(saved)} - ${percent.toFixed(1).replace(/\.0$/, '')}%`; - const message = saved > 0 ? savedMessage : 'already optimized'; - - if (saved > 0) { - totalBytes += originalSize; - totalSavedBytes += saved; - totalFiles++; - } + file.contents = data; - if (options.verbose) { - log(`${PLUGIN_NAME}:`, chalk.green('✔ ') + file.relative + chalk.gray(` (${message})`)); + return file; + }, { + async * onFinish() { // eslint-disable-line require-yield + if (!options.silent) { + const percent = totalBytes > 0 ? (totalSavedBytes / totalBytes) * 100 : 0; + let message = `Minified ${totalFiles} ${plur('image', totalFiles)}`; + + if (totalFiles > 0) { + message += chalk.gray(` (saved ${prettyBytes(totalSavedBytes)} - ${percent.toFixed(1).replace(/\.0$/, '')}%)`); } - file.contents = data; - callback(null, file); - } catch (error) { - callback(new PluginError(PLUGIN_NAME, error, {fileName: file.path})); - } - })(); - }, callback => { - if (!options.silent) { - const percent = totalBytes > 0 ? (totalSavedBytes / totalBytes) * 100 : 0; - let message = `Minified ${totalFiles} ${plur('image', totalFiles)}`; - - if (totalFiles > 0) { - message += chalk.gray(` (saved ${prettyBytes(totalSavedBytes)} - ${percent.toFixed(1).replace(/\.0$/, '')}%)`); + console.log(`${PLUGIN_NAME}:`, message); } - - log(`${PLUGIN_NAME}:`, message); - } - - callback(); + }, }); } -export const gifsicle = exposePlugin('gifsicle'); -export const mozjpeg = exposePlugin('mozjpeg'); -export const optipng = exposePlugin('optipng'); -export const svgo = exposePlugin('svgo'); +export const gifsicle = await exposePlugin('gifsicle'); +export const mozjpeg = await exposePlugin('mozjpeg'); +export const optipng = await exposePlugin('optipng'); +export const svgo = await exposePlugin('svgo'); diff --git a/package.json b/package.json index c71ea52..20545ad 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "type": "module", "exports": "./index.js", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "scripts": { "test": "xo && ava" @@ -38,26 +38,23 @@ "svg" ], "dependencies": { - "chalk": "^4.1.2", - "fancy-log": "^1.3.3", + "chalk": "^5.3.0", + "gulp-plugin-extras": "^0.2.2", "imagemin": "^8.0.1", - "plugin-error": "^1.0.1", - "plur": "^4.0.0", - "pretty-bytes": "^5.6.0", - "through2-concurrent": "^2.0.0" + "plur": "^5.1.0", + "pretty-bytes": "^6.1.1" }, "devDependencies": { - "ava": "^3.15.0", - "get-stream": "^6.0.1", + "ava": "^5.3.1", "imagemin-pngquant": "^9.0.2", - "vinyl": "^2.2.1", - "xo": "^0.44.0" + "vinyl": "^3.0.0", + "xo": "^0.56.0" }, "optionalDependencies": { "imagemin-gifsicle": "^7.0.0", - "imagemin-mozjpeg": "^9.0.0", + "imagemin-mozjpeg": "^10.0.0", "imagemin-optipng": "^8.0.0", - "imagemin-svgo": "^9.0.0" + "imagemin-svgo": "^10.0.1" }, "peerDependencies": { "gulp": ">=4" diff --git a/readme.md b/readme.md index b519cc1..b588723 100644 --- a/readme.md +++ b/readme.md @@ -6,8 +6,8 @@ ## Install -``` -$ npm install --save-dev gulp-imagemin +```sh +npm install --save-dev gulp-imagemin ``` ## Usage @@ -28,15 +28,23 @@ export default () => ( ### Custom plugin options ```js +import imagemin, {gifsicle, mozjpeg, optipng, svgo} from 'gulp-imagemin'; + // … .pipe(imagemin([ - imagemin.gifsicle({interlaced: true}), - imagemin.mozjpeg({quality: 75, progressive: true}), - imagemin.optipng({optimizationLevel: 5}), - imagemin.svgo({ + gifsicle({interlaced: true}), + mozjpeg({quality: 75, progressive: true}), + optipng({optimizationLevel: 5}), + svgo({ plugins: [ - {removeViewBox: true}, - {cleanupIDs: false} + { + name: 'removeViewBox', + active: true + }, + { + name: 'cleanupIDs', + active: false + } ] }) ])) @@ -46,12 +54,15 @@ export default () => ( ### Custom plugin options and custom `gulp-imagemin` options ```js +import imagemin, {svgo} from 'gulp-imagemin'; + // … .pipe(imagemin([ - imagemin.svgo({ + svgo({ plugins: [ { - removeViewBox: true + name: 'removeViewBox', + active: true } ] }) @@ -79,7 +90,7 @@ Unsupported files are ignored. #### plugins Type: `Array`\ -Default: `[imagemin.gifsicle(), imagemin.mozjpeg(), imagemin.optipng(), imagemin.svgo()]` +Default: `[gifsicle(), mozjpeg(), optipng(), svgo()]` [Plugins](https://www.npmjs.com/browse/keyword/imageminplugin) to use. This will completely overwrite all the default plugins. So, if you want to use custom plugins and you need some of defaults too, then you should pass default plugins as well. Note that the default plugins come with good defaults and should be sufficient in most cases. See the individual plugins for supported options. diff --git a/test.js b/test.js index 2d6c2c3..5af868f 100644 --- a/test.js +++ b/test.js @@ -3,7 +3,6 @@ import path from 'node:path'; import {fileURLToPath} from 'node:url'; import imageminPngquant from 'imagemin-pngquant'; import Vinyl from 'vinyl'; -import getStream from 'get-stream'; import test from 'ava'; import gulpImagemin, {mozjpeg, svgo} from './index.js'; @@ -23,7 +22,7 @@ const createFixture = async (plugins, file = 'fixture.png') => { test('minify images', async t => { const {buffer, stream} = await createFixture(); - const file = await getStream.array(stream); + const file = await stream.toArray(); t.true(file[0].contents.length < buffer.length); }); @@ -35,16 +34,16 @@ test('minify JPEG with custom settings', async t => { smooth: 45, }; const {buffer, stream} = await createFixture([mozjpeg(mozjpegOptions)], 'fixture.jpg'); - const file = await getStream.array(stream); + const file = await stream.toArray(); t.true(file[0].contents.length < buffer.length); }); test('use custom plugins', async t => { const {stream} = await createFixture([imageminPngquant()]); - const compareStream = (await createFixture()).stream; - const file = await getStream.array(stream); - const compareFile = await getStream.array(compareStream); + const {stream: compareStream} = await createFixture(); + const file = await stream.toArray(); + const compareFile = await compareStream.toArray(); t.true(file[0].contents.length < compareFile[0].contents.length); }); @@ -57,9 +56,9 @@ test('use custom svgo settings', async t => { }, }; const {stream} = await createFixture([svgo(svgoOptions)], 'fixture-svg-logo.svg'); - const compareStream = (await createFixture(null, 'fixture-svg-logo.svg')).stream; - const file = await getStream.array(stream); - const compareFile = await getStream.array(compareStream); + const {stream: compareStream} = await createFixture(null, 'fixture-svg-logo.svg'); + const file = await stream.toArray(); + const compareFile = await compareStream.toArray(); t.true(file[0].contents.length > compareFile[0].contents.length); }); @@ -67,7 +66,7 @@ test('use custom svgo settings', async t => { test('skip unsupported images', async t => { const stream = gulpImagemin(); stream.end(new Vinyl({path: path.join(__dirname, 'fixture.bmp')})); - const file = await getStream.array(stream); + const file = await stream.toArray(); t.is(file[0].contents, null); });