Files
IdeaSDK/scripts/package-hello-plugin.mjs
Blomios 6bca9cc4c0 feat(sdk): ESM multi-fichiers + storage plugin-owned (#134/#139)
Documente et illustre deux contrats SDK :

- Multi-fichiers ESM : le `main` du manifeste peut importer d'autres
  fichiers du package via specifiers relatifs, servis par IdeA sur
  `idea-plugin://` (build `tsc` non bundlé). Le packager embarque tout
  `dist/**/*.js` et vérifie la présence du `main`. Les bare specifiers
  (`node_modules`) restent hors contrat : à bundler ou vendorer.
- Storage plugin-owned : `ctx.storage` est la place canonique de l'état
  interne du plugin (compteurs, flags, préférences, caches), hors des
  fichiers projet. Les APIs workspace/config restent pour le contenu
  project-owned.

L'exemple hello-plugin est éclaté en modules (constants, core/layout,
core/workspace, core/storage) pour exercer l'import relatif et le storage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 12:28:30 +02:00

146 lines
5.3 KiB
JavaScript

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