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:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user