diff --git a/package-lock.json b/package-lock.json index c6dae60..4adc1b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,6 @@ }, "devDependencies": { "@babel/eslint-parser": "7.25.1", - "@eloquent/git-version-webpack-plugin": "5.0.1", "autoprefixer": "10.4.20", "copy-webpack-plugin": "12.0.2", "css-loader": "7.1.2", @@ -450,27 +449,6 @@ "node": ">=10.0.0" } }, - "node_modules/@eloquent/git-version-webpack-plugin": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@eloquent/git-version-webpack-plugin/-/git-version-webpack-plugin-5.0.1.tgz", - "integrity": "sha512-p445+YEiw6U8nfaiPe3e+HhMGVdlW3q3YraD22N2oI1ldVLF9UOVHJcWWs3Nqg2JQKCNHXa4vnb6mTgEDoclSQ==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-require": "^1.0.3", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "html-webpack-plugin": "^5.3.1" - }, - "peerDependencies": { - "webpack": "^5.34.0" - } - }, "node_modules/@emnapi/runtime": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", @@ -8222,14 +8200,6 @@ ], "license": "MIT" }, - "node_modules/safe-require": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/safe-require/-/safe-require-1.0.4.tgz", - "integrity": "sha512-1elAbSH1u7HVMfbuqktLWAN0wMOeT+FnJVqMhBgEJLvL95m+KT433tiJdGMV1e3TstQXRt1YrKQDRBu0Kpk4WA==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "Fair" - }, "node_modules/safe-stable-stringify": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", diff --git a/package.json b/package.json index d6366bb..f478266 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ }, "devDependencies": { "@babel/eslint-parser": "7.25.1", - "@eloquent/git-version-webpack-plugin": "5.0.1", "autoprefixer": "10.4.20", "copy-webpack-plugin": "12.0.2", "css-loader": "7.1.2", diff --git a/webpack.config.js b/webpack.config.js index 03c9dd4..ce503c5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,16 +1,15 @@ import path from "path" import { glob } from "glob" -import { fileURLToPath } from 'url'; +import { fileURLToPath } from "url" import { WebpackManifestPlugin } from "webpack-manifest-plugin" -import GitVersionPlugin from "@eloquent/git-version-webpack-plugin" import FaviconsWebpackPlugin from "favicons-webpack-plugin" import RemoveEmptyScriptsPlugin from "webpack-remove-empty-scripts" import CopyPlugin from "copy-webpack-plugin" -import SRIPlugin from "./webpack.plugins.js" +import { SRIPlugin, GitVersionPlugin } from "./webpack.plugins.js" -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) const nodeModulesPath = path.resolve(__dirname, "node_modules") var config = { @@ -108,7 +107,7 @@ var config = { }), new GitVersionPlugin({ - path: "../VERSION" + outputFile: "../VERSION" }) ] } @@ -125,7 +124,7 @@ export default (argv) => { type: "asset/resource", generator: { filename: "fonts/[name][ext]" - }, + } }, { test: /\.(sa|sc|c)ss$/i, diff --git a/webpack.plugins.js b/webpack.plugins.js index 5a607b0..0eda026 100644 --- a/webpack.plugins.js +++ b/webpack.plugins.js @@ -3,53 +3,145 @@ import crypto from "crypto" import path from "path" import { validate } from "schema-utils" -export default class SRIPlugin { +import { access as accessCps } from "fs" +import { execFile as execFileCps } from "child_process" +import { promisify } from "util" + +class SRIPlugin { static defaultOptions = { algorithm: "sha512", sourceFile: "assets.json" } constructor(options = {}) { - const schema = { - type: "object", - properties: { - outputFile: { - type: "string" - }, - algorithm: { - type: "string" - } - } - } - this.options = { ...SRIPlugin.defaultOptions, ...options } - validate(schema, options, { - name: "SRI Plugin", - baseDataPath: "options" - }) + validate( + { + type: "object", + properties: { + sourceFile: { type: "string" }, + outputFile: { type: "string" }, + algorithm: { type: "string" } + } + }, + options, + { + name: "SRI Plugin", + baseDataPath: "options" + } + ) } apply(compiler) { - compiler.hooks.done.tap("SRIPlugin", (manifest) => { - let data = JSON.parse(fs.readFileSync(this.options.sourceFile, "utf8")) - let outputFile = this.options.outputFile ? this.options.outputFile : this.options.sourceFile + compiler.hooks.done.tap("SRIPlugin", () => { + const data = JSON.parse(fs.readFileSync(this.options.sourceFile, "utf8")) + const outputFile = this.options.outputFile || this.options.sourceFile + const { algorithm } = this.options - const checksum = (str, algorithm = this.options.algorithm, encoding = "base64") => - crypto.createHash(algorithm).update(str, "utf8").digest(encoding) - const fileSum = (file, algorithm) => checksum(fs.readFileSync(file), algorithm) - const calculateSRI = (file, algorithm = this.options.algorithm) => - `${algorithm}-${fileSum(path.join(".", "static", file), algorithm)}` + const calculateSRI = (file) => { + const fileContent = fs.readFileSync(path.join(".", "static", file)) + const hash = crypto.createHash(algorithm).update(fileContent).digest("base64") + return `${algorithm}-${hash}` + } Object.keys(data).forEach((key) => { - let element = data[key] - element.integrity = calculateSRI(element.src) + data[key].integrity = calculateSRI(data[key].src) }) - fs.writeFileSync(outputFile, JSON.stringify(data, null, 2), { - encoding: "utf8", - flag: "w" - }) + fs.writeFileSync(outputFile, JSON.stringify(data, null, 2), { encoding: "utf8", flag: "w" }) }) } } + +class GitVersionPlugin { + static defaultOptions = { + outputFile: "VERSION" + } + + constructor(options = {}) { + this.options = { ...GitVersionPlugin.defaultOptions, ...options } + + validate( + { + type: "object", + properties: { + outputFile: { type: "string" } + } + }, + options, + { + baseDataPath: "options", + name: "GitVersion Plugin" + } + ) + } + + apply(compiler) { + const { webpack, hooks, context } = compiler + const { Compilation } = webpack + + hooks.beforeCompile.tapPromise("GitVersionPlugin", async () => { + const access = promisify(accessCps) + + try { + await access(".git") + this.dependsOnGit = true + } catch { + this.dependsOnGit = false + } + }) + + hooks.compilation.tap("GitVersionPlugin", (compilation) => { + if (this.dependsOnGit) { + compilation.fileDependencies.add(path.join(context, ".git/logs/HEAD")) + compilation.contextDependencies.add(path.join(context, ".git/refs/tags")) + } + + compilation.hooks.processAssets.tapPromise( + { + name: "GitVersionPlugin", + stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL + }, + async (assets) => { + try { + const v = await this.version() + + assets[this.options.outputFile] = { + source: () => `${v}\n`, + size: () => v.length + 1 + } + } catch { + assets[this.options.outputFile] = { + source: () => "", + size: () => 0 + } + } + } + ) + }) + } + + async version() { + const execFile = promisify(execFileCps) + + try { + const { stdout: describe } = await execFile("git", ["describe", "--long", "--tags"]) + const [, tag, offset] = describe.trim().match(/^(.*)-(\d+)-g[0-9a-f]+$/) + return parseInt(offset) === 0 ? tag : this.getBranchAndHash() + } catch { + return this.getBranchAndHash() + } + } + + async getBranchAndHash() { + const execFile = promisify(execFileCps) + const [{ stdout: branch }, { stdout: hash }] = await Promise.all([ + execFile("git", ["rev-parse", "--abbrev-ref", "HEAD"]), + execFile("git", ["rev-parse", "HEAD"]) + ]) + return `${branch.trim()}@${hash.substring(0, 7)}` + } +} + +export { SRIPlugin, GitVersionPlugin }