fix(plugins): install local plugins on packaged desktop
The plugin compiler spawned esbuild through app.asar, and the subprocess imported the React client barrel. Point esbuild at the unpacked binary, add a react-free @paseo/plugin/server entry, and pass a typed PluginTheme so plugin UI follows the active theme on desktop and mobile.
This commit is contained in:
@@ -162,6 +162,45 @@ describe("evaluatePluginClientBundle", () => {
|
||||
).toThrow("must return a cleanup function");
|
||||
});
|
||||
|
||||
it("resolves @paseo/plugin/server for shared RPC contracts", () => {
|
||||
const plugin = evaluatePluginClientBundle(
|
||||
"example",
|
||||
`(function(require) {
|
||||
const { defineRpc, defineAttachmentSource } = require("@paseo/plugin/server");
|
||||
const search = defineRpc({ name: "issues.search", input: {}, output: {} });
|
||||
const module = { exports: {} };
|
||||
module.exports.default = function(plugin) {
|
||||
plugin.addAttachmentSource(defineAttachmentSource({
|
||||
id: "issues",
|
||||
title: "Issue",
|
||||
icon: "CircleDot",
|
||||
pickerTitle: "Attach issue",
|
||||
searchPlaceholder: "Search",
|
||||
search,
|
||||
}));
|
||||
return function() {};
|
||||
};
|
||||
return module.exports;
|
||||
})`,
|
||||
);
|
||||
|
||||
expect(plugin.attachmentSources.map((source) => source.search.name)).toEqual(["issues.search"]);
|
||||
});
|
||||
|
||||
it("rejects modules that are not part of the client runtime", () => {
|
||||
expect(() =>
|
||||
evaluatePluginClientBundle(
|
||||
"example",
|
||||
`(function(require) {
|
||||
require("fs");
|
||||
const module = { exports: {} };
|
||||
module.exports.default = function() { return function() {}; };
|
||||
return module.exports;
|
||||
})`,
|
||||
),
|
||||
).toThrow('Module "fs" is not available in plugin client code');
|
||||
});
|
||||
|
||||
it("does not publish partial contributions when setup fails", () => {
|
||||
expect(() =>
|
||||
evaluatePluginClientBundle(
|
||||
|
||||
@@ -159,6 +159,9 @@ export function evaluatePluginClientBundle(id: string, bundle: string): Evaluate
|
||||
useRpc,
|
||||
};
|
||||
}
|
||||
if (name === "@paseo/plugin/server") {
|
||||
return { defineAttachmentSource, defineRpc };
|
||||
}
|
||||
if (name === "@tanstack/react-query") return ReactQuery;
|
||||
if (name === "zod") return Zod;
|
||||
throw new Error(`Module "${name}" is not available in plugin client code`);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import type { PluginSurfaceProps } from "@paseo/plugin";
|
||||
import type { PluginSurfaceProps, PluginTheme } from "@paseo/plugin";
|
||||
import { ChevronDown, X } from "lucide-react-native";
|
||||
import { useCallback, useMemo, useRef, useState, type ComponentType } from "react";
|
||||
import { Platform, Pressable, Text, View } from "react-native";
|
||||
@@ -14,6 +14,7 @@ import { useHostRuntimeClient, useHosts } from "@/runtime/host-runtime";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
import type { ShortcutKey } from "@/utils/format-shortcut";
|
||||
import { resolvePluginIcon } from "./icons";
|
||||
import { toPluginTheme } from "./theme";
|
||||
import { useInstalledPlugin, usePluginInstallations } from "./registry";
|
||||
import { buildPluginSurfaceRoute } from "./routes";
|
||||
import { rememberPluginContributionHost } from "./sidebar-groups";
|
||||
@@ -27,10 +28,9 @@ import {
|
||||
} from "./surface-contribution";
|
||||
|
||||
const EMPTY_SHORTCUT_KEYS: ShortcutKey[] = [];
|
||||
const EMPTY_THEME_DTO: Record<string, unknown> = {};
|
||||
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||
const pluginThemeMapping = (theme: Theme) => ({
|
||||
themeDto: JSON.parse(JSON.stringify(theme)) as Record<string, unknown>,
|
||||
theme: toPluginTheme(theme),
|
||||
});
|
||||
const ThemedX = withUnistyles(X);
|
||||
const ThemedChevronDown = withUnistyles(ChevronDown);
|
||||
@@ -57,18 +57,18 @@ function SurfaceRenderer({
|
||||
plugin,
|
||||
layout,
|
||||
host,
|
||||
themeDto = EMPTY_THEME_DTO,
|
||||
theme,
|
||||
}: {
|
||||
Surface: ComponentType<PluginSurfaceProps>;
|
||||
runtime: NonNullable<ReturnType<typeof createPluginSurfaceRuntime>>;
|
||||
plugin: NonNullable<ReturnType<typeof useInstalledPlugin>>;
|
||||
layout: PluginSurfaceProps["layout"];
|
||||
host: PluginSurfaceProps["host"];
|
||||
themeDto?: Record<string, unknown>;
|
||||
theme: PluginTheme;
|
||||
}) {
|
||||
return (
|
||||
<PluginRuntimeBoundary plugin={plugin} runtime={runtime}>
|
||||
<Surface theme={themeDto} host={host} layout={layout} />
|
||||
<Surface theme={theme} host={host} layout={layout} />
|
||||
</PluginRuntimeBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { lightTheme } from "@/styles/theme";
|
||||
import { toPluginTheme } from "./theme";
|
||||
|
||||
describe("toPluginTheme", () => {
|
||||
it("maps the app theme into the plugin color tokens", () => {
|
||||
expect(toPluginTheme(lightTheme)).toEqual({
|
||||
colors: {
|
||||
surface0: lightTheme.colors.surface0,
|
||||
foreground: lightTheme.colors.foreground,
|
||||
foregroundMuted: lightTheme.colors.foregroundMuted,
|
||||
accent: lightTheme.colors.accent,
|
||||
accentForeground: lightTheme.colors.accentForeground,
|
||||
statusDanger: lightTheme.colors.statusDanger,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { PluginTheme } from "@paseo/plugin";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
|
||||
export function toPluginTheme(theme: Theme): PluginTheme {
|
||||
return {
|
||||
colors: {
|
||||
surface0: theme.colors.surface0,
|
||||
foreground: theme.colors.foreground,
|
||||
foregroundMuted: theme.colors.foregroundMuted,
|
||||
accent: theme.colors.accent,
|
||||
accentForeground: theme.colors.accentForeground,
|
||||
statusDanger: theme.colors.statusDanger,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
PluginAgentPanelProps,
|
||||
PluginHostProps,
|
||||
PluginTheme,
|
||||
PluginWorkspacePanelProps,
|
||||
} from "@paseo/plugin";
|
||||
import { PluginClientStateProvider } from "@paseo/plugin/host";
|
||||
@@ -18,6 +19,7 @@ import { useWorkspaceExists } from "@/stores/session-store-hooks";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
import { normalizeWorkspaceOpaqueId } from "@/utils/workspace-identity";
|
||||
import { createPluginClientStateSource } from "../client-state/source";
|
||||
import { toPluginTheme } from "../theme";
|
||||
import { resolvePluginIcon } from "../icons";
|
||||
import { useInstalledPlugin } from "../registry";
|
||||
import { PluginRuntimeBoundary } from "../runtime-boundary";
|
||||
@@ -25,9 +27,8 @@ import { createPluginSurfaceRuntime } from "../surface-runtime";
|
||||
import { SurfaceErrorBoundary } from "../surface-error-boundary";
|
||||
import { resolvePluginWorkspacePanel } from "./resolution";
|
||||
|
||||
const EMPTY_THEME_DTO: Record<string, unknown> = {};
|
||||
const pluginThemeMapping = (theme: Theme) => ({
|
||||
themeDto: JSON.parse(JSON.stringify(theme)) as Record<string, unknown>,
|
||||
theme: toPluginTheme(theme),
|
||||
});
|
||||
|
||||
function resolvePlatform(): PluginHostProps["layout"]["platform"] {
|
||||
@@ -36,7 +37,7 @@ function resolvePlatform(): PluginHostProps["layout"]["platform"] {
|
||||
return "web";
|
||||
}
|
||||
|
||||
function PluginPanelBody({ themeDto = EMPTY_THEME_DTO }: { themeDto?: Record<string, unknown> }) {
|
||||
function PluginPanelBody({ theme }: { theme: PluginTheme }) {
|
||||
const { serverId, workspaceId, target } = usePaneContext();
|
||||
invariant(target.kind === "plugin", "PluginPanel requires plugin target");
|
||||
const plugin = useInstalledPlugin(serverId, target.pluginId);
|
||||
@@ -75,7 +76,7 @@ function PluginPanelBody({ themeDto = EMPTY_THEME_DTO }: { themeDto?: Record<str
|
||||
if (contribution.context === "workspace") {
|
||||
const props: PluginWorkspacePanelProps = {
|
||||
context: "workspace",
|
||||
theme: themeDto,
|
||||
theme,
|
||||
host,
|
||||
layout,
|
||||
workspaceId,
|
||||
@@ -86,7 +87,7 @@ function PluginPanelBody({ themeDto = EMPTY_THEME_DTO }: { themeDto?: Record<str
|
||||
} else if (agentExists && target.context === "agent") {
|
||||
const props: PluginAgentPanelProps = {
|
||||
context: "agent",
|
||||
theme: themeDto,
|
||||
theme,
|
||||
host,
|
||||
layout,
|
||||
workspaceId,
|
||||
|
||||
Reference in New Issue
Block a user