@typespec/compiler
Version:
TypeSpec Compiler Preview
345 lines • 12.7 kB
JavaScript
import { getDirectoryPath, 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 { parseNodeModuleSpecifier } 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 { baseDir } = options;
const absoluteStart = await realpath(resolvePath(baseDir));
if (!(await isDirectory(host, absoluteStart))) {
throw new TypeError(`Provided basedir '${baseDir}'is not a directory.`);
}
// 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;
}
}
// Try to resolve package itself.
const self = await resolveSelf(specifier, absoluteStart);
if (self)
return self;
// Try to resolve as a node_module package.
const module = await resolveAsNodeModule(specifier, absoluteStart);
if (module)
return module;
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;
}
}
/**
* Returns a list of all the parent directory and the given one.
*/
function listDirHierarchy(baseDir) {
const paths = [baseDir];
let current = getDirectoryPath(baseDir);
while (current !== paths[paths.length - 1]) {
paths.push(current);
current = getDirectoryPath(current);
}
return paths;
}
/**
* Equivalent implementation to node LOAD_PACKAGE_SELF
* Resolve if the import is importing the current package.
*/
async function resolveSelf(name, baseDir) {
for (const dir of listDirHierarchy(baseDir)) {
const pkgFile = resolvePath(dir, "package.json");
if (!(await isFile(host, pkgFile)))
continue;
const pkg = await readPackage(host, pkgFile);
if (pkg.name === name) {
return loadPackage(dir, pkg);
}
else {
return undefined;
}
}
return undefined;
}
/**
* 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(importSpecifier, baseDir) {
const module = parseNodeModuleSpecifier(importSpecifier);
if (module === null)
return undefined;
const dirs = listDirHierarchy(baseDir);
for (const dir of dirs) {
const n = await loadPackageAtPath(joinPaths(dir, "node_modules", module.packageName), module.subPath);
if (n)
return n;
}
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(path, 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 resolved = await resolveAsNodeModule(id, 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: 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(directory, pkg, subPath) {
const e = await resolveNodePackageExports(subPath ?? "", pkg, directory);
if (e)
return e;
if (subPath !== undefined && subPath !== "") {
return undefined;
}
return loadPackageLegacy(directory, 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) };
}
}
async function readPackage(host, pkgfile) {
const content = await host.readFile(pkgfile);
return {
...JSON.parse(content),
file: {
path: pkgfile,
text: content,
},
};
}
async function isDirectory(host, path) {
try {
const stats = await host.stat(path);
return stats.isDirectory();
}
catch (e) {
if (e.code === "ENOENT" || e.code === "ENOTDIR") {
return false;
}
throw e;
}
}
async function isFile(host, path) {
try {
const stats = await host.stat(path);
return stats.isFile();
}
catch (e) {
if (e.code === "ENOENT" || e.code === "ENOTDIR") {
return false;
}
throw e;
}
}
function pathToFileURL(path) {
return `file://${path}`;
}
function fileURLToPath(url) {
if (!url.startsWith("file://"))
throw new Error("Cannot convert non file: URL to path");
const pathname = url.slice("file://".length);
for (let n = 0; n < pathname.length; n++) {
if (pathname[n] === "%") {
const third = pathname.codePointAt(n + 2) | 0x20;
if (pathname[n + 1] === "2" && third === 102) {
throw new Error("Invalid url to path: must not include encoded / characters");
}
}
}
return decodeURIComponent(pathname);
}
//# sourceMappingURL=module-resolver.js.map