Skip to content
Merged
Show file tree
Hide file tree
Changes from 89 commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
8ada49c
Add spec
alcuadrado Mar 30, 2026
a99a5b9
Fix typos and cspell config
alcuadrado Apr 13, 2026
d72d7c1
Add config field to the builtin:solidity type extensions
alcuadrado Mar 31, 2026
dbaec84
Add config validation and resolution
alcuadrado Mar 31, 2026
be8bf71
Fix some tests that construct SolidityConfig objects manually
alcuadrado Mar 31, 2026
8f5c52b
Throw in the disabled apis
alcuadrado Mar 31, 2026
b6c93bf
Update getRootFilePaths()
alcuadrado Mar 31, 2026
7fd2e2b
Update emitArtifacts()
alcuadrado Mar 31, 2026
35752f6
Update getArtifactsDirectory()
alcuadrado Mar 31, 2026
6f75b54
Remove outdated assertion
alcuadrado Mar 31, 2026
2e1a389
Fix existing build scope tests, as they were based on split mode
alcuadrado Mar 31, 2026
1b26dd5
Add tests for the new behavior
alcuadrado Mar 31, 2026
770b1be
Update the plan so that phase 4 fixes the tests that needed build to …
alcuadrado Mar 31, 2026
b3c2444
Reintroduced accidentally deleted descriptor
alcuadrado Apr 13, 2026
d863c0e
Reintroduced accidentally deleted jsdoc
alcuadrado Apr 13, 2026
9b79ed8
Tiny optimization to getScope()
alcuadrado Apr 13, 2026
ed5c4c5
Make test portable
alcuadrado Apr 13, 2026
832063f
Include the artifacts directory and if a root file emitted ts types o…
alcuadrado Apr 12, 2026
69a4ed0
Address copilot feedback
alcuadrado Apr 13, 2026
9518909
Add missing test
alcuadrado Apr 13, 2026
dff3b3d
Document expected failures after this phase
alcuadrado Apr 12, 2026
47f513f
Update the spec to simplify how --no-contracts and --no-tests work wi…
alcuadrado Apr 12, 2026
aeeab09
Update build-cleanup-artifacts tests to run full builds
alcuadrado Apr 12, 2026
fe0400a
Update the build task
alcuadrado Apr 12, 2026
4c56c96
Adds SolidityBuildSystem tests for splitTestCompilation:false behavior
alcuadrado Apr 12, 2026
42170c7
Add cleanup artifacts tests for unified builds
alcuadrado Apr 12, 2026
6a6bd2e
Add tests of the build task when using unified builds and update the …
alcuadrado Apr 12, 2026
7ef6c6a
Make test portable
alcuadrado Apr 13, 2026
3868caf
Add missing error descriptor
alcuadrado Apr 13, 2026
dee44f5
Fix return type of build task
alcuadrado Apr 13, 2026
3e8e721
Update bulitin tasks: run, test, and console
alcuadrado Apr 12, 2026
a405dc4
Fix existing race condition
alcuadrado Apr 12, 2026
c3d1d31
Update the SPEC to reflect some DX improvements
alcuadrado Apr 12, 2026
6db5c9c
Update test solidity task
alcuadrado Apr 12, 2026
e7eef17
Update the tests
alcuadrado Apr 12, 2026
cdcb497
Remove duplicated code
alcuadrado Apr 12, 2026
7114817
Simplify and optimize code a bit
alcuadrado Apr 12, 2026
768b800
Fix typos and cspell config
alcuadrado Apr 12, 2026
0460272
Minor simplification
alcuadrado Apr 12, 2026
a1fa452
Disable spellcheck in the spec file
alcuadrado Apr 13, 2026
8fee584
Improve type-safety
alcuadrado Apr 13, 2026
bda6aa6
spellcheck
alcuadrado Apr 13, 2026
835dd6e
Get scopes in parallel
alcuadrado Apr 13, 2026
43f99fb
Fix typo
alcuadrado Apr 13, 2026
a63341e
Fix test files resolution
alcuadrado Apr 13, 2026
3dd4360
Validate that the provided test files exist
alcuadrado Apr 13, 2026
3322ffd
Add integration tests for the ArtifactManager in unified build mode
alcuadrado Apr 12, 2026
3ee06b2
Update the typechain plugin to filter out tests in unified mode
alcuadrado Apr 12, 2026
3997e74
Add a test of an npm root
alcuadrado Apr 12, 2026
5c3dedd
Remove duplicated test
alcuadrado Apr 13, 2026
a130e61
Sort FQNs when multiple candidates are found for a bare contract name
alcuadrado Apr 13, 2026
a78068c
Fix three path comparision bugs in the build system
alcuadrado Apr 13, 2026
0eff216
Update how the typechain hook get the right contract paths in unified…
alcuadrado Apr 13, 2026
972369b
Update the tests
alcuadrado Apr 13, 2026
b80aa4a
Improve comment
alcuadrado Apr 13, 2026
c43aaf5
Update hardhat-mocha
alcuadrado Apr 12, 2026
d78de32
Cleanup the tests
alcuadrado Apr 13, 2026
a551ab3
Simplify tests
alcuadrado Apr 13, 2026
c0f9741
Test noCompile with and without splitTestsCompilation
alcuadrado Apr 13, 2026
f14d975
Update hardhat-node-test-runner
alcuadrado Apr 12, 2026
6695e09
Improve tests
alcuadrado Apr 13, 2026
9641ac4
Update hardhat-ignition
alcuadrado Apr 12, 2026
24ef213
Improve tests
alcuadrado Apr 13, 2026
4f5a3c5
Add PLUGIN_MIGRATION_GUIDE.md
alcuadrado Apr 12, 2026
3d2cb10
Add empty file to trigger the full CI
alcuadrado Apr 12, 2026
1efe5e6
Remove code duplication in the SolidityBuildSystemImplementation
alcuadrado Apr 13, 2026
ce19625
Small type-safety improvement in build task
alcuadrado Apr 13, 2026
3602f5c
Improve error descriptor
alcuadrado Apr 13, 2026
c1ef662
Update plugin migration guide
alcuadrado Apr 13, 2026
c90a604
Remove the spec
alcuadrado Apr 13, 2026
b6fdcac
Merge pull request #8128 from NomicFoundation/dont-split-compilations…
alcuadrado Apr 16, 2026
b7ed426
Add comment
alcuadrado Apr 16, 2026
ba2ab6a
Merge pull request #8129 from NomicFoundation/dont-split-compilations…
alcuadrado Apr 16, 2026
5928034
Merge pull request #8130 from NomicFoundation/dont-split-compilations…
alcuadrado Apr 16, 2026
2163afb
Improve type
alcuadrado Apr 16, 2026
97a5215
Improve doc comment
alcuadrado Apr 16, 2026
4409680
Rename function
alcuadrado Apr 16, 2026
9777d1c
Merge pull request #8131 from NomicFoundation/dont-split-compilations…
alcuadrado Apr 16, 2026
8b4f0b2
Merge pull request #8132 from NomicFoundation/dont-split-compilations…
alcuadrado Apr 16, 2026
55586b7
Use the right type
alcuadrado Apr 16, 2026
c67029d
Add a comment
alcuadrado Apr 16, 2026
5697e12
Merge pull request #8133 from NomicFoundation/dont-split-compilations…
alcuadrado Apr 16, 2026
3b58254
Merge pull request #8134 from NomicFoundation/dont-split-compilations…
alcuadrado Apr 16, 2026
021fe6e
Merge pull request #8135 from NomicFoundation/dont-split-compilations…
alcuadrado Apr 16, 2026
b61878e
Merge pull request #8136 from NomicFoundation/dont-split-compilations…
alcuadrado Apr 16, 2026
f94b313
Merge pull request #8137 from NomicFoundation/dont-split-compilations…
alcuadrado Apr 16, 2026
32c5c1c
Update PLUGIN_MIGRATION_GUIDE.md
alcuadrado Apr 16, 2026
2931710
Update PLUGIN_MIGRATION_GUIDE.md
alcuadrado Apr 16, 2026
fbe80d4
Merge pull request #8138 from NomicFoundation/dont-split-compilations…
alcuadrado Apr 16, 2026
353cf86
Add hardhat changseet
alcuadrado Apr 16, 2026
4fe12fe
Add plugins changesets
alcuadrado Apr 16, 2026
e3b5324
Add peer bumps
alcuadrado Apr 16, 2026
7fb929e
Delete plugin migration guide
alcuadrado Apr 16, 2026
ff3109b
Delete empty file
alcuadrado Apr 16, 2026
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
151 changes: 151 additions & 0 deletions PLUGIN_MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Plugin Migration Guide: `splitTestsCompilation`

