Files
glagolify/scripts/package-extension.js
T
StanPonomarev df16075421
Release Firefox and Chromium extensions / Package and attach browser extensions (release) Successful in 54s
chromium support
2026-08-30 21:00:05 +02:00

290 lines
8.6 KiB
JavaScript

"use strict";
const { createHash } = require("node:crypto");
const { mkdir, readFile, writeFile } = require("node:fs/promises");
const path = require("node:path");
const { deflateRawSync } = require("node:zlib");
const FIREFOX_ARCHIVE_FILES = Object.freeze([
"background.js",
"content.css",
"content.js",
"fonts/NotoSansGlagolitic-Regular.ttf",
"fonts/OFL.txt",
"fonts/Shafarik-OFL.txt",
"fonts/Shafarik-Regular.ttf",
"icons/icon-16.svg",
"icons/icon-32.svg",
"icons/icon-48.svg",
"icons/icon-96.svg",
"manifest.json",
"popup.css",
"popup.html",
"popup.js",
"transliterate.js"
]);
const CHROMIUM_ARCHIVE_FILES = Object.freeze([
...FIREFOX_ARCHIVE_FILES.map((relativePath) =>
relativePath.endsWith(".svg") ? relativePath.replace(/\.svg$/, ".png") : relativePath
),
"icons/icon-128.png",
"icons/icon-48.svg"
]);
const TARGETS = new Set(["firefox", "chromium"]);
const VERSION_PATTERN = /^(0|[1-9][0-9]{0,8})(\.(0|[1-9][0-9]{0,8})){0,3}$/;
const UTF8_FLAG = 0x0800;
const DEFLATE_METHOD = 8;
const DOS_TIME = 0;
const DOS_DATE = (1 << 5) | 1;
function normalizeVersion(value) {
if (typeof value !== "string" || value.length === 0) {
throw new Error("A package or release version is required.");
}
const version = /^[vV]/.test(value) ? value.slice(1) : value;
if (!VERSION_PATTERN.test(version)) {
throw new Error(
`Invalid Mozilla extension version ${JSON.stringify(value)}. ` +
"Use one to four dot-separated integers without leading zeroes; a leading v is allowed."
);
}
return version;
}
function createManifest(contents, version, target) {
const manifest = JSON.parse(contents.toString("utf8"));
manifest.version = version;
if (target === "firefox") {
return manifest;
}
if (version.split(".").some((part) => Number(part) > 65535)) {
throw new Error(`Chromium extension version parts cannot exceed 65535: ${version}`);
}
manifest.manifest_version = 3;
manifest.minimum_chrome_version = "102";
delete manifest.browser_specific_settings;
manifest.host_permissions = manifest.permissions.filter(
(permission) => permission === "<all_urls>" || permission.includes("://")
);
manifest.permissions = manifest.permissions.filter(
(permission) => !manifest.host_permissions.includes(permission)
);
manifest.background = { service_worker: "background.js" };
manifest.action = manifest.browser_action;
delete manifest.browser_action;
manifest.web_accessible_resources = [
{
resources: manifest.web_accessible_resources,
matches: ["<all_urls>"]
}
];
for (const icons of [manifest.icons, manifest.action.default_icon]) {
for (const size of Object.keys(icons)) {
icons[size] = icons[size].replace(/\.svg$/, ".png");
}
}
manifest.icons["128"] = "icons/icon-128.png";
return manifest;
}
function crc32(buffer) {
let crc = 0xffffffff;
for (const byte of buffer) {
crc ^= byte;
for (let bit = 0; bit < 8; bit += 1) {
crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
}
}
return (crc ^ 0xffffffff) >>> 0;
}
function createZip(entries) {
const localRecords = [];
const centralRecords = [];
let offset = 0;
for (const entry of entries) {
const name = Buffer.from(entry.name, "utf8");
const compressed = deflateRawSync(entry.contents, { level: 9 });
const checksum = crc32(entry.contents);
const localHeader = Buffer.alloc(30);
localHeader.writeUInt32LE(0x04034b50, 0);
localHeader.writeUInt16LE(20, 4);
localHeader.writeUInt16LE(UTF8_FLAG, 6);
localHeader.writeUInt16LE(DEFLATE_METHOD, 8);
localHeader.writeUInt16LE(DOS_TIME, 10);
localHeader.writeUInt16LE(DOS_DATE, 12);
localHeader.writeUInt32LE(checksum, 14);
localHeader.writeUInt32LE(compressed.length, 18);
localHeader.writeUInt32LE(entry.contents.length, 22);
localHeader.writeUInt16LE(name.length, 26);
localHeader.writeUInt16LE(0, 28);
const localRecord = Buffer.concat([localHeader, name, compressed]);
localRecords.push(localRecord);
const centralHeader = Buffer.alloc(46);
centralHeader.writeUInt32LE(0x02014b50, 0);
centralHeader.writeUInt16LE(0x0314, 4);
centralHeader.writeUInt16LE(20, 6);
centralHeader.writeUInt16LE(UTF8_FLAG, 8);
centralHeader.writeUInt16LE(DEFLATE_METHOD, 10);
centralHeader.writeUInt16LE(DOS_TIME, 12);
centralHeader.writeUInt16LE(DOS_DATE, 14);
centralHeader.writeUInt32LE(checksum, 16);
centralHeader.writeUInt32LE(compressed.length, 20);
centralHeader.writeUInt32LE(entry.contents.length, 24);
centralHeader.writeUInt16LE(name.length, 28);
centralHeader.writeUInt16LE(0, 30);
centralHeader.writeUInt16LE(0, 32);
centralHeader.writeUInt16LE(0, 34);
centralHeader.writeUInt16LE(0, 36);
centralHeader.writeUInt32LE((0o100644 << 16) >>> 0, 38);
centralHeader.writeUInt32LE(offset, 42);
centralRecords.push(Buffer.concat([centralHeader, name]));
offset += localRecord.length;
}
const centralDirectory = Buffer.concat(centralRecords);
const endRecord = Buffer.alloc(22);
endRecord.writeUInt32LE(0x06054b50, 0);
endRecord.writeUInt16LE(0, 4);
endRecord.writeUInt16LE(0, 6);
endRecord.writeUInt16LE(entries.length, 8);
endRecord.writeUInt16LE(entries.length, 10);
endRecord.writeUInt32LE(centralDirectory.length, 12);
endRecord.writeUInt32LE(offset, 16);
endRecord.writeUInt16LE(0, 20);
return Buffer.concat([...localRecords, centralDirectory, endRecord]);
}
async function packageExtension({
rootDirectory,
outputDirectory,
target = "firefox",
version: requestedVersion
}) {
if (!TARGETS.has(target)) {
throw new Error(`Unknown extension target: ${target}`);
}
const packageMetadata = JSON.parse(
await readFile(path.join(rootDirectory, "package.json"), "utf8")
);
const version = normalizeVersion(requestedVersion ?? packageMetadata.version);
const archiveFiles =
target === "firefox" ? FIREFOX_ARCHIVE_FILES : CHROMIUM_ARCHIVE_FILES;
const entries = [];
for (const relativePath of archiveFiles) {
let contents = await readFile(path.join(rootDirectory, relativePath));
if (relativePath === "manifest.json") {
const manifest = createManifest(contents, version, target);
contents = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
}
entries.push({ name: relativePath, contents });
}
const archive = createZip(entries);
const archiveName =
target === "firefox"
? `glagolify-${version}.xpi`
: `glagolify-${version}-chromium.zip`;
const archivePath = path.join(outputDirectory, archiveName);
const checksumPath = `${archivePath}.sha256`;
const digest = createHash("sha256").update(archive).digest("hex");
await mkdir(outputDirectory, { recursive: true });
await writeFile(archivePath, archive);
await writeFile(checksumPath, `${digest} ${archiveName}\n`);
return { archiveName, archivePath, checksumPath, digest, target, version };
}
function parseArguments(arguments_) {
let outputDirectory = "dist";
let target = "all";
let version;
for (let index = 0; index < arguments_.length; index += 1) {
const argument = arguments_[index];
if (
argument === "--version" ||
argument === "--output-dir" ||
argument === "--target"
) {
const value = arguments_[index + 1];
if (!value || value.startsWith("--")) {
throw new Error(`${argument} requires a value.`);
}
if (argument === "--version") {
version = value;
} else if (argument === "--output-dir") {
outputDirectory = value;
} else {
target = value;
}
index += 1;
} else {
throw new Error(`Unknown argument: ${argument}`);
}
}
if (target !== "all" && !TARGETS.has(target)) {
throw new Error(`Unknown extension target: ${target}`);
}
return { outputDirectory, target, version };
}
async function main() {
const rootDirectory = path.resolve(__dirname, "..");
const options = parseArguments(process.argv.slice(2));
const targets = options.target === "all" ? [...TARGETS] : [options.target];
for (const target of targets) {
const result = await packageExtension({
rootDirectory,
outputDirectory: path.resolve(rootDirectory, options.outputDirectory),
target,
version: options.version
});
process.stdout.write(
`Created ${path.relative(rootDirectory, result.archivePath)} for ${target} ` +
`version ${result.version}\nSHA-256 ${result.digest}\n`
);
}
}
if (require.main === module) {
main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});
}
module.exports = {
CHROMIUM_ARCHIVE_FILES,
FIREFOX_ARCHIVE_FILES,
normalizeVersion,
packageExtension
};