mirror of
https://github.com/Alvin-Zilverstand/return-youtube-dislike.git
synced 2026-10-27 23:30:15 +01:00
205 lines
7.8 KiB
JavaScript
205 lines
7.8 KiB
JavaScript
|
|
const assert = require("node:assert/strict");
|
||
|
|
const crypto = require("node:crypto");
|
||
|
|
const fs = require("node:fs");
|
||
|
|
const path = require("node:path");
|
||
|
|
|
||
|
|
const USERSCRIPT_BUILD_RECEIPT_SCHEMA_VERSION = 1;
|
||
|
|
const USERSCRIPT_BUILD_RECEIPT_FILENAME = "userscript-build-receipt.json";
|
||
|
|
const USERSCRIPT_ARTIFACT_RELATIVE_PATH = "Extensions/UserScript/Return Youtube Dislike.user.js";
|
||
|
|
const USERSCRIPT_LIVE_ARTIFACT_RELATIVE_PATH = "test-results/live-build/userscript/Return Youtube Dislike.user.js";
|
||
|
|
const USERSCRIPT_BUILD_RECEIPT_RELATIVE_PATH = "test-results/build-receipts/userscript-production.json";
|
||
|
|
const USERSCRIPT_LIVE_BUILD_RECEIPT_RELATIVE_PATH = "test-results/live-build/userscript/userscript-build-receipt.json";
|
||
|
|
const LIVE_BUILD_ID_PATTERN = /^[a-f0-9]{32}$/;
|
||
|
|
const TEST_FILE_PATTERN = /(?:^|[.])(e2e|spec|test)\.[cm]?[jt]sx?$/i;
|
||
|
|
const STATIC_BUILD_INPUTS = Object.freeze([
|
||
|
|
".babelrc",
|
||
|
|
".nvmrc",
|
||
|
|
"package-lock.json",
|
||
|
|
"package.json",
|
||
|
|
"userscript-build-receipt.js",
|
||
|
|
"webpack.live-build-marker.js",
|
||
|
|
"webpack.userscript.config.js",
|
||
|
|
"Extensions/UserScript/userscript.meta.js",
|
||
|
|
"Extensions/UserScript/userscript-version.json",
|
||
|
|
]);
|
||
|
|
|
||
|
|
function normalizeRelativePath(filePath) {
|
||
|
|
return filePath.split(path.sep).join("/");
|
||
|
|
}
|
||
|
|
|
||
|
|
function comparePaths(left, right) {
|
||
|
|
return left < right ? -1 : left > right ? 1 : 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
function listSourceFiles(repositoryRoot, relativeDirectory) {
|
||
|
|
const files = [];
|
||
|
|
const absoluteDirectory = path.join(repositoryRoot, relativeDirectory);
|
||
|
|
assert.ok(
|
||
|
|
fs.statSync(absoluteDirectory).isDirectory(),
|
||
|
|
`Missing userscript build input directory ${relativeDirectory}.`,
|
||
|
|
);
|
||
|
|
|
||
|
|
function visit(directory, relative) {
|
||
|
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
||
|
|
const entryRelativePath = path.join(relative, entry.name);
|
||
|
|
if (entry.isDirectory()) visit(path.join(directory, entry.name), entryRelativePath);
|
||
|
|
else if (entry.isFile() && !TEST_FILE_PATTERN.test(entry.name)) {
|
||
|
|
files.push(normalizeRelativePath(entryRelativePath));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
visit(absoluteDirectory, relativeDirectory);
|
||
|
|
return files;
|
||
|
|
}
|
||
|
|
|
||
|
|
function collectUserscriptBuildInputs(repositoryRoot) {
|
||
|
|
const root = path.resolve(repositoryRoot);
|
||
|
|
const inputs = [
|
||
|
|
...STATIC_BUILD_INPUTS,
|
||
|
|
...listSourceFiles(root, path.join("Extensions", "UserScript", "src")),
|
||
|
|
...listSourceFiles(root, path.join("Extensions", "common")),
|
||
|
|
];
|
||
|
|
const uniqueInputs = [...new Set(inputs.map(normalizeRelativePath))].sort(comparePaths);
|
||
|
|
for (const relativePath of uniqueInputs) {
|
||
|
|
assert.ok(fs.statSync(path.join(root, relativePath)).isFile(), `Missing userscript build input ${relativePath}.`);
|
||
|
|
}
|
||
|
|
return uniqueInputs;
|
||
|
|
}
|
||
|
|
|
||
|
|
function hashFiles(repositoryRoot, relativePaths) {
|
||
|
|
const root = path.resolve(repositoryRoot);
|
||
|
|
const hash = crypto.createHash("sha256");
|
||
|
|
for (const relativePath of relativePaths) {
|
||
|
|
const contents = fs.readFileSync(path.join(root, relativePath));
|
||
|
|
hash.update(relativePath, "utf8");
|
||
|
|
hash.update("\0", "utf8");
|
||
|
|
hash.update(String(contents.length), "utf8");
|
||
|
|
hash.update("\0", "utf8");
|
||
|
|
hash.update(contents);
|
||
|
|
hash.update("\0", "utf8");
|
||
|
|
}
|
||
|
|
return hash.digest("hex");
|
||
|
|
}
|
||
|
|
|
||
|
|
function sha256(filePath) {
|
||
|
|
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
||
|
|
}
|
||
|
|
|
||
|
|
function createUserscriptBuildReceipt(repositoryRoot, { artifactPath, buildId = "", liveTestBuild, mode }) {
|
||
|
|
assert.equal(typeof mode, "string", "The userscript build mode must be a string.");
|
||
|
|
assert.equal(typeof liveTestBuild, "boolean", "The userscript receipt must declare whether it is a live build.");
|
||
|
|
if (liveTestBuild) {
|
||
|
|
assert.match(buildId, LIVE_BUILD_ID_PATTERN, "A live userscript receipt requires an exact build ID.");
|
||
|
|
} else {
|
||
|
|
assert.equal(buildId, "", "A production userscript receipt must not contain a live build ID.");
|
||
|
|
}
|
||
|
|
const root = path.resolve(repositoryRoot);
|
||
|
|
const inputs = collectUserscriptBuildInputs(root);
|
||
|
|
return {
|
||
|
|
artifactPath: normalizeRelativePath(path.relative(root, path.resolve(artifactPath))),
|
||
|
|
buildId,
|
||
|
|
inputHash: hashFiles(root, inputs),
|
||
|
|
inputHashAlgorithm: "sha256",
|
||
|
|
inputs,
|
||
|
|
liveTestBuild,
|
||
|
|
mode,
|
||
|
|
schemaVersion: USERSCRIPT_BUILD_RECEIPT_SCHEMA_VERSION,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function writeUserscriptBuildReceipt(receiptPath, artifactPath, receipt) {
|
||
|
|
const completedReceipt = { ...receipt, artifactHash: sha256(artifactPath) };
|
||
|
|
fs.mkdirSync(path.dirname(receiptPath), { recursive: true });
|
||
|
|
fs.writeFileSync(receiptPath, `${JSON.stringify(completedReceipt, null, 2)}\n`);
|
||
|
|
return completedReceipt;
|
||
|
|
}
|
||
|
|
|
||
|
|
function verifyUserscriptBuildReceipt({
|
||
|
|
artifactPath,
|
||
|
|
expectedBuildId = "",
|
||
|
|
liveTestBuild,
|
||
|
|
receiptPath,
|
||
|
|
repositoryRoot,
|
||
|
|
}) {
|
||
|
|
const root = path.resolve(repositoryRoot);
|
||
|
|
assert.ok(fs.existsSync(receiptPath), `Missing userscript build receipt ${receiptPath}; rebuild before testing.`);
|
||
|
|
const receipt = JSON.parse(fs.readFileSync(receiptPath, "utf8"));
|
||
|
|
assert.equal(
|
||
|
|
receipt.schemaVersion,
|
||
|
|
USERSCRIPT_BUILD_RECEIPT_SCHEMA_VERSION,
|
||
|
|
"The userscript build receipt schema is invalid.",
|
||
|
|
);
|
||
|
|
assert.equal(receipt.mode, "production", "The userscript artifact was not produced in production mode.");
|
||
|
|
assert.equal(receipt.liveTestBuild, liveTestBuild, "The userscript receipt belongs to the wrong artifact kind.");
|
||
|
|
assert.equal(receipt.buildId, expectedBuildId, "The userscript receipt belongs to a different live build.");
|
||
|
|
assert.equal(receipt.inputHashAlgorithm, "sha256", "The userscript build receipt uses an unknown hash algorithm.");
|
||
|
|
assert.equal(
|
||
|
|
receipt.artifactPath,
|
||
|
|
normalizeRelativePath(path.relative(root, path.resolve(artifactPath))),
|
||
|
|
"The userscript receipt belongs to a different artifact path.",
|
||
|
|
);
|
||
|
|
|
||
|
|
const current = createUserscriptBuildReceipt(root, {
|
||
|
|
artifactPath,
|
||
|
|
buildId: expectedBuildId,
|
||
|
|
liveTestBuild,
|
||
|
|
mode: "production",
|
||
|
|
});
|
||
|
|
assert.deepEqual(receipt.inputs, current.inputs, "Userscript build inputs changed after the artifact was generated.");
|
||
|
|
assert.equal(
|
||
|
|
receipt.inputHash,
|
||
|
|
current.inputHash,
|
||
|
|
"Userscript build inputs changed after the artifact was generated; rebuild before testing.",
|
||
|
|
);
|
||
|
|
assert.ok(fs.statSync(artifactPath).isFile(), `Missing generated userscript artifact ${artifactPath}.`);
|
||
|
|
assert.equal(receipt.artifactHash, sha256(artifactPath), "The generated userscript differs from its build receipt.");
|
||
|
|
return receipt;
|
||
|
|
}
|
||
|
|
|
||
|
|
class UserscriptBuildReceiptPlugin {
|
||
|
|
constructor({ artifactPath, buildId, liveTestBuild, mode, receiptPath, repositoryRoot }) {
|
||
|
|
this.artifactPath = artifactPath;
|
||
|
|
this.buildId = buildId;
|
||
|
|
this.liveTestBuild = liveTestBuild;
|
||
|
|
this.mode = mode;
|
||
|
|
this.receipt = null;
|
||
|
|
this.receiptPath = receiptPath;
|
||
|
|
this.repositoryRoot = repositoryRoot;
|
||
|
|
}
|
||
|
|
|
||
|
|
apply(compiler) {
|
||
|
|
compiler.hooks.beforeCompile.tap("UserscriptBuildReceiptPlugin", () => {
|
||
|
|
const outputPath = compiler.options.output?.path;
|
||
|
|
const outputFilename = compiler.options.output?.filename;
|
||
|
|
const emittedArtifactPath =
|
||
|
|
typeof outputPath === "string" && typeof outputFilename === "string"
|
||
|
|
? path.resolve(outputPath, outputFilename)
|
||
|
|
: null;
|
||
|
|
|
||
|
|
this.receipt = null;
|
||
|
|
if (emittedArtifactPath !== path.resolve(this.artifactPath)) return;
|
||
|
|
|
||
|
|
this.receipt = createUserscriptBuildReceipt(this.repositoryRoot, this);
|
||
|
|
});
|
||
|
|
compiler.hooks.afterEmit.tap("UserscriptBuildReceiptPlugin", () => {
|
||
|
|
if (!this.receipt) return;
|
||
|
|
writeUserscriptBuildReceipt(this.receiptPath, this.artifactPath, this.receipt);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = {
|
||
|
|
USERSCRIPT_ARTIFACT_RELATIVE_PATH,
|
||
|
|
USERSCRIPT_BUILD_RECEIPT_FILENAME,
|
||
|
|
USERSCRIPT_BUILD_RECEIPT_RELATIVE_PATH,
|
||
|
|
USERSCRIPT_BUILD_RECEIPT_SCHEMA_VERSION,
|
||
|
|
USERSCRIPT_LIVE_ARTIFACT_RELATIVE_PATH,
|
||
|
|
USERSCRIPT_LIVE_BUILD_RECEIPT_RELATIVE_PATH,
|
||
|
|
UserscriptBuildReceiptPlugin,
|
||
|
|
collectUserscriptBuildInputs,
|
||
|
|
createUserscriptBuildReceipt,
|
||
|
|
verifyUserscriptBuildReceipt,
|
||
|
|
writeUserscriptBuildReceipt,
|
||
|
|
};
|