## Overview

This version of Hardhat introduces a `splitTestsCompilation` Solidity config field that controls whether Solidity test files are compiled in a separate pass from contract files. Previous to this version, they were always compiled separately.

```typescript
export default {
solidity: {
version: "0.8.28",
splitTestsCompilation: true, // opt in to the previous two-pass behavior
},
};
```

- **Default: `false`** — contracts and tests are compiled together in a single pass under `scope: "contracts"`.
- **`true`** — contracts and tests are compiled in separate passes (the previous default behavior).

This guide covers what changes for plugin authors and how to adapt.

## Configuration

The field is accepted in all object-typed Solidity user configs (`SingleVersionSolidityUserConfig`, `MultiVersionSolidityUserConfig`, `BuildProfilesSolidityUserConfig`). String and string-array configs always resolve to `false`.

The resolved value is available at `hre.config.solidity.splitTestsCompilation`.

## Build System (`hre.solidity`)

### `scope: "tests"` is rejected when `splitTestsCompilation` is `false`

When `splitTestsCompilation` is `false`, tests are compiled together with contracts under `scope: "contracts"`. Using `scope: "tests"` is a logic error and throws a `HardhatError` with descriptor `SOLIDITY.SPLIT_TESTS_COMPILATION_DISABLED` in the following APIs:

