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:
Mohamed Boudra
2026-08-19 11:54:55 +02:00
parent ca2ee380df
commit 91024bc029
25 changed files with 643 additions and 153 deletions
+5
View File
@@ -13,6 +13,11 @@
"source": "./src/index.ts",
"default": "./dist/index.js"
},
"./server": {
"types": "./dist/server.d.ts",
"source": "./src/server.ts",
"default": "./dist/server.js"
},
"./host": {
"types": "./dist/host.d.ts",
"source": "./src/host.ts",
+12 -1
View File
@@ -3,8 +3,19 @@ import type { PaseoApi } from "@getpaseo/client";
import type { ZodType, input as ZodInput, output as ZodOutput } from "zod";
import type { PluginRpcContract } from "./rpc.js";
export interface PluginTheme {
readonly colors: {
readonly surface0: string;
readonly foreground: string;
readonly foregroundMuted: string;
readonly accent: string;
readonly accentForeground: string;
readonly statusDanger: string;
};
}
export interface PluginHostProps {
theme: Record<string, unknown>;
theme: PluginTheme;
host: {
id: string;
label: string;
+5 -9
View File
@@ -1,9 +1,12 @@
export {
PluginAttachmentItemSchema,
PluginAttachmentSearchPayloadSchema,
defineAttachmentSource,
defineRpc,
type PluginAttachmentItem,
type PluginAttachmentSearchPayload,
} from "./attachments.js";
type PluginRpcContract,
} from "./server.js";
export type {
PluginAttachmentSourceContribution,
PluginAgentCommandContext,
@@ -17,6 +20,7 @@ export type {
PluginGlobalCommandContext,
PluginHandlerContext,
PluginHostProps,
PluginTheme,
PluginSidebarContribution,
PluginSurfaceContribution,
PluginSurfaceProps,
@@ -27,12 +31,4 @@ export type {
} from "./contracts.js";
export { usePaseo } from "./paseo-context.js";
export { useAgent, useWorkspace } from "./client-state.js";
import type { PluginAttachmentSourceContribution } from "./contracts.js";
export { useRpc } from "./rpc-context.js";
export { defineRpc, type PluginRpcContract } from "./rpc.js";
export function defineAttachmentSource<Definition extends PluginAttachmentSourceContribution>(
definition: Definition,
): Definition {
return definition;
}
+57
View File
@@ -0,0 +1,57 @@
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const srcDir = path.dirname(fileURLToPath(import.meta.url));
const forbiddenSpecifiers = new Set([
"react",
"react/jsx-runtime",
"react-native",
"use-sync-external-store",
"use-sync-external-store/shim/with-selector",
]);
function localFile(fromFile: string, specifier: string): string {
const withoutJs = specifier.replace(/\.js$/, "");
for (const candidate of [`${withoutJs}.ts`, `${withoutJs}.tsx`, withoutJs]) {
const resolved = path.resolve(path.dirname(fromFile), candidate);
if (existsSync(resolved)) return resolved;
}
throw new Error(`Could not resolve ${specifier} from ${fromFile}`);
}
function valueImportSpecifiers(source: string): string[] {
const specifiers: string[] = [];
const pattern = /(?:^|\n)(?:export|import)\s+(?!type\b)(?:[\s\S]*?)from\s+["']([^"']+)["']/g;
for (const match of source.matchAll(pattern)) {
if (match[1]) specifiers.push(match[1]);
}
return specifiers;
}
function collectBareSpecifiers(entry: string): string[] {
const seen = new Set<string>();
const bare: string[] = [];
const queue = [entry];
while (queue.length > 0) {
const file = queue.pop();
if (!file || seen.has(file)) continue;
seen.add(file);
for (const specifier of valueImportSpecifiers(readFileSync(file, "utf8"))) {
if (specifier.startsWith(".")) {
queue.push(localFile(file, specifier));
continue;
}
bare.push(specifier);
}
}
return bare;
}
describe("@paseo/plugin/server", () => {
it("does not load react or the client hook graph", () => {
const specifiers = collectBareSpecifiers(path.join(srcDir, "server.ts"));
expect(specifiers.filter((specifier) => forbiddenSpecifiers.has(specifier))).toEqual([]);
});
});
+16
View File
@@ -0,0 +1,16 @@
import type { PluginAttachmentSourceContribution } from "./contracts.js";
export {
PluginAttachmentItemSchema,
PluginAttachmentSearchPayloadSchema,
type PluginAttachmentItem,
type PluginAttachmentSearchPayload,
} from "./attachments.js";
export type { PluginAttachmentSourceContribution, PluginHandlerContext } from "./contracts.js";
export { defineRpc, type PluginRpcContract } from "./rpc.js";
export function defineAttachmentSource<Definition extends PluginAttachmentSourceContribution>(
definition: Definition,
): Definition {
return definition;
}