laravel-mix-make-file-hash
Version:
Convert the default Laravel Mix querystring hashing to filename hashing.
180 lines (170 loc) • 5.19 kB
JavaScript
const path = require("path");
const del = require("del");
const fs = require("fs");
const micromatch = require("micromatch");
const { promisify } = require("util");
const writeFile = promisify(fs.writeFile);
const readFile = promisify(fs.readFile);
const copyFile = promisify(fs.copyFile);
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.[a-f0-9].css'
const oldHashedFilePathsGlob = path
.join(publicPath, oldHash)
.replace(/(.+)\.([^?]+)\?id=(.+)$/g, "$1.[a-f0-9].$2");
const deletedPaths = await del(
[oldHashedFilePathsGlob],
delOptions
).catch(error => console.error(error));
debug &&
deletedPaths.length &&
console.debug(
`Removed stale hash files: ${oldHashedFilePathsGlob} (${deletedPaths})`
);
}
};
const getNewFilename = file =>
file.replace(/(.+)\.([^?]+)\?id=(.+)$/g, "$1.$3.$2");
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,
disableFileOperations,
debug
}) => {
const newJson = {};
if (!manifest) return newJson;
for (let [oldNonHash, oldHash] of Object.entries(manifest)) {
const newFilePath = getNewFilename(path.join(publicPath, oldHash));
const oldFilePath = path.join(publicPath, oldNonHash);
if (!disableFileOperations) {
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, blacklist) => {
if (!blacklist || blacklist.length === 0)
return { filteredManifest: manifest };
let removedLines;
let filteredManifest;
Object.entries(manifest).forEach(([key, val]) => {
if (micromatch.contains(key, blacklist)) {
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,
blacklist,
delOptions,
keepBlacklistedEntries = false,
disableFileOperations = 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`
);
if (disableFileOperations) {
console.log(
`Note: File changes have been disabled with “disableFileOperations”. Only the mix-manifest.json will be updated.\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,
blacklist || 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
};
if (!disableFileOperations) {
await deleteStaleHashedFiles({
manifest,
publicPath,
delOptions: delOptionsUnforced,
debug
});
}
const newManifest = await makeNewHashedFiles({
manifest: filteredManifest,
publicPath,
delOptions: delOptionsUnforced,
disableFileOperations,
debug
});
const combinedManifest =
keepBlacklistedEntries && removedLines
? { ...newManifest, ...removedLines }
: newManifest;
return await writeManifest({
manifest: combinedManifest,
manifestFilePath,
debug
});
};
module.exports = makeFileHash;