@oh-my-pi/pi-coding-agent 17.0.7 → 17.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/dist/cli.js +5025 -4930
  3. package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
  4. package/dist/types/config/model-registry.d.ts +28 -1
  5. package/dist/types/config/model-resolver.d.ts +5 -1
  6. package/dist/types/config/settings-schema.d.ts +50 -3
  7. package/dist/types/dap/client.d.ts +19 -0
  8. package/dist/types/extensibility/extensions/runner.d.ts +4 -2
  9. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  10. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
  11. package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
  12. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +1 -1
  13. package/dist/types/extensibility/typebox.d.ts +4 -0
  14. package/dist/types/hindsight/client.d.ts +10 -0
  15. package/dist/types/hindsight/config.d.ts +8 -0
  16. package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
  17. package/dist/types/lsp/client.d.ts +2 -0
  18. package/dist/types/mcp/manager.d.ts +6 -2
  19. package/dist/types/mcp/render.d.ts +2 -2
  20. package/dist/types/mcp/tool-bridge.d.ts +16 -3
  21. package/dist/types/mcp/types.d.ts +6 -0
  22. package/dist/types/mnemopi/state.d.ts +2 -0
  23. package/dist/types/modes/components/oauth-selector.d.ts +8 -0
  24. package/dist/types/modes/components/settings-defs.d.ts +1 -0
  25. package/dist/types/modes/components/user-message.d.ts +25 -1
  26. package/dist/types/modes/controllers/extension-ui-controller.d.ts +8 -0
  27. package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
  28. package/dist/types/modes/interactive-mode.d.ts +7 -1
  29. package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
  30. package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
  31. package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
  32. package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
  33. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  34. package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
  35. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  36. package/dist/types/modes/types.d.ts +14 -1
  37. package/dist/types/modes/utils/ui-helpers.d.ts +12 -8
  38. package/dist/types/sdk.d.ts +1 -0
  39. package/dist/types/session/agent-session-error-log.test.d.ts +1 -0
  40. package/dist/types/session/agent-session.d.ts +86 -3
  41. package/dist/types/session/session-loader.d.ts +6 -4
  42. package/dist/types/task/executor.d.ts +10 -0
  43. package/dist/types/task/index.d.ts +1 -0
  44. package/dist/types/task/prewalk.d.ts +3 -0
  45. package/dist/types/task/types.d.ts +14 -0
  46. package/dist/types/tools/ask.d.ts +10 -0
  47. package/dist/types/tools/hub/jobs.d.ts +1 -1
  48. package/dist/types/tools/index.d.ts +3 -0
  49. package/dist/types/tools/report-tool-issue.d.ts +24 -9
  50. package/dist/types/tools/tool-timeouts.d.ts +8 -2
  51. package/dist/types/utils/changelog.d.ts +4 -6
  52. package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
  53. package/dist/types/web/search/types.d.ts +1 -1
  54. package/package.json +13 -13
  55. package/scripts/legacy-pi-virtual-module.ts +1 -1
  56. package/src/cli/grievances-cli.ts +2 -2
  57. package/src/cli/models-cli.ts +37 -17
  58. package/src/cli/update-cli.ts +14 -1
  59. package/src/cli/usage-cli.ts +1 -1
  60. package/src/config/__tests__/model-registry.test.ts +147 -0
  61. package/src/config/model-registry.ts +87 -24
  62. package/src/config/model-resolver.ts +56 -32
  63. package/src/config/settings-schema.ts +46 -3
  64. package/src/config/settings.ts +68 -5
  65. package/src/dap/client.ts +86 -7
  66. package/src/edit/diff.ts +4 -4
  67. package/src/eval/py/prelude.py +5 -5
  68. package/src/export/html/template.js +23 -9
  69. package/src/extensibility/extensions/runner.ts +9 -4
  70. package/src/extensibility/extensions/types.ts +3 -0
  71. package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
  72. package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
  73. package/src/extensibility/plugins/legacy-pi-compat.ts +851 -310
  74. package/src/extensibility/typebox.ts +22 -9
  75. package/src/hindsight/client.test.ts +42 -0
  76. package/src/hindsight/client.ts +40 -3
  77. package/src/hindsight/config.ts +18 -0
  78. package/src/internal-urls/registry-helpers.ts +40 -0
  79. package/src/launch/broker.ts +31 -5
  80. package/src/lsp/client.ts +23 -0
  81. package/src/lsp/clients/biome-client.ts +3 -4
  82. package/src/lsp/index.ts +26 -8
  83. package/src/main.ts +16 -5
  84. package/src/mcp/manager.ts +39 -8
  85. package/src/mcp/render.ts +94 -35
  86. package/src/mcp/tool-bridge.ts +116 -11
  87. package/src/mcp/types.ts +7 -0
  88. package/src/mnemopi/state.ts +70 -3
  89. package/src/modes/components/agent-dashboard.ts +6 -2
  90. package/src/modes/components/agent-hub.ts +26 -9
  91. package/src/modes/components/chat-transcript-builder.ts +13 -2
  92. package/src/modes/components/diff.ts +2 -2
  93. package/src/modes/components/oauth-selector.ts +24 -7
  94. package/src/modes/components/plan-review-overlay.ts +20 -4
  95. package/src/modes/components/settings-defs.ts +5 -2
  96. package/src/modes/components/settings-selector.ts +9 -5
  97. package/src/modes/components/tool-execution.test.ts +63 -2
  98. package/src/modes/components/user-message.ts +100 -1
  99. package/src/modes/controllers/command-controller.ts +36 -12
  100. package/src/modes/controllers/event-controller.ts +4 -1
  101. package/src/modes/controllers/extension-ui-controller.ts +18 -0
  102. package/src/modes/controllers/input-controller.ts +47 -12
  103. package/src/modes/controllers/mcp-command-controller.ts +70 -40
  104. package/src/modes/controllers/selector-controller.ts +82 -6
  105. package/src/modes/interactive-mode.ts +39 -4
  106. package/src/modes/rpc/rpc-client.ts +94 -3
  107. package/src/modes/rpc/rpc-frame.ts +164 -4
  108. package/src/modes/rpc/rpc-messages.ts +127 -0
  109. package/src/modes/rpc/rpc-mode.ts +79 -7
  110. package/src/modes/rpc/rpc-types.ts +34 -2
  111. package/src/modes/setup-wizard/scenes/model.ts +5 -7
  112. package/src/modes/setup-wizard/scenes/providers.ts +3 -2
  113. package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
  114. package/src/modes/setup-wizard/scenes/theme.ts +13 -3
  115. package/src/modes/setup-wizard/scenes/types.ts +9 -1
  116. package/src/modes/setup-wizard/scenes/web-search.ts +6 -1
  117. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  118. package/src/modes/types.ts +16 -3
  119. package/src/modes/utils/ui-helpers.ts +40 -20
  120. package/src/prompts/goals/guided-goal-system.md +24 -3
  121. package/src/prompts/system/workflow-notice.md +1 -1
  122. package/src/prompts/tools/task.md +12 -2
  123. package/src/sdk.ts +179 -51
  124. package/src/session/agent-session-error-log.test.ts +59 -0
  125. package/src/session/agent-session.ts +351 -46
  126. package/src/session/session-context.test.ts +224 -1
  127. package/src/session/session-context.ts +41 -2
  128. package/src/session/session-loader.ts +10 -5
  129. package/src/slash-commands/helpers/usage-report.ts +3 -1
  130. package/src/system-prompt.ts +1 -2
  131. package/src/task/executor.ts +44 -27
  132. package/src/task/index.ts +63 -8
  133. package/src/task/prewalk.ts +6 -0
  134. package/src/task/structured-subagent.ts +3 -1
  135. package/src/task/types.ts +16 -0
  136. package/src/task/worktree.ts +25 -11
  137. package/src/tools/ask.ts +15 -0
  138. package/src/tools/bash.ts +14 -6
  139. package/src/tools/browser.ts +1 -1
  140. package/src/tools/debug.ts +1 -1
  141. package/src/tools/eval.ts +4 -5
  142. package/src/tools/fetch.ts +1 -1
  143. package/src/tools/hub/index.ts +1 -1
  144. package/src/tools/hub/jobs.ts +67 -8
  145. package/src/tools/hub/launch.ts +7 -0
  146. package/src/tools/index.ts +3 -0
  147. package/src/tools/report-tool-issue.ts +79 -28
  148. package/src/tools/tool-timeouts.ts +10 -3
  149. package/src/tools/write.ts +31 -0
  150. package/src/utils/changelog.ts +54 -58
  151. package/src/utils/git.ts +57 -49
  152. package/src/web/search/providers/firecrawl.ts +46 -13
  153. package/src/web/search/types.ts +5 -1
@@ -1,13 +1,35 @@
1
1
  /// <reference path="./legacy-pi-virtual-modules.d.ts" />
2
+
2
3
  import * as fs from "node:fs";
3
4
  import { createRequire, isBuiltin } from "node:module";
4
5
  import * as path from "node:path";
5
6
  import * as url from "node:url";
7
+ import type { ParseResult, ParserPlugin } from "@babel/parser";
8
+ import { parse as parseBabel } from "@babel/parser";
9
+ import * as traverseModule from "@babel/traverse";
6
10
  import { isCompiledBinary, stripWindowsExtendedLengthPathPrefix } from "@oh-my-pi/pi-utils";
7
11
  import { registerPluginCacheInvalidator } from "../../discovery/helpers";
8
12
 
9
13
  const IS_COMPILED_BINARY = isCompiledBinary();
10
14
 
15
+ function isBabelTraverse(value: unknown): value is typeof traverseModule.default {
16
+ return typeof value === "function";
17
+ }
18
+
19
+ // Bun's compiled CJS interop wraps Babel traverse's default one level deeper.
20
+ const traverseDefault: unknown = traverseModule.default;
21
+ const nestedTraverse =
22
+ traverseDefault !== null && typeof traverseDefault === "object" && "default" in traverseDefault
23
+ ? traverseDefault.default
24
+ : undefined;
25
+ const traverseCandidate = isBabelTraverse(traverseDefault) ? traverseDefault : nestedTraverse;
26
+ if (!isBabelTraverse(traverseCandidate)) {
27
+ throw new TypeError(
28
+ `Invalid @babel/traverse export: expected function, got default=${typeof traverseDefault}, nested=${typeof nestedTraverse}`,
29
+ );
30
+ }
31
+ const traverseAst = traverseCandidate;
32
+
11
33
  // === Bundled host modules (issue #3423) ===
12
34
  //
13
35
  // Bun 1.3.14 stopped exposing `--compile` extras through any filesystem-style
@@ -42,6 +64,104 @@ interface BundledVirtualResolveResult {
42
64
  namespace: typeof BUNDLED_VIRTUAL_NAMESPACE;
43
65
  }
44
66
 
