UNPKG

@typespec/compiler

Version:

TypeSpec compiler and standard library

261 lines 10.3 kB
import { joinPaths, normalizePath, resolvePath } from "../core/path-utils.js"; import { resolvePackageExports } from "./esm/resolve-package-exports.js"; import { resolvePackageImports } from "./esm/resolve-package-imports.js"; import { EsmResolveError, InvalidPackageTargetError, NoMatchingConditionsError, } from "./esm/utils.js"; import { NodePackageResolver } from "./node-package-resolver.js"; import { fileURLToPath, isFile, listDirHierarchy, parseNodeModuleSpecifier, pathToFileURL, readPackage, } from "./utils.js"; export class ResolveModuleError extends Error { code; pkgJson; constructor(code, message, pkgJson) { super(message); this.code = code; this.pkgJson = pkgJson; } } const defaultDirectoryIndexFiles = ["index.mjs", "index.js"]; /** * Resolve a module * @param host * @param specifier * @param options * @returns * @throws {ResolveModuleError} When the module cannot be resolved. */ export async function resolveModule(host, specifier, options) { const nodePackageResolver = new NodePackageResolver(host); const { baseDir } = options; const absoluteStart = await realpath(resolvePath(baseDir)); // Check if the module name is referencing a path(./foo, /foo, file:/foo) if (/^(?:\.\.?(?:\/|$)|\/|([A-Za-z]:)?[/\\])/.test(specifier)) { const res = resolvePath(absoluteStart, specifier); const m = (await loadAsFile(res)) || (await loadAsDirectory(res)); if (m) { return m; } } // If specifier starts with '#', resolve subpath imports. if (specifier.startsWith("#")) { const dirs = listDirHierarchy(baseDir); for (const dir of dirs) { const pkgFile = resolvePath(dir, "package.json"); if (!(await isFile(host, pkgFile))) continue; const pkg = await readPackage(host, pkgFile); const module = await resolveNodePackageImports(pkg, dir); if (module) return module; } } const moduleSpecifier = parseNodeModuleSpecifier(specifier); if (moduleSpecifier !== null) { const pkg = await nodePackageResolver.resolve(moduleSpecifier.packageName, absoluteStart); const n = pkg && (await loadPackage(pkg, moduleSpecifier.subPath)); if (n) return n; } throw new ResolveModuleError("MODULE_NOT_FOUND", `Cannot find module '${specifier}' from '${baseDir}'`); async function realpath(path) { try { return normalizePath(await host.realpath(path)); } catch (e) { if (e.code === "ENOENT" || e.code === "ENOTDIR") { return path; } throw e; } } /** * Equivalent implementation to node LOAD_NODE_MODULES with a few non supported features. * Cannot load any random file under the load path(only packages). */ async function resolveAsNodeModule(specifier, baseDir) { const pkg = await nodePackageResolver.resolveFromNodeModules(specifier.packageName, baseDir); if (pkg) { return await loadPackage(pkg, specifier.subPath); } else { return undefined; } } async function loadPackageAtPath(path, subPath) { const pkgFile = resolvePath(path, "package.json"); if (!(await isFile(host, pkgFile))) return undefined; const pkg = await readPackage(host, pkgFile); const n = await loadPackage(pkg, subPath); if (n) return n; return undefined; } async function resolveNodePackageImports(pkg, pkgDir) { if (!pkg.imports) return undefined; let match; try { match = await resolvePackageImports({ packageUrl: pathToFileURL(pkgDir), specifier, moduleDirs: ["node_modules"], conditions: options.conditions ?? [], ignoreDefaultCondition: options.fallbackOnMissingCondition, resolveId: async (id, baseDir) => { const specifier = parseNodeModuleSpecifier(id); if (specifier === null) return undefined; const resolved = await resolveAsNodeModule(specifier, fileURLToPath(baseDir.toString())); return resolved && pathToFileURL(resolved.mainFile); }, }, pkg.imports); } catch (error) { if (error instanceof InvalidPackageTargetError) { throw new ResolveModuleError("INVALID_MODULE_IMPORT_TARGET", error.message, pkg); } else if (error instanceof EsmResolveError) { throw new ResolveModuleError("INVALID_MODULE", error.message, pkg); } else { throw error; } } if (!match) return undefined; const resolved = await resolveEsmMatch(match, true, pkg); return { type: "module", mainFile: resolved, manifest: pkg, path: pkgDir, }; } /** * Try to load using package.json exports. * @param importSpecifier A combination of the package name and exports entry. * @param directory `node_modules` directory. */ async function resolveNodePackageExports(subPath, pkg, pkgDir) { if (!pkg.exports) return undefined; let match; try { match = await resolvePackageExports({ packageUrl: pathToFileURL(pkgDir), specifier: specifier, moduleDirs: ["node_modules"], conditions: options.conditions ?? [], ignoreDefaultCondition: options.fallbackOnMissingCondition, resolveId: (id, baseDir) => { throw new ResolveModuleError("INVALID_MODULE", "Not supported", pkg); }, }, subPath === "" ? "." : `./${subPath}`, pkg.exports); } catch (error) { if (error instanceof NoMatchingConditionsError) { // For back compat we allow to fallback to main field for the `.` entry. if (subPath === "") { return; } else { throw new ResolveModuleError("INVALID_MODULE", error.message, pkg); } } else if (error instanceof InvalidPackageTargetError) { throw new ResolveModuleError("INVALID_MODULE_EXPORT_TARGET", error.message, pkg); } else if (error instanceof EsmResolveError) { throw new ResolveModuleError("INVALID_MODULE", error.message, pkg); } else { throw error; } } if (!match) return undefined; const resolved = await resolveEsmMatch(match, false, pkg); return { type: "module", mainFile: resolved, manifest: pkg, path: await realpath(pkgDir), }; } async function resolveEsmMatch(match, isImports, pkg) { const resolved = await realpath(fileURLToPath(match)); if (await isFile(host, resolved)) { return resolved; } throw new ResolveModuleError(isImports ? "INVALID_MODULE_IMPORT_TARGET" : "INVALID_MODULE_EXPORT_TARGET", `Import "${specifier}" resolving to "${resolved}" is not a file.`, pkg); } async function loadAsDirectory(directory) { const pkg = await loadPackageAtPath(directory); if (pkg) { return pkg; } for (const file of options.directoryIndexFiles ?? defaultDirectoryIndexFiles) { const resolvedFile = await loadAsFile(joinPaths(directory, file)); if (resolvedFile) { return resolvedFile; } } return undefined; } async function loadPackage(pkg, subPath) { const e = await resolveNodePackageExports(subPath ?? "", pkg, pkg.dir); if (e) return e; if (subPath !== undefined && subPath !== "") { return undefined; } return loadPackageLegacy(pkg.dir, pkg); } async function loadPackageLegacy(directory, pkg) { const mainFile = options.resolveMain ? options.resolveMain(pkg) : pkg.main; if (mainFile === undefined || mainFile === null) { throw new ResolveModuleError("INVALID_MODULE", `Package ${pkg.name} is missing a main file or exports field.`, pkg); } if (typeof mainFile !== "string") { throw new ResolveModuleError("INVALID_MAIN", `Package ${pkg.name} main file "${mainFile}" must be a string.`, pkg); } const mainFullPath = resolvePath(directory, mainFile); let loaded; try { loaded = (await loadAsFile(mainFullPath)) ?? (await loadAsDirectory(mainFullPath)); } catch (e) { throw new ResolveModuleError("INVALID_MAIN", `Package ${pkg.name} main file "${mainFile}" is not pointing to a valid file or directory.`, pkg); } if (loaded) { if (loaded.type === "module") { return loaded; } return { type: "module", path: await realpath(directory), mainFile: loaded.path, manifest: pkg, }; } else { throw new ResolveModuleError("INVALID_MAIN", `Package ${pkg.name} main file "${mainFile}" is not pointing to a valid file or directory.`, pkg); } } async function loadAsFile(file) { if (await isFile(host, file)) { return resolvedFile(file); } const extensions = [".mjs", ".js"]; for (const ext of extensions) { const fileWithExtension = file + ext; if (await isFile(host, fileWithExtension)) { return resolvedFile(fileWithExtension); } } return undefined; } async function resolvedFile(path) { return { type: "file", path: await realpath(path) }; } } //# sourceMappingURL=module-resolver.js.map