Compare commits

..

No commits in common. "master" and "v5.0.0" have entirely different histories.

18 changed files with 265 additions and 623 deletions

View File

@ -6,17 +6,19 @@ jobs:
linter:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install Bun
uses: oven-sh/setup-bun@v2
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v2.1.5
with:
node-version: '14.x'
- name: Cache NPM dependencies
uses: actions/cache@v4
uses: actions/cache@v2.1.4
with:
path: node_modules
key: ${{ runner.os }}-npm-cache
restore-keys: |
${{ runner.os }}-npm-cache
- name: Install Dependencies
run: bun install
run: npm install
- name: Lint
run: bun run lint
run: npm run lint

View File

@ -1,23 +0,0 @@
name: Semgrep
on:
pull_request: {}
push:
branches:
- master
paths:
- .github/workflows/semgrep.yml
schedule:
# Weekly
- cron: "0 0 * * 0"
jobs:
semgrep:
name: Scan
runs-on: ubuntu-latest
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
container:
image: returntocorp/semgrep
if: (github.actor != 'dependabot[bot]')
steps:
- uses: actions/checkout@v5
- run: semgrep ci

View File

@ -2,34 +2,21 @@ name: Snyk
on:
schedule:
# Weekly
- cron: "0 0 * * 0"
pull_request:
branches:
- "dependabot/github_actions/github/codeql-action**"
# Every day
- cron: '0 0 * * *'
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install Bun
uses: oven-sh/setup-bun@v2
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v2.1.5
with:
node-version: '14.x'
- name: Install Dependencies
run: bun install
run: npm install
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/node@master
continue-on-error: true # To make sure that SARIF upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
command: test
args: --sarif-file-output=snyk.sarif
- name: Upload result to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: snyk.sarif
- name: Monitor for vulnerabilities
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

View File

@ -8,45 +8,51 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: ["20", "22", "24"]
node-version: ['12', '14', '15', '16']
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v2.1.5
with:
node-version: ${{ matrix.node-version }}
- name: Cache NPM dependencies
uses: actions/cache@v4
uses: actions/cache@v2.1.4
with:
path: node_modules
key: ${{ runner.os }}-npm-cache
restore-keys: ${{ runner.os }}-npm-cache
- name: Determine unrs-resolver 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 Dependencies
shell: bash
run: |
npm install
npm install --include=optional --force @mongodb-js/zstd
npm install "@unrs/resolver-binding-$PLATFORM"
run: npm install
- name: Test
run: npm run test
env:
CI: true
coverage:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
node-version: ['14.x']
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2.1.5
with:
node-version: ${{ matrix.node-version }}
- name: Cache NPM dependencies
uses: actions/cache@v2.1.4
with:
path: node_modules
key: ${{ runner.os }}-npm-cache
restore-keys: ${{ runner.os }}-npm-cache
- name: Install Dependencies
run: npm install
- name: Coverage
run: npm run test
env:
CI: true
- name: Upload coverage report to Codecov
if: matrix.os == 'ubuntu-latest' && matrix.node-version == '22'
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v1
with:
fail_ci_if_error: true
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

1
.gitignore vendored
View File

@ -4,4 +4,3 @@ package-lock.json
tmp/
*.log
coverage/
bun.lockb

1
.npmrc
View File

@ -1 +1,2 @@
package-lock=false
optional=false

6
.snyk
View File

@ -2,4 +2,8 @@
version: v1.12.0
# ignores vulnerabilities until expiry date; change duration by modifying expiry date
ignore:
patch:
SNYK-JS-LODASH-450202:
- '*':
reason: Patch/update unavailable. Introduced through Snyk.
expires: '2019-12-31T00:00:00.000Z'
patch: {}

View File

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2018-2024 curbengh, 2016-2018 rozbo
Copyright (c) 2018-2019 curbengh, 2016-2018 rozbo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

259
README.md
View File

