From c4fa93e460f5eddda5a48da0b996dc5e46f25e53 Mon Sep 17 00:00:00 2001 From: roboomp Date: Fri, 5 Jun 2026 07:59:28 +0000 Subject: [PATCH] fix(plugins): preserved null package import exclusions Kept explicit null package imports as exclusions so exact entries and active conditions do not fall through to wildcard or fallback targets.\n\nFixes #1889 --- .../extensibility/plugins/legacy-pi-compat.ts | 24 +++-- .../test/plugin-extensions-discovery.test.ts | 102 ++++++++++++++++++ 2 files changed, 119 insertions(+), 7 deletions(-) diff --git a/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts b/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts index 8ecf04a6f..70817b916 100644 --- a/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts +++ b/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts @@ -49,6 +49,7 @@ const SOURCE_MODULE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", const SUPPORTED_PACKAGE_IMPORT_CONDITIONS = new Set(["bun", "node", "import", "default"]); const packageRootCache = new Map(); const packageImportsCache = new Map | null>(); +const PACKAGE_IMPORT_EXCLUDED = Symbol("packageImportExcluded"); // Extensions that imported `@sinclair/typebox` directly used to resolve against a // real `@sinclair/typebox` install. The runtime dep was replaced with the Zod-backed @@ -334,14 +335,20 @@ async function readPackageImports(packageRoot: string): Promise { expect(extension?.commands.has("json-schema-ext")).toBe(true); }); + it("preserves exact null package import exclusions ahead of wildcard fallbacks", async () => { + const pluginsDir = getPluginsDir(); + const pluginDir = path.join(pluginsDir, "node_modules", "null-exact-import-plugin"); + const extensionPath = path.join(pluginDir, "src", "index.ts"); + fs.rmSync(path.join(pluginsDir, "node_modules"), { recursive: true, force: true }); + fs.mkdirSync(path.join(pluginDir, "src"), { recursive: true }); + fs.writeFileSync( + path.join(pluginsDir, "package.json"), + JSON.stringify({ + name: "omp-plugins", + private: true, + dependencies: { + "null-exact-import-plugin": "1.0.0", + }, + }), + ); + fs.writeFileSync( + path.join(pluginDir, "package.json"), + JSON.stringify({ + name: "null-exact-import-plugin", + version: "1.0.0", + imports: { + "#src/internal": null, + "#src/*": "./src/*", + }, + pi: { + extensions: ["./src/index.ts"], + }, + }), + ); + fs.writeFileSync(path.join(pluginDir, "src", "internal.ts"), 'export const commandName = "null-exact-ext";'); + fs.writeFileSync( + extensionPath, + [ + 'import { commandName } from "#src/internal";', + "", + "export default function(pi) {", + "\tpi.registerCommand(commandName, { handler: async () => {} });", + "}", + ].join("\n"), + ); + + const result = await discoverAndLoadExtensions([], projectDir.path()); + const extension = result.extensions.find(ext => ext.path === extensionPath); + const pluginError = result.errors.find(err => err.path === extensionPath); + + expect(pluginError?.error).toContain("#src/internal"); + expect(extension).toBeUndefined(); + }); + + it("preserves active null conditional package import exclusions", async () => { + const pluginsDir = getPluginsDir(); + const pluginDir = path.join(pluginsDir, "node_modules", "null-conditional-import-plugin"); + const extensionPath = path.join(pluginDir, "src", "index.ts"); + fs.rmSync(path.join(pluginsDir, "node_modules"), { recursive: true, force: true }); + fs.mkdirSync(path.join(pluginDir, "src"), { recursive: true }); + fs.writeFileSync( + path.join(pluginsDir, "package.json"), + JSON.stringify({ + name: "omp-plugins", + private: true, + dependencies: { + "null-conditional-import-plugin": "1.0.0", + }, + }), + ); + fs.writeFileSync( + path.join(pluginDir, "package.json"), + JSON.stringify({ + name: "null-conditional-import-plugin", + version: "1.0.0", + imports: { + "#blocked": { + node: null, + default: "./src/blocked.ts", + }, + }, + pi: { + extensions: ["./src/index.ts"], + }, + }), + ); + fs.writeFileSync(path.join(pluginDir, "src", "blocked.ts"), 'export const commandName = "null-conditional-ext";'); + fs.writeFileSync( + extensionPath, + [ + 'import { commandName } from "#blocked";', + "", + "export default function(pi) {", + "\tpi.registerCommand(commandName, { handler: async () => {} });", + "}", + ].join("\n"), + ); + + const result = await discoverAndLoadExtensions([], projectDir.path()); + const extension = result.extensions.find(ext => ext.path === extensionPath); + const pluginError = result.errors.find(err => err.path === extensionPath); + + expect(pluginError?.error).toContain("#blocked"); + expect(extension).toBeUndefined(); + }); + it("rewrites side-effect imports of package-import aliases and legacy Pi scopes", async () => { const pluginsDir = getPluginsDir(); const pluginDir = path.join(pluginsDir, "node_modules", "side-effect-plugin");