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
+39
View File
@@ -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(
+3
View File
@@ -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`);
+6 -6
View File
@@ -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>
);
}
+18
View File
@@ -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,
},
});
});
});
+15
View File
@@ -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,
@@ -50,7 +50,7 @@ describe("plugin scaffold", () => {
await Promise.all([
writeFile(
path.join(directory, "inspect.shared.ts"),
`import { defineRpc } from "@paseo/plugin";
`import { defineRpc } from "@paseo/plugin/server";
import { z } from "zod";
export const inspect = defineRpc({
@@ -62,7 +62,7 @@ export const inspect = defineRpc({
),
writeFile(
path.join(directory, "inspect.server.ts"),
`import type { PluginHandlerContext } from "@paseo/plugin";
`import type { PluginHandlerContext } from "@paseo/plugin/server";
import type { output as ZodOutput } from "zod";
import { inspect } from "./inspect.shared";
+105 -56
View File
@@ -2,13 +2,95 @@ import { mkdir, readdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { PluginIdSchema } from "@getpaseo/protocol/messages";
const SDK_DECLARATIONS = `declare module "@paseo/plugin" {
import type { ComponentType } from "react";
const SDK_DECLARATIONS = `declare module "@paseo/plugin/server" {
import type { PaseoApi } from "@getpaseo/client";
import type { ZodType, input as ZodInput, output as ZodOutput } from "zod";
export interface PluginRpcContract<
InputSchema extends ZodType = ZodType,
OutputSchema extends ZodType = ZodType,
> {
name: string;
input: InputSchema;
output: OutputSchema;
}
export interface PluginAttachmentItem {
id: string;
identifier: string;
title: string;
subtitle?: string;
url: string;
text: string;
resourceType: string;
}
export interface PluginAttachmentSearchPayload {
items: PluginAttachmentItem[];
}
export interface PluginAttachmentSourceContribution {
id: string;
title: string;
icon: string;
pickerTitle: string;
searchPlaceholder: string;
search: PluginRpcContract;
}
export interface PluginHandlerContext {
paseo: PaseoApi;
}
export function defineRpc<InputSchema extends ZodType, OutputSchema extends ZodType>(definition: {
name: string;
input: InputSchema;
output: OutputSchema;
}): PluginRpcContract<InputSchema, OutputSchema>;
export function defineAttachmentSource<Definition extends PluginAttachmentSourceContribution>(
definition: Definition,
): Definition;
export const PluginAttachmentItemSchema: import("zod").ZodType<PluginAttachmentItem>;
export const PluginAttachmentSearchPayloadSchema: import("zod").ZodType<PluginAttachmentSearchPayload>;
}
declare module "@paseo/plugin" {
import type { ComponentType } from "react";
import type { PaseoApi } from "@getpaseo/client";
import type { ZodType, input as ZodInput, output as ZodOutput } from "zod";
import type {
PluginAttachmentSourceContribution,
PluginHandlerContext,
PluginRpcContract,
} from "@paseo/plugin/server";
export {
PluginAttachmentItemSchema,
PluginAttachmentSearchPayloadSchema,
defineAttachmentSource,
defineRpc,
type PluginAttachmentItem,
type PluginAttachmentSearchPayload,
type PluginAttachmentSourceContribution,
type PluginHandlerContext,
type PluginRpcContract,
} from "@paseo/plugin/server";
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 };
layout: { compact: boolean; platform: "ios" | "android" | "web" };
}
@@ -65,38 +147,6 @@ const SDK_DECLARATIONS = `declare module "@paseo/plugin" {
| { id: string; title: string; icon: string; context: "workspace"; Component: ComponentType<PluginWorkspacePanelProps> }
| { id: string; title: string; icon: string; context: "agent"; Component: ComponentType<PluginAgentPanelProps> };
export interface PluginRpcContract<
InputSchema extends ZodType = ZodType,
OutputSchema extends ZodType = ZodType,
> {
name: string;
input: InputSchema;
output: OutputSchema;
}
export interface PluginAttachmentItem {
id: string;
identifier: string;
title: string;
subtitle?: string;
url: string;
text: string;
resourceType: string;
}
export interface PluginAttachmentSearchPayload {
items: PluginAttachmentItem[];
}
export interface PluginAttachmentSourceContribution {
id: string;
title: string;
icon: string;
pickerTitle: string;
searchPlaceholder: string;
search: PluginRpcContract;
}
export interface PluginSidebarContribution {
id: string;
title: string;
@@ -140,10 +190,6 @@ const SDK_DECLARATIONS = `declare module "@paseo/plugin" {
| { id: string; title: string; icon: string; keywords?: readonly string[]; context: "workspace"; onSelect(context: PluginWorkspaceCommandContext): void | Promise<void> }
| { id: string; title: string; icon: string; keywords?: readonly string[]; context: "agent"; onSelect(context: PluginAgentCommandContext): void | Promise<void> };
export interface PluginHandlerContext {
paseo: PaseoApi;
}
export interface PluginContext {
handle<InputSchema extends ZodType, OutputSchema extends ZodType>(
contract: PluginRpcContract<InputSchema, OutputSchema>,
@@ -162,12 +208,6 @@ const SDK_DECLARATIONS = `declare module "@paseo/plugin" {
export type PluginCleanup = () => void | Promise<void>;
export type PluginContribution = (plugin: PluginContext) => PluginCleanup;
export function defineRpc<InputSchema extends ZodType, OutputSchema extends ZodType>(definition: {
name: string;
input: InputSchema;
output: OutputSchema;
}): PluginRpcContract<InputSchema, OutputSchema>;
export function useRpc<InputSchema extends ZodType, OutputSchema extends ZodType>(
contract: PluginRpcContract<InputSchema, OutputSchema>,
): (input: ZodInput<InputSchema>) => Promise<ZodOutput<OutputSchema>>;
@@ -183,13 +223,6 @@ const SDK_DECLARATIONS = `declare module "@paseo/plugin" {
agentId: string,
selector: (agent: PluginAgentSnapshot) => Selection,
): Selection | null;
export function defineAttachmentSource<Definition extends PluginAttachmentSourceContribution>(
definition: Definition,
): Definition;
export const PluginAttachmentItemSchema: import("zod").ZodType<PluginAttachmentItem>;
export const PluginAttachmentSearchPayloadSchema: import("zod").ZodType<PluginAttachmentSearchPayload>;
}
`;
@@ -218,11 +251,27 @@ export default function contribute(plugin: PluginContext) {
}
`;
const CLIENT_SURFACE = `import React from "react";
import { Text } from "react-native";
const CLIENT_SURFACE = `import type { PluginSurfaceProps } from "@paseo/plugin";
import React, { useMemo } from "react";
import { Text, View } from "react-native";
export function MainSurface() {
return <Text>Hello from my plugin</Text>;
export function MainSurface({ theme, layout }: PluginSurfaceProps) {
const styles = useMemo(
() => ({
screen: {
flex: 1,
padding: layout.compact ? 16 : 24,
backgroundColor: theme.colors.surface0,
},
text: { color: theme.colors.foreground },
}),
[theme, layout.compact],
);
return (
<View style={styles.screen}>
<Text style={styles.text}>Hello from my plugin</Text>
</View>
);
}
`;
+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;
}
@@ -0,0 +1,105 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
compilePlugin,
resolveExistingAsarUnpackedEsbuildBinary,
unpackedEsbuildBinaryFromPackageDir,
} from "./compiler.js";
const asarEsbuildDir = path.join("Resources", "app.asar", "node_modules", "esbuild");
describe("asar esbuild binary resolution", () => {
it("rewrites an asar package path to the unpacked platform binary", () => {
expect(unpackedEsbuildBinaryFromPackageDir(asarEsbuildDir, "darwin", "arm64")).toBe(
path.join(
"Resources",
"app.asar.unpacked",
"node_modules",
"@esbuild",
"darwin-arm64",
"bin",
"esbuild",
),
);
expect(unpackedEsbuildBinaryFromPackageDir(asarEsbuildDir, "win32", "x64")).toBe(
path.join(
"Resources",
"app.asar.unpacked",
"node_modules",
"@esbuild",
"win32-x64",
"esbuild.exe",
),
);
});
it("ignores package paths that are not inside app.asar", () => {
expect(
unpackedEsbuildBinaryFromPackageDir(path.join("node_modules", "esbuild"), "darwin", "arm64"),
).toBeNull();
});
it("returns null when the unpacked binary is missing", () => {
expect(
resolveExistingAsarUnpackedEsbuildBinary(asarEsbuildDir, "darwin", "arm64", () => false),
).toBeNull();
});
it("returns the unpacked path when the binary exists", () => {
expect(
resolveExistingAsarUnpackedEsbuildBinary(asarEsbuildDir, "linux", "x64", () => true),
).toBe(
path.join(
"Resources",
"app.asar.unpacked",
"node_modules",
"@esbuild",
"linux-x64",
"bin",
"esbuild",
),
);
});
});
const temporaryDirectories: string[] = [];
afterEach(async () => {
await Promise.all(
temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true })),
);
});
describe("plugin author module externals", () => {
it("leaves @paseo/plugin/server external in both bundles", async () => {
const directory = await mkdtemp(path.join(tmpdir(), "paseo-plugin-compiler-"));
temporaryDirectories.push(directory);
const entryPath = path.join(directory, "index.ts");
await writeFile(
entryPath,
`import type { PluginContext } from "@paseo/plugin";
import { defineRpc } from "@paseo/plugin/server";
import { z } from "zod";
const ping = defineRpc({
name: "ping",
input: z.object({}),
output: z.object({ ok: z.boolean() }),
});
export default function contribute(plugin: PluginContext) {
plugin.handle(ping, async () => ({ ok: true }));
return () => undefined;
}
`,
);
const { clientBundle, serverBundle } = await compilePlugin(entryPath);
expect(clientBundle).toContain("@paseo/plugin/server");
expect(serverBundle).toContain("@paseo/plugin/server");
expect(clientBundle).not.toContain("Invalid plugin RPC method");
expect(serverBundle).not.toContain("Invalid plugin RPC method");
});
});
+54 -2
View File
@@ -1,7 +1,57 @@
import { existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { createRequire } from "node:module";
import path from "node:path";
import { parse } from "@babel/parser";
import { build, type Plugin } from "esbuild";
import type { Plugin } from "esbuild";
// esbuild resolves its own platform binary via require.resolve() the first time its
// module is evaluated. Inside the packaged desktop app that resolves to a path under
// app.asar even though electron-builder unpacks the real binary to app.asar.unpacked.
// child_process.spawn bypasses Electron's asar fs shim, so the OS rejects that path
// with ENOTDIR. Point esbuild at the real unpacked binary before its module loads.
export function unpackedEsbuildBinaryFromPackageDir(
esbuildPackageDir: string,
platform: NodeJS.Platform,
arch: string,
): string | null {
const asarSegment = `${path.sep}app.asar${path.sep}`;
const asarIndex = esbuildPackageDir.indexOf(asarSegment);
if (asarIndex === -1) return null;
return path.join(
esbuildPackageDir.slice(0, asarIndex),
"app.asar.unpacked",
"node_modules",
`@esbuild/${platform}-${arch}`,
...(platform === "win32" ? ["esbuild.exe"] : ["bin", "esbuild"]),
);
}
export function resolveExistingAsarUnpackedEsbuildBinary(
esbuildPackageDir: string,
platform: NodeJS.Platform = process.platform,
arch: string = process.arch,
exists: (file: string) => boolean = existsSync,
): string | null {
const binaryPath = unpackedEsbuildBinaryFromPackageDir(esbuildPackageDir, platform, arch);
return binaryPath && exists(binaryPath) ? binaryPath : null;
}
function resolveAsarUnpackedEsbuildBinary(): string | null {
const require = createRequire(import.meta.url);
let esbuildDir: string;
try {
esbuildDir = path.dirname(require.resolve("esbuild/package.json"));
} catch {
return null;
}
return resolveExistingAsarUnpackedEsbuildBinary(esbuildDir);
}
if (!process.env.ESBUILD_BINARY_PATH) {
const unpackedBinary = resolveAsarUnpackedEsbuildBinary();
if (unpackedBinary) process.env.ESBUILD_BINARY_PATH = unpackedBinary;
}
type PluginBuildTarget = "client" | "server";
@@ -228,6 +278,7 @@ function createUnusedPlatformModulePlugin(target: PluginBuildTarget): Plugin {
}
async function compileTarget(entryPath: string, target: PluginBuildTarget): Promise<string> {
const { build } = await import("esbuild");
const source = await readFile(entryPath, "utf8");
const filteredSource = filterEntrypoint(source, target);
const result = await build({
@@ -245,13 +296,14 @@ async function compileTarget(entryPath: string, target: PluginBuildTarget): Prom
target === "client"
? [
"@paseo/plugin",
"@paseo/plugin/server",
"@tanstack/react-query",
"react",
"react/jsx-runtime",
"react-native",
"zod",
]
: ["@paseo/plugin", "zod"],
: ["@paseo/plugin", "@paseo/plugin/server", "zod"],
plugins: [createRuntimeBoundaryPlugin(target), createUnusedPlatformModulePlugin(target)],
logLevel: "silent",
treeShaking: true,
@@ -5,7 +5,7 @@ import {
defineRpc,
type PluginHandlerContext,
type PluginRpcContract,
} from "@paseo/plugin";
} from "@paseo/plugin/server";
import { createPaseoApi, type PaseoApi } from "@getpaseo/client";
import { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import { createPluginDaemonTransportFactory } from "./daemon-transport.js";
@@ -61,8 +61,10 @@ function register(contract: PluginRpcContract, handler: RpcHandler): void {
handlers.set(method, { contract: { ...contract, name: method }, handler });
}
const pluginAuthorRuntime = { defineAttachmentSource, defineRpc };
function runtimeRequire(name: string): unknown {
if (name === "@paseo/plugin") return { defineAttachmentSource, defineRpc };
if (name === "@paseo/plugin" || name === "@paseo/plugin/server") return pluginAuthorRuntime;
return nodeRequire(name);
}
@@ -597,7 +597,7 @@ export function Surface() {
),
writeFile(
path.join(directory, "inspect.shared.ts"),
`import { defineRpc } from "@paseo/plugin";
`import { defineRpc } from "@paseo/plugin/server";
import { z } from "zod";
export const inspectRpc = defineRpc({