@ -1,40 +1,58 @@
# hexo-yam
[![npm version](https://img.shields.io/npm/v/hexo-yam?logo=npm)](https://www.npmjs.com/package/hexo-yam)
[![Build Status](https://img.shields.io/github/actions/workflow/status/curbengh/hexo-yam/tester.yml?branch=master&logo=github)](https://github.com/curbengh/hexo-yam/actions?query=workflow%3ATester)
[![Build Status](https://img.shields.io/github/workflow/status/curbengh/hexo-yam/Tester?logo=github)](https://github.com/curbengh/hexo-yam/actions?query=workflow%3ATester)
[![codecov](https://img.shields.io/codecov/c/gh/curbengh/hexo-yam?logo=codecov)](https://codecov.io/gh/curbengh/hexo-yam)
[![NPM Dependencies](https://img.shields.io/librariesio/release/npm/hexo-yam)](https://libraries.io/npm/hexo-yam)
[![Known Vulnerabilities](https://snyk.io/test/github/curbengh/hexo-yam/badge.svg)](https://snyk.io/test/github/curbengh/hexo-yam)
[![Known Vulnerabilities](https://img.shields.io/snyk/vulnerabilities/npm/hexo-yam?logo=snyk)](https://snyk.io/test/npm/hexo-yam)
[![Total alerts](https://img.shields.io/lgtm/alerts/g/curbengh/hexo-yam.svg?logo=lgtm)](https://lgtm.com/projects/g/curbengh/hexo-yam/alerts/)
[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/curbengh/hexo-yam.svg?logo=lgtm)](https://lgtm.com/projects/g/curbengh/hexo-yam/context:javascript)
Yet Another Minifier for Hexo. Minify and compress HTML, JS, CSS, SVG, XML and JSON. [Other files](https://github.com/curbengh/hexo-yam/blob/ba77db0094a7c07ea9f70f010bfc15541d4105ca/index.js#L64) are also compressed. Support gzip and [brotli](https://en.wikipedia.org/wiki/Brotli) [compressions](https://en.wikipedia.org/wiki/HTTP_compression).
Yet Another Minifier for Hexo. Minify HTML, JS, CSS, SVG, XML and JSON. Compress static [web assets](https://github.com/curbengh/hexo-yam/blob/ba77db0094a7c07ea9f70f010bfc15541d4105ca/index.js#L64) using gzip, brotli and zstd.
## Table of contents
- [Version 5](#version-5)
- [Installation](#installation)
- [Options](#options)
- [HTML](#html)
- [CSS](#css)
- [JS](#js)
- [SVG](#svg)
- [XML](#xml)
- [JSON](#json)
- [Gzip](#gzip)
- [Brotli](#brotli)
- [Zstd](#zstd)
- [XML](#xml)
- [JSON](#json)
- [Globbing](#globbing)
- [HTTP Compression](#http-compression)
## Version 5
In v5, `svg.plugins:` option should follow svgo v2+ syntax:
``` diff
minify:
svg:
plugins:
- - removeComments: false
- - cleanupIDs: false
+ - name: 'removeComments'
+ active: false
+ - name: 'cleanupIDs'
+ active: false
```
## Installation
```bash
$ npm install --save hexo-yam
``` bash
$ npm install hexo-yam --save
```
## Options
```yaml
``` yaml
minify:
enable: true
previewServer: true
html:
css:
js:
@ -46,7 +64,6 @@ minify:
```
- **enable** - Enable the plugin. Defaults to `true`.
- **previewServer** - Disable the plugin when running `hexo server`. Defaults to `true`.
- **html** - See [HTML](#html) section
- **css** - See [CSS](#css) section
- **js** - See [JS](#js) section
@ -58,13 +75,12 @@ minify:
## HTML
```yaml
``` yaml
minify:
html:
enable: true
exclude:
```
- **enable** - Enable the plugin. Defaults to `true`.
- **priority** - Plugin's priority. Defaults to `10`. Set lower value to set higher priority and vice versa.
- **verbose** - Verbose output. Defaults to `false`.
@ -75,14 +91,13 @@ For more options, see [HTMLMinifier](https://github.com/kangax/html-minifier).
## CSS
```yaml
``` yaml
minify:
css:
enable: true
exclude:
- "*.min.css"
- '*.min.css'
```
- **enable** - Enable the plugin. Defaults to `true`.
- **priority** - Plugin's priority. Defaults to `10`.
- **verbose** - Verbose output. Defaults to `false`.
@ -94,14 +109,13 @@ For more options, see [clean-css](https://github.com/jakubpawlowicz/clean-css).
## JS
```yaml
``` yaml
minify:
js:
enable: true
exclude:
- "*.min.js"
- '*.min.js'
```
- **enable** - Enable the plugin. Defaults to `true`.
- **priority** - Plugin's priority. Defaults to `10`.
- **verbose** - Verbose output. Defaults to `false`.
@ -116,15 +130,14 @@ For more options, see [Terser](https://github.com/terser-js/terser).
## SVG
```yaml
``` yaml
minify:
svg:
enable: true
include:
- "*.svg"
- "!*.min.svg"
- '*.svg'
- '!*.min.svg'
```
- **enable** - Enable the plugin. Defaults to `true`.
- **priority** - Plugin's priority. Defaults to `10`.
- **verbose** - Verbose output. Defaults to `false`.
@ -132,29 +145,84 @@ minify:
- Exclude `*.min.svg` by default.
- **plugins** - Plugin options.
- Examples:
```yaml
``` yaml
plugins:
# Retain comments
removeComments: false
- name: 'removeComments'
active: false
# Do not remove unused ID attributes
cleanupIds: false
- name: 'cleanupIDs'
active: false
```
- For more options, see [svgo](https://github.com/svg/svgo).
- **globOptions** - See [globbing](#globbing) section.
## Gzip
``` yaml
minify:
gzip:
enable: true
include:
- '*.html'
- '*.css'
- '*.js'
- '*.txt'
- '*.ttf'
- '*.atom'
- '*.stl'
- '*.xml'
- '*.svg'
- '*.eot'
- '*.json'
```
- **enable** - Enable the plugin. Defaults to `true`.
- **priority** - Plugin's priority. Defaults to `10`.
- **verbose** - Verbose output. Defaults to `false`.
- **include** - Include files. Support [wildcard](http://www.globtester.com/) pattern(s) in a string or array.
- Support one-liner, `include: ['*.html','*.css','*.js']`.
- Must include asterisk and single quotes. `.html` is invalid. `'*.html'` is valid.
- **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)
## Brotli
``` yaml
minify:
brotli:
enable: true
include:
- '*.html'
- '*.css'
- '*.js'
- '*.txt'
- '*.ttf'
- '*.atom'
- '*.stl'
- '*.xml'
- '*.svg'
- '*.eot'
- '*.json'
```
- **enable** - Enable the plugin. Defaults to `true`.
- **priority** - Plugin's priority. Defaults to `10`.
- **verbose** - Verbose output. Defaults to `false`.
- **include** - Include files. Support [wildcard](http://www.globtester.com/) pattern(s) in a string or array.
- **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)
## XML
Remove whitespaces in xml.
```yaml
``` yaml
minify:
xml:
enable: false
include:
- "*.xml"
- "!*.min.xml"
- '*.xml'
- '!*.min.xml'
```
- **enable** - Enable the plugin. Defaults to `false`.
- **priority** - Plugin's priority. Defaults to `10`.
- **verbose** - Verbose output. Defaults to `false`.
@ -169,15 +237,14 @@ For more options, see [minify-xml](https://github.com/kristian/minify-xml#option
Remove whitespaces in json.
```yaml
``` yaml
minify:
json:
enable: false
include:
- "*.json"
- "!*.min.json"
- '*.json'
- '!*.min.json'
```
- **enable** - Enable the plugin. Defaults to `false`.
- **priority** - Plugin's priority. Defaults to `10`.
- **verbose** - Verbose output. Defaults to `false`.
@ -185,89 +252,6 @@ minify:
- Exclude `*.min.json` by default.
- **globOptions** - See [globbing](#globbing) section.
## Gzip
```yaml
minify:
gzip:
enable: true
include:
- "*.html"
- "*.css"
- "*.js"
- "*.txt"
- "*.ttf"
- "*.atom"
- "*.stl"
- "*.xml"
- "*.svg"
- "*.eot"
- "*.json"
```
- **enable** - Enable the plugin. Defaults to `true`.
- **priority** - Plugin's priority. Defaults to `10`.
- **verbose** - Verbose output. Defaults to `false`.
- **include** - Include files. Support [wildcard](http://www.globtester.com/) pattern(s) in a string or array.
- Support one-liner, `include: ['*.html','*.css','*.js']`.
- Must include asterisk and single quotes. `.html` is invalid. `'*.html'` is valid.
- **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)
## Brotli
```yaml
minify:
brotli:
enable: true
include:
- "*.html"
- "*.css"
- "*.js"
- "*.txt"
- "*.ttf"
- "*.atom"
- "*.stl"
- "*.xml"
- "*.svg"
- "*.eot"
- "*.json"
```
- **enable** - Enable the plugin. Defaults to `true`.
- **priority** - Plugin's priority. Defaults to `10`.
- **verbose** - Verbose output. Defaults to `false`.
- **include** - Include files. Support [wildcard](http://www.globtester.com/) pattern(s) in a string or array.
- **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)
## Zstd
```yaml
minify:
zstd:
enable: false
include:
- "*.html"
- "*.css"
- "*.js"
- "*.txt"
- "*.ttf"
- "*.atom"
- "*.stl"
- "*.xml"
- "*.svg"
- "*.eot"
- "*.json"
```
- **enable** - Enable the plugin. Defaults to `false`.
- **priority** - Plugin's priority. Defaults to `10`.
- **verbose** - Verbose output. Defaults to `false`.
- **include** - Include files. Support [wildcard](http://www.globtester.com/) pattern(s) in a string or array.
- **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)
## Globbing
Use "globOptions" to customise how glob patterns match files. Refer to [micromatch](https://github.com/micromatch/micromatch#options) for available options.
@ -276,32 +260,51 @@ Use "globOptions" to customise how glob patterns match files. Refer to [micromat
- basename is disabled depending on each pattern.
- This means the following options would work,
```yml
``` yml
exclude:
- "*foo.html" # basename is enabled
- "**/bar/*/*.html" # basename is automatically disabled
- "*baz.css" # basename is enabled
- '*foo.html' # basename is enabled
- '**/bar/*/*.html' # basename is automatically disabled
- '*baz.css' # basename is enabled
globOptions:
basename: true # default
```
- This behaviour doesn't apply to pattern that starts with `!` (negation).
- This limitation only applies to `include:` option used in svg, gzip and brotli.
- This means the following options would _not_ work,
- This means the following options would *not* work,
```yml
``` yml
include:
- "!foo.svg"
- "!**/bar/*/*.svg"
- '!foo.svg'
- '!**/bar/*/*.svg'
globOptions:
basename: true
```
- basename will stay disabled, if explicitly disabled in "globOptions".
## HTTP Compression
While most modern web browsers [support Brotli](https://www.caniuse.com/#feat=brotli), you also need to consider whether the web/app server, hosting platform, reverse proxy or CDN (whichever relevant to you) support it.
As of Sep 2020, GitHub Pages and GitLab Pages *do not* support brotli yet. You can generate `.br` files, but they won't serve those files.
Name | Brotli support
--- | ---
GitHub Pages | [In consideration](https://github.community/t5/GitHub-Pages/Support-for-pre-compressed-assets-and-brotli-compression/m-p/22055)
GitLab Pages | Yes
Netlify | Yes
Hexo Server | [In progress](https://github.com/hexojs/hexo-server/pull/100)
If you have access to the web server config, you should disable on-the-fly compression for static files (that are already compressed by this plugin),
- [nginx](https://github.com/google/ngx_brotli): Make sure both filter and static modules are enabled. This way pre-compressed `.br` files will be served while dynamic content can be compressed on-the-fly. Protip: `brotli_types text/plain text/css application/javascript application/json image/svg+xml application/xml+rss;` to prevent compressing media files (which are already compressed anyway).
- [Apache](https://httpd.apache.org/docs/2.4/en/mod/mod_brotli.html): See 'Serving pre-compressed content' section of [mod_brotli](https://httpd.apache.org/docs/2.4/en/mod/mod_brotli.html).
- [Caddy](https://caddyserver.com/features): [0.9.4+](https://caddyserver.com/blog/caddy-0_9_4-released) by default support pre-compressed `.gz` `.br` files and on-the-fly gzip compress dynamic files. v2 requires [manual configuration](https://mdleom.com/blog/2020/11/12/caddy2-pre-compressed/), serving them automatically to be supported [in future](https://github.com/caddyserver/caddy/issues/2665).
- [express](https://github.com/expressjs/express)/[connect](https://github.com/senchalabs/connect): Use [pre-compressed-assets](https://github.com/domadams/pre-compressed-assets). You still can continue to use [compression](https://github.com/expressjs/compression)/[shrink-ray-current](https://github.com/Alorel/shrink-ray) for dynamic files.
## Credits
All credits go to the following work:
- [hexo-neat](https://github.com/rozbo/hexo-neat) by rozbo
- gzip feature is inspired by [hexo-generator-optimize](https://github.com/JackyRen/hexo-generator-optimize)

View File

@ -1,5 +0,0 @@
[install]
optional = false
[install.lockfile]
save = false

View File

@ -1,9 +1,10 @@
/* global hexo */
'use strict'
const { extendDefaultPlugins } = require('svgo')
hexo.config.minify = Object.assign({
enable: true,
previewServer: true
enable: true
}, hexo.config.minify)
hexo.config.minify.html = Object.assign({
@ -49,7 +50,7 @@ hexo.config.minify.svg = Object.assign({
priority: 10,
verbose: false,
include: ['*.svg', '!*.min.svg'],
plugins: {},
plugins: extendDefaultPlugins([]),
globOptions: { basename: true }
}, hexo.config.minify.svg)
@ -69,14 +70,6 @@ hexo.config.minify.brotli = Object.assign({
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,
priority: 10,
@ -94,7 +87,7 @@ hexo.config.minify.json = Object.assign({
globOptions: { basename: true }
}, hexo.config.minify.json)
if (hexo.config.minify.enable === true && !(hexo.config.minify.previewServer === true && ['s', 'server'].includes(hexo.env.cmd))) {
if (hexo.config.minify.enable === true) {
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)
@ -102,7 +95,6 @@ if (hexo.config.minify.enable === true && !(hexo.config.minify.previewServer ===
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)
}

View File

@ -1,16 +1,15 @@
'use strict'
const { minify: htmlMinify } = require('html-minifier-terser')
const { minify: htmlMinify } = require('html-minifier')
const CleanCSS = require('clean-css')
const { minify: terserMinify } = require('terser')
const { optimize: svgOptimize } = require('svgo')
const zlib = require('node:zlib')
const { promisify } = require('node:util')
const { optimize: svgOptimize, extendDefaultPlugins } = 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) {
@ -60,7 +59,7 @@ function logFn (original, minified, path, ext) {
log.log(`${ext}: ${path} [${saved}% saved]`)
}
async function minifyHtml (str, data) {
function minifyHtml (str, data) {
const hexo = this
const options = hexo.config.minify.html
if (options.enable === false || !str) return
@ -72,7 +71,7 @@ async function minifyHtml (str, data) {
if (isMatch(path, exclude, globOptions)) return str
try {
const result = await htmlMinify(str, options)
const result = htmlMinify(str, options)
if (verbose) logFn.call(this, str, result, path, 'html')
return result
@ -137,14 +136,7 @@ function minifySvg () {
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
}
}]
const plugins = Array.isArray(options.plugins) ? extendDefaultPlugins(options.plugins) : extendDefaultPlugins([])
return Promise.all((match(routeList, include, globOptions)).map((path) => {
return new Promise((resolve, reject) => {
@ -153,12 +145,12 @@ function minifySvg () {
assetPath.on('data', (chunk) => (assetTxt += chunk))
assetPath.on('end', async () => {
if (assetTxt.length) {
try {
const { data } = svgOptimize(assetTxt, { ...options, plugins })
const { data, error } = svgOptimize(assetTxt, { ...options, plugins })
if (data) {
if (verbose) logFn.call(this, assetTxt, data, path, 'svg')
resolve(route.set(path, data))
} catch (err) {
reject(new Error(`Path: ${path}\n${err}`))
} else if (error) {
reject(new Error(`Path: ${path}\n${error}`))
}
}
resolve()
@ -231,39 +223,6 @@ function brotliFn () {
}))
}
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
@ -331,7 +290,6 @@ module.exports = {
minifySvg,
gzipFn,
brotliFn,
zstdFn,
minifyXml,
minifyJson
}

View File

@ -1,7 +1,7 @@
{
"name": "hexo-yam",
"description": "Yet Another Minifier. Minify and compress html, js, css, svg, xml and json",
"version": "9.0.0",
"version": "5.0.0",
"readme": "README.md",
"main": "index.js",
"directories": {
@ -16,35 +16,36 @@
"test": "jest"
},
"engines": {
"node": ">= 20.9.0"
"node": ">= 12.13.0"
},
"author": "curben",
"license": "MIT",
"homepage": "https://github.com/curbengh/hexo-yam",
"repository": {
"type": "git",
"url": "git+https://github.com/curbengh/hexo-yam.git"
},
"repository": "curbengh/hexo-yam",
"dependencies": {
"clean-css": "^5.1.2",
"html-minifier-terser": "^7.2.0",
"html-minifier": "^4.0.0",
"micromatch": "^4.0.2",
"minify-xml": "^3.2.0",
"svgo": "^4.0.0",
"terser": "^5.3.0",
"@mongodb-js/zstd": "^2.0.1"
"minify-xml": "^2.1.1",
"svgo": "^2.3.0",
"terser": "^5.3.0"
},
"devDependencies": {
"hexo": "^7.1.0",
"jest": "^30.0.4",
"standard": "^17.0.0"
"hexo": "^5.1.1",
"jest": "^26.4.2",
"standard": "^16.0.1"
},
"keywords": [
"html",
"js",
"css",
"svg",
"xml",
"json",
"minify",
"compress",
"gzip",
"brotli",
"zstd",
"hexo-yam",
"hexo"
],
@ -52,9 +53,6 @@
"clearMocks": true,
"collectCoverage": true,
"coverageDirectory": "./coverage/",
"testEnvironment": "node",
"testEnvironmentOptions": {
"globalsCleanup": "off"
}
"testEnvironment": "node"
}
}

View File

@ -2,8 +2,8 @@
'use strict'
const Hexo = require('hexo')
const zlib = require('node:zlib')
const { promisify } = require('node:util')
const zlib = require('zlib')
const { promisify } = require('util')
const brotli = promisify(zlib.brotliCompress)
const unbrotli = promisify(zlib.brotliDecompress)

View File

@ -2,8 +2,8 @@
'use strict'
const Hexo = require('hexo')
const zlib = require('node:zlib')
const { promisify } = require('node:util')
const zlib = require('zlib')
const { promisify } = require('util')
const gzip = promisify(zlib.gzip)
const unzip = promisify(zlib.unzip)

View File

@ -2,7 +2,7 @@
'use strict'
const Hexo = require('hexo')
const { minify: htmlMinify } = require('html-minifier-terser')
const { minify: htmlMinify } = require('html-minifier')
describe('html', () => {
const hexo = new Hexo(__dirname)
@ -26,107 +26,105 @@ describe('html', () => {
globOptions: { basename: true }
}
}
let expected = ''
beforeAll(async () => {
expected = await htmlMinify(input, defaultCfg.html)
})
const expected = htmlMinify(input, defaultCfg.html)
beforeEach(() => {
hexo.config.minify = JSON.parse(JSON.stringify(defaultCfg))
})
test('default', async () => {
const result = await h(input, { path })
test('default', () => {
const result = h(input, { path })
expect(result).toBe(expected)
})
test('disable', async () => {
test('disable', () => {
hexo.config.minify.html.enable = false
const result = await h(input, { path })
const result = h(input, { path })
expect(result).toBeUndefined()
})
test('empty file', async () => {
const result = await h('', { path })
test('empty file', () => {
const result = h('', { path })
expect(result).toBeUndefined()
})
test('option', async () => {
test('option', () => {
const customOpt = { removeEmptyAttributes: false }
hexo.config.minify.html = customOpt
const result = await h(input, { path })
const expected = await htmlMinify(input, customOpt)
const result = h(input, { path })
const expected = htmlMinify(input, customOpt)
expect(result).toBe(input)
expect(result).toBe(expected)
})
test('option - verbose', async () => {
test('option - verbose', () => {
hexo.config.minify.html.verbose = true
hexo.log.log = jest.fn()
await h(input, { path })
h(input, { path })
expect(hexo.log.log.mock.calls[0][0]).toContain(`html: ${path}`)
})
test('exclude', async () => {
test('exclude', () => {
const exclude = '*.min.html'
hexo.config.minify.html.exclude = exclude
const result = await h(input, { path: 'foo/bar.min.html' })
const result = h(input, { path: 'foo/bar.min.html' })
expect(result).toBe(input)
})
test('exclude - slash in pattern', async () => {
test('exclude - slash in pattern', () => {
const exclude = '**/lectus/**/*.html'
hexo.config.minify.html.exclude = exclude
const result = await h(input, { path: 'eleifend/lectus/nullam/dapibus/netus.html' })
const result = h(input, { path: 'eleifend/lectus/nullam/dapibus/netus.html' })
expect(result).toBe(input)
})
test('exclude - basename is true + slash', async () => {
test('exclude - basename is true + slash', () => {
const exclude = ['**/lectus/**/*.html', 'bar.html']
const globOptions = { basename: true }
hexo.config.minify.html.exclude = exclude
hexo.config.minify.html.globOptions = globOptions
const result = await h(input, { path: 'foo/bar.html' })
const result = h(input, { path: 'foo/bar.html' })
expect(result).toBe(input)
})
test('exclude - basename is false + slash', async () => {
test('exclude - basename is false + slash', () => {
const exclude = ['**/lectus/**/*.html', 'bar.html']
const globOptions = { basename: false }
hexo.config.minify.html.exclude = exclude
hexo.config.minify.html.globOptions = globOptions
const result = await h(input, { path: 'foo/bar.html' })
const result = h(input, { path: 'foo/bar.html' })
expect(result).toBe(expected)
})
test('null', async () => {
test('null', () => {
hexo.config.minify.html.exclude = null
hexo.config.minify.html.globOptions = null
const result = await h(input, { path: null })
const result = h(input, { path: null })
expect(result).toBe(expected)
})
test('invalid string', async () => {
test('invalid string', () => {
const invalid = '<html><>?:"{}|_+</html>'
await expect(h(invalid, { path })).rejects.toThrow('Parse Error: <>?:"{}|_+</html>')
expect(() => {
h(invalid, { path })
}).toThrow(`Path: ${path}\nError: Parse Error`)
})
})

View File

@ -2,20 +2,13 @@
'use strict'
const Hexo = require('hexo')
const { optimize: svgOptimize } = require('svgo')
const { optimize: svgOptimize, extendDefaultPlugins } = require('svgo')
describe('svg', () => {
const hexo = new Hexo(__dirname)
const s = require('../lib/filter').minifySvg.bind(hexo)
const input = '<svg><rect x="1" y="2" width="3" height="4" id="a"/></svg>'
const path = 'foo.svg'
// svgo's plugins option
let plugins = [{
name: 'preset-default',
params: {
overrides: {}
}
}]
beforeEach(() => {
hexo.config.minify = {
@ -23,16 +16,10 @@ describe('svg', () => {
enable: true,
verbose: false,
include: ['*.svg', '!*.min.svg'],
plugins: {},
plugins: extendDefaultPlugins([]),
globOptions: { basename: true }
}
}
plugins = [{
name: 'preset-default',
params: {
overrides: {}
}
}]
hexo.route.set(path, input)
})
@ -43,7 +30,7 @@ describe('svg', () => {
test('default', async () => {
await s()
const { data } = svgOptimize(input, { plugins })
const { data } = svgOptimize(input, hexo.config.minify.svg)
const output = hexo.route.get(path)
let result = ''
@ -70,18 +57,13 @@ describe('svg', () => {
})
test('option', async () => {
const customOpt = {
cleanupIds: false
}
hexo.config.minify.svg.plugins = customOpt
plugins = [{
name: 'preset-default',
params: {
overrides: customOpt
}
const customOpt = [{
name: 'cleanupIDs',
active: false
}]
hexo.config.minify.svg.plugins = customOpt
await s()
const { data } = svgOptimize(input, { plugins })
const { data } = svgOptimize(input, { ...hexo.config.minify.svg, plugins: extendDefaultPlugins(customOpt) })
const output = hexo.route.get(path)
let result = ''
@ -103,7 +85,7 @@ describe('svg', () => {
test('option - plugins - invalid', async () => {
hexo.config.minify.svg.plugins = 'invalid'
await s()
const { data } = svgOptimize(input, { plugins })
const { data } = svgOptimize(input, { ...hexo.config.minify.svg, plugins: extendDefaultPlugins([]) })
const output = hexo.route.get(path)
let result = ''
@ -117,14 +99,10 @@ describe('svg', () => {
const input = '{}'
hexo.route.set(path, input)
let expected
try {
svgOptimize(input, { plugins })
} catch (err) {
expected = err
}
expect(expected).toBeDefined()
await expect(s()).rejects.toThrow(`Path: ${path}\n${expected}`)
const { error } = svgOptimize(input, hexo.config.minify.svg)
expect(error).toBeDefined()
await expect(s()).rejects.toThrow(`Path: ${path}\n${error}`)
})
test('include - exclude *.min.svg by default', async () => {
@ -145,7 +123,7 @@ describe('svg', () => {
const path = 'foo/bar.svg'
hexo.route.set(path, input)
await s()
const { data } = svgOptimize(input, { plugins })
const { data } = svgOptimize(input, hexo.config.minify.svg)
const output = hexo.route.get(path)
let result = ''
@ -160,7 +138,7 @@ describe('svg', () => {
const path = 'eleifend/lectus/nullam/dapibus/netus.svg'
hexo.route.set(path, input)
await s()
const { data } = svgOptimize(input, { plugins })
const { data } = svgOptimize(input, hexo.config.minify.svg)
const output = hexo.route.get(path)
let result = ''
@ -188,7 +166,7 @@ describe('svg', () => {
hexo.route.set(inpath, input)
})
await s()
const { data } = svgOptimize(input, { plugins })
const { data } = svgOptimize(input, hexo.config.minify.svg)
const minPaths = paths.slice(0, 2)
const unminPaths = paths.slice(2)
@ -232,7 +210,7 @@ describe('svg', () => {
hexo.route.set(inpath, input)
})
await s()
const { data } = svgOptimize(input, { plugins })
const { data } = svgOptimize(input, hexo.config.minify.svg)
paths.forEach((inpath) => {
const output = hexo.route.get(inpath)

View File

@ -1,256 +0,0 @@
/* eslint-env jest */
'use strict'
const Hexo = require('hexo')
const { compress: zstd, decompress: unzstd } = require('@mongodb-js/zstd')
describe('zstd', () => {
const hexo = new Hexo(__dirname)
const z = require('../lib/filter').zstdFn.bind(hexo)
const path = 'foo.txt'
const input = 'Lorem ipsum dolor sit amet consectetur adipiscing elit fusce'
const inputBuf = Buffer.from(input, 'utf8')
beforeEach(() => {
hexo.config.minify = {
zstd: {
enable: true,
verbose: false,
include: ['*.html', '*.css', '*.js', '*.txt', '*.ttf', '*.atom', '*.stl', '*.xml', '*.svg', '*.eot', '*.json'],
globOptions: { basename: true }
}
}
hexo.route.set(path, input)
})
afterEach(() => {
const routeList = hexo.route.list()
routeList.forEach((path) => hexo.route.remove(path))
})
test('default', async () => {
await z()
const output = hexo.route.get(path.concat('.zst'))
const buf = []
output.on('data', (chunk) => (buf.push(chunk)))
output.on('end', async () => {
const result = Buffer.concat(buf)
const expected = await zstd(inputBuf)
const resultUnzst = await unzstd(result)
const expectedUnzst = await unzstd(expected)
expect(result.equals(expected)).toBe(true)
expect(resultUnzst.toString()).toBe(input)
expect(expectedUnzst.toString()).toBe(input)
})
})
test('disable', async () => {
hexo.config.minify.zstd.enable = false
const result = await z()
expect(result).toBeUndefined()
})
test('empty file', async () => {
hexo.route.set(path, '')
const routeList = hexo.route.list()
expect(routeList).not.toContain(path.concat('.zst'))
const result = await z()
expect(result).toBeDefined()
expect(result[0]).toBeUndefined()
})
test('option', async () => {
const level = 1
hexo.config.minify.zstd.level = level
await z()
const output = hexo.route.get(path.concat('.zst'))
const buf = []
output.on('data', (chunk) => (buf.push(chunk)))
output.on('end', async () => {
const result = Buffer.concat(buf)
const expected = await zstd(inputBuf, level)
expect(result.equals(expected)).toBe(true)
})
})
test('option - verbose', async () => {
hexo.config.minify.zstd.verbose = true
hexo.log.log = jest.fn()
await z()
expect(hexo.log.log.mock.calls[0][0]).toContain(`zstd: ${path}`)
})
test('option - level is string', async () => {
const level = 'foo'
hexo.config.minify.zstd.level = level
await z()
const output = hexo.route.get(path.concat('.zst'))
const buf = []
output.on('data', (chunk) => (buf.push(chunk)))
output.on('end', async () => {
const result = Buffer.concat(buf)
const expected = await zstd(inputBuf, undefined)
expect(result.equals(expected)).toBe(true)
})
})
test('include - exclude non-text file by default', async () => {
const path = 'foo.jpg'
hexo.route.set(path, input)
await z()
const result = hexo.route.get(path.concat('.zst'))
expect(result).toBeUndefined()
})
test('include - basename', async () => {
hexo.config.minify.zstd.include = 'bar.txt'
const path = 'foo/bar.txt'
hexo.route.set(path, input)
await z()
const result = hexo.route.get(path.concat('.zst'))
expect(result).toBeDefined()
})
test('include - slash in pattern', async () => {
hexo.config.minify.zstd.include = '**/lectus/**/*.txt'
const path = 'eleifend/lectus/nullam/dapibus/netus.txt'
hexo.route.set(path, input)
await z()
const result = hexo.route.get(path.concat('.zst'))
expect(result).toBeDefined()
})
test('include - basename + slash + basename enabled', async () => {
hexo.route.remove(path)
const paths = [
'lorem/ipsum/dolor.html',
'gravida/sociis/erat/ante.css',
'aptent/elementum.js',
'felis/blandit/cursus.svg'
]
hexo.config.minify.zstd.include = [
'*.html',
'**/sociis/**/*.css'
]
paths.forEach((inpath) => {
hexo.route.set(inpath, input)
})
await z()
const routeList = hexo.route.list()
const expected = [
'lorem/ipsum/dolor.html.zst',
'gravida/sociis/erat/ante.css.zst'
]
const notExpected = [
'aptent/elementum.js.zst',
'felis/blandit/cursus.svg.zst'
]
expect(routeList).toEqual(expect.arrayContaining(expected))
expect(routeList).toEqual(expect.not.arrayContaining(notExpected))
})
test('include - basename + slash + basename disabled', async () => {
hexo.route.remove(path)
const paths = [
'lorem/ipsum/dolor.html',
'gravida/sociis/erat/ante.css',
'aptent/elementum.js',
'felis/blandit/cursus.svg'
]
hexo.config.minify.zstd.include = [
'*.html',
'**/sociis/**/*.css'
]
hexo.config.minify.zstd.globOptions = {
basename: false
}
paths.forEach((inpath) => {
hexo.route.set(inpath, input)
})
await z()
const routeList = hexo.route.list()
const expected = [
'gravida/sociis/erat/ante.css.zst'
]
const notExpected = [
'lorem/ipsum/dolor.html.zst',
'aptent/elementum.js.zst',
'felis/blandit/cursus.svg.zst'
]
expect(routeList).toEqual(expect.arrayContaining(expected))
expect(routeList).toEqual(expect.not.arrayContaining(notExpected))
})
test('include - reverse pattern + basename disabled', async () => {
hexo.route.remove(path)
const paths = [
'lorem/ipsum/dolor.html',
'gravida/sociis/erat/ante.css',
'aptent/elementum.js',
'felis/blandit/cursus.svg'
]
hexo.config.minify.zstd.include = [
'!dolor.html'
]
hexo.config.minify.zstd.globOptions = {
basename: false
}
paths.forEach((inpath) => {
hexo.route.set(inpath, input)
})
await z()
const routeList = hexo.route.list()
const expected = paths.map((path) => path.concat('.zst'))
expect(routeList).toEqual(expect.arrayContaining(expected))
})
test('blns', async () => {
const blns = require('./fixtures/blns.json')
for (const nStr of blns) {
hexo.route.remove(path)
hexo.route.set(path, nStr)
await z()
const output = hexo.route.get(path.concat('.zst'))
const buf = []
output.on('data', (chunk) => (buf.push(chunk)))
output.on('end', async () => {
const result = Buffer.concat(buf)
const resultUnzst = await unzstd(result)
expect(resultUnzst.toString()).toBe(nStr)
})
}
})
})