mirror of
https://github.com/Anarios/return-youtube-dislike.git
synced 2026-09-12 10:22:10 +02:00
e500088e65
Add Firefox data consent, repair contributor sign-in, and introduce optional Hide clutter buttons. Harden video navigation, voting, and action layouts with shared browser regression coverage and reproducible release packaging.
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,
|
|
};
|