import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; const pluginRoot = join(process.cwd(), "examples", "hello-plugin"); const manifestPath = join(pluginRoot, "idea-plugin.json"); const manifest = JSON.parse(await readFile(manifestPath, "utf8")); const main = requireString(manifest, "main"); const version = requireString(manifest, "version"); const archivePath = join(pluginRoot, "build", `hello-plugin-${version}.zip`); const distEntries = await collectFiles(join(pluginRoot, "dist"), "dist"); if (!distEntries.some((entry) => entry.archivePath === main)) { throw new Error(`Built plugin dist does not contain manifest main: ${main}`); } const archiveEntries = [ { archivePath: "idea-plugin.json", sourcePath: manifestPath }, ...distEntries, { archivePath: "README.md", sourcePath: join(pluginRoot, "README.md") } ]; const DOS_TIME_MIDNIGHT = 0; const DOS_DATE_1980_01_01 = 33; const CRC32_TABLE = Array.from({ length: 256 }, (_, index) => { let value = index; for (let bit = 0; bit < 8; bit += 1) { value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1; } return value >>> 0; }); await rm(join(pluginRoot, "build"), { recursive: true, force: true }); await mkdir(dirname(archivePath), { recursive: true }); const files = []; for (const entry of archiveEntries) { if (entry.archivePath.startsWith("/") || entry.archivePath.includes("..")) { throw new Error(`Refusing unsafe archive path: ${entry.archivePath}`); } files.push({ archivePath: entry.archivePath, data: await readFile(entry.sourcePath) }); } await writeFile(archivePath, createZip(files)); console.log(`created ${archivePath}`); async function collectFiles(sourceDir, archiveDir) { const entries = await readdir(sourceDir, { withFileTypes: true }); const files = []; for (const entry of entries) { const sourcePath = join(sourceDir, entry.name); const archivePath = `${archiveDir}/${entry.name}`; if (entry.isDirectory()) { files.push(...await collectFiles(sourcePath, archivePath)); } else if (entry.isFile()) { files.push({ archivePath, sourcePath }); } } return files.sort((left, right) => left.archivePath.localeCompare(right.archivePath)); } function requireString(record, key) { if (typeof record[key] !== "string" || record[key].trim().length === 0) { throw new Error(`idea-plugin.json field "${key}" must be a non-empty string`); } return record[key]; } function createZip(files) { const localFileHeaders = []; const centralDirectoryHeaders = []; let offset = 0; for (const file of files) { const filename = Buffer.from(file.archivePath, "utf8"); const checksum = crc32(file.data); const localFileHeader = Buffer.alloc(30); localFileHeader.writeUInt32LE(0x04034b50, 0); localFileHeader.writeUInt16LE(20, 4); localFileHeader.writeUInt16LE(0x0800, 6); localFileHeader.writeUInt16LE(0, 8); localFileHeader.writeUInt16LE(DOS_TIME_MIDNIGHT, 10); localFileHeader.writeUInt16LE(DOS_DATE_1980_01_01, 12); localFileHeader.writeUInt32LE(checksum, 14); localFileHeader.writeUInt32LE(file.data.length, 18); localFileHeader.writeUInt32LE(file.data.length, 22); localFileHeader.writeUInt16LE(filename.length, 26); localFileHeader.writeUInt16LE(0, 28); localFileHeaders.push(localFileHeader, filename, file.data); const centralDirectoryHeader = Buffer.alloc(46); centralDirectoryHeader.writeUInt32LE(0x02014b50, 0); centralDirectoryHeader.writeUInt16LE(20, 4); centralDirectoryHeader.writeUInt16LE(20, 6); centralDirectoryHeader.writeUInt16LE(0x0800, 8); centralDirectoryHeader.writeUInt16LE(0, 10); centralDirectoryHeader.writeUInt16LE(DOS_TIME_MIDNIGHT, 12); centralDirectoryHeader.writeUInt16LE(DOS_DATE_1980_01_01, 14); centralDirectoryHeader.writeUInt32LE(checksum, 16); centralDirectoryHeader.writeUInt32LE(file.data.length, 20); centralDirectoryHeader.writeUInt32LE(file.data.length, 24); centralDirectoryHeader.writeUInt16LE(filename.length, 28); centralDirectoryHeader.writeUInt16LE(0, 30); centralDirectoryHeader.writeUInt16LE(0, 32); centralDirectoryHeader.writeUInt16LE(0, 34); centralDirectoryHeader.writeUInt16LE(0, 36); centralDirectoryHeader.writeUInt32LE(0, 38); centralDirectoryHeader.writeUInt32LE(offset, 42); centralDirectoryHeaders.push(centralDirectoryHeader, filename); offset += localFileHeader.length + filename.length + file.data.length; } const centralDirectory = Buffer.concat(centralDirectoryHeaders); const endOfCentralDirectory = Buffer.alloc(22); endOfCentralDirectory.writeUInt32LE(0x06054b50, 0); endOfCentralDirectory.writeUInt16LE(0, 4); endOfCentralDirectory.writeUInt16LE(0, 6); endOfCentralDirectory.writeUInt16LE(files.length, 8); endOfCentralDirectory.writeUInt16LE(files.length, 10); endOfCentralDirectory.writeUInt32LE(centralDirectory.length, 12); endOfCentralDirectory.writeUInt32LE(offset, 16); endOfCentralDirectory.writeUInt16LE(0, 20); return Buffer.concat([...localFileHeaders, centralDirectory, endOfCentralDirectory]); } function crc32(data) { let value = 0xffffffff; for (const byte of data) { value = (value >>> 8) ^ CRC32_TABLE[(value ^ byte) & 0xff]; } return (value ^ 0xffffffff) >>> 0; }