diff --git a/bun.lock b/bun.lock
index 4cde14918..587c7d05e 100644
--- a/bun.lock
+++ b/bun.lock
@@ -76,6 +76,7 @@
"dependencies": {
"@agentclientprotocol/sdk": "catalog:",
"@babel/parser": "catalog:",
+ "@babel/traverse": "catalog:",
"@mozilla/readability": "catalog:",
"@oh-my-pi/hashline": "catalog:",
"@oh-my-pi/omp-stats": "catalog:",
diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md
index b915e340a..dc1e59c91 100644
--- a/packages/coding-agent/CHANGELOG.md
+++ b/packages/coding-agent/CHANGELOG.md
@@ -84,6 +84,9 @@
- Fixed in-progress aborts awaiting `session_stop` extension handlers whose results would be discarded.
- Fixed `/retry` reporting "Nothing to retry" after a stream stalled or aborted mid-tool-call.
- Fixed locally consumed extension commands triggering automatic title generation and exposing their command text to the title model.
+### Fixed
+
+- Fixed compiled binaries failing to load legacy Pi extensions with minified imports, `pi-ai/compat`, or transitive runtime dependencies. The compatibility loader now follows compact static imports, resolves transitive on-disk ESM imports and CommonJS requires with package conditions, and restores the legacy `copyToClipboard` and `decodeKittyPrintable` root exports used by `pi-vimmode` and `pi-web-access`.
## [17.0.7] - 2026-07-21
diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json
index 530b7a001..567d59c5a 100644
--- a/packages/coding-agent/package.json
+++ b/packages/coding-agent/package.json
@@ -51,6 +51,7 @@
"dependencies": {
"@agentclientprotocol/sdk": "catalog:",
"@babel/parser": "catalog:",
+ "@babel/traverse": "catalog:",
"@mozilla/readability": "catalog:",
"@oh-my-pi/hashline": "catalog:",
"@oh-my-pi/omp-stats": "catalog:",
diff --git a/packages/coding-agent/scripts/legacy-pi-virtual-module.ts b/packages/coding-agent/scripts/legacy-pi-virtual-module.ts
index b759a6044..a83ef4f7f 100644
--- a/packages/coding-agent/scripts/legacy-pi-virtual-module.ts
+++ b/packages/coding-agent/scripts/legacy-pi-virtual-module.ts
@@ -19,7 +19,7 @@ const BUNDLED_PACKAGES: readonly BundledPackage[] = [
{ dir: "ai", identifier: "PiAi", rootShim: "legacy-pi-ai-shim.ts" },
{ dir: "coding-agent", identifier: "PiCodingAgent", rootShim: "legacy-pi-coding-agent-shim.ts" },
{ dir: "natives", identifier: "PiNatives", rootShim: null },
- { dir: "tui", identifier: "PiTui", rootShim: null },
+ { dir: "tui", identifier: "PiTui", rootShim: "legacy-pi-tui-shim.ts" },
{ dir: "utils", identifier: "PiUtils", rootShim: null },
];
diff --git a/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts b/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts
index 6542a6481..ef2c08a6e 100644
--- a/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts
+++ b/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts
@@ -1362,4 +1362,5 @@ export function getPackageDir(): string {
export * from "../index";
export { formatBytes as formatSize } from "../tools/render-utils";
+export { copyToClipboard } from "../utils/clipboard";
export { Type } from "./typebox";
diff --git a/packages/coding-agent/src/extensibility/legacy-pi-tui-shim.ts b/packages/coding-agent/src/extensibility/legacy-pi-tui-shim.ts
new file mode 100644
index 000000000..4ac5f57fd
--- /dev/null
+++ b/packages/coding-agent/src/extensibility/legacy-pi-tui-shim.ts
@@ -0,0 +1,10 @@
+/**
+ * Compatibility shim for legacy extensions importing the package root of
+ * `@earendil-works/pi-tui` or `@mariozechner/pi-tui`.
+ *
+ * The historical root exported `decodeKittyPrintable`; the canonical TUI now
+ * exposes the equivalent, broader `decodePrintableKey` helper. Keep the legacy
+ * name available without reintroducing it into the canonical package surface.
+ */
+export * from "@oh-my-pi/pi-tui";
+export { decodePrintableKey as decodeKittyPrintable } from "@oh-my-pi/pi-tui";
diff --git a/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts b/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts
index 40d1a7ca8..7d9b287f2 100644
--- a/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts
+++ b/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts
@@ -1,13 +1,35 @@
///
+
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 * as traverseModule from "@babel/traverse";
import { isCompiledBinary, stripWindowsExtendedLengthPathPrefix } from "@oh-my-pi/pi-utils";
import { registerPluginCacheInvalidator } from "../../discovery/helpers";
const IS_COMPILED_BINARY = isCompiledBinary();
+function isBabelTraverse(value: unknown): value is typeof traverseModule.default {
+ return typeof value === "function";
+}
+
+// Bun's compiled CJS interop wraps Babel traverse's default one level deeper.
+const traverseDefault: unknown = traverseModule.default;
+const nestedTraverse =
+ traverseDefault !== null && typeof traverseDefault === "object" && "default" in traverseDefault
+ ? traverseDefault.default
+ : undefined;
+const traverseCandidate = isBabelTraverse(traverseDefault) ? traverseDefault : nestedTraverse;
+if (!isBabelTraverse(traverseCandidate)) {
+ throw new TypeError(
+ `Invalid @babel/traverse export: expected function, got default=${typeof traverseDefault}, nested=${typeof nestedTraverse}`,
+ );
+}
+const traverseAst = traverseCandidate;
+
// === Bundled host modules (issue #3423) ===
//
// Bun 1.3.14 stopped exposing `--compile` extras through any filesystem-style
@@ -42,6 +64,99 @@ interface BundledVirtualResolveResult {
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 },
+ );
+ }
+}
+
+function collectExtensionSpecifierReferences(
+ source: string,
+ importerPath: string,
+ ast: ParseResult = parseExtensionSource(source, importerPath),
+): ExtensionSpecifierReference[] {
+ const references: ExtensionSpecifierReference[] = [];
+ const record = (kind: ExtensionSpecifierReference["kind"], literal: unknown): void => {
+ if (!literal || typeof literal !== "object") return;
+ const node = literal as { type?: string; value?: unknown; start?: number | null; end?: number | null };
+ 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 });
+ }
+ };
+ traverseAst(ast, {
+ enter(nodePath) {
+ const node = nodePath.node;
+ 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 === "CallExpression") {
+ if (node.callee.type === "Import") {
+ record("import", node.arguments[0]);
+ } else if (
+ node.callee.type === "Identifier" &&
+ node.callee.name === "require" &&
+ !nodePath.scope.hasBinding("require", true)
+ ) {
+ record("require", node.arguments[0]);
+ }
+ }
+ },
+ });
+ return references;
+}
+
+function applySpecifierReplacements(
+ source: string,
+ replacements: ReadonlyArray,
+): 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 = {};
let bundledModuleLoadersPromise: Promise | null = null;
@@ -180,6 +295,7 @@ const PI_PACKAGE_ALTERNATION = PI_PACKAGE_NAMES.join("|");
const PI_SUBPATH_REMAPS: ReadonlyMap = new Map([
["pi-ai/utils/oauth", "pi-ai/oauth"],
["pi-ai/utils/oauth/", "pi-ai/oauth/"],
+ ["pi-ai/compat", "pi-ai"],
]);
function remapLegacyPiSubpath(rest: string): string {
@@ -198,18 +314,16 @@ function remapLegacyPiSubpath(rest: string): string {
}
const LEGACY_PI_SPECIFIER_FILTER = new RegExp(`^@(?:${PI_SCOPE_ALTERNATION})/(?:${PI_PACKAGE_ALTERNATION})(?:/.*)?$`);
-const LEGACY_PI_IMPORT_SPECIFIER_REGEX = new RegExp(
- `((?:from\\s+|import\\s+|import\\s*\\(\\s*)["'])(@(?:${PI_SCOPE_ALTERNATION})/(?:${PI_PACKAGE_ALTERNATION})(?:/[^"'()\\s]+)?)(["'])`,
- "g",
-);
const resolvedSpecifierFallbacks = new Map();
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();
const packageImportsCache = new Map | null>();
const nodePackageRootCache = new Map>();
const packageManifestCache = new Map | null>>();
const bareDependencyResolutionCache = new Map>();
+const bareRequireResolutionCache = new Map>();
const realpathCache = new Map>();
const nativeAddonResolutionCache = new Map>();
const nativeAddonRequireScanCache = new Map>();
@@ -222,6 +336,7 @@ function clearLegacyPiResolutionCaches(): void {
nodePackageRootCache.clear();
packageManifestCache.clear();
bareDependencyResolutionCache.clear();
+ bareRequireResolutionCache.clear();
nativeAddonResolutionCache.clear();
nativeAddonRequireScanCache.clear();
nativeAddonLoaderModulePaths.clear();
@@ -338,14 +453,21 @@ const LEGACY_PI_CODING_AGENT_SHIM_PATH = IS_COMPILED_BINARY
? bundledModuleVirtualSpecifier(`${CANONICAL_PI_SCOPE}/pi-coding-agent`)
: sourceShimPath("legacy-pi-coding-agent-shim.ts");
-// Package-root overrides. Shim entries (`pi-ai`, `pi-coding-agent`) always
-// replace the canonical surface so the legacy `Type` runtime and the legacy
-// helpers stay reachable. The bundled host packages (`pi-agent-core`,
-// `pi-natives`, `pi-tui`, `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 the bundled packages live at `node_modules/@oh-my-pi/pi-*`.
+// 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:` specifiers handed
// to the synthetic onLoad in `installLegacyPiSpecifierShim()` — bunfs paths
@@ -390,6 +512,7 @@ export function __buildLegacyPiPackageRootOverrides(
const candidates: Record = {
[`${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) {
@@ -473,33 +596,6 @@ function toImportSpecifier(resolvedPath: string): string {
return url.pathToFileURL(stripWindowsExtendedLengthPathPrefix(resolvedPath)).href;
}
-function rewriteLegacyPiImports(source: string): string {
- return source.replace(
- LEGACY_PI_IMPORT_SPECIFIER_REGEX,
- (match, prefix: string, specifier: string, suffix: string) => {
- const remappedSpecifier = remapLegacyPiSpecifier(specifier);
- if (!remappedSpecifier) {
- return match;
- }
-
- try {
- return `${prefix}${toImportSpecifier(resolveCanonicalPiSpecifier(remappedSpecifier))}${suffix}`;
- } catch {
- // Resolution failed — typically in compiled binary mode where
- // Bun.resolveSync cannot walk up from /$bunfs/root to find the
- // bundled node_modules. Leave the specifier unchanged so Bun
- // resolves it natively against the extension's own peer deps.
- return match;
- }
- },
- );
-}
-
-// Match the bare TypeBox import specifiers (static + dynamic). Subpath imports
-// like `@sinclair/typebox/compiler` are intentionally excluded — they expose
-// TypeBox-only APIs the Zod-backed shim does not provide.
-const TYPEBOX_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["'])(@sinclair\/typebox|typebox)(["'])/g;
-
/**
* Rewrite the extension-owned specifiers OMP must host-resolve — legacy
* `@(scope)/pi-*`, bare TypeBox packages, package `imports` aliases like
@@ -508,43 +604,61 @@ const TYPEBOX_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["'
* are left untouched so Bun resolves them from the extension's real on-disk
* location.
*
- * When `mtimeTag` is provided, extension-owned graph specifiers (relative
- * `./`/`../`, package `#alias/*`, and extension-local bare deps) also carry a
- * `?mtime=` cache-bust so Bun rekeys them on same-process reloads. 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.
+ * When `mtimeTag` is provided, extension-owned relative graph specifiers
+ * (`./`/`../`) and, by default, resolved package `#alias/*` and extension-local
+ * bare deps also carry a `?mtime=` 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 {
// 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 withPi = rewriteLegacyPiImports(source);
- // When the TypeBox shim is missing (release build dropped the entrypoint —
- // issue #3414), leave bare specifiers untouched so Bun resolves a real
- // `typebox` / `@sinclair/typebox` install from the extension's own
- // `node_modules`. `resolveTypeBoxSpecifier` mirrors the fall-through.
- const withTypeBox = TYPEBOX_SHIM_PATH
- ? withPi.replace(
- TYPEBOX_IMPORT_SPECIFIER_REGEX,
- (_match, prefix: string, _specifier: string, suffix: string) =>
- `${prefix}${toImportSpecifier(TYPEBOX_SHIM_PATH)}${suffix}`,
- )
- : withPi;
- const withPkg = await rewriteExtensionPackageImports(withTypeBox, importerPath, mtimeTag);
- const withBare = await rewriteExtensionBareImports(withPkg, importerPath, mtimeTag);
- const withNativeAddons = await rewriteExtensionNativeAddonRequires(withBare, importerPath);
- if (!mtimeTag) {
- return withNativeAddons;
+ const references = collectExtensionSpecifierReferences(source, importerPath);
+ const replacements: Array = [];
+ 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 });
+ }
}
- return withNativeAddons.replace(
- RELATIVE_GRAPH_IMPORT_SPECIFIER_REGEX,
- (_match, prefix: string, specifier: string, suffix: string) => `${prefix}${specifier}?mtime=${mtimeTag}${suffix}`,
- );
+ const withImports = applySpecifierReplacements(source, replacements);
+ return rewriteExtensionSpecifiers(withImports, importerPath);
}
/** Test seam for compiled-binary legacy extension source rewriting. */
@@ -552,15 +666,11 @@ export async function __rewriteLegacyExtensionSourceForTests(
source: string,
importerPath: string,
mtimeTag: string | null = null,
+ resolvedImportMtimeTag: string | null = mtimeTag,
): Promise {
- return rewriteLegacyExtensionSource(source, importerPath, mtimeTag);
+ return rewriteLegacyExtensionSource(source, importerPath, mtimeTag, resolvedImportMtimeTag);
}
-// Match relative graph specifiers so their `./foo.ts` /`../foo` targets get a
-// `?mtime=` cache-bust suffix without disturbing already-rewritten
-// `file://` URLs or bare/host specifiers.
-const RELATIVE_GRAPH_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["'])(\.\.?\/[^"'?\s]*)(["'])/g;
-
/**
* Build the import specifier for a graph-resolved absolute path. POSIX
* emits a bare filesystem path with an optional `?mtime=` (Bun keys
@@ -628,6 +738,33 @@ async function resolveSourceModuleFile(basePath: string): Promise
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 {
+ 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 {
+ 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 {
let dir = path.dirname(importerPath);
while (true) {
@@ -672,7 +809,10 @@ async function readPackageImports(packageRoot: string): Promise = SUPPORTED_PACKAGE_IMPORT_CONDITIONS,
+): PackageImportTargetSelection {
if (entry === null) {
return PACKAGE_IMPORT_EXCLUDED;
}
@@ -681,7 +821,7 @@ function selectPackageImportTarget(entry: unknown): PackageImportTargetSelection
}
if (Array.isArray(entry)) {
for (const item of entry) {
- const target = selectPackageImportTarget(item);
+ const target = selectPackageImportTarget(item, conditions);
if (target !== null) return target;
}
return null;
@@ -690,10 +830,10 @@ function selectPackageImportTarget(entry: unknown): PackageImportTargetSelection
return null;
}
for (const [condition, value] of Object.entries(entry)) {
- if (!SUPPORTED_PACKAGE_IMPORT_CONDITIONS.has(condition)) {
+ if (!conditions.has(condition)) {
continue;
}
- const target = selectPackageImportTarget(value);
+ const target = selectPackageImportTarget(value, conditions);
if (target !== null) return target;
}
return null;
@@ -708,7 +848,7 @@ async function resolvePackageImportTarget(
return null;
}
const substituted = wildcard === null ? target : target.replaceAll("*", wildcard);
- return resolveSourceModuleFile(path.resolve(packageRoot, substituted));
+ return resolvePackageSourceTarget(packageRoot, path.resolve(packageRoot, substituted));
}
async function resolvePackageImportSpecifier(specifier: string, importerPath: string): Promise {
@@ -765,38 +905,6 @@ async function resolvePackageImportSpecifier(specifier: string, importerPath: st
return resolvePackageImportTarget(packageRoot, bestMatch.target, bestMatch.wildcard);
}
-const PACKAGE_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["'])(#[^"'()\s]+)(["'])/g;
-
-async function rewriteExtensionPackageImports(
- source: string,
- importerPath: string,
- mtimeTag: string | null = null,
-): Promise {
- let rewritten = "";
- let lastIndex = 0;
- for (const match of source.matchAll(PACKAGE_IMPORT_SPECIFIER_REGEX)) {
- const matchIndex = match.index;
- if (matchIndex === undefined) continue;
-
- const [fullMatch, prefix, specifier, suffix] = match;
- if (!prefix || !specifier || !suffix) continue;
-
- const resolved = await resolvePackageImportSpecifier(specifier, importerPath);
- if (!resolved) continue;
-
- rewritten += source.slice(lastIndex, matchIndex);
- rewritten += `${prefix}${toGraphImportSpecifier(resolved, mtimeTag)}${suffix}`;
- lastIndex = matchIndex + fullMatch.length;
- }
-
- if (lastIndex === 0) {
- return source;
- }
- return `${rewritten}${source.slice(lastIndex)}`;
-}
-
-const BARE_EXTENSION_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["'])([^"'()\s]+)(["'])/g;
-
function isBareExtensionDependencySpecifier(specifier: string): boolean {
if (
specifier.startsWith(".") ||
@@ -872,6 +980,58 @@ async function readPackageManifestUncached(packageRoot: string): Promise {
+ 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 {
+ 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,
@@ -881,16 +1041,17 @@ async function resolvePackageExportTarget(
return null;
}
const substituted = wildcard === null ? target : target.replaceAll("*", wildcard);
- return resolveSourceModuleFile(path.resolve(packageRoot, substituted));
+ return resolvePackageSourceTarget(packageRoot, path.resolve(packageRoot, substituted));
}
async function resolveNodePackageExport(
packageRoot: string,
subpath: string | null,
manifest: Record,
+ conditions: ReadonlySet = SUPPORTED_PACKAGE_IMPORT_CONDITIONS,
): Promise {
const exportsField = manifest.exports;
- const rootTarget = subpath === null ? selectPackageImportTarget(exportsField) : null;
+ const rootTarget = subpath === null ? selectPackageImportTarget(exportsField, conditions) : null;
if (rootTarget !== null && rootTarget !== PACKAGE_IMPORT_EXCLUDED) {
return resolvePackageExportTarget(packageRoot, rootTarget, null);
}
@@ -899,30 +1060,41 @@ async function resolveNodePackageExport(
}
const exactKey = subpath === null ? "." : `./${subpath}`;
- const exactTarget = selectPackageImportTarget(exportsField[exactKey]);
- if (exactTarget !== null && exactTarget !== PACKAGE_IMPORT_EXCLUDED) {
- return resolvePackageExportTarget(packageRoot, exactTarget, null);
+ 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) continue;
+ 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 (!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),
+ };
}
- const target = selectPackageImportTarget(entry);
- if (target === null || target === PACKAGE_IMPORT_EXCLUDED) {
- continue;
- }
- return resolvePackageExportTarget(
- packageRoot,
- target,
- subpath.slice(prefix.length, subpath.length - suffix.length),
- );
}
- return null;
+ return bestMatch?.target && bestMatch.target !== PACKAGE_IMPORT_EXCLUDED
+ ? resolvePackageExportTarget(packageRoot, bestMatch.target, bestMatch.wildcard)
+ : null;
}
async function resolveNodePackageFallback(
@@ -931,16 +1103,16 @@ async function resolveNodePackageFallback(
manifest: Record,
): Promise {
if (subpath !== null) {
- return resolveSourceModuleFile(path.join(packageRoot, subpath));
+ return resolvePackageSourceTarget(packageRoot, path.join(packageRoot, subpath));
}
for (const field of ["module", "main"]) {
const target = manifest[field];
if (typeof target === "string") {
- const resolved = await resolveSourceModuleFile(path.resolve(packageRoot, target));
+ const resolved = await resolvePackageSourceTarget(packageRoot, path.resolve(packageRoot, target));
if (resolved) return resolved;
}
}
- return resolveSourceModuleFile(path.join(packageRoot, "index"));
+ return resolvePackageSourceTarget(packageRoot, path.join(packageRoot, "index"));
}
async function resolveNodePackageDependency(specifier: string, importerPath: string): Promise {
@@ -950,9 +1122,72 @@ async function resolveNodePackageDependency(specifier: string, importerPath: str
if (!packageRoot) return null;
const manifest = await readPackageManifest(packageRoot);
if (!manifest) return null;
- return (
- (await resolveNodePackageExport(packageRoot, parsed.subpath, manifest)) ??
- (await resolveNodePackageFallback(packageRoot, parsed.subpath, manifest))
+ return Object.hasOwn(manifest, "exports")
+ ? resolveNodePackageExport(packageRoot, parsed.subpath, manifest)
+ : resolveNodePackageFallback(packageRoot, parsed.subpath, manifest);
+}
+
+async function resolveNodePackageRequire(specifier: string, importerPath: string): Promise {
+ 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 {
+ 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 {
+ 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)),
);
}
@@ -971,24 +1206,26 @@ async function resolveExtensionBareDependency(specifier: string, importerPath: s
}
async function resolveExtensionBareDependencyUncached(specifier: string, importerPath: string): Promise {
+ // 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 resolved;
+ return validateResolvedBarePackagePath(specifier, importerPath, resolved);
}
} catch {
// Compiled binaries do not reliably resolve runtime extension node_modules.
}
- return resolveNodePackageDependency(specifier, importerPath);
+ return null;
}
const NATIVE_ADDON_EXTENSION = ".node";
-// Match CommonJS require calls so bare native-addon specifiers can be pinned
-// to absolute paths. Only requires whose resolution lands on a `.node` addon
-// are rewritten; everything else stays on Bun's native resolver.
-const NATIVE_ADDON_REQUIRE_SPECIFIER_REGEX = /(\brequire\s*\(\s*["'])([^"'()\s]+)(["']\s*\))/g;
-
/**
* 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
@@ -1025,48 +1262,119 @@ async function resolveExtensionNativeAddonUncached(specifier: string, importerPa
target =
typeof main === "string" && main.endsWith(NATIVE_ADDON_EXTENSION) ? path.resolve(packageRoot, main) : null;
}
- if (!target || !(await pathExists(target))) {
+ if (!target) {
return null;
}
- return realpathOrSelf(target);
+ return resolvePackageFileTarget(packageRoot, target);
+}
+
+async function resolveExtensionBareRequire(specifier: string, importerPath: string): Promise {
+ 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 {
+ 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 bare `require()` specifiers that resolve to native `.node` addons
- * into absolute-path requires. In `bun build --compile` binaries, Bun's bare
- * resolution fails for packages whose `main` is a `.node` addon ("Cannot find
- * module '@scope/pkg-'") even when the package sits in the
- * extension's own node_modules; requiring the addon by absolute path works.
+ * 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 rewriteExtensionNativeAddonRequires(source: string, importerPath: string): Promise {
- let rewritten = "";
- let lastIndex = 0;
- for (const match of source.matchAll(NATIVE_ADDON_REQUIRE_SPECIFIER_REGEX)) {
- const matchIndex = match.index;
- if (matchIndex === undefined) continue;
-
- const [fullMatch, prefix, specifier, suffix] = match;
- if (!prefix || !specifier || !suffix) continue;
-
- const resolved = await resolveExtensionNativeAddon(specifier, importerPath);
+async function rewriteExtensionSpecifiers(
+ source: string,
+ importerPath: string,
+ rewriteImports = false,
+): Promise {
+ const references = collectExtensionSpecifierReferences(source, importerPath);
+ const resolvedSpecifierTargets = new Map();
+ const replacements: Array = [];
+ 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;
-
- rewritten += source.slice(lastIndex, matchIndex);
- // Forward slashes keep Windows paths valid inside single- or double-quoted literals.
- rewritten += `${prefix}${stripWindowsExtendedLengthPathPrefix(resolved).replaceAll("\\", "/")}${suffix}`;
- lastIndex = matchIndex + fullMatch.length;
+ 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);
+}
- if (lastIndex === 0) {
+function rewriteExtensionSpecifiersFromCache(source: string, importerPath: string): string {
+ const resolvedSpecifierTargets = extensionSynchronousSpecifierTargets.get(importerPath);
+ if (!resolvedSpecifierTargets || resolvedSpecifierTargets.size === 0) {
return source;
}
- return `${rewritten}${source.slice(lastIndex)}`;
+ const replacements: Array = [];
+ 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 rewriteExtensionNativeAddonRequires} can pin its
+ * extension graph so {@link rewriteExtensionSpecifiers} can pin its
* platform-package requires to absolute paths.
*/
async function moduleRequiresNativeAddon(modulePath: string): Promise {
@@ -1085,63 +1393,33 @@ async function moduleRequiresNativeAddonUncached(modulePath: string): Promise {
- let rewritten = "";
- let lastIndex = 0;
- for (const match of source.matchAll(BARE_EXTENSION_IMPORT_SPECIFIER_REGEX)) {
- const matchIndex = match.index;
- if (matchIndex === undefined) continue;
-
- const [fullMatch, prefix, specifier, suffix] = match;
- if (!prefix || !specifier || !suffix) continue;
-
- const resolved = await resolveExtensionBareDependency(specifier, importerPath);
- if (!resolved) continue;
-
- rewritten += source.slice(lastIndex, matchIndex);
- rewritten += `${prefix}${toGraphImportSpecifier(resolved, mtimeTag)}${suffix}`;
- lastIndex = matchIndex + fullMatch.length;
- }
-
- if (lastIndex === 0) {
- return source;
- }
- return `${rewritten}${source.slice(lastIndex)}`;
-}
-
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
-// Match source modules in an extension graph: relative imports, package
-// `imports` aliases such as `#src/*`, and extension-local bare dependency
-// entries. Bare imports inside node_modules dependencies remain native Bun
-// resolutions; once the dependency entry is hooked, its relative children are
-// still collected and rewritten with the reload mtime tag. `require()` calls
-// are scanned too so CJS entries and napi-rs loaders reached without an
-// import statement still join the graph.
-const EXTENSION_GRAPH_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["'])([^"'()\s]+)(["'])/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>();
+const extensionGraphCacheBustResolvedImportModules = new Map>();
const commonJsModuleSources = new Map();
const commonJsFallbackModulePaths = new Map();
+const extensionSynchronousSpecifierTargets = new Map>();
+const commonJsGraphModulePaths = new Set();
const COMMONJS_REQUIRE_GLOBAL = "__ompLegacyPiRequireGraphModule";
const commonJsModuleDefinitions = new Map();
const commonJsModuleCache = new Map<
@@ -1162,7 +1440,13 @@ function evaluateGraphCommonJs(modulePath: string): unknown {
if (cached) {
return cached.exports;
}
- const definition = commonJsModuleDefinitions.get(modulePath);
+ 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}`);
}
@@ -1179,6 +1463,14 @@ function evaluateGraphCommonJs(modulePath: string): unknown {
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 {
@@ -1186,7 +1478,7 @@ function evaluateGraphCommonJs(modulePath: string): unknown {
} catch {
// Builtins and virtual modules have no filesystem realpath.
}
- return commonJsModuleDefinitions.has(graphPath) ? evaluateGraphCommonJs(graphPath) : nativeRequire(specifier);
+ return commonJsGraphModulePaths.has(graphPath) ? evaluateGraphCommonJs(graphPath) : nativeRequire(specifier);
},
{
resolve: nativeRequire.resolve,
@@ -1234,30 +1526,43 @@ async function realpathOrSelfUncached(p: string): Promise {
}
}
+interface ExtensionModuleGraph {
+ readonly modules: Map;
+ readonly cacheBustResolvedImportModules: Set;
+ readonly commonJsPaths: Set;
+}
+
/**
* 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 and package `imports` aliases are always graph-owned.
- * Extension-local bare dependency entries are also included so their relative
- * children receive the reload mtime tag; bare imports inside those dependencies
- * remain native Bun resolutions to avoid taking over full third-party graphs.
- * CommonJS modules reached through `require()` stay on Bun's native loader
- * unless they resolve native addons. CommonJS reached through ESM imports stays
- * graph-owned so the load hook can expose its exports through an ESM default.
+ * 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