Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/olive-shirts-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hardhat": patch
---

Suggest installing hardhat-foundry when appropriate
Original file line number Diff line number Diff line change
Expand Up @@ -696,13 +696,21 @@ export class ResolverImplementation implements Resolver {
);

if (dependencyResolution === undefined) {
// Check if the from file's package has foundry.toml
const foundryTomlPath = path.join(
from.package.rootFsPath,
"foundry.toml",
);
const fromHasFoundryToml = await exists(foundryTomlPath);

return {
success: false,
error: {
type: ImportResolutionErrorType.IMPORT_OF_UNINSTALLED_PACKAGE,
fromFsPath: from.fsPath,
importPath,
installationName: parsedDirectImport.package,
importerPackageHasFoundryToml: fromHasFoundryToml,
},
};
}
Expand Down Expand Up @@ -1103,6 +1111,7 @@ export class ResolverImplementation implements Resolver {
type: RootResolutionErrorType.NPM_ROOT_FILE_OF_UNINSTALLED_PACKAGE,
npmModule,
installationName: error.installationName,
projectHasFoundryToml: error.importerPackageHasFoundryToml,
};
}
case ImportResolutionErrorType.IMPORT_WITH_REMAPPING_ERRORS: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,14 @@ Note that the npm module is being remapped by ${formatRemappingReference(error.u
}

case RootResolutionErrorType.NPM_ROOT_FILE_OF_UNINSTALLED_PACKAGE: {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these errors message updates dont have a corresponding unit test to verify the behavior works with hasFoundryToml and without it. same for the one further below

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has 100% coverage after d80c347, thanks!

return `The package "${error.installationName}" is not installed.`;
const baseMessage = `The package "${error.installationName}" is not installed.`;
if (error.projectHasFoundryToml === true) {
return `${baseMessage}

Your project has a foundry.toml, and you may need to install the "@nomicfoundation/hardhat-foundry" plugin.
Learn more about Hardhat's Foundry compatibility here: https://hardhat.org/foundry-compatibility`;
Copy link
Copy Markdown
Contributor

@marianfe marianfe Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this URL is currently a 404! UPDATE: just saw that it is sent as a next PR

}
return baseMessage;
}

case RootResolutionErrorType.NPM_ROOT_FILE_RESOLUTION_WITH_REMAPPING_ERRORS: {
Expand Down Expand Up @@ -107,21 +114,28 @@ export function formatImportResolutionError(
}

case ImportResolutionErrorType.ILLEGAL_RELATIVE_IMPORT: {
return "The import has too many '../', and trying to leave its package.";
return "The import has too many '../' and is trying to leave its package.";
}

case ImportResolutionErrorType.RELATIVE_IMPORT_INTO_NODE_MODULES: {
return `You are trying to import a file from your node_modules directory with its file system path.

You should write your the path of your imports into npm modules just as you would do in JavaScript files.`;
You should write your imports into npm modules just as you would do in JavaScript files.`;
}

case ImportResolutionErrorType.IMPORT_WITH_INVALID_NPM_SYNTAX: {
return "You are trying to import an npm file but its syntax is invalid.";
}

case ImportResolutionErrorType.IMPORT_OF_UNINSTALLED_PACKAGE: {
return `The package "${error.installationName}" is not installed.`;
const baseMessage = `The package "${error.installationName}" is not installed.`;
if (error.importerPackageHasFoundryToml === true) {
return `${baseMessage}

The file importing this package is inside a Foundry project (foundry.toml detected), and you may need to install the "@nomicfoundation/hardhat-foundry" plugin.
Learn more about Hardhat's Foundry compatibility here: https://hardhat.org/foundry-compatibility`;
Comment thread
marianfe marked this conversation as resolved.
}
return baseMessage;
}

case ImportResolutionErrorType.IMPORT_WITH_REMAPPING_ERRORS: {
Expand All @@ -148,7 +162,7 @@ If you still want to be able to do it, try adding this remapping "${error.sugges
? "the package"
: "the project";

const message = `The file ${error.packageExportsResolvedSubpath ?? error.subpath} doesn't exist within ${packageOrProject}.`;
const message = `The file "${error.packageExportsResolvedSubpath ?? error.subpath}" doesn't exist within ${packageOrProject}.`;

return formatResolutionErrorRemappingsOrPackageExportsNotes({
message,
Expand Down
22 changes: 22 additions & 0 deletions v-next/hardhat/src/types/solidity/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ export interface NpmRootFileOfUninstalledPackageError {
type: RootResolutionErrorType.NPM_ROOT_FILE_OF_UNINSTALLED_PACKAGE;
npmModule: string;
installationName: string;
/**
* A boolean indicating if the Hardhat project has a foundry.toml file in its
* root.
*/
projectHasFoundryToml?: boolean;
}

export interface NpmRootResolutionWithRemappingErrors {
Expand All @@ -109,6 +114,10 @@ export interface NpmRootFileWithIncorrectCasingError
extends ResolvedFileReference {
type: RootResolutionErrorType.NPM_ROOT_FILE_WITH_INCORRECT_CASING;
npmModule: string;
/**
* The correct casing of the file, expressed as the relative fs path from the
* package's root, using forward slashes, and without a leading "./".
*/
correctCasing: string;
}

Expand Down Expand Up @@ -216,6 +225,10 @@ export interface ImportInvalidCasingError extends ResolvedFileReference {
type: ImportResolutionErrorType.IMPORT_INVALID_CASING;
fromFsPath: string;
importPath: string;
/**
* The correct casing of the file, expressed as the relative fs path from the
* package's root, using forward slashes, and without a leading "./".
*/
correctCasing: string;
}

Expand All @@ -230,6 +243,11 @@ export interface ImportOfUninstalledPackageError {
fromFsPath: string;
importPath: string;
installationName: string;
/**
* A boolean indicating weather the import is from a source file whose package
* has a foundry.toml file in its root.
*/
importerPackageHasFoundryToml?: boolean;
}

export interface ImportWithRemappingErrorsError {
Expand Down Expand Up @@ -348,10 +366,14 @@ export interface ResolvedFileReference {
// package's input source name root, after applying the user remapping.
// - For an npm import, it's the npm-subpath. i.e. the module identifier
// without the package name.
//
// This never includes a leading "./".
subpath: string;

// The subpath after resolving package.exports
//
// Only present when actually using package.exports.
//
// This never includes a leading "./".
packageExportsResolvedSubpath?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,40 @@ other-exports/=node_modules/exports/other/`,
type: RootResolutionErrorType.NPM_ROOT_FILE_OF_UNINSTALLED_PACKAGE,
npmModule,
installationName: "not-installed",
projectHasFoundryToml: false,
};

assert.deepEqual(
await resolver.resolveNpmDependencyFileAsRoot(npmModule),
{
success: false,
error: expectedError,
},
);
});

it("Should set projectHasFoundryToml to true when project has foundry.toml", async () => {
const templateWithFoundry: TestProjectTemplate = {
name: "foundry-project-npm-root",
version: "1.0.0",
files: {
"foundry.toml": "[profile.default]\nsrc = 'contracts'\n",
"contracts/A.sol": `A`,
},
};

await using project = await useTestProjectTemplate(templateWithFoundry);
const resolver = await ResolverImplementation.create(
project.path,
readUtf8File,
);

const npmModule = "forge-std/Test.sol";
const expectedError: NpmRootResolutionError = {
type: RootResolutionErrorType.NPM_ROOT_FILE_OF_UNINSTALLED_PACKAGE,
npmModule,
installationName: "forge-std",
projectHasFoundryToml: true,
};

assert.deepEqual(
Expand Down Expand Up @@ -2127,11 +2161,173 @@ submodule2/=lib/submodule2/src/`,
fromFsPath: absoluteFilePath,
importPath,
installationName: "not-installed",
importerPackageHasFoundryToml: false,
},
},
);
});

describe("foundry.toml detection for uninstalled packages", () => {
it("Should set importerPackageHasFoundryToml to true when project has foundry.toml", async () => {
const templateWithFoundry: TestProjectTemplate = {
name: "foundry-project",
version: "1.0.0",
files: {
"foundry.toml": "[profile.default]\nsrc = 'contracts'\n",
"contracts/A.sol": `A`,
},
};

await using project =
await useTestProjectTemplate(templateWithFoundry);
const resolver = await ResolverImplementation.create(
project.path,
readUtf8File,
);
const absoluteFilePath = path.join(project.path, "contracts/A.sol");
const result = await resolver.resolveProjectFile(absoluteFilePath);
assert.ok(result.success, "Result should be successful");

const importPath = "forge-std/Test.sol";
assert.deepEqual(
await resolver.resolveImport(result.value, importPath),
{
success: false,
error: {
type: ImportResolutionErrorType.IMPORT_OF_UNINSTALLED_PACKAGE,
fromFsPath: absoluteFilePath,
importPath,
installationName: "forge-std",
importerPackageHasFoundryToml: true,
},
},
);
});

it("Should set importerPackageHasFoundryToml to true when npm dependency has foundry.toml", async () => {
const templateWithNpmFoundry: TestProjectTemplate = {
name: "project-with-foundry-dep",
version: "1.0.0",
files: {
"contracts/A.sol": `A`,
},
dependencies: {
"my-foundry-dep": {
name: "my-foundry-dep",
version: "1.0.0",
files: {
"foundry.toml": "[profile.default]\nsrc = 'src'\n",
"src/Lib.sol": `import "forge-std/Test.sol";`,
},
},
},
};

await using project = await useTestProjectTemplate(
templateWithNpmFoundry,
);
const resolver = await ResolverImplementation.create(
project.path,
readUtf8File,
);

// First, resolve a project file that imports from the npm dependency
const absoluteFilePath = path.join(project.path, "contracts/A.sol");
const projectResult =
await resolver.resolveProjectFile(absoluteFilePath);
assert.ok(projectResult.success, "Project file should resolve");

// Resolve the npm dependency file
const npmDepResult = await resolver.resolveImport(
projectResult.value,
"my-foundry-dep/src/Lib.sol",
);
assert.ok(npmDepResult.success, "Npm dependency should resolve");

// Now try to import an uninstalled package from the npm dependency
const importPath = "forge-std/Test.sol";
const errorResult = await resolver.resolveImport(
npmDepResult.value.file,
importPath,
);

assert.deepEqual(errorResult, {
success: false,
error: {
type: ImportResolutionErrorType.IMPORT_OF_UNINSTALLED_PACKAGE,
fromFsPath: path.join(
project.path,
"node_modules/my-foundry-dep/src/Lib.sol",
),
importPath,
installationName: "forge-std",
importerPackageHasFoundryToml: true,
},
});
});

it("Should set importerPackageHasFoundryToml to false when npm dependency does not have foundry.toml", async () => {
const templateWithoutFoundry: TestProjectTemplate = {
name: "project-without-foundry-dep",
version: "1.0.0",
files: {
"contracts/A.sol": `A`,
},
dependencies: {
"regular-dep": {
name: "regular-dep",
version: "1.0.0",
files: {
"src/Lib.sol": `import "some-lib/Foo.sol";`,
},
},
},
};

await using project = await useTestProjectTemplate(
templateWithoutFoundry,
);
const resolver = await ResolverImplementation.create(
project.path,
readUtf8File,
);

// First, resolve a project file that imports from the npm dependency
const absoluteFilePath = path.join(project.path, "contracts/A.sol");
const projectResult =
await resolver.resolveProjectFile(absoluteFilePath);
assert.ok(projectResult.success, "Project file should resolve");

// Resolve the npm dependency file
const npmDepResult = await resolver.resolveImport(
projectResult.value,
"regular-dep/src/Lib.sol",
);
assert.ok(npmDepResult.success, "Npm dependency should resolve");

// Now try to import an uninstalled package from the npm dependency
const importPath = "some-lib/Foo.sol";
const errorResult = await resolver.resolveImport(
npmDepResult.value.file,
importPath,
);

assert.deepEqual(errorResult, {
success: false,
error: {
type: ImportResolutionErrorType.IMPORT_OF_UNINSTALLED_PACKAGE,
fromFsPath: path.join(
project.path,
"node_modules/regular-dep/src/Lib.sol",
),
importPath,
installationName: "some-lib",
importerPackageHasFoundryToml: false,
},
});
});
});

describe("Without package.exports", () => {
it("Should fail if the file doesn't exist within the package", async () => {
await using project = await useTestProjectTemplate(template);
Expand Down
Loading
Loading