From 95cc197a85d25e07948eeb23159b828f73060d84 Mon Sep 17 00:00:00 2001 From: curbengh <43627182+curbengh@users.noreply.github.com> Date: Sat, 5 Oct 2019 04:20:43 +0100 Subject: [PATCH 1/6] feat: support pre-compressed-assets --- .eslintrc | 2 +- README.md | 4 +-- index.js | 4 ++- lib/middlewares/pre_compressed.js | 40 ++++++++++++++++++++++ lib/middlewares/route.js | 7 ++-- test/index.js | 56 ++++++++++++++++++++++++++++++- 6 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 lib/middlewares/pre_compressed.js diff --git a/.eslintrc b/.eslintrc index 91288aa..4ab2c05 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,4 +1,4 @@ { "extends": "hexo", "root": true -} \ No newline at end of file +} diff --git a/README.md b/README.md index eeeec90..7727c74 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,7 @@ server: cache: false header: true serveStatic: - extensions: - - html + preCompressed: false ``` - **port**: Server port @@ -51,6 +50,7 @@ server: - Suitable for production environment only. - **header**: Add `X-Powered-By: Hexo` header - **serveStatic**: Extra options passed to [serve-static](https://github.com/expressjs/serve-static#options) +- **preCompressed**: Serve pre-compressed assets, requires a [minifier plugin](https://hexo.io/plugins/) that supports gzip or brotli ## License diff --git a/index.js b/index.js index 9f366b4..6f383eb 100644 --- a/index.js +++ b/index.js @@ -8,7 +8,8 @@ hexo.config.server = Object.assign({ // `undefined` uses Node's default (try `::` with fallback to `0.0.0.0`) ip: undefined, compress: false, - header: true + header: true, + preCompressed: false }, hexo.config.server); hexo.extend.console.register('server', 'Start the server.', { @@ -23,6 +24,7 @@ hexo.extend.console.register('server', 'Start the server.', { }, require('./lib/server')); hexo.extend.filter.register('server_middleware', require('./lib/middlewares/header')); +hexo.extend.filter.register('server_middleware', require('./lib/middlewares/pre_compressed')); hexo.extend.filter.register('server_middleware', require('./lib/middlewares/gzip')); hexo.extend.filter.register('server_middleware', require('./lib/middlewares/logger')); hexo.extend.filter.register('server_middleware', require('./lib/middlewares/route')); diff --git a/lib/middlewares/pre_compressed.js b/lib/middlewares/pre_compressed.js new file mode 100644 index 0000000..a9dfbd2 --- /dev/null +++ b/lib/middlewares/pre_compressed.js @@ -0,0 +1,40 @@ +'use strict'; + +const { getType } = require('mime'); + +module.exports = function(app) { + const { config, route } = this; + const { root } = config; + + if (!config.server.preCompressed) return; + + app.use(root, (req, res, next) => { + const { headers, method } = req; + const routeList = route.list(); + const url = decodeURIComponent(req.url); + const acceptEncoding = headers['accept-encoding'] || ''; + const reqUrl = url.endsWith('/') ? url.concat('index.html') : url; + const contentType = getType(reqUrl); + const vary = res.getHeader('Vary'); + + if (method !== 'GET' && method !== 'HEAD') return next(); + + res.setHeader('Content-Type', contentType + '; charset=utf-8'); + + if (acceptEncoding.includes('br') && (routeList.includes(url.slice(1) + '.br') || url.endsWith('/'))) { + req.url = encodeURI(reqUrl + '.br'); + res.setHeader('Content-Encoding', 'br'); + } else if (acceptEncoding.includes('gzip') && (routeList.includes(url.slice(1) + '.gz') || url.endsWith('/'))) { + req.url = encodeURI(reqUrl + '.gz'); + res.setHeader('Content-Encoding', 'gzip'); + } + + if (!vary) { + res.setHeader('Vary', 'Accept-Encoding'); + } else if (!vary.includes('Accept-Encoding')) { + res.setHeader('Vary', vary + ', Accept-Encoding'); + } + + return next(); + }); +}; diff --git a/lib/middlewares/route.js b/lib/middlewares/route.js index b3f9d2d..3b0f58d 100644 --- a/lib/middlewares/route.js +++ b/lib/middlewares/route.js @@ -6,7 +6,8 @@ const mime = require('mime'); module.exports = function(app) { const { config, route } = this; const { args = {} } = this.env; - const { root } = config; + const { root, server } = config; + const preCompressed = server.preCompressed ? server.preCompressed : false; if (args.s || args.static) return; @@ -63,7 +64,9 @@ module.exports = function(app) { return; } - res.setHeader('Content-Type', extname ? mime.getType(extname) : 'application/octet-stream'); + if (!preCompressed || (!req.url.endsWith('gz') && !req.url.endsWith('br'))) { + res.setHeader('Content-Type', extname ? mime.getType(extname) : 'application/octet-stream'); + } if (method === 'GET') { data.pipe(res).on('error', next); diff --git a/test/index.js b/test/index.js index 34b99ba..b79778b 100644 --- a/test/index.js +++ b/test/index.js @@ -28,14 +28,22 @@ describe('server', () => { // Register fake generator hexo.extend.generator.register('test', () => [ + {path: '', data: 'index'}, {path: 'index.html', data: 'index'}, {path: 'foo/index.html', data: 'foo'}, + {path: 'foo/index.html.gz', data: 'foo'}, {path: 'bar/baz.html', data: 'baz'}, - {path: 'bar.jpg', data: 'bar'} + {path: 'bar/baz.html.gz', data: 'baz'}, + {path: 'bar.jpg', data: 'bar'}, + {path: 'foo/', data: 'foo'}, + {path: 'foo bar.js', data: 'file'}, + {path: 'foo bar.js.gz', data: ''}, + {path: 'foo bar.js.br', data: ''} ]); // Register middlewares hexo.extend.filter.register('server_middleware', require('../lib/middlewares/header')); + hexo.extend.filter.register('server_middleware', require('../lib/middlewares/pre_compressed')); hexo.extend.filter.register('server_middleware', require('../lib/middlewares/gzip')); hexo.extend.filter.register('server_middleware', require('../lib/middlewares/logger')); hexo.extend.filter.register('server_middleware', require('../lib/middlewares/route')); @@ -102,6 +110,52 @@ describe('server', () => { .expect('Content-Type', 'image/jpeg') .expect(200))); + describe('options.preCompressed', () => { + it('Serve brotli (br) if supported', () => { + hexo.config.server.preCompressed = true; + + return Promise.using( + prepareServer(), + app => request(app) + .get('/foo%20bar.js') + .set('Accept-Encoding', 'gzip, br') + .expect('Content-Encoding', 'br') + .expect('Content-Type', 'application/javascript; charset=utf-8') + ).finally(() => { + hexo.config.server.preCompressed = false; + }); + }); + + it('Serve gzip if br is not supported', () => { + hexo.config.server.preCompressed = true; + + return Promise.using( + prepareServer(), + app => request(app) + .get('/foo%20bar.js') + .set('Accept-Encoding', 'gzip') + .expect('Content-Encoding', 'gzip') + .expect('Content-Type', 'application/javascript; charset=utf-8') + ).finally(() => { + hexo.config.server.preCompressed = false; + }); + }); + + it('Disable', () => { + hexo.config.server.preCompressed = false; + + return Promise.using( + prepareServer(), + app => request(app) + .get('/foo%20bar.js') + .set('Accept-Encoding', 'gzip, br') + .then(res => { + res.headers.should.not.have.property('Content-Encoding'); + }) + ); + }); + }); + it('Enable compression if options.compress is true', () => { hexo.config.server.compress = true; From 8b313db9f29179e42e320dbe4ce17cdaecaa1102 Mon Sep 17 00:00:00 2001 From: curbengh <43627182+curbengh@users.noreply.github.com> Date: Sat, 28 Dec 2019 00:37:40 +0000 Subject: [PATCH 2/6] test(preCompressed): index.html handling --- test/index.js | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/test/index.js b/test/index.js index b79778b..ef2b6bc 100644 --- a/test/index.js +++ b/test/index.js @@ -38,7 +38,9 @@ describe('server', () => { {path: 'foo/', data: 'foo'}, {path: 'foo bar.js', data: 'file'}, {path: 'foo bar.js.gz', data: ''}, - {path: 'foo bar.js.br', data: ''} + {path: 'foo bar.js.br', data: ''}, + {path: 'foo/index.html.br', data: ''}, + {path: 'foo/index.html.gz', data: ''} ]); // Register middlewares @@ -154,6 +156,34 @@ describe('server', () => { }) ); }); + + it('route / to /index.html (br)', () => { + hexo.config.server.preCompressed = true; + + return Promise.using( + prepareServer(), + app => request(app) + .get('/foo/') + .set('Accept-Encoding', 'gzip, br') + .expect('Content-Encoding', 'br') + ).finally(() => { + hexo.config.server.preCompressed = false; + }); + }); + + it('route / to /index.html (br)', () => { + hexo.config.server.preCompressed = true; + + return Promise.using( + prepareServer(), + app => request(app) + .get('/foo/') + .set('Accept-Encoding', 'gzip') + .expect('Content-Encoding', 'gzip') + ).finally(() => { + hexo.config.server.preCompressed = false; + }); + }); }); it('Enable compression if options.compress is true', () => { From 2caee166ab22131a53f0d27a432ab5cfc069c92a Mon Sep 17 00:00:00 2001 From: curbengh <43627182+curbengh@users.noreply.github.com> Date: Sat, 28 Dec 2019 00:43:49 +0000 Subject: [PATCH 3/6] refactor(test-preCompressed): async/await --- test/index.js | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/test/index.js b/test/index.js index ef2b6bc..1c5ae31 100644 --- a/test/index.js +++ b/test/index.js @@ -113,22 +113,22 @@ describe('server', () => { .expect(200))); describe('options.preCompressed', () => { - it('Serve brotli (br) if supported', () => { + beforeEach(() => { hexo.config.server.preCompressed = false; }); + + it('Serve brotli (br) if supported', async () => { hexo.config.server.preCompressed = true; - return Promise.using( + await Promise.using( prepareServer(), app => request(app) .get('/foo%20bar.js') .set('Accept-Encoding', 'gzip, br') .expect('Content-Encoding', 'br') .expect('Content-Type', 'application/javascript; charset=utf-8') - ).finally(() => { - hexo.config.server.preCompressed = false; - }); + ); }); - it('Serve gzip if br is not supported', () => { + it('Serve gzip if br is not supported', async () => { hexo.config.server.preCompressed = true; return Promise.using( @@ -138,15 +138,13 @@ describe('server', () => { .set('Accept-Encoding', 'gzip') .expect('Content-Encoding', 'gzip') .expect('Content-Type', 'application/javascript; charset=utf-8') - ).finally(() => { - hexo.config.server.preCompressed = false; - }); + ); }); - it('Disable', () => { + it('Disable', async () => { hexo.config.server.preCompressed = false; - return Promise.using( + await Promise.using( prepareServer(), app => request(app) .get('/foo%20bar.js') @@ -157,32 +155,28 @@ describe('server', () => { ); }); - it('route / to /index.html (br)', () => { + it('route / to /index.html (br)', async () => { hexo.config.server.preCompressed = true; - return Promise.using( + await Promise.using( prepareServer(), app => request(app) .get('/foo/') .set('Accept-Encoding', 'gzip, br') .expect('Content-Encoding', 'br') - ).finally(() => { - hexo.config.server.preCompressed = false; - }); + ); }); - it('route / to /index.html (br)', () => { + it('route / to /index.html (gzip)', async () => { hexo.config.server.preCompressed = true; - return Promise.using( + await Promise.using( prepareServer(), app => request(app) .get('/foo/') .set('Accept-Encoding', 'gzip') .expect('Content-Encoding', 'gzip') - ).finally(() => { - hexo.config.server.preCompressed = false; - }); + ); }); }); From a285dc4af501213815f506e3e94115c661b1f50e Mon Sep 17 00:00:00 2001 From: curbengh <43627182+curbengh@users.noreply.github.com> Date: Sat, 28 Dec 2019 04:07:37 +0000 Subject: [PATCH 4/6] refactor: utilize route.get() --- lib/middlewares/pre_compressed.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/middlewares/pre_compressed.js b/lib/middlewares/pre_compressed.js index a9dfbd2..782ed64 100644 --- a/lib/middlewares/pre_compressed.js +++ b/lib/middlewares/pre_compressed.js @@ -10,7 +10,6 @@ module.exports = function(app) { app.use(root, (req, res, next) => { const { headers, method } = req; - const routeList = route.list(); const url = decodeURIComponent(req.url); const acceptEncoding = headers['accept-encoding'] || ''; const reqUrl = url.endsWith('/') ? url.concat('index.html') : url; @@ -21,10 +20,10 @@ module.exports = function(app) { res.setHeader('Content-Type', contentType + '; charset=utf-8'); - if (acceptEncoding.includes('br') && (routeList.includes(url.slice(1) + '.br') || url.endsWith('/'))) { + if (acceptEncoding.includes('br') && (route.get(url.slice(1) + '.br') || url.endsWith('/'))) { req.url = encodeURI(reqUrl + '.br'); res.setHeader('Content-Encoding', 'br'); - } else if (acceptEncoding.includes('gzip') && (routeList.includes(url.slice(1) + '.gz') || url.endsWith('/'))) { + } else if (acceptEncoding.includes('gzip') && (route.get(url.slice(1) + '.gz') || url.endsWith('/'))) { req.url = encodeURI(reqUrl + '.gz'); res.setHeader('Content-Encoding', 'gzip'); } From 8db24bfa52322539ee7c9dd6acaa6f21bb43ec4a Mon Sep 17 00:00:00 2001 From: MDLeom <43627182+curbengh@users.noreply.github.com> Date: Wed, 29 Jul 2020 08:36:47 +0100 Subject: [PATCH 5/6] chore(deps-dev): bump eslint-config-hexo from 4.0.0 to 4.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 98e00ff..d7fd188 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "chai": "^4.2.0", "chai-as-promised": "^7.1.1", "eslint": "^7.0.0", - "eslint-config-hexo": "^4.0.0", + "eslint-config-hexo": "^4.1.0", "hexo": "^5.0.0", "hexo-fs": "^3.0.1", "hexo-util": "^2.1.0", From 40fe06df25e5d914292e9dcd2743721d6dbf7948 Mon Sep 17 00:00:00 2001 From: MDLeom <43627182+curbengh@users.noreply.github.com> Date: Wed, 29 Jul 2020 11:26:09 +0100 Subject: [PATCH 6/6] fix(pre_compressed): rename preCompressed to pre_compressed - fix compatibility with trailing .html --- README.md | 4 +-- index.js | 2 +- lib/middlewares/pre_compressed.js | 47 ++++++++++++++++++++++--------- lib/middlewares/route.js | 4 +-- test/index.js | 18 ++++++------ 5 files changed, 48 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 7727c74..8603511 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ server: cache: false header: true serveStatic: - preCompressed: false + pre_compressed: false ``` - **port**: Server port @@ -50,7 +50,7 @@ server: - Suitable for production environment only. - **header**: Add `X-Powered-By: Hexo` header - **serveStatic**: Extra options passed to [serve-static](https://github.com/expressjs/serve-static#options) -- **preCompressed**: Serve pre-compressed assets, requires a [minifier plugin](https://hexo.io/plugins/) that supports gzip or brotli +- **pre_compressed**: Serve pre-compressed assets, requires a [minifier plugin](https://hexo.io/plugins/) that supports gzip or brotli ## License diff --git a/index.js b/index.js index 6f383eb..abf8d4a 100644 --- a/index.js +++ b/index.js @@ -9,7 +9,7 @@ hexo.config.server = Object.assign({ ip: undefined, compress: false, header: true, - preCompressed: false + pre_compressed: false }, hexo.config.server); hexo.extend.console.register('server', 'Start the server.', { diff --git a/lib/middlewares/pre_compressed.js b/lib/middlewares/pre_compressed.js index 782ed64..3a3ea34 100644 --- a/lib/middlewares/pre_compressed.js +++ b/lib/middlewares/pre_compressed.js @@ -5,27 +5,48 @@ const { getType } = require('mime'); module.exports = function(app) { const { config, route } = this; const { root } = config; + const { pretty_urls } = config; + const { trailing_index, trailing_html } = pretty_urls ? pretty_urls : {}; - if (!config.server.preCompressed) return; + if (!config.server.pre_compressed) return; app.use(root, (req, res, next) => { - const { headers, method } = req; - const url = decodeURIComponent(req.url); + const { headers, method, url: requestUrl } = req; const acceptEncoding = headers['accept-encoding'] || ''; - const reqUrl = url.endsWith('/') ? url.concat('index.html') : url; - const contentType = getType(reqUrl); const vary = res.getHeader('Vary'); + const url = route.format(decodeURIComponent(requestUrl)); + const data = route.get(url); if (method !== 'GET' && method !== 'HEAD') return next(); - res.setHeader('Content-Type', contentType + '; charset=utf-8'); - - if (acceptEncoding.includes('br') && (route.get(url.slice(1) + '.br') || url.endsWith('/'))) { - req.url = encodeURI(reqUrl + '.br'); - res.setHeader('Content-Encoding', 'br'); - } else if (acceptEncoding.includes('gzip') && (route.get(url.slice(1) + '.gz') || url.endsWith('/'))) { - req.url = encodeURI(reqUrl + '.gz'); - res.setHeader('Content-Encoding', 'gzip'); + const preFn = (acceptEncoding, url, req, res) => { + res.setHeader('Content-Type', getType(url) + '; charset=utf-8'); + + if (acceptEncoding.includes('br') && route.get(url + '.br')) { + req.url = encodeURI('/' + url + '.br'); + res.setHeader('Content-Encoding', 'br'); + } else if (acceptEncoding.includes('gzip') && route.get(url + '.gz')) { + req.url = encodeURI('/' + url + '.gz'); + res.setHeader('Content-Encoding', 'gzip'); + } + }; + + if (data) { + if ((trailing_html === false && !requestUrl.endsWith('/index.html') && requestUrl.endsWith('.html')) || (trailing_index === false && requestUrl.endsWith('/index.html'))) { + // location `foo/bar.html`; request `foo/bar.html`; redirect to `foo/bar` + // location `foo/index.html`; request `foo/index.html`; redirect to `foo/` + return next(); + } + // location `foo/bar/index.html`; request `foo/bar/` or `foo/bar/index.html; proxy to the location + // also applies to non-html + preFn(acceptEncoding, url, req, res); + } else { + if (route.get(url + '.html')) { + // location `foo/bar.html`; request `foo/bar`; proxy to the `foo/bar.html.br` + preFn(acceptEncoding, url + '.html', req, res); + } else { + return next(); + } } if (!vary) { diff --git a/lib/middlewares/route.js b/lib/middlewares/route.js index 3b0f58d..28a823d 100644 --- a/lib/middlewares/route.js +++ b/lib/middlewares/route.js @@ -7,7 +7,7 @@ module.exports = function(app) { const { config, route } = this; const { args = {} } = this.env; const { root, server } = config; - const preCompressed = server.preCompressed ? server.preCompressed : false; + const preCompressed = server.pre_compressed ? server.pre_compressed : false; if (args.s || args.static) return; @@ -64,7 +64,7 @@ module.exports = function(app) { return; } - if (!preCompressed || (!req.url.endsWith('gz') && !req.url.endsWith('br'))) { + if (!preCompressed || (!req.url.endsWith('.br') && !req.url.endsWith('.gz'))) { res.setHeader('Content-Type', extname ? mime.getType(extname) : 'application/octet-stream'); } diff --git a/test/index.js b/test/index.js index 1c5ae31..0c6be24 100644 --- a/test/index.js +++ b/test/index.js @@ -112,11 +112,11 @@ describe('server', () => { .expect('Content-Type', 'image/jpeg') .expect(200))); - describe('options.preCompressed', () => { - beforeEach(() => { hexo.config.server.preCompressed = false; }); + describe('options.pre_compressed', () => { + beforeEach(() => { hexo.config.server.pre_compressed = false; }); it('Serve brotli (br) if supported', async () => { - hexo.config.server.preCompressed = true; + hexo.config.server.pre_compressed = true; await Promise.using( prepareServer(), @@ -129,7 +129,7 @@ describe('server', () => { }); it('Serve gzip if br is not supported', async () => { - hexo.config.server.preCompressed = true; + hexo.config.server.pre_compressed = true; return Promise.using( prepareServer(), @@ -142,7 +142,7 @@ describe('server', () => { }); it('Disable', async () => { - hexo.config.server.preCompressed = false; + hexo.config.server.pre_compressed = false; await Promise.using( prepareServer(), @@ -155,8 +155,8 @@ describe('server', () => { ); }); - it('route / to /index.html (br)', async () => { - hexo.config.server.preCompressed = true; + it('route / to /index.html.br', async () => { + hexo.config.server.pre_compressed = true; await Promise.using( prepareServer(), @@ -167,8 +167,8 @@ describe('server', () => { ); }); - it('route / to /index.html (gzip)', async () => { - hexo.config.server.preCompressed = true; + it('route / to /index.html.gz', async () => { + hexo.config.server.pre_compressed = true; await Promise.using( prepareServer(),