Skip to content

Commit dafef6b

Browse files
committed
search up dir tree for hardhat config
1 parent 3c87b5f commit dafef6b

File tree

9 files changed

+218
-34
lines changed

9 files changed

+218
-34
lines changed

Diff for: v-next/hardhat/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"eslint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
3737
"prettier": "prettier \"**/*.{ts,js,md,json}\"",
3838
"test": "glob --cmd=\"node --import tsx/esm --test\" \"test/**/*.ts\"",
39+
"test:only": "glob --cmd=\"node --import tsx/esm --test --test-only\" \"test/**/*.ts\"",
3940
"test:github": "glob --cmd=\"node --import tsx/esm --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout\" \"test/**/*.ts\"",
4041
"pretest": "pnpm build",
4142
"build": "tsc --build .",

Diff for: v-next/hardhat/src/index.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
import {
2+
importUserConfig,
3+
resolveConfigPath,
4+
} from "./internal/helpers/config-loading.js";
15
import { getHardhatRuntimeEnvironmentSingleton } from "./internal/hre-singleton.js";
26

3-
// TODO:
4-
// - Load the config from the file system.
5-
const hre = await getHardhatRuntimeEnvironmentSingleton({});
7+
const configPath = await resolveConfigPath();
8+
const userConfig = await importUserConfig(configPath);
9+
10+
const hre = await getHardhatRuntimeEnvironmentSingleton(userConfig);
611

712
export const { config, tasks, globalArguments, hooks, interruptions } = hre;
813

Diff for: v-next/hardhat/src/internal/cli/main.ts

