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
+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([]);
});
});