Compare commits

..

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

18 changed files with 254 additions and 581 deletions

View File

@ -6,17 +6,19 @@ jobs:
linter: linter:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v2.3.4
- name: Install Bun - name: Use Node.js 14.x
uses: oven-sh/setup-bun@v2 uses: actions/setup-node@v2.4.1
with:
node-version: '14.x'
- name: Cache NPM dependencies - name: Cache NPM dependencies
uses: actions/cache@v4 uses: actions/cache@v2.1.6
with: with:
path: node_modules path: node_modules
key: ${{ runner.os }}-npm-cache key: ${{ runner.os }}-npm-cache
restore-keys: | restore-keys: |
${{ runner.os }}-npm-cache ${{ runner.os }}-npm-cache
- name: Install Dependencies - name: Install Dependencies
run: bun install run: npm install
- name: Lint - 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: on:
schedule: schedule:
# Weekly # Every day
- cron: "0 0 * * 0" - cron: '0 0 * * *'
pull_request:
branches:
- "dependabot/github_actions/github/codeql-action**"
jobs: jobs:
security: security:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v2.3.4
- name: Install Bun - name: Use Node.js 14.x
uses: oven-sh/setup-bun@v2 uses: actions/setup-node@v2.4.1
with:
node-version: '14.x'
- name: Install Dependencies - name: Install Dependencies
run: bun install run: npm install
- name: Run Snyk to check for vulnerabilities - 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 uses: snyk/actions/node@master
env: env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

View File

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

1
.gitignore vendored
View File

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

1
.npmrc
View File

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

6
.snyk
View File

@ -2,4 +2,8 @@
version: v1.12.0 version: v1.12.0
# ignores vulnerabilities until expiry date; change duration by modifying expiry date # ignores vulnerabilities until expiry date; change duration by modifying expiry date
ignore: 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) 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

265
README.md
View File