+5-31
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { isAbsolute, resolve } from "node:path";
2-
31
import {
42
buildGlobalParameterMap,
53
resolvePluginList,
@@ -13,6 +11,10 @@ import { Task } from "@nomicfoundation/hardhat-core/types/tasks";
1311

1412
import "tsx"; // NOTE: This is important, it allows us to load .ts files form the CLI
1513
import { builtinPlugins } from "../builtin-plugins/index.js";
14+
import {
15+
importUserConfig,
16+
resolveConfigPath,
17+
} from "../helpers/config-loading.js";
1618
import { getHardhatRuntimeEnvironmentSingleton } from "../hre-singleton.js";
1719

1820
export async function main(cliArguments: string[]) {
@@ -66,9 +68,7 @@ export async function main(cliArguments: string[]) {
6668
}
6769

6870
if (configPath === undefined) {
69-
// TODO: Find the closest config file
70-
// if HARDHAT_CONFIG exists, use it
71-
throw new Error("Missing --config");
71+
configPath = await resolveConfigPath();
7272
}
7373

7474
try {
@@ -265,29 +265,3 @@ function getTaskFromCliArguments(
265265

266266
return task;
267267
}
268-
269-
async function importUserConfig(configPath: string) {
270-
const normalizedPath = isAbsolute(configPath)
271-
? configPath
272-
: resolve(process.cwd(), configPath);
273-
274-
const { exists } = await import("@nomicfoundation/hardhat-utils/fs");
275-
276-
if (!(await exists(normalizedPath))) {
277-
throw new Error(`Config file ${configPath} not found`);
278-
}
279-
280-
const imported = await import(normalizedPath);
281-
282-
if (!("default" in imported)) {
283-
throw new Error(`No config exported in ${configPath}`);
284-
}
285-
286-
const config = imported.default;
287-
288-
if (typeof config !== "object" || config === null) {
289-
throw new Error(`Invalid config exported in ${configPath}`);
290-
}
291-
292-
return config;
293-
}
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { isAbsolute, resolve } from "node:path";
2+
3+
import { findUp } from "@nomicfoundation/hardhat-utils/fs";
4+
5+
async function findClosestHardhatConfig(): Promise<string> {
6+
let hardhatConfigPath = await findUp("hardhat.config.js");
7+
8+
if (hardhatConfigPath !== undefined) {
9+
return hardhatConfigPath;
10+
}
11+
12+
hardhatConfigPath = await findUp("hardhat.config.ts");
13+
14+
if (hardhatConfigPath !== undefined) {
15+
return hardhatConfigPath;
16+
}
17+
18+
throw new Error("No Hardhat config file found");
19+
}
20+
21+
export async function resolveConfigPath(): Promise<string> {
22+
const configPath = process.env.HARDHAT_CONFIG;
23+
24+
if (configPath !== undefined) {
25+
return configPath;
26+
}
27+
28+
return findClosestHardhatConfig();
29+
}
30+
31+
export async function importUserConfig(configPath: string) {
32+
const normalizedPath = isAbsolute(configPath)
33+
? configPath
34+
: resolve(process.cwd(), configPath);
35+
36+
const { exists } = await import("@nomicfoundation/hardhat-utils/fs");
37+
38+
if (!(await exists(normalizedPath))) {
39+
throw new Error(`Config file ${configPath} not found`);
40+
}
41+
42+
const imported = await import(normalizedPath);
43+
44+
if (!("default" in imported)) {
45+
throw new Error(`No config exported in ${configPath}`);
46+
}
47+
48+
const config = imported.default;
49+
50+
if (typeof config !== "object" || config === null) {
51+
throw new Error(`Invalid config exported in ${configPath}`);
52+
}
53+
54+
return config;
55+
}

Diff for: v-next/hardhat/test/fixture-projects/config-js/hardhat.config.js

Whitespace-only changes.

Diff for: v-next/hardhat/test/fixture-projects/config-ts/hardhat.config.ts

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
plugins: [{ id: "test-plugin" }],
3+
};

Diff for: v-next/hardhat/test/helpers/project.ts

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import fsPromises from "node:fs/promises";
2+
import path from "node:path";
3+
import { before, after } from "node:test";
4+
5+
import { exists } from "@nomicfoundation/hardhat-utils/fs";
6+
7+
/**
8+
* This helper adds mocha hooks to run the tests inside one of the projects
9+
* from test/fixture-projects.
10+
*
11+
* @param projectName The base name of the folder with the project to use.
12+
* @param changeDirTo If provided, the working directory will be changed to this. Must be a child of the project folder.
13+
*/
14+
export function useFixtureProject(projectName: string, changeDirTo?: string) {
15+
let projectPath: string;
16+
let prevWorkingDir: string;
17+
18+
before(async () => {
19+
projectPath = await getFixtureProjectPath(projectName, changeDirTo);
20+
prevWorkingDir = process.cwd();
21+
process.chdir(projectPath);
22+
});
23+
24+
after(() => {
25+
process.chdir(prevWorkingDir);
26+
});
27+
}
28+
29+
async function getFixtureProjectPath(
30+
projectName: string,
31+
changeDirTo?: string,
32+
): Promise<string> {
33+
const normalizedProjectName = projectName.replaceAll("/", path.sep);
34+
35+
let projectPath = path.join(
36+
import.meta.dirname,
37+
"..",
38+
"fixture-projects",
39+
normalizedProjectName,
40+
);
41+
42+
if (changeDirTo !== undefined) {
43+
projectPath = path.join(projectPath, changeDirTo);
44+
}
45+
46+
if (!(await exists(projectPath))) {
47+
console.log("projectPath", projectPath);
48+
throw new Error(`Fixture project ${projectName} doesn't exist`);
49+
}
50+
51+
return getRealPath(projectPath);
52+
}
53+
54+
/**
55+
* Returns the real path of absolutePath, resolving symlinks.
56+
*
57+
* @throws Error if absolutePath doesn't exist.
58+
*/
59+
async function getRealPath(absolutePath: string): Promise<string> {
60+
try {
61+
// This method returns the actual casing.
62+
// Please read Node.js' docs to learn more.
63+
return await fsPromises.realpath(path.normalize(absolutePath));
64+
} catch {
65+
throw new Error(`Invalid directory ${absolutePath}`);
66+
}
67+
}

Diff for: v-next/hardhat/test/hre/index.ts

+79
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { describe, it } from "node:test";
33

44
import { createHardhatRuntimeEnvironment } from "../../src/hre.js";
55
import { builtinPlugins } from "../../src/internal/builtin-plugins/index.js";
6+
import { resolveConfigPath } from "../../src/internal/helpers/config-loading.js";
67
import { getHardhatRuntimeEnvironmentSingleton } from "../../src/internal/hre-singleton.js";
8+
import { useFixtureProject } from "../helpers/project.js";
79

810
describe("HRE", () => {
911
describe("createHardhatRuntimeEnvironment", () => {
@@ -32,4 +34,81 @@ describe("HRE", () => {
3234
assert.deepEqual(hre1, hre2);
3335
});
3436
});
37+
38+
describe("config loading", () => {
39+
describe("resolveConfigPath", async () => {
40+
it("should return the HARDHAT_CONFIG env variable if it is set", async () => {
41+
process.env.HARDHAT_CONFIG = "hardhat.config.js";
42+
43+
assert.equal(await resolveConfigPath(), "hardhat.config.js");
44+
45+
delete process.env.HARDHAT_CONFIG;
46+
});
47+
48+
it("should throw if the config file is not found", async () => {
49+
await assert.rejects(resolveConfigPath(), {
50+
message: "No Hardhat config file found",
51+
});
52+
});
53+
54+
describe("javascript config", () => {
55+
describe("current dir", () => {
56+
useFixtureProject("config-js");
57+
58+
it("should load a config file in the current directory", async () => {
59+
const configPath = await resolveConfigPath();
60+
61+
assert(configPath.endsWith("hardhat.config.js"));
62+
});
63+
});
64+
65+
describe("nested dir", () => {
66+
useFixtureProject("config-js", "nested-folder");
67+
68+
it("should load a config file in the parent directory", async () => {
69+
const configPath = await resolveConfigPath();
70+
71+
assert(configPath.endsWith("hardhat.config.js"));
72+
});
73+
});
74+
});
75+
76+
describe("typescript config", () => {
77+
describe("current dir", () => {
78+
useFixtureProject("config-ts");
79+
80+
it("should load a config file in the current directory", async () => {
81+
const configPath = await resolveConfigPath();
82+
83+
assert(configPath.endsWith("hardhat.config.ts"));
84+
});
85+
});
86+
87+
describe("nested dir", () => {
88+
useFixtureProject("config-ts", "nested-folder");
89+
90+
it("should load a config file in the parent directory", async () => {
91+
const configPath = await resolveConfigPath();
92+
93+
assert(configPath.endsWith("hardhat.config.ts"));
94+
});
95+
});
96+
});
97+
});
98+
99+
// This test works individually but fails when running all tests
100+
// due to the hre singleton being used in tests above.
101+
// ESM modules cache is not accessible like `require.cache` in CJS,
102+
// so a workaround is needed.
103+
// TODO: Fix this test
104+
describe.skip("programmatic API", () => {
105+
useFixtureProject("loaded-config");
106+
107+
it("should load the config file", async () => {
108+
const hre = await import("../../src/index.js");
109+
110+
assert.deepEqual(hre.config.plugins, [{ id: "test-plugin" }]);
111+
});
112+
});
113+
});
35114
});

0 commit comments

Comments
 (0)