Files
oh-my-pi/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts
T

2533 lines
93 KiB
TypeScript

/// <reference path="./legacy-pi-virtual-modules.d.ts" />
import * as fs from "node:fs";
import { createRequire, isBuiltin } from "node:module";
import * as path from "node:path";
import * as url from "node:url";
import type { ParseResult, ParserPlugin } from "@babel/parser";
import { parse as parseBabel } from "@babel/parser";
import { isCompiledBinary, stripWindowsExtendedLengthPathPrefix } from "@oh-my-pi/pi-utils";
import { registerPluginCacheInvalidator } from "../../discovery/helpers";
const IS_COMPILED_BINARY = isCompiledBinary();
// === Bundled host modules (issue #3423) ===
//
// Bun 1.3.14 stopped exposing `--compile` extras through any filesystem-style
// API: `fs.existsSync`, `Bun.file().exists()`, `Bun.resolveSync`, and even
// `import("/$bunfs/...")` / `import("file:///$bunfs/...")` all fail for the
// embedded entries. Bun.plugin `onResolve` also no longer fires for transitive
// imports inside runtime-loaded extensions.
//
// Compiled builds retain lazy loaders for host packages and serve requested
// surfaces through `omp-legacy-pi-bundled:<key>` synthetic modules.
// `scripts/legacy-pi-virtual-module.ts` derives literal dynamic-import edges
// from current package exports inside a Bun build plugin: no generated source
// or duplicate key list exists on disk. Deferring each host module evaluation
// avoids cycles with an extension-loading command that is itself in the
// retained package graph.
const BUNDLED_VIRTUAL_SCHEME = "omp-legacy-pi-bundled:";
const BUNDLED_VIRTUAL_NAMESPACE = "omp-legacy-pi-bundled";
const BUNDLED_MODULES_GLOBAL = "__ompLegacyPiBundledModules";
const TYPEBOX_BUNDLED_MODULE_KEY = "typebox";
type BundledModule = Readonly<Record<string, unknown>>;
type BundledModules = Readonly<Record<string, BundledModule>>;
type BundledModuleLoaders = Readonly<Record<string, () => Promise<BundledModule>>>;
interface LegacyPiResolveResult {
path: string;
namespace?: string;
}
interface BundledVirtualResolveResult {
path: string;
namespace: typeof BUNDLED_VIRTUAL_NAMESPACE;
}
interface ExtensionSpecifierReference {
readonly kind: "import" | "require";
readonly specifier: string;
readonly start: number;
readonly end: number;
}
function parseExtensionSource(source: string, importerPath: string): ParseResult {
const extension = path.extname(importerPath).toLowerCase();
const plugins: ParserPlugin[] = ["decorators-legacy", "explicitResourceManagement"];
if (extension === ".ts" || extension === ".mts" || extension === ".cts" || extension === ".tsx") {
plugins.push("typescript");
}
if (extension === ".jsx" || extension === ".tsx") {
plugins.push("jsx");
}
try {
return parseBabel(source, {
sourceType: "unambiguous",
allowAwaitOutsideFunction: true,
allowReturnOutsideFunction: true,
allowImportExportEverywhere: true,
allowNewTargetOutsideFunction: true,
allowSuperOutsideMethod: true,
allowUndeclaredExports: true,
errorRecovery: true,
plugins,
});
} catch (error) {
throw new Error(
`Failed to parse extension source for dependency rewriting: ${importerPath}: ${error instanceof Error ? error.message : String(error)}`,
{ cause: error },
);
}
}
const REQUIRE_BINDING = 1 << 0;
const OBJECT_BINDING = 1 << 1;
const EXPORTS_BINDING = 1 << 2;
const MODULE_BINDING = 1 << 3;
interface StructuralAstNode {
readonly type: string;
readonly [key: string]: unknown;
}
interface BindingScope {
readonly parent: BindingScope | null;
readonly ownsVarBindings: boolean;
bindings: number;
}
interface ScopedAstNode {
readonly node: StructuralAstNode;
readonly scope: BindingScope;
readonly order: number;
}
interface ScopeWalkItem {
readonly node: StructuralAstNode;
readonly scope: BindingScope | null;
readonly parent: StructuralAstNode | null;
readonly parentKey: string | null;
}
function asAstNode(value: unknown): StructuralAstNode | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const candidate = value as { readonly type?: unknown };
return typeof candidate.type === "string" ? (candidate as StructuralAstNode) : null;
}
function nodeArray(node: StructuralAstNode, key: string): readonly unknown[] | null {
const value = node[key];
return Array.isArray(value) ? value : null;
}
function nodeArgument(node: StructuralAstNode | null, index: number): StructuralAstNode | null {
if (!node) return null;
const values = nodeArray(node, "arguments");
return values ? asAstNode(values[index]) : null;
}
function isIdentifier(node: StructuralAstNode | null, name: string): boolean {
return node?.type === "Identifier" && node.name === name;
}
function trackedBinding(name: unknown): number {
switch (name) {
case "require":
return REQUIRE_BINDING;
case "Object":
return OBJECT_BINDING;
case "exports":
return EXPORTS_BINDING;
case "module":
return MODULE_BINDING;
default:
return 0;
}
}
function addPatternBindings(scope: BindingScope, pattern: unknown): void {
const stack: unknown[] = [pattern];
while (stack.length > 0) {
const node = asAstNode(stack.pop());
if (!node) continue;
switch (node.type) {
case "Identifier":
scope.bindings |= trackedBinding(node.name);
break;
case "AssignmentPattern":
stack.push(node.left);
break;
case "RestElement":
stack.push(node.argument);
break;
case "ArrayPattern": {
const elements = nodeArray(node, "elements");
if (elements) stack.push(...elements);
break;
}
case "ObjectPattern": {
const properties = nodeArray(node, "properties");
if (!properties) break;
for (const value of properties) {
const property = asAstNode(value);
if (!property) continue;
stack.push(property.type === "RestElement" ? property.argument : property.value);
}
break;
}
case "TSParameterProperty":
stack.push(node.parameter);
break;
}
}
}
function isFunctionScopeNode(node: StructuralAstNode): boolean {
switch (node.type) {
case "FunctionDeclaration":
case "FunctionExpression":
case "ArrowFunctionExpression":
case "ObjectMethod":
case "ClassMethod":
case "ClassPrivateMethod":
case "TSDeclareFunction":
case "TSDeclareMethod":
case "DeclareFunction":
return true;
default:
return false;
}
}
function isFunctionDeclarationNode(node: StructuralAstNode): boolean {
return node.type === "FunctionDeclaration" || node.type === "TSDeclareFunction" || node.type === "DeclareFunction";
}
function isClassScopeNode(node: StructuralAstNode): boolean {
return node.type === "ClassDeclaration" || node.type === "ClassExpression";
}
function scopeKind(node: StructuralAstNode, parent: StructuralAstNode | null, parentKey: string | null): 0 | 1 | 2 {
if (
node.type === "Program" ||
isFunctionScopeNode(node) ||
node.type === "StaticBlock" ||
node.type === "TSModuleBlock"
) {
return 2;
}
if (
isClassScopeNode(node) ||
node.type === "CatchClause" ||
node.type === "ForStatement" ||
node.type === "ForInStatement" ||
node.type === "ForOfStatement" ||
node.type === "SwitchStatement"
) {
return 1;
}
if (node.type === "BlockStatement" && !(parent && isFunctionScopeNode(parent) && parentKey === "body")) {
return 1;
}
return 0;
}
function nearestVarScope(scope: BindingScope): BindingScope {
let current = scope;
while (!current.ownsVarBindings && current.parent) current = current.parent;
return current;
}
function registerOuterDeclaration(node: StructuralAstNode, scope: BindingScope | null): void {
if (!scope) return;
if (
(isFunctionDeclarationNode(node) && node.type !== "TSDeclareFunction" && node.type !== "DeclareFunction") ||
(node.type === "ClassDeclaration" && node.declare !== true)
) {
addPatternBindings(scope, node.id);
}
}
function registerScopeBindings(node: StructuralAstNode, scope: BindingScope): void {
if (isFunctionScopeNode(node)) {
addPatternBindings(scope, node.id);
const parameters = nodeArray(node, "params");
if (parameters) {
for (const parameter of parameters) addPatternBindings(scope, parameter);
}
}
if (isClassScopeNode(node)) addPatternBindings(scope, node.id);
if (node.type === "CatchClause") addPatternBindings(scope, node.param);
if (node.type === "ImportDeclaration") {
const specifiers = nodeArray(node, "specifiers");
if (specifiers) {
for (const value of specifiers) {
const specifier = asAstNode(value);
if (specifier) addPatternBindings(scope, specifier.local);
}
}
} else if (node.type === "TSImportEqualsDeclaration") {
addPatternBindings(scope, node.id);
} else if (node.type === "VariableDeclaration") {
const target = node.kind === "var" ? nearestVarScope(scope) : scope;
const declarations = nodeArray(node, "declarations");
if (declarations) {
for (const value of declarations) {
const declaration = asAstNode(value);
if (declaration) addPatternBindings(target, declaration.id);
}
}
}
}
function isAstMetadataKey(key: string): boolean {
switch (key) {
case "loc":
case "extra":
case "range":
case "comments":
case "tokens":
case "errors":
case "leadingComments":
case "trailingComments":
case "innerComments":
case "parent":
case "parentPath":
case "scope":
case "hub":
return true;
default:
return false;
}
}
function scopeForChild(
node: StructuralAstNode,
key: string,
outerScope: BindingScope | null,
nodeScope: BindingScope,
): BindingScope {
if (node.type === "SwitchStatement" && key === "discriminant") {
return outerScope ?? nodeScope;
}
if (isFunctionScopeNode(node) && (key === "key" || key === "decorators")) {
return outerScope ?? nodeScope;
}
if (isClassScopeNode(node) && key === "decorators") {
return outerScope ?? nodeScope;
}
return nodeScope;
}
/**
* Builds only the lexical information needed by legacy extension rewriting.
* Scope frames are fully populated before selected nodes are returned, so
* hoisted and TDZ bindings behave independently of textual declaration order.
*/
function collectScopedAstNodes(root: unknown, select: (node: StructuralAstNode) => boolean): ScopedAstNode[] {
const rootNode = asAstNode(root);
if (!rootNode) return [];
const selected: ScopedAstNode[] = [];
const stack: ScopeWalkItem[] = [{ node: rootNode, scope: null, parent: null, parentKey: null }];
const seen = new WeakSet<object>();
let order = 0;
while (stack.length > 0) {
const item = stack.pop();
if (!item || seen.has(item.node)) continue;
seen.add(item.node);
registerOuterDeclaration(item.node, item.scope);
const kind = scopeKind(item.node, item.parent, item.parentKey);
const activeScope: BindingScope | null =
kind === 0
? item.scope
: {
parent: item.scope,
ownsVarBindings: kind === 2,
bindings: 0,
};
if (activeScope) {
registerScopeBindings(item.node, activeScope);
if (select(item.node)) selected.push({ node: item.node, scope: activeScope, order });
}
order++;
for (const key in item.node) {
if (isAstMetadataKey(key)) continue;
const childScope = activeScope ? scopeForChild(item.node, key, item.scope, activeScope) : null;
const value = item.node[key];
if (Array.isArray(value)) {
for (const element of value) {
const child = asAstNode(element);
if (child) stack.push({ node: child, scope: childScope, parent: item.node, parentKey: key });
}
} else {
const child = asAstNode(value);
if (child) stack.push({ node: child, scope: childScope, parent: item.node, parentKey: key });
}
}
}
selected.sort((left, right) => {
const leftStart = typeof left.node.start === "number" ? left.node.start : Number.MAX_SAFE_INTEGER;
const rightStart = typeof right.node.start === "number" ? right.node.start : Number.MAX_SAFE_INTEGER;
return leftStart - rightStart || left.order - right.order;
});
return selected;
}
function scopeHasBinding(scope: BindingScope, binding: number): boolean {
let current: BindingScope | null = scope;
while (current) {
if ((current.bindings & binding) !== 0) return true;
current = current.parent;
}
return false;
}
function isSpecifierReferenceNode(node: StructuralAstNode): boolean {
switch (node.type) {
case "ImportDeclaration":
case "ExportNamedDeclaration":
case "ExportAllDeclaration":
case "ImportExpression":
case "TSImportEqualsDeclaration":
case "CallExpression":
return true;
default:
return false;
}
}
function isUncomputedMember(node: StructuralAstNode | null, objectName: string, propertyName: string): boolean {
if (node?.type !== "MemberExpression" || node.computed === true) return false;
return isIdentifier(asAstNode(node.object), objectName) && isIdentifier(asAstNode(node.property), propertyName);
}
function isUnshadowedExportsTarget(node: StructuralAstNode | null, scope: BindingScope): boolean {
return (
(isIdentifier(node, "exports") && !scopeHasBinding(scope, EXPORTS_BINDING)) ||
(isUncomputedMember(node, "module", "exports") && !scopeHasBinding(scope, MODULE_BINDING))
);
}
function isGlobalRequireCall(node: StructuralAstNode | null, scope: BindingScope): boolean {
return (
node?.type === "CallExpression" &&
isIdentifier(asAstNode(node.callee), "require") &&
!scopeHasBinding(scope, REQUIRE_BINDING)
);
}
function staticMemberPropertyName(node: StructuralAstNode): string | null {
const property = asAstNode(node.property);
if (node.computed !== true && property?.type === "Identifier" && typeof property.name === "string") {
return property.name;
}
if (node.computed === true && property?.type === "StringLiteral" && typeof property.value === "string") {
return property.value;
}
return null;
}
function staticObjectPropertyName(node: StructuralAstNode): string | null {
if (node.computed === true) return null;
const key = asAstNode(node.key);
if (key?.type === "Identifier" && typeof key.name === "string") return key.name;
return key?.type === "StringLiteral" && typeof key.value === "string" ? key.value : null;
}
function collectExtensionSpecifierReferences(
source: string,
importerPath: string,
ast: ParseResult = parseExtensionSource(source, importerPath),
): ExtensionSpecifierReference[] {
const references: ExtensionSpecifierReference[] = [];
const record = (kind: ExtensionSpecifierReference["kind"], literal: unknown): void => {
const node = asAstNode(literal);
if (
node?.type === "StringLiteral" &&
typeof node.value === "string" &&
typeof node.start === "number" &&
typeof node.end === "number"
) {
references.push({ kind, specifier: node.value, start: node.start, end: node.end });
}
};
for (const { node, scope } of collectScopedAstNodes(ast, isSpecifierReferenceNode)) {
if (
node.type === "ImportDeclaration" ||
node.type === "ExportNamedDeclaration" ||
node.type === "ExportAllDeclaration"
) {
record("import", node.source);
} else if (node.type === "ImportExpression") {
record("import", node.source);
} else if (node.type === "TSImportEqualsDeclaration") {
const moduleReference = asAstNode(node.moduleReference);
if (moduleReference?.type === "TSExternalModuleReference") {
record("require", moduleReference.expression);
}
} else if (node.type === "CallExpression") {
const callee = asAstNode(node.callee);
if (callee?.type === "Import") {
record("import", nodeArgument(node, 0));
} else if (isIdentifier(callee, "require") && !scopeHasBinding(scope, REQUIRE_BINDING)) {
record("require", nodeArgument(node, 0));
}
}
}
return references;
}
function applySpecifierReplacements(
source: string,
replacements: ReadonlyArray<ExtensionSpecifierReference & { readonly replacement: string }>,
): string {
let rewritten = source;
for (const reference of [...replacements].sort((left, right) => right.start - left.start)) {
rewritten = `${rewritten.slice(0, reference.start)}${JSON.stringify(reference.replacement)}${rewritten.slice(reference.end)}`;
}
return rewritten;
}
const loadedBundledModules: Record<string, BundledModule> = {};
let bundledModuleLoadersPromise: Promise<BundledModuleLoaders> | null = null;
/**
* Load the build-supplied module registry without evaluating its host modules.
*
* `globalThis` bridges the synthetic ES modules, which cannot close over this
* file's lexical scope. Dev/test runs never execute the conditional import;
* binary builds resolve it through the in-memory build plugin.
*/
function ensureBundledModuleLoadersLoaded(): Promise<BundledModuleLoaders> {
if (!IS_COMPILED_BINARY) {
return Promise.reject(new Error("omp:legacy-pi-shim: bundled modules are only available in compiled mode"));
}
if (!bundledModuleLoadersPromise) {
bundledModuleLoadersPromise = import("omp-legacy-pi-modules").then(module => {
Reflect.set(globalThis, BUNDLED_MODULES_GLOBAL, loadedBundledModules);
return module.BUNDLED_PI_MODULE_LOADERS;
});
}
return bundledModuleLoadersPromise;
}
async function loadBundledModule(moduleKey: string): Promise<void> {
const loaders = await ensureBundledModuleLoadersLoaded();
const loader = loaders[moduleKey];
if (!loader) {
throw new Error(`omp:legacy-pi-shim: no bundled module registered for ${moduleKey}`);
}
loadedBundledModules[moduleKey] = await loader();
}
function bundledModuleVirtualSpecifier(moduleKey: string): string {
return `${BUNDLED_VIRTUAL_SCHEME}${moduleKey}`;
}
function isBundledVirtualSpecifier(value: string): boolean {
return value.startsWith(BUNDLED_VIRTUAL_SCHEME);
}
function toLegacyPiResolveResult(resolvedPath: string): LegacyPiResolveResult {
if (isBundledVirtualSpecifier(resolvedPath)) {
const registryKey = resolvedPath.slice(BUNDLED_VIRTUAL_SCHEME.length);
return { path: registryKey, namespace: BUNDLED_VIRTUAL_NAMESPACE };
}
return { path: resolvedPath };
}
/** Maps a bundled virtual specifier or registry key to Bun's plugin namespace shape. */
export function resolveBundledVirtualSpecifier(specifier: string): BundledVirtualResolveResult {
const registryKey = isBundledVirtualSpecifier(specifier)
? specifier.slice(BUNDLED_VIRTUAL_SCHEME.length)
: specifier;
if (!registryKey) {
throw new Error("omp:legacy-pi-shim: bundled virtual specifier has no registry key");
}
return { path: registryKey, namespace: BUNDLED_VIRTUAL_NAMESPACE };
}
/**
* Build a synthetic ES module for one live bundled namespace. Every export
* reads through the global bridge; no bunfs path or copied package is involved.
*/
function synthesizeBundledModuleSourceFromModules(moduleKey: string, modules: BundledModules): string {
const mod = modules[moduleKey];
if (!mod) {
throw new Error(`omp:legacy-pi-shim: no bundled module registered for ${moduleKey}`);
}
const lines: string[] = [
`const __omp_bundled = globalThis[${JSON.stringify(BUNDLED_MODULES_GLOBAL)}][${JSON.stringify(moduleKey)}];`,
];
let hasDefault = false;
for (const exportName in mod) {
if (exportName === "default") {
hasDefault = true;
continue;
}
lines.push(`export const ${exportName} = __omp_bundled[${JSON.stringify(exportName)}];`);
}
if (hasDefault) {
lines.push("export default __omp_bundled.default;");
}
lines.push("");
return lines.join("\n");
}
/**
* Build the synthetic source served for one
* `omp-legacy-pi-bundled:<key>` import.
*/
async function synthesizeBundledModuleSource(moduleKey: string): Promise<string> {
await loadBundledModule(moduleKey);
return synthesizeBundledModuleSourceFromModules(moduleKey, loadedBundledModules);
}
/** Test seam for the virtual module's named/default export forwarding. */
export function __synthesizeLegacyPiBundledSourceWithModules(
moduleKey: string,
modules: Readonly<Record<string, Readonly<Record<string, unknown>>>>,
): string {
return synthesizeBundledModuleSourceFromModules(moduleKey, modules);
}
/** Test seam for the global bridge key shared with synthetic module source. */
export function __getLegacyPiBundledModulesGlobal(): string {
return BUNDLED_MODULES_GLOBAL;
}
// Canonical scope for in-process pi packages. Plugins published against any of
// the aliased scopes below (mariozechner's original publish, earendil-works'
// fork, or the canonical @oh-my-pi scope itself) are remapped to this scope and
// resolved against the bundled copy that ships inside the omp binary. This
// keeps plugins running against the exact runtime state of the host (single
// module registry, single tool registry, etc.) regardless of which historical
// scope name they happened to declare in their peerDependencies.
const CANONICAL_PI_SCOPE = "@oh-my-pi";
// Scopes that have historically been used to publish (or alias) the same set
// of internal pi-* packages. `@oh-my-pi` is intentionally included so direct
// canonical imports still pass through the same host-bundled package resolution
// path instead of pulling a duplicate copy from plugin node_modules.
const PI_SCOPE_ALIASES = ["oh-my-pi", "mariozechner", "earendil-works"] as const;
// Internal pi-* package basenames bundled inside the omp binary.
const PI_PACKAGE_NAMES = ["pi-agent-core", "pi-ai", "pi-coding-agent", "pi-natives", "pi-tui", "pi-utils"] as const;
const PI_SCOPE_ALTERNATION = PI_SCOPE_ALIASES.join("|");
const PI_PACKAGE_ALTERNATION = PI_PACKAGE_NAMES.join("|");
// Upstream `@mariozechner/*` packages exposed a few subpaths at the package
// root that we relocated under a different folder. Each entry rewrites
// `<pkg>/<from>` → `<pkg>/<to>` after the scope has been canonicalised, so
// plugins importing the upstream layout still resolve to a real file in our
// bundled copy. Entries ending in `/` rewrite the whole subtree; add new
// `pkg/from -> pkg/to` pairs whenever an upstream-only subpath breaks resolution.
const PI_SUBPATH_REMAPS: ReadonlyMap<string, string> = new Map<string, string>([
["pi-ai/utils/oauth", "pi-ai/oauth"],
["pi-ai/utils/oauth/", "pi-ai/oauth/"],
["pi-ai/compat", "pi-ai"],
]);
function remapLegacyPiSubpath(rest: string): string {
const exact = PI_SUBPATH_REMAPS.get(rest);
if (exact) {
return exact;
}
for (const [from, to] of PI_SUBPATH_REMAPS) {
if (from.endsWith("/") && rest.startsWith(from)) {
return `${to}${rest.slice(from.length)}`;
}
}
return rest;
}
const LEGACY_PI_SPECIFIER_FILTER = new RegExp(`^@(?:${PI_SCOPE_ALTERNATION})/(?:${PI_PACKAGE_ALTERNATION})(?:/.*)?$`);
const resolvedSpecifierFallbacks = new Map<string, string>();
const SOURCE_MODULE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"] as const;
const SUPPORTED_PACKAGE_IMPORT_CONDITIONS = new Set(["bun", "node", "import", "default"]);
const SUPPORTED_PACKAGE_REQUIRE_CONDITIONS = new Set(["bun", "node", "require", "default"]);
const packageRootCache = new Map<string, string | null>();
const packageImportsCache = new Map<string, Record<string, unknown> | null>();
const nodePackageRootCache = new Map<string, Promise<string | null>>();
const packageManifestCache = new Map<string, Promise<Record<string, unknown> | null>>();
const bareDependencyResolutionCache = new Map<string, Promise<string | null>>();
const bareRequireResolutionCache = new Map<string, Promise<string | null>>();
const realpathCache = new Map<string, Promise<string>>();
const nativeAddonResolutionCache = new Map<string, Promise<string | null>>();
const nativeAddonRequireScanCache = new Map<string, Promise<boolean>>();
const nativeAddonLoaderModulePaths = new Set<string>();
function clearLegacyPiResolutionCaches(): void {
resolvedSpecifierFallbacks.clear();
packageRootCache.clear();
packageImportsCache.clear();
nodePackageRootCache.clear();
packageManifestCache.clear();
bareDependencyResolutionCache.clear();
bareRequireResolutionCache.clear();
nativeAddonResolutionCache.clear();
nativeAddonRequireScanCache.clear();
nativeAddonLoaderModulePaths.clear();
realpathCache.clear();
}
registerPluginCacheInvalidator(clearLegacyPiResolutionCaches);
const PACKAGE_IMPORT_EXCLUDED = Symbol("packageImportExcluded");
// Extensions that imported TypeBox directly used to resolve against a real
// `@sinclair/typebox` or `typebox` install. The runtime dep was replaced with
// the Zod-backed shim under `extensibility/typebox.ts`; plugins still importing
// either public name are redirected to that shim so existing extensions keep
// working without code changes. Submodules like `@sinclair/typebox/compiler`
// are intentionally not remapped — those expose TypeBox-only APIs the shim does
// not provide and plugins relying on them must vendor TypeBox directly.
const TYPEBOX_SPECIFIER_FILTER = /^(?:@sinclair\/typebox|typebox)$/;
// Compat-shim path resolution. In compiled-binary mode every bundled surface
// is served through the `omp-legacy-pi-bundled:` virtual namespace (see the
// bundled-module block above) — bunfs paths are unreachable on Bun 1.3.14+, so the
// pre-#3423 helpers that derived `/$bunfs/root/...` paths from
// `import.meta.dir` are gone. Dev / source-link / installed-package modes
// still need a real filesystem path for the source shims, which
// `sourceShimPath` computes either from the npm prebuilt `dist/cli.js`
// bundle (`PI_BUNDLED=true`) or directly from the monorepo source tree.
/**
* Compute the package root for the npm prebuilt `dist/cli.js` bundle.
*
* `bundle-dist.ts` defines `process.env.PI_BUNDLED="true"`; after bundling,
* `import.meta.dir` points at `<package>/dist`. Do not resolve the package via
* bare `@oh-my-pi/pi-coding-agent` here: from a global install Bun can pick an
* older cache entry, recreating mixed-runtime plugin loading.
*/
export function __computeBundledSelfPackageRoot(metaDir: string, pathImpl: typeof path = path): string {
const normalizedMetaDir = pathImpl.normalize(metaDir);
if (pathImpl.basename(normalizedMetaDir) === "dist") {
return pathImpl.resolve(metaDir, "..");
}
const pluginsDirSuffix = pathImpl.join("src", "extensibility", "plugins");
if (normalizedMetaDir.endsWith(pluginsDirSuffix)) {
return pathImpl.resolve(metaDir, "..", "..", "..");
}
return pathImpl.resolve(metaDir);
}
function resolveBundledSelfPackageRoot(): string | undefined {
if (!process.env.PI_BUNDLED) return undefined;
return __computeBundledSelfPackageRoot(import.meta.dir);
}
const BUNDLED_SELF_PACKAGE_ROOT = resolveBundledSelfPackageRoot();
function sourceShimPath(file: string): string {
return BUNDLED_SELF_PACKAGE_ROOT
? path.join(BUNDLED_SELF_PACKAGE_ROOT, "src", "extensibility", file)
: path.resolve(import.meta.dir, "..", file);
}
/**
* Resolve the path the TypeBox compatibility shim ships at, then drop it when
* the source file is missing.
*
* In compiled-binary mode the shim is served through the
* `omp-legacy-pi-bundled:` virtual namespace (issue #3423) — bunfs paths are
* unreachable on Bun 1.3.14+, so the virtual specifier is always available and
* needs no filesystem probe. In dev / source-link / installed-package mode the
* shim is an on-disk source file; validation mirrors
* `__validateLegacyPiPackageRootOverrides` (#2168): if the computed candidate
* doesn't exist (e.g. an install that dropped the source — issue #3414),
* `resolveTypeBoxSpecifier` returns `undefined` and
* `rewriteLegacyExtensionSource` leaves bare `typebox` / `@sinclair/typebox`
* specifiers alone, so Bun falls through to native resolution against the
* extension's own `node_modules`.
*
* Exported for tests; production callers use `TYPEBOX_SHIM_PATH`.
*/
export function __resolveTypeBoxShimPath(
isCompiled: boolean,
sourcePath: string,
pathExistsSync: (p: string) => boolean = fs.existsSync,
): string | null {
if (isCompiled) {
return bundledModuleVirtualSpecifier(TYPEBOX_BUNDLED_MODULE_KEY);
}
return pathExistsSync(sourcePath) ? sourcePath : null;
}
const TYPEBOX_SHIM_PATH = __resolveTypeBoxShimPath(IS_COMPILED_BINARY, sourceShimPath("typebox.ts"));
// Legacy extensions historically imported `Type` (and `Static`/`TSchema`) from
// the package root of `@(scope)/pi-ai`. pi-ai 15.1.0 removed the runtime `Type`
// export (see `packages/ai/CHANGELOG.md`), so the bare canonical specifier no
// longer satisfies those imports. The override below redirects only the bare
// pi-ai package root onto a sibling shim that re-exports the canonical surface
// plus the borrowed `Type` runtime from the Zod-backed TypeBox shim. Subpath
// imports such as `@oh-my-pi/pi-ai/oauth` continue to resolve directly
// against the bundled pi-ai package.
const LEGACY_PI_AI_SHIM_PATH = IS_COMPILED_BINARY
? bundledModuleVirtualSpecifier(`${CANONICAL_PI_SCOPE}/pi-ai`)
: sourceShimPath("legacy-pi-ai-shim.ts");
// The coding-agent's own `./src/index.ts` cannot be listed as an extra
// `bun --compile` entrypoint alongside the CLI entry without breaking binary
// startup (issue #1474 follow-up). In compiled-binary mode the legacy
// `@(scope)/pi-coding-agent` root therefore resolves through the bundled
// module shim; in dev / source-link / installed-package mode it points at the
// sibling source shim whose distinct file path avoids the #1474 collision
// while still re-exporting the canonical package surface.
const LEGACY_PI_CODING_AGENT_SHIM_PATH = IS_COMPILED_BINARY
? bundledModuleVirtualSpecifier(`${CANONICAL_PI_SCOPE}/pi-coding-agent`)
: sourceShimPath("legacy-pi-coding-agent-shim.ts");
// Legacy pi-tui exported `decodeKittyPrintable` from its package root. The
// canonical TUI replaced it with the broader `decodePrintableKey`; route only
// legacy root imports through a sibling shim that preserves the old name.
const LEGACY_PI_TUI_SHIM_PATH = IS_COMPILED_BINARY
? bundledModuleVirtualSpecifier(`${CANONICAL_PI_SCOPE}/pi-tui`)
: sourceShimPath("legacy-pi-tui-shim.ts");
// Package-root overrides. Shim entries (`pi-ai`, `pi-coding-agent`, `pi-tui`)
// always replace the canonical surface so legacy helpers stay reachable. The
// other bundled host packages (`pi-agent-core`, `pi-natives`, `pi-utils`) are
// added only in compiled-binary mode to route extensions onto the in-process
// module instance — in dev / source-link / installed-package mode the canonical
// specifier resolves cleanly through `Bun.resolveSync` and hardcoding a
// source-tree path would miss installs where bundled packages live at
// `node_modules/@oh-my-pi/pi-*`.
//
// Compiled-binary entries are `omp-legacy-pi-bundled:<key>` specifiers handed
// to the synthetic onLoad in `installLegacyPiSpecifierShim()` — bunfs paths
// are unusable on Bun 1.3.14+ (issue #3423). Filesystem-shaped overrides are
// still validated against on-disk presence so a missing dev-mode shim falls
// through to `getResolvedSpecifier`.
/**
* Drop overrides whose filesystem targets are missing so they can fall
* through to the canonical-resolution path. Virtual `omp-legacy-pi-bundled:`
* entries always pass — live bundled module references are the source of truth
* in compiled mode where bunfs paths are unreachable (issue #3423).
*
* `pathExistsSync` defaults to `fs.existsSync`; tests inject a stub to
* simulate the missing-entrypoint failure mode without touching the real FS.
*/
export function __validateLegacyPiPackageRootOverrides(
candidates: Record<string, string>,
pathExistsSync: (p: string) => boolean = fs.existsSync,
): Record<string, string> {
const valid: Record<string, string> = {};
for (const key in candidates) {
const candidate = candidates[key];
if (candidate && (isBundledVirtualSpecifier(candidate) || pathExistsSync(candidate))) {
valid[key] = candidate;
}
}
return valid;
}
/**
* Compute the override map keyed by every canonical specifier the host serves
* directly: the pi-ai / pi-coding-agent roots (compat shims that re-attach
* legacy helpers) plus, in compiled mode, every build-supplied module key.
* Subpath coverage stops `@(scope)/pi-ai/oauth` and friends from falling
* through to the extension's absent peer install when bunfs walks fail.
*/
export function __buildLegacyPiPackageRootOverrides(
isCompiled: boolean,
bundledModuleKeys: Iterable<string> = [],
): Record<string, string> {
const candidates: Record<string, string> = {
[`${CANONICAL_PI_SCOPE}/pi-ai`]: LEGACY_PI_AI_SHIM_PATH,
[`${CANONICAL_PI_SCOPE}/pi-coding-agent`]: LEGACY_PI_CODING_AGENT_SHIM_PATH,
[`${CANONICAL_PI_SCOPE}/pi-tui`]: LEGACY_PI_TUI_SHIM_PATH,
};
if (isCompiled) {
for (const key of bundledModuleKeys) {
// Shim-bearing roots already map to their compat surfaces; TypeBox
// has a dedicated TYPEBOX_SHIM_PATH route.
if (key in candidates || key === TYPEBOX_BUNDLED_MODULE_KEY) continue;
candidates[key] = bundledModuleVirtualSpecifier(key);
}
}
return __validateLegacyPiPackageRootOverrides(candidates);
}
// Seeded with compat roots at module init; first compiled extension load adds
// every key supplied by the in-memory build module.
let legacyPiPackageRootOverrides = __buildLegacyPiPackageRootOverrides(IS_COMPILED_BINARY);
let legacyPiOverridesReadyPromise: Promise<void> | null = null;
/** Complete compiled-mode overrides from the lazy host-module registry. */
function ensureLegacyPiOverridesReady(): Promise<void> {
if (!IS_COMPILED_BINARY) {
return Promise.resolve();
}
if (!legacyPiOverridesReadyPromise) {
legacyPiOverridesReadyPromise = ensureBundledModuleLoadersLoaded().then(loaders => {
legacyPiPackageRootOverrides = __buildLegacyPiPackageRootOverrides(true, Object.keys(loaders));
});
}
return legacyPiOverridesReadyPromise;
}
let isLegacyPiSpecifierShimInstalled = false;
function remapLegacyPiSpecifier(specifier: string): string | null {
if (!LEGACY_PI_SPECIFIER_FILTER.test(specifier)) {
return null;
}
const slashIdx = specifier.indexOf("/", 1);
// Filter guarantees a slash exists, but guard anyway to keep the type narrow.
if (slashIdx === -1) {
return null;
}
const rest = specifier.slice(slashIdx + 1);
const remappedSubpath = remapLegacyPiSubpath(rest);
return `${CANONICAL_PI_SCOPE}/${remappedSubpath}`;
}
function getResolvedSpecifier(specifier: string): string {
const cached = resolvedSpecifierFallbacks.get(specifier);
if (cached) {
return cached;
}
const resolved = Bun.resolveSync(specifier, import.meta.dir);
resolvedSpecifierFallbacks.set(specifier, resolved);
return resolved;
}
/**
* Resolve a canonical `@oh-my-pi/*` specifier to a filesystem path, preferring
* a bundled compat shim when one is registered for the package root.
*
* Falls back to `getResolvedSpecifier` (which may throw under compiled binary
* mode); callers handle that the same way they would for non-overridden
* specifiers.
*/
function resolveCanonicalPiSpecifier(remappedSpecifier: string): string {
const override = legacyPiPackageRootOverrides[remappedSpecifier];
if (override) {
return override;
}
return getResolvedSpecifier(remappedSpecifier);
}
function toImportSpecifier(resolvedPath: string): string {
// Virtual `omp-legacy-pi-bundled:` specifiers are served by the synthetic
// onLoad in `installLegacyPiSpecifierShim()`; wrapping them as `file://`
// would corrupt the scheme.
if (isBundledVirtualSpecifier(resolvedPath)) {
return resolvedPath;
}
return url.pathToFileURL(stripWindowsExtendedLengthPathPrefix(resolvedPath)).href;
}
/**
* Rewrite the extension-owned specifiers OMP must host-resolve — legacy
* `@(scope)/pi-*`, bare TypeBox packages, package `imports` aliases like
* `#src/*`, and extension-local bare dependencies — to absolute `file://` URLs
* or compiled-mode virtual specifiers. Relative siblings and built-in modules
* are left untouched so Bun resolves them from the extension's real on-disk
* location.
*
* When `mtimeTag` is provided, extension-owned relative graph specifiers
* (`./`/`../`) and, by default, resolved package `#alias/*` and extension-local
* bare deps also carry a `?mtime=<tag>` cache-bust so Bun rekeys them on
* same-process reloads. `resolvedImportMtimeTag` can disable the tag for
* resolved package and bare ESM imports inside third-party dependencies, whose
* transitive ESM imports retain a query-free importer path for Bun's runtime
* `node_modules` resolution. Host package rewrites (legacy
* `@(scope)/pi-*`, TypeBox shim) always emit `file://` URLs because they resolve
* to in-process host code that never changes between reloads.
*/
async function rewriteLegacyExtensionSource(
source: string,
importerPath: string,
mtimeTag: string | null = null,
resolvedImportMtimeTag: string | null = mtimeTag,
): Promise<string> {
// Compiled mode completes the override map from the build-supplied module
// keys on first use; every rewrite path must see the full map.
await ensureLegacyPiOverridesReady();
const references = collectExtensionSpecifierReferences(source, importerPath);
const replacements: Array<ExtensionSpecifierReference & { replacement: string }> = [];
for (const reference of references) {
if (reference.kind !== "import") continue;
const specifier = reference.specifier;
let replacement: string | null = null;
const remappedSpecifier = remapLegacyPiSpecifier(specifier);
if (remappedSpecifier) {
try {
replacement = toImportSpecifier(resolveCanonicalPiSpecifier(remappedSpecifier));
} catch {
// Compiled fallback may be absent from a malformed build. Continue to
// the extension's on-disk peer dependency resolution below.
}
}
if (!replacement && TYPEBOX_SHIM_PATH && (specifier === "typebox" || specifier === "@sinclair/typebox")) {
replacement = toImportSpecifier(TYPEBOX_SHIM_PATH);
}
if (!replacement && specifier.startsWith("#")) {
const resolved = await resolvePackageImportSpecifier(specifier, importerPath);
if (resolved) replacement = toGraphImportSpecifier(resolved, resolvedImportMtimeTag);
}
if (!replacement && isBareExtensionDependencySpecifier(specifier)) {
const resolved = await resolveExtensionBareDependency(specifier, importerPath);
if (resolved) replacement = toGraphImportSpecifier(resolved, resolvedImportMtimeTag);
}
if (!replacement && mtimeTag && /^\.\.?\//.test(specifier) && !specifier.includes("?")) {
replacement = `${specifier}?mtime=${mtimeTag}`;
}
if (replacement && replacement !== specifier) {
replacements.push({ ...reference, replacement });
}
}
const withImports = applySpecifierReplacements(source, replacements);
return rewriteExtensionSpecifiers(withImports, importerPath);
}
/** Test seam for compiled-binary legacy extension source rewriting. */
export async function __rewriteLegacyExtensionSourceForTests(
source: string,
importerPath: string,
mtimeTag: string | null = null,
resolvedImportMtimeTag: string | null = mtimeTag,
): Promise<string> {
return rewriteLegacyExtensionSource(source, importerPath, mtimeTag, resolvedImportMtimeTag);
}
/**
* Build the import specifier for a graph-resolved absolute path. POSIX
* emits a bare filesystem path with an optional `?mtime=<tag>` (Bun keys
* query strings for bare-path specifiers), so same-process extension
* reloads pick up edits to package-alias (`#foo/*`) and extension-local
* bare deps. Windows and bundled virtual specifiers keep the current
* `file://` / virtual form — Bun ignores queries on `file://` URLs, so
* cache-bust does not reach Windows extensions until Bun changes that.
*/
function toGraphImportSpecifier(resolvedPath: string, mtimeTag: string | null): string {
if (isBundledVirtualSpecifier(resolvedPath)) {
return resolvedPath;
}
if (process.platform === "win32" || !mtimeTag) {
return url.pathToFileURL(stripWindowsExtendedLengthPathPrefix(resolvedPath)).href;
}
return `${stripWindowsExtendedLengthPathPrefix(resolvedPath)}?mtime=${mtimeTag}`;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
async function pathExists(p: string): Promise<boolean> {
try {
await fs.promises.stat(p);
return true;
} catch {
return false;
}
}
function hasSourceModuleExtension(p: string): boolean {
const ext = path.extname(p).toLowerCase();
return (SOURCE_MODULE_EXTENSIONS as readonly string[]).includes(ext);
}
async function resolveSourceModuleFile(basePath: string): Promise<string | null> {
try {
const stats = await fs.promises.stat(basePath);
if (stats.isFile()) {
// Non-source files (JSON, WASM, text assets, etc.) bypass the on-load
// rewrite hook so Bun's native loaders handle them; our hook would
// otherwise pass them through `getLoader()` which falls back to `js`.
return hasSourceModuleExtension(basePath) ? realpathOrSelf(basePath) : null;
}
if (stats.isDirectory()) {
for (const extension of SOURCE_MODULE_EXTENSIONS) {
const resolved = await resolveSourceModuleFile(path.join(basePath, `index${extension}`));
if (resolved) return resolved;
}
}
} catch {
// Fall through to extension candidates below.
}
if (path.extname(basePath)) {
return null;
}
for (const extension of SOURCE_MODULE_EXTENSIONS) {
const resolved = await resolveSourceModuleFile(`${basePath}${extension}`);
if (resolved) return resolved;
}
return null;
}
function isPathInsideRoot(rootPath: string, candidatePath: string): boolean {
const relative = path.relative(rootPath, candidatePath);
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
}
async function resolvePackageSourceTarget(packageRoot: string, targetPath: string): Promise<string | null> {
const candidate = path.resolve(targetPath);
if (!isPathInsideRoot(path.resolve(packageRoot), candidate)) {
return null;
}
const resolved = await resolveSourceModuleFile(candidate);
if (!resolved) {
return null;
}
const realPackageRoot = await realpathOrSelf(packageRoot);
return isPathInsideRoot(realPackageRoot, resolved) ? resolved : null;
}
async function resolvePackageFileTarget(packageRoot: string, targetPath: string): Promise<string | null> {
const candidate = path.resolve(targetPath);
if (!isPathInsideRoot(path.resolve(packageRoot), candidate) || !(await pathExists(candidate))) {
return null;
}
const [realPackageRoot, resolved] = await Promise.all([realpathOrSelf(packageRoot), realpathOrSelf(candidate)]);
return isPathInsideRoot(realPackageRoot, resolved) ? resolved : null;
}
async function findPackageRoot(importerPath: string): Promise<string | null> {
let dir = path.dirname(importerPath);
while (true) {
const cached = packageRootCache.get(dir);
if (cached !== undefined) {
return cached;
}
if (await pathExists(path.join(dir, "package.json"))) {
packageRootCache.set(path.dirname(importerPath), dir);
return dir;
}
const parent = path.dirname(dir);
if (parent === dir) {
packageRootCache.set(path.dirname(importerPath), null);
return null;
}
dir = parent;
}
}
async function readPackageImports(packageRoot: string): Promise<Record<string, unknown> | null> {
const cached = packageImportsCache.get(packageRoot);
if (cached !== undefined) {
return cached;
}
let imports: Record<string, unknown> | null = null;
try {
const pkg = await Bun.file(path.join(packageRoot, "package.json")).json();
if (isRecord(pkg) && isRecord(pkg.imports)) {
imports = pkg.imports;
}
} catch {
imports = null;
}
packageImportsCache.set(packageRoot, imports);
return imports;
}
type PackageImportTargetSelection = string | typeof PACKAGE_IMPORT_EXCLUDED | null;
type ResolvedPackageImportTargetSelection = string | typeof PACKAGE_IMPORT_EXCLUDED;
function selectPackageImportTarget(
entry: unknown,
conditions: ReadonlySet<string> = SUPPORTED_PACKAGE_IMPORT_CONDITIONS,
): PackageImportTargetSelection {
if (entry === null) {
return PACKAGE_IMPORT_EXCLUDED;
}
if (typeof entry === "string") {
return entry;
}
if (Array.isArray(entry)) {
for (const item of entry) {
const target = selectPackageImportTarget(item, conditions);
if (target !== null) return target;
}
return null;
}
if (!isRecord(entry)) {
return null;
}
for (const [condition, value] of Object.entries(entry)) {
if (!conditions.has(condition)) {
continue;
}
const target = selectPackageImportTarget(value, conditions);
if (target !== null) return target;
}
return null;
}
async function resolvePackageImportTarget(
packageRoot: string,
target: string,
wildcard: string | null,
): Promise<string | null> {
if (!target.startsWith("./")) {
return null;
}
const substituted = wildcard === null ? target : target.replaceAll("*", wildcard);
return resolvePackageSourceTarget(packageRoot, path.resolve(packageRoot, substituted));
}
async function resolvePackageImportSpecifier(specifier: string, importerPath: string): Promise<string | null> {
if (!specifier.startsWith("#")) {
return null;
}
const packageRoot = await findPackageRoot(importerPath);
if (!packageRoot) {
return null;
}
const imports = await readPackageImports(packageRoot);
if (!imports) {
return null;
}
const exactTarget = selectPackageImportTarget(imports[specifier]);
if (exactTarget === PACKAGE_IMPORT_EXCLUDED) {
return null;
}
if (exactTarget !== null) {
return resolvePackageImportTarget(packageRoot, exactTarget, null);
}
let bestMatch: { keyLength: number; target: ResolvedPackageImportTargetSelection; wildcard: string } | null = null;
for (const [key, entry] of Object.entries(imports)) {
const starIndex = key.indexOf("*");
if (starIndex === -1) continue;
const prefix = key.slice(0, starIndex);
const suffix = key.slice(starIndex + 1);
if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix)) {
continue;
}
const target = selectPackageImportTarget(entry);
if (target === null) {
continue;
}
if (!bestMatch || key.length > bestMatch.keyLength) {
bestMatch = {
keyLength: key.length,
target,
wildcard: specifier.slice(prefix.length, specifier.length - suffix.length),
};
}
}
if (!bestMatch || bestMatch.target === PACKAGE_IMPORT_EXCLUDED) {
return null;
}
return resolvePackageImportTarget(packageRoot, bestMatch.target, bestMatch.wildcard);
}
function isBareExtensionDependencySpecifier(specifier: string): boolean {
if (
specifier.startsWith(".") ||
specifier.startsWith("/") ||
specifier.startsWith("#") ||
specifier.startsWith("node:") ||
specifier.startsWith("bun:") ||
/^[a-z][a-z0-9+.-]*:/i.test(specifier)
) {
return false;
}
const packageName = specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
return Boolean(packageName && !isBuiltin(packageName));
}
interface BarePackageSpecifier {
readonly name: string;
readonly subpath: string | null;
}
function splitBarePackageSpecifier(specifier: string): BarePackageSpecifier | null {
const parts = specifier.split("/");
if (specifier.startsWith("@")) {
const [scope, name, ...rest] = parts;
if (!scope || !name) return null;
return { name: `${scope}/${name}`, subpath: rest.length > 0 ? rest.join("/") : null };
}
const [name, ...rest] = parts;
if (!name) return null;
return { name, subpath: rest.length > 0 ? rest.join("/") : null };
}
async function findNodePackageRoot(packageName: string, importerPath: string): Promise<string | null> {
const cacheKey = `${packageName}\0${path.resolve(path.dirname(importerPath))}`;
const cached = nodePackageRootCache.get(cacheKey);
if (cached) return cached;
const promise = findNodePackageRootUncached(packageName, importerPath);
nodePackageRootCache.set(cacheKey, promise);
return promise;
}
async function findNodePackageRootUncached(packageName: string, importerPath: string): Promise<string | null> {
let dir = path.dirname(importerPath);
while (true) {
const candidate = path.join(dir, "node_modules", packageName);
if (await pathExists(path.join(candidate, "package.json"))) {
return candidate;
}
const parent = path.dirname(dir);
if (parent === dir) {
return null;
}
dir = parent;
}
}
async function readPackageManifest(packageRoot: string): Promise<Record<string, unknown> | null> {
const cached = packageManifestCache.get(packageRoot);
if (cached) return cached;
const promise = readPackageManifestUncached(packageRoot);
packageManifestCache.set(packageRoot, promise);
return promise;
}
async function readPackageManifestUncached(packageRoot: string): Promise<Record<string, unknown> | null> {
try {
const manifest = await Bun.file(path.join(packageRoot, "package.json")).json();
return isRecord(manifest) ? manifest : null;
} catch {
return null;
}
}
type ExtensionModuleKind = "commonjs" | "esm";
class ExtensionModuleKindConflictError extends Error {}
async function isCommonJsModulePath(
modulePath: string,
sourceType?: "script" | "module",
inheritedKind?: ExtensionModuleKind,
): Promise<boolean> {
const extension = path.extname(modulePath).toLowerCase();
if (extension === ".cjs" || extension === ".cts") {
return true;
}
if (extension !== ".js" && extension !== ".jsx") {
return false;
}
const packageRoot = await findPackageRoot(modulePath);
const manifest = packageRoot ? await readPackageManifest(packageRoot) : null;
if (manifest?.type === "module") {
return false;
}
if (manifest?.type === "commonjs") {
return true;
}
const parsedSourceType =
sourceType ?? parseExtensionSource(await Bun.file(modulePath).text(), modulePath).program.sourceType;
if (parsedSourceType === "module") {
return false;
}
if (inheritedKind) {
return inheritedKind === "commonjs";
}
const declaredModuleEntry =
packageRoot && typeof manifest?.module === "string"
? await resolvePackageSourceTarget(packageRoot, path.resolve(packageRoot, manifest.module))
: null;
return !declaredModuleEntry || path.resolve(modulePath) !== path.resolve(declaredModuleEntry);
}
async function isGraphOwnedCommonJsModule(
modulePath: string,
entryRealPath: string,
sourceType?: "script" | "module",
inheritedKind?: ExtensionModuleKind,
): Promise<boolean> {
const extension = path.extname(modulePath).toLowerCase();
if (modulePath === entryRealPath && extension !== ".cjs" && extension !== ".cts") {
return false;
}
return isCommonJsModulePath(modulePath, sourceType, inheritedKind);
}
async function resolvePackageExportTarget(
packageRoot: string,
target: string,
wildcard: string | null,
): Promise<string | null> {
if (!target.startsWith("./")) {
return null;
}
const substituted = wildcard === null ? target : target.replaceAll("*", wildcard);
return resolvePackageSourceTarget(packageRoot, path.resolve(packageRoot, substituted));
}
async function resolveNodePackageExport(
packageRoot: string,
subpath: string | null,
manifest: Record<string, unknown>,
conditions: ReadonlySet<string> = SUPPORTED_PACKAGE_IMPORT_CONDITIONS,
): Promise<string | null> {
const exportsField = manifest.exports;
const rootTarget = subpath === null ? selectPackageImportTarget(exportsField, conditions) : null;
if (rootTarget !== null && rootTarget !== PACKAGE_IMPORT_EXCLUDED) {
return resolvePackageExportTarget(packageRoot, rootTarget, null);
}
if (!isRecord(exportsField)) {
return null;
}
const exactKey = subpath === null ? "." : `./${subpath}`;
if (Object.hasOwn(exportsField, exactKey)) {
const exactTarget = selectPackageImportTarget(exportsField[exactKey], conditions);
return exactTarget !== null && exactTarget !== PACKAGE_IMPORT_EXCLUDED
? resolvePackageExportTarget(packageRoot, exactTarget, null)
: null;
}
let bestMatch: {
keyLength: number;
prefixLength: number;
target: PackageImportTargetSelection;
wildcard: string;
} | null = null;
for (const [key, entry] of Object.entries(exportsField)) {
const starIndex = key.indexOf("*");
if (starIndex === -1 || subpath === null || !key.startsWith("./")) continue;
const prefix = key.slice(2, starIndex);
const suffix = key.slice(starIndex + 1);
if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix)) continue;
if (
!bestMatch ||
prefix.length > bestMatch.prefixLength ||
(prefix.length === bestMatch.prefixLength && key.length > bestMatch.keyLength)
) {
bestMatch = {
keyLength: key.length,
prefixLength: prefix.length,
target: selectPackageImportTarget(entry, conditions),
wildcard: subpath.slice(prefix.length, subpath.length - suffix.length),
};
}
}
return bestMatch?.target && bestMatch.target !== PACKAGE_IMPORT_EXCLUDED
? resolvePackageExportTarget(packageRoot, bestMatch.target, bestMatch.wildcard)
: null;
}
async function resolveNodePackageFallback(
packageRoot: string,
subpath: string | null,
manifest: Record<string, unknown>,
): Promise<string | null> {
if (subpath !== null) {
return resolvePackageSourceTarget(packageRoot, path.join(packageRoot, subpath));
}
for (const field of ["module", "main"]) {
const target = manifest[field];
if (typeof target === "string") {
const resolved = await resolvePackageSourceTarget(packageRoot, path.resolve(packageRoot, target));
if (resolved) return resolved;
}
}
return resolvePackageSourceTarget(packageRoot, path.join(packageRoot, "index"));
}
async function resolveNodePackageDependency(specifier: string, importerPath: string): Promise<string | null> {
const parsed = splitBarePackageSpecifier(specifier);
if (!parsed) return null;
const packageRoot = await findNodePackageRoot(parsed.name, importerPath);
if (!packageRoot) return null;
const manifest = await readPackageManifest(packageRoot);
if (!manifest) return null;
return Object.hasOwn(manifest, "exports")
? resolveNodePackageExport(packageRoot, parsed.subpath, manifest)
: resolveNodePackageFallback(packageRoot, parsed.subpath, manifest);
}
async function resolveNodePackageRequire(specifier: string, importerPath: string): Promise<string | null> {
const parsed = splitBarePackageSpecifier(specifier);
if (!parsed) return null;
const packageRoot = await findNodePackageRoot(parsed.name, importerPath);
if (!packageRoot) return null;
const manifest = await readPackageManifest(packageRoot);
if (!manifest) return null;
if (Object.hasOwn(manifest, "exports")) {
return resolveNodePackageExport(packageRoot, parsed.subpath, manifest, SUPPORTED_PACKAGE_REQUIRE_CONDITIONS);
}
if (parsed.subpath !== null) {
return resolvePackageSourceTarget(packageRoot, path.join(packageRoot, parsed.subpath));
}
const main = manifest.main;
return typeof main === "string"
? await resolvePackageSourceTarget(packageRoot, path.resolve(packageRoot, main))
: await resolvePackageSourceTarget(packageRoot, path.join(packageRoot, "index"));
}
async function validateResolvedBarePackagePath(
specifier: string,
importerPath: string,
resolvedPath: string,
): Promise<string | null> {
const parsed = splitBarePackageSpecifier(specifier);
const packageRoot = parsed ? await findNodePackageRoot(parsed.name, importerPath) : null;
return packageRoot ? resolvePackageFileTarget(packageRoot, resolvedPath) : null;
}
async function isSelectedNoTypeEsmPackageBranch(
specifier: string,
importerPath: string,
resolvedPath: string,
): Promise<boolean> {
const parsed = splitBarePackageSpecifier(specifier);
const packageRoot = parsed ? await findNodePackageRoot(parsed.name, importerPath) : null;
const manifest = packageRoot ? await readPackageManifest(packageRoot) : null;
if (!packageRoot || !manifest || manifest.type !== undefined) {
return false;
}
if (parsed?.subpath === null && typeof manifest.module === "string") {
const moduleEntry = await resolvePackageSourceTarget(packageRoot, path.resolve(packageRoot, manifest.module));
if (moduleEntry && path.resolve(moduleEntry) === path.resolve(resolvedPath)) {
return true;
}
}
if (!Object.hasOwn(manifest, "exports")) {
return false;
}
const importTarget = await resolveNodePackageExport(packageRoot, parsed?.subpath ?? null, manifest);
const requireTarget = await resolveNodePackageExport(
packageRoot,
parsed?.subpath ?? null,
manifest,
SUPPORTED_PACKAGE_REQUIRE_CONDITIONS,
);
return Boolean(
importTarget &&
path.resolve(importTarget) === path.resolve(resolvedPath) &&
(!requireTarget || path.resolve(requireTarget) !== path.resolve(importTarget)),
);
}
async function resolveExtensionBareDependency(specifier: string, importerPath: string): Promise<string | null> {
if (!isBareExtensionDependencySpecifier(specifier)) {
return null;
}
const cacheKey = `${specifier}\0${path.resolve(path.dirname(importerPath))}`;
const cached = bareDependencyResolutionCache.get(cacheKey);
if (cached) return cached;
const promise = resolveExtensionBareDependencyUncached(specifier, importerPath);
bareDependencyResolutionCache.set(cacheKey, promise);
return promise;
}
async function resolveExtensionBareDependencyUncached(specifier: string, importerPath: string): Promise<string | null> {
// Resolve against the runtime package manifest first. Besides working in a
// compiled binary, this preserves the package's ESM `import` condition when
// the absolute target is later loaded outside normal package resolution.
const packageResolved = await resolveNodePackageDependency(specifier, importerPath);
if (packageResolved) {
return packageResolved;
}
try {
const resolved = Bun.resolveSync(specifier, path.dirname(importerPath));
if (resolved && resolved !== specifier && !resolved.startsWith("node:") && !resolved.startsWith("bun:")) {
return validateResolvedBarePackagePath(specifier, importerPath, resolved);
}
} catch {
// Compiled binaries do not reliably resolve runtime extension node_modules.
}
return null;
}
const NATIVE_ADDON_EXTENSION = ".node";
/**
* Resolve a bare specifier whose target is a native `.node` addon — either a
* package subpath ending in `.node`, or a package whose `main` points at an
* addon (the napi-rs per-platform package convention, e.g.
* `@yuuang/ffi-rs-darwin-arm64` → `ffi-rs.darwin-arm64.node`). Returns the
* addon's absolute realpath, or null when the specifier is not a native addon.
*/
async function resolveExtensionNativeAddon(specifier: string, importerPath: string): Promise<string | null> {
if (!isBareExtensionDependencySpecifier(specifier)) {
return null;
}
const cacheKey = `${specifier}\0${path.resolve(path.dirname(importerPath))}`;
const cached = nativeAddonResolutionCache.get(cacheKey);
if (cached) return cached;
const promise = resolveExtensionNativeAddonUncached(specifier, importerPath);
nativeAddonResolutionCache.set(cacheKey, promise);
return promise;
}
async function resolveExtensionNativeAddonUncached(specifier: string, importerPath: string): Promise<string | null> {
const parsed = splitBarePackageSpecifier(specifier);
if (!parsed) return null;
const packageRoot = await findNodePackageRoot(parsed.name, importerPath);
if (!packageRoot) return null;
let target: string | null = null;
if (parsed.subpath !== null) {
target = parsed.subpath.endsWith(NATIVE_ADDON_EXTENSION) ? path.join(packageRoot, parsed.subpath) : null;
} else {
const manifest = await readPackageManifest(packageRoot);
const main = manifest?.main;
target =
typeof main === "string" && main.endsWith(NATIVE_ADDON_EXTENSION) ? path.resolve(packageRoot, main) : null;
}
if (!target) {
return null;
}
return resolvePackageFileTarget(packageRoot, target);
}
async function resolveExtensionBareRequire(specifier: string, importerPath: string): Promise<string | null> {
if (!isBareExtensionDependencySpecifier(specifier)) {
return null;
}
const cacheKey = `${specifier}\0${path.resolve(path.dirname(importerPath))}`;
const cached = bareRequireResolutionCache.get(cacheKey);
if (cached) return cached;
const resolution = (async () => {
const nativeAddon = await resolveExtensionNativeAddon(specifier, importerPath);
if (nativeAddon) {
return nativeAddon;
}
const packageResolved = await resolveNodePackageRequire(specifier, importerPath);
if (packageResolved) {
return realpathOrSelf(packageResolved);
}
try {
const resolved = createRequire(importerPath).resolve(specifier);
return resolved === specifier || resolved.startsWith("node:") || resolved.startsWith("bun:")
? null
: await validateResolvedBarePackagePath(specifier, importerPath, resolved);
} catch {
return null;
}
})();
bareRequireResolutionCache.set(cacheKey, resolution);
return resolution;
}
async function resolveExtensionCommonJsRequire(specifier: string, importerPath: string): Promise<string | null> {
const remappedSpecifier = remapLegacyPiSpecifier(specifier);
if (remappedSpecifier) {
try {
const resolved = resolveCanonicalPiSpecifier(remappedSpecifier);
if (isBundledVirtualSpecifier(resolved)) {
const moduleKey = resolved.slice(BUNDLED_VIRTUAL_SCHEME.length);
if (!(moduleKey in loadedBundledModules)) {
await loadBundledModule(moduleKey);
}
}
return resolved;
} catch {
// A malformed compiled registry can still fall through to an
// extension-installed legacy peer dependency.
}
}
return resolveExtensionBareRequire(specifier, importerPath);
}
/**
* Rewrite CommonJS graph specifiers that cannot resolve from the bridge's
* generated function: bare `require()` calls and, for graph-owned CommonJS
* sources, import specifiers. Resolved targets are retained for synchronous
* lazy hydration after load-time source caches clear.
*/
async function rewriteExtensionSpecifiers(
source: string,
importerPath: string,
rewriteImports = false,
): Promise<string> {
const references = collectExtensionSpecifierReferences(source, importerPath);
const resolvedSpecifierTargets = new Map<string, string>();
const replacements: Array<ExtensionSpecifierReference & { replacement: string }> = [];
for (const reference of references) {
let resolved: string | null = null;
if (reference.kind === "require") {
resolved = await resolveExtensionCommonJsRequire(reference.specifier, importerPath);
} else if (rewriteImports) {
if (reference.specifier.startsWith(".")) {
const candidate = Bun.resolveSync(reference.specifier, path.dirname(importerPath));
resolved = hasSourceModuleExtension(candidate) ? await realpathOrSelf(candidate) : null;
} else if (reference.specifier.startsWith("#")) {
resolved = await resolvePackageImportSpecifier(reference.specifier, importerPath);
} else {
resolved = await resolveExtensionBareDependency(reference.specifier, importerPath);
}
}
if (!resolved) continue;
const replacement = stripWindowsExtendedLengthPathPrefix(resolved).replaceAll("\\", "/");
resolvedSpecifierTargets.set(`${reference.kind}\0${reference.specifier}`, replacement);
replacements.push({ ...reference, replacement });
}
extensionSynchronousSpecifierTargets.set(importerPath, resolvedSpecifierTargets);
return applySpecifierReplacements(source, replacements);
}
function rewriteExtensionSpecifiersFromCache(source: string, importerPath: string): string {
const resolvedSpecifierTargets = extensionSynchronousSpecifierTargets.get(importerPath);
if (!resolvedSpecifierTargets || resolvedSpecifierTargets.size === 0) {
return source;
}
const replacements: Array<ExtensionSpecifierReference & { replacement: string }> = [];
for (const reference of collectExtensionSpecifierReferences(source, importerPath)) {
const replacement = resolvedSpecifierTargets.get(`${reference.kind}\0${reference.specifier}`);
if (replacement) {
replacements.push({ ...reference, replacement });
}
}
return applySpecifierReplacements(source, replacements);
}
/**
* Whether a module's source contains a bare require that resolves to a native
* `.node` addon — i.e. a napi-rs style loader that must be hooked into the
* extension graph so {@link rewriteExtensionSpecifiers} can pin its
* platform-package requires to absolute paths.
*/
async function moduleRequiresNativeAddon(modulePath: string): Promise<boolean> {
const cached = nativeAddonRequireScanCache.get(modulePath);
if (cached) return cached;
const promise = moduleRequiresNativeAddonUncached(modulePath);
nativeAddonRequireScanCache.set(modulePath, promise);
return promise;
}
async function moduleRequiresNativeAddonUncached(modulePath: string): Promise<boolean> {
let source: string;
try {
source = await Bun.file(modulePath).text();
} catch {
return false;
}
for (const reference of collectExtensionSpecifierReferences(source, modulePath)) {
if (reference.kind === "require" && (await resolveExtensionNativeAddon(reference.specifier, modulePath))) {
return true;
}
}
return false;
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
// Source modules in an extension graph are discovered from parsed static,
// dynamic, re-export, and direct CommonJS require specifiers. Parsing keeps
// import-looking text in strings, templates, regex literals, and comments out
// of dependency resolution.
// Extension source realpaths already covered by an installed load-time hook for
// each entry. `Bun.plugin()` registrations are process-global and permanent, so
// reloads install supplemental hooks only for modules added to the graph since
// the previous load.
const extensionGraphHookModules = new Map<string, Set<string>>();
const extensionGraphCacheBustResolvedImportModules = new Map<string, Set<string>>();
const commonJsModuleSources = new Map<string, string>();
const commonJsFallbackModulePaths = new Map<string, string>();
const extensionSynchronousSpecifierTargets = new Map<string, Map<string, string>>();
const commonJsGraphModulePaths = new Set<string>();
const COMMONJS_REQUIRE_GLOBAL = "__ompLegacyPiRequireGraphModule";
const commonJsModuleDefinitions = new Map<string, { source: string; filename: string; dirname: string }>();
const commonJsModuleCache = new Map<
string,
{
exports: unknown;
filename: string;
id: string;
path: string;
require: NodeJS.Require;
loaded: boolean;
}
>();
const commonJsTypeScriptTranspiler = new Bun.Transpiler({ loader: "ts" });
function evaluateGraphCommonJs(modulePath: string): unknown {
const cached = commonJsModuleCache.get(modulePath);
if (cached) {
return cached.exports;
}
let definition = commonJsModuleDefinitions.get(modulePath);
if (!definition && commonJsGraphModulePaths.has(modulePath)) {
const targetPath = commonJsFallbackModulePaths.get(modulePath) ?? modulePath;
const source = rewriteExtensionSpecifiersFromCache(fs.readFileSync(targetPath, "utf8"), modulePath);
synthesizeCommonJsDefaultModule(modulePath, source, targetPath);
definition = commonJsModuleDefinitions.get(modulePath);
}
if (!definition) {
throw new Error(`Missing graph-owned CommonJS definition: ${modulePath}`);
}
const nativeRequire = createRequire(definition.filename);
const module = {
exports: {},
filename: definition.filename,
id: definition.filename,
path: definition.dirname,
require: nativeRequire,
loaded: false,
};
commonJsModuleCache.set(modulePath, module);
const graphRequire: NodeJS.Require = Object.assign(
(specifier: string) => {
if (isBundledVirtualSpecifier(specifier)) {
const moduleKey = specifier.slice(BUNDLED_VIRTUAL_SCHEME.length);
const bundledModule = loadedBundledModules[moduleKey];
if (!bundledModule) {
throw new Error(`Missing bundled CommonJS host module: ${moduleKey}`);
}
return bundledModule;
}
const resolved = nativeRequire.resolve(specifier);
let graphPath = resolved;
try {
graphPath = fs.realpathSync(resolved);
} catch {
// Builtins and virtual modules have no filesystem realpath.
}
return commonJsGraphModulePaths.has(graphPath) ? evaluateGraphCommonJs(graphPath) : nativeRequire(specifier);
},
{
resolve: nativeRequire.resolve,
cache: nativeRequire.cache,
extensions: nativeRequire.extensions,
main: nativeRequire.main,
},
);
module.require = graphRequire;
const execute = new Function("exports", "require", "module", "__filename", "__dirname", definition.source);
try {
execute.call(module.exports, module.exports, graphRequire, module, definition.filename, definition.dirname);
module.loaded = true;
return module.exports;
} catch (error) {
commonJsModuleCache.delete(modulePath);
throw error;
}
}
/**
* Register {@link evaluateGraphCommonJs} as the graph-owned CommonJS require
* bridge on `globalThis`, first-wins.
*
* On source-link installs the `@(scope)/pi-coding-agent` root shim is served
* from `src/`, so an extension import can evaluate a second instance of this
* module with empty graph state. An unconditional set would let that empty
* instance clobber the host bundle's populated bridge and break transitive
* CommonJS resolution (#6449); guarding preserves the first (host-owned)
* registration. Idempotent: a subsequent call with a value already present is a
* no-op.
*/
export function ensureGraphCommonJsRequireRegistered(): void {
if (!Reflect.get(globalThis, COMMONJS_REQUIRE_GLOBAL)) {
Reflect.set(globalThis, COMMONJS_REQUIRE_GLOBAL, evaluateGraphCommonJs);
}
}
ensureGraphCommonJsRequireRegistered();
let legacyPiLoadTag = 0;
function nextLegacyPiLoadTag(): string {
legacyPiLoadTag = Math.max(legacyPiLoadTag + 1, Date.now());
return String(legacyPiLoadTag);
}
/** Resolve symlinks in a path, falling back to the input if realpath fails. */
async function realpathOrSelf(p: string): Promise<string> {
const cached = realpathCache.get(p);
if (cached) return cached;
const promise = realpathOrSelfUncached(p);
realpathCache.set(p, promise);
return promise;
}
async function realpathOrSelfUncached(p: string): Promise<string> {
try {
return await fs.promises.realpath(p);
} catch {
return p;
}
}
interface ExtensionModuleGraph {
readonly modules: Map<string, string>;
readonly cacheBustResolvedImportModules: Set<string>;
readonly commonJsPaths: Set<string>;
}
/**
* Walk the extension's import graph starting at `entryRealPath`, returning the
* realpath of every reachable source module OMP must rewrite at load time.
* Relative imports, package `imports` aliases, and ESM bare dependencies are
* graph-owned recursively because compiled Bun cannot resolve runtime
* `node_modules` from those modules. Graph-owned CommonJS modules also own
* their relative and bare CommonJS descendants, which are evaluated by the
* synchronous bridge. Resolved ESM imports inside third-party dependencies
* omit the reload tag so their importer paths stay query-free.
*/
async function collectExtensionModules(entryRealPath: string): Promise<ExtensionModuleGraph> {
const modules = new Map<string, string>();
const commonJsPaths = new Set<string>();
const queuedCacheBustResolvedImports = new Map<string, boolean>([[entryRealPath, true]]);
const queuedModuleKinds = new Map<string, ExtensionModuleKind>([[entryRealPath, "esm"]]);
const queuedEsmBranchPaths = new Set<string>();
const queue: Array<{
file: string;
cacheBustResolvedImports: boolean;
moduleKind?: ExtensionModuleKind;
esmBranch?: boolean;
}> = [{ file: entryRealPath, cacheBustResolvedImports: true, moduleKind: "esm" }];
while (queue.length > 0) {
const item = queue.pop();
if (!item) {
continue;
}
const file = item.file;
const cacheBustResolvedImports = queuedCacheBustResolvedImports.get(file) ?? item.cacheBustResolvedImports;
const inheritedModuleKind = queuedModuleKinds.get(file) ?? item.moduleKind;
const esmBranch = queuedEsmBranchPaths.has(file) || item.esmBranch === true;
if (modules.has(file)) {
continue;
}
let source: string;
try {
source = await Bun.file(file).text();
} catch {
continue;
}
modules.set(file, source);
const ast = parseExtensionSource(source, file);
const sourceIsCommonJs = await isGraphOwnedCommonJsModule(
file,
entryRealPath,
ast.program.sourceType,
inheritedModuleKind,
);
if (sourceIsCommonJs) {
commonJsPaths.add(file);
}
const dir = path.dirname(file);
const references = collectExtensionSpecifierReferences(source, file, ast);
for (const reference of references) {
const specifier = reference.specifier;
try {
let resolved: string | null = null;
let nextCacheBustResolvedImports = cacheBustResolvedImports;
let resolvedModuleKind: ExtensionModuleKind | undefined;
let resolvedEsmBranch = false;
let requiresNativeAddonRewrite = false;
const isRequired = reference.kind === "require";
if (specifier.startsWith(".")) {
const candidate = Bun.resolveSync(specifier, dir);
if (hasSourceModuleExtension(candidate)) {
const inheritedTargetKind = isRequired
? sourceIsCommonJs
? "commonjs"
: undefined
: sourceIsCommonJs
? "commonjs"
: esmBranch
? "esm"
: undefined;
const targetIsCommonJs = await isCommonJsModulePath(candidate, undefined, inheritedTargetKind);
const isCommonJsDescendant = isRequired && sourceIsCommonJs && targetIsCommonJs;
requiresNativeAddonRewrite =
isRequired && !isCommonJsDescendant && (await moduleRequiresNativeAddon(candidate));
if (!isRequired || isCommonJsDescendant || requiresNativeAddonRewrite) {
resolved = await realpathOrSelf(candidate);
resolvedModuleKind = targetIsCommonJs ? "commonjs" : "esm";
resolvedEsmBranch = !targetIsCommonJs && esmBranch;
}
}
} else if (specifier.startsWith("#")) {
const candidate = await resolvePackageImportSpecifier(specifier, file);
if (candidate) {
const inheritedTargetKind = isRequired
? sourceIsCommonJs
? "commonjs"
: undefined
: sourceIsCommonJs
? "commonjs"
: esmBranch
? "esm"
: undefined;
const targetIsCommonJs = await isCommonJsModulePath(candidate, undefined, inheritedTargetKind);
const isCommonJsDescendant = isRequired && sourceIsCommonJs && targetIsCommonJs;
requiresNativeAddonRewrite =
isRequired && !isCommonJsDescendant && (await moduleRequiresNativeAddon(candidate));
if (!isRequired || isCommonJsDescendant || requiresNativeAddonRewrite) {
resolved = candidate;
resolvedModuleKind = targetIsCommonJs ? "commonjs" : "esm";
resolvedEsmBranch = !targetIsCommonJs && esmBranch;
}
}
} else if (
isBareExtensionDependencySpecifier(specifier) &&
!remapLegacyPiSpecifier(specifier) &&
specifier !== "typebox" &&
specifier !== "@sinclair/typebox"
) {
const dependencyEntry = isRequired
? await resolveExtensionBareRequire(specifier, file)
: await resolveExtensionBareDependency(specifier, file);
const isHookableEntry = Boolean(dependencyEntry && hasSourceModuleExtension(dependencyEntry));
const selectedEsmBranch =
!isRequired &&
isHookableEntry &&
dependencyEntry !== null &&
(await isSelectedNoTypeEsmPackageBranch(specifier, file, dependencyEntry));
const inheritedTargetKind = isRequired
? sourceIsCommonJs
? "commonjs"
: undefined
: selectedEsmBranch
? "esm"
: undefined;
const isCommonJsEntry =
isHookableEntry && dependencyEntry
? await isCommonJsModulePath(dependencyEntry, undefined, inheritedTargetKind)
: false;
if (isHookableEntry && dependencyEntry && (!isRequired || (sourceIsCommonJs && isCommonJsEntry))) {
resolved = await realpathOrSelf(dependencyEntry);
} else if (isHookableEntry && dependencyEntry && isRequired) {
requiresNativeAddonRewrite = await moduleRequiresNativeAddon(dependencyEntry);
if (requiresNativeAddonRewrite) {
resolved = await realpathOrSelf(dependencyEntry);
}
}
if (resolved) {
resolvedModuleKind = isCommonJsEntry ? "commonjs" : "esm";
resolvedEsmBranch = selectedEsmBranch && !isCommonJsEntry;
}
nextCacheBustResolvedImports = false;
}
if (resolved && requiresNativeAddonRewrite) {
nativeAddonLoaderModulePaths.add(resolved);
}
if (resolved) {
const queuedCacheBust = queuedCacheBustResolvedImports.get(resolved) ?? false;
const mergedCacheBust = queuedCacheBust || nextCacheBustResolvedImports;
queuedCacheBustResolvedImports.set(resolved, mergedCacheBust);
const queuedModuleKind = queuedModuleKinds.get(resolved);
if (queuedModuleKind && resolvedModuleKind && queuedModuleKind !== resolvedModuleKind) {
throw new ExtensionModuleKindConflictError(
`Conflicting extension module kinds for ${resolved}: ${queuedModuleKind} and ${resolvedModuleKind}`,
);
}
const mergedModuleKind = queuedModuleKind ?? resolvedModuleKind;
if (mergedModuleKind) {
queuedModuleKinds.set(resolved, mergedModuleKind);
}
if (resolvedEsmBranch) {
queuedEsmBranchPaths.add(resolved);
}
if (!modules.has(resolved)) {
queue.push({
file: resolved,
cacheBustResolvedImports: mergedCacheBust,
moduleKind: mergedModuleKind,
esmBranch: resolvedEsmBranch,
});
}
}
} catch (error) {
if (error instanceof ExtensionModuleKindConflictError) {
throw error;
}
// Unresolvable import (e.g. a type-only path); skip it.
}
}
}
for (const [modulePath, source] of modules) {
if (commonJsPaths.has(modulePath) || nativeAddonLoaderModulePaths.has(modulePath)) {
modules.set(modulePath, await rewriteExtensionSpecifiers(source, modulePath, commonJsPaths.has(modulePath)));
}
}
return {
modules,
commonJsPaths,
cacheBustResolvedImportModules: new Set(
[...queuedCacheBustResolvedImports]
.filter(([modulePath, enabled]) => enabled && modules.has(modulePath))
.map(([modulePath]) => modulePath),
),
};
}
/**
* Discovers CommonJS export names Bun normally exposes to ESM importers. The
* bridge must declare them statically because its default export is synthetic.
*/
const COMMONJS_NAMED_EXPORT_IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
function collectCommonJsNamedExports(source: string, modulePath: string, visited = new Set<string>()): string[] {
let realModulePath = modulePath;
try {
realModulePath = fs.realpathSync(modulePath);
} catch {
// The caller's path remains the stable cycle key when realpath fails.
}
if (visited.has(realModulePath)) {
return [];
}
visited.add(realModulePath);
const names = new Set<string>();
const reexportSpecifiers = new Set<string>();
const ast = parseExtensionSource(source, modulePath);
for (const { node, scope } of collectScopedAstNodes(
ast,
candidate => candidate.type === "CallExpression" || candidate.type === "AssignmentExpression",
)) {
if (node.type === "CallExpression") {
const callee = asAstNode(node.callee);
if (isUncomputedMember(callee, "Object", "defineProperty") && !scopeHasBinding(scope, OBJECT_BINDING)) {
const target = nodeArgument(node, 0);
const property = nodeArgument(node, 1);
if (
isUnshadowedExportsTarget(target, scope) &&
property?.type === "StringLiteral" &&
typeof property.value === "string" &&
property.value !== "default" &&
COMMONJS_NAMED_EXPORT_IDENTIFIER.test(property.value)
) {
names.add(property.value);
}
continue;
}
if (isIdentifier(callee, "__exportStar")) {
const sourceCall = nodeArgument(node, 0);
const target = nodeArgument(node, 1);
if (isUnshadowedExportsTarget(target, scope) && isGlobalRequireCall(sourceCall, scope)) {
const argument = nodeArgument(sourceCall, 0);
if (argument?.type === "StringLiteral" && typeof argument.value === "string") {
reexportSpecifiers.add(argument.value);
}
}
continue;
}
}
if (node.type !== "AssignmentExpression" || node.operator !== "=") continue;
const left = asAstNode(node.left);
if (left?.type !== "MemberExpression") continue;
const propertyName = staticMemberPropertyName(left);
const object = asAstNode(left.object);
if (propertyName !== null && isUnshadowedExportsTarget(object, scope)) {
if (propertyName !== "default" && COMMONJS_NAMED_EXPORT_IDENTIFIER.test(propertyName)) {
names.add(propertyName);
}
continue;
}
if (!isUncomputedMember(left, "module", "exports") || scopeHasBinding(scope, MODULE_BINDING)) continue;
const right = asAstNode(node.right);
if (right?.type === "ObjectExpression") {
const properties = nodeArray(right, "properties");
if (properties) {
for (const value of properties) {
const property = asAstNode(value);
if (!property || (property.type !== "ObjectProperty" && property.type !== "ObjectMethod")) continue;
const name = staticObjectPropertyName(property);
if (name && name !== "default" && COMMONJS_NAMED_EXPORT_IDENTIFIER.test(name)) {
names.add(name);
}
}
}
continue;
}
if (isGlobalRequireCall(right, scope)) {
const argument = nodeArgument(right, 0);
if (argument?.type === "StringLiteral" && typeof argument.value === "string") {
reexportSpecifiers.add(argument.value);
}
}
}
const nativeRequire = createRequire(modulePath);
for (const specifier of reexportSpecifiers) {
try {
const resolved = fs.realpathSync(nativeRequire.resolve(specifier));
const reexportedSource = rewriteExtensionSpecifiersFromCache(fs.readFileSync(resolved, "utf8"), resolved);
for (const name of collectCommonJsNamedExports(reexportedSource, resolved, visited)) {
names.add(name);
}
} catch {
// Native modules and non-source re-exports do not expose analyzable names.
}
}
return [...names];
}
/**
* The shared evaluator gives ESM imports and sibling `require()` calls the
* same `module.exports` value and cycle-aware cache.
*/
function synthesizeCommonJsDefaultModule(modulePath: string, source: string, targetPath = modulePath): string {
let commonJsSource = source;
if (commonJsSource.startsWith("#!")) {
const firstLineEnd = commonJsSource.indexOf("\n");
commonJsSource = firstLineEnd === -1 ? "" : commonJsSource.slice(firstLineEnd + 1);
}
const executableSource = targetPath.endsWith(".cts")
? commonJsTypeScriptTranspiler.transformSync(commonJsSource)
: commonJsSource;
commonJsModuleDefinitions.set(modulePath, {
source: executableSource,
filename: targetPath,
dirname: path.dirname(targetPath),
});
commonJsModuleCache.delete(modulePath);
const exportsBinding = "__ompLegacyPiCommonJsExports";
const namedExports = collectCommonJsNamedExports(executableSource, targetPath)
.map(
(name, index) =>
`const __ompLegacyPiCommonJsExport${index} = ${exportsBinding}[${JSON.stringify(name)}]; export { __ompLegacyPiCommonJsExport${index} as ${name} };`,
)
.join("\n");
return `const ${exportsBinding} = globalThis[${JSON.stringify(COMMONJS_REQUIRE_GLOBAL)}](${JSON.stringify(modulePath)});\nexport default ${exportsBinding};\n${namedExports}\n`;
}
/**
* Linkedom's canvas bridge uses its bundled fallback because OMP does not ship
* native canvas.
*/
async function prepareCommonJsDefaultModule(modulePath: string, source: string): Promise<string> {
const packageRoot = await findPackageRoot(modulePath);
if (!packageRoot) {
return synthesizeCommonJsDefaultModule(modulePath, source);
}
const manifest = await readPackageManifest(packageRoot);
const packageRelativePath = path.relative(packageRoot, modulePath).split(path.sep).join("/");
if (manifest?.name !== "linkedom" || packageRelativePath !== "commonjs/canvas.cjs") {
return synthesizeCommonJsDefaultModule(modulePath, source);
}
const targetPath = path.join(packageRoot, "commonjs", "canvas-shim.cjs");
commonJsFallbackModulePaths.set(modulePath, targetPath);
return synthesizeCommonJsDefaultModule(modulePath, await Bun.file(targetPath).text(), targetPath);
}
/**
* Install exact-path load hooks for the current extension graph. ESM/TS source
* retains the async rewrite path. Graph-owned CommonJS modules and native-addon
* loaders stay synchronous because Bun rejects `require()` targets backed by
* async `onLoad` callbacks.
*/
async function installExtensionGraphHook(
entryRealPath: string,
modules: Map<string, string>,
commonJsPaths: Set<string>,
cacheBustResolvedImportModules: ReadonlySet<string>,
): Promise<{ asyncModules: Map<string, string>; syncSourceModules: Map<string, string> }> {
const asyncModules = new Map<string, string>();
const syncSourceModules = new Map<string, string>();
for (const [modulePath, source] of modules) {
if (commonJsPaths.has(modulePath)) {
continue;
}
if (nativeAddonLoaderModulePaths.has(modulePath)) {
syncSourceModules.set(modulePath, source);
} else {
asyncModules.set(modulePath, source);
}
}
if (asyncModules.size > 0) {
const alternation = [...asyncModules.keys()].map(escapeRegExp).join("|");
const filter = new RegExp(`^(?:${alternation})(?:\\?mtime=\\d+)?$`);
const hookId = Bun.hash(`${entryRealPath}\0async\0${[...asyncModules.keys()].join("\0")}`).toString(36);
Bun.plugin({
name: `omp:legacy-pi-ext:${hookId}`,
setup(build) {
build.onLoad({ filter, namespace: "file" }, async args => {
const queryIndex = args.path.indexOf("?mtime=");
const sourcePath = queryIndex >= 0 ? args.path.slice(0, queryIndex) : args.path;
const mtimeTag = queryIndex >= 0 ? args.path.slice(queryIndex + "?mtime=".length) : null;
const cached = asyncModules.get(sourcePath);
let raw: string;
if (cached !== undefined) {
// consume-once: preserves ?mtime edit-pickup for re-imports
asyncModules.delete(sourcePath);
raw = cached;
} else {
raw = await Bun.file(sourcePath).text();
}
const resolvedImportMtimeTag = cacheBustResolvedImportModules.has(sourcePath) ? mtimeTag : null;
return {
contents: await rewriteLegacyExtensionSource(raw, sourcePath, mtimeTag, resolvedImportMtimeTag),
loader: getLoader(sourcePath),
};
});
},
});
}
if (commonJsPaths.size > 0) {
const alternation = [...commonJsPaths].map(escapeRegExp).join("|");
const filter = new RegExp(`^(?:${alternation})(?:\\?mtime=\\d+)?$`);
const hookId = Bun.hash(`${entryRealPath}\0commonjs\0${[...commonJsPaths].join("\0")}`).toString(36);
Bun.plugin({
name: `omp:legacy-pi-ext:${hookId}`,
setup(build) {
build.onLoad({ filter, namespace: "file" }, args => {
const queryIndex = args.path.indexOf("?mtime=");
const sourcePath = queryIndex >= 0 ? args.path.slice(0, queryIndex) : args.path;
let source = commonJsModuleSources.get(sourcePath);
if (source === undefined) {
const targetPath = commonJsFallbackModulePaths.get(sourcePath) ?? sourcePath;
const raw = rewriteExtensionSpecifiersFromCache(fs.readFileSync(targetPath, "utf8"), sourcePath);
source = synthesizeCommonJsDefaultModule(sourcePath, raw, targetPath);
}
return { contents: source, loader: getLoader(sourcePath) };
});
},
});
}
if (syncSourceModules.size > 0) {
const alternation = [...syncSourceModules.keys()].map(escapeRegExp).join("|");
const filter = new RegExp(`^(?:${alternation})(?:\\?mtime=\\d+)?$`);
const hookId = Bun.hash(`${entryRealPath}\0sync-source\0${[...syncSourceModules.keys()].join("\0")}`).toString(
36,
);
Bun.plugin({
name: `omp:legacy-pi-ext:${hookId}`,
setup(build) {
build.onLoad({ filter, namespace: "file" }, args => {
const queryIndex = args.path.indexOf("?mtime=");
const sourcePath = queryIndex >= 0 ? args.path.slice(0, queryIndex) : args.path;
const source = syncSourceModules.get(sourcePath);
if (source === undefined) {
throw new Error(`Missing pre-rewritten CommonJS extension source: ${sourcePath}`);
}
return { contents: source, loader: getLoader(sourcePath) };
});
},
});
}
return { asyncModules, syncSourceModules };
}
/**
* Ensure every currently reachable extension source module has a load-time
* rewrite hook. The entry graph can grow across reloads, so each call collects
* the current graph and registers hooks for paths not covered by earlier loads.
*
* Returns a clearable handle to drop cached sources that weren't consumed
* during the initial load; `undefined` when no new modules were discovered.
*/
async function ensureExtensionGraphHook(entryRealPath: string): Promise<{ clear(): void } | undefined> {
const {
modules: currentModules,
commonJsPaths,
cacheBustResolvedImportModules: discoveredCacheBustModules,
} = await collectExtensionModules(entryRealPath);
let cacheBustResolvedImportModules = extensionGraphCacheBustResolvedImportModules.get(entryRealPath);
if (!cacheBustResolvedImportModules) {
cacheBustResolvedImportModules = new Set<string>();
extensionGraphCacheBustResolvedImportModules.set(entryRealPath, cacheBustResolvedImportModules);
}
for (const modulePath of discoveredCacheBustModules) {
cacheBustResolvedImportModules.add(modulePath);
}
for (const [modulePath, source] of currentModules) {
if (commonJsPaths.has(modulePath)) {
commonJsModuleSources.set(modulePath, await prepareCommonJsDefaultModule(modulePath, source));
commonJsGraphModulePaths.add(modulePath);
}
}
let hookedModules = extensionGraphHookModules.get(entryRealPath);
if (!hookedModules) {
hookedModules = new Set<string>();
extensionGraphHookModules.set(entryRealPath, hookedModules);
}
const pendingModules = new Map<string, string>();
const pendingCommonJsPaths = new Set<string>();
for (const [modulePath, source] of currentModules) {
if (!hookedModules.has(modulePath)) {
pendingModules.set(modulePath, source);
if (commonJsPaths.has(modulePath)) {
pendingCommonJsPaths.add(modulePath);
}
}
}
if (pendingModules.size === 0 && commonJsPaths.size === 0) {
return undefined;
}
let asyncModules = new Map<string, string>();
let syncSourceModules = new Map<string, string>();
if (pendingModules.size > 0) {
({ asyncModules, syncSourceModules } = await installExtensionGraphHook(
entryRealPath,
pendingModules,
pendingCommonJsPaths,
cacheBustResolvedImportModules,
));
for (const modulePath of pendingModules.keys()) {
hookedModules.add(modulePath);
}
}
return {
clear() {
asyncModules.clear();
syncSourceModules.clear();
for (const modulePath of commonJsPaths) {
commonJsModuleSources.delete(modulePath);
commonJsModuleDefinitions.delete(modulePath);
commonJsModuleCache.delete(modulePath);
}
},
};
}
/**
* Load a legacy Pi extension module from its real on-disk location.
*
* The extension runs in place, so its `import.meta.url` is the real source file
* and `__dirname`-relative `readFileSync` asset loads (HTML/CSS bundled next to
* the entry) resolve exactly as they do under the original Pi runtime — no
* temp-directory mirroring and no asset copying. An `onLoad` hook scoped to the
* entry's source graph rewrites only host-resolved compatibility imports in the
* extension's own source; everything else resolves natively.
*/
export async function loadLegacyPiModule(resolvedPath: string): Promise<unknown> {
// Bun reports the realpath of a loaded module to `onLoad` and exposes it as
// `import.meta.url`. Resolve symlinks here too (macOS `/var`→`/private/var`,
// `bun link`/pnpm installs) so the rewrite filter matches the path Bun
// actually hands the hook.
const entryRealPath = await realpathOrSelf(path.resolve(resolvedPath));
await ensureLegacyPiOverridesReady();
const pendingSources = await ensureExtensionGraphHook(entryRealPath);
try {
// Dynamic import is required: legacy extension entry paths are user/plugin supplied at runtime.
// On POSIX, use the raw filesystem path so Bun keys the `?mtime`
// suffix as part of the module identity; Bun ignores query strings on
// `file://` specifiers, which would serve stale edited source.
const entrySpecifier =
process.platform === "win32" || isBundledVirtualSpecifier(entryRealPath)
? toImportSpecifier(entryRealPath)
: entryRealPath;
return await import(`${entrySpecifier}?mtime=${nextLegacyPiLoadTag()}`);
} finally {
// Drop whatever the initial import didn't consume: graph modules only
// reached by lazy dynamic imports must be read from disk at their actual
// import time, not served from this load-time snapshot.
pendingSources?.clear();
}
}
function getLoader(path: string): "js" | "jsx" | "ts" | "tsx" {
if (path.endsWith(".tsx")) {
return "tsx";
}
if (path.endsWith(".jsx")) {
return "jsx";
}
if (path.endsWith(".ts") || path.endsWith(".mts") || path.endsWith(".cts")) {
return "ts";
}
return "js";
}
function resolveLegacyPiSpecifier(args: { path: string; importer: string }): LegacyPiResolveResult | undefined {
const remappedSpecifier = remapLegacyPiSpecifier(args.path);
if (!remappedSpecifier) {
return undefined;
}
// Primary: resolve the canonical @oh-my-pi/* specifier from the host binary
// location. Works in dev mode and in source-link installs.
try {
return toLegacyPiResolveResult(resolveCanonicalPiSpecifier(remappedSpecifier));
} catch {
// Fallback for compiled binary mode: the bundled packages live inside
// /$bunfs/root and aren't reachable by filesystem resolution. Prefer the
// canonical specifier against the importing file's directory when the
// plugin installed @oh-my-pi peer deps, then try the original legacy
// specifier for plugins that still vendor only @mariozechner or
// @earendil-works peer deps.
const importerDir = path.dirname(args.importer);
try {
return toLegacyPiResolveResult(Bun.resolveSync(remappedSpecifier, importerDir));
} catch {
try {
return toLegacyPiResolveResult(Bun.resolveSync(args.path, importerDir));
} catch {
return undefined;
}
}
}
}
function resolveTypeBoxSpecifier(): LegacyPiResolveResult | undefined {
return TYPEBOX_SHIM_PATH ? toLegacyPiResolveResult(TYPEBOX_SHIM_PATH) : undefined;
}
export function installLegacyPiSpecifierShim(): void {
if (isLegacyPiSpecifierShimInstalled) {
return;
}
isLegacyPiSpecifierShimInstalled = true;
Bun.plugin({
name: "omp:legacy-pi-shim",
setup(build) {
build.onResolve({ filter: LEGACY_PI_SPECIFIER_FILTER, namespace: "file" }, resolveLegacyPiSpecifier);
build.onResolve({ filter: TYPEBOX_SPECIFIER_FILTER, namespace: "file" }, resolveTypeBoxSpecifier);
build.onResolve({ filter: /^omp-legacy-pi-bundled:.+$/, namespace: "file" }, args =>
resolveBundledVirtualSpecifier(args.path),
);
build.onResolve({ filter: /.*/, namespace: BUNDLED_VIRTUAL_NAMESPACE }, args =>
resolveBundledVirtualSpecifier(args.path),
);
// Compiled mode serves `omp-legacy-pi-bundled:<key>` imports from
// live host module references. No bunfs path leaves this loader.
build.onLoad({ filter: /.*/, namespace: BUNDLED_VIRTUAL_NAMESPACE }, async args => {
return { contents: await synthesizeBundledModuleSource(args.path), loader: "js" };
});
},
});
}
/** Test seam: clears the memoized canonical specifier resolutions. */
export function __resetLegacyPiResolutionCache(): void {
clearLegacyPiResolutionCaches();
}