mirror of https://github.com/curbengh/hexo-yam
feat: split filter
This commit is contained in:
parent
4bd000bfee
commit
83484f0e47
|
@ -5,3 +5,4 @@ tmp/
|
|||
*.log
|
||||
coverage/
|
||||
bun.lockb
|
||||
*.bak
|
||||
|
|
115
index.js
115
index.js
|
@ -1,12 +1,13 @@
|
|||
/* global hexo */
|
||||
'use strict'
|
||||
|
||||
hexo.config.minify = Object.assign({
|
||||
hexo.config.minify = {
|
||||
enable: true,
|
||||
previewServer: true
|
||||
}, hexo.config.minify)
|
||||
previewServer: true,
|
||||
...hexo.config.minify
|
||||
}
|
||||
|
||||
hexo.config.minify.html = Object.assign({
|
||||
hexo.config.minify.html = {
|
||||
enable: true,
|
||||
priority: 10,
|
||||
verbose: false,
|
||||
|
@ -21,19 +22,21 @@ hexo.config.minify.html = Object.assign({
|
|||
removeStyleLinkTypeAttributes: true,
|
||||
minifyJS: true,
|
||||
minifyCSS: true,
|
||||
globOptions: { basename: true }
|
||||
}, hexo.config.minify.html)
|
||||
globOptions: { basename: true },
|
||||
...hexo.config.minify.html
|
||||
}
|
||||
|
||||
hexo.config.minify.css = Object.assign({
|
||||
hexo.config.minify.css = {
|
||||
enable: true,
|
||||
priority: 10,
|
||||
verbose: false,
|
||||
exclude: ['*.min.css'],
|
||||
level: 2,
|
||||
globOptions: { basename: true }
|
||||
}, hexo.config.minify.css)
|
||||
globOptions: { basename: true },
|
||||
...hexo.config.minify.css
|
||||
}
|
||||
|
||||
hexo.config.minify.js = Object.assign({
|
||||
hexo.config.minify.js = {
|
||||
enable: true,
|
||||
priority: 10,
|
||||
verbose: false,
|
||||
|
@ -41,68 +44,100 @@ hexo.config.minify.js = Object.assign({
|
|||
compress: {},
|
||||
mangle: true,
|
||||
output: {},
|
||||
globOptions: { basename: true }
|
||||
}, hexo.config.minify.js)
|
||||
globOptions: { basename: true },
|
||||
...hexo.config.minify.js
|
||||
}
|
||||
|
||||
hexo.config.minify.svg = Object.assign({
|
||||
hexo.config.minify.svg = {
|
||||
enable: true,
|
||||
priority: 10,
|
||||
verbose: false,
|
||||
include: ['*.svg', '!*.min.svg'],
|
||||
plugins: {},
|
||||
globOptions: { basename: true }
|
||||
}, hexo.config.minify.svg)
|
||||
globOptions: { basename: true },
|
||||
...hexo.config.minify.svg
|
||||
}
|
||||
|
||||
hexo.config.minify.gzip = Object.assign({
|
||||
hexo.config.minify.gzip = {
|
||||
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)
|
||||
globOptions: { basename: true },
|
||||
...hexo.config.minify.gzip
|
||||
}
|
||||
|
||||
hexo.config.minify.brotli = Object.assign({
|
||||
hexo.config.minify.brotli = {
|
||||
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)
|
||||
globOptions: { basename: true },
|
||||
...hexo.config.minify.brotli
|
||||
}
|
||||
|
||||
hexo.config.minify.zstd = Object.assign({
|
||||
hexo.config.minify.zstd = {
|
||||
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)
|
||||
globOptions: { basename: true },
|
||||
...hexo.config.minify.zstd
|
||||
}
|
||||
|
||||
hexo.config.minify.xml = Object.assign({
|
||||
hexo.config.minify.xml = {
|
||||
enable: false,
|
||||
priority: 10,
|
||||
verbose: false,
|
||||
include: ['*.xml', '!*.min.xml'],
|
||||
removeComments: true,
|
||||
globOptions: { basename: true }
|
||||
}, hexo.config.minify.xml)
|
||||
globOptions: { basename: true },
|
||||
...hexo.config.minify.xml
|
||||
}
|
||||
|
||||
hexo.config.minify.json = Object.assign({
|
||||
hexo.config.minify.json = {
|
||||
enable: false,
|
||||
priority: 10,
|
||||
verbose: false,
|
||||
include: ['*.json', '!*.min.json'],
|
||||
globOptions: { basename: true }
|
||||
}, hexo.config.minify.json)
|
||||
globOptions: { basename: true },
|
||||
...hexo.config.minify.json
|
||||
}
|
||||
|
||||
if (hexo.config.minify.enable === true && !(hexo.config.minify.previewServer === true && ['s', 'server'].includes(hexo.env.cmd))) {
|
||||
const filter = require('./lib/filter')
|
||||
hexo.extend.filter.register('after_render:html', filter.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)
|
||||
hexo.extend.filter.register('after_generate', filter.minifySvg, hexo.config.minify.svg.priority)
|
||||
hexo.extend.filter.register('after_generate', filter.gzipFn, hexo.config.minify.gzip.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)
|
||||
hexo.extend.filter.register('after_generate', filter.minifyXml, hexo.config.minify.xml.priority)
|
||||
hexo.extend.filter.register('after_generate', filter.minifyJson, hexo.config.minify.json.priority)
|
||||
if (hexo.config.minify.html.enable === true) {
|
||||
hexo.extend.filter.register('after_render:html', require('./lib/html').minifyHtml, hexo.config.minify.html.priority)
|
||||
}
|
||||
if (hexo.config.minify.css.enable === true) {
|
||||
hexo.extend.filter.register('after_render:css', require('./lib/css').minifyCss, hexo.config.minify.css.priority)
|
||||
}
|
||||
if (hexo.config.minify.js.enable === true) {
|
||||
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.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}`)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
'use strict'
|
||||
const CleanCSS = require('clean-css')
|
||||
const { isMatch, logFn } = require('./tools')
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @param {{ path: string }} data
|
||||
*/
|
||||
async function minifyCss(str, data) {
|
||||
const hexo = this
|
||||
const options = hexo.config.minify.css
|
||||
if (!str) return str
|
||||
|
||||
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}`)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
minifyCss
|
||||
}
|
342
lib/filter.js
342
lib/filter.js
|
@ -1,337 +1,11 @@
|
|||
'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 = {
|
||||
minifyHtml,
|
||||
minifyCss,
|
||||
minifyJs,
|
||||
minifySvg,
|
||||
gzipFn,
|
||||
brotliFn,
|
||||
zstdFn,
|
||||
minifyXml,
|
||||
minifyJson
|
||||
... require('./html'),
|
||||
... require('./css'),
|
||||
... require('./js'),
|
||||
... require('./svg'),
|
||||
... require('./zlib'),
|
||||
... require('./zstd'),
|
||||
... require('./xml'),
|
||||
... require('./json'),
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
'use strict'
|
||||
const { minify: htmlMinify } = require('html-minifier-terser')
|
||||
const { isMatch, logFn } = require('./tools')
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @param {{ path: string }} data
|
||||
*/
|
||||
async function minifyHtml(str, data) {
|
||||
const hexo = this
|
||||
const options = hexo.config.minify.html
|
||||
if (!str) return str
|
||||
|
||||
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}`)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
minifyHtml
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
'use strict'
|
||||
const { minify: terserMinify } = require('terser')
|
||||
const { isMatch, logFn } = require('./tools')
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @param {{ path: string }} data
|
||||
*/
|
||||
async function minifyJs(str, data) {
|
||||
const hexo = this
|
||||
const options = hexo.config.minify.js
|
||||
if (!str) return str
|
||||
|
||||
const { path } = data
|
||||
const { exclude, globOptions, verbose } = options
|
||||
|
||||
if (isMatch(path, exclude, globOptions)) return str
|
||||
|
||||
// Terser doesn't like unsupported options
|
||||
const jsOptions = { ...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}`)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
minifyJs
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
'use strict'
|
||||
const { match, logFn } = require('./tools')
|
||||
|
||||
function minifyJson() {
|
||||
const hexo = this
|
||||
const options = hexo.config.minify.json
|
||||
|
||||
const { route } = hexo
|
||||
/** @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: string) => 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')
|
||||
resolve(route.set(path, result))
|
||||
} catch (err) {
|
||||
reject(new Error(`Path: ${path}\n${err}`))
|
||||
}
|
||||
}
|
||||
resolve(assetTxt)
|
||||
})
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
minifyJson
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
'use strict'
|
||||
const { optimize: svgOptimize } = require('svgo')
|
||||
const { match, logFn } = require('./tools')
|
||||
|
||||
function minifySvg() {
|
||||
const hexo = this
|
||||
const options = hexo.config.minify.svg
|
||||
|
||||
const { route } = hexo
|
||||
/** @type {string[]} */
|
||||
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((/** @type {(value: string) => 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 })
|
||||
if (verbose) logFn.call(this, assetTxt, data, path, 'svg')
|
||||
resolve(route.set(path, data))
|
||||
} catch (err) {
|
||||
reject(new Error(`Path: ${path}\n${err}`))
|
||||
}
|
||||
}
|
||||
resolve(assetTxt)
|
||||
})
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
minifySvg
|
||||
}
|
|
@ -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
|
||||
// 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 && !pattern.includes('/')
|
||||
const tmp = micromatch(input, pattern, { ...options, basename })
|
||||
if (tmp.length) output.push(...tmp)
|
||||
}
|
||||
return [...new Set(output)]
|
||||
}
|
||||
return input
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} original
|
||||
* @param {string} ext
|
||||
* @param {string} minified
|
||||
* @param {string} path
|
||||
*/
|
||||
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
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
'use strict'
|
||||
const { minify: compressXml } = require('minify-xml')
|
||||
const { match, logFn } = require('./tools')
|
||||
|
||||
function minifyXml() {
|
||||
const hexo = this
|
||||
const options = hexo.config.minify.xml
|
||||
|
||||
const { route } = hexo
|
||||
/** @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: string) => 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')
|
||||
resolve(route.set(path, result))
|
||||
} catch (err) {
|
||||
reject(new Error(`Path: ${path}\n${err}`))
|
||||
}
|
||||
}
|
||||
resolve(assetTxt)
|
||||
})
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
minifyXml
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
'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')
|
||||
|
||||
function gzipFn() {
|
||||
const hexo = this
|
||||
const options = hexo.config.minify.gzip
|
||||
|
||||
const { route } = hexo
|
||||
/** @type {string} */
|
||||
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((/** @type {(value: string) => 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 })
|
||||
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(assetTxt)
|
||||
})
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
function brotliFn() {
|
||||
const hexo = this
|
||||
const options = hexo.config.minify.brotli
|
||||
|
||||
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((/** @type {(value: string) => 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 } })
|
||||
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(assetTxt)
|
||||
})
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
gzipFn,
|
||||
brotliFn
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
'use strict'
|
||||
const { compress: zstd } = require('@mongodb-js/zstd')
|
||||
const { match, logFn } = require('./tools')
|
||||
|
||||
function zstdFn() {
|
||||
const hexo = this
|
||||
const options = hexo.config.minify.zstd
|
||||
|
||||
const { route } = hexo
|
||||
/** @type {string[]} */
|
||||
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((/** @type {(value: string) => 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, assetTxt, result, path, 'zstd')
|
||||
resolve(route.set(path + '.zst', result))
|
||||
} catch (err) {
|
||||
reject(new Error(`Path: ${path}\n${err}`))
|
||||
}
|
||||
}
|
||||
resolve(assetTxt)
|
||||
})
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
zstdFn
|
||||
}
|
Loading…
Reference in New Issue