67
+ interface ExtensionSpecifierReference {
68
+ readonly kind: "import" | "require";
69
+ readonly specifier: string;
70
+ readonly start: number;
71
+ readonly end: number;
72
+ }
73
+
74
+ function parseExtensionSource(source: string, importerPath: string): ParseResult {
75
+ const extension = path.extname(importerPath).toLowerCase();
76
+ const plugins: ParserPlugin[] = ["decorators-legacy", "explicitResourceManagement"];
77
+ if (extension === ".ts" || extension === ".mts" || extension === ".cts" || extension === ".tsx") {
78
+ plugins.push("typescript");
79
+ }
80
+ if (extension === ".jsx" || extension === ".tsx") {
81
+ plugins.push("jsx");
82
+ }
83
+
84
+ try {
85
+ return parseBabel(source, {
86
+ sourceType: "unambiguous",
87
+ allowAwaitOutsideFunction: true,
88
+ allowReturnOutsideFunction: true,
89
+ allowImportExportEverywhere: true,
90
+ allowNewTargetOutsideFunction: true,
91
+ allowSuperOutsideMethod: true,
92
+ allowUndeclaredExports: true,
93
+ errorRecovery: true,
94
+ plugins,
95
+ });
96
+ } catch (error) {
97
+ throw new Error(
98
+ `Failed to parse extension source for dependency rewriting: ${importerPath}: ${error instanceof Error ? error.message : String(error)}`,
99
+ { cause: error },
100
+ );
101
+ }
102
+ }
103
+
104
+ function collectExtensionSpecifierReferences(
105
+ source: string,
106
+ importerPath: string,
107
+ ast: ParseResult = parseExtensionSource(source, importerPath),
108
+ ): ExtensionSpecifierReference[] {
109
+ const references: ExtensionSpecifierReference[] = [];
110
+ const record = (kind: ExtensionSpecifierReference["kind"], literal: unknown): void => {
111
+ if (!literal || typeof literal !== "object") return;
112
+ const node = literal as { type?: string; value?: unknown; start?: number | null; end?: number | null };
113
+ if (
114
+ node.type === "StringLiteral" &&
115
+ typeof node.value === "string" &&
116
+ typeof node.start === "number" &&
117
+ typeof node.end === "number"
118
+ ) {
119
+ references.push({ kind, specifier: node.value, start: node.start, end: node.end });
120
+ }
121
+ };
122
+ traverseAst(ast, {
123
+ enter(nodePath) {
124
+ const node = nodePath.node;
125
+ if (
126
+ node.type === "ImportDeclaration" ||
127
+ node.type === "ExportNamedDeclaration" ||
128
+ node.type === "ExportAllDeclaration"
129
+ ) {
130
+ record("import", node.source);
131
+ } else if (node.type === "ImportExpression") {
132
+ record("import", node.source);
133
+ } else if (
134
+ node.type === "TSImportEqualsDeclaration" &&
135
+ node.moduleReference.type === "TSExternalModuleReference"
136
+ ) {
137
+ record("require", node.moduleReference.expression);
138
+ } else if (node.type === "CallExpression") {
139
+ if (node.callee.type === "Import") {
140
+ record("import", node.arguments[0]);
141
+ } else if (
142
+ node.callee.type === "Identifier" &&
143
+ node.callee.name === "require" &&
144
+ !nodePath.scope.hasBinding("require", true)
145
+ ) {
146
+ record("require", node.arguments[0]);
147
+ }
148
+ }
149
+ },
150
+ });
151
+ return references;
152
+ }
153
+
154
+ function applySpecifierReplacements(
155
+ source: string,
156
+ replacements: ReadonlyArray<ExtensionSpecifierReference & { readonly replacement: string }>,
157
+ ): string {
158
+ let rewritten = source;
159
+ for (const reference of [...replacements].sort((left, right) => right.start - left.start)) {
160
+ rewritten = `${rewritten.slice(0, reference.start)}${JSON.stringify(reference.replacement)}${rewritten.slice(reference.end)}`;
161
+ }
162
+ return rewritten;
163
+ }
164
+
45
165
  const loadedBundledModules: Record<string, BundledModule> = {};
46
166
  let bundledModuleLoadersPromise: Promise<BundledModuleLoaders> | null = null;
47
167
 
@@ -180,6 +300,7 @@ const PI_PACKAGE_ALTERNATION = PI_PACKAGE_NAMES.join("|");
180
300
  const PI_SUBPATH_REMAPS: ReadonlyMap<string, string> = new Map<string, string>([
181
301
  ["pi-ai/utils/oauth", "pi-ai/oauth"],
182
302
  ["pi-ai/utils/oauth/", "pi-ai/oauth/"],
303
+ ["pi-ai/compat", "pi-ai"],
183
304
  ]);
184
305
 