@ -1,40 +1,68 @@
# hexo-yam # hexo-yam
[![npm version](https://img.shields.io/npm/v/hexo-yam?logo=npm)](https://www.npmjs.com/package/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) [![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) [![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 ## Table of contents
- [Version 5](#version-5)
- [Installation](#installation) - [Installation](#installation)
- [Options](#options) - [Options](#options)
- [HTML](#html) - [HTML](#html)
- [CSS](#css) - [CSS](#css)
- [JS](#js) - [JS](#js)
- [SVG](#svg) - [SVG](#svg)
- [XML](#xml)
- [JSON](#json)
- [Gzip](#gzip) - [Gzip](#gzip)
- [Brotli](#brotli) - [Brotli](#brotli)
- [Zstd](#zstd) - [XML](#xml)
- [JSON](#json)
- [Globbing](#globbing) - [Globbing](#globbing)
- [HTTP Compression](#http-compression)
## Version 6
In v6, `svg.plugins:` option should be the following syntax:
``` diff
minify:
svg:
plugins:
# v6
+ removeComments: false
+ cleanupIDs: false
+ builtinPluginName:
+ optionName: 'optionValue'
# v5
- - name: 'removeComments'
- active: false
- - name: 'cleanupIDs'
- active: false
# v4
- - removeComments: false
- - cleanupIDs: false
```
The option only overrides svgo's default plugins, other options are not supported.
## Installation ## Installation
``` bash
```bash $ npm install hexo-yam --save
$ npm install --save hexo-yam
``` ```
## Options ## Options
```yaml ``` yaml
minify: minify:
enable: true enable: true
previewServer: true
html: html:
css: css:
js: js:
@ -46,7 +74,6 @@ minify:
``` ```
- **enable** - Enable the plugin. Defaults to `true`. - **enable** - Enable the plugin. Defaults to `true`.
- **previewServer** - Disable the plugin when running `hexo server`. Defaults to `true`.
- **html** - See [HTML](#html) section - **html** - See [HTML](#html) section
- **css** - See [CSS](#css) section - **css** - See [CSS](#css) section
- **js** - See [JS](#js) section - **js** - See [JS](#js) section
@ -58,13 +85,12 @@ minify:
## HTML ## HTML
```yaml ``` yaml
minify: minify:
html: html:
enable: true enable: true
exclude: exclude:
``` ```
- **enable** - Enable the plugin. Defaults to `true`. - **enable** - Enable the plugin. Defaults to `true`.
- **priority** - Plugin's priority. Defaults to `10`. Set lower value to set higher priority and vice versa. - **priority** - Plugin's priority. Defaults to `10`. Set lower value to set higher priority and vice versa.
- **verbose** - Verbose output. Defaults to `false`. - **verbose** - Verbose output. Defaults to `false`.
@ -75,14 +101,13 @@ For more options, see [HTMLMinifier](https://github.com/kangax/html-minifier).
## CSS ## CSS
```yaml ``` yaml
minify: minify:
css: css:
enable: true enable: true
exclude: exclude:
- "*.min.css" - '*.min.css'
``` ```
- **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`.
@ -94,14 +119,13 @@ For more options, see [clean-css](https://github.com/jakubpawlowicz/clean-css).
## JS ## JS
```yaml ``` yaml
minify: minify:
js: js:
enable: true enable: true
exclude: exclude:
- "*.min.js" - '*.min.js'
``` ```
- **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`.
@ -116,15 +140,14 @@ For more options, see [Terser](https://github.com/terser-js/terser).
## SVG ## SVG
```yaml ``` yaml
minify: minify:
svg: svg:
enable: true enable: true
include: include:
- "*.svg" - '*.svg'
- "!*.min.svg" - '!*.min.svg'
``` ```
- **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`.
@ -132,29 +155,82 @@ minify:
- Exclude `*.min.svg` by default. - Exclude `*.min.svg` by default.
- **plugins** - Plugin options. - **plugins** - Plugin options.
- Examples: - Examples:
```yaml ``` yaml
plugins: plugins:
# Retain comments # Retain comments
removeComments: false removeComments: false
# Do not remove unused ID attributes # Do not remove unused ID attributes
cleanupIds: false cleanupIDs: false
``` ```
- For more options, see [svgo](https://github.com/svg/svgo). - For more options, see [svgo](https://github.com/svg/svgo).
- **globOptions** - See [globbing](#globbing) section. - **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 ## XML
Remove whitespaces in xml. Remove whitespaces in xml.
```yaml ``` yaml
minify: minify:
xml: xml:
enable: false enable: false
include: include:
- "*.xml" - '*.xml'
- "!*.min.xml" - '!*.min.xml'
``` ```
- **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`.
@ -169,15 +245,14 @@ For more options, see [minify-xml](https://github.com/kristian/minify-xml#option
Remove whitespaces in json. Remove whitespaces in json.
```yaml ``` yaml
minify: minify:
json: json:
enable: false enable: false
include: include:
- "*.json" - '*.json'
- "!*.min.json" - '!*.min.json'
``` ```
- **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`.
@ -185,89 +260,6 @@ minify:
- Exclude `*.min.json` by default. - Exclude `*.min.json` by default.
- **globOptions** - See [globbing](#globbing) section. - **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 ## Globbing
Use "globOptions" to customise how glob patterns match files. Refer to [micromatch](https://github.com/micromatch/micromatch#options) for available options. Use "globOptions" to customise how glob patterns match files. Refer to [micromatch](https://github.com/micromatch/micromatch#options) for available options.
@ -276,32 +268,51 @@ Use "globOptions" to customise how glob patterns match files. Refer to [micromat
- basename is disabled depending on each pattern. - basename is disabled depending on each pattern.
- This means the following options would work, - This means the following options would work,
```yml ``` yml
exclude: exclude:
- "*foo.html" # basename is enabled - '*foo.html' # basename is enabled
- "**/bar/*/*.html" # basename is automatically disabled - '**/bar/*/*.html' # basename is automatically disabled
- "*baz.css" # basename is enabled - '*baz.css' # basename is enabled
globOptions: globOptions:
basename: true # default basename: true # default
``` ```
- This behaviour doesn't apply to pattern that starts with `!` (negation). - 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 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: include:
- "!foo.svg" - '!foo.svg'
- "!**/bar/*/*.svg" - '!**/bar/*/*.svg'
globOptions: globOptions:
basename: true basename: true
``` ```
- basename will stay disabled, if explicitly disabled in "globOptions". - 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 ## Credits
All credits go to the following work: All credits go to the following work:
- [hexo-neat](https://github.com/rozbo/hexo-neat) by rozbo - [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) - 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

@ -2,8 +2,7 @@
'use strict' 'use strict'
hexo.config.minify = Object.assign({ hexo.config.minify = Object.assign({
enable: true, enable: true
previewServer: true
}, hexo.config.minify) }, hexo.config.minify)
hexo.config.minify.html = Object.assign({ hexo.config.minify.html = Object.assign({
@ -69,14 +68,6 @@ hexo.config.minify.brotli = Object.assign({
globOptions: { basename: true } globOptions: { basename: true }
}, hexo.config.minify.brotli) }, 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({ hexo.config.minify.xml = Object.assign({
enable: false, enable: false,
priority: 10, priority: 10,
@ -94,7 +85,7 @@ hexo.config.minify.json = Object.assign({
globOptions: { basename: true } globOptions: { basename: true }
}, hexo.config.minify.json) }, 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') 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: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:css', filter.minifyCss, hexo.config.minify.css.priority)
@ -102,7 +93,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.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.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.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.minifyXml, hexo.config.minify.xml.priority)
hexo.extend.filter.register('after_generate', filter.minifyJson, hexo.config.minify.json.priority) hexo.extend.filter.register('after_generate', filter.minifyJson, hexo.config.minify.json.priority)
} }

View File

@ -1,16 +1,15 @@
'use strict' 'use strict'
const { minify: htmlMinify } = require('html-minifier-terser') const { minify: htmlMinify } = require('html-minifier')
const CleanCSS = require('clean-css') const CleanCSS = require('clean-css')
const { minify: terserMinify } = require('terser') const { minify: terserMinify } = require('terser')
const { optimize: svgOptimize } = require('svgo') const { optimize: svgOptimize } = require('svgo')
const zlib = require('node:zlib') const zlib = require('zlib')
const { promisify } = require('node:util') const { promisify } = require('util')
const gzip = promisify(zlib.gzip) const gzip = promisify(zlib.gzip)
const br = promisify(zlib.brotliCompress) const br = promisify(zlib.brotliCompress)
const { minify: compressXml } = require('minify-xml') const { minify: compressXml } = require('minify-xml')
const micromatch = require('micromatch') const micromatch = require('micromatch')
const { compress: zstd } = require('@mongodb-js/zstd')
const isMatch = (path = '', patterns = [], options = {}) => { const isMatch = (path = '', patterns = [], options = {}) => {
if (path && patterns) { if (path && patterns) {
@ -60,7 +59,7 @@ function logFn (original, minified, path, ext) {
log.log(`${ext}: ${path} [${saved}% saved]`) log.log(`${ext}: ${path} [${saved}% saved]`)
} }
async function minifyHtml (str, data) { function minifyHtml (str, data) {
const hexo = this const hexo = this
const options = hexo.config.minify.html const options = hexo.config.minify.html
if (options.enable === false || !str) return if (options.enable === false || !str) return
@ -72,7 +71,7 @@ async function minifyHtml (str, data) {
if (isMatch(path, exclude, globOptions)) return str if (isMatch(path, exclude, globOptions)) return str
try { try {
const result = await htmlMinify(str, options) const result = htmlMinify(str, options)
if (verbose) logFn.call(this, str, result, path, 'html') if (verbose) logFn.call(this, str, result, path, 'html')
return result return result
@ -153,12 +152,12 @@ function minifySvg () {
assetPath.on('data', (chunk) => (assetTxt += chunk)) assetPath.on('data', (chunk) => (assetTxt += chunk))
assetPath.on('end', async () => { assetPath.on('end', async () => {
if (assetTxt.length) { if (assetTxt.length) {
try { const { data, error } = svgOptimize(assetTxt, { ...options, plugins })
const { data } = svgOptimize(assetTxt, { ...options, plugins }) if (data) {
if (verbose) logFn.call(this, assetTxt, data, path, 'svg') if (verbose) logFn.call(this, assetTxt, data, path, 'svg')
resolve(route.set(path, data)) resolve(route.set(path, data))
} catch (err) { } else if (error) {
reject(new Error(`Path: ${path}\n${err}`)) reject(new Error(`Path: ${path}\n${error}`))
} }
} }
resolve() resolve()
@ -231,39 +230,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 () { function minifyXml () {
const hexo = this const hexo = this
const options = hexo.config.minify.xml const options = hexo.config.minify.xml
@ -331,7 +297,6 @@ module.exports = {
minifySvg, minifySvg,
gzipFn, gzipFn,
brotliFn, brotliFn,
zstdFn,
minifyXml, minifyXml,
minifyJson minifyJson
} }

View File

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

View File

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

View File

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

View File

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

@ -71,7 +71,7 @@ describe('svg', () => {
test('option', async () => { test('option', async () => {
const customOpt = { const customOpt = {
cleanupIds: false cleanupIDs: false
} }
hexo.config.minify.svg.plugins = customOpt hexo.config.minify.svg.plugins = customOpt
plugins = [{ plugins = [{
@ -117,14 +117,10 @@ describe('svg', () => {
const input = '{}' const input = '{}'
hexo.route.set(path, input) hexo.route.set(path, input)
let expected const { error } = svgOptimize(input, { plugins })
try {
svgOptimize(input, { plugins }) expect(error).toBeDefined()
} catch (err) { await expect(s()).rejects.toThrow(`Path: ${path}\n${error}`)
expected = err
}
expect(expected).toBeDefined()
await expect(s()).rejects.toThrow(`Path: ${path}\n${expected}`)
}) })
test('include - exclude *.min.svg by default', async () => { test('include - exclude *.min.svg by default', async () => {

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)
})
}
})
})