feat(plugins,host): pluginRoot officiel transmis au contexte activate(ctx) des plugins — #285 (domain ports + infrastructure plugin: racine du package installé résolue depuis la même source de vérité que la substitution manifest ${pluginRoot}; application runtime_catalog + slash_command; backend dto + app-tauri dto_plugins; frontend domain + plugins runtime loader transmettent pluginRoot au contexte activé; bump pointeur IdeaSDK e3a0f46 via feature/285-plugin-root-runtime-api; QA verte: cargo check domain/application/infrastructure/backend/app-tauri, runtime_catalog 5/5, plugin_install_load 5/5, dto_plugins 2/2, frontend typecheck + 33 tests/3 fichiers, cargo fmt --check, git diff --check; réserves: rebuild AppImage + validation live ctx.pluginRoot dans le binaire actif restants)
This commit is contained in:
@ -2004,6 +2004,8 @@ export interface PluginRuntimePlugin {
|
||||
*/
|
||||
activationScope?: "app" | "project";
|
||||
bundleUrl: string;
|
||||
/** Canonical host-local root of the active installed plugin package. */
|
||||
pluginRoot: string;
|
||||
iconUrl?: string;
|
||||
contentHash: string;
|
||||
contributes: PluginContributionDto;
|
||||
|
||||
@ -23,6 +23,7 @@ function runtimePlugin(overrides: Partial<PluginRuntimePlugin> = {}): PluginRunt
|
||||
displayName: "Refreshable v1",
|
||||
version: "1.0.0",
|
||||
bundleUrl: dataUrl("export function activate() {}"),
|
||||
pluginRoot: "/data/idea/plugins/installed/dev.acme.refreshable",
|
||||
contentHash: "v1",
|
||||
contributes: { menus: [], menuItems: [], layouts: [], mcpServers: [] },
|
||||
...overrides,
|
||||
|
||||
@ -101,6 +101,7 @@ class ProjectScopedRuntimeCatalogPluginGateway extends MockPluginGateway {
|
||||
version: "1.0.0",
|
||||
activationScope: "project",
|
||||
bundleUrl: this.bundleUrl,
|
||||
pluginRoot: "/data/idea/plugins/installed/dev.acme.project-plugin",
|
||||
contentHash: "project-plugin",
|
||||
contributes: { menus: [], menuItems: [], layouts: [], mcpServers: [] },
|
||||
},
|
||||
@ -127,6 +128,7 @@ class MixedActivationScopeRuntimeCatalogPluginGateway extends MockPluginGateway
|
||||
capabilities: ["tooling"],
|
||||
activationScope: "app",
|
||||
bundleUrl: this.failingAppBundleUrl,
|
||||
pluginRoot: "/data/idea/plugins/installed/dev.acme.app-needs-project",
|
||||
contentHash: "app-needs-project",
|
||||
contributes: { menus: [], menuItems: [], layouts: [], mcpServers: [] },
|
||||
},
|
||||
@ -136,6 +138,7 @@ class MixedActivationScopeRuntimeCatalogPluginGateway extends MockPluginGateway
|
||||
version: "1.0.0",
|
||||
activationScope: "project",
|
||||
bundleUrl: this.projectBundleUrl,
|
||||
pluginRoot: "/data/idea/plugins/installed/dev.acme.project-plugin",
|
||||
contentHash: "project-plugin",
|
||||
contributes: { menus: [], menuItems: [], layouts: [], mcpServers: [] },
|
||||
},
|
||||
|
||||
@ -29,6 +29,7 @@ function entry(overrides: Partial<PluginRuntimePlugin> & { bundleUrl: string }):
|
||||
id: "mock.plugin",
|
||||
displayName: "Mock Plugin",
|
||||
version: "1.0.0",
|
||||
pluginRoot: "/data/idea/plugins/installed/mock.plugin",
|
||||
contentHash: "abc123",
|
||||
contributes: emptyContributes(),
|
||||
...overrides,
|
||||
@ -39,17 +40,25 @@ describe("loadPlugins", () => {
|
||||
it("loads a well-formed plugin bundle and calls activate(ctx)", async () => {
|
||||
const bundle = dataUrl(`
|
||||
export function activate(ctx) {
|
||||
globalThis.__activatedWith = ctx.pluginId;
|
||||
globalThis.__activatedWith = { pluginId: ctx.pluginId, pluginRoot: ctx.pluginRoot };
|
||||
return {};
|
||||
}
|
||||
`);
|
||||
const { registry, failures } = await loadPlugins(
|
||||
[entry({ id: "dev.acme.one", displayName: "One", bundleUrl: bundle })],
|
||||
[entry({
|
||||
id: "dev.acme.one",
|
||||
displayName: "One",
|
||||
bundleUrl: bundle,
|
||||
pluginRoot: "/data/idea/plugins/installed/dev.acme.one",
|
||||
})],
|
||||
gateways,
|
||||
);
|
||||
expect(failures).toEqual([]);
|
||||
expect(registry.list().map((p) => p.pluginId)).toEqual(["dev.acme.one"]);
|
||||
expect((globalThis as Record<string, unknown>).__activatedWith).toBe("dev.acme.one");
|
||||
expect((globalThis as Record<string, unknown>).__activatedWith).toEqual({
|
||||
pluginId: "dev.acme.one",
|
||||
pluginRoot: "/data/idea/plugins/installed/dev.acme.one",
|
||||
});
|
||||
});
|
||||
|
||||
it("loads an SDK-style default export containing activate(ctx)", async () => {
|
||||
@ -242,6 +251,7 @@ describe("loadPlugins", () => {
|
||||
"menu",
|
||||
"pluginDisplayName",
|
||||
"pluginId",
|
||||
"pluginRoot",
|
||||
"services",
|
||||
"storage",
|
||||
"subscriptions",
|
||||
|
||||
@ -38,6 +38,7 @@ export type { PluginGatewaySet } from "./registry";
|
||||
|
||||
export interface IdeaPluginContext {
|
||||
pluginId: string;
|
||||
pluginRoot: string;
|
||||
pluginDisplayName: string;
|
||||
version: string;
|
||||
logger: PluginLogger;
|
||||
@ -270,6 +271,7 @@ async function loadOne(
|
||||
const pluginId = safePluginId(entry);
|
||||
const displayName = nonEmptyString(entryObject.displayName) ?? pluginId;
|
||||
const version = nonEmptyString(entryObject.version) ?? "";
|
||||
const pluginRoot = nonEmptyString(entryObject.pluginRoot);
|
||||
let activation: void | PluginActivation = undefined;
|
||||
const subscriptions: Disposable[] = [];
|
||||
try {
|
||||
@ -280,6 +282,9 @@ async function loadOne(
|
||||
if (!bundleUrl) {
|
||||
throw new Error("missing plugin bundle URL");
|
||||
}
|
||||
if (!pluginRoot) {
|
||||
throw new Error("missing installed plugin root");
|
||||
}
|
||||
|
||||
installHostReactImportMap();
|
||||
const mod = resolveIdeaPluginModule(
|
||||
@ -308,6 +313,7 @@ async function loadOne(
|
||||
|
||||
const ctx: IdeaPluginContext = {
|
||||
pluginId,
|
||||
pluginRoot,
|
||||
pluginDisplayName: displayName,
|
||||
version,
|
||||
logger: createPluginLogger(entry),
|
||||
|
||||
Reference in New Issue
Block a user