207 lines
6.3 KiB
JavaScript
207 lines
6.3 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 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 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 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, version: requestedVersion }) {
|
|
const packageMetadata = JSON.parse(
|
|
await readFile(path.join(rootDirectory, "package.json"), "utf8")
|
|
);
|
|
const version = normalizeVersion(requestedVersion ?? packageMetadata.version);
|
|
const entries = [];
|
|
|
|
for (const relativePath of ARCHIVE_FILES) {
|
|
let contents = await readFile(path.join(rootDirectory, relativePath));
|
|
|
|
if (relativePath === "manifest.json") {
|
|
const manifest = JSON.parse(contents.toString("utf8"));
|
|
manifest.version = version;
|
|
contents = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
|
|
}
|
|
|
|
entries.push({ name: relativePath, contents });
|
|
}
|
|
|
|
const archive = createZip(entries);
|
|
const archiveName = `glagolify-${version}.xpi`;
|
|
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, version };
|
|
}
|
|
|
|
function parseArguments(arguments_) {
|
|
let outputDirectory = "dist";
|
|
let version;
|
|
|
|
for (let index = 0; index < arguments_.length; index += 1) {
|
|
const argument = arguments_[index];
|
|
|
|
if (argument === "--version" || argument === "--output-dir") {
|
|
const value = arguments_[index + 1];
|
|
if (!value || value.startsWith("--")) {
|
|
throw new Error(`${argument} requires a value.`);
|
|
}
|
|
|
|
if (argument === "--version") {
|
|
version = value;
|
|
} else {
|
|
outputDirectory = value;
|
|
}
|
|
index += 1;
|
|
} else {
|
|
throw new Error(`Unknown argument: ${argument}`);
|
|
}
|
|
}
|
|
|
|
return { outputDirectory, version };
|
|
}
|
|
|
|
async function main() {
|
|
const rootDirectory = path.resolve(__dirname, "..");
|
|
const options = parseArguments(process.argv.slice(2));
|
|
const result = await packageExtension({
|
|
rootDirectory,
|
|
outputDirectory: path.resolve(rootDirectory, options.outputDirectory),
|
|
version: options.version
|
|
});
|
|
|
|
process.stdout.write(
|
|
`Created ${path.relative(rootDirectory, result.archivePath)} for version ${result.version}\n` +
|
|
`SHA-256 ${result.digest}\n`
|
|
);
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main().catch((error) => {
|
|
console.error(error.message);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|
|
|
|
module.exports = { ARCHIVE_FILES, normalizeVersion, packageExtension };
|