├── .github ├── FUNDING.yml └── stale.yml ├── .gitignore ├── LICENSE ├── README.md ├── package.json └── src └── index.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ctf0 2 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 3 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 10 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - enhancement 8 | - bug 9 | # Label to use when marking an issue as stale 10 | staleLabel: stale 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: false 13 | # Comment to post when closing a stale issue. Set to `false` to disable 14 | closeComment: > 15 | This issue has been automatically marked as stale because it has not had 16 | recent activity. It will be closed if no further activity occurs. Thank you 17 | for your contributions. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .history 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Muah 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Laravel Mix Versionhash 3 |
4 | npm npm 5 |

6 | 7 | Auto append hash to file instead of using virtual one [Read More](https://github.com/JeffreyWay/laravel-mix/issues/1022) 8 | 9 | 10 | ### :exclamation: Looking For Maintainers :exclamation: 11 | ### as i dont have enough time to work on the package anymore, so anyone wants to join forces plz get in-touch, thanks. 12 | 13 |
14 | 15 | ## Installation 16 | 17 | ```bash 18 | npm install laravel-mix-versionhash --save 19 | ``` 20 | 21 | ## Usage 22 | 23 | ```js 24 | require('laravel-mix-versionhash') 25 | 26 | mix.versionHash(); 27 | ``` 28 | 29 | - for removing old files use [Clean for WebPack](https://github.com/johnagan/clean-webpack-plugin) 30 | 31 | ## Options 32 | 33 | | option | type | default | description | 34 | |-----------|--------|---------|---------------------------------------------------------------------------------------------------| 35 | | length | int | `6` | the hash string length | 36 | | delimiter | string | `'.'` | the delimiter for filename and hash,
note that anything other than `. - _` will be removed | 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "version": "2.0.1", 4 | "author": "ctf0", 5 | "name": "laravel-mix-versionhash", 6 | "main": "src/index.js", 7 | "description": "auto append hash to file name", 8 | "homepage": "https://github.com/ctf0/laravel-mix-versionhash", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/ctf0/laravel-mix-versionhash.git" 12 | }, 13 | "dependencies": { 14 | "proxy-method": "^1.0.0" 15 | }, 16 | "peerDependencies": { 17 | "laravel-mix": "^6.0.0", 18 | "collect.js": "^4.28.6" 19 | }, 20 | "keywords": [ 21 | "laravel", 22 | "mix", 23 | "webpack", 24 | "hash" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const laravel_mix = require('laravel-mix') 2 | const File = require('laravel-mix/src/File') 3 | const proxyMethod = require('proxy-method') 4 | const ConcatenateFilesTask = require('laravel-mix/src/tasks/ConcatenateFilesTask') 5 | const forIn = require('lodash/forIn') 6 | const escapeStringRegexp = require('escape-string-regexp') 7 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 8 | const path = require('path') 9 | const collect = require('collect.js') 10 | const separator = '.' 11 | 12 | /** 13 | * Version Hash for Laravel mix. 14 | * 15 | * @see https://laravel-mix.com/ 16 | */ 17 | class VersionHash { 18 | 19 | /** 20 | * Constructor. 21 | */ 22 | constructor() { 23 | // hash the generated assets once build is complete 24 | this.registerHashAssets() 25 | 26 | // look for instances of combining file(s) 27 | this.hashForCombine() 28 | } 29 | 30 | /** 31 | * Dependencies for plugin. 32 | * 33 | * @return {String[]} 34 | */ 35 | dependencies() { 36 | return [ 37 | 'jsonfile', 38 | 'escape-string-regexp', 39 | 'path', 40 | 'proxy-method' 41 | ] 42 | } 43 | 44 | /** 45 | * Plugin functionality. 46 | * 47 | * @param {length: Number, delimiter: String, exclude: String[]} options 48 | */ 49 | register(options = {}) { 50 | this.options = Object.assign({ 51 | length : 6, 52 | delimiter: separator, 53 | exclude : [] 54 | }, options) 55 | } 56 | 57 | /** 58 | * Apply configuration to webpack configuration. 59 | * 60 | * @param {Object} webpackConfig 61 | */ 62 | webpackConfig(webpackConfig) { 63 | if (!this.options) { 64 | this.register({}) 65 | } 66 | 67 | const length = this.options.length 68 | const delimiter = this.getDelimiter() 69 | 70 | /* Js ----------------------------------------------------------------------- */ 71 | 72 | let chunkhash = `[name]${delimiter}[chunkhash:${length}].js` 73 | let usesExtract = webpackConfig.optimization && webpackConfig.optimization.runtimeChunk 74 | webpackConfig.output.filename = chunkhash 75 | 76 | if (typeof webpackConfig.output.chunkFilename != "function" && !usesExtract) { 77 | // merge chunkFilename paths 78 | let directory = path.dirname(webpackConfig.output.chunkFilename) 79 | webpackConfig.output.chunkFilename = `${directory}/${chunkhash}` 80 | } else { 81 | webpackConfig.output.chunkFilename = chunkhash 82 | } 83 | 84 | /* Css ---------------------------------------------------------------------- */ 85 | 86 | let contenthash = `[hash:${length}].css` 87 | 88 | forIn(webpackConfig.plugins, (value) => { 89 | if (value instanceof MiniCssExtractPlugin && !value.options.filename.includes(contenthash)) { 90 | let csspath = value.options.filename.substring(0, value.options.filename.lastIndexOf('.')) 91 | let filename = `${csspath}${delimiter}${contenthash}` 92 | 93 | if (value.options.filename != filename) { 94 | value.options.filename = filename 95 | } 96 | } 97 | }) 98 | 99 | /* Files Inside Css --------------------------------------------------------- */ 100 | 101 | forIn(webpackConfig.module.rules, (rule) => { 102 | 103 | // check if the rule is /(\.(png|jpe?g|gif|webp)$|^((?!font).)*\.svg$)/ 104 | if ('.png'.match(new RegExp(rule.test))) { 105 | forIn(rule.loaders, (loader) => { 106 | if (loader.loader === 'file-loader') { 107 | loader.options.name = (path) => { 108 | if (!/node_modules|bower_components/.test(path)) { 109 | return Config.fileLoaderDirs.images + `/[name]${delimiter}[hash:${length}].[ext]` 110 | } 111 | 112 | return Config.fileLoaderDirs.images + 113 | '/vendor/' + 114 | path.replace(/\\/g, '/').replace(/((.*(node_modules|bower_components))|images|image|img|assets)\//g, '') + 115 | `?[hash:${length}]` 116 | } 117 | } 118 | }) 119 | } 120 | 121 | // check if the rule is /(\.(woff2?|ttf|eot|otf)$|font.*\.svg$)/ 122 | if ('.woff'.match(new RegExp(rule.test))) { 123 | forIn(rule.loaders, (loader) => { 124 | if (loader.loader === 'file-loader') { 125 | loader.options.name = (path) => { 126 | if (!/node_modules|bower_components/.test(path)) { 127 | return Config.fileLoaderDirs.fonts + `/[name]${delimiter}[hash:${length}].[ext]` 128 | } 129 | 130 | return Config.fileLoaderDirs.fonts + 131 | '/vendor/' + 132 | path.replace(/\\/g, '/').replace(/((.*(node_modules|bower_components))|fonts|font|assets)\//g, '') + 133 | `?[hash:${length}]` 134 | } 135 | } 136 | }) 137 | } 138 | 139 | // check if the rule is /\.(cur|ani)$/ 140 | if ('.cur'.match(new RegExp(rule.test))) { 141 | forIn(rule.loaders, (loader) => { 142 | if (loader.loader === 'file-loader') { 143 | loader.options.name = `[name]${delimiter}[hash:${length}].[ext]` 144 | } 145 | }) 146 | } 147 | 148 | }) 149 | } 150 | 151 | /** 152 | * Update backslashes to forward slashes for consistency. 153 | * 154 | * @return {Object} 155 | */ 156 | webpackPlugins() { 157 | const combinedFiles = this.combinedFiles 158 | 159 | return new class { 160 | apply(compiler) { 161 | compiler.hooks.done.tap('done', (stats) => { 162 | forIn(stats.compilation.assets, (asset, path) => { 163 | if (combinedFiles[path]) { 164 | delete stats.compilation.assets[path] 165 | stats.compilation.assets[path.replace(/\\/g, '/')] = asset 166 | } 167 | }) 168 | }) 169 | } 170 | }() 171 | } 172 | 173 | /** 174 | * Get configured delimiter with appropriate filtering. 175 | * 176 | * @return {String} 177 | */ 178 | getDelimiter() { 179 | return this.options.delimiter.replace(/[^.\-_]/g, '') || separator 180 | } 181 | 182 | /** 183 | * TODO 184 | */ 185 | exclude(key) { 186 | return this.options.exclude.some((e) => e == key) 187 | } 188 | 189 | /** 190 | * Add listener to account for hashing in filename(s) persisted to manifest. 191 | * 192 | * @return {this} 193 | */ 194 | registerHashAssets() { 195 | Mix.listen('build', () => { 196 | if (!this.options) { 197 | this.register({}) 198 | } 199 | 200 | let op_length = this.options.length 201 | const delimiter = escapeStringRegexp(this.getDelimiter()) 202 | const removeHashFromKeyRegex = new RegExp(`${delimiter}([a-f0-9]{${op_length}})\\.([^.]+)$`, 'g') 203 | const removeHashFromKeyRegexWithMap = new RegExp(`${delimiter}([a-f0-9]{${op_length}})\\.([^.]+)\\.map$`, 'g') 204 | 205 | const file = File.find(`${Config.publicPath}/${Mix.manifest.name}`) 206 | let newJson = {} 207 | 208 | forIn(JSON.parse(file.read()), (value, key) => { 209 | key = key.endsWith('.map') 210 | ? key.replace(removeHashFromKeyRegexWithMap, '.$2.map') 211 | : key.replace(removeHashFromKeyRegex, '.$2') 212 | 213 | newJson[key] = value 214 | }) 215 | 216 | file.write(collect(newJson) 217 | .sortKeys() 218 | .all() 219 | ) 220 | }) 221 | 222 | return this 223 | } 224 | 225 | /** 226 | * Intercept functionality that generates combined asset(s). 227 | * 228 | * @return {this} 229 | */ 230 | hashForCombine() { 231 | this.combinedFiles = {} 232 | 233 | // hook into Mix's task collection to update file name hashes 234 | forIn(Mix.tasks, (task) => { 235 | if (task instanceof ConcatenateFilesTask) { 236 | proxyMethod.after(task, 'merge', () => { 237 | const file = task.assets.pop() 238 | const hash = `${this.getDelimiter()}${file.version().substr(0, this.options.length)}` 239 | const hashed = file.rename(`${file.nameWithoutExtension()}${hash}${file.extension()}`) 240 | 241 | task.assets.push(hashed) 242 | this.combinedFiles[hashed.pathFromPublic()] = true 243 | }) 244 | } 245 | }) 246 | 247 | return this 248 | } 249 | } 250 | 251 | laravel_mix.extend('versionHash', new VersionHash()) 252 | --------------------------------------------------------------------------------