Release Firefox and Chromium extensions / Package and attach browser extensions (release) Successful in 6s
305 lines
9.0 KiB
JavaScript
305 lines
9.0 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 (target === "chromium" && relativePath === "content.css") {
|
|
contents = Buffer.from(
|
|
contents
|
|
.toString("utf8")
|
|
.replaceAll(
|
|
'url("fonts/',
|
|
'url("chrome-extension://__MSG_@@extension_id__/fonts/'
|
|
)
|
|
.replaceAll(
|
|
"unicode-range: U+2C00-2C5F;",
|
|
"unicode-range: U+0301, U+2C00-2C5F;"
|
|
)
|
|
);
|
|
}
|
|
|
|
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
|
|
};
|