fix(#105): corrige le contexte plugin pour hello-plugin

Ajoute logger, subscriptions et commandes scindées au contexte plugin
activé. Les plugins full-trust s'attendent à ce shape public.
This commit is contained in:
2026-07-29 17:59:14 +02:00
parent 3f27eb878b
commit 2c3a46e690
22 changed files with 1082 additions and 12 deletions

View File

@ -117,6 +117,47 @@ describe("loadPlugins", () => {
expect((globalThis as Record<string, unknown>).__ranCommand).toBe(true);
});
it("loads plugins built against the public SDK context shape", async () => {
const bundle = dataUrl(`
export function activate(ctx) {
ctx.logger.info("hello");
const disposable = ctx.commands.registerCommand("hello-plugin.sayHello", () => {
globalThis.__helloCommandRan = true;
return "Hello from IdeA";
});
ctx.subscriptions.push(disposable);
}
`);
const { registry, failures } = await loadPlugins(
[
entry({
id: "com.example.hello-plugin",
displayName: "Hello Plugin",
bundleUrl: bundle,
contributes: {
...emptyContributes(),
menuItems: [
{
id: "hello-plugin.sayHello.item",
targetMenuId: "plugin:hello-plugin.menu",
label: "Say Hello",
command: "hello-plugin.sayHello",
},
],
},
}),
],
gateways,
);
expect(failures).toEqual([]);
await registry.runCommand("com.example.hello-plugin", "hello-plugin.sayHello");
expect((globalThis as Record<string, unknown>).__helloCommandRan).toBe(true);
await registry.remove("com.example.hello-plugin");
expect(registry.get("com.example.hello-plugin")).toBeUndefined();
});
it("calls dispose() on removal (best-effort)", async () => {
const bundle = dataUrl(`
export function activate(ctx) {

View File

@ -21,6 +21,7 @@ import {
PluginLayoutRegistry,
PluginMenuRegistry,
PluginRuntimeRegistry,
type Disposable,
type LoadedPlugin,
type PluginGatewaySet,
} from "./registry";
@ -31,7 +32,9 @@ export interface IdeaPluginContext extends PluginGatewaySet {
pluginId: string;
pluginDisplayName: string;
version: string;
commands: PluginCommandRegistry;
logger: PluginLogger;
subscriptions: Disposable[];
commands: PluginCommandContext;
layouts: PluginLayoutRegistry;
menu: PluginMenuRegistry;
}
@ -40,8 +43,23 @@ export interface PluginActivation {
dispose?: () => void | Promise<void>;
}
export interface PluginLogger {
debug(message: string, ...args: unknown[]): void;
info(message: string, ...args: unknown[]): void;
warn(message: string, ...args: unknown[]): void;
error(message: string, ...args: unknown[]): void;
}
export interface PluginCommandContext {
register(commandId: string, handler: (...args: unknown[]) => void | Promise<void>): Disposable;
registerCommand(
commandId: string,
handler: (...args: unknown[]) => unknown | Promise<unknown>,
): Disposable;
}
export interface IdeaPluginModule {
activate(ctx: IdeaPluginContext): PluginActivation | Promise<PluginActivation>;
activate(ctx: IdeaPluginContext): void | PluginActivation | Promise<void | PluginActivation>;
}
export interface PluginLoadFailure {
@ -63,6 +81,42 @@ function isIdeaPluginModule(mod: unknown): mod is IdeaPluginModule {
);
}
function createPluginLogger(entry: PluginRuntimePlugin): PluginLogger {
const prefix = `[plugin:${entry.id}]`;
return {
debug: (message, ...args) => console.debug(prefix, message, ...args),
info: (message, ...args) => console.info(prefix, message, ...args),
warn: (message, ...args) => console.warn(prefix, message, ...args),
error: (message, ...args) => console.error(prefix, message, ...args),
};
}
function createCommandContext(commands: PluginCommandRegistry): PluginCommandContext {
return {
register: (commandId, handler) => commands.register(commandId, handler),
registerCommand: (commandId, handler) =>
commands.register(commandId, async (...args) => {
await handler(...args);
}),
};
}
async function disposeAll(disposables: Disposable[], activation?: void | PluginActivation): Promise<void> {
try {
await activation?.dispose?.();
} catch {
/* best-effort */
}
for (const disposable of disposables.splice(0).reverse()) {
try {
disposable.dispose();
} catch {
/* best-effort */
}
}
}
async function loadOne(
entry: PluginRuntimePlugin,
gateways: PluginGatewaySet,
@ -86,12 +140,15 @@ async function loadOne(
const commands = new PluginCommandRegistry(entry.id, declaredCommandIds);
const layouts = new PluginLayoutRegistry(entry.id, declaredLayoutTypes);
const menu = new PluginMenuRegistry(entry.id);
const subscriptions: Disposable[] = [];
const ctx: IdeaPluginContext = {
pluginId: entry.id,
pluginDisplayName: entry.displayName,
version: entry.version,
commands,
logger: createPluginLogger(entry),
subscriptions,
commands: createCommandContext(commands),
layouts,
menu,
...gateways,
@ -107,13 +164,9 @@ async function loadOne(
layouts,
menu,
dispose: async () => {
// Best-effort, full-trust (carnet §1.3) — a broken `dispose()` must not
// prevent removing the plugin from the runtime registry.
try {
await activation.dispose?.();
} catch {
/* best-effort */
}
// Best-effort, full-trust (carnet §1.3) — a broken `dispose()` or
// subscription must not prevent removing the plugin from the registry.
await disposeAll(subscriptions, activation);
},
};
return { plugin };