import { isBuiltin } from "node:module"; import { access } from "node:fs/promises"; import { resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import type { API } from "tsrs/async"; import type { TsRuntime } from "./runtime.ts"; import type { TsCompiler } from "./compiler.ts"; import { resolveTsSource, withTsImportApi } from "./imports-api.ts"; import { cacheExactNpmImport, runImportCommand } from "../code-mode/npm-cache.ts"; import { compileTsImportSource } from "./imports-source.ts"; export interface TsImportOptions { cwd: string; agentDir: string; cacheDir: string; /** Caller-owned disposable directory, separate from the persistent npm cache. */ sourceDir?: string; /** Caller-owned module identity shared by static/dynamic imports throughout a live kernel. */ sourceModules?: Map; compiler?: Pick; signal?: AbortSignal; onNpmImport?: (specifier: string) => void; } export interface TsImportResolutions { moduleResolutions: Map; runtimeSpecifiers: Map; } async function importLocation(specifier: string, options: TsImportOptions): Promise<{ cwd: string; bare: string }> { options.signal?.throwIfAborted(); if (specifier.startsWith("npm:")) { const location = await cacheExactNpmImport(specifier, options.cacheDir, options.signal); options.onNpmImport?.(specifier); return location; } if (/^[a-zA-Z][\w+.-]*:/.test(specifier) && !specifier.startsWith("node:") && !specifier.startsWith("file:")) { throw new Error(`Unsupported import ${specifier}. Use node:, an installed package, an exact npm: import, or a local file.`); } return { cwd: resolve(options.cwd), bare: specifier }; } async function runtimeLocation(bare: string, cwd: string, signal?: AbortSignal, resolveSource?: () => Promise, mode: "import" | "require" = "import"): Promise { signal?.throwIfAborted(); if (isBuiltin(bare)) return bare.startsWith("node:") ? bare : `node:${bare}`; const localSource = async (): Promise => { if (!/^\.{1,2}\//.test(bare) || !/\.(?:js|mjs|cjs)$/.test(bare)) return undefined; const source = await resolveSource?.(); if (!source || !/(? { const location = await importLocation(specifier, options); return runtimeLocation(location.bare, location.cwd, options.signal, async () => { const resolveSource = (owner: API, runtime: TsRuntime) => resolveTsSource(location.bare, location.cwd, owner, runtime, mode); return compiler ? resolveSource(compiler.api, compiler.runtime) : withTsImportApi(options, resolveSource); }, mode); } export async function resolveTsRuntimeImport(specifier: string, options: TsImportOptions): Promise { const url = await resolveRawImport(specifier, options); return tsImportSourcePath(url) ? compileTsImportSource(url, options, resolveRawImport) : url; } /** Types use tsrs's Node resolver; runtime uses Node's import conditions, never require conditions. */ export async function resolveTsImports(specifiers: readonly string[], options: TsImportOptions): Promise { const moduleResolutions = new Map(); const runtimeSpecifiers = new Map(); const graphOptions = { ...options, sourceModules: options.sourceModules ?? new Map() }; if (specifiers.length === 0) return { moduleResolutions, runtimeSpecifiers }; await withTsImportApi(options, async (api, runtime) => { const { ModuleKind, ModuleResolutionKind } = runtime.api; const resolver = await api.createModuleResolver({ module: ModuleKind.NodeNext, moduleResolution: ModuleResolutionKind.NodeNext, allowJs: true }); try { for (const specifier of new Set(specifiers)) { const { cwd, bare } = await importLocation(specifier, options); const builtin = isBuiltin(bare); const typeName = bare.startsWith("file:") ? fileURLToPath(bare) : bare; const types = (await resolver.resolveModuleName(typeName, cwd, ModuleKind.ESNext)).resolvedModule; if (types) moduleResolutions.set(specifier, types.resolvedFileName); // Node built-ins are ambient declarations supplied by @types/node. if (!types && !builtin && /\.(?:ts|mts|cts)$/.test(typeName)) { const path = resolve(cwd, typeName); await access(path); moduleResolutions.set(specifier, path); } try { runtimeSpecifiers.set(specifier, await runtimeLocation(bare, cwd, options.signal, async () => types?.resolvedFileName)); } catch (error) { options.signal?.throwIfAborted(); // Type-only packages/exports need no executable entry. A value import still // resolves through the runtime bridge and reports the original failure. if (!types) throw error; } } } finally { await resolver.dispose(); } }); for (const [specifier, runtime] of runtimeSpecifiers) { if (tsImportSourcePath(runtime)) runtimeSpecifiers.set(specifier, await compileTsImportSource(runtime, graphOptions, resolveRawImport, (name, path) => moduleResolutions.set(name, path))); } return { moduleResolutions, runtimeSpecifiers }; } function tsImportSourcePath(url: string): string | undefined { if (!url.startsWith("file:")) return undefined; const path = fileURLToPath(url); return /(?