- `hre.solidity.getRootFilePaths({ scope: "tests" })`
- `hre.solidity.build(rootFiles, { scope: "tests" })`
- `hre.solidity.getCompilationJobs(rootFiles, { scope: "tests" })`
- `hre.solidity.emitArtifacts(compilationJob, compilerOutput, { scope: "tests" })`
- `hre.solidity.cleanupArtifacts(rootFiles, { scope: "tests" })`

**Exception:** `hre.solidity.getArtifactsDirectory("tests")` does **not** throw. It returns the main artifacts path (same as `"contracts"`), since it is a read-only query with no side effects.

### `getRootFilePaths({ scope: "contracts" })`

When `splitTestsCompilation` is `false`, this returns **all** build roots — contract roots, test roots, and npm roots — together.

When `splitTestsCompilation` is `true`, it returns contract roots and npm roots only (unchanged).

### `getArtifactsDirectory(scope)`

| `splitTestsCompilation` | `scope: "contracts"` | `scope: "tests"` |
| ----------------------- | -------------------- | -------------------------- |
| `false` | `artifactsPath` | `artifactsPath` |
| `true` | `artifactsPath` | `cachePath/test-artifacts` |

### File classification is unchanged

`hre.solidity.getScope(fsPath)` continues to classify files as `"contracts"` or `"tests"` based on path and suffix rules. Use this API to distinguish contract files from test files when processing mixed sets.

### `cleanupArtifacts()`

When `splitTestsCompilation` is `false`:

- Cleanup operates on the main artifacts directory.
- Duplicate contract-name detection runs across the mixed contract/test artifact set.
- `onCleanUpArtifacts` receives the mixed contract/test artifact set, so if you are hooking into it, you may need to adapt your Hook Handler. See below.

## Artifacts

When `splitTestsCompilation` is `false`, both contract and test artifacts live under the same `paths.artifacts` directory. This means:

