Skip to content
LogoLogo

How it works

Every Tevm bundler plugin is a thin adapter. The interesting work happens in a shared core, which is why Vite, webpack, Bun and the TypeScript language service all agree about what a .sol import means.

The pipeline

your build tool
      │  "load ./Counter.s.sol"
      ▼
@tevm/<tool>-plugin      adapter: maps the tool's hooks onto the unplugin
      ▼
@tevm/unplugin           one plugin definition, seven build tools
      ▼
@tevm/base-bundler       orchestrates cache → resolve → compile → codegen
      │
      ├── @tevm/config          loads tevm.config.json + foundry.toml + tsconfig paths
      ├── @tevm/bundler-cache   .tevm/ artifact cache, keyed on a compile fingerprint
      ├── @tevm/resolutions     walks the Solidity import graph, applies remappings/libs
      ├── @tevm/compiler        drives solc over the graph, extracts ABI/bytecode/AST
      │     └── @tevm/solc      downloads and pins a specific solc version
      └── @tevm/runtime         emits the .ts / .mjs / .cjs / .d.ts module text
      ▼
a module exporting `createContract(...)` for each contract in the file

1. Configuration

@tevm/config resolves your tevm.config.json, merges in Foundry remappings and library paths when foundryProject is set, and fills in defaults. The result is a ResolvedCompilerConfig — every downstream package takes this as its first argument, so there is exactly one place where "what does this project's Solidity setup look like" is decided.

2. Cache lookup

@tevm/bundler-cache computes a fingerprint over the resolved config, the solc version, and the contents of every file in the import graph. On a hit, the cached artifacts are returned and solc never runs. See Caching.

3. Resolution

@tevm/resolutions reads the entry .sol file, finds its import statements, applies remappings and library search paths, and recurses. The output is a ModuleInfo graph: absolute path, raw code, rewritten code, and the list of imported modules for every file involved.

That graph is also what the bundler reports back to your build tool as watch dependencies, so editing an imported library triggers a rebuild of exactly the modules that transitively import it.

4. Compilation

@tevm/compiler turns the module graph into a solc standard JSON input, runs it through the solc instance provided by @tevm/solc, and extracts the ABI, NatSpec userdoc, and (when requested) the evm.bytecode / evm.deployedBytecode objects and the AST.

Bytecode is only requested for .s.sol files. This is a deliberate size optimisation: most application code reads from contracts that are already deployed and has no use for creation bytecode in the bundle.

5. Code generation

@tevm/runtime emits module text. For .s.sol the emitted module looks like this:

import { createContract } from '@tevm/contract'
 
const _Counter = {
	name: 'Counter',
	humanReadableAbi: ['function count() view returns (uint256)', 'function increment()'],
	bytecode: '0x60806040...',
	deployedBytecode: '0x60806040...',
} as const
 
export const Counter = createContract(_Counter)

The same artifacts can be emitted as .d.ts, .ts, .mjs or .cjs. The bundlers ask for ESM; the TypeScript plugin asks for .d.ts. That is the whole reason the editor and the build never disagree — they render the same artifacts through the same generator.

6. Cache write

The artifacts and the generated module are written back under .tevm/ so the next build, the next tsc run, and the language server all reuse them.

Why a shared core matters

The failure mode this design avoids is the one every ABI-codegen setup eventually hits: the editor's idea of a contract and the bundle's idea of a contract drift apart, and you get a green type check and a runtime revert. Here there is one resolver, one compiler invocation path, and one code generator.

If you want to drive that core yourself — for a build tool with no adapter, or a test harness, or a script — see Building a custom integration.