// oxlint-disable-next-line typescript/triple-slash-reference -- legacy virtual module declarations.
///
import { Database } from "bun:sqlite";
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 type * as BabelParser from "@babel/parser";
import {
getDbBusyTimeoutMs,
getLegacyPiExtensionCacheDbPath,
isCompiledBinary,
isRecord,
logger,
stripWindowsExtendedLengthPathPrefix,
} from "@oh-my-pi/pi-utils";
import { registerPluginCacheInvalidator } from "../../discovery/helpers";
const USE_BUNDLED_PI_MODULES = isCompiledBinary() || Boolean(process.env.PI_BUNDLED);
// === Bundled host modules (issue #3423) ===
//
// Bun 1.3.14 stopped exposing `--compile` extras through any filesystem-style
// API: `fs.existsSync`, `Bun.file().exists()`, `Bun.resolveSync`, and even
// `import("/$bunfs/...")` / `import("file:///$bunfs/...")` all fail for the
// embedded entries. Bun.plugin `onResolve` also no longer fires for transitive
// imports inside runtime-loaded extensions.
//
// Compiled binaries and npm bundles retain lazy loaders for host packages and
// serve requested surfaces through `omp-legacy-pi-bundled:` synthetic modules.
// `scripts/legacy-pi-virtual-module.ts` derives literal dynamic-import edges
// from current package exports inside a Bun build plugin: no generated source
// or duplicate key list exists on disk. Deferring each host module evaluation
// avoids cycles with an extension-loading command that is itself in the
// retained package graph.
const BUNDLED_VIRTUAL_SCHEME = "omp-legacy-pi-bundled:";
const BUNDLED_VIRTUAL_NAMESPACE = "omp-legacy-pi-bundled";
const BUNDLED_HOST_NAMESPACE = "omp-legacy-pi-host";
const BUNDLED_HOST_SCHEME = `${BUNDLED_HOST_NAMESPACE}:`;
const TYPEBOX_BUNDLED_MODULE_KEY = "typebox";
type BundledModule = Readonly>;
type BundledModuleLoaders = Readonly Promise>>;
interface LegacyPiResolveResult {
path: string;
namespace?: string;
}
interface BundledVirtualResolveResult {
path: string;
namespace: typeof BUNDLED_VIRTUAL_NAMESPACE | typeof BUNDLED_HOST_NAMESPACE;
}
interface ExtensionSpecifierReference {
readonly kind: "import" | "require";
readonly specifier: string;
readonly start: number;
readonly end: number;
}
function parseExtensionSource(source: string, importerPath: string): ParseResult {
// Installing specifier hooks needs no parser; only a real source rewrite does.
const { parse: parseBabel }: typeof BabelParser = require("@babel/parser");
const extension = path.extname(importerPath).toLowerCase();
const plugins: ParserPlugin[] = ["decorators-legacy", "explicitResourceManagement"];
if (extension === ".ts" || extension === ".mts" || extension === ".cts" || extension === ".tsx") {
plugins.push("typescript");
}
if (extension === ".jsx" || extension === ".tsx") {
plugins.push("jsx");
}
try {
return parseBabel(source, {
sourceType: "unambiguous",
allowAwaitOutsideFunction: true,
allowReturnOutsideFunction: true,
allowImportExportEverywhere: true,
allowNewTargetOutsideFunction: true,
allowSuperOutsideMethod: true,
allowUndeclaredExports: true,
errorRecovery: true,
plugins,
});
} catch (error) {
throw new Error(
`Failed to parse extension source for dependency rewriting: ${importerPath}: ${error instanceof Error ? error.message : String(error)}`,
{ cause: error },
);
}
}
const REQUIRE_BINDING = 1 << 0;
const EXPORTS_BINDING = 1 << 1;
const MODULE_BINDING = 1 << 2;
interface StructuralAstNode {
readonly type: string;
readonly [key: string]: unknown;
}
interface BindingScope {
readonly parent: BindingScope | null;
readonly ownsVarBindings: boolean;
bindings: number;
}
interface ScopedAstNode {
readonly node: StructuralAstNode;
readonly scope: BindingScope;
readonly order: number;
}
interface ScopeWalkItem {
readonly node: StructuralAstNode;
readonly scope: BindingScope | null;
readonly parent: StructuralAstNode | null;
readonly parentKey: string | null;
}
function asAstNode(value: unknown): StructuralAstNode | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const candidate = value as { readonly type?: unknown };
return typeof candidate.type === "string" ? (candidate as StructuralAstNode) : null;
}
function nodeArray(node: StructuralAstNode, key: string): readonly unknown[] | null {
const value = node[key];
return Array.isArray(value) ? value : null;
}
function nodeArgument(node: StructuralAstNode | null, index: number): StructuralAstNode | null {
if (!node) return null;
const values = nodeArray(node, "arguments");
return values ? asAstNode(values[index]) : null;
}
function isIdentifier(node: StructuralAstNode | null, name: string): boolean {
return node?.type === "Identifier" && node.name === name;
}
function trackedBinding(name: unknown): number {
switch (name) {
case "require":
return REQUIRE_BINDING;
case "exports":
return EXPORTS_BINDING;
case "module":
return MODULE_BINDING;
default:
return 0;
}
}
function addPatternBindings(scope: BindingScope, pattern: unknown): void {
const stack: unknown[] = [pattern];
while (stack.length > 0) {
const node = asAstNode(stack.pop());
if (!node) continue;
switch (node.type) {
case "Identifier":
scope.bindings |= trackedBinding(node.name);
break;
case "AssignmentPattern":
stack.push(node.left);
break;
case "RestElement":
stack.push(node.argument);
break;
case "ArrayPattern": {
const elements = nodeArray(node, "elements");
if (elements) stack.push(...elements);
break;
}
case "ObjectPattern": {
const properties = nodeArray(node, "properties");
if (!properties) break;
for (const value of properties) {
const property = asAstNode(value);
if (!property) continue;
stack.push(property.type === "RestElement" ? property.argument : property.value);
}
break;
}
case "TSParameterProperty":
stack.push(node.parameter);
break;
}
}
}
function isFunctionScopeNode(node: StructuralAstNode): boolean {
switch (node.type) {
case "FunctionDeclaration":
case "FunctionExpression":
case "ArrowFunctionExpression":
case "ObjectMethod":
case "ClassMethod":
case "ClassPrivateMethod":
case "TSDeclareFunction":
case "TSDeclareMethod":
case "DeclareFunction":
return true;
default:
return false;
}
}
function isFunctionDeclarationNode(node: StructuralAstNode): boolean {
return node.type === "FunctionDeclaration" || node.type === "TSDeclareFunction" || node.type === "DeclareFunction";
}
function isClassScopeNode(node: StructuralAstNode): boolean {
return node.type === "ClassDeclaration" || node.type === "ClassExpression";
}
function scopeKind(node: StructuralAstNode, parent: StructuralAstNode | null, parentKey: string | null): 0 | 1 | 2 {
if (
node.type === "Program" ||
isFunctionScopeNode(node) ||
node.type === "StaticBlock" ||
node.type === "TSModuleBlock"
) {
return 2;
}
if (
isClassScopeNode(node) ||
node.type === "CatchClause" ||
node.type === "ForStatement" ||
node.type === "ForInStatement" ||
node.type === "ForOfStatement" ||
node.type === "SwitchStatement"
) {
return 1;
}
if (node.type === "BlockStatement" && !(parent && isFunctionScopeNode(parent) && parentKey === "body")) {
return 1;
}
return 0;
}
function nearestVarScope(scope: BindingScope): BindingScope {
let current = scope;
while (!current.ownsVarBindings && current.parent) current = current.parent;
return current;
}
function registerOuterDeclaration(node: StructuralAstNode, scope: BindingScope | null): void {
if (!scope) return;
if (
(isFunctionDeclarationNode(node) && node.type !== "TSDeclareFunction" && node.type !== "DeclareFunction") ||
(node.type === "ClassDeclaration" && node.declare !== true)
) {
addPatternBindings(scope, node.id);
}
}
function registerScopeBindings(node: StructuralAstNode, scope: BindingScope): void {
if (isFunctionScopeNode(node)) {
addPatternBindings(scope, node.id);
const parameters = nodeArray(node, "params");
if (parameters) {
for (const parameter of parameters) addPatternBindings(scope, parameter);
}
}
if (isClassScopeNode(node)) addPatternBindings(scope, node.id);
if (node.type === "CatchClause") addPatternBindings(scope, node.param);
if (node.type === "ImportDeclaration") {
const specifiers = nodeArray(node, "specifiers");
if (specifiers) {
for (const value of specifiers) {
const specifier = asAstNode(value);
if (specifier) addPatternBindings(scope, specifier.local);
}
}
} else if (node.type === "TSImportEqualsDeclaration") {
addPatternBindings(scope, node.id);
} else if (node.type === "VariableDeclaration") {
const target = node.kind === "var" ? nearestVarScope(scope) : scope;
const declarations = nodeArray(node, "declarations");
if (declarations) {
for (const value of declarations) {
const declaration = asAstNode(value);
if (declaration) addPatternBindings(target, declaration.id);
}
}
}
}
function isAstMetadataKey(key: string): boolean {
switch (key) {
case "loc":
case "extra":
case "range":
case "comments":
case "tokens":
case "errors":
case "leadingComments":
case "trailingComments":
case "innerComments":
case "parent":
case "parentPath":
case "scope":
case "hub":
return true;
default:
return false;
}
}
function scopeForChild(
node: StructuralAstNode,
key: string,
outerScope: BindingScope | null,
nodeScope: BindingScope,
): BindingScope {
if (node.type === "SwitchStatement" && key === "discriminant") {
return outerScope ?? nodeScope;
}
if (isFunctionScopeNode(node) && (key === "key" || key === "decorators")) {
return outerScope ?? nodeScope;
}
if (isClassScopeNode(node) && key === "decorators") {
return outerScope ?? nodeScope;
}
return nodeScope;
}
/**
* Builds only the lexical information needed by legacy extension rewriting.
* Scope frames are fully populated before selected nodes are returned, so
* hoisted and TDZ bindings behave independently of textual declaration order.
*/
function collectScopedAstNodes(root: unknown, select: (node: StructuralAstNode) => boolean): ScopedAstNode[] {
const rootNode = asAstNode(root);
if (!rootNode) return [];
const selected: ScopedAstNode[] = [];
const stack: ScopeWalkItem[] = [{ node: rootNode, scope: null, parent: null, parentKey: null }];
const seen = new WeakSet