- `getAllArtifactPaths()` includes test artifacts.
- `getAllFullyQualifiedNames()` includes test artifacts.
- Bare-name lookup can become **ambiguous** if a test contract and a source contract share the same name. Ambiguous names type to `never` in the generated `artifacts.d.ts`. Users hitting a collision should switch to fully qualified names (e.g. `"contracts/Foo.sol:Foo"` instead of `"Foo"`); this affects APIs like `hardhat-ethers`'s `getContractFactory` / `getContractAt` / `deployContract`, `hardhat-viem`'s `deployContract` / `getContractAt`, `hardhat-verify`'s automatic contract inference, and `hardhat-ignition`'s artifact resolution.
- Fully qualified name lookup continues to work without ambiguity.
- **Test roots do not get per-source `artifacts.d.ts` files.** Only contract roots emit TypeScript declarations. They are not part of the `ArtifactMap` interface from `hardhat/types/artifacts`.
- This means that test contracts aren't part of the autocompletion in the `ethers` and `viem` plugins.

Plugins using `hre.artifacts` must no longer assume that "artifacts path" means "contracts only."

### Filtering test artifacts

You can take a look at the `hardhat-typechain` plugin to understand how to filter out the test artifacts.

## Build Task (`hardhat build` / `hardhat compile`)

### When `splitTestsCompilation` is `false`

The build task uses a **single compilation pass** under `scope: "contracts"`.

<!-- prettier-ignore -->
| Scenario | Behavior |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Full build (no flags, no files) | Compiles all contracts and tests. Runs cleanup. Regenerates top-level `artifacts.d.ts`. Fires `onCleanUpArtifacts`.|
| Explicit `files` | Partial build of exactly those files. No cleanup. No `artifacts.d.ts` regeneration. No `onCleanupArtifacts`. |
| `--no-tests` (no files) | Partial build of contract roots only. No cleanup. No `artifacts.d.ts` regeneration. No `onCleanupArtifacts`. |
| `--no-contracts` (no files) | Partial build of test roots only. No cleanup. No `artifacts.d.ts` regeneration. No `onCleanupArtifacts`. |
| `files` + compatible flag (e.g. contract files + `--no-tests`) | Partial build of the provided files. No cleanup. No `artifacts.d.ts` regeneration. No `onCleanupArtifacts`. |
| `files` + incompatible flag (e.g. test files + `--no-tests`) | Throws `HardhatError` with descriptor `SOLIDITY.INCOMPATIBLE_FILES_WITH_BUILD_FLAGS`. |

**Important:** `--no-tests` and `--no-contracts` behave as synthetic partial builds when `splitTestsCompilation` is `false`. This is different from `splitTestsCompilation: true`, where `--no-tests` runs a full contracts build with cleanup. Plugins that depend on cleanup running after `--no-tests` should account for this.

### When `splitTestsCompilation` is `true`

Current two-pass behavior is preserved. `--no-tests` and `--no-contracts` each skip one full pass with cleanup.

### Return value

Both modes return:

```typescript
{
contractRootPaths: string[];
testRootPaths: string[];
}
```

The arrays reflect the roots actually built, partitioned using `getScope()`.

### Plugin pattern for calling `build`

If your plugin calls `build` with `noTests: true`, update it to branch on the config. In unified mode (`splitTestsCompilation: false`), passing `noTests: true` turns the build into a partial build — cleanup and `artifacts.d.ts` regeneration are skipped. Since tests are compiled in the same pass as contracts anyway, there is no performance benefit to excluding them, and the plugin loses full-build semantics it likely depends on.

```typescript
// Before
await hre.tasks.getTask("build").run({ noTests: true });

// After: only skip tests when they live in a separate compilation pass.
const noTests = hre.config.solidity.splitTestsCompilation;
await hre.tasks.getTask("build").run({ noTests });
```

## Solidity Hooks

### `build` hook

When `splitTestsCompilation` is `false`, full builds call the hook **once** with `scope: "contracts"` and a mixed set of contract and test roots. Plugins that need contract-only behavior must filter per file with `getScope()`.

### `preprocessProjectFileBeforeBuilding`

The same compilation may include both contract and test files. Plugins can distinguish with `context.solidity.getScope(fsPath)`.

### `preprocessSolcInputBeforeBuilding`

`solcInput.sources` may contain both contract and test sources together when `splitTestsCompilation` is `false`.

### `onCleanUpArtifacts`

