hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
1,153 lines (1,036 loc) • 34.5 kB
text/typescript
import type {
Resolver,
RemappedNpmPackagesGraphJson,
Remapping,
ResolvedNpmUserRemapping,
ResolvedUserRemapping,
Result,
} from "./types.js";
import type {
ImportResolutionError,
NpmRootResolutionError,
ProjectRootResolutionError,
ResolvedFileReference,
UserRemappingReference,
} from "../../../../../types/solidity/errors.js";
import type {
ResolvedNpmPackage,
ResolvedFile,
FileContent,
ProjectResolvedFile,
NpmPackageResolvedFile,
} from "../../../../../types/solidity/resolved-file.js";
import path from "node:path";
import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors";
import { exists } from "@nomicfoundation/hardhat-utils/fs";
import { AsyncMutex } from "@nomicfoundation/hardhat-utils/synchronization";
import { analyze } from "@nomicfoundation/solidity-analyzer";
import {
ImportResolutionErrorType,
RootResolutionErrorType,
} from "../../../../../types/solidity/errors.js";
import { ResolvedFileType } from "../../../../../types/solidity/resolved-file.js";
import { parseNpmDirectImport } from "./npm-module-parsing.js";
import {
isResolvedUserRemapping,
RemappedNpmPackagesGraphImplementation,
} from "./remapped-npm-packages-graph.js";
import { applyValidRemapping, formatRemapping } from "./remappings.js";
import {
fsPathToSourceNamePath,
sourceNamePathJoin,
sourceNamePathToFsPath,
} from "./source-name-utils.js";
import { UserRemappingType } from "./types.js";
import {
PathValidationErrorType,
resolveSubpathWithPackageExports,
validateFsPath,
} from "./utils.js";
const NPM_PACKAGES_WITH_SIMULATED_PACKAGE_EXPORTS = new Set(["forge-std"]);
export class ResolverImplementation implements Resolver {
readonly #projectRoot: string;
readonly #npmPackageGraph: RemappedNpmPackagesGraphImplementation;
readonly #hhProjectPackage: ResolvedNpmPackage;
readonly #readUtf8File: (absPath: string) => Promise<string>;
/**
* IMPORTANT: This mutex must be acquired before writing to any of the mutable
* fields of this class.
*
* We do this by using the mutex in the public methods, which don't call each
* other.
*/
readonly #mutex = new AsyncMutex();
/**
* We use this map to ensure that we only resolve each file once.
**/
readonly #resolvedFileByInputSourceName: Map<string, ResolvedFile> =
new Map();
/**
* A fake `<root>.sol` file that we use to resolve npm roots using
* the same logic we use for imports.
*/
readonly #fakeRootFile: ProjectResolvedFile;
/**
* Creates a new resolver.
*
* @param projectRoot The absolute path to the Hardhat project root.
* @param readUtf8File A function that reads a UTF-8 file.
* @returns The resolver or the user remapping errors found.
*/
public static async create(
projectRoot: string,
readUtf8File: (absPath: string) => Promise<string>,
): Promise<Resolver> {
const map =
await RemappedNpmPackagesGraphImplementation.create(projectRoot);
return new ResolverImplementation(projectRoot, map, readUtf8File);
}
private constructor(
projectRoot: string,
npmPackagesGraph: RemappedNpmPackagesGraphImplementation,
readUtf8File: (absPath: string) => Promise<string>,
) {
this.#projectRoot = projectRoot;
this.#npmPackageGraph = npmPackagesGraph;
this.#hhProjectPackage = npmPackagesGraph.getHardhatProjectPackage();
this.#readUtf8File = readUtf8File;
const fakeRootFileName = "<fake-root-do-not-use>.sol";
this.#fakeRootFile = {
type: ResolvedFileType.PROJECT_FILE,
inputSourceName: sourceNamePathJoin(
this.#hhProjectPackage.inputSourceNameRoot,
fakeRootFileName,
),
fsPath: path.join(this.#projectRoot, fakeRootFileName),
content: {
importPaths: [],
text: "",
versionPragmas: [],
},
package: this.#hhProjectPackage,
};
}
public async resolveProjectFile(
absoluteFilePath: string,
): Promise<Result<ProjectResolvedFile, ProjectRootResolutionError>> {
return this.#mutex.exclusiveRun(async () => {
return this.#resolveProjectFile(absoluteFilePath);
});
}
public async resolveNpmDependencyFileAsRoot(
npmModule: string,
): Promise<
Result<
{ file: NpmPackageResolvedFile; remapping?: ResolvedNpmUserRemapping },
NpmRootResolutionError
>
> {
return this.#mutex.exclusiveRun(async () => {
return this.#resolveNpmDependencyFileAsRoot(npmModule);
});
}
public async resolveImport(
from: ResolvedFile,
importPath: string,
): Promise<
Result<
{ file: ResolvedFile; remapping?: Remapping | ResolvedUserRemapping },
ImportResolutionError
>
> {
return this.#mutex.exclusiveRun(async () =>
this.#resolveImport(from, importPath),
);
}
async #resolveProjectFile(
absoluteFilePath: string,
): Promise<Result<ProjectResolvedFile, ProjectRootResolutionError>> {
if (!absoluteFilePath.startsWith(this.#projectRoot)) {
return {
success: false,
error: {
type: RootResolutionErrorType.PROJECT_ROOT_FILE_NOT_IN_PROJECT,
absoluteFilePath,
},
};
}
const relativeFilePath = path.relative(this.#projectRoot, absoluteFilePath);
// We first check if the file has already been resolved.
//
// Note that it may have received the right path, but with the wrong
// casing. We don't care at this point, as it would just mean a cache
// miss, and we proceed to get the right casing in that case.
//
// However, as most of the time these absolute paths are read from the file
// system, they'd have the right casing in general.
//
// If we need to fetch the right casing, we'd have to recheck the cache,
// to avoid re-resolving the file.
let inputSourceName = sourceNamePathJoin(
this.#hhProjectPackage.inputSourceNameRoot,
fsPathToSourceNamePath(relativeFilePath),
);
const existing = this.#resolvedFileByInputSourceName.get(inputSourceName);
if (existing !== undefined) {
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions --
The cache is type-unsafe, but we are sure this is a ProjectResolvedFile
because of how its input source name is created */
const existingProjectResolvedFile = existing as ProjectResolvedFile;
return {
success: true,
value: existingProjectResolvedFile,
};
}
if (relativeFilePath.startsWith("node_modules" + path.sep)) {
return {
success: false,
error: {
type: RootResolutionErrorType.PROJECT_ROOT_FILE_IN_NODE_MODULES,
absoluteFilePath,
},
};
}
const pathValidation = await validateFsPath(
this.#projectRoot,
relativeFilePath,
);
let realCasingRelativePath = relativeFilePath;
if (pathValidation.success === false) {
if (
pathValidation.error.type === PathValidationErrorType.DOES_NOT_EXIST
) {
return {
success: false,
error: {
type: RootResolutionErrorType.PROJECT_ROOT_FILE_DOES_NOT_EXIST,
absoluteFilePath,
},
};
}
// Now that we have the correct casing, we "fix" the input source name.
realCasingRelativePath = pathValidation.error.correctCasing;
inputSourceName = sourceNamePathJoin(
this.#hhProjectPackage.inputSourceNameRoot,
fsPathToSourceNamePath(realCasingRelativePath),
);
}
// Maybe it was already resolved, so we need to check with the right
// casing
const resolvedWithTheRightCasing =
this.#resolvedFileByInputSourceName.get(inputSourceName);
if (resolvedWithTheRightCasing !== undefined) {
const resolvedWithTheRightCasingProjectResolvedFile =
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
-- Same as above, we know it's a project file because of its input
source name */
resolvedWithTheRightCasing as ProjectResolvedFile;
return {
success: true,
value: resolvedWithTheRightCasingProjectResolvedFile,
};
}
const fsPathWithTheRightCasing = path.join(
this.#projectRoot,
realCasingRelativePath,
);
const resolvedFile = await this.#buildResolvedFile({
npmPackage: this.#hhProjectPackage,
fsPath: fsPathWithTheRightCasing,
inputSourceName,
});
this.#resolvedFileByInputSourceName.set(inputSourceName, resolvedFile);
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions --
We know it's a project file, because we created it. */
const resolvedFileAsProjectFile = resolvedFile as ProjectResolvedFile;
return {
success: true,
value: resolvedFileAsProjectFile,
};
}
public toJSON(): {
resolvedFileBySourceName: Record<string, ResolvedFile>;
remappedNpmPackagesGraph: RemappedNpmPackagesGraphJson;
} {
return {
resolvedFileBySourceName: Object.fromEntries(
this.#resolvedFileByInputSourceName.entries(),
),
remappedNpmPackagesGraph: this.#npmPackageGraph.toJSON(),
};
}
async #resolveNpmDependencyFileAsRoot(
npmModule: string,
): Promise<
Result<
{ file: NpmPackageResolvedFile; remapping?: ResolvedNpmUserRemapping },
NpmRootResolutionError
>
> {
// We want to be sure that the resolution of npm root files is treated
// exactly like imports, so we use a fake root file and resolve the
// npmModule as if it were a directImport inside of it.
//
// NOTE: As we use a public method here, we don't acquire the mutex,
// but we don't modify the state in this method itself.
const parsedNpmModule = parseNpmDirectImport(npmModule);
if (parsedNpmModule === undefined) {
return {
success: false,
error: {
type: RootResolutionErrorType.NPM_ROOT_FILE_NAME_WITH_INVALID_FORMAT,
npmModule,
},
};
}
const result = await this.#resolveImport(this.#fakeRootFile, npmModule);
if (result.success === false) {
return {
success: false,
error: this.#importResolutionErrorToNpmRootResolutionError(
npmModule,
result.error,
),
};
}
const resolvedFile = result.value.file;
assertHardhatInvariant(
result.value.remapping !== undefined,
"We must have a remapping here, because we either resolved though a user remapping, or npm",
);
// If resolving this fake import results in using a user remapping, we
// need to return it.
//
// If instead, if using a generated remapping for that import, we don't
// return it, as this is not a real import.
const remapping = isResolvedUserRemapping(result.value.remapping)
? result.value.remapping
: undefined;
// This could happen due to a user remapping
if (resolvedFile.type !== ResolvedFileType.NPM_PACKAGE_FILE) {
assertHardhatInvariant(
remapping !== undefined,
"Expected user remapping to be present if the import resolved to a local file",
);
return {
success: false,
error: {
type: RootResolutionErrorType.NPM_ROOT_FILE_RESOLVES_TO_PROJECT_FILE,
npmModule,
userRemapping: {
originalUserRemapping: remapping.originalFormat,
actualUserRemapping: formatRemapping(remapping),
remappingSource: remapping.source,
},
resolvedFileFsPath: resolvedFile.fsPath,
},
};
}
// By this point, we know if that we have a user remapping, it's into an
// npm package, because otherwise we would have returned an error.
// We need to do this invariant assertion here, because otherwise TS will
// complain.
let remappingResult: ResolvedNpmUserRemapping | undefined;
if (remapping !== undefined) {
assertHardhatInvariant(
remapping.type === UserRemappingType.NPM,
"If we have a user remapping, it must be a npm remapping",
);
remappingResult = remapping;
}
return {
success: true,
value: {
file: resolvedFile,
remapping: remappingResult,
},
};
}
async #resolveImport(
from: ResolvedFile,
importPath: string,
): Promise<
Result<
{
file: ResolvedFile;
remapping?: Remapping | ResolvedUserRemapping;
},
ImportResolutionError
>
> {
// Imports shouldn't include windows separators
if (importPath.includes("\\")) {
return {
success: false,
error: {
type: ImportResolutionErrorType.IMPORT_WITH_WINDOWS_PATH_SEPARATORS,
fromFsPath: from.fsPath,
importPath,
},
};
}
const isRelativeImport =
importPath.startsWith("./") || importPath.startsWith("../");
const directImport = isRelativeImport
? sourceNamePathJoin(path.dirname(from.inputSourceName), importPath)
: importPath;
if (isRelativeImport) {
// If the import is relative, it shouldn't leave its package
if (!directImport.startsWith(from.package.inputSourceNameRoot)) {
return {
success: false,
error: {
type: ImportResolutionErrorType.ILLEGAL_RELATIVE_IMPORT,
fromFsPath: from.fsPath,
importPath,
},
};
}
// It also shouldn't get into its package's node_modules
if (
directImport.startsWith(
sourceNamePathJoin(from.package.inputSourceNameRoot, "node_modules"),
)
) {
return {
success: false,
error: {
type: ImportResolutionErrorType.RELATIVE_IMPORT_INTO_NODE_MODULES,
fromFsPath: from.fsPath,
importPath,
},
};
}
}
// Now, we get the best user remapping, if there's any.
const bestUserRemappingResult =
await this.#npmPackageGraph.selectBestUserRemapping(from, directImport);
if (bestUserRemappingResult.success === false) {
return {
success: false,
error: {
type: ImportResolutionErrorType.IMPORT_WITH_REMAPPING_ERRORS,
fromFsPath: from.fsPath,
importPath,
remappingErrors: bestUserRemappingResult.error,
},
};
}
const bestUserRemapping = bestUserRemappingResult.value;
if (isRelativeImport) {
// Relative imports should be resolved based on the file system, so
// they should not be affected by user remapping.
if (bestUserRemapping !== undefined) {
return {
success: false,
error: {
type: ImportResolutionErrorType.RELATIVE_IMPORT_CLASHES_WITH_USER_REMAPPING,
fromFsPath: from.fsPath,
importPath,
directImport,
userRemapping: this.#buildUserRemappingReference({
userRemapping: bestUserRemapping,
}),
},
};
}
return this.#resolveRelativeImport({
from,
importPath,
directImport,
});
} else {
if (bestUserRemapping !== undefined) {
// If the import isn't relative, and there's a user remapping, we
// prioritize that.
return this.#resolveUserRemappedImport({
from,
importPath,
directImport,
remapping: bestUserRemapping,
});
}
// Otherwise it should be resolved through npm
const npmResolutionResult = await this.#resolveImportThroughNpm({
from,
importPath,
directImport,
});
if (npmResolutionResult.success === true) {
return { success: true, value: npmResolutionResult.value };
}
// If the npm resolution fails because the package was not installed, or
// because the import was invalid, we try to detect if the user was
// trying to use a direct import (i.e. not relative) to import a local
// file.
//
// We do this to improve the error message that we generate, and suggest
// a user remapping if they insist on using direct imports.
if (
npmResolutionResult.error.type ===
ImportResolutionErrorType.IMPORT_OF_UNINSTALLED_PACKAGE ||
npmResolutionResult.error.type ===
ImportResolutionErrorType.IMPORT_WITH_INVALID_NPM_SYNTAX
) {
const improvedError = await this.#tryToGenerateDirectLocalImportError({
from,
importPath,
});
if (improvedError !== undefined) {
return {
success: false,
error: improvedError,
};
}
}
return npmResolutionResult;
}
}
async #resolveRelativeImport({
from,
importPath,
directImport,
}: {
from: ResolvedFile;
importPath: string;
directImport: string;
}): Promise<Result<{ file: ResolvedFile }, ImportResolutionError>> {
const inputSourceName = directImport;
const existing = this.#resolvedFileByInputSourceName.get(inputSourceName);
if (existing !== undefined) {
return { success: true, value: { file: existing } };
}
const relativeSourceNamePath = this.#getRelativeSourceNamePath({
npmPackage: from.package,
fileInputSourceName: inputSourceName,
});
const relativeFsPath = sourceNamePathToFsPath(relativeSourceNamePath);
return this.#commonImportResolution({
from,
importPath,
npmPackage: from.package,
inputSourceName,
relativeFsPathWithinPackage: relativeFsPath,
subpath: relativeSourceNamePath,
});
}
/**
* Resolves a user remapped import.
* @returns The resolved file, or an error. If successful, the remapping is
* always present, and of type `ResolvedUserRemapping`.
*/
async #resolveUserRemappedImport({
from,
importPath,
directImport,
remapping,
}: {
from: ResolvedFile;
importPath: string;
directImport: string;
remapping: ResolvedUserRemapping;
}): Promise<
Result<
{ file: ResolvedFile; remapping?: Remapping | ResolvedUserRemapping },
ImportResolutionError
>
> {
const remappedDirectImport = applyValidRemapping(directImport, remapping);
const inputSourceName = remappedDirectImport;
const existing =
this.#resolvedFileByInputSourceName.get(remappedDirectImport);
if (existing !== undefined) {
return { success: true, value: { file: existing, remapping } };
}
const fromNpmPackage =
from.type === ResolvedFileType.NPM_PACKAGE_FILE
? from.package
: this.#hhProjectPackage;
// We get the npm package that's the target of the remapping. If none
// is present, that's because it's remapping to a local file, so it's
// the fromNpmPackage.
const targetNpmPackage =
remapping.type === UserRemappingType.NPM
? remapping.targetNpmPackage.package
: fromNpmPackage;
// A user remapping is created based on the fs path in the package, so
// we can get the relative path based on the input source name root of the
// target package.
const relativeSourceNamePath = this.#getRelativeSourceNamePath({
npmPackage: targetNpmPackage,
fileInputSourceName: inputSourceName,
});
const relativeFsPath = sourceNamePathToFsPath(relativeSourceNamePath);
return this.#commonImportResolution({
from,
importPath,
npmPackage: targetNpmPackage,
inputSourceName,
relativeFsPathWithinPackage: relativeFsPath,
subpath: relativeSourceNamePath,
userRemapping: remapping,
});
}
/**
* Resolves an import through npm.
* @returns The resolved file, or an error. If successful, the remapping is
* always present.
*/
async #resolveImportThroughNpm({
from,
importPath,
directImport,
}: {
from: ResolvedFile;
importPath: string;
directImport: string;
}): Promise<
Result<
{ file: ResolvedFile; remapping?: Remapping | ResolvedUserRemapping },
ImportResolutionError
>
> {
const parsedDirectImport = parseNpmDirectImport(directImport);
if (parsedDirectImport === undefined) {
return {
success: false,
error: {
type: ImportResolutionErrorType.IMPORT_WITH_INVALID_NPM_SYNTAX,
fromFsPath: from.fsPath,
importPath,
},
};
}
const fromNpmPackage =
from.type === ResolvedFileType.NPM_PACKAGE_FILE
? from.package
: this.#hhProjectPackage;
const installationName = parsedDirectImport.package;
const dependencyResolution =
await this.#npmPackageGraph.resolveDependencyByInstallationName(
fromNpmPackage,
installationName,
);
if (dependencyResolution === undefined) {
return {
success: false,
error: {
type: ImportResolutionErrorType.IMPORT_OF_UNINSTALLED_PACKAGE,
fromFsPath: from.fsPath,
importPath,
installationName: parsedDirectImport.package,
},
};
}
const dependency = dependencyResolution.package;
const subpath = parsedDirectImport.subpath;
let resolvedSubpath: string | undefined;
if (
dependency.exports !== undefined ||
NPM_PACKAGES_WITH_SIMULATED_PACKAGE_EXPORTS.has(dependency.name)
) {
const dependencyWithPackageExports = {
...dependency,
exports: dependency.exports ?? {
// If someone imports with the full path, we want it to work
"./src/*.sol": "./src/*.sol",
// If they import assuming the typical remapping into src/, we also
// want it to work
"./*.sol": "./src/*.sol",
},
};
const pathResolutionResult = resolveSubpathWithPackageExports(
dependencyWithPackageExports,
subpath,
);
if (pathResolutionResult.success === false) {
return {
success: false,
error: {
type: ImportResolutionErrorType.IMPORT_OF_NON_EXPORTED_NPM_FILE,
fromFsPath: from.fsPath,
importPath,
...this.#buildResolvedFileReference({
npmPackage: dependency,
subpath,
}),
},
};
}
resolvedSubpath = pathResolutionResult.value;
}
const inputSourceName = sourceNamePathJoin(
dependency.inputSourceNameRoot,
resolvedSubpath ?? subpath,
);
// There are two types of remappings that we may use for npm imports.
//
// One is a generic remapping from fromNpmPackage to dependency using
// installationName.
//
// The other one, while more verbose, is a remapping specifically from
// forNpmPackage into inputSourceName, using directImport.
//
// We use the second one when applying package.exports, and it changes the
// subpath of the file.
const remapping =
resolvedSubpath !== undefined && resolvedSubpath !== subpath
? await this.#npmPackageGraph.generateRemappingIntoNpmFile(
fromNpmPackage,
directImport,
inputSourceName,
)
: dependencyResolution.generatedRemapping;
const existing = this.#resolvedFileByInputSourceName.get(inputSourceName);
if (existing !== undefined) {
return {
success: true,
value: {
file: existing,
remapping,
},
};
}
const relativePath = resolvedSubpath ?? subpath;
const relativeFsPathWithinPackage = sourceNamePathToFsPath(relativePath);
return this.#commonImportResolution({
from,
importPath,
npmPackage: dependency,
inputSourceName,
relativeFsPathWithinPackage,
subpath,
generatedRemapping: remapping,
packageExportsResolvedSubpath: resolvedSubpath,
});
}
/**
* Once the other methods selected which file to import, the rest of the logic
* is shared in this method.
*/
async #commonImportResolution({
from,
importPath,
npmPackage,
inputSourceName,
relativeFsPathWithinPackage,
subpath,
userRemapping,
generatedRemapping,
packageExportsResolvedSubpath,
}: {
from: ResolvedFile;
importPath: string;
npmPackage: ResolvedNpmPackage;
inputSourceName: string;
relativeFsPathWithinPackage: string;
subpath: string;
userRemapping?: ResolvedUserRemapping;
generatedRemapping?: Remapping;
packageExportsResolvedSubpath?: string;
}): Promise<
Result<
{ file: ResolvedFile; remapping?: Remapping | ResolvedUserRemapping },
ImportResolutionError
>
> {
const pathValidation = await validateFsPath(
npmPackage.rootFsPath,
relativeFsPathWithinPackage,
);
if (pathValidation.success === false) {
if (
pathValidation.error.type === PathValidationErrorType.DOES_NOT_EXIST
) {
return {
success: false,
error: {
type: ImportResolutionErrorType.IMPORT_DOES_NOT_EXIST,
fromFsPath: from.fsPath,
importPath,
...this.#buildResolvedFileReference({
npmPackage,
subpath,
userRemapping,
packageExportsResolvedSubpath,
}),
},
};
}
return {
success: false,
error: {
type: ImportResolutionErrorType.IMPORT_INVALID_CASING,
fromFsPath: from.fsPath,
importPath,
...this.#buildResolvedFileReference({
npmPackage,
subpath,
userRemapping,
packageExportsResolvedSubpath,
}),
correctCasing: fsPathToSourceNamePath(
pathValidation.error.correctCasing,
),
},
};
}
const fsPath = path.join(
npmPackage.rootFsPath,
relativeFsPathWithinPackage,
);
const resolvedFile = await this.#buildResolvedFile({
npmPackage,
fsPath,
inputSourceName,
});
this.#resolvedFileByInputSourceName.set(inputSourceName, resolvedFile);
return {
success: true,
value: {
file: resolvedFile,
// We only call this function with one of the two, but userRemappings
// have priority in general, so writing it this way is safer.
remapping: userRemapping ?? generatedRemapping,
},
};
}
async #tryToGenerateDirectLocalImportError({
from,
importPath,
}: {
from: ResolvedFile;
importPath: string;
}): Promise<ImportResolutionError | undefined> {
let baseDir = path.dirname(from.fsPath);
const firstDir = importPath.substring(0, importPath.indexOf("/"));
// If there's no directory separator, or the import is just a directory
// we don't suggest a remapping
if (firstDir === "" || firstDir.length === importPath.length - 1) {
return undefined;
}
while (baseDir.startsWith(from.package.rootFsPath)) {
const firstDirPath = path.join(baseDir, firstDir);
if (await exists(firstDirPath)) {
const baseDirSourceName = fsPathToSourceNamePath(
path.relative(from.package.rootFsPath, baseDir),
);
const context =
from.package === this.#hhProjectPackage
? baseDirSourceName !== ""
? baseDirSourceName + "/"
: ""
: sourceNamePathJoin(
from.package.inputSourceNameRoot,
baseDirSourceName + "/",
);
const prefix = firstDir + "/";
const target =
from.package === this.#hhProjectPackage
? prefix
: sourceNamePathJoin(from.package.inputSourceNameRoot, prefix);
return {
type: ImportResolutionErrorType.DIRECT_IMPORT_TO_LOCAL_FILE,
fromFsPath: from.fsPath,
importPath,
suggestedRemapping: formatRemapping({
context,
prefix,
target,
}),
};
}
baseDir = path.dirname(baseDir);
}
return undefined;
}
async #buildResolvedFile({
inputSourceName,
fsPath,
npmPackage,
}: {
inputSourceName: string;
fsPath: string;
npmPackage: ResolvedNpmPackage;
}): Promise<ResolvedFile> {
const content = await this.#readFileContent({ absolutePath: fsPath });
return {
type:
npmPackage === this.#hhProjectPackage
? ResolvedFileType.PROJECT_FILE
: ResolvedFileType.NPM_PACKAGE_FILE,
inputSourceName,
fsPath,
content,
package: npmPackage,
};
}
#buildResolvedFileReference({
npmPackage: targetNpmPackage,
subpath,
userRemapping,
packageExportsResolvedSubpath,
}: {
npmPackage: ResolvedNpmPackage;
subpath: string;
userRemapping?: ResolvedUserRemapping;
packageExportsResolvedSubpath?: string;
}): ResolvedFileReference {
return {
resolvedFileType:
targetNpmPackage === this.#hhProjectPackage
? ResolvedFileType.PROJECT_FILE
: ResolvedFileType.NPM_PACKAGE_FILE,
npmPackage: {
name: targetNpmPackage.name,
version: targetNpmPackage.version,
rootFsPath: targetNpmPackage.rootFsPath,
},
userRemapping:
userRemapping === undefined
? undefined
: this.#buildUserRemappingReference({ userRemapping }),
subpath,
packageExportsResolvedSubpath,
};
}
#buildUserRemappingReference({
userRemapping,
}: {
userRemapping: ResolvedUserRemapping;
}): UserRemappingReference {
return {
originalUserRemapping: userRemapping.originalFormat,
actualUserRemapping: formatRemapping(userRemapping),
remappingSource: userRemapping.source,
};
}
/**
* Returns the relative input source name from the npmPackage's input source name root.
* @param nmpPackage The package
* @param fileInputSourceName The file's input source name.
*/
#getRelativeSourceNamePath({
npmPackage: nmpPackage,
fileInputSourceName,
}: {
npmPackage: ResolvedNpmPackage;
fileInputSourceName: string;
}) {
return fileInputSourceName.substring(
nmpPackage.inputSourceNameRoot.length + 1,
);
}
#importResolutionErrorToNpmRootResolutionError(
npmModule: string,
error: ImportResolutionError,
): NpmRootResolutionError {
switch (error.type) {
/* c8 ignore start */
case ImportResolutionErrorType.ILLEGAL_RELATIVE_IMPORT: {
assertHardhatInvariant(
false,
"This could not happen, as we validated that the module name is a valid npm module",
);
}
case ImportResolutionErrorType.RELATIVE_IMPORT_CLASHES_WITH_USER_REMAPPING: {
assertHardhatInvariant(
false,
"This should never happen: An npm root file must not be a relative import",
);
}
/* c8 ignore end */
case ImportResolutionErrorType.IMPORT_WITH_INVALID_NPM_SYNTAX:
case ImportResolutionErrorType.IMPORT_WITH_WINDOWS_PATH_SEPARATORS:
case ImportResolutionErrorType.RELATIVE_IMPORT_INTO_NODE_MODULES: {
return {
type: RootResolutionErrorType.NPM_ROOT_FILE_NAME_WITH_INVALID_FORMAT,
npmModule,
};
}
case ImportResolutionErrorType.IMPORT_DOES_NOT_EXIST: {
assertHardhatInvariant(
error.npmPackage !== undefined,
"We should have a npm package if the import doesn't exist, as we know are doing an npm import",
);
return {
type: RootResolutionErrorType.NPM_ROOT_FILE_DOES_NOT_EXIST_WITHIN_ITS_PACKAGE,
npmModule,
userRemapping: error.userRemapping,
npmPackage: error.npmPackage,
resolvedFileType: error.resolvedFileType,
subpath: error.subpath,
packageExportsResolvedSubpath: error.packageExportsResolvedSubpath,
};
}
case ImportResolutionErrorType.IMPORT_INVALID_CASING: {
assertHardhatInvariant(
error.npmPackage !== undefined,
"We should have a npm package if the import doesn't exist, as we know are doing an npm import",
);
return {
type: RootResolutionErrorType.NPM_ROOT_FILE_WITH_INCORRECT_CASING,
npmModule,
correctCasing: error.correctCasing,
userRemapping: error.userRemapping,
npmPackage: error.npmPackage,
resolvedFileType: error.resolvedFileType,
subpath: error.subpath,
packageExportsResolvedSubpath: error.packageExportsResolvedSubpath,
};
}
case ImportResolutionErrorType.IMPORT_OF_UNINSTALLED_PACKAGE: {
return {
type: RootResolutionErrorType.NPM_ROOT_FILE_OF_UNINSTALLED_PACKAGE,
npmModule,
installationName: error.installationName,
};
}
case ImportResolutionErrorType.IMPORT_WITH_REMAPPING_ERRORS: {
return {
type: RootResolutionErrorType.NPM_ROOT_FILE_RESOLUTION_WITH_REMAPPING_ERRORS,
npmModule,
remappingErrors: error.remappingErrors,
};
}
case ImportResolutionErrorType.IMPORT_OF_NON_EXPORTED_NPM_FILE: {
return {
type: RootResolutionErrorType.NPM_ROOT_FILE_NON_EXPORTED_FILE,
npmModule,
userRemapping: error.userRemapping,
npmPackage: error.npmPackage,
resolvedFileType: error.resolvedFileType,
subpath: error.subpath,
packageExportsResolvedSubpath: error.packageExportsResolvedSubpath,
};
}
case ImportResolutionErrorType.DIRECT_IMPORT_TO_LOCAL_FILE: {
return {
type: RootResolutionErrorType.NPM_ROOT_FILE_RESOLVES_TO_PROJECT_FILE,
npmModule,
userRemapping: undefined,
resolvedFileFsPath: path.join(
this.#hhProjectPackage.rootFsPath,
npmModule,
),
};
}
}
}
/**
* Reads and analyzes the file at the given absolute path.
*/
async #readFileContent({
absolutePath,
}: {
absolutePath: string;
}): Promise<FileContent> {
const text = await this.#readUtf8File(absolutePath);
const { imports, versionPragmas } = analyze(text);
return {
text,
importPaths: imports,
versionPragmas,
};
}
}