From 57bfea70e74ca2e087c3d15b57f9fb9e21c090d4 Mon Sep 17 00:00:00 2001 From: siyu <1817907332@qq.com> Date: Sun, 16 Aug 2026 18:41:13 +0800 Subject: [PATCH] fix: restrict dev tsx loader to repo-local plugins (Windows external plugin workers) --- .../plugin-tsx-loader-condition.test.ts | 33 +++++++++++++++++++ server/src/services/plugin-loader.ts | 23 +++++++++---- 2 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 server/src/__tests__/plugin-tsx-loader-condition.test.ts diff --git a/server/src/__tests__/plugin-tsx-loader-condition.test.ts b/server/src/__tests__/plugin-tsx-loader-condition.test.ts new file mode 100644 index 0000000000..d3629fef4c --- /dev/null +++ b/server/src/__tests__/plugin-tsx-loader-condition.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import path from "node:path"; +import { isRepoLocalPluginPath, REPO_ROOT } from "../services/plugin-loader.js"; + +describe("isRepoLocalPluginPath", () => { + it("returns true for a package inside the repository", () => { + const pkgPath = path.join(REPO_ROOT, "packages", "plugins", "examples", "plugin-hello-world-example"); + expect(isRepoLocalPluginPath(pkgPath)).toBe(true); + }); + + it("returns true when the package path equals the repository root", () => { + expect(isRepoLocalPluginPath(REPO_ROOT)).toBe(true); + }); + + it("returns false for a package outside the repository", () => { + const outside = path.join(REPO_ROOT, "..", "external-plugin"); + expect(isRepoLocalPluginPath(outside)).toBe(false); + }); + + it("returns false for a sibling directory that merely shares the repo name prefix", () => { + const sibling = path.join(path.dirname(REPO_ROOT), path.basename(REPO_ROOT) + "-external"); + expect(isRepoLocalPluginPath(sibling)).toBe(false); + }); + + it("returns false for null and undefined", () => { + expect(isRepoLocalPluginPath(null)).toBe(false); + expect(isRepoLocalPluginPath(undefined)).toBe(false); + }); + + it("returns false for an empty string", () => { + expect(isRepoLocalPluginPath("")).toBe(false); + }); +}); diff --git a/server/src/services/plugin-loader.ts b/server/src/services/plugin-loader.ts index d25427d9aa..b42a43de38 100644 --- a/server/src/services/plugin-loader.ts +++ b/server/src/services/plugin-loader.ts @@ -703,6 +703,19 @@ function isPathWithin(root: string, target: string): boolean { return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)); } +/** + * True when a plugin package path is inside this repository. + * + * The dev tsx loader is only applied to repo-local packages, because + * external plugins ship prebuilt JS and tsx's `--import` loader crashes + * on Windows when the worker entry is an absolute drive-letter path + * (ERR_UNSUPPORTED_ESM_URL_SCHEME, protocol 'e:'). + */ +export function isRepoLocalPluginPath(packagePath: string | null | undefined): boolean { + if (!packagePath) return false; + return isPathWithin(REPO_ROOT, path.resolve(packagePath)); +} + export function isRepoBundledPluginPath( packageRoot: string, options: { repoRoot?: string } = {}, @@ -2295,18 +2308,16 @@ export function pluginLoader( // entry as a raw drive-letter path (e.g. `E:\plugins\dist\worker.js`) // and fails with ERR_UNSUPPORTED_ESM_URL_SCHEME (protocol 'e:'). // External plugins ship prebuilt JS and do not need the tsx loader. - const PLUGIN_REPO_ROOT = path.resolve(__dirname, "../../.."); const resolvedPkgPath = activePlugin.packagePath ? path.resolve(activePlugin.packagePath) : null; - const isRepoLocalPlugin = + if ( resolvedPkgPath !== null && - (resolvedPkgPath === PLUGIN_REPO_ROOT || - resolvedPkgPath.startsWith(PLUGIN_REPO_ROOT + path.sep)); - if (isRepoLocalPlugin && existsSync(DEV_TSX_LOADER_PATH)) { + isRepoLocalPluginPath(resolvedPkgPath) && + existsSync(DEV_TSX_LOADER_PATH) + ) { workerOptions.execArgv = ["--import", DEV_TSX_LOADER_PATH]; } - await workerManager.startWorker(pluginId, workerOptions); registered.worker = true;