diff --git a/README.md b/README.md index 17226b1..f3d674e 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ compress({ | `--from` (alias `--input`) | `from` (alias `input`) | Input folder | `process.cwd()` | | `--to` (alias `--output`) | `to` (alias `output`) | Output folder | `from` param value | | `--formats` | `formats` | Formats of output files | `['gzip', 'brotli']`| +| `--with-zopfli` | `withZopfli` | Enable zopfli algorithm for gzip | `false` | | `--ext-white-list` | `extWhiteList` | A list of extensions that will be used to filter the necessary files | `['.html', '.css', '.js', '.json', '.svg', '.txt', '.xml']` | | `--concurrency` | `concurrency` | number of parallel handlers | `os.cpus().length` | | `--file-size` | `fileSize` | File size treshold in bytes. Files smaller than this size will be ignored | `0` | diff --git a/cli.js b/cli.js index 1982563..066173a 100644 --- a/cli.js +++ b/cli.js @@ -63,7 +63,8 @@ compress({ formats, extWhiteList: params['ext-white-list'], concurrency: params['concurrency'], - fileSize: params['file-size'] + fileSize: params['file-size'], + withZopfli: params['with-zopfli'] }) .then(() => { const diffTime = Math.round(performance.now() - startTime); diff --git a/lib/constants.js b/lib/constants.js index 2a67bdf..f396de3 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -8,20 +8,31 @@ const FORMATS = { exports.FORMATS = FORMATS; -exports.DEFAULT_COMPRESS_SETTINGS = { - [FORMATS.GZIP]: { - level: zlib.constants.Z_BEST_COMPRESSION - }, - [FORMATS.BROTLI]: { - params: { - [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT, - [zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY - } - }, - [FORMATS.ZSTD]: { - params: { - [zlib.constants.ZSTD_c_compressionLevel]: 22, - [zlib.constants.ZSTD_c_checksumFlag]: 1, +exports.DEFAULT_COMPRESS_SETTINGS = (params) => { + const { withZopfli = false } = params; + return { + [FORMATS.GZIP]: withZopfli + ? { + verbose: false, + verbose_more: false, + numiterations: 15, + blocksplitting: true, + blocksplittingmax: 15, + } + : { + level: zlib.constants.Z_BEST_COMPRESSION + }, + [FORMATS.BROTLI]: { + params: { + [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT, + [zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY + } + }, + [FORMATS.ZSTD]: { + params: { + [zlib.constants.ZSTD_c_compressionLevel]: 22, + [zlib.constants.ZSTD_c_checksumFlag]: 1, + } } } } diff --git a/lib/functions.js b/lib/functions.js index 10cba2c..6332b8b 100644 --- a/lib/functions.js +++ b/lib/functions.js @@ -1,9 +1,10 @@ const os = require('os'); const fs = require('fs'); -const fsp = require('fs/promises'); const path = require('path'); -const { pipeline } = require('stream/promises'); +const stream = require('stream'); const zlib = require('zlib'); +const { Buffer } = require('buffer') +const { gzipAsync } = require('@gfx/zopfli'); const CONSTANTS = require('./constants'); const { walk, makeDir } = require('./fs-utils'); @@ -16,20 +17,37 @@ const { flatMap } = require('./generators-utils'); - -const FORMAT_TO_STREAM = { - [CONSTANTS.FORMATS.GZIP]: zlib.createGzip, - [CONSTANTS.FORMATS.BROTLI]: zlib.createBrotliCompress, - [CONSTANTS.FORMATS.ZSTD]: zlib.createZstdCompress || (() => { - throw new Error(`Your version of Node.js doesn't support zstd. Node.js has zstd support since 23.8.0 and 22.15.0.`) - }) +const FORMAT_TO_STREAM = (params) => { + const { withZopfli = false } = params; + return { + [CONSTANTS.FORMATS.GZIP]: + withZopfli + ? ( + function createZopfli(options) { + return stream.Duplex.from(async function* (source) { + const chunks = []; + for await (const chunk of source) { + chunks.push(chunk); + } + yield await gzipAsync(Buffer.concat(chunks), options); + }) + } + ) + : zlib.createGzip, + [CONSTANTS.FORMATS.BROTLI]: zlib.createBrotliCompress, + [CONSTANTS.FORMATS.ZSTD]: zlib.createZstdCompress || (() => { + throw new Error(`Your version of Node.js doesn't support zstd. Node.js has zstd support since 23.8.0 and 22.15.0.`) + }) + } } -function createCompressStream({ fromPath, toPath, format, compressOptions }) { - return pipeline( +function createCompressStream({ fromPath, toPath, format, compressOptions, withZopfli }) { + const params = { withZopfli } + + return stream.promises.pipeline( fs.createReadStream(fromPath), - FORMAT_TO_STREAM[format](compressOptions || CONSTANTS.DEFAULT_COMPRESS_SETTINGS[format]), + FORMAT_TO_STREAM(params)[format](compressOptions || CONSTANTS.DEFAULT_COMPRESS_SETTINGS[format]), fs.createWriteStream(toPath + CONSTANTS.FORMAT_TO_EXT[format]) ); } @@ -51,14 +69,15 @@ async function compress(params) { formats = [CONSTANTS.FORMATS.GZIP], extWhiteList = CONSTANTS.EXT_WHITE_LIST, concurrency = getConcurrency(), - fileSize + fileSize, + withZopfli = false } = params; const handlers = [ filter(item => item.dirent.isFile()), filter(item => extWhiteList.includes(path.extname(item.direntPath))), fileSize && map(async item => { - const stat = await fsp.stat(item.direntPath); + const stat = await fs.promises.stat(item.direntPath); return { ...item, size: stat.size @@ -83,7 +102,7 @@ async function compress(params) { await makeDir(toBasePath); - return createCompressStream({ fromPath, toPath, format: item.format }); + return createCompressStream({ fromPath, toPath, format: item.format, withZopfli }); } }), ].filter(Boolean); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c41c31d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,51 @@ +{ + "name": "@web-alchemy/web-compressor", + "version": "3.0.10", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@web-alchemy/web-compressor", + "version": "3.0.10", + "dependencies": { + "@gfx/zopfli": "^1.0.15" + }, + "bin": { + "web-compressor": "bin/web-compressor" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@gfx/zopfli": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@gfx/zopfli/-/zopfli-1.0.15.tgz", + "integrity": "sha512-7mBgpi7UD82fsff5ThQKet0uBTl4BYerQuc+/qA1ELTwWEiIedRTcD3JgiUu9wwZ2kytW8JOb165rSdAt8PfcQ==", + "dependencies": { + "base64-js": "^1.3.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + } + } +} diff --git a/package.json b/package.json index a4e9381..5f457a7 100644 --- a/package.json +++ b/package.json @@ -34,5 +34,8 @@ "zstandard", "archive", "CLI" - ] + ], + "dependencies": { + "@gfx/zopfli": "^1.0.15" + } }