When `splitTestsCompilation` is `false`, this hook only fires during full builds (not partial builds from `--no-tests`, `--no-contracts`, or explicit files). It receives mixed contract/test artifact paths. Take a look at the `hardhat-typechain` plugin for an example of how to filter out test artifacts.

### Unchanged hooks

`downloadCompilers`, `getCompiler`, `invokeSolc`, `readSourceFile`, and `readNpmPackageRemappings` are not affected.
4 changes: 4 additions & 0 deletions cspell.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,9 @@ export default defineConfig({
"**/artifacts/**/*.json",
"**/artifacts/**/*.d.ts",
"**/build-info/**/*",
"packages/*/artifacts",
"packages/*/cache",
"packages/*/test/fixture-projects/**/artifacts",
"packages/*/test/fixture-projects/**/cache",
],
});
56 changes: 56 additions & 0 deletions packages/hardhat-errors/src/descriptors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1192,6 +1192,36 @@ Remaining test suites: {suites}`,
websiteDescription:
"An inline config key was used that does not apply to the type of test function it was attached to. Fuzz test functions (test*) only accept fuzz.* keys and top-level keys, while invariant test functions (invariant*) only accept invariant.* keys and top-level keys.",
},
SELECTED_TEST_FILES_NOT_COMPILED: {
number: 814,
messageTemplate: `The following Solidity test files have not been compiled:

{files}

