├── .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 |
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 |
--------------------------------------------------------------------------------