diff --git a/README.md b/README.md index 4d4a2255..6ae3034a 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ module.exports = { |**`include`**|`{RegExp\|Array}`|`undefined`|Files to `include`| |**`exclude`**|`{RegExp\|Array}`|`undefined`|Files to `exclude`| |**`cache`**|`{Boolean\|String}`|`false`|Enable file caching| +|**`cacheKeys`**|`{Function(defaultCacheKeys, file) -> {Object}}`|`defaultCacheKeys => defaultCacheKeys`|Allows you to override default cache keys| |**`parallel`**|`{Boolean\|Number}`|`false`|Use multi-process parallel running to improve the build speed| |**`sourceMap`**|`{Boolean}`|`false`|Use source maps to map error message locations to modules (This slows down the compilation) ⚠️ **`cheap-source-map` options don't work with this plugin**| |**`uglifyOptions`**|`{Object}`|[`{...defaults}`](https://github.com/webpack-contrib/uglifyjs-webpack-plugin/tree/master#uglifyoptions)|`uglify` [Options](https://github.com/mishoo/UglifyJS2/tree/harmony#minify-options)| @@ -112,6 +113,35 @@ Default path to cache directory: `node_modules/.cache/uglifyjs-webpack-plugin`. Path to cache directory. +### `cacheKeys` + +**webpack.config.js** +```js +[ + new UglifyJsPlugin({ + cache: true, + cacheKeys: (defaultCacheKeys, file, options) => { + defaultCacheKeys.myCacheKey = 'myCacheKeyValue'; + + return defaultCacheKeys; + }, + }) +] +``` + +Allows you to override default cache keys. + +Default keys: +```js +{ + 'uglify-es': versions.uglify, // uglify version + 'uglifyjs-webpack-plugin': versions.plugin, // plugin version + 'uglifyjs-webpack-plugin-options': this.options, // plugin options + path: compiler.outputPath ? `${compiler.outputPath}/${file}` : file, // asset path + hash: crypto.createHash('md4').update(input).digest('hex'), // source file hash +} +``` + ### `parallel` #### `{Boolean}` diff --git a/src/index.js b/src/index.js index 19dbdb3a..026dc236 100644 --- a/src/index.js +++ b/src/index.js @@ -8,7 +8,6 @@ import { SourceMapSource, RawSource, ConcatSource } from 'webpack-sources'; import RequestShortener from 'webpack/lib/RequestShortener'; import ModuleFilenameHelpers from 'webpack/lib/ModuleFilenameHelpers'; import validateOptions from 'schema-utils'; -import serialize from 'serialize-javascript'; import schema from './options.json'; import Uglify from './uglify'; import versions from './uglify/versions'; @@ -27,6 +26,7 @@ class UglifyJsPlugin { extractComments = false, sourceMap = false, cache = false, + cacheKeys = defaultCacheKeys => defaultCacheKeys, parallel = false, include, exclude, @@ -38,6 +38,7 @@ class UglifyJsPlugin { extractComments, sourceMap, cache, + cacheKeys, parallel, include, exclude, @@ -171,13 +172,15 @@ class UglifyJsPlugin { }; if (this.options.cache) { - task.cacheKey = serialize({ + const defaultCacheKeys = { 'uglify-es': versions.uglify, 'uglifyjs-webpack-plugin': versions.plugin, 'uglifyjs-webpack-plugin-options': this.options, path: compiler.outputPath ? `${compiler.outputPath}/${file}` : file, hash: crypto.createHash('md4').update(input).digest('hex'), - }); + }; + + task.cacheKeys = this.options.cacheKeys(defaultCacheKeys, file); } tasks.push(task); diff --git a/src/options.json b/src/options.json index e73aa639..cb6d17fb 100644 --- a/src/options.json +++ b/src/options.json @@ -10,6 +10,9 @@ { "type": "string" } ] }, + "cacheKeys": { + "instanceof": "Function" + }, "parallel": { "oneOf": [ { "type": "boolean" }, diff --git a/src/uglify/index.js b/src/uglify/index.js index 75b0e218..ca99f210 100644 --- a/src/uglify/index.js +++ b/src/uglify/index.js @@ -57,7 +57,7 @@ export default class { const done = () => step(index, result); if (this.cacheDir && !result.error) { - cacache.put(this.cacheDir, task.cacheKey, JSON.stringify(data)).then(done, done); + cacache.put(this.cacheDir, serialize(task.cacheKeys), JSON.stringify(data)).then(done, done); } else { done(); } @@ -65,7 +65,7 @@ export default class { }; if (this.cacheDir) { - cacache.get(this.cacheDir, task.cacheKey).then(({ data }) => step(index, JSON.parse(data)), enqueue); + cacache.get(this.cacheDir, serialize(task.cacheKeys)).then(({ data }) => step(index, JSON.parse(data)), enqueue); } else { enqueue(); } diff --git a/test/__snapshots__/cache-options.test.js.snap b/test/__snapshots__/cache-options.test.js.snap index cf265a1a..c31f5d6b 100644 --- a/test/__snapshots__/cache-options.test.js.snap +++ b/test/__snapshots__/cache-options.test.js.snap @@ -79,3 +79,39 @@ exports[`when options.cache true matches snapshot: main.0c220ec66316af2c1b24.js exports[`when options.cache true matches snapshot: manifest.d6857f782c13a99b5917.js 1`] = `"!function(r){var n=window.webpackJsonp;window.webpackJsonp=function(e,u,c){for(var f,i,p,a=0,l=[];a { }); }); }); + + describe('with cacheKey option', () => { + let eventBindings; + let eventBinding; + + beforeAll(() => cacache.rm.all(cacheDir)); + + afterAll(() => cacache.rm.all(cacheDir)); + + beforeEach(() => { + const pluginEnvironment = new PluginEnvironment(); + const compilerEnv = pluginEnvironment.getEnvironmentStub(); + compilerEnv.context = ''; + + const plugin = new UglifyJsPlugin({ + cache: true, + cacheKeys: (defaultCacheKeys, file) => { + // eslint-disable-next-line no-param-reassign + defaultCacheKeys.myCacheKey = 1; + // eslint-disable-next-line no-param-reassign + defaultCacheKeys.myCacheKeyBasedOnFile = `file-${file}`; + + return defaultCacheKeys; + }, + }); + plugin.apply(compilerEnv); + eventBindings = pluginEnvironment.getEventBindings(); + }); + + it('binds one event handler', () => { + expect(eventBindings.length).toBe(1); + }); + + describe('compilation handler', () => { + beforeEach(() => { + [eventBinding] = eventBindings; + }); + + it('binds to compilation event', () => { + expect(eventBinding.name).toBe('compilation'); + }); + + describe('when called', () => { + let chunkPluginEnvironment; + let compilationEventBindings; + let compilationEventBinding; + let compilation; + let callback; + + beforeEach(() => { + chunkPluginEnvironment = new PluginEnvironment(); + compilation = chunkPluginEnvironment.getEnvironmentStub(); + compilation.assets = Object.assign({}, assets); + compilation.errors = []; + + eventBinding.handler(compilation); + compilationEventBindings = chunkPluginEnvironment.getEventBindings(); + }); + + it('binds one event handler', () => { + expect(compilationEventBindings.length).toBe(1); + }); + + describe('optimize-chunk-assets handler', () => { + beforeEach(() => { + [compilationEventBinding] = compilationEventBindings; + }); + + it('binds to optimize-chunk-assets event', () => { + expect(compilationEventBinding.name).toEqual('optimize-chunk-assets'); + }); + + it('only calls callback once', (done) => { + callback = jest.fn(); + compilationEventBinding.handler([''], () => { + callback(); + expect(callback.mock.calls.length).toBe(1); + done(); + }); + }); + + it('cache files', (done) => { + const files = ['test.js', 'test1.js', 'test2.js', 'test3.js']; + + cacache.get = jest.fn(cacache.get); + cacache.put = jest.fn(cacache.put); + + compilationEventBinding.handler([{ + files, + }], () => { + // Try to found cached files, but we don't have their in cache + expect(cacache.get.mock.calls.length).toBe(4); + // Put files in cache + expect(cacache.put.mock.calls.length).toBe(4); + + cacache + .ls(cacheDir) + .then((cacheEntriesList) => { + const cacheKeys = Object.keys(cacheEntriesList); + + // Make sure that we cached files + expect(cacheKeys.length).toBe(files.length); + cacheKeys.forEach((cacheEntry) => { + // eslint-disable-next-line no-new-func + const cacheEntryOptions = new Function(`'use strict'\nreturn ${cacheEntry}`)(); + + expect(cacheEntryOptions.myCacheKey).toBe(1); + expect(cacheEntryOptions.myCacheKeyBasedOnFile).toMatch(/file-test(.)?\.js/); + expect([cacheEntryOptions.path, cacheEntryOptions.hash]) + .toMatchSnapshot(cacheEntryOptions.path); + }); + + // Reset compilation assets and mocks + compilation.assets = Object.assign({}, assets); + compilation.errors = []; + + cacache.get.mockClear(); + cacache.put.mockClear(); + + compilationEventBinding.handler([{ + files, + }], () => { + // Now we have cached files so we get their and don't put + expect(cacache.get.mock.calls.length).toBe(4); + expect(cacache.put.mock.calls.length).toBe(0); + + done(); + }); + }); + }); + }); + }); + }); + }); + + it('matches snapshot', () => { + const compiler = createCompiler(); + new UglifyJsPlugin({ + cache: true, + cacheKeys: (defaultCacheKeys, file) => { + // eslint-disable-next-line no-param-reassign + defaultCacheKeys.myCacheKey = 1; + // eslint-disable-next-line no-param-reassign + defaultCacheKeys.myCacheLeyBasedOnFile = `file-${file}`; + + return defaultCacheKeys; + }, + }).apply(compiler); + + return compile(compiler) + .then((stats) => { + const errors = stats.compilation.errors.map(cleanErrorStack); + const warnings = stats.compilation.warnings.map(cleanErrorStack); + + expect(errors).toMatchSnapshot('errors'); + expect(warnings).toMatchSnapshot('warnings'); + + for (const file in stats.compilation.assets) { + if (Object.prototype.hasOwnProperty.call(stats.compilation.assets, file)) { + expect(stats.compilation.assets[file].source()).toMatchSnapshot(file); + } + } + }); + }); + }); });