laravel-mix-make-file-hash
Advanced tools
Comparing version
208
index.js
@@ -1,49 +0,165 @@ | ||
const del = require("del") | ||
const fs = require("fs") | ||
const jsonFile = require("jsonfile") | ||
const forEach = require("lodash/forEach") | ||
const forIn = require("lodash/forIn") | ||
const path = require("path"); | ||
const del = require("del"); | ||
const fs = require("fs"); | ||
const { promisify } = require("util"); | ||
const writeFile = promisify(fs.writeFile); | ||
const readFile = promisify(fs.readFile); | ||
const copyFile = promisify(fs.copyFile); | ||
const makeFileHash = (publicPath, manifestFilePath, delSyncOptions = {}) => { | ||
const delOptions = { ...{ force: false }, ...delSyncOptions } | ||
const deleteStaleHashedFiles = async ({ | ||
manifest, | ||
publicPath, | ||
delOptions, | ||
debug | ||
}) => { | ||
for (let oldHash of Object.values(manifest)) { | ||
// A glob pattern of all files with the new file naming style e.g. 'app.*.css' | ||
const oldHashedFilePathsGlob = path | ||
.join(publicPath, oldHash) | ||
.replace(/([^.]+)\.([^?]+)\?id=(.+)$/g, "$1.*.$2"); | ||
const deletedPaths = await del( | ||
[oldHashedFilePathsGlob], | ||
delOptions | ||
).catch(error => console.error(error)); | ||
debug && | ||
deletedPaths.length && | ||
console.debug( | ||
`Removed stale hash files: ${oldHashedFilePathsGlob} (${deletedPaths})` | ||
); | ||
} | ||
}; | ||
// Parse the mix-manifest file | ||
jsonFile.readFile(manifestFilePath, (err, obj) => { | ||
const newJson = {} | ||
const oldFiles = [] | ||
forIn(obj, (value, key) => { | ||
// Get the hash from the ?id= query string parameter and | ||
// move it into the file name e.g. 'app.abcd1234.css' | ||
const newFilename = value.replace( | ||
/([^.]+)\.([^?]+)\?id=(.+)$/g, | ||
"$1.$3.$2" | ||
) | ||
// Create a glob pattern of all files with the new file naming style e.g. 'app.*.css' | ||
const oldAsGlob = value.replace( | ||
/([^.]+)\.([^?]+)\?id=(.+)$/g, | ||
"$1.*.$2" | ||
) | ||
// Delete old versioned file(s) that match the glob pattern | ||
del.sync([`${publicPath}${oldAsGlob}`], delOptions) | ||
// Copy as new versioned file name | ||
fs.copyFile( | ||
`${publicPath}${key}`, | ||
`${publicPath}${newFilename}`, | ||
err => { | ||
err && console.error(err) | ||
} | ||
) | ||
newJson[key] = newFilename | ||
oldFiles.push(key) | ||
}) | ||
forEach(oldFiles, key => { | ||
del.sync([`${publicPath}${key}`], delOptions) | ||
}) | ||
// Write the new contents of the mix manifest file | ||
jsonFile.writeFile(manifestFilePath, newJson, { spaces: 4 }, err => { | ||
if (err) console.error(err) | ||
}) | ||
}) | ||
} | ||
const getNewFilename = file => | ||
file.replace(/([^.]+)\.([^?]+)\?id=(.+)$/g, "$1.$3.$2"); | ||
module.exports = makeFileHash | ||
const normalizeData = content => { | ||
if (Buffer.isBuffer(content)) content = content.toString("utf8"); | ||
content = content.replace(/^\uFEFF/, ""); | ||
return content; | ||
}; | ||
const standardizeArgs = args => | ||
typeof args[0] === "object" | ||
? args[0] | ||
: { | ||
publicPath: args[0], | ||
manifestFilePath: args[1], | ||
delOptions: args[3] || {} | ||
}; | ||
const makeNewHashedFiles = async ({ | ||
manifest, | ||
publicPath, | ||
delOptions, | ||
debug | ||
}) => { | ||
const newJson = {}; | ||
for (let [oldNonHash, oldHash] of Object.entries(manifest)) { | ||
const newFilePath = getNewFilename(path.join(publicPath, oldHash)); | ||
const oldFilePath = path.join(publicPath, oldNonHash); | ||
await copyFile(oldFilePath, newFilePath).catch(error => | ||
console.error(error) | ||
); | ||
await del([oldFilePath], delOptions).catch(error => console.error(error)); | ||
debug && | ||
console.debug( | ||
`Renamed '${oldFilePath}' to '${newFilePath}' (delOptions '${JSON.stringify( | ||
delOptions | ||
)}')` | ||
); | ||
newJson[oldNonHash] = getNewFilename(oldHash); | ||
} | ||
return newJson; | ||
}; | ||
const filterManifest = (manifest, fileTypesBlacklist) => { | ||
if (!fileTypesBlacklist || fileTypesBlacklist.length === 0) | ||
return { filteredManifest: manifest }; | ||
let removedLines; | ||
let filteredManifest; | ||
Object.entries(manifest).forEach(([key, val]) => { | ||
const fileType = key.split(".").pop(); | ||
if (fileTypesBlacklist.includes(fileType)) { | ||
return (removedLines = { ...removedLines, [key]: val }); | ||
} | ||
filteredManifest = { ...filteredManifest, [key]: val }; | ||
}); | ||
return { filteredManifest, removedLines }; | ||
}; | ||
const writeManifest = async ({ manifestFilePath, manifest, debug }) => { | ||
const EOL = "\n"; | ||
const jsonManifest = JSON.stringify(manifest, null, 4); | ||
const formattedManifest = jsonManifest.replace(/\n/g, EOL) + EOL; | ||
await writeFile(manifestFilePath, formattedManifest).catch(error => | ||
console.error(error) | ||
); | ||
debug && | ||
console.debug( | ||
`Finished updating '${manifestFilePath}' with the new filenames:\n`, | ||
JSON.parse(formattedManifest) | ||
); | ||
return JSON.parse(formattedManifest); | ||
}; | ||
const makeFileHash = async (...args) => { | ||
const { | ||
publicPath, | ||
manifestFilePath, | ||
fileTypesBlacklist, | ||
delOptions, | ||
keepBlacklistedEntries = false, | ||
debug | ||
} = standardizeArgs(args); | ||
if (!publicPath) | ||
return console.error(`Error: 'Make file hash' needs a 'publicPath'!\n`); | ||
if (!manifestFilePath) | ||
return console.error( | ||
`Error: 'Make file hash' needs a 'manifestFilePath'!\n` | ||
); | ||
const rawManifest = await readFile(manifestFilePath).catch(error => | ||
console.error(error) | ||
); | ||
const manifest = await JSON.parse(normalizeData(rawManifest)); | ||
debug && console.debug(`Manifest found: '${manifestFilePath}'`); | ||
const { filteredManifest, removedLines } = filterManifest( | ||
manifest, | ||
fileTypesBlacklist | ||
); | ||
debug && | ||
removedLines && | ||
keepBlacklistedEntries && | ||
console.debug(`Files that will not be re-hashed:\n`, removedLines); | ||
debug && | ||
removedLines && | ||
!keepBlacklistedEntries && | ||
console.debug(`Files removed from manifest:\n`, removedLines); | ||
// Don't force delete by default | ||
const delOptionsUnforced = { | ||
...{ force: false }, | ||
...delOptions | ||
}; | ||
await deleteStaleHashedFiles({ | ||
manifest, | ||
publicPath, | ||
delOptions: delOptionsUnforced, | ||
debug | ||
}); | ||
const newManifest = await makeNewHashedFiles({ | ||
manifest: filteredManifest, | ||
publicPath, | ||
delOptions: delOptionsUnforced, | ||
debug | ||
}); | ||
const combinedManifest = | ||
keepBlacklistedEntries && removedLines | ||
? { ...newManifest, ...removedLines } | ||
: newManifest; | ||
return await writeManifest({ | ||
manifest: combinedManifest, | ||
manifestFilePath, | ||
debug | ||
}); | ||
}; | ||
module.exports = makeFileHash; |
{ | ||
"name": "laravel-mix-make-file-hash", | ||
"version": "1.2.0", | ||
"version": "2.0.0", | ||
"description": "Convert the default Laravel Mix querystring hashing to filename hashing.", | ||
@@ -21,4 +21,5 @@ "main": "index.js", | ||
"dependencies": { | ||
"del": "^5.1.0", | ||
"lodash": "^4.17.11" | ||
} | ||
} |
112
README.md
# Laravel Mix make file hash | ||
Mix has querystring hashing by default which doesn't work too well with some caching systems. | ||
By default, the hashing system in Laravel Mix will append a querystring to the filename to invalidate the cache. | ||
Querystring hashing looks like this:<br> | ||
Example from `mix-manifest.json`: | ||
```json | ||
{ | ||
"/dist/main.js": "/dist/main.js?id=1e70e7c11a9185f57e64" | ||
} | ||
``` | ||
(OLD) main.css?id=abcd1234 | ||
``` | ||
After mix has done it's thing, this script converts that querystring hashing to filename hashing: | ||
The problem is that this way of hashing may not work with some caching systems. | ||
Instead, **"Laravel Mix make file hash" will move the hash into the filename of the hashed files**. So your `mix-manifest.json` will look something like this: | ||
```json | ||
{ | ||
"/dist/main.js": "/dist/main.1e70e7c11a9185f57e64.js" | ||
} | ||
``` | ||
(NEW) main.abcd1234.css | ||
``` | ||
## Under the hood | ||
To accomplish the re-hashing, this script will: | ||
1. Read the `mix-manifest.json` generated by Laravel Mix. | ||
2. Remove stale hashed files. | ||
3. Rename the files found in the manifest to include the hash within the filename. | ||
4. Update `mix-manifest.json` with the new filenames. | ||
5. Return the contents of the new manifest for further usage (if required). | ||
Turn on debug to activate noisy feedback: | ||
`{ debug: true }` | ||
## Installation | ||
Install package from [npmjs.com](https://www.npmjs.com/package/laravel-mix-make-file-hash): | ||
```bash | ||
npm i -D laravel-mix-make-file-hash | ||
# or | ||
yarn add laravel-mix-make-file-hash -D | ||
``` | ||
## Usage | ||
## Normal usage | ||
This is not a Laravel mix plugin so use with `mix.then()` like this: | ||
```js | ||
if (mix.inProduction()) { | ||
mix.version(); | ||
mix.then(() => { | ||
const convertToFileHash = require("laravel-mix-make-file-hash"); | ||
convertToFileHash({ | ||
publicPath: "web", | ||
manifestFilePath: "web/mix-manifest.json" | ||
}); | ||
}); | ||
} | ||
``` | ||
// Allow versioning in production | ||
mix.version() | ||
## Promise usage | ||
// Run after mix finishes | ||
mix.then(() => { | ||
const laravelMixMakeFileHash = require("laravel-mix-make-file-hash") | ||
laravelMixMakeFileHash('web', 'web/mix-manifest.json') | ||
}) | ||
`convertToFileHash` returns a Promise containing the new `mix-manifest.json` contents. This means you can perform another task after the `convertToFileHash` has finished the re-hashing. | ||
```js | ||
if (mix.inProduction()) { | ||
mix.version(); | ||
mix.then(async () => { | ||
const convertToFileHash = require("laravel-mix-make-file-hash"); | ||
const fileHashedManifest = await convertToFileHash({ | ||
publicPath: "web", | ||
manifestFilePath: "web/mix-manifest.json" | ||
}); | ||
// Do something here... | ||
}); | ||
} | ||
@@ -41,15 +81,33 @@ ``` | ||
| Name | Type | Default | Description | | ||
| ---------------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------ | | ||
| publicPath \* | `string` | `undefined` | The path to your public folder (eg: `"web"`) | | ||
| manifestFilePath \* | `string` | `undefined` | The filePath to your mix-manifest.json<br /> (eg: `"web/mix-manifest.json"`) | | ||
| fileTypesBlacklist | `string`/`array` | `undefined` | A list of filetypes to ignore re-hashing | | ||
| keepBlacklistedEntries | `boolean` | `false` | Whether to keep blacklisted entries in the manifest | | ||
| delOptions | `object` | `{ force: false }` | Options to provide to del - [See options](https://www.npmjs.com/package/del#options) | | ||
| debug | `boolean` | `false` | Debug exactly what's happening (or meant to happen) during runtime | | ||
\* = Required | ||
## Full config example | ||
```js | ||
laravelMixMakeFileHash( | ||
publicPath, | ||
manifestFilePath, | ||
delSyncOptions = { force: true }, | ||
) | ||
if (mix.inProduction()) { | ||
mix.version(); | ||
mix.then(async () => { | ||
const convertToFileHash = require("laravel-mix-make-file-hash"); | ||
convertToFileHash({ | ||
publicPath: "web", | ||
manifestFilePath: "web/mix-manifest.json", | ||
fileTypesBlacklist: ["html"], | ||
keepBlacklistedEntries: true, | ||
delOptions: { force: false }, | ||
debug: false | ||
}); | ||
}) | ||
``` | ||
## Under the hood | ||
## Links | ||
It'll first look at your manifest, then create a new file with the updated hash and then remove the old file. | ||
Most of the code is from tomgrohl on this | ||
[Laravel Mix issue on querystring hashing](https://github.com/JeffreyWay/laravel-mix/issues/1022#issuecomment-379168021) | ||
This script was created for the [Agency Webpack Mix Config](https://github.com/ben-rogerson/agency-webpack-mix-config). |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
10461
13.98%156
239.13%113
105.45%2
100%4
-50%1
Infinity%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added