Run \`hardhat build\` to compile your project before running tests with \`--no-compile\`.`,
websiteTitle: "Selected Solidity test files not compiled",
websiteDescription: `You ran Solidity tests with \`--no-compile\`, but some of the selected test files have not been compiled yet. Run \`hardhat build\` first, or remove the \`--no-compile\` flag.`,
},
SELECTED_FILES_ARE_NOT_SOLIDITY_TESTS: {
number: 815,
messageTemplate: `Trying to run these files as Solidity tests, but they aren't:

{files}

Double-check the files that you are providing to the \`test solidity\` task.`,
websiteTitle: "Invalid Solidity test files",
websiteDescription: `You ran the \`test solidity\` task with files that aren't classified as Solidity tests.`,
},
SELECTED_TEST_FILES_DO_NOT_EXIST: {
number: 816,
messageTemplate: `The following Solidity test files do not exist:

{files}

Double-check the paths you are providing to the \`test solidity\` task.`,
websiteTitle: "Selected Solidity test files do not exist",
websiteDescription: `You ran the \`test solidity\` task with files that do not exist on disk.`,
},
},
SOLIDITY: {
PROJECT_ROOT_RESOLUTION_ERROR: {
Expand Down Expand Up @@ -1346,6 +1376,32 @@ Solidity test files must be placed in your test directory, or in your contracts

Solidity test files must be placed in your test directory, or in your contracts directory and end in .t.sol.`,
},
SPLIT_TESTS_COMPILATION_DISABLED: {
number: 916,
messageTemplate: `A method of the SolidityBuildSystem was called with \`scope: "tests"\`, but \`splitTestsCompilation\` is disabled in your config.

When \`splitTestsCompilation\` is \`false\`, contracts and tests are compiled together under \`scope: "contracts"\`, so \`scope: "tests"\` is not a valid option.

Set \`solidity.splitTestsCompilation\` to \`true\` in your Hardhat config to enable this build scope.`,
websiteTitle: "Split tests compilation is disabled",
websiteDescription: `The Solidity build system was called with \`scope: "tests"\`, but \`splitTestsCompilation\` is disabled in your config.

When \`splitTestsCompilation\` is \`false\`, contracts and tests are compiled together under \`scope: "contracts"\`, so \`scope: "tests"\` is not a valid option.

Set \`solidity.splitTestsCompilation\` to \`true\` in your Hardhat config to enable this build scope.`,
},
INCOMPATIBLE_FILES_WITH_BUILD_FLAGS: {
number: 917,
messageTemplate: `Some of the files you are trying to build are incompatible with the \`--no-contracts\` or \`--no-tests\` flag you provided:

{files}

Try re-running without these files, or without the flag.`,
websiteTitle: "Incompatible files with build flags",
websiteDescription: `You are trying to build a list of files while using \`--no-contracts\` or \`--no-tests\`, but some of those files are incompatible with the flag you provided.

For example, you may be trying to build a test file with \`--no-tests\`, which isn't a valid operation.`,
},
},
ARTIFACTS: {
NOT_FOUND: {
Expand Down
3 changes: 2 additions & 1 deletion packages/hardhat-ignition/src/internal/tasks/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,10 @@ const taskDeploy: NewTaskActionFunction<TaskDeployArguments> = async (
);
}

const noTests = hre.config.solidity.splitTestsCompilation;
await hre.tasks.getTask("build").run({
quiet: true,
noTests: true,
noTests,
defaultBuildProfile: "production",
});

Expand Down
3 changes: 2 additions & 1 deletion packages/hardhat-ignition/src/internal/tasks/visualize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ const visualizeTask: NewTaskActionFunction<TaskVisualizeArguments> = async (
{ noOpen, modulePath }: { noOpen: boolean; modulePath: string },
hre: HardhatRuntimeEnvironment,
) => {
await hre.tasks.getTask("build").run({ noTests: true, quiet: true });
const noTests = hre.config.solidity.splitTestsCompilation;
await hre.tasks.getTask("build").run({ noTests, quiet: true });

const userModule = await loadModule(hre.config.paths.ignition, modulePath);

Expand Down
95 changes: 95 additions & 0 deletions packages/hardhat-ignition/test/deploy/build-invocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";

import { assert } from "chai";
import { overrideTask } from "hardhat/config";
import { createHardhatRuntimeEnvironment } from "hardhat/hre";

import hardhatIgnitionPlugin from "../../src/index.js";

describe("deploy - build invocation", function () {
function buildArgCaptor() {
const buildArgs: any[] = [];
const buildOverride = overrideTask("build")
.setAction(async () => ({
default: async (args: any, _hre, runSuper) => {
buildArgs.push(args);
return runSuper(args);
},
}))
.build();
return { buildArgs, buildOverride };
}

function getProjectConfig() {
const projectPath = path.join(
path.dirname(fileURLToPath(import.meta.url)),
"../fixture-projects",
"minimal",
);

const configPath = path.join(projectPath, "hardhat.config.js");

return { projectPath, configPath };
}

it("should call build without noTests when splitTestsCompilation is false", async function () {
const { buildArgs, buildOverride } = buildArgCaptor();
const { projectPath, configPath } = getProjectConfig();

const { default: userConfig } = await import(
pathToFileURL(configPath).href
);

const hre = await createHardhatRuntimeEnvironment(
{
...userConfig,
plugins: [hardhatIgnitionPlugin],
tasks: [buildOverride],
},
{ config: configPath },
projectPath,
);

await hre.tasks.getTask(["ignition", "deploy"]).run({
modulePath: path.join(projectPath, "ignition", "modules", "MyModule.js"),
});

assert.equal(buildArgs.length, 1);
assert.equal(buildArgs[0].noTests, false);
assert.equal(buildArgs[0].defaultBuildProfile, "production");
assert.equal(buildArgs[0].quiet, true);
});

it("should call build with noTests when splitTestsCompilation is true", async function () {
const { buildArgs, buildOverride } = buildArgCaptor();
const { projectPath, configPath } = getProjectConfig();

const { default: userConfig } = await import(
pathToFileURL(configPath).href
);

const hre = await createHardhatRuntimeEnvironment(
{
...userConfig,
solidity: {
...userConfig.solidity,
splitTestsCompilation: true,
},
plugins: [hardhatIgnitionPlugin],
tasks: [buildOverride],
},
{ config: configPath },
projectPath,
);

await hre.tasks.getTask(["ignition", "deploy"]).run({
modulePath: path.join(projectPath, "ignition", "modules", "MyModule.js"),
});

assert.equal(buildArgs.length, 1);
assert.equal(buildArgs[0].noTests, true);
assert.equal(buildArgs[0].defaultBuildProfile, "production");
assert.equal(buildArgs[0].quiet, true);
});
});
Loading
Loading