import { createWriteStream, readdirSync, statSync, readFileSync, mkdirSync } from "fs"; import { join, relative } from "path"; import { createDeflateRaw } from "zlib"; const pkg = JSON.parse(readFileSync("package.json", "utf8")); const version = pkg.version; mkdirSync("artifacts", { recursive: true }); for (const browser of ["chrome", "firefox"]) { const distDir = `dist/${browser}`; const outPath = `artifacts/yt-playlist-features-${version}-${browser}.zip`; await writeZip(distDir, outPath); console.log(`Created ${outPath}`); } /** * Create a ZIP file from a directory. * Pure Node.js implementation — no external dependencies. */ async function writeZip(dir, outPath) { const files = collectFiles(dir); const stream = createWriteStream(outPath); const entries = []; let offset = 0; for (const filePath of files) { const data = readFileSync(filePath); const name = relative(dir, filePath).replace(/\\/g, "/"); const nameBytes = Buffer.from(name, "utf8"); const compressed = await deflate(data); const crc = crc32(data); const localHeader = Buffer.alloc(30); localHeader.writeUInt32LE(0x04034b50, 0); // signature localHeader.writeUInt16LE(20, 4); // version needed localHeader.writeUInt16LE(0, 6); // flags localHeader.writeUInt16LE(8, 8); // compression: deflate localHeader.writeUInt16LE(0, 10); // mod time localHeader.writeUInt16LE(0, 12); // mod date localHeader.writeUInt32LE(crc, 14); localHeader.writeUInt32LE(compressed.length, 18); localHeader.writeUInt32LE(data.length, 22); localHeader.writeUInt16LE(nameBytes.length, 26); localHeader.writeUInt16LE(0, 28); // extra field length const localHeaderOffset = offset; stream.write(localHeader); stream.write(nameBytes); stream.write(compressed); offset += localHeader.length + nameBytes.length + compressed.length; entries.push({ nameBytes, crc, compressed, data, localHeaderOffset }); } const centralStart = offset; for (const entry of entries) { const cdHeader = Buffer.alloc(46); cdHeader.writeUInt32LE(0x02014b50, 0); // signature cdHeader.writeUInt16LE(20, 4); // version made by cdHeader.writeUInt16LE(20, 6); // version needed cdHeader.writeUInt16LE(0, 8); // flags cdHeader.writeUInt16LE(8, 10); // compression cdHeader.writeUInt16LE(0, 12); // mod time cdHeader.writeUInt16LE(0, 14); // mod date cdHeader.writeUInt32LE(entry.crc, 16); cdHeader.writeUInt32LE(entry.compressed.length, 20); cdHeader.writeUInt32LE(entry.data.length, 24); cdHeader.writeUInt16LE(entry.nameBytes.length, 28); cdHeader.writeUInt16LE(0, 30); // extra field length cdHeader.writeUInt16LE(0, 32); // comment length cdHeader.writeUInt16LE(0, 34); // disk start cdHeader.writeUInt16LE(0, 36); // internal attrs cdHeader.writeUInt32LE(0, 38); // external attrs cdHeader.writeUInt32LE(entry.localHeaderOffset, 42); stream.write(cdHeader); stream.write(entry.nameBytes); offset += cdHeader.length + entry.nameBytes.length; } const centralSize = offset - centralStart; const eocd = Buffer.alloc(22); eocd.writeUInt32LE(0x06054b50, 0); // signature eocd.writeUInt16LE(0, 4); // disk number eocd.writeUInt16LE(0, 6); // central dir disk eocd.writeUInt16LE(entries.length, 8); eocd.writeUInt16LE(entries.length, 10); eocd.writeUInt32LE(centralSize, 12); eocd.writeUInt32LE(centralStart, 16); eocd.writeUInt16LE(0, 20); // comment length stream.write(eocd); await new Promise((resolve) => stream.end(resolve)); } function collectFiles(dir) { const results = []; for (const entry of readdirSync(dir)) { const full = join(dir, entry); if (statSync(full).isDirectory()) { results.push(...collectFiles(full)); } else { results.push(full); } } return results.sort(); } function deflate(data) { return new Promise((resolve, reject) => { const chunks = []; const deflater = createDeflateRaw(); deflater.on("data", (chunk) => chunks.push(chunk)); deflater.on("end", () => resolve(Buffer.concat(chunks))); deflater.on("error", reject); deflater.end(data); }); } function crc32(buf) { let crc = 0xffffffff; for (let i = 0; i < buf.length; i++) { crc ^= buf[i]; for (let j = 0; j < 8; j++) { crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0); } } return (crc ^ 0xffffffff) >>> 0; }