diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d2679a695..49bc5c7f08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1530,6 +1530,9 @@ importers: p-map: specifier: ^7.0.2 version: 7.0.2 + resolve.exports: + specifier: ^2.0.3 + version: 2.0.3 semver: specifier: ^7.6.3 version: 7.6.3 @@ -6191,6 +6194,10 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + resolve@1.1.7: resolution: {integrity: sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==} @@ -11381,6 +11388,8 @@ snapshots: resolve-pkg-maps@1.0.0: {} + resolve.exports@2.0.3: {} + resolve@1.1.7: {} resolve@1.17.0: diff --git a/v-next/hardhat-errors/src/descriptors.ts b/v-next/hardhat-errors/src/descriptors.ts index 027cca11c6..a8c0e12f48 100644 --- a/v-next/hardhat-errors/src/descriptors.ts +++ b/v-next/hardhat-errors/src/descriptors.ts @@ -954,7 +954,7 @@ Remaining test suites: {suites}`, websiteTitle: "Imported file doesn't exist", websiteDescription: `An imported file doesn't exist.`, }, - IMPORTED_FILE_WITH_ICORRECT_CASING: { + IMPORTED_FILE_WITH_INCORRECT_CASING: { number: 1203, messageTemplate: 'The import "{importPath} from "{from}" exists, but its casing is incorrect. The correct casing is "{correctCasing}".', @@ -1195,6 +1195,12 @@ Please check Hardhat's output for more details.`, websiteTitle: "Invalid solcjs compiler", websiteDescription: `Hardhat successfully downloaded a WASM version of solc {version} but it is invalid. The compile function is missing.`, }, + RESOLVE_NOT_EXPORTED_NPM_FILE: { + number: 1232, + messageTemplate: `You are tying to resolve the npm file "{module}", but it's not exported by its package`, + websiteTitle: "Resolution of not-exported npm file", + websiteDescription: `You are tying to resolve an npm file that is not exported by its package.`, + }, }, VIEM: { NETWORK_NOT_FOUND: { diff --git a/v-next/hardhat-utils/src/package.ts b/v-next/hardhat-utils/src/package.ts index 2c7553b7ca..8b07ab0a6c 100644 --- a/v-next/hardhat-utils/src/package.ts +++ b/v-next/hardhat-utils/src/package.ts @@ -1,3 +1,5 @@ +import fs from "node:fs"; +import { createRequire } from "node:module"; import path from "node:path"; import { ensureError } from "./error.js"; @@ -7,6 +9,7 @@ import { } from "./errors/package.js"; import { findUp, readJsonFile } from "./fs.js"; import { getFilePath } from "./internal/package.js"; +import { ensureTrailingSlash } from "./string.js"; /** * The structure of a `package.json` file. This is a subset of the actual @@ -102,6 +105,34 @@ export async function findClosestPackageRoot( return path.dirname(packageJsonPath); } +/** + * Finds the package json for a given package + * @param from the absolute path from where to start the search + * @param packageName the name of the package to find + * @returns the absolute real path (resolved symlinks) of the package.json + */ +export async function findPackageJson( + from: string, + packageName: string, +): Promise { + const require = createRequire(ensureTrailingSlash(from)); + + const lookupPaths = require.resolve.paths(packageName) ?? []; + + const pathToTest = [...packageName.split("/"), "package.json"]; + + for (const lookupPath of lookupPaths) { + const packageJsonPath = path.join(lookupPath, ...pathToTest); + + try { + await fs.promises.access(packageJsonPath, fs.constants.R_OK); + return await fs.promises.realpath(packageJsonPath); + } catch (error) { + continue; + } + } +} + export { PackageJsonNotFoundError, PackageJsonReadError, diff --git a/v-next/hardhat-utils/src/string.ts b/v-next/hardhat-utils/src/string.ts index 736537f5d7..0cd96d7c92 100644 --- a/v-next/hardhat-utils/src/string.ts +++ b/v-next/hardhat-utils/src/string.ts @@ -60,3 +60,10 @@ export function camelToSnakeCase(str: string): string { export function camelToKebabCase(str: string): string { return str.replace(/[A-Z0-9]/g, (match) => `-${match.toLowerCase()}`); } + +/** + * Ensures a string ends with a slash. + */ +export function ensureTrailingSlash(path: string): string { + return path.endsWith("/") ? path : path + "/"; +} diff --git a/v-next/hardhat/package.json b/v-next/hardhat/package.json index 7de30ed8aa..e0b70f3734 100644 --- a/v-next/hardhat/package.json +++ b/v-next/hardhat/package.json @@ -32,7 +32,8 @@ "./types/tasks": "./dist/src/types/tasks.js", "./types/user-interruptions": "./dist/src/types/user-interruptions.js", "./types/utils": "./dist/src/types/utils.js", - "./types/solidity": "./dist/src/types/solidity.js" + "./types/solidity": "./dist/src/types/solidity.js", + "./console.sol": "./console.sol" }, "keywords": [ "ethereum", @@ -97,6 +98,7 @@ "ethereum-cryptography": "^2.2.1", "micro-eth-signer": "^0.13.0", "p-map": "^7.0.2", + "resolve.exports": "^2.0.3", "semver": "^7.6.3", "tsx": "^4.11.0", "ws": "^8.18.0", diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/debug-utils.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/debug-utils.ts index 537859c13d..62d02e11e6 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/debug-utils.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/debug-utils.ts @@ -16,7 +16,7 @@ export function printDependencyGraphAndRemappingsSummary( const rootRepresentations: string[] = []; for (const [rootFile, resolvedFile] of roots.entries()) { - if (resolvedFile.type === ResolvedFileType.NPM_PACKGE_FILE) { + if (resolvedFile.type === ResolvedFileType.NPM_PACKAGE_FILE) { rootRepresentations.push(`- ${rootFile} -> ${resolvedFile.sourceName} ${resolvedFile.fsPath}`); } else { diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/resolver/dependency-resolver.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/resolver/dependency-resolver.ts index 9860335be1..a40ef8df26 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/resolver/dependency-resolver.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/resolver/dependency-resolver.ts @@ -21,10 +21,13 @@ import { readJsonFile, readUtf8File, } from "@ignored/hardhat-vnext-utils/fs"; -import { findClosestPackageJson } from "@ignored/hardhat-vnext-utils/package"; +import { + findClosestPackageJson, + findPackageJson, +} from "@ignored/hardhat-vnext-utils/package"; import { shortenPath } from "@ignored/hardhat-vnext-utils/path"; -import { ResolutionError, resolve } from "@ignored/hardhat-vnext-utils/resolve"; import { analyze } from "@nomicfoundation/solidity-analyzer"; +import * as resolve from "resolve.exports"; import { ResolvedFileType } from "../../../../../types/solidity/resolved-file.js"; import { AsyncMutex } from "../../../../core/async-mutex.js"; @@ -123,7 +126,7 @@ export class ResolverImplementation implements Resolver { * and the user remaps `dep/=nope/`, it could break `foo`'s import. * * To avoid this situation we set all the prefixes that `foo` needs unaffected - * by the user remapping, with a higher presedence than user remappings. + * by the user remapping, with a higher precedence than user remappings. */ readonly #localPrefixesByPackage: Map> = new Map(); @@ -138,7 +141,6 @@ export class ResolverImplementation implements Resolver { * * @param projectRoot The absolute path to the Hardhat project root. * @param userRemappingStrings The remappings provided by the user. - * @param workingDirectory The absolute path to the working directory. */ public static async create( projectRoot: string, @@ -182,7 +184,7 @@ export class ResolverImplementation implements Resolver { // We first check if the file has already been resolved. // - // Note that it may have recevied the right path, but with the wrong + // 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. // @@ -285,11 +287,14 @@ export class ResolverImplementation implements Resolver { "Resolving a local file as if it were an npm module", ); + const subpath = parsedNpmModule.subpath; + const resolvedSubpath = resolveSubpath(npmPackage, subpath); + let trueCaseFsPath: string; try { trueCaseFsPath = await getFileTrueCase( npmPackage.rootFsPath, - parsedNpmModule.path, + resolvedSubpath, ); } catch (error) { ensureError(error, FileNotFoundError); @@ -306,7 +311,7 @@ export class ResolverImplementation implements Resolver { const sourceName = sourceNamePathJoin( npmPackageToRootSourceName(npmPackage.name, npmPackage.version), - fsPathToSourceNamePath(trueCaseFsPath), + fsPathToSourceNamePath(subpath), ); const resolvedWithTheRightCasing = @@ -321,7 +326,7 @@ export class ResolverImplementation implements Resolver { const fsPath = path.join(npmPackage.rootFsPath, trueCaseFsPath); const resolvedFile: NpmPackageResolvedFile = { - type: ResolvedFileType.NPM_PACKGE_FILE, + type: ResolvedFileType.NPM_PACKAGE_FILE, sourceName, fsPath, content: await readFileContent(fsPath), @@ -357,7 +362,7 @@ export class ResolverImplementation implements Resolver { importPath, ); - if (from.type === ResolvedFileType.NPM_PACKGE_FILE) { + if (from.type === ResolvedFileType.NPM_PACKAGE_FILE) { if (!directImport.startsWith(from.package.rootSourceName)) { throw new HardhatError( HardhatError.ERRORS.SOLIDITY.ILLEGAL_PACKAGE_IMPORT, @@ -388,7 +393,7 @@ export class ResolverImplementation implements Resolver { directImport, }); - case ResolvedFileType.NPM_PACKGE_FILE: + case ResolvedFileType.NPM_PACKAGE_FILE: return this.#resolveImportFromNpmPackageFile({ from, importPath, @@ -511,7 +516,7 @@ export class ResolverImplementation implements Resolver { // 4. Resolving an import from an npm package to one of its own files with a // direct import — This case is different from 3, as without especial care // it could be affected by one of the user remappings. - // 5. Resolving an import to a different npm package using our own remmapings + // 5. Resolving an import to a different npm package using our own remappings /** * Resolves an import from a project file. @@ -739,7 +744,9 @@ export class ResolverImplementation implements Resolver { // If we import a file through npm and end up in the Hardhat project, // we are going to remap the importPackageName to "", so that the path // section of the parsed direct import should be the relative path. - fsPathWithinTheProject: sourceNamePathToFsPath(parsedDirectImport.path), + fsPathWithinTheProject: sourceNamePathToFsPath( + parsedDirectImport.subpath, + ), }); } @@ -747,7 +754,7 @@ export class ResolverImplementation implements Resolver { from, importPath, importedPackage: dependency, - fsPathWithinThePackage: sourceNamePathToFsPath(parsedDirectImport.path), + subpath: sourceNamePathToFsPath(parsedDirectImport.subpath), }); } @@ -844,9 +851,10 @@ export class ResolverImplementation implements Resolver { return existing as NpmPackageResolvedFile; } - const relativeFileFsPath = sourceNamePathToFsPath( + const subpath = sourceNamePathToFsPath( path.relative(remapping.targetNpmPackage.rootSourceName, directImport), ); + const resolvedSubpath = resolveSubpath(remapping.targetNpmPackage, subpath); // We don't add the dependency to `this.#dependencyMaps` because we // don't need a new remapping for this package, as it's already @@ -855,17 +863,17 @@ export class ResolverImplementation implements Resolver { await this.#validateExistanceAndCasingOfImport({ from, importPath, - relativeFsPathToValidate: relativeFileFsPath, + relativeFsPathToValidate: resolvedSubpath, absoluteFsPathToValidateFrom: remapping.targetNpmPackage.rootFsPath, }); const fsPath = path.join( remapping.targetNpmPackage.rootFsPath, - relativeFileFsPath, + resolvedSubpath, ); const resolvedFile: NpmPackageResolvedFile = { - type: ResolvedFileType.NPM_PACKGE_FILE, + type: ResolvedFileType.NPM_PACKAGE_FILE, sourceName, fsPath, content: await readFileContent(fsPath), @@ -918,7 +926,7 @@ export class ResolverImplementation implements Resolver { const filePath = path.join(from.package.rootFsPath, relativePath); const resolvedFile: NpmPackageResolvedFile = { - type: ResolvedFileType.NPM_PACKGE_FILE, + type: ResolvedFileType.NPM_PACKAGE_FILE, sourceName, fsPath: filePath, content: await readFileContent(filePath), @@ -973,7 +981,7 @@ export class ResolverImplementation implements Resolver { const fsPath = path.join(from.package.rootFsPath, relativeFsPath); const resolvedFile: NpmPackageResolvedFile = { - type: ResolvedFileType.NPM_PACKGE_FILE, + type: ResolvedFileType.NPM_PACKAGE_FILE, sourceName, fsPath, content: await readFileContent(fsPath), @@ -995,7 +1003,7 @@ export class ResolverImplementation implements Resolver { * @param from The file from which the import is being resolved. * @param importPath The import path, as written in the source code. * @param importedPackage The NpmPackage that is being imported. - * @param pathWithinThePackage The path to the file to import, within the + * @param subpath The path to the file to import, within the * package. That means, after parsing the direct import, and stripping the * package part. */ @@ -1003,16 +1011,16 @@ export class ResolverImplementation implements Resolver { from, importPath, importedPackage, - fsPathWithinThePackage, + subpath, }: { from: ResolvedFile; importPath: string; importedPackage: ResolvedNpmPackage; - fsPathWithinThePackage: string; + subpath: string; }): Promise { const sourceName = - importedPackage.rootSourceName + - fsPathToSourceNamePath(fsPathWithinThePackage); + importedPackage.rootSourceName + fsPathToSourceNamePath(subpath); + const existing = this.#resolvedFileBySourceName.get(sourceName); if (existing !== undefined) { /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- @@ -1020,20 +1028,19 @@ export class ResolverImplementation implements Resolver { return existing as NpmPackageResolvedFile; } + const resolvedSubpath = resolveSubpath(importedPackage, subpath); + await this.#validateExistanceAndCasingOfImport({ from, importPath, - relativeFsPathToValidate: fsPathWithinThePackage, + relativeFsPathToValidate: resolvedSubpath, absoluteFsPathToValidateFrom: importedPackage.rootFsPath, }); - const fsPath = path.join( - importedPackage.rootFsPath, - fsPathWithinThePackage, - ); + const fsPath = path.join(importedPackage.rootFsPath, resolvedSubpath); const resolvedFile: NpmPackageResolvedFile = { - type: ResolvedFileType.NPM_PACKGE_FILE, + type: ResolvedFileType.NPM_PACKAGE_FILE, sourceName, fsPath, content: await readFileContent(fsPath), @@ -1081,30 +1088,15 @@ export class ResolverImplementation implements Resolver { // We also need to figure out a way to test this inside the monorepo, // without the package `hardhat` in the top-level `node_modules` folder // interfering with the resolution. - const packageJsonResolution = + + const packageJsonPath = packageName === "hardhat" - ? ({ - success: true, - absolutePath: await findClosestPackageJson(import.meta.dirname), - } as const) - : resolve(packageName + "/package.json", baseResolutionDirectory); - - if (packageJsonResolution.success === false) { - if (packageJsonResolution.error === ResolutionError.MODULE_NOT_FOUND) { - throw new HardhatError( - HardhatError.ERRORS.SOLIDITY.NPM_DEPEDNDENCY_NOT_INSTALLED, - { - from: - from === PROJECT_ROOT_SENTINEL - ? "your project" - : `"${shortenPath(from.rootFsPath)}"`, - packageName, - }, - ); - } + ? await findClosestPackageJson(import.meta.dirname) + : await findPackageJson(baseResolutionDirectory, packageName); + if (packageJsonPath === undefined) { throw new HardhatError( - HardhatError.ERRORS.SOLIDITY.NPM_DEPEDNDENCY_USES_EXPORTS, + HardhatError.ERRORS.SOLIDITY.NPM_DEPEDNDENCY_NOT_INSTALLED, { from: from === PROJECT_ROOT_SENTINEL @@ -1115,16 +1107,16 @@ export class ResolverImplementation implements Resolver { ); } - const packageJsonPath = packageJsonResolution.absolutePath; - if (isPackageJsonFromProject(packageJsonPath, this.#projectRoot)) { dependenciesMap.set(packageName, PROJECT_ROOT_SENTINEL); return PROJECT_ROOT_SENTINEL; } - const packageJson = await readJsonFile<{ name: string; version: string }>( - packageJsonPath, - ); + const packageJson = await readJsonFile<{ + name: string; + version: string; + exports?: resolve.Exports; + }>(packageJsonPath); const name = packageJson.name; const version = isPackageJsonFromMonorepo( @@ -1137,6 +1129,7 @@ export class ResolverImplementation implements Resolver { const npmPackage: ResolvedNpmPackage = { name, version, + exports: packageJson.exports, rootFsPath: path.dirname(packageJsonPath), rootSourceName: npmPackageToRootSourceName(name, version), }; @@ -1191,19 +1184,6 @@ export class ResolverImplementation implements Resolver { ); } - if ( - HardhatError.isHardhatError( - error, - HardhatError.ERRORS.SOLIDITY.NPM_DEPEDNDENCY_USES_EXPORTS, - ) - ) { - throw new HardhatError( - HardhatError.ERRORS.SOLIDITY.IMPORTED_NPM_DEPENDENCY_THAT_USES_EXPORTS, - { from: shortenPath(from.fsPath), importPath }, - error, - ); - } - throw error; } } @@ -1298,7 +1278,7 @@ export class ResolverImplementation implements Resolver { if (relativeFsPathToValidate !== trueCaseFsPath) { throw new HardhatError( - HardhatError.ERRORS.SOLIDITY.IMPORTED_FILE_WITH_ICORRECT_CASING, + HardhatError.ERRORS.SOLIDITY.IMPORTED_FILE_WITH_INCORRECT_CASING, { importPath, from: shortenPath(from.fsPath), @@ -1315,7 +1295,7 @@ export class ResolverImplementation implements Resolver { #parseNpmDirectImport(directImport: string): | { package: string; - path: string; + subpath: string; } | undefined { // NOTE: We assume usage of path.posix.sep in the direct import @@ -1337,7 +1317,7 @@ export class ResolverImplementation implements Resolver { // path can be later safely treated as a system aware path const parsedPath = match.groups.path.replaceAll(path.posix.sep, path.sep); - return { package: match.groups.package, path: parsedPath }; + return { package: match.groups.package, subpath: parsedPath }; } } @@ -1378,30 +1358,18 @@ async function validateAndResolveUserRemapping( const { packageName, packageVersion } = parsed; - const dependencyPackageJsonResolution = resolve( - `${packageName}/package.json`, + const dependencyPackageJsonPath = await findPackageJson( projectRoot, + packageName, ); - if (dependencyPackageJsonResolution.success === false) { - if ( - dependencyPackageJsonResolution.error === ResolutionError.MODULE_NOT_FOUND - ) { - throw new HardhatError( - HardhatError.ERRORS.SOLIDITY.REMAPPING_TO_UNINSTALLED_PACKAGE, - { remapping: remappingString, package: packageName }, - ); - } - + if (dependencyPackageJsonPath === undefined) { throw new HardhatError( - HardhatError.ERRORS.SOLIDITY.REMAPPING_TO_PACKAGE_USING_EXPORTS, + HardhatError.ERRORS.SOLIDITY.REMAPPING_TO_UNINSTALLED_PACKAGE, { remapping: remappingString, package: packageName }, ); } - const dependencyPackageJsonPath = - dependencyPackageJsonResolution.absolutePath; - if (isPackageJsonFromMonorepo(dependencyPackageJsonPath, projectRoot)) { if (packageVersion !== "local") { throw new HardhatError( @@ -1422,10 +1390,18 @@ async function validateAndResolveUserRemapping( ); } + const npmPackage: ResolvedNpmPackage = { + name: packageName, + version: packageVersion, + rootFsPath: path.dirname(dependencyPackageJsonPath), + rootSourceName: npmPackageToRootSourceName(packageName, packageVersion), + }; + if (isPackageJsonFromNpmPackage(dependencyPackageJsonPath)) { - const dependencyPackageJson = await readJsonFile<{ version: string }>( - dependencyPackageJsonPath, - ); + const dependencyPackageJson = await readJsonFile<{ + version: string; + exports: resolve.Exports; + }>(dependencyPackageJsonPath); if (dependencyPackageJson.version !== packageVersion) { throw new HardhatError( @@ -1438,14 +1414,9 @@ async function validateAndResolveUserRemapping( }, ); } - } - const npmPackage: ResolvedNpmPackage = { - name: packageName, - version: packageVersion, - rootFsPath: path.dirname(dependencyPackageJsonPath), - rootSourceName: npmPackageToRootSourceName(packageName, packageVersion), - }; + npmPackage.exports = dependencyPackageJson.exports; + } return { ...remapping, @@ -1560,3 +1531,39 @@ async function readFileContent(absolutePath: string): Promise { versionPragmas, }; } + +/** + * Resolves a subpath for a given package, when it uses package#exports + * @param npmPackage + * @param subpath + * @returns + */ +function resolveSubpath( + npmPackage: ResolvedNpmPackage, + subpath: string, +): string { + if (npmPackage.exports === undefined) { + return subpath; + } + + try { + const resolveOutput = resolve.exports(npmPackage, subpath); + + assertHardhatInvariant( + resolveOutput !== undefined, + "resolve.exports should always return a result when package.exports exist", + ); + + const resolvedSubpath = resolveOutput[0].slice(2); // skip the leading './' + + return resolvedSubpath.replace(/\/|\\/g, path.sep); // use fs path separator + } catch (error) { + ensureError(error, Error); + + throw new HardhatError( + HardhatError.ERRORS.SOLIDITY.RESOLVE_NOT_EXPORTED_NPM_FILE, + { module: `${npmPackage.name}/${subpath}` }, + error, + ); + } +} diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/root-paths-utils.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/root-paths-utils.ts index 2c63a8dd21..b515d9290d 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/root-paths-utils.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/root-paths-utils.ts @@ -29,7 +29,7 @@ export type ParsedRootPath = { npmPath: string } | { fsPath: string }; export function parseRootPath( rootPath: string, ): { npmPath: string } | { fsPath: string } { - if (rootPath.startsWith("npm:")) { + if (isNpmRootPath(rootPath)) { return { npmPath: rootPath.substring(4) }; } @@ -73,7 +73,7 @@ export function formatRootPath( publicSourceName: string, rootFile: ResolvedFile, ): string { - if (rootFile.type !== ResolvedFileType.NPM_PACKGE_FILE) { + if (rootFile.type !== ResolvedFileType.NPM_PACKAGE_FILE) { return publicSourceName; } diff --git a/v-next/hardhat/src/types/solidity/resolved-file.ts b/v-next/hardhat/src/types/solidity/resolved-file.ts index be8512f956..97894d25d5 100644 --- a/v-next/hardhat/src/types/solidity/resolved-file.ts +++ b/v-next/hardhat/src/types/solidity/resolved-file.ts @@ -1,3 +1,5 @@ +import type { Exports } from "resolve.exports"; + /** * The representation of an npm package. */ @@ -12,6 +14,11 @@ export interface ResolvedNpmPackage { */ version: string; + /** + * The exports of the package. + */ + exports?: Exports; + /** * The path to the package's root directory. */ @@ -35,7 +42,7 @@ export interface ResolvedNpmPackage { */ export enum ResolvedFileType { PROJECT_FILE = "PROJECT_FILE", - NPM_PACKGE_FILE = "NPM_PACKAGE_FILE", + NPM_PACKAGE_FILE = "NPM_PACKAGE_FILE", } /** @@ -64,7 +71,7 @@ export interface ProjectResolvedFile { * A file that's part of an npm package. */ export interface NpmPackageResolvedFile { - type: ResolvedFileType.NPM_PACKGE_FILE; + type: ResolvedFileType.NPM_PACKAGE_FILE; /** * The source of an npm package file is `npm/@/`. diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/dependency-resolver.ts b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/dependency-resolver.ts index dbacf0e952..fc86c1cfb3 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/dependency-resolver.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/dependency-resolver.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-shadow -- test cases can overwrite higher scope variables */ import type { Resolver } from "../../../../../../src/internal/builtin-plugins/solidity/build-system/resolver/types.js"; import type { ResolvedFile, @@ -63,26 +64,26 @@ function assertNpmPackageResolvedFile( pacakge: Omit, packagePathFromTestFixturesRoot: string, filePathFromPackageRoot: string, + filePathFromTestFixturesRoot = path.join( + packagePathFromTestFixturesRoot, + filePathFromPackageRoot, + ), ): asserts resolvedFile is NpmPackageResolvedFile { assert.ok( - resolvedFile.type === ResolvedFileType.NPM_PACKGE_FILE, + resolvedFile.type === ResolvedFileType.NPM_PACKAGE_FILE, `Resolved file ${resolvedFile.fsPath} is not an npm file`, ); - const filePathFromTestFixturesRoot = path.join( - packagePathFromTestFixturesRoot, - filePathFromPackageRoot, - ); - const packageRootPath = path.join( TEST_FIXTURES_ROOT, packagePathFromTestFixturesRoot, ); - assert.deepEqual(resolvedFile.package, { - ...pacakge, - rootFsPath: packageRootPath, - }); + assert.equal(resolvedFile.package.name, pacakge.name); + assert.equal(resolvedFile.package.version, pacakge.version); + assert.equal(resolvedFile.package.rootSourceName, pacakge.rootSourceName); + assert.equal(resolvedFile.package.rootFsPath, packageRootPath); + assert.equal( resolvedFile.sourceName, pacakge.rootSourceName + fsPathToSourceNamePath(filePathFromPackageRoot), @@ -228,7 +229,8 @@ describe("Resolver", () => { await assertRejectsWithHardhatError( resolver.resolveImport(contractsFileSol, "../file.sol"), - HardhatError.ERRORS.SOLIDITY.IMPORTED_FILE_WITH_ICORRECT_CASING, + HardhatError.ERRORS.SOLIDITY + .IMPORTED_FILE_WITH_INCORRECT_CASING, { importPath: "../file.sol", from: path.join("contracts", "File.sol"), @@ -286,7 +288,8 @@ describe("Resolver", () => { await assertRejectsWithHardhatError( resolver.resolveImport(contractsFileSol, "contracts/file2.sol"), - HardhatError.ERRORS.SOLIDITY.IMPORTED_FILE_WITH_ICORRECT_CASING, + HardhatError.ERRORS.SOLIDITY + .IMPORTED_FILE_WITH_INCORRECT_CASING, { importPath: "contracts/file2.sol", from: path.join("contracts", "File.sol"), @@ -345,15 +348,19 @@ describe("Resolver", () => { "hardhat/console.sol", ); - assert.deepEqual(consoleSol.type, ResolvedFileType.NPM_PACKGE_FILE); - assert.deepEqual(consoleSol.package, { - name: "@ignored/hardhat-vnext", - version: "local", // The test considers it part of the monorepo, because it's the same package - rootSourceName: "npm/@ignored/hardhat-vnext@local/", - rootFsPath: await getRealPath( + assert.equal(consoleSol.type, ResolvedFileType.NPM_PACKAGE_FILE); + assert.equal(consoleSol.package.name, "@ignored/hardhat-vnext"); + assert.equal(consoleSol.package.version, "local"); + assert.equal( + consoleSol.package.rootSourceName, + "npm/@ignored/hardhat-vnext@local/", + ); + assert.equal( + consoleSol.package.rootFsPath, + await getRealPath( path.join(import.meta.dirname, "../../../../../.."), ), - }); + ); const hardhatFile = await resolver.resolveImport( contractsFileSol, @@ -386,18 +393,6 @@ describe("Resolver", () => { ); }); - it("Should fail if the package uses package.json#exports", async () => { - await assertRejectsWithHardhatError( - resolver.resolveImport(contractsFileSol, "exports/File.sol"), - HardhatError.ERRORS.SOLIDITY - .IMPORTED_NPM_DEPENDENCY_THAT_USES_EXPORTS, - { - from: path.join("contracts", "File.sol"), - importPath: "exports/File.sol", - }, - ); - }); - it("Should validate that the files exist with the right casing", async () => { await assertRejectsWithHardhatError( resolver.resolveImport(contractsFileSol, "dependency/nope.sol"), @@ -410,7 +405,7 @@ describe("Resolver", () => { await assertRejectsWithHardhatError( resolver.resolveImport(contractsFileSol, "dependency/file.sol"), - HardhatError.ERRORS.SOLIDITY.IMPORTED_FILE_WITH_ICORRECT_CASING, + HardhatError.ERRORS.SOLIDITY.IMPORTED_FILE_WITH_INCORRECT_CASING, { from: path.join("contracts", "File.sol"), importPath: "dependency/file.sol", @@ -925,4 +920,348 @@ describe("Resolver", () => { }); }); }); + + describe("NPM with package exports handling", function () { + describe("resolving dependenciesAsRoot for packages that use exports", function () { + it("resolves the exported dependency contract correctly", async () => { + const resolver = await ResolverImplementation.create( + FIXTURE_HARDHAT_PROJECT_ROOT, + [], + ); + const file = await resolver.resolveNpmDependencyFileAsRoot( + "exports/Exported.sol", + ); + + assertNpmPackageResolvedFile( + file, + { + name: "exports", + version: "3.0.0", + rootSourceName: "npm/exports@3.0.0/", + }, + "monorepo/node_modules/exports", + "Exported.sol", + "monorepo/node_modules/exports/contracts/Exported.sol", + ); + + assert.equal(file.sourceName, "npm/exports@3.0.0/Exported.sol"); + }); + + it("throws RESOLVE_NON_EXISTENT_NPM_FILE when trying to use the real path instead of the exported path", async () => { + const resolver = await ResolverImplementation.create( + FIXTURE_HARDHAT_PROJECT_ROOT, + [], + ); + + await assertRejectsWithHardhatError( + resolver.resolveNpmDependencyFileAsRoot( + "exports/contracts/Exported.sol", + ), + HardhatError.ERRORS.SOLIDITY.RESOLVE_NON_EXISTENT_NPM_FILE, + { + module: "exports/contracts/Exported.sol", + }, + ); + }); + + it("throws RESOLVE_NON_EXISTENT_NPM_FILE when trying to use a path that resolves exports but the file doesnt exist", async () => { + const resolver = await ResolverImplementation.create( + FIXTURE_HARDHAT_PROJECT_ROOT, + [], + ); + + await assertRejectsWithHardhatError( + resolver.resolveNpmDependencyFileAsRoot( + "exports/ResolvesButDoesntExist.sol", + ), + HardhatError.ERRORS.SOLIDITY.RESOLVE_NON_EXISTENT_NPM_FILE, + { + module: "exports/ResolvesButDoesntExist.sol", + }, + ); + }); + + it("throws RESOLVE_NOT_EXPORTED_NPM_FILE for non-exported files", async () => { + const resolver = await ResolverImplementation.create( + FIXTURE_HARDHAT_PROJECT_ROOT, + [], + ); + + await assertRejectsWithHardhatError( + resolver.resolveNpmDependencyFileAsRoot("exports/NotExported"), + HardhatError.ERRORS.SOLIDITY.RESOLVE_NOT_EXPORTED_NPM_FILE, + { + module: "exports/NotExported", // not using .sol, because that would match the *.sol rule + }, + ); + }); + }); + + describe("resolving direct imports to npm packages that use exports", function () { + let file: ResolvedFile; + let resolver: Resolver; + + beforeEach(async () => { + resolver = await ResolverImplementation.create( + FIXTURE_HARDHAT_PROJECT_ROOT, + [], + ); + file = await resolver.resolveProjectFile( + path.resolve(FIXTURE_HARDHAT_PROJECT_ROOT, "contracts/File.sol"), + ); + }); + + describe("without remappings", function () { + it("resolves a file that uses a single export", async () => { + const resolvedFile = await resolver.resolveImport( + file, + "exports/SingleExport.sol", + ); + + assertNpmPackageResolvedFile( + resolvedFile, + { + name: "exports", + version: "3.0.0", + rootSourceName: "npm/exports@3.0.0/", + }, + "monorepo/node_modules/exports", + "SingleExport.sol", + "monorepo/node_modules/exports/contracts2/SingleExport.sol", + ); + + assert.equal( + resolvedFile.sourceName, + "npm/exports@3.0.0/SingleExport.sol", + ); + }); + + it("resolves a file that uses wildcard export", async () => { + const resolvedFile = await resolver.resolveImport( + file, + "exports/Exported.sol", + ); + + assertNpmPackageResolvedFile( + resolvedFile, + { + name: "exports", + version: "3.0.0", + rootSourceName: "npm/exports@3.0.0/", + }, + "monorepo/node_modules/exports", + "Exported.sol", + "monorepo/node_modules/exports/contracts/Exported.sol", + ); + + assert.equal( + resolvedFile.sourceName, + "npm/exports@3.0.0/Exported.sol", + ); + }); + + it("requires correct casing", async () => { + await assertRejectsWithHardhatError( + resolver.resolveImport(file, "exports/exported.sol"), + HardhatError.ERRORS.SOLIDITY.IMPORTED_FILE_WITH_INCORRECT_CASING, + { + importPath: "exports/exported.sol", + from: path.join("contracts", "File.sol"), + correctCasing: "contracts/Exported.sol", + }, + ); + }); + + it("requires the file to be exported", async () => { + await assertRejectsWithHardhatError( + resolver.resolveImport(file, "exports/NotExported"), + HardhatError.ERRORS.SOLIDITY.RESOLVE_NOT_EXPORTED_NPM_FILE, + { module: "exports/NotExported" }, + ); + }); + + describe("exports that use conditions", function () { + it("resolves with the 'node' condition", async () => { + const resolvedFile = await resolver.resolveImport( + file, + "exports/Condition-node.sol", + ); + + assertNpmPackageResolvedFile( + resolvedFile, + { + name: "exports", + version: "3.0.0", + rootSourceName: "npm/exports@3.0.0/", + }, + "monorepo/node_modules/exports", + "Condition-node.sol", + "monorepo/node_modules/exports/contracts4/Condition-node.sol", + ); + + assert.equal( + resolvedFile.sourceName, + "npm/exports@3.0.0/Condition-node.sol", + ); + }); + + it("resolves with the 'import' condition", async () => { + const resolvedFile = await resolver.resolveImport( + file, + "exports/Condition-import.sol", + ); + + assertNpmPackageResolvedFile( + resolvedFile, + { + name: "exports", + version: "3.0.0", + rootSourceName: "npm/exports@3.0.0/", + }, + "monorepo/node_modules/exports", + "Condition-import.sol", + "monorepo/node_modules/exports/contracts4/Condition-import.sol", + ); + + assert.equal( + resolvedFile.sourceName, + "npm/exports@3.0.0/Condition-import.sol", + ); + }); + + it("resolves with the 'default' condition", async () => { + const resolvedFile = await resolver.resolveImport( + file, + "exports/Condition-default.sol", + ); + + assertNpmPackageResolvedFile( + resolvedFile, + { + name: "exports", + version: "3.0.0", + rootSourceName: "npm/exports@3.0.0/", + }, + "monorepo/node_modules/exports", + "Condition-default.sol", + "monorepo/node_modules/exports/contracts4/Condition-default.sol", + ); + + assert.equal( + resolvedFile.sourceName, + "npm/exports@3.0.0/Condition-default.sol", + ); + }); + + it("resolves with nested supported conditions", async () => { + const resolvedFile = await resolver.resolveImport( + file, + "exports/Condition-nested.sol", + ); + + assertNpmPackageResolvedFile( + resolvedFile, + { + name: "exports", + version: "3.0.0", + rootSourceName: "npm/exports@3.0.0/", + }, + "monorepo/node_modules/exports", + "Condition-nested.sol", + "monorepo/node_modules/exports/contracts4/Condition-nested.sol", + ); + + assert.equal( + resolvedFile.sourceName, + "npm/exports@3.0.0/Condition-nested.sol", + ); + }); + + it("doesnt resolve with the 'require' condition", async () => { + assertRejectsWithHardhatError( + resolver.resolveImport(file, "exports/Condition-require.sol"), + HardhatError.ERRORS.SOLIDITY.RESOLVE_NOT_EXPORTED_NPM_FILE, + { module: "exports/Condition-require.sol" }, + ); + }); + }); + }); + + describe("with remappings", function () { + it("resolves exported file when remapping part of the path", async () => { + resolver = await ResolverImplementation.create( + FIXTURE_HARDHAT_PROJECT_ROOT, + ["remapped_pkg/=npm/exports@3.0.0/"], + ); + + const resolvedFile = await resolver.resolveImport( + file, + "remapped_pkg/SingleExport.sol", + ); + + assertNpmPackageResolvedFile( + resolvedFile, + { + name: "exports", + version: "3.0.0", + rootSourceName: "npm/exports@3.0.0/", + }, + "monorepo/node_modules/exports", + "SingleExport.sol", + "monorepo/node_modules/exports/contracts2/SingleExport.sol", + ); + + assert.equal( + resolvedFile.sourceName, + "npm/exports@3.0.0/SingleExport.sol", + ); + }); + + it("resolves the exported file when remapping the whole path", async () => { + const resolver = await ResolverImplementation.create( + FIXTURE_HARDHAT_PROJECT_ROOT, + ["remapped_file=npm/exports@3.0.0/SingleExport.sol"], + ); + + const resolvedFile = await resolver.resolveImport( + file, + "remapped_file", + ); + + assertNpmPackageResolvedFile( + resolvedFile, + { + name: "exports", + version: "3.0.0", + rootSourceName: "npm/exports@3.0.0/", + }, + "monorepo/node_modules/exports", + "SingleExport.sol", + "monorepo/node_modules/exports/contracts2/SingleExport.sol", + ); + + assert.equal( + resolvedFile.sourceName, + "npm/exports@3.0.0/SingleExport.sol", + ); + }); + + it("throws an error when the remapped file doesnt exist", async () => { + const resolver = await ResolverImplementation.create( + FIXTURE_HARDHAT_PROJECT_ROOT, + ["remapped_pkg/=npm/exports@3.0.0/"], + ); + + await assertRejectsWithHardhatError( + resolver.resolveImport(file, "remapped_pkg/NonExistent.sol"), + HardhatError.ERRORS.SOLIDITY.IMPORTED_FILE_DOESNT_EXIST, + { + importPath: "remapped_pkg/NonExistent.sol", + from: path.join("contracts", "File.sol"), + }, + ); + }); + }); + }); + }); }); diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/NotExported b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/NotExported new file mode 100644 index 0000000000..e69de29bb2 diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts/Exported.sol b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts/Exported.sol new file mode 100644 index 0000000000..dc45f8953d --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts/Exported.sol @@ -0,0 +1 @@ +monorepo/node_modules/exports/contracts/Exported.sol diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts2/SingleExport.sol b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts2/SingleExport.sol new file mode 100644 index 0000000000..2fbe3af0e8 --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts2/SingleExport.sol @@ -0,0 +1 @@ +monorepo/node_modules/exports/contracts2/SingleExport.sol diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts3/Main.sol b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts3/Main.sol new file mode 100644 index 0000000000..873e320705 --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts3/Main.sol @@ -0,0 +1 @@ +monorepo/node_modules/exports/contracts3/Main.sol diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts4/Condition-default.sol b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts4/Condition-default.sol new file mode 100644 index 0000000000..fa19350cd5 --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts4/Condition-default.sol @@ -0,0 +1 @@ +monorepo/node_modules/exports/contracts4/Condition-default.sol diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts4/Condition-import.sol b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts4/Condition-import.sol new file mode 100644 index 0000000000..c5ef04c6b2 --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts4/Condition-import.sol @@ -0,0 +1 @@ +monorepo/node_modules/exports/contracts4/Condition-import.sol diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts4/Condition-nested.sol b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts4/Condition-nested.sol new file mode 100644 index 0000000000..640842f2ec --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts4/Condition-nested.sol @@ -0,0 +1 @@ +monorepo/node_modules/exports/contracts4/Condition-nested.sol diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts4/Condition-node.sol b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts4/Condition-node.sol new file mode 100644 index 0000000000..5b047be5de --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts4/Condition-node.sol @@ -0,0 +1 @@ +monorepo/node_modules/exports/contracts4/Condition-node.sol diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts4/Condition-require.sol b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts4/Condition-require.sol new file mode 100644 index 0000000000..13d739c796 --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/contracts4/Condition-require.sol @@ -0,0 +1 @@ +monorepo/node_modules/exports/contracts4/Condition-require.sol diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/package.json b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/package.json index 53a33a48a3..1c00b19832 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/package.json +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/resolver/test-fixtures/monorepo/node_modules/exports/package.json @@ -1,7 +1,26 @@ { - "name": "hardhat", + "name": "exports", "version": "3.0.0", "exports": { - ".": "./fool.js" + "./SingleExport.sol": "./contracts2/SingleExport.sol", + "./Condition-node.sol": { + "node": "./contracts4/Condition-node.sol" + }, + "./Condition-import.sol": { + "import": "./contracts4/Condition-import.sol" + }, + "./Condition-require.sol": { + "require": "./contracts4/Condition-require.sol" + }, + "./Condition-default.sol": { + "default": "./contracts4/Condition-default.sol" + }, + "./Condition-nested.sol": { + "node": { + "import": "./contracts4/Condition-nested.sol" + } + }, + "./*.sol": "./contracts/*.sol", + ".": "./contracts3/Main.sol" } }