feat(plugins): split plugin code by runtime

Keep the entry point focused on contribution wiring so client UI and daemon handlers can use their own runtime APIs safely. Surface compilation failures and lifecycle transitions in the retained plugin log tail.
This commit is contained in:
Mohamed Boudra
2026-08-17 23:45:19 +02:00
parent 5ed1a852d1
commit 3cc8749437
17 changed files with 657 additions and 265 deletions
@@ -12,7 +12,7 @@ afterEach(async () => {
});
describe("plugin scaffold", () => {
it("creates a standalone strict TSX project that typechecks", async () => {
it("creates a standalone split-runtime project that typechecks", async () => {
const parent = await mkdtemp(path.join(process.cwd(), ".plugin-scaffold-"));
directories.push(parent);
const directory = path.join(parent, "hello-plugin");
@@ -34,6 +34,12 @@ describe("plugin scaffold", () => {
expect(JSON.parse(await readFile(path.join(directory, "paseo-plugin.json"), "utf8"))).toEqual({
id: "hello-plugin",
});
await expect(readFile(path.join(directory, "index.ts"), "utf8")).resolves.toContain(
'from "./main.client"',
);
await expect(readFile(path.join(directory, "main.client.tsx"), "utf8")).resolves.toContain(
"Hello from my plugin",
);
});
it("typechecks client and server Paseo API access", async () => {
@@ -41,27 +47,45 @@ describe("plugin scaffold", () => {
directories.push(parent);
const directory = path.join(parent, "paseo-api-plugin");
await scaffoldPluginDirectory(directory);
await writeFile(
path.join(directory, "index.tsx"),
`import React from "react";
import { Text } from "react-native";
await Promise.all([
writeFile(
path.join(directory, "inspect.shared.ts"),
`import { defineRpc } from "@paseo/plugin";
import { z } from "zod";
import {
defineRpc,
type PluginContext,
type PluginAgentPanelProps,
usePaseo,
useAgent,
useWorkspace,
} from "@paseo/plugin";
const inspect = defineRpc({
export const inspect = defineRpc({
name: "inspect",
input: z.object({}),
output: z.object({ configured: z.boolean() }),
});
`,
),
writeFile(
path.join(directory, "inspect.server.ts"),
`import type { PluginHandlerContext } from "@paseo/plugin";
import type { output as ZodOutput } from "zod";
import { inspect } from "./inspect.shared";
function Surface() {
export async function inspectConfig(
_input: ZodOutput<typeof inspect.input>,
{ paseo }: PluginHandlerContext,
) {
return { configured: Boolean((await paseo.config.get()).config) };
}
`,
),
writeFile(
path.join(directory, "main.client.tsx"),
`import React from "react";
import { Text } from "react-native";
import {
type PluginAgentPanelProps,
useAgent,
usePaseo,
useWorkspace,
} from "@paseo/plugin";
export function Surface() {
const paseo = usePaseo();
const createWorkspace = () => paseo.workspaces.create({
source: { kind: "directory", path: "/repo" },
@@ -70,7 +94,7 @@ function Surface() {
return <Text>Paseo API</Text>;
}
function AgentPanel({ workspaceId, agentId }: PluginAgentPanelProps) {
export function AgentPanel({ workspaceId, agentId }: PluginAgentPanelProps) {
const workspaceName = useWorkspace(workspaceId, (workspace) => {
// @ts-expect-error Plugin snapshots are readonly.
workspace.name = "mutated";
@@ -83,11 +107,17 @@ function AgentPanel({ workspaceId, agentId }: PluginAgentPanelProps) {
});
return <Text>{workspaceName}: {agentTitle}</Text>;
}
`,
),
writeFile(
path.join(directory, "index.ts"),
`import type { PluginContext } from "@paseo/plugin";
import { AgentPanel, Surface } from "./main.client";
import { inspectConfig } from "./inspect.server";
import { inspect } from "./inspect.shared";
export default function contribute(plugin: PluginContext) {
plugin.handle(inspect, async (_input, { paseo }) => ({
configured: Boolean((await paseo.config.get()).config),
}));
plugin.handle(inspect, inspectConfig);
plugin.addSurface("main", Surface);
plugin.addWorkspacePanel({
id: "review",
@@ -110,7 +140,8 @@ export default function contribute(plugin: PluginContext) {
return () => {};
}
`,
);
),
]);
const configPath = path.join(directory, "tsconfig.json");
const loaded = ts.readConfigFile(configPath, ts.sys.readFile);
+12 -8
View File
@@ -206,16 +206,11 @@ const TSCONFIG = {
esModuleInterop: true,
allowSyntheticDefaultImports: true,
},
include: ["index.tsx", "paseo-plugin.d.ts"],
include: ["**/*.ts", "**/*.tsx"],
};
const ENTRY = `import type { PluginContext } from "@paseo/plugin";
import React from "react";
import { Text } from "react-native";
function MainSurface() {
return <Text>Hello from my plugin</Text>;
}
import { MainSurface } from "./main.client";
export default function contribute(plugin: PluginContext) {
plugin.addSurface("main", MainSurface);
@@ -223,6 +218,14 @@ export default function contribute(plugin: PluginContext) {
}
`;
const CLIENT_SURFACE = `import React from "react";
import { Text } from "react-native";
export function MainSurface() {
return <Text>Hello from my plugin</Text>;
}
`;
export interface PluginScaffold {
id: string;
directory: string;
@@ -260,7 +263,8 @@ export async function scaffoldPluginDirectory(
["package.json", `${JSON.stringify(packageJson, null, 2)}\n`],
["tsconfig.json", `${JSON.stringify(TSCONFIG, null, 2)}\n`],
["paseo-plugin.d.ts", SDK_DECLARATIONS],
["index.tsx", ENTRY],
["index.ts", ENTRY],
["main.client.tsx", CLIENT_SURFACE],
]);
await Promise.all(
[...files].map(([filename, contents]) =>