From 15f930dd3bb036709c40418e0426fdb4adf6ce42 Mon Sep 17 00:00:00 2001 From: Blomios Date: Tue, 4 Aug 2026 00:39:25 +0200 Subject: [PATCH] =?UTF-8?q?feat(runtime):=20r=C3=A9sout=20react/react-dom?= =?UTF-8?q?=20vers=20l'instance=20host=20pour=20les=20layouts=20plugin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le runtime SDK expose désormais react/react-dom résolus contre l'instance hébergée par IdeA plutôt qu'une copie embarquée, pour que les layouts plugin en JSX/hooks partagent le même arbre React que l'host. hello-plugin migre son layout d'exemple en .tsx pour illustrer le contrat. Co-Authored-By: Claude Opus 4.8 --- examples/hello-plugin/package-lock.json | 75 +++++++++++++++++++++++ examples/hello-plugin/package.json | 11 +++- examples/hello-plugin/src/core/layout.ts | 17 ----- examples/hello-plugin/src/core/layout.tsx | 58 ++++++++++++++++++ examples/hello-plugin/src/index.ts | 10 +++ examples/hello-plugin/tsconfig.build.json | 3 +- examples/hello-plugin/tsconfig.json | 2 + package-lock.json | 65 ++++++++++++++++++++ package.json | 10 ++- src/index.ts | 5 +- src/runtime.ts | 39 ++++++++++-- 11 files changed, 270 insertions(+), 25 deletions(-) delete mode 100644 examples/hello-plugin/src/core/layout.ts create mode 100644 examples/hello-plugin/src/core/layout.tsx diff --git a/examples/hello-plugin/package-lock.json b/examples/hello-plugin/package-lock.json index 9935509..0514481 100644 --- a/examples/hello-plugin/package-lock.json +++ b/examples/hello-plugin/package-lock.json @@ -9,6 +9,16 @@ "version": "0.1.0", "dependencies": { "@idea/plugin-sdk": "file:../.." + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "peerDependencies": { + "react": "^18.2.0 || ^19.0.0", + "react-dom": "^18.2.0 || ^19.0.0" } }, "../..": { @@ -16,12 +26,77 @@ "version": "0.1.0", "license": "MIT", "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", "typescript": "^5.5.0" + }, + "peerDependencies": { + "react": "^18.2.0 || ^19.0.0", + "react-dom": "^18.2.0 || ^19.0.0" } }, "node_modules/@idea/plugin-sdk": { "resolved": "../..", "link": true + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "dev": true, + "license": "MIT" } } } diff --git a/examples/hello-plugin/package.json b/examples/hello-plugin/package.json index 937f370..b8ae5b5 100644 --- a/examples/hello-plugin/package.json +++ b/examples/hello-plugin/package.json @@ -9,6 +9,15 @@ }, "dependencies": { "@idea/plugin-sdk": "file:../.." + }, + "peerDependencies": { + "react": "^18.2.0 || ^19.0.0", + "react-dom": "^18.2.0 || ^19.0.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" } } - diff --git a/examples/hello-plugin/src/core/layout.ts b/examples/hello-plugin/src/core/layout.ts deleted file mode 100644 index 08fb697..0000000 --- a/examples/hello-plugin/src/core/layout.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { PluginLayoutProps } from "@idea/plugin-sdk"; - -let hasLoggedFirstLayoutRender = false; - -export function HelloWorldLayout(props: PluginLayoutProps): string { - if (!hasLoggedFirstLayoutRender) { - hasLoggedFirstLayoutRender = true; - console.info("[hello-plugin] layout first render", { - projectId: props.projectId, - nodeId: props.nodeId, - layoutType: props.layoutType, - hasState: props.state !== undefined - }); - } - - return "hello-world"; -} diff --git a/examples/hello-plugin/src/core/layout.tsx b/examples/hello-plugin/src/core/layout.tsx new file mode 100644 index 0000000..09c522b --- /dev/null +++ b/examples/hello-plugin/src/core/layout.tsx @@ -0,0 +1,58 @@ +import type { PluginLayoutProps } from "@idea/plugin-sdk"; +import type { ReactElement } from "react"; +import { useMemo, useState } from "react"; + +let hasLoggedFirstLayoutRender = false; + +export function HelloWorldLayout(props: PluginLayoutProps): ReactElement { + const [localClicks, setLocalClicks] = useState(0); + const hostState = useMemo(() => { + return typeof props.state === "object" && props.state !== null && !Array.isArray(props.state) + ? (props.state as Record) + : {}; + }, [props.state]); + const persistedClicks = + typeof hostState.clicks === "number" && Number.isFinite(hostState.clicks) + ? hostState.clicks + : 0; + + if (!hasLoggedFirstLayoutRender) { + hasLoggedFirstLayoutRender = true; + console.info("[hello-plugin] layout first render", { + projectId: props.projectId, + nodeId: props.nodeId, + layoutType: props.layoutType, + hasState: props.state !== undefined + }); + } + + return ( +
+
+

Hello Plugin

+

+ React layout rendered by IdeA host React. +

