"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 };