185
306
  function remapLegacyPiSubpath(rest: string): string {
@@ -198,18 +319,16 @@ function remapLegacyPiSubpath(rest: string): string {
198
319
  }
199
320
 
200
321
  const LEGACY_PI_SPECIFIER_FILTER = new RegExp(`^@(?:${PI_SCOPE_ALTERNATION})/(?:${PI_PACKAGE_ALTERNATION})(?:/.*)?$`);
201
- const LEGACY_PI_IMPORT_SPECIFIER_REGEX = new RegExp(
202
- `((?:from\\s+|import\\s+|import\\s*\\(\\s*)["'])(@(?:${PI_SCOPE_ALTERNATION})/(?:${PI_PACKAGE_ALTERNATION})(?:/[^"'()\\s]+)?)(["'])`,
203
- "g",
204
- );
205
322
  const resolvedSpecifierFallbacks = new Map<string, string>();
206
323
  const SOURCE_MODULE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"] as const;
207
324
  const SUPPORTED_PACKAGE_IMPORT_CONDITIONS = new Set(["bun", "node", "import", "default"]);
325
+ const SUPPORTED_PACKAGE_REQUIRE_CONDITIONS = new Set(["bun", "node", "require", "default"]);
208
326
  const packageRootCache = new Map<string, string | null>();
209
327
  const packageImportsCache = new Map<string, Record<string, unknown> | null>();
210
328
  const nodePackageRootCache = new Map<string, Promise<string | null>>();
211
329
  const packageManifestCache = new Map<string, Promise<Record<string, unknown> | null>>();
212
330
  const bareDependencyResolutionCache = new Map<string, Promise<string | null>>();
331
+ const bareRequireResolutionCache = new Map<string, Promise<string | null>>();
213
332
  const realpathCache = new Map<string, Promise<string>>();
214
333
  const nativeAddonResolutionCache = new Map<string, Promise<string | null>>();
215
334
  const nativeAddonRequireScanCache = new Map<string, Promise<boolean>>();
@@ -222,6 +341,7 @@ function clearLegacyPiResolutionCaches(): void {
222
341
  nodePackageRootCache.clear();
223
342
  packageManifestCache.clear();
224
343
  bareDependencyResolutionCache.clear();
344
+ bareRequireResolutionCache.clear();
225
345
  nativeAddonResolutionCache.clear();
226
346
  nativeAddonRequireScanCache.clear();
227
347
  nativeAddonLoaderModulePaths.clear();
@@ -338,14 +458,21 @@ const LEGACY_PI_CODING_AGENT_SHIM_PATH = IS_COMPILED_BINARY
338
458
  ? bundledModuleVirtualSpecifier(`${CANONICAL_PI_SCOPE}/pi-coding-agent`)
339
459
  : sourceShimPath("legacy-pi-coding-agent-shim.ts");
340
460
 
341
- // Package-root overrides. Shim entries (`pi-ai`, `pi-coding-agent`) always
342
- // replace the canonical surface so the legacy `Type` runtime and the legacy
343
- // helpers stay reachable. The bundled host packages (`pi-agent-core`,
344
- // `pi-natives`, `pi-tui`, `pi-utils`) are added only in compiled-binary mode
345
- // to route extensions onto the in-process module instance — in dev /
346
- // source-link / installed-package mode the canonical specifier resolves
347
- // cleanly through `Bun.resolveSync` and hardcoding a source-tree path would
348
- // miss installs where the bundled packages live at `node_modules/@oh-my-pi/pi-*`.
461
+ // Legacy pi-tui exported `decodeKittyPrintable` from its package root. The
462
+ // canonical TUI replaced it with the broader `decodePrintableKey`; route only
463
+ // legacy root imports through a sibling shim that preserves the old name.
464
+ const LEGACY_PI_TUI_SHIM_PATH = IS_COMPILED_BINARY
465
+ ? bundledModuleVirtualSpecifier(`${CANONICAL_PI_SCOPE}/pi-tui`)
466
+ : sourceShimPath("legacy-pi-tui-shim.ts");
467
+
468
+ // Package-root overrides. Shim entries (`pi-ai`, `pi-coding-agent`, `pi-tui`)
469
+ // always replace the canonical surface so legacy helpers stay reachable. The
470
+ // other bundled host packages (`pi-agent-core`, `pi-natives`, `pi-utils`) are
471
+ // added only in compiled-binary mode to route extensions onto the in-process
472
+ // module instance — in dev / source-link / installed-package mode the canonical
473
+ // specifier resolves cleanly through `Bun.resolveSync` and hardcoding a
474
+ // source-tree path would miss installs where bundled packages live at
475
+ // `node_modules/@oh-my-pi/pi-*`.
349
476
  //
350
477
  // Compiled-binary entries are `omp-legacy-pi-bundled:<key>` specifiers handed
351
478
  // to the synthetic onLoad in `installLegacyPiSpecifierShim()` — bunfs paths
@@ -390,6 +517,7 @@ export function __buildLegacyPiPackageRootOverrides(
390
517
  const candidates: Record<string, string> = {
391
518
  [`${CANONICAL_PI_SCOPE}/pi-ai`]: LEGACY_PI_AI_SHIM_PATH,
392
519
  [`${CANONICAL_PI_SCOPE}/pi-coding-agent`]: LEGACY_PI_CODING_AGENT_SHIM_PATH,
520
+ [`${CANONICAL_PI_SCOPE}/pi-tui`]: LEGACY_PI_TUI_SHIM_PATH,
393
521
  };
394
522
  if (isCompiled) {
395
523
  for (const key of bundledModuleKeys) {
@@ -473,33 +601,6 @@ function toImportSpecifier(resolvedPath: string): string {
473
601
  return url.pathToFileURL(stripWindowsExtendedLengthPathPrefix(resolvedPath)).href;
474
602
  }
475
603
 
476
- function rewriteLegacyPiImports(source: string): string {
477
- return source.replace(
478
- LEGACY_PI_IMPORT_SPECIFIER_REGEX,
479
- (match, prefix: string, specifier: string, suffix: string) => {
480
- const remappedSpecifier = remapLegacyPiSpecifier(specifier);
481
- if (!remappedSpecifier) {
482
- return match;
483
- }
484
-
485
- try {
486
- return `${prefix}${toImportSpecifier(resolveCanonicalPiSpecifier(remappedSpecifier))}${suffix}`;
487
- } catch {
488
- // Resolution failed — typically in compiled binary mode where
489
- // Bun.resolveSync cannot walk up from /$bunfs/root to find the
490
- // bundled node_modules. Leave the specifier unchanged so Bun
491
- // resolves it natively against the extension's own peer deps.
492
- return match;
493
- }
494
- },
495
- );
496
- }
497
-
498
- // Match the bare TypeBox import specifiers (static + dynamic). Subpath imports
499
- // like `@sinclair/typebox/compiler` are intentionally excluded — they expose
500
- // TypeBox-only APIs the Zod-backed shim does not provide.
501
- const TYPEBOX_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["'])(@sinclair\/typebox|typebox)(["'])/g;
502
-
503
604
  /**
504
605
  * Rewrite the extension-owned specifiers OMP must host-resolve — legacy
505
606
  * `@(scope)/pi-*`, bare TypeBox packages, package `imports` aliases like
@@ -508,43 +609,61 @@ const TYPEBOX_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["'
508
609
  * are left untouched so Bun resolves them from the extension's real on-disk
509
610
  * location.
510
611
  *
511
- * When `mtimeTag` is provided, extension-owned graph specifiers (relative
512
- * `./`/`../`, package `#alias/*`, and extension-local bare deps) also carry a
513
- * `?mtime=<tag>` cache-bust so Bun rekeys them on same-process reloads. Host
514
- * package rewrites (legacy `@(scope)/pi-*`, TypeBox shim) always emit
515
- * `file://` URLs because they resolve to in-process host code that never
516
- * changes between reloads.
612
+ * When `mtimeTag` is provided, extension-owned relative graph specifiers
613
+ * (`./`/`../`) and, by default, resolved package `#alias/*` and extension-local
614
+ * bare deps also carry a `?mtime=<tag>` cache-bust so Bun rekeys them on
615
+ * same-process reloads. `resolvedImportMtimeTag` can disable the tag for
616
+ * resolved package and bare ESM imports inside third-party dependencies, whose
617
+ * transitive ESM imports retain a query-free importer path for Bun's runtime
618
+ * `node_modules` resolution. Host package rewrites (legacy
619
+ * `@(scope)/pi-*`, TypeBox shim) always emit `file://` URLs because they resolve
620
+ * to in-process host code that never changes between reloads.
517
621
  */
518
622
  async function rewriteLegacyExtensionSource(
519
623
  source: string,
520
624
  importerPath: string,
521
625
  mtimeTag: string | null = null,
626
+ resolvedImportMtimeTag: string | null = mtimeTag,
522
627
  ): Promise<string> {
523
628
  // Compiled mode completes the override map from the build-supplied module
524
629
  // keys on first use; every rewrite path must see the full map.
525
630
  await ensureLegacyPiOverridesReady();
526
- const withPi = rewriteLegacyPiImports(source);
527
- // When the TypeBox shim is missing (release build dropped the entrypoint —
528
- // issue #3414), leave bare specifiers untouched so Bun resolves a real
529
- // `typebox` / `@sinclair/typebox` install from the extension's own
530
- // `node_modules`. `resolveTypeBoxSpecifier` mirrors the fall-through.
531
- const withTypeBox = TYPEBOX_SHIM_PATH
532
- ? withPi.replace(
533
- TYPEBOX_IMPORT_SPECIFIER_REGEX,
534
- (_match, prefix: string, _specifier: string, suffix: string) =>
535
- `${prefix}${toImportSpecifier(TYPEBOX_SHIM_PATH)}${suffix}`,
536
- )
537
- : withPi;
538
- const withPkg = await rewriteExtensionPackageImports(withTypeBox, importerPath, mtimeTag);
539
- const withBare = await rewriteExtensionBareImports(withPkg, importerPath, mtimeTag);
540
- const withNativeAddons = await rewriteExtensionNativeAddonRequires(withBare, importerPath);
541
- if (!mtimeTag) {
542
- return withNativeAddons;
543
- }
544
- return withNativeAddons.replace(
545
- RELATIVE_GRAPH_IMPORT_SPECIFIER_REGEX,
546
- (_match, prefix: string, specifier: string, suffix: string) => `${prefix}${specifier}?mtime=${mtimeTag}${suffix}`,
547
- );
631
+ const references = collectExtensionSpecifierReferences(source, importerPath);
632
+ const replacements: Array<ExtensionSpecifierReference & { replacement: string }> = [];
633
+ for (const reference of references) {
634
+ if (reference.kind !== "import") continue;
635
+
636
+ const specifier = reference.specifier;
637
+ let replacement: string | null = null;
638
+ const remappedSpecifier = remapLegacyPiSpecifier(specifier);
639
+ if (remappedSpecifier) {
640
+ try {
641
+ replacement = toImportSpecifier(resolveCanonicalPiSpecifier(remappedSpecifier));
642
+ } catch {
643
+ // Compiled fallback may be absent from a malformed build. Continue to
644
+ // the extension's on-disk peer dependency resolution below.
645
+ }
646
+ }
647
+ if (!replacement && TYPEBOX_SHIM_PATH && (specifier === "typebox" || specifier === "@sinclair/typebox")) {
648
+ replacement = toImportSpecifier(TYPEBOX_SHIM_PATH);
649
+ }
650
+ if (!replacement && specifier.startsWith("#")) {
651
+ const resolved = await resolvePackageImportSpecifier(specifier, importerPath);
652
+ if (resolved) replacement = toGraphImportSpecifier(resolved, resolvedImportMtimeTag);
653
+ }
654
+ if (!replacement && isBareExtensionDependencySpecifier(specifier)) {
655
+ const resolved = await resolveExtensionBareDependency(specifier, importerPath);
656
+ if (resolved) replacement = toGraphImportSpecifier(resolved, resolvedImportMtimeTag);
657
+ }
658
+ if (!replacement && mtimeTag && /^\.\.?\//.test(specifier) && !specifier.includes("?")) {
659
+ replacement = `${specifier}?mtime=${mtimeTag}`;
660
+ }
661
+ if (replacement && replacement !== specifier) {
662
+ replacements.push({ ...reference, replacement });
663
+ }
664
+ }
665
+ const withImports = applySpecifierReplacements(source, replacements);
666
+ return rewriteExtensionSpecifiers(withImports, importerPath);
548
667
  }
549
668
 
550
669
  /** Test seam for compiled-binary legacy extension source rewriting. */
@@ -552,15 +671,11 @@ export async function __rewriteLegacyExtensionSourceForTests(
552
671
  source: string,
553
672
  importerPath: string,
554
673
  mtimeTag: string | null = null,
674
+ resolvedImportMtimeTag: string | null = mtimeTag,
555
675
  ): Promise<string> {
556
- return rewriteLegacyExtensionSource(source, importerPath, mtimeTag);
676
+ return rewriteLegacyExtensionSource(source, importerPath, mtimeTag, resolvedImportMtimeTag);
557
677
  }
558
678
 
559
- // Match relative graph specifiers so their `./foo.ts` /`../foo` targets get a
560
- // `?mtime=<tag>` cache-bust suffix without disturbing already-rewritten
561
- // `file://` URLs or bare/host specifiers.
562
- const RELATIVE_GRAPH_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["'])(\.\.?\/[^"'?\s]*)(["'])/g;
563
-
564
679
  /**
565
680
  * Build the import specifier for a graph-resolved absolute path. POSIX
566
681
  * emits a bare filesystem path with an optional `?mtime=<tag>` (Bun keys
@@ -628,6 +743,33 @@ async function resolveSourceModuleFile(basePath: string): Promise<string | null>
628
743
  return null;
629
744
  }
630
745
 
746
+ function isPathInsideRoot(rootPath: string, candidatePath: string): boolean {
747
+ const relative = path.relative(rootPath, candidatePath);
748
+ return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
749
+ }
750
+
751
+ async function resolvePackageSourceTarget(packageRoot: string, targetPath: string): Promise<string | null> {
752
+ const candidate = path.resolve(targetPath);
753
+ if (!isPathInsideRoot(path.resolve(packageRoot), candidate)) {
754
+ return null;
755
+ }
756
+ const resolved = await resolveSourceModuleFile(candidate);
757
+ if (!resolved) {
758
+ return null;
759
+ }
760
+ const realPackageRoot = await realpathOrSelf(packageRoot);
761
+ return isPathInsideRoot(realPackageRoot, resolved) ? resolved : null;
762
+ }
763
+
764
+ async function resolvePackageFileTarget(packageRoot: string, targetPath: string): Promise<string | null> {
765
+ const candidate = path.resolve(targetPath);
766
+ if (!isPathInsideRoot(path.resolve(packageRoot), candidate) || !(await pathExists(candidate))) {
767
+ return null;
768
+ }
769
+ const [realPackageRoot, resolved] = await Promise.all([realpathOrSelf(packageRoot), realpathOrSelf(candidate)]);
770
+ return isPathInsideRoot(realPackageRoot, resolved) ? resolved : null;
771
+ }
772
+
631
773
  async function findPackageRoot(importerPath: string): Promise<string | null> {
632
774
  let dir = path.dirname(importerPath);
633
775
  while (true) {
@@ -672,7 +814,10 @@ async function readPackageImports(packageRoot: string): Promise<Record<string, u
672
814
  type PackageImportTargetSelection = string | typeof PACKAGE_IMPORT_EXCLUDED | null;
673
815
  type ResolvedPackageImportTargetSelection = string | typeof PACKAGE_IMPORT_EXCLUDED;
674
816
 
675
- function selectPackageImportTarget(entry: unknown): PackageImportTargetSelection {
817
+ function selectPackageImportTarget(
818
+ entry: unknown,
819
+ conditions: ReadonlySet<string> = SUPPORTED_PACKAGE_IMPORT_CONDITIONS,
820
+ ): PackageImportTargetSelection {
676
821
  if (entry === null) {
677
822
  return PACKAGE_IMPORT_EXCLUDED;
678
823
  }
@@ -681,7 +826,7 @@ function selectPackageImportTarget(entry: unknown): PackageImportTargetSelection
681
826
  }
682
827
  if (Array.isArray(entry)) {
683
828
  for (const item of entry) {
684
- const target = selectPackageImportTarget(item);
829
+ const target = selectPackageImportTarget(item, conditions);
685
830
  if (target !== null) return target;
686
831
  }
687
832
  return null;
@@ -690,10 +835,10 @@ function selectPackageImportTarget(entry: unknown): PackageImportTargetSelection
690
835
  return null;
691
836
  }
692
837
  for (const [condition, value] of Object.entries(entry)) {
693
- if (!SUPPORTED_PACKAGE_IMPORT_CONDITIONS.has(condition)) {
838
+ if (!conditions.has(condition)) {
694
839
  continue;
695
840
  }
696
- const target = selectPackageImportTarget(value);
841
+ const target = selectPackageImportTarget(value, conditions);
697
842
  if (target !== null) return target;
698
843
  }
699
844
  return null;
@@ -708,7 +853,7 @@ async function resolvePackageImportTarget(
708
853
  return null;
709
854
  }
710
855
  const substituted = wildcard === null ? target : target.replaceAll("*", wildcard);
711
- return resolveSourceModuleFile(path.resolve(packageRoot, substituted));
856
+ return resolvePackageSourceTarget(packageRoot, path.resolve(packageRoot, substituted));
712
857
  }
713
858
 
714
859
  async function resolvePackageImportSpecifier(specifier: string, importerPath: string): Promise<string | null> {
@@ -765,38 +910,6 @@ async function resolvePackageImportSpecifier(specifier: string, importerPath: st
765
910
  return resolvePackageImportTarget(packageRoot, bestMatch.target, bestMatch.wildcard);
766
911
  }
767
912
 
768
- const PACKAGE_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["'])(#[^"'()\s]+)(["'])/g;
769
-
770
- async function rewriteExtensionPackageImports(
771
- source: string,
772
- importerPath: string,
773
- mtimeTag: string | null = null,
774
- ): Promise<string> {
775
- let rewritten = "";
776
- let lastIndex = 0;
777
- for (const match of source.matchAll(PACKAGE_IMPORT_SPECIFIER_REGEX)) {
778
- const matchIndex = match.index;
779
- if (matchIndex === undefined) continue;
780
-
781
- const [fullMatch, prefix, specifier, suffix] = match;
782
- if (!prefix || !specifier || !suffix) continue;
783
-
784
- const resolved = await resolvePackageImportSpecifier(specifier, importerPath);
785
- if (!resolved) continue;
786
-
787
- rewritten += source.slice(lastIndex, matchIndex);
788
- rewritten += `${prefix}${toGraphImportSpecifier(resolved, mtimeTag)}${suffix}`;
789
- lastIndex = matchIndex + fullMatch.length;
790
- }
791
-
792
- if (lastIndex === 0) {
793
- return source;
794
- }
795
- return `${rewritten}${source.slice(lastIndex)}`;
796
- }
797
-
798
- const BARE_EXTENSION_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["'])([^"'()\s]+)(["'])/g;
799
-
800
913
  function isBareExtensionDependencySpecifier(specifier: string): boolean {
801
914
  if (
802
915
  specifier.startsWith(".") ||
@@ -872,6 +985,58 @@ async function readPackageManifestUncached(packageRoot: string): Promise<Record<
872
985
  }
873
986
  }
874
987
 
988
+ type ExtensionModuleKind = "commonjs" | "esm";
989
+ class ExtensionModuleKindConflictError extends Error {}
990
+
991
+ async function isCommonJsModulePath(
992
+ modulePath: string,
993
+ sourceType?: "script" | "module",
994
+ inheritedKind?: ExtensionModuleKind,
995
+ ): Promise<boolean> {
996
+ const extension = path.extname(modulePath).toLowerCase();
997
+ if (extension === ".cjs" || extension === ".cts") {
998
+ return true;
999
+ }
1000
+ if (extension !== ".js" && extension !== ".jsx") {
1001
+ return false;
1002
+ }
1003
+
1004
+ const packageRoot = await findPackageRoot(modulePath);
1005
+ const manifest = packageRoot ? await readPackageManifest(packageRoot) : null;
1006
+ if (manifest?.type === "module") {
1007
+ return false;
1008
+ }
1009
+ if (manifest?.type === "commonjs") {
1010
+ return true;
1011
+ }
1012
+ const parsedSourceType =
1013
+ sourceType ?? parseExtensionSource(await Bun.file(modulePath).text(), modulePath).program.sourceType;
1014
+ if (parsedSourceType === "module") {
1015
+ return false;
1016
+ }
1017
+ if (inheritedKind) {
1018
+ return inheritedKind === "commonjs";
1019
+ }
1020
+ const declaredModuleEntry =
1021
+ packageRoot && typeof manifest?.module === "string"
1022
+ ? await resolvePackageSourceTarget(packageRoot, path.resolve(packageRoot, manifest.module))
1023
+ : null;
1024
+ return !declaredModuleEntry || path.resolve(modulePath) !== path.resolve(declaredModuleEntry);
1025
+ }
1026
+
1027
+ async function isGraphOwnedCommonJsModule(
1028
+ modulePath: string,
1029
+ entryRealPath: string,
1030
+ sourceType?: "script" | "module",
1031
+ inheritedKind?: ExtensionModuleKind,
1032
+ ): Promise<boolean> {
1033
+ const extension = path.extname(modulePath).toLowerCase();
1034
+ if (modulePath === entryRealPath && extension !== ".cjs" && extension !== ".cts") {
1035
+ return false;
1036
+ }
1037
+ return isCommonJsModulePath(modulePath, sourceType, inheritedKind);
1038
+ }
1039
+
875
1040
  async function resolvePackageExportTarget(
876
1041
  packageRoot: string,
877
1042
  target: string,
@@ -881,16 +1046,17 @@ async function resolvePackageExportTarget(
881
1046
  return null;
882
1047
  }
883
1048
  const substituted = wildcard === null ? target : target.replaceAll("*", wildcard);
884
- return resolveSourceModuleFile(path.resolve(packageRoot, substituted));
1049
+ return resolvePackageSourceTarget(packageRoot, path.resolve(packageRoot, substituted));
885
1050
  }
886
1051
 
887
1052
  async function resolveNodePackageExport(
888
1053
  packageRoot: string,
889
1054
  subpath: string | null,
890
1055
  manifest: Record<string, unknown>,
1056
+ conditions: ReadonlySet<string> = SUPPORTED_PACKAGE_IMPORT_CONDITIONS,
891
1057
  ): Promise<string | null> {
892
1058
  const exportsField = manifest.exports;
893
- const rootTarget = subpath === null ? selectPackageImportTarget(exportsField) : null;
1059
+ const rootTarget = subpath === null ? selectPackageImportTarget(exportsField, conditions) : null;
894
1060
  if (rootTarget !== null && rootTarget !== PACKAGE_IMPORT_EXCLUDED) {
895
1061
  return resolvePackageExportTarget(packageRoot, rootTarget, null);
896
1062
  }
@@ -899,30 +1065,41 @@ async function resolveNodePackageExport(
899
1065
  }
900
1066
 
901
1067
  const exactKey = subpath === null ? "." : `./${subpath}`;
902
- const exactTarget = selectPackageImportTarget(exportsField[exactKey]);
903
- if (exactTarget !== null && exactTarget !== PACKAGE_IMPORT_EXCLUDED) {
904
- return resolvePackageExportTarget(packageRoot, exactTarget, null);
905
- }
906
-
1068
+ if (Object.hasOwn(exportsField, exactKey)) {
1069
+ const exactTarget = selectPackageImportTarget(exportsField[exactKey], conditions);
1070
+ return exactTarget !== null && exactTarget !== PACKAGE_IMPORT_EXCLUDED
1071
+ ? resolvePackageExportTarget(packageRoot, exactTarget, null)
1072
+ : null;
1073
+ }
1074
+
1075
+ let bestMatch: {
1076
+ keyLength: number;
1077
+ prefixLength: number;
1078
+ target: PackageImportTargetSelection;
1079
+ wildcard: string;
1080
+ } | null = null;
907
1081
  for (const [key, entry] of Object.entries(exportsField)) {
908
1082
  const starIndex = key.indexOf("*");
909
- if (starIndex === -1 || subpath === null) continue;
1083
+ if (starIndex === -1 || subpath === null || !key.startsWith("./")) continue;
910
1084
  const prefix = key.slice(2, starIndex);
911
1085
  const suffix = key.slice(starIndex + 1);
912
- if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix)) {
913
- continue;
914
- }
915
- const target = selectPackageImportTarget(entry);
916
- if (target === null || target === PACKAGE_IMPORT_EXCLUDED) {
917
- continue;
1086
+ if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix)) continue;
1087
+ if (
1088
+ !bestMatch ||
1089
+ prefix.length > bestMatch.prefixLength ||
1090
+ (prefix.length === bestMatch.prefixLength && key.length > bestMatch.keyLength)
1091
+ ) {
1092
+ bestMatch = {
1093
+ keyLength: key.length,
1094
+ prefixLength: prefix.length,
1095
+ target: selectPackageImportTarget(entry, conditions),
1096
+ wildcard: subpath.slice(prefix.length, subpath.length - suffix.length),
1097
+ };
918
1098
  }
919
- return resolvePackageExportTarget(
920
- packageRoot,
921
- target,
922
- subpath.slice(prefix.length, subpath.length - suffix.length),
923
- );
924
1099
  }
925
- return null;
1100
+ return bestMatch?.target && bestMatch.target !== PACKAGE_IMPORT_EXCLUDED
1101
+ ? resolvePackageExportTarget(packageRoot, bestMatch.target, bestMatch.wildcard)
1102
+ : null;
926
1103
  }
927
1104
 
928
1105
  async function resolveNodePackageFallback(
@@ -931,16 +1108,16 @@ async function resolveNodePackageFallback(
931
1108
  manifest: Record<string, unknown>,
932
1109
  ): Promise<string | null> {
933
1110
  if (subpath !== null) {
934
- return resolveSourceModuleFile(path.join(packageRoot, subpath));
1111
+ return resolvePackageSourceTarget(packageRoot, path.join(packageRoot, subpath));
935
1112
  }
936
1113
  for (const field of ["module", "main"]) {
937
1114
  const target = manifest[field];
938
1115
  if (typeof target === "string") {
939
- const resolved = await resolveSourceModuleFile(path.resolve(packageRoot, target));
1116
+ const resolved = await resolvePackageSourceTarget(packageRoot, path.resolve(packageRoot, target));
940
1117
  if (resolved) return resolved;
941
1118
  }
942
1119
  }
943
- return resolveSourceModuleFile(path.join(packageRoot, "index"));
1120
+ return resolvePackageSourceTarget(packageRoot, path.join(packageRoot, "index"));
944
1121
  }
945
1122
 
946
1123
  async function resolveNodePackageDependency(specifier: string, importerPath: string): Promise<string | null> {
@@ -950,9 +1127,72 @@ async function resolveNodePackageDependency(specifier: string, importerPath: str
950
1127
  if (!packageRoot) return null;
951
1128
  const manifest = await readPackageManifest(packageRoot);
952
1129
  if (!manifest) return null;
953
- return (
954
- (await resolveNodePackageExport(packageRoot, parsed.subpath, manifest)) ??
955
- (await resolveNodePackageFallback(packageRoot, parsed.subpath, manifest))
1130
+ return Object.hasOwn(manifest, "exports")
1131
+ ? resolveNodePackageExport(packageRoot, parsed.subpath, manifest)
1132
+ : resolveNodePackageFallback(packageRoot, parsed.subpath, manifest);
1133
+ }
1134
+
1135
+ async function resolveNodePackageRequire(specifier: string, importerPath: string): Promise<string | null> {
1136
+ const parsed = splitBarePackageSpecifier(specifier);
1137
+ if (!parsed) return null;
1138
+ const packageRoot = await findNodePackageRoot(parsed.name, importerPath);
1139
+ if (!packageRoot) return null;
1140
+ const manifest = await readPackageManifest(packageRoot);
1141
+ if (!manifest) return null;
1142
+
1143
+ if (Object.hasOwn(manifest, "exports")) {
1144
+ return resolveNodePackageExport(packageRoot, parsed.subpath, manifest, SUPPORTED_PACKAGE_REQUIRE_CONDITIONS);
1145
+ }
1146
+ if (parsed.subpath !== null) {
1147
+ return resolvePackageSourceTarget(packageRoot, path.join(packageRoot, parsed.subpath));
1148
+ }
1149
+ const main = manifest.main;
1150
+ return typeof main === "string"
1151
+ ? await resolvePackageSourceTarget(packageRoot, path.resolve(packageRoot, main))
1152
+ : await resolvePackageSourceTarget(packageRoot, path.join(packageRoot, "index"));
1153
+ }
1154
+
1155
+ async function validateResolvedBarePackagePath(
1156
+ specifier: string,
1157
+ importerPath: string,
1158
+ resolvedPath: string,
1159
+ ): Promise<string | null> {
1160
+ const parsed = splitBarePackageSpecifier(specifier);
1161
+ const packageRoot = parsed ? await findNodePackageRoot(parsed.name, importerPath) : null;
1162
+ return packageRoot ? resolvePackageFileTarget(packageRoot, resolvedPath) : null;
1163
+ }
1164
+
1165
+ async function isSelectedNoTypeEsmPackageBranch(
1166
+ specifier: string,
1167
+ importerPath: string,
1168
+ resolvedPath: string,
1169
+ ): Promise<boolean> {
1170
+ const parsed = splitBarePackageSpecifier(specifier);
1171
+ const packageRoot = parsed ? await findNodePackageRoot(parsed.name, importerPath) : null;
1172
+ const manifest = packageRoot ? await readPackageManifest(packageRoot) : null;
1173
+ if (!packageRoot || !manifest || manifest.type !== undefined) {
1174
+ return false;
1175
+ }
1176
+ if (parsed?.subpath === null && typeof manifest.module === "string") {
1177
+ const moduleEntry = await resolvePackageSourceTarget(packageRoot, path.resolve(packageRoot, manifest.module));
1178
+ if (moduleEntry && path.resolve(moduleEntry) === path.resolve(resolvedPath)) {
1179
+ return true;
1180
+ }
1181
+ }
1182
+ if (!Object.hasOwn(manifest, "exports")) {
1183
+ return false;
1184
+ }
1185
+ const importTarget = await resolveNodePackageExport(packageRoot, parsed?.subpath ?? null, manifest);
1186
+ const requireTarget = await resolveNodePackageExport(
1187
+ packageRoot,
1188
+ parsed?.subpath ?? null,
1189
+ manifest,
1190
+ SUPPORTED_PACKAGE_REQUIRE_CONDITIONS,
1191
+ );
1192
+ return Boolean(
1193
+ importTarget &&
1194
+ path.resolve(importTarget) === path.resolve(resolvedPath) &&
1195
+ (!requireTarget || path.resolve(requireTarget) !== path.resolve(importTarget)),
956
1196
  );
957
1197
  }
958
1198
 
@@ -971,24 +1211,26 @@ async function resolveExtensionBareDependency(specifier: string, importerPath: s
971
1211
  }
972
1212
 
973
1213
  async function resolveExtensionBareDependencyUncached(specifier: string, importerPath: string): Promise<string | null> {
1214
+ // Resolve against the runtime package manifest first. Besides working in a
1215
+ // compiled binary, this preserves the package's ESM `import` condition when
1216
+ // the absolute target is later loaded outside normal package resolution.
1217
+ const packageResolved = await resolveNodePackageDependency(specifier, importerPath);
1218
+ if (packageResolved) {
1219
+ return packageResolved;
1220
+ }
974
1221
  try {
975
1222
  const resolved = Bun.resolveSync(specifier, path.dirname(importerPath));
976
1223
  if (resolved && resolved !== specifier && !resolved.startsWith("node:") && !resolved.startsWith("bun:")) {
977
- return resolved;
1224
+ return validateResolvedBarePackagePath(specifier, importerPath, resolved);
978
1225
  }
979
1226
  } catch {
980
1227
  // Compiled binaries do not reliably resolve runtime extension node_modules.
981
1228
  }
982
- return resolveNodePackageDependency(specifier, importerPath);
1229
+ return null;
983
1230
  }
984
1231
 
985
1232
  const NATIVE_ADDON_EXTENSION = ".node";
986
1233
 
987
- // Match CommonJS require calls so bare native-addon specifiers can be pinned
988
- // to absolute paths. Only requires whose resolution lands on a `.node` addon
989
- // are rewritten; everything else stays on Bun's native resolver.
990
- const NATIVE_ADDON_REQUIRE_SPECIFIER_REGEX = /(\brequire\s*\(\s*["'])([^"'()\s]+)(["']\s*\))/g;
991
-
992
1234
  /**
993
1235
  * Resolve a bare specifier whose target is a native `.node` addon — either a
994
1236
  * package subpath ending in `.node`, or a package whose `main` points at an
@@ -1025,48 +1267,119 @@ async function resolveExtensionNativeAddonUncached(specifier: string, importerPa
1025
1267
  target =
1026
1268
  typeof main === "string" && main.endsWith(NATIVE_ADDON_EXTENSION) ? path.resolve(packageRoot, main) : null;
1027
1269
  }
1028
- if (!target || !(await pathExists(target))) {
1270
+ if (!target) {
1029
1271
  return null;
1030
1272
  }
1031
- return realpathOrSelf(target);
1273
+ return resolvePackageFileTarget(packageRoot, target);
1032
1274
  }
1033
1275
 
1034
- /**
1035
- * Rewrite bare `require()` specifiers that resolve to native `.node` addons
1036
- * into absolute-path requires. In `bun build --compile` binaries, Bun's bare
1037
- * resolution fails for packages whose `main` is a `.node` addon ("Cannot find
1038
- * module '@scope/pkg-<platform>'") even when the package sits in the
1039
- * extension's own node_modules; requiring the addon by absolute path works.
1040
- */
1041
- async function rewriteExtensionNativeAddonRequires(source: string, importerPath: string): Promise<string> {
1042
- let rewritten = "";
1043
- let lastIndex = 0;
1044
- for (const match of source.matchAll(NATIVE_ADDON_REQUIRE_SPECIFIER_REGEX)) {
1045
- const matchIndex = match.index;
1046
- if (matchIndex === undefined) continue;
1276
+ async function resolveExtensionBareRequire(specifier: string, importerPath: string): Promise<string | null> {
1277
+ if (!isBareExtensionDependencySpecifier(specifier)) {
1278
+ return null;
1279
+ }
1047
1280
 
1048
- const [fullMatch, prefix, specifier, suffix] = match;
1049
- if (!prefix || !specifier || !suffix) continue;
1281
+ const cacheKey = `${specifier}\0${path.resolve(path.dirname(importerPath))}`;
1282
+ const cached = bareRequireResolutionCache.get(cacheKey);
1283
+ if (cached) return cached;
1050
1284
 
1051
- const resolved = await resolveExtensionNativeAddon(specifier, importerPath);
1052
- if (!resolved) continue;
1285
+ const resolution = (async () => {
1286
+ const nativeAddon = await resolveExtensionNativeAddon(specifier, importerPath);
1287
+ if (nativeAddon) {
1288
+ return nativeAddon;
1289
+ }
1290
+ const packageResolved = await resolveNodePackageRequire(specifier, importerPath);
1291
+ if (packageResolved) {
1292
+ return realpathOrSelf(packageResolved);
1293
+ }
1294
+ try {
1295
+ const resolved = createRequire(importerPath).resolve(specifier);
1296
+ return resolved === specifier || resolved.startsWith("node:") || resolved.startsWith("bun:")
1297
+ ? null
1298
+ : await validateResolvedBarePackagePath(specifier, importerPath, resolved);
1299
+ } catch {
1300
+ return null;
1301
+ }
1302
+ })();
1303
+ bareRequireResolutionCache.set(cacheKey, resolution);
1304
+ return resolution;
1305
+ }
1306
+
1307
+ async function resolveExtensionCommonJsRequire(specifier: string, importerPath: string): Promise<string | null> {
1308
+ const remappedSpecifier = remapLegacyPiSpecifier(specifier);
1309
+ if (remappedSpecifier) {
1310
+ try {
1311
+ const resolved = resolveCanonicalPiSpecifier(remappedSpecifier);
1312
+ if (isBundledVirtualSpecifier(resolved)) {
1313
+ const moduleKey = resolved.slice(BUNDLED_VIRTUAL_SCHEME.length);
1314
+ if (!(moduleKey in loadedBundledModules)) {
1315
+ await loadBundledModule(moduleKey);
1316
+ }
1317
+ }
1318
+ return resolved;
1319
+ } catch {
1320
+ // A malformed compiled registry can still fall through to an
1321
+ // extension-installed legacy peer dependency.
1322
+ }
1323
+ }
1324
+ return resolveExtensionBareRequire(specifier, importerPath);
1325
+ }
1053
1326
 
1054
- rewritten += source.slice(lastIndex, matchIndex);
1055
- // Forward slashes keep Windows paths valid inside single- or double-quoted literals.
1056
- rewritten += `${prefix}${stripWindowsExtendedLengthPathPrefix(resolved).replaceAll("\\", "/")}${suffix}`;
1057
- lastIndex = matchIndex + fullMatch.length;
1327
+ /**
1328
+ * Rewrite CommonJS graph specifiers that cannot resolve from the bridge's
1329
+ * generated function: bare `require()` calls and, for graph-owned CommonJS
1330
+ * sources, import specifiers. Resolved targets are retained for synchronous
1331
+ * lazy hydration after load-time source caches clear.
1332
+ */
1333
+ async function rewriteExtensionSpecifiers(
1334
+ source: string,
1335
+ importerPath: string,
1336
+ rewriteImports = false,
1337
+ ): Promise<string> {
1338
+ const references = collectExtensionSpecifierReferences(source, importerPath);
1339
+ const resolvedSpecifierTargets = new Map<string, string>();
1340
+ const replacements: Array<ExtensionSpecifierReference & { replacement: string }> = [];
1341
+ for (const reference of references) {
1342
+ let resolved: string | null = null;
1343
+ if (reference.kind === "require") {
1344
+ resolved = await resolveExtensionCommonJsRequire(reference.specifier, importerPath);
1345
+ } else if (rewriteImports) {
1346
+ if (reference.specifier.startsWith(".")) {
1347
+ const candidate = Bun.resolveSync(reference.specifier, path.dirname(importerPath));
1348
+ resolved = hasSourceModuleExtension(candidate) ? await realpathOrSelf(candidate) : null;
1349
+ } else if (reference.specifier.startsWith("#")) {
1350
+ resolved = await resolvePackageImportSpecifier(reference.specifier, importerPath);
1351
+ } else {
1352
+ resolved = await resolveExtensionBareDependency(reference.specifier, importerPath);
1353
+ }
1354
+ }
1355
+ if (!resolved) continue;
1356
+ const replacement = stripWindowsExtendedLengthPathPrefix(resolved).replaceAll("\\", "/");
1357
+ resolvedSpecifierTargets.set(`${reference.kind}\0${reference.specifier}`, replacement);
1358
+ replacements.push({ ...reference, replacement });
1058
1359
  }
1360
+ extensionSynchronousSpecifierTargets.set(importerPath, resolvedSpecifierTargets);
1361
+ return applySpecifierReplacements(source, replacements);
1362
+ }
1059
1363
 
1060
- if (lastIndex === 0) {
1364
+ function rewriteExtensionSpecifiersFromCache(source: string, importerPath: string): string {
1365
+ const resolvedSpecifierTargets = extensionSynchronousSpecifierTargets.get(importerPath);
1366
+ if (!resolvedSpecifierTargets || resolvedSpecifierTargets.size === 0) {
1061
1367
  return source;
1062
1368
  }
1063
- return `${rewritten}${source.slice(lastIndex)}`;
1369
+ const replacements: Array<ExtensionSpecifierReference & { replacement: string }> = [];
1370
+ for (const reference of collectExtensionSpecifierReferences(source, importerPath)) {
1371
+ const replacement = resolvedSpecifierTargets.get(`${reference.kind}\0${reference.specifier}`);
1372
+ if (replacement) {
1373
+ replacements.push({ ...reference, replacement });
1374
+ }
1375
+ }
1376
+ return applySpecifierReplacements(source, replacements);
1064
1377
  }
1065
1378
 
1066
1379
  /**
1067
1380
  * Whether a module's source contains a bare require that resolves to a native
1068
1381
  * `.node` addon — i.e. a napi-rs style loader that must be hooked into the
1069
- * extension graph so {@link rewriteExtensionNativeAddonRequires} can pin its
1382
+ * extension graph so {@link rewriteExtensionSpecifiers} can pin its
1070
1383
  * platform-package requires to absolute paths.
1071
1384
  */
1072
1385
  async function moduleRequiresNativeAddon(modulePath: string): Promise<boolean> {
@@ -1085,63 +1398,33 @@ async function moduleRequiresNativeAddonUncached(modulePath: string): Promise<bo
1085
1398
  } catch {
1086
1399
  return false;
1087
1400
  }
1088
- for (const match of source.matchAll(NATIVE_ADDON_REQUIRE_SPECIFIER_REGEX)) {
1089
- const specifier = match[2];
1090
- if (specifier && (await resolveExtensionNativeAddon(specifier, modulePath))) {
1401
+ for (const reference of collectExtensionSpecifierReferences(source, modulePath)) {
1402
+ if (reference.kind === "require" && (await resolveExtensionNativeAddon(reference.specifier, modulePath))) {
1091
1403
  return true;
1092
1404
  }
1093
1405
  }
1094
1406
  return false;
1095
1407
  }
1096
1408
 
1097
- async function rewriteExtensionBareImports(
1098
- source: string,
1099
- importerPath: string,
1100
- mtimeTag: string | null = null,
1101
- ): Promise<string> {
1102
- let rewritten = "";
1103
- let lastIndex = 0;
1104
- for (const match of source.matchAll(BARE_EXTENSION_IMPORT_SPECIFIER_REGEX)) {
1105
- const matchIndex = match.index;
1106
- if (matchIndex === undefined) continue;
1107
-
1108
- const [fullMatch, prefix, specifier, suffix] = match;
1109
- if (!prefix || !specifier || !suffix) continue;
1110
-
1111
- const resolved = await resolveExtensionBareDependency(specifier, importerPath);
1112
- if (!resolved) continue;
1113
-
1114
- rewritten += source.slice(lastIndex, matchIndex);
1115
- rewritten += `${prefix}${toGraphImportSpecifier(resolved, mtimeTag)}${suffix}`;
1116
- lastIndex = matchIndex + fullMatch.length;
1117
- }
1118
-
1119
- if (lastIndex === 0) {
1120
- return source;
1121
- }
1122
- return `${rewritten}${source.slice(lastIndex)}`;
1123
- }
1124
-
1125
1409
  function escapeRegExp(value: string): string {
1126
1410
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1127
1411
  }
1128
1412
 
1129
- // Match source modules in an extension graph: relative imports, package
1130
- // `imports` aliases such as `#src/*`, and extension-local bare dependency
1131
- // entries. Bare imports inside node_modules dependencies remain native Bun
1132
- // resolutions; once the dependency entry is hooked, its relative children are
1133
- // still collected and rewritten with the reload mtime tag. `require()` calls
1134
- // are scanned too so CJS entries and napi-rs loaders reached without an
1135
- // import statement still join the graph.
1136
- const EXTENSION_GRAPH_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["'])([^"'()\s]+)(["'])/g;
1413
+ // Source modules in an extension graph are discovered from parsed static,
1414
+ // dynamic, re-export, and direct CommonJS require specifiers. Parsing keeps
1415
+ // import-looking text in strings, templates, regex literals, and comments out
1416
+ // of dependency resolution.
1137
1417
 
1138
1418
  // Extension source realpaths already covered by an installed load-time hook for
1139
1419
  // each entry. `Bun.plugin()` registrations are process-global and permanent, so
1140
1420
  // reloads install supplemental hooks only for modules added to the graph since
1141
1421
  // the previous load.
1142
1422
  const extensionGraphHookModules = new Map<string, Set<string>>();
1423
+ const extensionGraphCacheBustResolvedImportModules = new Map<string, Set<string>>();
1143
1424
  const commonJsModuleSources = new Map<string, string>();
1144
1425
  const commonJsFallbackModulePaths = new Map<string, string>();
1426
+ const extensionSynchronousSpecifierTargets = new Map<string, Map<string, string>>();
1427
+ const commonJsGraphModulePaths = new Set<string>();
1145
1428
  const COMMONJS_REQUIRE_GLOBAL = "__ompLegacyPiRequireGraphModule";
1146
1429
  const commonJsModuleDefinitions = new Map<string, { source: string; filename: string; dirname: string }>();
1147
1430
  const commonJsModuleCache = new Map<
@@ -1162,7 +1445,13 @@ function evaluateGraphCommonJs(modulePath: string): unknown {
1162
1445
  if (cached) {
1163
1446
  return cached.exports;
1164
1447
  }
1165
- const definition = commonJsModuleDefinitions.get(modulePath);
1448
+ let definition = commonJsModuleDefinitions.get(modulePath);
1449
+ if (!definition && commonJsGraphModulePaths.has(modulePath)) {
1450
+ const targetPath = commonJsFallbackModulePaths.get(modulePath) ?? modulePath;
1451
+ const source = rewriteExtensionSpecifiersFromCache(fs.readFileSync(targetPath, "utf8"), modulePath);
1452
+ synthesizeCommonJsDefaultModule(modulePath, source, targetPath);
1453
+ definition = commonJsModuleDefinitions.get(modulePath);
1454
+ }
1166
1455
  if (!definition) {
1167
1456
  throw new Error(`Missing graph-owned CommonJS definition: ${modulePath}`);
1168
1457
  }
@@ -1179,6 +1468,14 @@ function evaluateGraphCommonJs(modulePath: string): unknown {
1179
1468
  commonJsModuleCache.set(modulePath, module);
1180
1469
  const graphRequire: NodeJS.Require = Object.assign(
1181
1470
  (specifier: string) => {
1471
+ if (isBundledVirtualSpecifier(specifier)) {
1472
+ const moduleKey = specifier.slice(BUNDLED_VIRTUAL_SCHEME.length);
1473
+ const bundledModule = loadedBundledModules[moduleKey];
1474
+ if (!bundledModule) {
1475
+ throw new Error(`Missing bundled CommonJS host module: ${moduleKey}`);
1476
+ }
1477
+ return bundledModule;
1478
+ }
1182
1479
  const resolved = nativeRequire.resolve(specifier);
1183
1480
  let graphPath = resolved;
1184
1481
  try {
@@ -1186,7 +1483,7 @@ function evaluateGraphCommonJs(modulePath: string): unknown {
1186
1483
  } catch {
1187
1484
  // Builtins and virtual modules have no filesystem realpath.
1188
1485
  }
1189
- return commonJsModuleDefinitions.has(graphPath) ? evaluateGraphCommonJs(graphPath) : nativeRequire(specifier);
1486
+ return commonJsGraphModulePaths.has(graphPath) ? evaluateGraphCommonJs(graphPath) : nativeRequire(specifier);
1190
1487
  },
1191
1488
  {
1192
1489
  resolve: nativeRequire.resolve,
@@ -1234,30 +1531,43 @@ async function realpathOrSelfUncached(p: string): Promise<string> {
1234
1531
  }
1235
1532
  }
1236
1533
 
1534
+ interface ExtensionModuleGraph {
1535
+ readonly modules: Map<string, string>;
1536
+ readonly cacheBustResolvedImportModules: Set<string>;
1537
+ readonly commonJsPaths: Set<string>;
1538
+ }
1539
+
1237
1540
  /**
1238
1541
  * Walk the extension's import graph starting at `entryRealPath`, returning the
1239
1542
  * realpath of every reachable source module OMP must rewrite at load time.
1240
- * Relative imports and package `imports` aliases are always graph-owned.
1241
- * Extension-local bare dependency entries are also included so their relative
1242
- * children receive the reload mtime tag; bare imports inside those dependencies
1243
- * remain native Bun resolutions to avoid taking over full third-party graphs.
1244
- * CommonJS modules reached through `require()` stay on Bun's native loader
1245
- * unless they resolve native addons. CommonJS reached through ESM imports stays
1246
- * graph-owned so the load hook can expose its exports through an ESM default.
1543
+ * Relative imports, package `imports` aliases, and ESM bare dependencies are
1544
+ * graph-owned recursively because compiled Bun cannot resolve runtime
1545
+ * `node_modules` from those modules. Graph-owned CommonJS modules also own
1546
+ * their relative and bare CommonJS descendants, which are evaluated by the
1547
+ * synchronous bridge. Resolved ESM imports inside third-party dependencies
1548
+ * omit the reload tag so their importer paths stay query-free.
1247
1549
  */
1248
- async function collectExtensionModules(entryRealPath: string): Promise<Map<string, string>> {
1550
+ async function collectExtensionModules(entryRealPath: string): Promise<ExtensionModuleGraph> {
1249
1551
  const modules = new Map<string, string>();
1250
- const queuedFollowBareDependencies = new Map<string, boolean>([[entryRealPath, true]]);
1251
- const queue: Array<{ file: string; followBareDependencies: boolean }> = [
1252
- { file: entryRealPath, followBareDependencies: true },
1253
- ];
1552
+ const commonJsPaths = new Set<string>();
1553
+ const queuedCacheBustResolvedImports = new Map<string, boolean>([[entryRealPath, true]]);
1554
+ const queuedModuleKinds = new Map<string, ExtensionModuleKind>([[entryRealPath, "esm"]]);
1555
+ const queuedEsmBranchPaths = new Set<string>();
1556
+ const queue: Array<{
1557
+ file: string;
1558
+ cacheBustResolvedImports: boolean;
1559
+ moduleKind?: ExtensionModuleKind;
1560
+ esmBranch?: boolean;
1561
+ }> = [{ file: entryRealPath, cacheBustResolvedImports: true, moduleKind: "esm" }];
1254
1562
  while (queue.length > 0) {
1255
1563
  const item = queue.pop();
1256
1564
  if (!item) {
1257
1565
  continue;
1258
1566
  }
1259
1567
  const file = item.file;
1260
- const followBareDependencies = queuedFollowBareDependencies.get(file) ?? item.followBareDependencies;
1568
+ const cacheBustResolvedImports = queuedCacheBustResolvedImports.get(file) ?? item.cacheBustResolvedImports;
1569
+ const inheritedModuleKind = queuedModuleKinds.get(file) ?? item.moduleKind;
1570
+ const esmBranch = queuedEsmBranchPaths.has(file) || item.esmBranch === true;
1261
1571
  if (modules.has(file)) {
1262
1572
  continue;
1263
1573
  }
@@ -1268,109 +1578,330 @@ async function collectExtensionModules(entryRealPath: string): Promise<Map<strin
1268
1578
  continue;
1269
1579
  }
1270
1580
  modules.set(file, source);
1271
- const dir = path.dirname(file);
1272
- const specifiers = new Set<string>();
1273
- const requiredSpecifiers = new Set<string>();
1274
- for (const match of source.matchAll(EXTENSION_GRAPH_SPECIFIER_REGEX)) {
1275
- if (match[2]) specifiers.add(match[2]);
1276
- }
1277
- for (const match of source.matchAll(NATIVE_ADDON_REQUIRE_SPECIFIER_REGEX)) {
1278
- if (match[2]) {
1279
- specifiers.add(match[2]);
1280
- requiredSpecifiers.add(match[2]);
1281
- }
1581
+ const ast = parseExtensionSource(source, file);
1582
+ const sourceIsCommonJs = await isGraphOwnedCommonJsModule(
1583
+ file,
1584
+ entryRealPath,
1585
+ ast.program.sourceType,
1586
+ inheritedModuleKind,
1587
+ );
1588
+ if (sourceIsCommonJs) {
1589
+ commonJsPaths.add(file);
1282
1590
  }
1283
- for (const specifier of specifiers) {
1591
+ const dir = path.dirname(file);
1592
+ const references = collectExtensionSpecifierReferences(source, file, ast);
1593
+ for (const reference of references) {
1594
+ const specifier = reference.specifier;
1284
1595
  try {
1285
1596
  let resolved: string | null = null;
1286
- let nextFollowsBareDependencies = followBareDependencies;
1287
- const isRequired = requiredSpecifiers.has(specifier);
1597
+ let nextCacheBustResolvedImports = cacheBustResolvedImports;
1598
+ let resolvedModuleKind: ExtensionModuleKind | undefined;
1599
+ let resolvedEsmBranch = false;
1600
+ let requiresNativeAddonRewrite = false;
1601
+ const isRequired = reference.kind === "require";
1288
1602
  if (specifier.startsWith(".")) {
1289
1603
  const candidate = Bun.resolveSync(specifier, dir);
1290
- if (
1291
- hasSourceModuleExtension(candidate) &&
1292
- (!isRequired || (await moduleRequiresNativeAddon(candidate)))
1293
- ) {
1294
- resolved = await realpathOrSelf(candidate);
1604
+ if (hasSourceModuleExtension(candidate)) {
1605
+ const inheritedTargetKind = isRequired
1606
+ ? sourceIsCommonJs
1607
+ ? "commonjs"
1608
+ : undefined
1609
+ : sourceIsCommonJs
1610
+ ? "commonjs"
1611
+ : esmBranch
1612
+ ? "esm"
1613
+ : undefined;
1614
+ const targetIsCommonJs = await isCommonJsModulePath(candidate, undefined, inheritedTargetKind);
1615
+ const isCommonJsDescendant = isRequired && sourceIsCommonJs && targetIsCommonJs;
1616
+ requiresNativeAddonRewrite =
1617
+ isRequired && !isCommonJsDescendant && (await moduleRequiresNativeAddon(candidate));
1618
+ if (!isRequired || isCommonJsDescendant || requiresNativeAddonRewrite) {
1619
+ resolved = await realpathOrSelf(candidate);
1620
+ resolvedModuleKind = targetIsCommonJs ? "commonjs" : "esm";
1621
+ resolvedEsmBranch = !targetIsCommonJs && esmBranch;
1622
+ }
1295
1623
  }
1296
1624
  } else if (specifier.startsWith("#")) {
1297
1625
  const candidate = await resolvePackageImportSpecifier(specifier, file);
1298
- if (candidate && (!isRequired || (await moduleRequiresNativeAddon(candidate)))) {
1299
- resolved = candidate;
1626
+ if (candidate) {
1627
+ const inheritedTargetKind = isRequired
1628
+ ? sourceIsCommonJs
1629
+ ? "commonjs"
1630
+ : undefined
1631
+ : sourceIsCommonJs
1632
+ ? "commonjs"
1633
+ : esmBranch
1634
+ ? "esm"
1635
+ : undefined;
1636
+ const targetIsCommonJs = await isCommonJsModulePath(candidate, undefined, inheritedTargetKind);
1637
+ const isCommonJsDescendant = isRequired && sourceIsCommonJs && targetIsCommonJs;
1638
+ requiresNativeAddonRewrite =
1639
+ isRequired && !isCommonJsDescendant && (await moduleRequiresNativeAddon(candidate));
1640
+ if (!isRequired || isCommonJsDescendant || requiresNativeAddonRewrite) {
1641
+ resolved = candidate;
1642
+ resolvedModuleKind = targetIsCommonJs ? "commonjs" : "esm";
1643
+ resolvedEsmBranch = !targetIsCommonJs && esmBranch;
1644
+ }
1300
1645
  }
1301
1646
  } else if (
1302
- followBareDependencies &&
1303
1647
  isBareExtensionDependencySpecifier(specifier) &&
1304
1648
  !remapLegacyPiSpecifier(specifier) &&
1305
1649
  specifier !== "typebox" &&
1306
1650
  specifier !== "@sinclair/typebox"
1307
1651
  ) {
1308
- const parsed = splitBarePackageSpecifier(specifier);
1309
- const packageRoot = parsed ? await findNodePackageRoot(parsed.name, file) : null;
1310
- const manifest = packageRoot ? await readPackageManifest(packageRoot) : null;
1311
- const dependencyEntry = manifest ? await resolveExtensionBareDependency(specifier, file) : null;
1312
- const dependencyExtension = dependencyEntry ? path.extname(dependencyEntry) : null;
1313
- const isCommonJsEntry =
1314
- dependencyExtension === ".cjs" ||
1315
- dependencyExtension === ".cts" ||
1316
- ((dependencyExtension === ".js" || dependencyExtension === ".jsx") && manifest?.type !== "module");
1652
+ const dependencyEntry = isRequired
1653
+ ? await resolveExtensionBareRequire(specifier, file)
1654
+ : await resolveExtensionBareDependency(specifier, file);
1317
1655
  const isHookableEntry = Boolean(dependencyEntry && hasSourceModuleExtension(dependencyEntry));
1318
- const hookCommonJsEntry =
1319
- isHookableEntry && isCommonJsEntry && dependencyEntry
1320
- ? await moduleRequiresNativeAddon(dependencyEntry)
1656
+ const selectedEsmBranch =
1657
+ !isRequired &&
1658
+ isHookableEntry &&
1659
+ dependencyEntry !== null &&
1660
+ (await isSelectedNoTypeEsmPackageBranch(specifier, file, dependencyEntry));
1661
+ const inheritedTargetKind = isRequired
1662
+ ? sourceIsCommonJs
1663
+ ? "commonjs"
1664
+ : undefined
1665
+ : selectedEsmBranch
1666
+ ? "esm"
1667
+ : undefined;
1668
+ const isCommonJsEntry =
1669
+ isHookableEntry && dependencyEntry
1670
+ ? await isCommonJsModulePath(dependencyEntry, undefined, inheritedTargetKind)
1321
1671
  : false;
1322
- if (isHookableEntry && dependencyEntry && ((!isRequired && !isCommonJsEntry) || hookCommonJsEntry)) {
1672
+ if (isHookableEntry && dependencyEntry && (!isRequired || (sourceIsCommonJs && isCommonJsEntry))) {
1323
1673
  resolved = await realpathOrSelf(dependencyEntry);
1674
+ } else if (isHookableEntry && dependencyEntry && isRequired) {
1675
+ requiresNativeAddonRewrite = await moduleRequiresNativeAddon(dependencyEntry);
1676
+ if (requiresNativeAddonRewrite) {
1677
+ resolved = await realpathOrSelf(dependencyEntry);
1678
+ }
1324
1679
  }
1325
- if (resolved && hookCommonJsEntry) {
1326
- nativeAddonLoaderModulePaths.add(resolved);
1680
+ if (resolved) {
1681
+ resolvedModuleKind = isCommonJsEntry ? "commonjs" : "esm";
1682
+ resolvedEsmBranch = selectedEsmBranch && !isCommonJsEntry;
1327
1683
  }
1328
- nextFollowsBareDependencies = false;
1684
+ nextCacheBustResolvedImports = false;
1329
1685
  }
1330
- if (resolved && isRequired) {
1686
+ if (resolved && requiresNativeAddonRewrite) {
1331
1687
  nativeAddonLoaderModulePaths.add(resolved);
1332
1688
  }
1333
- if (resolved && !modules.has(resolved)) {
1334
- const queuedFollowsBareDependencies = queuedFollowBareDependencies.get(resolved) ?? false;
1335
- const mergedFollowsBareDependencies = queuedFollowsBareDependencies || nextFollowsBareDependencies;
1336
- queuedFollowBareDependencies.set(resolved, mergedFollowsBareDependencies);
1337
- queue.push({ file: resolved, followBareDependencies: mergedFollowsBareDependencies });
1689
+ if (resolved) {
1690
+ const queuedCacheBust = queuedCacheBustResolvedImports.get(resolved) ?? false;
1691
+ const mergedCacheBust = queuedCacheBust || nextCacheBustResolvedImports;
1692
+ queuedCacheBustResolvedImports.set(resolved, mergedCacheBust);
1693
+ const queuedModuleKind = queuedModuleKinds.get(resolved);
1694
+ if (queuedModuleKind && resolvedModuleKind && queuedModuleKind !== resolvedModuleKind) {
1695
+ throw new ExtensionModuleKindConflictError(
1696
+ `Conflicting extension module kinds for ${resolved}: ${queuedModuleKind} and ${resolvedModuleKind}`,
1697
+ );
1698
+ }
1699
+ const mergedModuleKind = queuedModuleKind ?? resolvedModuleKind;
1700
+ if (mergedModuleKind) {
1701
+ queuedModuleKinds.set(resolved, mergedModuleKind);
1702
+ }
1703
+ if (resolvedEsmBranch) {
1704
+ queuedEsmBranchPaths.add(resolved);
1705
+ }
1706
+ if (!modules.has(resolved)) {
1707
+ queue.push({
1708
+ file: resolved,
1709
+ cacheBustResolvedImports: mergedCacheBust,
1710
+ moduleKind: mergedModuleKind,
1711
+ esmBranch: resolvedEsmBranch,
1712
+ });
1713
+ }
1714
+ }
1715
+ } catch (error) {
1716
+ if (error instanceof ExtensionModuleKindConflictError) {
1717
+ throw error;
1338
1718
  }
1339
- } catch {
1340
1719
  // Unresolvable import (e.g. a type-only path); skip it.
1341
1720
  }
1342
1721
  }
1343
1722
  }
1344
- for (const modulePath of nativeAddonLoaderModulePaths) {
1345
- const source = modules.get(modulePath);
1346
- if (source !== undefined) {
1347
- modules.set(modulePath, await rewriteExtensionNativeAddonRequires(source, modulePath));
1723
+ for (const [modulePath, source] of modules) {
1724
+ if (commonJsPaths.has(modulePath) || nativeAddonLoaderModulePaths.has(modulePath)) {
1725
+ modules.set(modulePath, await rewriteExtensionSpecifiers(source, modulePath, commonJsPaths.has(modulePath)));
1348
1726
  }
1349
1727
  }
1350
- return modules;
1728
+ return {
1729
+ modules,
1730
+ commonJsPaths,
1731
+ cacheBustResolvedImportModules: new Set(
1732
+ [...queuedCacheBustResolvedImports]
1733
+ .filter(([modulePath, enabled]) => enabled && modules.has(modulePath))
1734
+ .map(([modulePath]) => modulePath),
1735
+ ),
1736
+ };
1351
1737
  }
1352
1738
 
1353
1739
  /**
1354
1740
  * Discovers CommonJS export names Bun normally exposes to ESM importers. The
1355
1741
  * bridge must declare them statically because its default export is synthetic.
1356
1742
  */
1357
- function collectCommonJsNamedExports(source: string): string[] {
1358
- const names = new Set<string>();
1359
- const assignmentPattern = /(?:^|[;\n])\s*(?:exports|module\.exports)\.([A-Za-z_$][\w$]*)\s*=/gm;
1360
- for (const match of source.matchAll(assignmentPattern)) {
1361
- const name = match[1];
1362
- if (name && name !== "default") {
1363
- names.add(name);
1364
- }
1743
+ const COMMONJS_NAMED_EXPORT_IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
1744
+
1745
+ function collectCommonJsNamedExports(source: string, modulePath: string, visited = new Set<string>()): string[] {
1746
+ let realModulePath = modulePath;
1747
+ try {
1748
+ realModulePath = fs.realpathSync(modulePath);
1749
+ } catch {
1750
+ // The caller's path remains the stable cycle key when realpath fails.
1751
+ }
1752
+ if (visited.has(realModulePath)) {
1753
+ return [];
1365
1754
  }
1366
- const objectPattern = /module\.exports\s*=\s*\{([\s\S]*?)\}/g;
1367
- for (const objectMatch of source.matchAll(objectPattern)) {
1368
- const propertyPattern = /(?:^|,)\s*(?:([A-Za-z_$][\w$]*)\s*(?=[:,]|$)|["']([A-Za-z_$][\w$]*)["']\s*:)/g;
1369
- for (const propertyMatch of objectMatch[1]?.matchAll(propertyPattern) ?? []) {
1370
- const name = propertyMatch[1] ?? propertyMatch[2];
1371
- if (name && name !== "default") {
1755
+ visited.add(realModulePath);
1756
+
1757
+ const names = new Set<string>();
1758
+
1759
+ const reexportSpecifiers = new Set<string>();
1760
+ const ast = parseExtensionSource(source, modulePath);
1761
+ traverseAst(ast, {
1762
+ enter(nodePath) {
1763
+ const node = nodePath.node;
1764
+ if (node.type === "CallExpression") {
1765
+ const definePropertyCall =
1766
+ node.callee.type === "MemberExpression" &&
1767
+ !node.callee.computed &&
1768
+ node.callee.object.type === "Identifier" &&
1769
+ node.callee.object.name === "Object" &&
1770
+ node.callee.property.type === "Identifier" &&
1771
+ node.callee.property.name === "defineProperty" &&
1772
+ !nodePath.scope.hasBinding("Object", true);
1773
+ if (definePropertyCall) {
1774
+ const target = node.arguments[0];
1775
+ const property = node.arguments[1];
1776
+ const targetsExports =
1777
+ (target?.type === "Identifier" &&
1778
+ target.name === "exports" &&
1779
+ !nodePath.scope.hasBinding("exports", true)) ||
1780
+ (target?.type === "MemberExpression" &&
1781
+ !target.computed &&
1782
+ target.object.type === "Identifier" &&
1783
+ target.object.name === "module" &&
1784
+ target.property.type === "Identifier" &&
1785
+ target.property.name === "exports" &&
1786
+ !nodePath.scope.hasBinding("module", true));
1787
+ if (
1788
+ targetsExports &&
1789
+ property?.type === "StringLiteral" &&
1790
+ property.value !== "default" &&
1791
+ COMMONJS_NAMED_EXPORT_IDENTIFIER.test(property.value)
1792
+ ) {
1793
+ names.add(property.value);
1794
+ }
1795
+ return;
1796
+ }
1797
+ if (node.callee.type === "Identifier" && node.callee.name === "__exportStar") {
1798
+ const source = node.arguments[0];
1799
+ const target = node.arguments[1];
1800
+ const targetsExports =
1801
+ (target?.type === "Identifier" &&
1802
+ target.name === "exports" &&
1803
+ !nodePath.scope.hasBinding("exports", true)) ||
1804
+ (target?.type === "MemberExpression" &&
1805
+ !target.computed &&
1806
+ target.object.type === "Identifier" &&
1807
+ target.object.name === "module" &&
1808
+ target.property.type === "Identifier" &&
1809
+ target.property.name === "exports" &&
1810
+ !nodePath.scope.hasBinding("module", true));
1811
+ if (
1812
+ targetsExports &&
1813
+ source?.type === "CallExpression" &&
1814
+ source.callee.type === "Identifier" &&
1815
+ source.callee.name === "require" &&
1816
+ !nodePath.scope.hasBinding("require", true)
1817
+ ) {
1818
+ const argument = source.arguments[0];
1819
+ if (argument?.type === "StringLiteral") {
1820
+ reexportSpecifiers.add(argument.value);
1821
+ }
1822
+ }
1823
+ return;
1824
+ }
1825
+ }
1826
+ if (node.type !== "AssignmentExpression" || node.operator !== "=" || node.left.type !== "MemberExpression") {
1827
+ return;
1828
+ }
1829
+ const left = node.left;
1830
+ const propertyName =
1831
+ !left.computed && left.property.type === "Identifier"
1832
+ ? left.property.name
1833
+ : left.computed && left.property.type === "StringLiteral"
1834
+ ? left.property.value
1835
+ : null;
1836
+ const object = left.object;
1837
+ const assignsExportsProperty =
1838
+ propertyName !== null &&
1839
+ ((object.type === "Identifier" &&
1840
+ object.name === "exports" &&
1841
+ !nodePath.scope.hasBinding("exports", true)) ||
1842
+ (object.type === "MemberExpression" &&
1843
+ !object.computed &&
1844
+ object.object.type === "Identifier" &&
1845
+ object.object.name === "module" &&
1846
+ object.property.type === "Identifier" &&
1847
+ object.property.name === "exports" &&
1848
+ !nodePath.scope.hasBinding("module", true)));
1849
+ if (assignsExportsProperty) {
1850
+ if (propertyName !== "default" && COMMONJS_NAMED_EXPORT_IDENTIFIER.test(propertyName)) {
1851
+ names.add(propertyName);
1852
+ }
1853
+ return;
1854
+ }
1855
+ const assignsModuleExports =
1856
+ !left.computed &&
1857
+ left.object.type === "Identifier" &&
1858
+ left.object.name === "module" &&
1859
+ left.property.type === "Identifier" &&
1860
+ left.property.name === "exports" &&
1861
+ !nodePath.scope.hasBinding("module", true);
1862
+ if (!assignsModuleExports) return;
1863
+
1864
+ const right = node.right;
1865
+ if (right.type === "ObjectExpression") {
1866
+ for (const property of right.properties) {
1867
+ if ((property.type !== "ObjectProperty" && property.type !== "ObjectMethod") || property.computed) {
1868
+ continue;
1869
+ }
1870
+ const name =
1871
+ property.key.type === "Identifier"
1872
+ ? property.key.name
1873
+ : property.key.type === "StringLiteral"
1874
+ ? property.key.value
1875
+ : null;
1876
+ if (name && name !== "default" && COMMONJS_NAMED_EXPORT_IDENTIFIER.test(name)) {
1877
+ names.add(name);
1878
+ }
1879
+ }
1880
+ return;
1881
+ }
1882
+ if (
1883
+ right.type === "CallExpression" &&
1884
+ right.callee.type === "Identifier" &&
1885
+ right.callee.name === "require" &&
1886
+ !nodePath.scope.hasBinding("require", true)
1887
+ ) {
1888
+ const argument = right.arguments[0];
1889
+ if (argument?.type === "StringLiteral") {
1890
+ reexportSpecifiers.add(argument.value);
1891
+ }
1892
+ }
1893
+ },
1894
+ });
1895
+ const nativeRequire = createRequire(modulePath);
1896
+ for (const specifier of reexportSpecifiers) {
1897
+ try {
1898
+ const resolved = fs.realpathSync(nativeRequire.resolve(specifier));
1899
+ const reexportedSource = rewriteExtensionSpecifiersFromCache(fs.readFileSync(resolved, "utf8"), resolved);
1900
+ for (const name of collectCommonJsNamedExports(reexportedSource, resolved, visited)) {
1372
1901
  names.add(name);
1373
1902
  }
1903
+ } catch {
1904
+ // Native modules and non-source re-exports do not expose analyzable names.
1374
1905
  }
1375
1906
  }
1376
1907
  return [...names];
@@ -1397,7 +1928,7 @@ function synthesizeCommonJsDefaultModule(modulePath: string, source: string, tar
1397
1928
  });
1398
1929
  commonJsModuleCache.delete(modulePath);
1399
1930
  const exportsBinding = "__ompLegacyPiCommonJsExports";
1400
- const namedExports = collectCommonJsNamedExports(executableSource)
1931
+ const namedExports = collectCommonJsNamedExports(executableSource, targetPath)
1401
1932
  .map(
1402
1933
  (name, index) =>
1403
1934
  `const __ompLegacyPiCommonJsExport${index} = ${exportsBinding}[${JSON.stringify(name)}]; export { __ompLegacyPiCommonJsExport${index} as ${name} };`,
@@ -1428,24 +1959,23 @@ async function prepareCommonJsDefaultModule(modulePath: string, source: string):
1428
1959
 
1429
1960
  /**
1430
1961
  * Install exact-path load hooks for the current extension graph. ESM/TS source
1431
- * retains the async rewrite path. CommonJS wrappers and native-addon loaders
1432
- * stay synchronous because Bun rejects `require()` targets backed by async
1433
- * `onLoad` callbacks.
1962
+ * retains the async rewrite path. Graph-owned CommonJS modules and native-addon
1963
+ * loaders stay synchronous because Bun rejects `require()` targets backed by
1964
+ * async `onLoad` callbacks.
1434
1965
  */
1435
1966
  async function installExtensionGraphHook(
1436
1967
  entryRealPath: string,
1437
1968
  modules: Map<string, string>,
1438
1969
  commonJsPaths: Set<string>,
1970
+ cacheBustResolvedImportModules: ReadonlySet<string>,
1439
1971
  ): Promise<{ asyncModules: Map<string, string>; syncSourceModules: Map<string, string> }> {
1440
1972
  const asyncModules = new Map<string, string>();
1441
1973
  const syncSourceModules = new Map<string, string>();
1442
1974
  for (const [modulePath, source] of modules) {
1443
- const extension = path.extname(modulePath);
1444
- if (extension === ".cjs" || extension === ".cts") {
1445
- if (!commonJsPaths.has(modulePath)) {
1446
- throw new Error(`Missing CommonJS compatibility source: ${modulePath}`);
1447
- }
1448
- } else if (nativeAddonLoaderModulePaths.has(modulePath)) {
1975
+ if (commonJsPaths.has(modulePath)) {
1976
+ continue;
1977
+ }
1978
+ if (nativeAddonLoaderModulePaths.has(modulePath)) {
1449
1979
  syncSourceModules.set(modulePath, source);
1450
1980
  } else {
1451
1981
  asyncModules.set(modulePath, source);
@@ -1472,8 +2002,9 @@ async function installExtensionGraphHook(
1472
2002
  } else {
1473
2003
  raw = await Bun.file(sourcePath).text();
1474
2004
  }
2005
+ const resolvedImportMtimeTag = cacheBustResolvedImportModules.has(sourcePath) ? mtimeTag : null;
1475
2006
  return {
1476
- contents: await rewriteLegacyExtensionSource(raw, sourcePath, mtimeTag),
2007
+ contents: await rewriteLegacyExtensionSource(raw, sourcePath, mtimeTag, resolvedImportMtimeTag),
1477
2008
  loader: getLoader(sourcePath),
1478
2009
  };
1479
2010
  });
@@ -1491,13 +2022,12 @@ async function installExtensionGraphHook(
1491
2022
  build.onLoad({ filter, namespace: "file" }, args => {
1492
2023
  const queryIndex = args.path.indexOf("?mtime=");
1493
2024
  const sourcePath = queryIndex >= 0 ? args.path.slice(0, queryIndex) : args.path;
1494
- const source =
1495
- commonJsModuleSources.get(sourcePath) ??
1496
- synthesizeCommonJsDefaultModule(
1497
- sourcePath,
1498
- fs.readFileSync(commonJsFallbackModulePaths.get(sourcePath) ?? sourcePath, "utf8"),
1499
- commonJsFallbackModulePaths.get(sourcePath) ?? sourcePath,
1500
- );
2025
+ let source = commonJsModuleSources.get(sourcePath);
2026
+ if (source === undefined) {
2027
+ const targetPath = commonJsFallbackModulePaths.get(sourcePath) ?? sourcePath;
2028
+ const raw = rewriteExtensionSpecifiersFromCache(fs.readFileSync(targetPath, "utf8"), sourcePath);
2029
+ source = synthesizeCommonJsDefaultModule(sourcePath, raw, targetPath);
2030
+ }
1501
2031
  return { contents: source, loader: getLoader(sourcePath) };
1502
2032
  });
1503
2033
  },
@@ -1537,13 +2067,23 @@ async function installExtensionGraphHook(
1537
2067
  * during the initial load; `undefined` when no new modules were discovered.
1538
2068
  */
1539
2069
  async function ensureExtensionGraphHook(entryRealPath: string): Promise<{ clear(): void } | undefined> {
1540
- const currentModules = await collectExtensionModules(entryRealPath);
1541
- const commonJsPaths = new Set<string>();
2070
+ const {
2071
+ modules: currentModules,
2072
+ commonJsPaths,
2073
+ cacheBustResolvedImportModules: discoveredCacheBustModules,
2074
+ } = await collectExtensionModules(entryRealPath);
2075
+ let cacheBustResolvedImportModules = extensionGraphCacheBustResolvedImportModules.get(entryRealPath);
2076
+ if (!cacheBustResolvedImportModules) {
2077
+ cacheBustResolvedImportModules = new Set<string>();
2078
+ extensionGraphCacheBustResolvedImportModules.set(entryRealPath, cacheBustResolvedImportModules);
2079
+ }
2080
+ for (const modulePath of discoveredCacheBustModules) {
2081
+ cacheBustResolvedImportModules.add(modulePath);
2082
+ }
1542
2083
  for (const [modulePath, source] of currentModules) {
1543
- const extension = path.extname(modulePath);
1544
- if (extension === ".cjs" || extension === ".cts") {
2084
+ if (commonJsPaths.has(modulePath)) {
1545
2085
  commonJsModuleSources.set(modulePath, await prepareCommonJsDefaultModule(modulePath, source));
1546
- commonJsPaths.add(modulePath);
2086
+ commonJsGraphModulePaths.add(modulePath);
1547
2087
  }
1548
2088
  }
1549
2089
  let hookedModules = extensionGraphHookModules.get(entryRealPath);
@@ -1573,6 +2113,7 @@ async function ensureExtensionGraphHook(entryRealPath: string): Promise<{ clear(
1573
2113
  entryRealPath,
1574
2114
  pendingModules,
1575
2115
  pendingCommonJsPaths,
2116
+ cacheBustResolvedImportModules,
1576
2117
  ));
1577
2118
  for (const modulePath of pendingModules.keys()) {
1578
2119
  hookedModules.add(modulePath);