This commit is contained in:
where where 2025-01-14 09:07:08 +00:00 committed by GitHub
commit 22bf3b409e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 714 additions and 413 deletions

View File

@ -24,20 +24,8 @@ jobs:
restore-keys: ${{ runner.os }}-npm-cache restore-keys: ${{ runner.os }}-npm-cache
- name: Install Dependencies - name: Install Dependencies
run: npm install run: npm install
- name: Determine zstd binary version
shell: bash
run: |
case "$RUNNER_OS" in
"Linux")
echo "PLATFORM=linux-x64-gnu" >> "$GITHUB_ENV" ;;
"Windows")
echo "PLATFORM=win32-x64-msvc" >> "$GITHUB_ENV" ;;
"macOS")
echo "PLATFORM=darwin-arm64" >> "$GITHUB_ENV" ;;
esac
- name: Install zstd binary - name: Install zstd binary
shell: bash run: npm install "@mongodb-js/zstd"
run: npm install "@mongodb-js/zstd-$PLATFORM"
- name: Test - name: Test
run: npm run test run: npm run test
env: env:

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ tmp/
*.log *.log
coverage/ coverage/
bun.lockb bun.lockb
*.bak

View File

@ -39,10 +39,11 @@ minify:
css: css:
js: js:
svg: svg:
gzip:
brotli:
xml: xml:
json: json:
gzip:
brotli:
zstd:
``` ```
- **enable** - Enable the plugin. Defaults to `true`. - **enable** - Enable the plugin. Defaults to `true`.
@ -51,10 +52,11 @@ minify:
- **css** - See [CSS](#css) section - **css** - See [CSS](#css) section
- **js** - See [JS](#js) section - **js** - See [JS](#js) section
- **svg** - See [SVG](#svg) section - **svg** - See [SVG](#svg) section
- **gzip** - See [Gzip](#gzip) section
- **brotli** - See [Brotli](#brotli) section
- **xml** - See [XML](#xml) section - **xml** - See [XML](#xml) section
- **json** - See [JSON](#json) section - **json** - See [JSON](#json) section
- **gzip** - See [Gzip](#gzip) section
- **brotli** - See [Brotli](#brotli) section
- **zstd** - See [Zstd](#zstd) section
## HTML ## HTML
@ -88,6 +90,8 @@ minify:
- **verbose** - Verbose output. Defaults to `false`. - **verbose** - Verbose output. Defaults to `false`.
- **exclude** - Exclude files. Support [wildcard](http://www.globtester.com/) pattern(s) in a string or array. - **exclude** - Exclude files. Support [wildcard](http://www.globtester.com/) pattern(s) in a string or array.
- **level** - Optimization level. Defaults to `2`. - **level** - Optimization level. Defaults to `2`.
- **sourceMap** - Source map options. Defaults to `false`.
- **mapIncludeSources** - Include sources in map, Defaults to `false`.
- **globOptions** - See [globbing](#globbing) section. - **globOptions** - See [globbing](#globbing) section.
For more options, see [clean-css](https://github.com/jakubpawlowicz/clean-css). For more options, see [clean-css](https://github.com/jakubpawlowicz/clean-css).
@ -110,6 +114,8 @@ minify:
- **mangle** - Mangle variable names. Defaults to `true`. Pass an object to specify [mangle options](https://github.com/terser-js/terser#mangle-options). - **mangle** - Mangle variable names. Defaults to `true`. Pass an object to specify [mangle options](https://github.com/terser-js/terser#mangle-options).
- **output** - Output options. - **output** - Output options.
- To retain comments, `output: {comments: true}`. - To retain comments, `output: {comments: true}`.
- **sourceMap** - Source map options. Defaults to `false`.
- To include sources in map, `sourceMap: { includeSources: true }`
- **globOptions** - See [globbing](#globbing) section. - **globOptions** - See [globbing](#globbing) section.
For more options, see [Terser](https://github.com/terser-js/terser). For more options, see [Terser](https://github.com/terser-js/terser).
@ -175,7 +181,9 @@ minify:
enable: false enable: false
include: include:
- "*.json" - "*.json"
- "*.webmanifest"
- "!*.min.json" - "!*.min.json"
- "!*.min.webmanifest"
``` ```
- **enable** - Enable the plugin. Defaults to `false`. - **enable** - Enable the plugin. Defaults to `false`.
@ -195,6 +203,8 @@ minify:
- "*.html" - "*.html"
- "*.css" - "*.css"
- "*.js" - "*.js"
- "*.map"
- "*.wasm"
- "*.txt" - "*.txt"
- "*.ttf" - "*.ttf"
- "*.atom" - "*.atom"
@ -203,6 +213,7 @@ minify:
- "*.svg" - "*.svg"
- "*.eot" - "*.eot"
- "*.json" - "*.json"
- "*.webmanifest"
``` ```
- **enable** - Enable the plugin. Defaults to `true`. - **enable** - Enable the plugin. Defaults to `true`.
@ -211,6 +222,7 @@ minify:
- **include** - Include files. Support [wildcard](http://www.globtester.com/) pattern(s) in a string or array. - **include** - Include files. Support [wildcard](http://www.globtester.com/) pattern(s) in a string or array.
- Support one-liner, `include: ['*.html','*.css','*.js']`. - Support one-liner, `include: ['*.html','*.css','*.js']`.
- Must include asterisk and single quotes. `.html` is invalid. `'*.html'` is valid. - Must include asterisk and single quotes. `.html` is invalid. `'*.html'` is valid.
- **ensureCompressed** - Ensure the compressed file is smaller than the original, otherwise do not output. Defaults to `true`.
- **globOptions** - See [globbing](#globbing) section. - **globOptions** - See [globbing](#globbing) section.
- **level** - Compression level; lower value may results in faster compression but slightly larger (compressed) file. Range `1-9`. Defaults to `9`, or the value of [`zlib.constants.Z_BEST_COMPRESSION`](https://nodejs.org/docs/latest-v12.x/api/zlib.html#zlib_zlib_constants) - **level** - Compression level; lower value may results in faster compression but slightly larger (compressed) file. Range `1-9`. Defaults to `9`, or the value of [`zlib.constants.Z_BEST_COMPRESSION`](https://nodejs.org/docs/latest-v12.x/api/zlib.html#zlib_zlib_constants)
@ -224,6 +236,8 @@ minify:
- "*.html" - "*.html"
- "*.css" - "*.css"
- "*.js" - "*.js"
- "*.map"
- "*.wasm"
- "*.txt" - "*.txt"
- "*.ttf" - "*.ttf"
- "*.atom" - "*.atom"
@ -232,12 +246,14 @@ minify:
- "*.svg" - "*.svg"
- "*.eot" - "*.eot"
- "*.json" - "*.json"
- "*.webmanifest"
``` ```
- **enable** - Enable the plugin. Defaults to `true`. - **enable** - Enable the plugin. Defaults to `true`.
- **priority** - Plugin's priority. Defaults to `10`. - **priority** - Plugin's priority. Defaults to `10`.
- **verbose** - Verbose output. Defaults to `false`. - **verbose** - Verbose output. Defaults to `false`.
- **include** - Include files. Support [wildcard](http://www.globtester.com/) pattern(s) in a string or array. - **include** - Include files. Support [wildcard](http://www.globtester.com/) pattern(s) in a string or array.
- **ensureCompressed** - Ensure the compressed file is smaller than the original, otherwise do not output. Defaults to `true`.
- **globOptions** - See [globbing](#globbing) section. - **globOptions** - See [globbing](#globbing) section.
- **level** - Compression level. Range `1-11`. Defaults to `11`, or the value of [`zlib.constants.BROTLI_MAX_QUALITY`](https://nodejs.org/docs/latest-v12.x/api/zlib.html#zlib_brotli_constants) - **level** - Compression level. Range `1-11`. Defaults to `11`, or the value of [`zlib.constants.BROTLI_MAX_QUALITY`](https://nodejs.org/docs/latest-v12.x/api/zlib.html#zlib_brotli_constants)
@ -251,6 +267,8 @@ minify:
- "*.html" - "*.html"
- "*.css" - "*.css"
- "*.js" - "*.js"
- "*.map"
- "*.wasm"
- "*.txt" - "*.txt"
- "*.ttf" - "*.ttf"
- "*.atom" - "*.atom"
@ -259,12 +277,14 @@ minify:
- "*.svg" - "*.svg"
- "*.eot" - "*.eot"
- "*.json" - "*.json"
- "*.webmanifest"
``` ```
- **enable** - Enable the plugin. Defaults to `false`. - **enable** - Enable the plugin. Defaults to `false`.
- **priority** - Plugin's priority. Defaults to `10`. - **priority** - Plugin's priority. Defaults to `10`.
- **verbose** - Verbose output. Defaults to `false`. - **verbose** - Verbose output. Defaults to `false`.
- **include** - Include files. Support [wildcard](http://www.globtester.com/) pattern(s) in a string or array. - **include** - Include files. Support [wildcard](http://www.globtester.com/) pattern(s) in a string or array.
- **ensureCompressed** - Ensure the compressed file is smaller than the original, otherwise do not output. Defaults to `true`.
- **globOptions** - See [globbing](#globbing) section. - **globOptions** - See [globbing](#globbing) section.
- **level** - Compression level. Range `1-22`. Defaults to `3`, or the value of [`DEFAULT_LEVEL`](https://github.com/mongodb-js/zstd/blob/a3a08c61c9045411c8275e248498dbc583457fb5/src/lib.rs#L9) - **level** - Compression level. Range `1-22`. Defaults to `3`, or the value of [`DEFAULT_LEVEL`](https://github.com/mongodb-js/zstd/blob/a3a08c61c9045411c8275e248498dbc583457fb5/src/lib.rs#L9)

169
index.js
View File

@ -1,12 +1,15 @@
/* global hexo */ /* global hexo */
'use strict' 'use strict'
hexo.config.minify = Object.assign({ /** @typedef {import("@types/hexo")} */
enable: true,
previewServer: true
}, hexo.config.minify)
hexo.config.minify.html = Object.assign({ hexo.config.minify = {
enable: true,
previewServer: true,
...hexo.config.minify
}
hexo.config.minify.html = {
enable: true, enable: true,
priority: 10, priority: 10,
verbose: false, verbose: false,
@ -21,19 +24,23 @@ hexo.config.minify.html = Object.assign({
removeStyleLinkTypeAttributes: true, removeStyleLinkTypeAttributes: true,
minifyJS: true, minifyJS: true,
minifyCSS: true, minifyCSS: true,
globOptions: { basename: true } globOptions: { basename: true },
}, hexo.config.minify.html) ...hexo.config.minify.html
}
hexo.config.minify.css = Object.assign({ hexo.config.minify.css = {
enable: true, enable: true,
priority: 10, priority: 10,
verbose: false, verbose: false,
exclude: ['*.min.css'], exclude: ['*.min.css'],
level: 2, level: 2,
globOptions: { basename: true } sourceMap: false,
}, hexo.config.minify.css) mapIncludeSources: false,
globOptions: { basename: true },
...hexo.config.minify.css
}
hexo.config.minify.js = Object.assign({ hexo.config.minify.js = {
enable: true, enable: true,
priority: 10, priority: 10,
verbose: false, verbose: false,
@ -41,68 +48,114 @@ hexo.config.minify.js = Object.assign({
compress: {}, compress: {},
mangle: true, mangle: true,
output: {}, output: {},
globOptions: { basename: true } sourceMap: false,
}, hexo.config.minify.js) globOptions: { basename: true },
...hexo.config.minify.js
}
hexo.config.minify.svg = Object.assign({ hexo.config.minify.svg = {
enable: true, enable: true,
priority: 10, priority: 10,
verbose: false, verbose: false,
include: ['*.svg', '!*.min.svg'], include: ['*.svg', '!*.min.svg'],
plugins: {}, plugins: {},
globOptions: { basename: true } globOptions: { basename: true },
}, hexo.config.minify.svg) ...hexo.config.minify.svg
}
hexo.config.minify.gzip = Object.assign({ hexo.config.minify.xml = {
enable: true,
priority: 10,
verbose: false,
include: ['*.html', '*.css', '*.js', '*.txt', '*.ttf', '*.atom', '*.stl', '*.xml', '*.svg', '*.eot', '*.json'],
globOptions: { basename: true }
}, hexo.config.minify.gzip)
hexo.config.minify.brotli = Object.assign({
enable: true,
priority: 10,
verbose: false,
include: ['*.html', '*.css', '*.js', '*.txt', '*.ttf', '*.atom', '*.stl', '*.xml', '*.svg', '*.eot', '*.json'],
globOptions: { basename: true }
}, hexo.config.minify.brotli)
hexo.config.minify.zstd = Object.assign({
enable: false,
priority: 10,
verbose: false,
include: ['*.html', '*.css', '*.js', '*.txt', '*.ttf', '*.atom', '*.stl', '*.xml', '*.svg', '*.eot', '*.json'],
globOptions: { basename: true }
}, hexo.config.minify.zstd)
hexo.config.minify.xml = Object.assign({
enable: false, enable: false,
priority: 10, priority: 10,
verbose: false, verbose: false,
include: ['*.xml', '!*.min.xml'], include: ['*.xml', '!*.min.xml'],
removeComments: true, removeComments: true,
globOptions: { basename: true } globOptions: { basename: true },
}, hexo.config.minify.xml) ...hexo.config.minify.xml
}
hexo.config.minify.json = Object.assign({ hexo.config.minify.json = {
enable: false, enable: false,
priority: 10, priority: 10,
verbose: false, verbose: false,
include: ['*.json', '!*.min.json'], include: ['*.json', '*.webmanifest', '!*.min.json', '!*.min.webmanifest'],
globOptions: { basename: true } globOptions: { basename: true },
}, hexo.config.minify.json) ...hexo.config.minify.json
}
hexo.config.minify.gzip = {
enable: true,
priority: 10,
verbose: false,
include: ['*.html', '*.css', '*.js', '*.map', '*.wasm', '*.txt', '*.ttf', '*.atom', '*.stl', '*.xml', '*.svg', '*.eot', '*.json', '*.webmanifest'],
ensureCompressed: true,
globOptions: { basename: true },
...hexo.config.minify.gzip
}
hexo.config.minify.brotli = {
enable: true,
priority: 10,
verbose: false,
include: ['*.html', '*.css', '*.js', '*.map', '*.wasm', '*.txt', '*.ttf', '*.atom', '*.stl', '*.xml', '*.svg', '*.eot', '*.json', '*.webmanifest'],
ensureCompressed: true,
globOptions: { basename: true },
...hexo.config.minify.brotli
}
hexo.config.minify.zstd = {
enable: false,
priority: 10,
verbose: false,
include: ['*.html', '*.css', '*.js', '*.map', '*.wasm', '*.txt', '*.ttf', '*.atom', '*.stl', '*.xml', '*.svg', '*.eot', '*.json', '*.webmanifest'],
ensureCompressed: true,
globOptions: { basename: true },
...hexo.config.minify.zstd
}
if (hexo.config.minify.enable === true && !(hexo.config.minify.previewServer === true && ['s', 'server'].includes(hexo.env.cmd))) { if (hexo.config.minify.enable === true && !(hexo.config.minify.previewServer === true && ['s', 'server'].includes(hexo.env.cmd))) {
const filter = require('./lib/filter') if (hexo.config.minify.html.enable === true) {
hexo.extend.filter.register('after_render:html', filter.minifyHtml, hexo.config.minify.html.priority) hexo.extend.filter.register('after_render:html', require('./lib/html').minifyHtml, hexo.config.minify.html.priority)
hexo.extend.filter.register('after_render:css', filter.minifyCss, hexo.config.minify.css.priority) }
hexo.extend.filter.register('after_render:js', filter.minifyJs, hexo.config.minify.js.priority) if (hexo.config.minify.css.enable === true) {
hexo.extend.filter.register('after_generate', filter.minifySvg, hexo.config.minify.svg.priority) if (hexo.config.minify.css.sourceMap) {
hexo.extend.filter.register('after_generate', filter.gzipFn, hexo.config.minify.gzip.priority) hexo.extend.filter.register('after_generate', require('./lib/css').minifyCssWithMap, hexo.config.minify.js.priority)
hexo.extend.filter.register('after_generate', filter.brotliFn, hexo.config.minify.brotli.priority) }
hexo.extend.filter.register('after_generate', filter.zstdFn, hexo.config.minify.zstd.priority) else {
hexo.extend.filter.register('after_generate', filter.minifyXml, hexo.config.minify.xml.priority) hexo.extend.filter.register('after_render:css', require('./lib/css').minifyCss, hexo.config.minify.css.priority)
hexo.extend.filter.register('after_generate', filter.minifyJson, hexo.config.minify.json.priority) }
} }
if (hexo.config.minify.js.enable === true) {
if (hexo.config.minify.js.sourceMap) {
hexo.extend.filter.register('after_generate', require('./lib/js').minifyJsWithMap, hexo.config.minify.js.priority)
}
else {
hexo.extend.filter.register('after_render:js', require('./lib/js').minifyJs, hexo.config.minify.js.priority)
}
}
if (hexo.config.minify.svg.enable === true) {
hexo.extend.filter.register('after_generate', require('./lib/svg').minifySvg, hexo.config.minify.svg.priority)
}
if (hexo.config.minify.xml.enable === true) {
hexo.extend.filter.register('after_generate', require('./lib/xml').minifyXml, hexo.config.minify.xml.priority)
}
if (hexo.config.minify.json.enable === true) {
hexo.extend.filter.register('after_generate', require('./lib/json').minifyJson, hexo.config.minify.json.priority)
}
if (hexo.config.minify.gzip.enable || hexo.config.minify.brotli.enable) {
const zlib = require('./lib/zlib')
if (hexo.config.minify.gzip.enable === true) {
hexo.extend.filter.register('after_generate', zlib.gzipFn, hexo.config.minify.gzip.priority)
}
if (hexo.config.minify.brotli.enable === true) {
hexo.extend.filter.register('after_generate', zlib.brotliFn, hexo.config.minify.brotli.priority)
}
}
if (hexo.config.minify.zstd.enable === true) {
try {
hexo.extend.filter.register('after_generate', require('./lib/zstd').zstdFn, hexo.config.minify.zstd.priority)
} catch (ex) {
const log = hexo.log || console
log.warn(`ZSTD load failed. ${ex}`)
}
}
}

75
lib/css.js Normal file
View File

@ -0,0 +1,75 @@
'use strict'
const CleanCSS = require('clean-css')
const { isMatch, match, logFn } = require('./tools')
/**
* @param {string} str
* @param {{ path: string }} data
* @this {import('@types/hexo')}
*/
async function minifyCss(str, data) {
const hexo = this
const options = hexo.config.minify.css
if (!str) return str
const path = data.path
const { exclude, globOptions, verbose } = options
if (isMatch(path, exclude, globOptions)) return str
try {
const styles = await new CleanCSS(options).minify(str).styles
if (verbose) logFn.call(this, str, styles, path, 'css')
return styles
} catch (err) {
throw new Error(`Path: ${path}\n${err}`)
}
}
/**
* @this {import('@types/hexo')}
*/
function minifyCssWithMap() {
const hexo = this
const options = hexo.config.minify.css
const { parse } = require('path')
const route = hexo.route
const routeList = route.list()
/** @type {{ exclude: string[] }} */
const { exclude, globOptions, verbose } = options
const include = ['*.css', ...exclude.map(x => `!${x}`)]
const cleanCSS = new CleanCSS(options)
return Promise.all((match(routeList, include, globOptions)).map(path => {
return new Promise((/** @type {(value: void) => void} */ resolve, reject) => {
const assetPath = route.get(path)
let assetTxt = ''
assetPath.on('data', chunk => (assetTxt += chunk))
assetPath.on('end', async () => {
if (assetTxt.length) {
try {
const { base, ext, name } = parse(path)
const { styles, sourceMap } = await cleanCSS.minify(assetTxt)
if (verbose) logFn.call(this, assetTxt, result, path, 'css')
route.set(path, `${styles}\n/*# sourceMappingURL=${base}.map */`)
const map = sourceMap.toJSON()
map.sources = [`${name}.source${ext}`]
if (options.mapIncludeSources === true) {
map.sourcesContent = [assetTxt]
}
route.set(`${path}.map`, JSON.stringify(map))
} catch (err) {
reject(new Error(`Path: ${path}\n${err}`))
}
}
resolve()
})
})
}))
}
module.exports = {
minifyCss,
minifyCssWithMap
}

View File

@ -1,337 +1,11 @@
'use strict' 'use strict'
const { minify: htmlMinify } = require('html-minifier-terser')
const CleanCSS = require('clean-css')
const { minify: terserMinify } = require('terser')
const { optimize: svgOptimize } = require('svgo')
const zlib = require('zlib')
const { promisify } = require('util')
const gzip = promisify(zlib.gzip)
const br = promisify(zlib.brotliCompress)
const { minify: compressXml } = require('minify-xml')
const micromatch = require('micromatch')
const { compress: zstd } = require('@mongodb-js/zstd')
const isMatch = (path = '', patterns = [], options = {}) => {
if (path && patterns) {
if (path.length && patterns.length) {
if (typeof patterns === 'string') patterns = [patterns]
for (const pattern of patterns) {
// disable basename if a pattern includes a slash
let { basename } = options
// only disable when basename is enabled
basename = basename && !pattern.includes('/')
if (micromatch.isMatch(path, pattern, { ...options, basename })) {
return true
}
}
}
}
return false
}
const match = (paths = [], patterns = [], options = {}) => {
let input = paths
if (paths && patterns) {
if (paths.length && patterns.length) {
const output = []
if (typeof patterns === 'string') patterns = [patterns]
const exclude = patterns.filter((pattern) => pattern.startsWith('!'))
const include = patterns.filter((pattern) => !pattern.startsWith('!'))
if (exclude.length) input = micromatch(paths, exclude, options)
if (include.length) {
for (const pattern of include) {
let { basename } = options
basename = basename && !pattern.includes('/')
const tmp = micromatch(input, pattern, { ...options, basename })
if (tmp.length) output.push(...tmp)
}
return [...new Set(output)]
}
return input
}
}
return paths
}
function logFn (original, minified, path, ext) {
const saved = ((original.length - minified.length) / original.length * 100).toFixed(2)
const log = this.log || console
log.log(`${ext}: ${path} [${saved}% saved]`)
}
async function minifyHtml (str, data) {
const hexo = this
const options = hexo.config.minify.html
if (options.enable === false || !str) return
const { path } = data
const { exclude, globOptions, verbose } = options
// Return if a path matches exclusion pattern
if (isMatch(path, exclude, globOptions)) return str
try {
const result = await htmlMinify(str, options)
if (verbose) logFn.call(this, str, result, path, 'html')
return result
} catch (err) {
throw new Error(`Path: ${path}\n${err}`)
}
}
async function minifyCss (str, data) {
const hexo = this
const options = hexo.config.minify.css
if (options.enable === false || !str) return
const { path } = data
const { exclude, globOptions, verbose } = options
if (isMatch(path, exclude, globOptions)) return str
try {
const { styles } = await new CleanCSS(options).minify(str)
if (verbose) logFn.call(this, str, styles, path, 'css')
return styles
} catch (err) {
throw new Error(`Path: ${path}\n${err}`)
}
}
async function minifyJs (str, data) {
const hexo = this
const options = hexo.config.minify.js
if (options.enable === false || !str) return
const { path } = data
const { exclude, globOptions, verbose } = options
if (isMatch(path, exclude, globOptions)) return str
// Terser doesn't like unsupported options
const jsOptions = Object.assign({}, options)
delete jsOptions.enable
delete jsOptions.priority
delete jsOptions.verbose
// Old option, retained to avoid crash when upgrading to v4
delete jsOptions.logger
delete jsOptions.exclude
delete jsOptions.globOptions
try {
const { code } = await terserMinify(str, jsOptions)
if (verbose) logFn.call(this, str, code, path, 'js')
return code
} catch (err) {
throw new Error(`Path: ${path}\n${err}`)
}
}
function minifySvg () {
const hexo = this
const options = hexo.config.minify.svg
if (options.enable === false) return
const { route } = hexo
const routeList = route.list()
const { globOptions, include, verbose } = options
// const plugins = Array.isArray(options.plugins) ? extendDefaultPlugins(options.plugins) : extendDefaultPlugins([])
const pluginCfg = Object.prototype.toString.call(options.plugins) === '[object Object]' ? { ...options.plugins } : {}
const plugins = [{
name: 'preset-default',
params: {
overrides: pluginCfg
}
}]
return Promise.all((match(routeList, include, globOptions)).map((path) => {
return new Promise((resolve, reject) => {
const assetPath = route.get(path)
let assetTxt = ''
assetPath.on('data', (chunk) => (assetTxt += chunk))
assetPath.on('end', async () => {
if (assetTxt.length) {
try {
const { data } = svgOptimize(assetTxt, { ...options, plugins })
if (verbose) logFn.call(this, assetTxt, data, path, 'svg')
resolve(route.set(path, data))
} catch (err) {
reject(new Error(`Path: ${path}\n${err}`))
}
}
resolve()
})
})
}))
}
function gzipFn () {
const hexo = this
const options = hexo.config.minify.gzip
if (options.enable === false) return
const { route } = hexo
const routeList = route.list()
const { globOptions, include, verbose } = options
let { level } = options
if (typeof level !== 'number') level = zlib.constants.Z_BEST_COMPRESSION
return Promise.all((match(routeList, include, globOptions)).map((path) => {
return new Promise((resolve, reject) => {
const assetPath = route.get(path)
let assetTxt = ''
assetPath.on('data', (chunk) => (assetTxt += chunk))
assetPath.on('end', async () => {
if (assetTxt.length) {
try {
const result = await gzip(assetTxt, { level })
if (verbose) logFn.call(this, assetTxt, result, path, 'gzip')
resolve(route.set(path + '.gz', result))
} catch (err) {
reject(new Error(`Path: ${path}\n${err}`))
}
}
resolve()
})
})
}))
}
function brotliFn () {
const hexo = this
const options = hexo.config.minify.brotli
if (options.enable === false) return
const { route } = hexo
const routeList = route.list()
const { globOptions, include, verbose } = options
let { level } = options
if (typeof level !== 'number') level = zlib.constants.BROTLI_MAX_QUALITY
return Promise.all((match(routeList, include, globOptions)).map((path) => {
return new Promise((resolve, reject) => {
const assetPath = route.get(path)
let assetTxt = ''
assetPath.on('data', (chunk) => (assetTxt += chunk))
assetPath.on('end', async () => {
if (assetTxt.length) {
try {
const result = await br(assetTxt, { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: level } })
if (verbose) logFn.call(this, assetTxt, result, path, 'brotli')
resolve(route.set(path + '.br', result))
} catch (err) {
reject(new Error(`Path: ${path}\n${err}`))
}
}
resolve()
})
})
}))
}
function zstdFn () {
const hexo = this
const options = hexo.config.minify.zstd
if (options.enable === false) return
const { route } = hexo
const routeList = route.list()
const { globOptions, include, verbose } = options
let { level } = options
if (typeof level !== 'number') level = undefined
return Promise.all((match(routeList, include, globOptions)).map((path) => {
return new Promise((resolve, reject) => {
const assetPath = route.get(path)
let assetTxt = ''
assetPath.on('data', (chunk) => (assetTxt += chunk))
assetPath.on('end', async () => {
if (assetTxt.length) {
try {
const input = Buffer.from(assetTxt, 'utf-8')
const result = await zstd(input, level)
if (verbose) logFn.call(this, assetTxt, result, path, 'zstd')
resolve(route.set(path + '.zst', result))
} catch (err) {
reject(new Error(`Path: ${path}\n${err}`))
}
}
resolve()
})
})
}))
}
function minifyXml () {
const hexo = this
const options = hexo.config.minify.xml
if (options.enable === false) return
const { route } = hexo
const routeList = route.list()
const { globOptions, include, verbose } = options
return Promise.all((match(routeList, include, globOptions)).map((path) => {
return new Promise((resolve, reject) => {
const assetPath = route.get(path)
let assetTxt = ''
assetPath.on('data', (chunk) => (assetTxt += chunk))
assetPath.on('end', () => {
if (assetTxt.length) {
try {
const result = compressXml(assetTxt, { ...options })
if (verbose) logFn.call(this, assetTxt, result, path, 'xml')
resolve(route.set(path, result))
} catch (err) {
reject(new Error(`Path: ${path}\n${err}`))
}
}
resolve()
})
})
}))
}
function minifyJson () {
const hexo = this
const options = hexo.config.minify.json
if (options.enable === false) return
const { route } = hexo
const routeList = route.list()
const { globOptions, include, verbose } = options
return Promise.all((match(routeList, include, globOptions)).map((path) => {
return new Promise((resolve, reject) => {
const assetPath = route.get(path)
let assetTxt = ''
assetPath.on('data', (chunk) => (assetTxt += chunk))
assetPath.on('end', () => {
if (assetTxt.length) {
try {
const result = JSON.stringify(JSON.parse(assetTxt))
if (verbose) logFn.call(this, assetTxt, result, path, 'json')
resolve(route.set(path, result))
} catch (err) {
reject(new Error(`Path: ${path}\n${err}`))
}
}
resolve()
})
})
}))
}
module.exports = { module.exports = {
minifyHtml, ... require('./html'),
minifyCss, ... require('./css'),
minifyJs, ... require('./js'),
minifySvg, ... require('./svg'),
gzipFn, ... require('./xml'),
brotliFn, ... require('./json'),
zstdFn, ... require('./zlib'),
minifyXml, ... require('./zstd')
minifyJson }
}

32
lib/html.js Normal file
View File

@ -0,0 +1,32 @@
'use strict'
const { minify: htmlMinify } = require('html-minifier-terser')
const { isMatch, logFn } = require('./tools')
/**
* @param {string} str
* @param {{ path: string }} data
* @this {import('@types/hexo')}
*/
async function minifyHtml(str, data) {
const hexo = this
const options = hexo.config.minify.html
if (!str) return str
const path = data.path
const { exclude, globOptions, verbose } = options
// Return if a path matches exclusion pattern
if (isMatch(path, exclude, globOptions)) return str
try {
const result = await htmlMinify(str, options)
if (verbose) logFn.call(this, str, result, path, 'html')
return result
} catch (err) {
throw new Error(`Path: ${path}\n${err}`)
}
}
module.exports = {
minifyHtml
}

88
lib/js.js Normal file
View File

@ -0,0 +1,88 @@
'use strict'
const { minify: terserMinify } = require('terser')
const { isMatch, match, logFn } = require('./tools')
/**
* @param {string} str
* @param {{ path: string }} data
* @this {import('@types/hexo')}
*/
async function minifyJs(str, data) {
const hexo = this
const options = hexo.config.minify.js
if (!str) return str
const path = data.path
const { exclude, globOptions, verbose, ...jsOptions } = options
if (isMatch(path, exclude, globOptions)) return str
// Terser doesn't like unsupported options
delete jsOptions.enable
delete jsOptions.priority
// Old option, retained to avoid crash when upgrading to v4
delete jsOptions.logger
try {
const code = await terserMinify(str, jsOptions).code
if (verbose) logFn.call(this, str, code, path, 'js')
return code
} catch (err) {
throw new Error(`Path: ${path}\n${err}`)
}
}
/**
* @this {import('@types/hexo')}
*/
function minifyJsWithMap() {
const hexo = this
const options = hexo.config.minify.js
const { parse } = require('path')
const route = hexo.route
const routeList = route.list()
/** @type {{ exclude: string[] }} */
const { exclude, globOptions, verbose, ...jsOptions } = options
const include = ['*.js', ...exclude.map(x => `!${x}`)]
// Terser doesn't like unsupported options
delete jsOptions.enable
delete jsOptions.priority
// Old option, retained to avoid crash when upgrading to v4
delete jsOptions.logger
return Promise.all((match(routeList, include, globOptions)).map(path => {
return new Promise((/** @type {(value: void) => void} */ resolve, reject) => {
const assetPath = route.get(path)
let assetTxt = ''
assetPath.on('data', chunk => (assetTxt += chunk))
assetPath.on('end', async () => {
if (assetTxt.length) {
try {
const { base, ext, name } = parse(path)
jsOptions.sourceMap = {
...jsOptions.sourceMap,
filename: base,
asObject: true,
url: `${base}.map`
}
const { code, map } = await terserMinify(assetTxt, { ...jsOptions })
if (verbose) logFn.call(this, assetTxt, result, path, 'js')
route.set(path, code)
map.sources = [`${name}.source${ext}`]
route.set(`${path}.map`, JSON.stringify(map))
} catch (err) {
reject(new Error(`Path: ${path}\n${err}`))
}
}
resolve()
})
})
}))
}
module.exports = {
minifyJs,
minifyJsWithMap
}

39
lib/json.js Normal file
View File

@ -0,0 +1,39 @@
'use strict'
const { match, logFn } = require('./tools')
/**
* @this {import('@types/hexo')}
*/
function minifyJson() {
const hexo = this
const options = hexo.config.minify.json
const route = hexo.route
/** @type {string[]} */
const routeList = route.list()
const { globOptions, include, verbose } = options
return Promise.all((match(routeList, include, globOptions)).map(path => {
return new Promise((/** @type {(value: void) => void} */ resolve, reject) => {
const assetPath = route.get(path)
let assetTxt = ''
assetPath.on('data', chunk => (assetTxt += chunk))
assetPath.on('end', () => {
if (assetTxt.length) {
try {
const result = JSON.stringify(JSON.parse(assetTxt))
if (verbose) logFn.call(this, assetTxt, result, path, 'json')
route.set(path, result)
} catch (err) {
reject(new Error(`Path: ${path}\n${err}`))
}
}
resolve()
})
})
}))
}
module.exports = {
minifyJson
}

48
lib/svg.js Normal file
View File

@ -0,0 +1,48 @@
'use strict'
const { optimize: svgOptimize } = require('svgo')
const { match, logFn } = require('./tools')
/**
* @this {import('@types/hexo')}
*/
function minifySvg() {
const hexo = this
const options = hexo.config.minify.svg
const route = hexo.route
/** @type {string[]} */
const routeList = route.list()
const { globOptions, include, verbose } = options
// const plugins = Array.isArray(options.plugins) ? extendDefaultPlugins(options.plugins) : extendDefaultPlugins([])
const pluginCfg = typeof options.plugins === 'object' ? { ...options.plugins } : {}
const plugins = [{
name: 'preset-default',
params: {
overrides: pluginCfg
}
}]
return Promise.all((match(routeList, include, globOptions)).map(path => {
return new Promise((/** @type {(value: void) => void} */ resolve, reject) => {
const assetPath = route.get(path)
let assetTxt = ''
assetPath.on('data', chunk => (assetTxt += chunk))
assetPath.on('end', async () => {
if (assetTxt.length) {
try {
const data = svgOptimize(assetTxt, { ...options, plugins }).data
if (verbose) logFn.call(this, assetTxt, data, path, 'svg')
route.set(path, data)
} catch (err) {
reject(new Error(`Path: ${path}\n${err}`))
}
}
resolve()
})
})
}))
}
module.exports = {
minifySvg
}

72
lib/tools.js Normal file
View File

@ -0,0 +1,72 @@
'use strict'
const micromatch = require('micromatch')
/**
* @param {string | string[]} patterns
* @param {{ basename: string }} options
*/
function isMatch(path = '', patterns = [], options = {}) {
if (path && patterns) {
if (path.length && patterns.length) {
if (typeof patterns === 'string') patterns = [patterns]
for (const pattern of patterns) {
// disable basename if a pattern includes a slash
let basename = options.basename
// only disable when basename is enabled
basename = basename && !pattern.includes('/')
if (micromatch.isMatch(path, pattern, { ...options, basename })) {
return true
}
}
}
}
return false
}
/**
* @param {string[]} paths
* @param {string | string[]} patterns
* @param {{ basename: string }} options
*/
function match(paths = [], patterns = [], options = {}) {
let input = paths
if (paths && patterns) {
if (paths.length && patterns.length) {
/** @type {string[]} */
const output = []
if (typeof patterns === 'string') patterns = [patterns]
const exclude = patterns.filter(pattern => pattern.startsWith('!'))
const include = patterns.filter(pattern => !pattern.startsWith('!'))
if (exclude.length) input = micromatch(paths, exclude, options)
if (include.length) {
for (const pattern of include) {
let basename = options.basename
basename = basename && !pattern.includes('/')
const tmp = micromatch(input, pattern, { ...options, basename })
if (tmp.length) output.push(...tmp)
}
return [...new Set(output)]
}
return input
}
}
return paths
}
/**
* @param {{ length: number }} original
* @param {{ length: number }} minified
* @param {string} path
* @param {string} ext
*/
function logFn(original, minified, path, ext) {
const saved = ((original.length - minified.length) / original.length * 100).toFixed(2)
const log = this.log || console
log.log(`${ext}: ${path} [${saved}% saved]`)
}
module.exports = {
isMatch,
match,
logFn
}

41
lib/xml.js Normal file
View File

@ -0,0 +1,41 @@
'use strict'
const { match, logFn } = require('./tools')
/**
* @this {import('@types/hexo')}
*/
async function minifyXml() {
const { minify: compressXml } = await import('minify-xml')
const hexo = this
const options = hexo.config.minify.xml
const route = hexo.route
/** @type {string[]} */
const routeList = route.list()
const { globOptions, include, verbose } = options
await Promise.all((match(routeList, include, globOptions)).map(path => {
return new Promise((/** @type {(value: void) => void} */ resolve, reject) => {
const assetPath = route.get(path)
let assetTxt = ''
assetPath.on('data', chunk => (assetTxt += chunk))
assetPath.on('end', () => {
if (assetTxt.length) {
try {
const result = compressXml(assetTxt, { ...options })
if (verbose) logFn.call(this, assetTxt, result, path, 'xml')
route.set(path, result)
} catch (err) {
reject(new Error(`Path: ${path}\n${err}`))
}
}
resolve()
})
})
}))
}
module.exports = {
minifyXml
}

87
lib/zlib.js Normal file
View File

@ -0,0 +1,87 @@
'use strict'
const zlib = require('zlib')
const { promisify } = require('util')
const gzip = promisify(zlib.gzip)
const br = promisify(zlib.brotliCompress)
const { match, logFn } = require('./tools')
/**
* @this {import('@types/hexo')}
*/
function gzipFn() {
const hexo = this
const options = hexo.config.minify.gzip
const route = hexo.route
/** @type {string[]} */
const routeList = route.list()
const { globOptions, include, verbose, ensureCompressed } = options
let level = options.level
if (typeof level !== 'number') level = zlib.constants.Z_BEST_COMPRESSION
return Promise.all((match(routeList, include, globOptions)).map(path => {
return new Promise((/** @type {(value: void) => void} */ resolve, reject) => {
const assetPath = route.get(path)
let assetTxt = ''
assetPath.on('data', chunk => (assetTxt += chunk))
assetPath.on('end', async () => {
if (assetTxt.length) {
try {
const result = await gzip(assetTxt, { level })
const buffer = Buffer.from(assetTxt)
if (verbose) logFn.call(this, buffer, result, path, 'gzip')
if (!ensureCompressed || buffer.length > result.length) {
route.set(path + '.gz', result)
}
} catch (err) {
reject(new Error(`Path: ${path}\n${err}`))
}
}
resolve()
})
})
}))
}
/**
* @this {import('@types/hexo')}
*/
function brotliFn() {
const hexo = this
const options = hexo.config.minify.brotli
const route = hexo.route
/** @type {string[]} */
const routeList = route.list()
const { globOptions, include, verbose, ensureCompressed } = options
let level = options.level
if (typeof level !== 'number') level = zlib.constants.BROTLI_MAX_QUALITY
return Promise.all((match(routeList, include, globOptions)).map(path => {
return new Promise((/** @type {(value: void) => void} */ resolve, reject) => {
const assetPath = route.get(path)
let assetTxt = ''
assetPath.on('data', chunk => (assetTxt += chunk))
assetPath.on('end', async () => {
if (assetTxt.length) {
try {
const result = await br(assetTxt, { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: level } })
const buffer = Buffer.from(assetTxt)
if (verbose) logFn.call(this, buffer, result, path, 'brotli')
if (!ensureCompressed || buffer.length > result.length) {
route.set(path + '.br', result)
}
} catch (err) {
reject(new Error(`Path: ${path}\n${err}`))
}
}
resolve()
})
})
}))
}
module.exports = {
gzipFn,
brotliFn
}

72
lib/zstd.js Normal file
View File

@ -0,0 +1,72 @@
'use strict'
/**
* @returns {{ compress: (data: Buffer, level?: number) => Promise<Buffer>, init: () => Promise<void> }}
*/
function importZstd() {
try {
const { compress } = require('@mongodb-js/zstd')
return { compress };
}
catch (ex) {
try {
const { init, compress } = require('@bokuweb/zstd-wasm')
return {
compress: async (buf, level) => Buffer.from(compress(buf, level)),
init
}
}
catch {
throw ex;
}
}
}
const { compress: zstd, init = undefined } = importZstd()
const { match, logFn } = require('./tools')
/**
* @this {import('@types/hexo')}
*/
async function zstdFn() {
const hexo = this
const options = hexo.config.minify.zstd
const route = hexo.route
/** @type {string[]} */
const routeList = route.list()
const { globOptions, include, verbose, ensureCompressed } = options
let level = options.level
if (typeof level !== 'number') level = undefined
if (typeof init === 'function') {
await init();
}
await Promise.all((match(routeList, include, globOptions)).map(path => {
return new Promise((/** @type {(value: void) => void} */ resolve, reject) => {
const assetPath = route.get(path)
let assetTxt = ''
assetPath.on('data', chunk => (assetTxt += chunk))
assetPath.on('end', async () => {
if (assetTxt.length) {
try {
const input = Buffer.from(assetTxt, 'utf-8')
const result = await zstd(input, level)
if (verbose) logFn.call(this, input, result, path, 'zstd')
if (!ensureCompressed || input.length > result.length) {
route.set(path + '.zst', result)
}
} catch (err) {
reject(new Error(`Path: ${path}\n${err}`))
}
}
resolve()
})
})
}))
}
module.exports = {
zstdFn
}

View File

@ -29,16 +29,27 @@
"clean-css": "^5.1.2", "clean-css": "^5.1.2",
"html-minifier-terser": "^7.2.0", "html-minifier-terser": "^7.2.0",
"micromatch": "^4.0.2", "micromatch": "^4.0.2",
"minify-xml": "^3.2.0", "minify-xml": "^4.5.2",
"svgo": "^3.0.0", "svgo": "^3.0.0",
"terser": "^5.3.0", "terser": "^5.3.0"
"@mongodb-js/zstd": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"hexo": "^7.1.0", "hexo": "^7.1.0",
"jest": "^29.1.2", "jest": "^29.1.2",
"standard": "^17.0.0" "standard": "^17.0.0"
}, },
"peerDependencies": {
"@mongodb-js/zstd": "^2.0.0",
"@bokuweb/zstd-wasm": "^0.0.22"
},
"peerDependenciesMeta": {
"@mongodb-js/zstd": {
"optional": true
},
"@bokuweb/zstd-wasm": {
"optional": true
}
},
"keywords": [ "keywords": [
"minify", "minify",
"compress", "compress",