+
+
+
+
Project
+
{props.projectId}
+
+
+
Layout
+
{props.layoutType}
+
+
+ +
+ ); +} diff --git a/examples/hello-plugin/src/index.ts b/examples/hello-plugin/src/index.ts index 007edea..4484e07 100644 --- a/examples/hello-plugin/src/index.ts +++ b/examples/hello-plugin/src/index.ts @@ -15,7 +15,17 @@ export function activate(ctx: ActivateContext): void { const commandDisposable = ctx.commands?.registerCommand(COMMAND_ID, async () => { const commandRunCount = await recordCommandRun(ctx); const feedback = await runHelloCommandTask(ctx); + const opened = await ctx.services?.windows.open({ + layoutType: LAYOUT_TYPE, + state: { openedFromCommand: commandRunCount ?? null } + }); ctx.logger.info("command executed", { commandId: COMMAND_ID, commandRunCount, feedback }); + if (opened) { + ctx.logger.info("layout window opened", { + label: opened.label, + alreadyOpen: opened.alreadyOpen + }); + } return feedback; }); diff --git a/examples/hello-plugin/tsconfig.build.json b/examples/hello-plugin/tsconfig.build.json index 9121acc..2e6cdef 100644 --- a/examples/hello-plugin/tsconfig.build.json +++ b/examples/hello-plugin/tsconfig.build.json @@ -14,6 +14,7 @@ } }, "include": [ - "src/**/*.ts" + "src/**/*.ts", + "src/**/*.tsx" ] } diff --git a/examples/hello-plugin/tsconfig.json b/examples/hello-plugin/tsconfig.json index 27c3a9e..b5da265 100644 --- a/examples/hello-plugin/tsconfig.json +++ b/examples/hello-plugin/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "declaration": false, "declarationMap": false, + "jsx": "react-jsx", "noEmit": true, "rootDir": "../..", "paths": { @@ -13,6 +14,7 @@ }, "include": [ "src/**/*.ts", + "src/**/*.tsx", "../../src/**/*.ts" ] } diff --git a/package-lock.json b/package-lock.json index ffbe024..6e83a4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,74 @@ "version": "0.1.0", "license": "MIT", "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", "typescript": "^5.5.0" + }, + "peerDependencies": { + "react": "^18.2.0 || ^19.0.0", + "react-dom": "^18.2.0 || ^19.0.0" } }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", diff --git a/package.json b/package.json index de67eac..5fde6fc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@idea/plugin-sdk", "version": "0.1.0", - "description": "Minimal public TypeScript SDK for IdeA plugins.", + "description": "Public TypeScript SDK for IdeA plugins.", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -29,7 +29,15 @@ "sdk" ], "license": "MIT", + "peerDependencies": { + "react": "^18.2.0 || ^19.0.0", + "react-dom": "^18.2.0 || ^19.0.0" + }, "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", "typescript": "^5.5.0" } } diff --git a/src/index.ts b/src/index.ts index be8b304..fb39ad6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,6 +43,7 @@ export type { IdeAPluginModule, JsonValue, LayoutRegistry, + OpenPluginWindowOptions, PluginLogger, PluginLayoutAvailability, PluginLayoutComponent, @@ -52,6 +53,7 @@ export type { PluginLayoutState, PluginServices, PluginStorage, + PluginWindow, ProjectConvention, ProjectModule, ProjectStructure, @@ -82,5 +84,6 @@ export type { WorkspaceTextFile, WorkspaceWatch, WorkspaceWatchEvent, - WorkspaceWatchHandler + WorkspaceWatchHandler, + WindowService } from "./runtime.js"; diff --git a/src/runtime.ts b/src/runtime.ts index 88286fe..e1728c7 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -1,3 +1,5 @@ +import type { ComponentType, ReactNode } from "react"; + export interface ActivateContext { pluginId: string; logger: PluginLogger; @@ -47,7 +49,7 @@ export interface PluginStorage { export type PluginLayoutState = JsonValue | undefined; export type PluginLayoutAvailability = "available"; -export type PluginLayoutRenderResult = unknown; +export type PluginLayoutRenderResult = ReactNode; export interface PluginLayoutProps { /** Project currently hosting this layout cell. */ @@ -64,9 +66,8 @@ export interface PluginLayoutProps = ( - props: PluginLayoutProps, -) => PluginLayoutRenderResult; +export type PluginLayoutComponent = + ComponentType>; export interface PluginLayoutDefinition { /** Must match a layout `type` declared in this plugin's manifest. */ @@ -87,6 +88,7 @@ export interface PluginServices { events: EventService; config: ConfigDocumentService; terminal: TerminalService; + windows: WindowService; } export interface WorkspaceProject { @@ -570,3 +572,32 @@ export interface TerminalService { /** Kills a PTY by id. */ close(sessionId: string): Promise; } + +export interface OpenPluginWindowOptions { + /** Layout `type` declared by this plugin in `contributes.layouts`. */ + layoutType: string; + /** Initial opaque state copied into the detached window surface. */ + state?: JsonValue; +} + +export interface PluginWindow { + label: string; + url: string; + alreadyOpen: boolean; + providerPluginDisplayName: string; + layoutLabel: string; + surface: { + pluginId: string; + layoutType: string; + state: JsonValue; + }; +} + +export interface WindowService { + /** + * Opens or focuses a detached IdeA OS window hosting one of this plugin's + * declared layout contributions. The host rejects layout ids absent from this + * plugin's manifest. + */ + open(options: OpenPluginWindowOptions): Promise; +}