laravel-mix-versionhash
Advanced tools
Comparing version 1.0.8 to 1.1.0
{ | ||
"license": "MIT", | ||
"version": "1.0.8", | ||
"version": "1.1.0", | ||
"author": "ctf0", | ||
@@ -5,0 +5,0 @@ "name": "laravel-mix-versionhash", |
@@ -31,4 +31,1 @@ <h1 align="center"> | ||
| delimiter | string | `'.'` | the delimiter for filename and hash, <br> note that anything other than `. - _` will be removed | | ||
## TODO | ||
- add option to exclude files from hashing. |
178
src/index.js
const mix = require('laravel-mix') | ||
const File = require('laravel-mix/src/File') | ||
const proxyMethod = require('proxy-method') | ||
const ConcatenateFilesTask = require('laravel-mix/src/tasks/ConcatenateFilesTask') | ||
const forIn = require('lodash/forIn') | ||
const jsonfile = require('jsonfile') | ||
const escapeStringRegexp = require('escape-string-regexp') | ||
@@ -9,41 +11,52 @@ const ExtractTextPlugin = require('extract-text-webpack-plugin') | ||
/** | ||
* Version Hash for Laravel Mix. | ||
* | ||
* @see https://laravel-mix.com/ | ||
*/ | ||
class VersionHash { | ||
register(options = {}) { | ||
this.options = Object.assign( | ||
{ | ||
length: 6, | ||
delimiter: separator, | ||
exclude: [] | ||
}, | ||
options | ||
) | ||
const delimiter = escapeStringRegexp(this.getDelimiter()) | ||
const mixManifest = `${Config.publicPath}/${Mix.manifest.name}` | ||
const removeHashFromKeyRegex = new RegExp(`${delimiter}([a-f0-9]{${this.options.length}})\\.([^.]+)$`, 'g') | ||
const removeHashFromKeyRegexWithMap = new RegExp(`${delimiter}([a-f0-9]{${this.options.length}})\\.([^.]+)\\.map$`, 'g') | ||
/** | ||
* Constructor. | ||
*/ | ||
constructor() { | ||
// hash the generated assets once build is complete | ||
this.registerHashAssets() | ||
return mix.webpackConfig().then(() => { | ||
jsonfile.readFile(mixManifest, (err, obj) => { | ||
let newJson = {} | ||
forIn(obj, (value, key) => { | ||
key = key.endsWith('.map') | ||
? key.replace(removeHashFromKeyRegexWithMap, '.$2.map') | ||
: key.replace(removeHashFromKeyRegex, '.$2') | ||
newJson[key] = value | ||
}) | ||
jsonfile.writeFile(mixManifest, newJson, {spaces: 2}, (err) => { | ||
if (err) console.error(err) | ||
}) | ||
}) | ||
}) | ||
// look for instances of combining file(s) | ||
this.hashForCombine() | ||
} | ||
/** | ||
* Dependencies for plugin. | ||
* | ||
* @return {String[]} | ||
*/ | ||
dependencies() { | ||
return ['jsonfile', 'escape-string-regexp', 'path'] | ||
return [ | ||
'jsonfile', | ||
'escape-string-regexp', | ||
'path', | ||
'proxy-method | ||
] | ||
} | ||
/** | ||
* Plugin functionality. | ||
* | ||
* @param {{length: {Number}, delimiter: {String}, exclude: {String[]}}} options | ||
*/ | ||
register(options = {}) { | ||
this.options = Object.assign({ | ||
length: 6, | ||
delimiter: separator, | ||
exclude: [] | ||
}, options) | ||
} | ||
/** | ||
* Apply configuration to webpack configuration. | ||
* | ||
* @param {Object} webpackConfig | ||
*/ | ||
webpackConfig(webpackConfig) { | ||
@@ -55,10 +68,11 @@ const length = this.options.length | ||
let chunkhash = `[name]${delimiter}[chunkhash:${length}].js` | ||
let usesExtract = webpackConfig.optimization && webpackConfig.optimization.runtimeChunk | ||
webpackConfig.output.filename = chunkhash | ||
let usesExtract = webpackConfig.optimization && webpackConfig.optimization.runtimeChunk | ||
if (webpackConfig.output.chunkFilename && !usesExtract) { | ||
// merge chunkFilename paths | ||
let directory = path.dirname(webpackConfig.output.chunkFilename) | ||
webpackConfig.output.chunkFilename = `${directory}/${chunkhash}` | ||
} else { | ||
@@ -71,3 +85,4 @@ webpackConfig.output.chunkFilename = chunkhash | ||
forIn(webpackConfig.plugins, (value, key) => { | ||
forIn(webpackConfig.plugins, value => { | ||
if (value instanceof ExtractTextPlugin && !value.filename.includes(contenthash)) { | ||
@@ -81,15 +96,104 @@ | ||
} | ||
} | ||
}) | ||
} | ||
/** | ||
* Update backslashes to forward slashes for consistency. | ||
* | ||
* @return {Object} | ||
*/ | ||
webpackPlugins() { | ||
const combinedFiles = this.combinedFiles | ||
return new class { | ||
apply(compiler) { | ||
compiler.plugin('done', stats => { | ||
forIn(stats.compilation.assets, (asset, path) => { | ||
if (combinedFiles[path]) { | ||
delete stats.compilation.assets[path] | ||
stats.compilation.assets[path.replace(/\\/g, '/')] = asset | ||
} | ||
}) | ||
}) | ||
} | ||
} | ||
} | ||
/** | ||
* Get configured delimiter with appropriate filtering. | ||
* | ||
* @return {String} | ||
*/ | ||
getDelimiter() { | ||
return this.options.delimiter.replace(/[^\.|\-|_]/g, '') || separator | ||
return this.options.delimiter.replace(/[^.\-_]/g, '') || separator | ||
} | ||
/** | ||
* TODO vet whether or not this needs to exist...? | ||
*/ | ||
exclude(key) { | ||
return this.options.exclude.some((e) => e == key) | ||
return this.options.exclude.some(e => e == key) | ||
} | ||
/** | ||
* Add listener to account for hashing in filename(s) persisted to manifest. | ||
* | ||
* @return {this} | ||
*/ | ||
registerHashAssets() { | ||
Mix.listen('build', () => { | ||
let op_length = this.options.length | ||
const delimiter = escapeStringRegexp(this.getDelimiter()) | ||
const removeHashFromKeyRegex = new RegExp(`${delimiter}([a-f0-9]{${op_length}})\\.([^.]+)$`, 'g') | ||
const removeHashFromKeyRegexWithMap = new RegExp(`${delimiter}([a-f0-9]{${op_length}})\\.([^.]+)\\.map$`, 'g') | ||
const file = File.find(`${Config.publicPath}/${Mix.manifest.name}`) | ||
let newJson = {} | ||
forIn(JSON.parse(file.read()), (value, key) => { | ||
if (key.endsWith('.map')) { | ||
key = key.replace(removeHashFromKeyRegexWithMap, '.$2.map') | ||
} else { | ||
key = key.replace(removeHashFromKeyRegex, '.$2') | ||
} | ||
newJson[key] = value | ||
}) | ||
file.write(newJson) | ||
}) | ||
return this | ||
} | ||
/** | ||
* Intercept functionality that generates combined asset(s). | ||
* | ||
* @return {this} | ||
*/ | ||
hashForCombine() { | ||
this.combinedFiles = {} | ||
// hook into Mix's task collection to update file name hashes | ||
proxyMethod.before(Mix, 'addTask', task => { | ||
if (task instanceof ConcatenateFilesTask) { | ||
proxyMethod.after(task, 'merge', () => { | ||
const file = task.assets.pop() | ||
const hash = `${this.getDelimiter()}${file.version().substr(0, this.options.length)}` | ||
const hashed = file.rename(`${file.nameWithoutExtension()}${hash}${file.extension()}`) | ||
task.assets.push(hashed) | ||
this.combinedFiles[hashed.pathFromPublic()] = true | ||
}) | ||
} | ||
}) | ||
return this | ||
} | ||
} | ||
mix.extend('versionHash', new VersionHash()) |
9266
162
31