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>
This commit is contained in:
2026-08-03 12:28:30 +02:00
parent c322055edb
commit 6bca9cc4c0
9 changed files with 269 additions and 158 deletions

View File

@ -1,4 +1,4 @@
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
const pluginRoot = join(process.cwd(), "examples", "hello-plugin");
@ -8,9 +8,13 @@ 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 },
{ archivePath: main, sourcePath: join(pluginRoot, main) },
...distEntries,
{ archivePath: "README.md", sourcePath: join(pluginRoot, "README.md") }
];
const DOS_TIME_MIDNIGHT = 0;
@ -43,6 +47,23 @@ for (const entry of archiveEntries) {
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`);