Skip to content

Commit 0530527

Browse files
authored
Merge pull request #5277 from NomicFoundation/config-loading
Dynamically find and load config
2 parents 87ad988 + 00397ea commit 0530527

File tree

13 files changed

+251
-36
lines changed

13 files changed

+251
-36
lines changed

v-next/hardhat-errors/src/descriptors.ts

+26
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,32 @@ Please double check the file path.`,
118118
119119
Please double check whether you have multiple versions of the same plugin installed.`,
120120
},
121+
NO_CONFIG_FILE_FOUND: {
122+
number: 5,
123+
messageTemplate: "No Hardhat config file found",
124+
websiteTitle: "No Hardhat config file found",
125+
websiteDescription:
126+
"Hardhat couldn't find a config file in the current directory or any of its parents.",
127+
},
128+
INVALID_CONFIG_PATH: {
129+
number: 6,
130+
messageTemplate: "Config file %configPath% not found",
131+
websiteTitle: "Invalid config path",
132+
websiteDescription: "The config file doesn't exist at the provided path.",
133+
},
134+
NO_CONFIG_EXPORTED: {
135+
number: 7,
136+
messageTemplate: "No config exported in %configPath%",
137+
websiteTitle: "No config exported",
138+
websiteDescription: "There is nothing exported from the config file.",
139+
},
140+
INVALID_CONFIG_OBJECT: {
141+
number: 8,
142+
messageTemplate: "Invalid config exported in %configPath%",
143+
websiteTitle: "Invalid config object",
144+
websiteDescription:
145+
"The config file doesn't export a valid configuration object.",
146+
},
121147
},
122148
INTERNAL: {
123149
ASSERTION_ERROR: {

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 .",

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

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-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { isAbsolute, resolve } from "node:path";
2+
import { pathToFileURL } from "node:url";
3+
4+
import { HardhatError } from "@nomicfoundation/hardhat-errors";
5+
import { findUp } from "@nomicfoundation/hardhat-utils/fs";
6+
7+
import { ERRORS } from "../../../../hardhat-errors/src/descriptors.js";
8+
9+
async function findClosestHardhatConfig(): Promise<string> {
10+
let hardhatConfigPath = await findUp("hardhat.config.ts");
11+
12+
if (hardhatConfigPath !== undefined) {
13+
return hardhatConfigPath;
14+
}
15+
16+
hardhatConfigPath = await findUp("hardhat.config.js");
17+
18+
if (hardhatConfigPath !== undefined) {
19+
return hardhatConfigPath;
20+
}
21+
22+
throw new HardhatError(ERRORS.GENERAL.NO_CONFIG_FILE_FOUND);
23+
}
24+
25+
export async function resolveConfigPath(): Promise<string> {
26+
const configPath = process.env.HARDHAT_CONFIG;
27+
28+
if (configPath !== undefined) {
29+
return configPath;
30+
}
31+
32+
return findClosestHardhatConfig();
33+
}
34+
35+
export async function importUserConfig(configPath: string) {
36+
const normalizedPath = isAbsolute(configPath)
37+
? configPath
38+
: resolve(process.cwd(), configPath);
39+
40+
const { exists } = await import("@nomicfoundation/hardhat-utils/fs");
41+
42+
if (!(await exists(normalizedPath))) {
43+
throw new HardhatError(ERRORS.GENERAL.INVALID_CONFIG_PATH, { configPath });
44+
}
45+
46+
const imported = await import(pathToFileURL(normalizedPath).href);
47+
48+
if (!("default" in imported)) {
49+
throw new HardhatError(ERRORS.GENERAL.NO_CONFIG_EXPORTED, { configPath });
50+
}
51+
52+
const config = imported.default;
53+
54+
if (typeof config !== "object" || config === null) {
55+
throw new HardhatError(ERRORS.GENERAL.INVALID_CONFIG_OBJECT, {
56+
configPath,
57+
});
58+
}
59+
60+
return config;
61+
}

v-next/hardhat/src/internal/hre-singleton.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { HardhatRuntimeEnvironment } from "../types/hre.js";
22

33
import { createHardhatRuntimeEnvironment } from "@nomicfoundation/hardhat-core";
44

5-
let hre: HardhatRuntimeEnvironment;
5+
let hre: HardhatRuntimeEnvironment | undefined;
66

77
/**
88
* This function returns a singleton instance of the Hardhat Runtime Environment.
@@ -20,3 +20,12 @@ export async function getHardhatRuntimeEnvironmentSingleton(
2020

2121
return hre;
2222
}
23+
24+
/**
25+
* This function resets the singleton instance of the Hardhat Runtime Environment.
26+
*
27+
* It should be used only in tests.
28+
*/
29+
export function resetHardhatRuntimeEnvironmentSingleton() {
30+
hre = undefined;
31+
}

v-next/hardhat/test/fixture-projects/config-js/hardhat.config.js

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// left empty so github commits the folder

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 @@
1+
// left empty so github commits the folder
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
plugins: [{ id: "test-plugin" }],
3+
};
+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import path from "node:path";
2+
import { before, after } from "node:test";
3+
4+
import { exists, getRealPath } from "@nomicfoundation/hardhat-utils/fs";
5+
6+
/**
7+
* This helper adds mocha hooks to run the tests inside one of the projects
8+
* from test/fixture-projects.
9+
*
10+
* @param projectName The base name of the folder with the project to use.
11+
* @param changeDirTo If provided, the working directory will be changed to this. Must be a child of the project folder.
12+
*/
13+
export function useFixtureProject(projectName: string, changeDirTo?: string) {
14+
let projectPath: string;
15+
let prevWorkingDir: string;
16+
17+
before(async () => {
18+
projectPath = await getFixtureProjectPath(projectName, changeDirTo);
19+
prevWorkingDir = process.cwd();
20+
process.chdir(projectPath);
21+
});
22+
23+
after(() => {
24+
process.chdir(prevWorkingDir);
25+
});
26+
}
27+
28+
async function getFixtureProjectPath(
29+
projectName: string,
30+
changeDirTo?: string,
31+
): Promise<string> {
32+
const normalizedProjectName = projectName.replaceAll("/", path.sep);
33+
34+
let projectPath = path.join(
35+
import.meta.dirname,
36+
"..",
37+
"fixture-projects",
38+
normalizedProjectName,
39+
);
40+
41+
if (changeDirTo !== undefined) {
42+
projectPath = path.join(projectPath, changeDirTo);
43+
}
44+
45+
if (!(await exists(projectPath))) {
46+
console.log("projectPath", projectPath);
47+
throw new Error(`Fixture project ${projectName} doesn't exist`);
48+
}
49+
50+
return getRealPath(projectPath);
51+
}

v-next/hardhat/test/hre/index.ts

+84-1
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,21 @@ 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 { getHardhatRuntimeEnvironmentSingleton } from "../../src/internal/hre-singleton.js";
6+
import { resolveConfigPath } from "../../src/internal/helpers/config-loading.js";
7+
import {
8+
getHardhatRuntimeEnvironmentSingleton,
9+
resetHardhatRuntimeEnvironmentSingleton,
10+
} from "../../src/internal/hre-singleton.js";
11+
import { useFixtureProject } from "../helpers/project.js";
712

813
describe("HRE", () => {
914
describe("createHardhatRuntimeEnvironment", () => {
1015
it("should include the built-in plugins", async () => {
1116
const hre = await createHardhatRuntimeEnvironment({});
1217

1318
assert.deepEqual(hre.config.plugins, builtinPlugins);
19+
20+
resetHardhatRuntimeEnvironmentSingleton();
1421
});
1522
});
1623

@@ -30,6 +37,82 @@ describe("HRE", () => {
3037
{ id: "custom task" },
3138
);
3239
assert.deepEqual(hre1, hre2);
40+
41+
resetHardhatRuntimeEnvironmentSingleton();
42+
});
43+
});
44+
45+
describe("config loading", () => {
46+
describe("resolveConfigPath", async () => {
47+
it("should return the HARDHAT_CONFIG env variable if it is set", async () => {
48+
process.env.HARDHAT_CONFIG = "env.config.js";
49+
50+
assert.equal(await resolveConfigPath(), "env.config.js");
51+
52+
delete process.env.HARDHAT_CONFIG;
53+
});
54+
55+
it("should throw if the config file is not found", async () => {
56+
await assert.rejects(resolveConfigPath(), {
57+
message: "HHE5: No Hardhat config file found",
58+
});
59+
});
60+
61+
describe("javascript config", () => {
62+
describe("current dir", () => {
63+
useFixtureProject("config-js");
64+
65+
it("should load a config file in the current directory", async () => {
66+
const configPath = await resolveConfigPath();
67+
68+
assert(configPath.endsWith("hardhat.config.js"));
69+
});
70+
});
71+
72+
describe("nested dir", () => {
73+
useFixtureProject("config-js", "nested-folder");
74+
75+
it("should load a config file in the parent directory", async () => {
76+
const configPath = await resolveConfigPath();
77+
78+
assert(configPath.endsWith("hardhat.config.js"));
79+
});
80+
});
81+
});
82+
83+
describe("typescript config", () => {
84+
describe("current dir", () => {
85+
useFixtureProject("config-ts");
86+
87+
it("should load a config file in the current directory", async () => {
88+
const configPath = await resolveConfigPath();
89+
90+
assert(configPath.endsWith("hardhat.config.ts"));
91+
});
92+
});
93+
94+
describe("nested dir", () => {
95+
useFixtureProject("config-ts", "nested-folder");
96+
97+
it("should load a config file in the parent directory", async () => {
98+
const configPath = await resolveConfigPath();
99+
100+
assert(configPath.endsWith("hardhat.config.ts"));
101+
});
102+
});
103+
});
104+
});
105+
106+
describe("programmatic API", () => {
107+
useFixtureProject("loaded-config");
108+
109+
it("should load the config file", async () => {
110+
const hre = await import("../../src/index.js");
111+
112+
assert.deepEqual(hre.config.plugins, [{ id: "test-plugin" }]);
113+
114+
resetHardhatRuntimeEnvironmentSingleton();
115+
});
33116
});
34117
});
35118
});

0 commit comments

Comments
 (0)