diff --git a/README.md b/README.md index 1ead58f8..9427e4e5 100644 --- a/README.md +++ b/README.md @@ -250,7 +250,7 @@ An array of paths to [CLDR JSON](https://github.com/dojo/i18n#loading-cldr-data) #### `compression`: Array<'gzip' | 'brotli'> -Options for compression when running in `dist` mode. Each array value represents a different algorithm, allowing both gzip and brotli builds to be output side-by-side. +Options for compression when running in `dist` mode. Each array value represents a different algorithm, allowing both gzip and brotli builds to be output side-by-side. When used in conjunction with the `--serve` flag (in `dist` mode _without_ memory watch), the compressed files will be served, with brotli preferred over gzip when available. ### `externals`: object diff --git a/package-lock.json b/package-lock.json index ea50da49..d9d5cd6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -406,9 +406,9 @@ } }, "@dojo/framework": { - "version": "5.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/@dojo/framework/-/framework-5.0.0-alpha.1.tgz", - "integrity": "sha512-uwK1YxvTI2osTXU1jAe9Sz8cQCm/kBH78Bqz76TrCKnqJjUXF9TRvRcdV9cUaGLI6v6C6oQvoLGJXn08+ErR7w==", + "version": "5.0.0-alpha.2", + "resolved": "https://registry.npmjs.org/@dojo/framework/-/framework-5.0.0-alpha.2.tgz", + "integrity": "sha512-1dL8/ObjIVW0ZgnyuXy+JKOpc5eCNjEjvIIGKSCShR8QYNvko7xfeY1CzO/Jh/yUV90pufehT0e8fxX82Gq8WQ==", "requires": { "@types/cldrjs": "0.4.20", "@types/globalize": "0.0.34", @@ -590,7 +590,7 @@ "resolved": "https://registry.npmjs.org/@dojo/webpack-contrib/-/webpack-contrib-5.0.0-alpha.1.tgz", "integrity": "sha512-8dHtvx/vnSnM226vQQMDU8q7FlZnPVfkaBoNYXjf7RSzNiGiuXOfX3Odev36AEZSlRj/q0FSmzQxpQcF+GJB+g==", "requires": { - "@dojo/framework": "^5.0.0-alpha.1", + "@dojo/framework": "^5.0.0-alpha.2", "acorn": "5.3.0", "acorn-dynamic-import": "3.0.0", "bfj-node4": "5.2.0", @@ -4532,9 +4532,9 @@ } }, "expand-template": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-1.1.1.tgz", - "integrity": "sha512-cebqLtV8KOZfw0UI8TEFWxtczxxC1jvyUvx6H4fyp1K1FN7A4Q+uggVUlOsI1K8AGU0rwOGqP8nCapdrw8CYQg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", "optional": true }, "expand-tilde": { @@ -4583,6 +4583,14 @@ "vary": "~1.1.2" } }, + "express-static-gzip": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/express-static-gzip/-/express-static-gzip-1.1.3.tgz", + "integrity": "sha512-k8Q4Dx4PDpzEb8kth4uiPWrBeJWJYSgnWMzNdjQUOsEyXfYKbsyZDkU/uXYKcorRwOie5Vzp4RMEVrJLMfB6rA==", + "requires": { + "serve-static": "^1.12.3" + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -5737,9 +5745,9 @@ }, "dependencies": { "ajv": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz", - "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz", + "integrity": "sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==", "requires": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", @@ -9659,9 +9667,9 @@ } }, "pako": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", - "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.7.tgz", + "integrity": "sha512-3HNK5tW4x8o5mO8RuHZp3Ydw9icZXx0RANAOMzlMzx7LVXhMJ4mo3MOBpzyd7r/+RUu8BmndP47LXT+vzjtWcQ==" }, "parallel-transform": { "version": "1.1.0", @@ -12590,13 +12598,13 @@ } }, "prebuild-install": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.2.1.tgz", - "integrity": "sha512-9DAccsInWHB48TBQi2eJkLPE049JuAI6FjIH0oIrij4bpDVEbX6JvlWRAcAAlUqBHhjgq0jNqA3m3bBXWm9v6w==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.2.2.tgz", + "integrity": "sha512-4e8VJnP3zJdZv/uP0eNWmr2r9urp4NECw7Mt1OSAi3rcLrbBRxGiAkfUFtre2MhQ5wfREAjRV+K1gubvs/GPsA==", "optional": true, "requires": { "detect-libc": "^1.0.3", - "expand-template": "^1.0.2", + "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.0", "mkdirp": "^0.5.1", diff --git a/package.json b/package.json index 3afbef84..41fa16c7 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "css-loader": "0.28.7", "eventsource-polyfill": "0.9.6", "express": "4.16.2", + "express-static-gzip": "1.1.3", "extract-text-webpack-plugin": "3.0.2", "file-loader": "1.1.5", "globby": "7.1.1", diff --git a/src/main.ts b/src/main.ts index 31006f48..5b962547 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,6 +12,7 @@ import * as proxy from 'http-proxy-middleware'; import * as history from 'connect-history-api-fallback'; const pkgDir = require('pkg-dir'); +const expressStaticGzip = require('express-static-gzip'); import devConfigFactory from './dev.config'; import unitConfigFactory from './unit.config'; import functionalConfigFactory from './functional.config'; @@ -170,7 +171,17 @@ function serve(config: webpack.Configuration, args: any): Promise { if (args.watch !== 'memory') { const outputDir = (config.output && config.output.path) || process.cwd(); - app.use(express.static(outputDir)); + if (args.mode === 'dist' && Array.isArray(args.compression)) { + const useBrotli = args.compression.includes('brotli'); + app.use( + expressStaticGzip(outputDir, { + enableBrotli: useBrotli, + orderPreference: useBrotli ? ['br'] : undefined + }) + ); + } else { + app.use(express.static(outputDir)); + } } if (args.proxy) { diff --git a/tests/unit/main.ts b/tests/unit/main.ts index 7920ef86..76d1f7fb 100644 --- a/tests/unit/main.ts +++ b/tests/unit/main.ts @@ -50,6 +50,7 @@ describe('command', () => { './unit.config', 'connect-history-api-fallback', 'express', + 'express-static-gzip', 'http-proxy-middleware', 'https', 'log-update', @@ -540,6 +541,73 @@ describe('command', () => { }); }); + it('serves compressed files in dist mode', () => { + const expressStaticGzip = mockModule.getMock('express-static-gzip').ctor; + const main = mockModule.getModuleUnderTest().default; + const outputDir = '/output/dist'; + const rc = { + mode: 'dist', + compression: ['gzip'], + serve: true + }; + output.path = outputDir; + return main.run(getMockConfiguration(), rc).then(() => { + assert.isTrue( + expressStaticGzip.calledWith(outputDir, { + enableBrotli: false, + orderPreference: undefined + }) + ); + }); + }); + + it('does not serve compressed files in dev mode', () => { + const expressStaticGzip = mockModule.getMock('express-static-gzip').ctor; + const main = mockModule.getModuleUnderTest().default; + const rc = { + mode: 'dev', + compression: ['gzip'], + serve: true + }; + return main.run(getMockConfiguration(), rc).then(() => { + assert.isFalse(expressStaticGzip.called); + }); + }); + + it('does not serve compressed files with memory watch', () => { + const expressStaticGzip = mockModule.getMock('express-static-gzip').ctor; + const main = mockModule.getModuleUnderTest().default; + const rc = { + mode: 'dist', + compression: ['gzip'], + serve: true, + watch: 'memory' + }; + return main.run(getMockConfiguration(), rc).then(() => { + assert.isFalse(expressStaticGzip.called); + }); + }); + + it('favors brotli over gzip', () => { + const expressStaticGzip = mockModule.getMock('express-static-gzip').ctor; + const main = mockModule.getModuleUnderTest().default; + const outputDir = '/output/dist'; + const rc = { + mode: 'dist', + compression: ['gzip', 'brotli'], + serve: true + }; + output.path = outputDir; + return main.run(getMockConfiguration(), rc).then(() => { + assert.isTrue( + expressStaticGzip.calledWith(outputDir, { + enableBrotli: true, + orderPreference: ['br'] + }) + ); + }); + }); + describe('https', () => { it('starts an https server if key and cert are available', () => { const main = mockModule.getModuleUnderTest().default;