Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const flattenAction: NewTaskActionFunction<FlattenActionArguments> = async (
rootPaths.toSorted(), // We sort them to have a deterministic order
config.paths.root,
readSourceFileFactory(hooks),
hooks,
);

let flattened = "";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem {
rootFilePaths.toSorted(), // We sort them to have a deterministic order
this.#options.projectRoot,
readSourceFileFactory(this.#hooks),
this.#hooks,
);

const { buildProfileName, buildProfile } = this.#getBuildProfile(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { RemappingsReaderFunction } from "./resolver/remapped-npm-packages-graph.js";
import type { HookManager } from "../../../../types/hooks.js";
import type { ResolvedFile } from "../../../../types/solidity/resolved-file.js";

import { HardhatError } from "@nomicfoundation/hardhat-errors";
Expand All @@ -17,8 +19,28 @@ export async function buildDependencyGraph(
rootFiles: string[],
projectRoot: string,
readFile: (absPath: string) => Promise<string>,
hookManager: HookManager,
): Promise<DependencyGraphImplementation> {
const resolver = await ResolverImplementation.create(projectRoot, readFile);
// Create the wrapper function that captures the hook manager
const remappingsReader: RemappingsReaderFunction = (
packageName,
packageVersion,
packagePath,
defaultBehavior,
) =>
hookManager.runHandlerChain(
"solidity",
"readNpmPackageRemappings",
[packageName, packageVersion, packagePath],
async (_context, name, version, path) =>
defaultBehavior(name, version, path),
);

const resolver = await ResolverImplementation.create(
projectRoot,
readFile,
remappingsReader,
);

const dependencyGraph = new DependencyGraphImplementation();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { parseNpmDirectImport } from "./npm-module-parsing.js";
import {
isResolvedUserRemapping,
RemappedNpmPackagesGraphImplementation,
type RemappingsReaderFunction,
} from "./remapped-npm-packages-graph.js";
import { applyValidRemapping, formatRemapping } from "./remappings.js";
import {
Expand Down Expand Up @@ -86,14 +87,18 @@ export class ResolverImplementation implements Resolver {
*
* @param projectRoot The absolute path to the Hardhat project root.
* @param readUtf8File A function that reads a UTF-8 file.
* @param remappingsReader Optional function to read remappings from packages.
* @returns The resolver or the user remapping errors found.
*/
public static async create(
projectRoot: string,
readUtf8File: (absPath: string) => Promise<string>,
remappingsReader?: RemappingsReaderFunction,
): Promise<Resolver> {
const map =
await RemappedNpmPackagesGraphImplementation.create(projectRoot);
const map = await RemappedNpmPackagesGraphImplementation.create(
projectRoot,
remappingsReader,
);

return new ResolverImplementation(projectRoot, map, readUtf8File);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ import { UserRemappingType } from "./types.js";

const HARDHAT_PROJECT_INPUT_SOURCE_NAME_ROOT = "project";

export type RemappingsReaderFunction = (
packageName: string,
packageVersion: string,
packagePath: string,
defaultBehavior: (
name: string,
version: string,
path: string,
) => Promise<Array<{ remappings: string[]; source: string }>>,
) => Promise<Array<{ remappings: string[]; source: string }>>;

export function isResolvedUserRemapping(
remapping: Remapping | ResolvedUserRemapping,
): remapping is ResolvedUserRemapping {
Expand All @@ -55,6 +66,11 @@ export class RemappedNpmPackagesGraphImplementation
*/
readonly #hardhatProjectPackage: ResolvedNpmPackage;

/**
* The remappings reader function to use when reading package remappings.
*/
readonly #remappingsReader?: RemappingsReaderFunction;

/**
* This is a map of all the npm packages. Every package that has been
* loaded by this class, is present in this map.
Expand Down Expand Up @@ -104,6 +120,7 @@ export class RemappedNpmPackagesGraphImplementation

public static async create(
projectRootPath: string,
remappingsReader?: RemappingsReaderFunction,
): Promise<RemappedNpmPackagesGraphImplementation> {
const projectPackageJson = await readJsonFile<PackageJson>(
path.join(projectRootPath, "package.json"),
Expand All @@ -117,11 +134,18 @@ export class RemappedNpmPackagesGraphImplementation
inputSourceNameRoot: HARDHAT_PROJECT_INPUT_SOURCE_NAME_ROOT,
};

return new RemappedNpmPackagesGraphImplementation(resolvedNpmPackage);
return new RemappedNpmPackagesGraphImplementation(
resolvedNpmPackage,
remappingsReader,
);
}

private constructor(hardhatProjectPackage: ResolvedNpmPackage) {
private constructor(
hardhatProjectPackage: ResolvedNpmPackage,
remappingsReader?: RemappingsReaderFunction,
) {
this.#hardhatProjectPackage = hardhatProjectPackage;
this.#remappingsReader = remappingsReader;
this.#insertNewPackage(hardhatProjectPackage);
}

Expand Down Expand Up @@ -397,41 +421,76 @@ export class RemappedNpmPackagesGraphImplementation
UserRemappingError[]
>
> {
const remappingsTxtFiles = await getAllFilesMatching(
npmPackage.rootFsPath,
(f) => path.basename(f) === "remappings.txt",
(f) => !f.endsWith("node_modules"),
);
const defaultBehavior = async (
packageName: string,
packageVersion: string,
packagePath: string,
) => {
// Default: read from remappings.txt files (existing implementation)
const remappingsTxtFiles = await getAllFilesMatching(
packagePath,
(f) => path.basename(f) === "remappings.txt",
(f) => !f.endsWith("node_modules"),
);

const results: Array<{ remappings: string[]; source: string }> = [];
for (const file of remappingsTxtFiles) {
const contents = await readUtf8File(file);
const lines = contents
.split("\n")
.map((line) => line.trim())
.filter((line) => line !== "" && !line.startsWith("#"));
results.push({ remappings: lines, source: file });
}

const remappings = [];
const errors = [];
return results;
};

for (const remappingsTxtFsPath of remappingsTxtFiles) {
const packageRemappingsTxtContents =
await readUtf8File(remappingsTxtFsPath);
// Call the wrapper function if provided, otherwise use default
let allRemappings: Array<{ remappings: string[]; source: string }>;

const rawUserRemappings = packageRemappingsTxtContents
.split("\n")
.map((line) => line.trim())
.filter((line) => line !== "")
.filter((line) => !line.startsWith("#"));
if (this.#remappingsReader !== undefined) {
allRemappings = await this.#remappingsReader(
npmPackage.name,
npmPackage.version,
npmPackage.rootFsPath,
defaultBehavior,
);
} else {
allRemappings = await defaultBehavior(
npmPackage.name,
npmPackage.version,
npmPackage.rootFsPath,
);
}

for (const userRemapping of rawUserRemappings) {
// Parse and deduplicate remappings
const remappings: Array<LocalUserRemapping | UnresolvedNpmUserRemapping> =
[];
const errors: UserRemappingError[] = [];
const seen = new Set<string>(); // Track by "context:prefix"

for (const { remappings: remappingStrings, source } of allRemappings) {
for (const remappingString of remappingStrings) {
const result = await this.#parseUserRemapping(
npmPackage,
remappingsTxtFsPath,
userRemapping,
source,
remappingString,
);

if (!result.success) {
errors.push(result.error);
} else {
// If parsing returned `undefined`, it means that it should be
// ignored.
if (result.value === undefined) {
continue;
}
continue;
}

if (result.value === undefined) {
continue;
}

// Deduplicate by (context + prefix) - first occurrence wins
const key = `${result.value.context}:${result.value.prefix}`;
if (!seen.has(key)) {
seen.add(key);
remappings.push(result.value);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,5 +240,33 @@ declare module "../../../types/hooks.js" {
nextSolcConfig: SolcConfig,
) => Promise<CompilerOutput>,
): Promise<CompilerOutput>;

/**
* Provide a handler for this hook to supply remappings for npm packages.
*
* This hook is called when the resolver needs to read remappings for a package.
* Handlers can provide remappings from alternative sources (e.g., foundry.toml)
* in addition to the default remappings.txt files.
*
* @param context The hook context.
* @param packageName The name of the npm package.
* @param packageVersion The version of the npm package.
* @param packagePath The absolute filesystem path to the package root.
* @param next A function to get remappings from other sources (including default behavior).
* @returns An array of remapping sources, each containing an array of remapping strings
* and the source path they came from.
*/
readNpmPackageRemappings: (
context: HookContext,
packageName: string,
packageVersion: string,
packagePath: string,
next: (
nextContext: HookContext,
nextPackageName: string,
nextPackageVersion: string,
nextPackagePath: string,
) => Promise<Array<{ remappings: string[]; source: string }>>,
) => Promise<Array<{ remappings: string[]; source: string }>>;
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { HookContext } from "../../../../../src/types/hooks.js";

import assert from "node:assert/strict";
import path from "node:path";
import { describe, it } from "node:test";
Expand All @@ -6,15 +8,20 @@ import { useFixtureProject } from "@nomicfoundation/hardhat-test-utils";
import { readUtf8File } from "@nomicfoundation/hardhat-utils/fs";

import { buildDependencyGraph } from "../../../../../src/internal/builtin-plugins/solidity/build-system/dependency-graph-building.js";
import { HookManagerImplementation } from "../../../../../src/internal/core/hook-manager.js";

describe("buildDependencyGraph", () => {
useFixtureProject("solidity/example-project");

it("should return an empty graph if no files are provided", async () => {
const hookManager = new HookManagerImplementation(process.cwd(), []);
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We don't care about hooks in this context
hookManager.setContext({} as HookContext);
const dependencyGraph = await buildDependencyGraph(
[],
process.cwd(),
readUtf8File,
hookManager,
);

assert.equal(dependencyGraph.getRoots().size, 0);
Expand All @@ -39,10 +46,14 @@ describe("buildDependencyGraph", () => {
"npm/@openzeppelin/contracts@5.1.0/access/Ownable.sol",
];

const hookManager = new HookManagerImplementation(process.cwd(), []);
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We don't care about hooks in this context
hookManager.setContext({} as HookContext);
const dependencyGraph = await buildDependencyGraph(
rootRelativePaths.map((p) => path.join(process.cwd(), p)),
process.cwd(),
readUtf8File,
hookManager,
);

const roots = dependencyGraph.getRoots();
Expand Down
Loading
Loading