Use @mariozechner/jiti fork with virtualModules for extension loading in Bun binary

- Switch from jiti to @mariozechner/jiti fork
- Use virtualModules option for bundled packages in compiled binary
- Disable tryNative so jiti handles all nested imports
- Lazy-load pi-coding-agent to avoid circular dependency
This commit is contained in:
Mario Zechner 2026-01-13 04:55:11 +01:00
parent cedd8fe306
commit 1919fd7c9c
4 changed files with 243 additions and 29 deletions

View file

@ -6,7 +6,7 @@
### Fixed
- Extensions now load correctly in compiled Bun binary by using jiti for module resolution with proper alias handling
- Extensions now load correctly in compiled Bun binary using `@mariozechner/jiti` fork with `virtualModules` support. Bundled packages (`@sinclair/typebox`, `@mariozechner/pi-tui`, `@mariozechner/pi-ai`, `@mariozechner/pi-coding-agent`) are accessible to extensions without filesystem node_modules.
## [0.45.1] - 2026-01-13

View file

@ -39,6 +39,7 @@
},
"dependencies": {
"@mariozechner/clipboard": "^0.3.0",
"@mariozechner/jiti": "^2.6.2",
"@mariozechner/pi-agent-core": "^0.45.2",
"@mariozechner/pi-ai": "^0.45.2",
"@mariozechner/pi-tui": "^0.45.2",
@ -47,7 +48,6 @@
"diff": "^8.0.2",
"file-type": "^21.1.1",
"glob": "^11.0.3",
"jiti": "^2.6.1",
"marked": "^15.0.12",
"minimatch": "^10.1.1",
"proper-lockfile": "^4.1.2",

View file

@ -1,5 +1,7 @@
/**
* Extension loader - loads TypeScript extension modules using jiti.
*
* Uses @mariozechner/jiti fork with virtualModules support for compiled Bun binaries.
*/
import * as fs from "node:fs";
@ -7,9 +9,15 @@ import { createRequire } from "node:module";
import * as os from "node:os";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { createJiti } from "@mariozechner/jiti";
import * as _bundledPiAi from "@mariozechner/pi-ai";
import type { KeyId } from "@mariozechner/pi-tui";
import { createJiti } from "jiti";
import { getAgentDir } from "../../config.js";
import * as _bundledPiTui from "@mariozechner/pi-tui";
// Static imports of packages that extensions may use.
// These MUST be static so Bun bundles them into the compiled binary.
// The virtualModules option then makes them available to extensions.
import * as _bundledTypebox from "@sinclair/typebox";
import { getAgentDir, isBunBinary } from "../../config.js";
import { createEventBus, type EventBus } from "../event-bus.js";
import type { ExecOptions } from "../exec.js";
import { execCommand } from "../exec.js";
@ -24,8 +32,28 @@ import type {
ToolDefinition,
} from "./types.js";
/** Modules available to extensions via virtualModules (for compiled Bun binary) */
let _lazyPiCodingAgent: unknown;
const VIRTUAL_MODULES: Record<string, unknown> = {
"@sinclair/typebox": _bundledTypebox,
"@mariozechner/pi-tui": _bundledPiTui,
"@mariozechner/pi-ai": _bundledPiAi,
// Lazy-loaded to avoid circular dependency (loader.ts is part of pi-coding-agent)
get "@mariozechner/pi-coding-agent"() {
if (!_lazyPiCodingAgent) {
// Dynamic require after module initialization completes
_lazyPiCodingAgent = require("../../index.js");
}
return _lazyPiCodingAgent;
},
};
const require = createRequire(import.meta.url);
/**
* Get aliases for jiti (used in Node.js/development mode).
* In Bun binary mode, virtualModules is used instead.
*/
let _aliases: Record<string, string> | null = null;
function getAliases(): Record<string, string> {
if (_aliases) return _aliases;
@ -33,27 +61,16 @@ function getAliases(): Record<string, string> {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packageIndex = path.resolve(__dirname, "../..", "index.js");
// Debug: log what we're resolving
if (process.env.DEBUG_EXTENSIONS) {
console.error("[DEBUG] import.meta.url:", import.meta.url);
console.error("[DEBUG] __dirname:", __dirname);
}
const typeboxEntry = require.resolve("@sinclair/typebox");
const typeboxRoot = typeboxEntry.replace(/\/build\/cjs\/index\.js$/, "");
_aliases = {
"@mariozechner/pi-coding-agent": packageIndex,
"@mariozechner/pi-coding-agent/extensions": path.resolve(__dirname, "index.js"),
"@mariozechner/pi-tui": require.resolve("@mariozechner/pi-tui"),
"@mariozechner/pi-ai": require.resolve("@mariozechner/pi-ai"),
"@sinclair/typebox": typeboxRoot,
};
if (process.env.DEBUG_EXTENSIONS) {
console.error("[DEBUG] aliases:", JSON.stringify(_aliases, null, 2));
}
return _aliases;
}
@ -224,12 +241,15 @@ function createExtensionAPI(
return api;
}
async function loadExtensionModule(path: string) {
async function loadExtensionModule(extensionPath: string) {
const jiti = createJiti(import.meta.url, {
alias: getAliases(),
// In Bun binary: use virtualModules for bundled packages (no filesystem resolution)
// Also disable tryNative so jiti handles ALL imports (not just the entry point)
// In Node.js/dev: use aliases to resolve to node_modules paths
...(isBunBinary ? { virtualModules: VIRTUAL_MODULES, tryNative: false } : { alias: getAliases() }),
});
const module = await jiti.import(path, { default: true });
const module = await jiti.import(extensionPath, { default: true });
const factory = module as ExtensionFactory;
return typeof factory !== "function" ? undefined : factory;
}