Skip to content
LogoLogo

Solidity imports

Every app in this repository except apps/next and apps/mud uses the same core Tevm feature: importing a .sol file directly from JavaScript or TypeScript.

import { ExampleContract } from './ExampleContract.sol'

At build time a Tevm bundler plugin compiles the Solidity with solc and replaces the import with a Contract object carrying the ABI, the deployed bytecode, and typed action creators.

The three pieces

A working setup always has three parts. Missing any one of them produces a confusing failure, so check all three before debugging further.

1. A bundler plugin

Pick the one matching your bundler. Each is a drop-in plugin — no other configuration is required.

import { bunPluginTevm } from '@tevm/bun-plugin'
import { plugin } from 'bun'
 
plugin(bunPluginTevm({}))

For Bun the plugin must be preloaded, including in tests — that is what apps/bun/bunfig.toml does:

preload = ["./plugins.ts"]
 
[test]
preload = ["./plugins.ts"]

2. The TypeScript plugin

Bundler plugins only run at build time. Your editor and tsc need @tevm/ts-plugin to resolve the same imports:

{
  "compilerOptions": {
    "plugins": [{ "name": "@tevm/ts-plugin" }]
  }
}

In VS Code you must also select the workspace TypeScript version, otherwise the bundled compiler runs without the plugin and every .sol import shows as unresolved.

apps/vite and apps/svelte-ethers ship a solidity.d.ts so plain tsc runs and non-TS-plugin tooling do not choke on the import specifier.

Configuring solc resolution

tevm.config.json controls how Solidity sources and remappings are resolved. apps/vite uses a Foundry project, so it only needs one flag:

{
  "foundryProject": true,
  "jsonAsConst": ["**/foo.json"]
}

With foundryProject: true the plugin reads foundry.toml for remappings, src, and libs, so import "@openzeppelin/contracts/token/ERC721/ERC721.sol" inside your Solidity resolves exactly as it does under forge build.

What you get back

The imported object is a Tevm Contract. Two shapes matter in practice:

import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
import { ExampleContract } from './ExampleContract.sol'
 
const publicClient = createPublicClient({
  chain: mainnet,
  transport: http('https://eth.llamarpc.com'),
})
 
// 1. `.abi` — a plain abitype-typed ABI, usable anywhere
const abi = ExampleContract.abi
 
// 2. `.read.<fn>(...)` — a pre-built, fully typed read action
const owner = await publicClient.readContract({
  ...ExampleContract.read.ownerOf(1n),
  address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2',
})
 
console.log(abi.length, owner)

Spreading ExampleContract.read.ownerOf(1n) supplies abi, functionName, and args in one go; you only add the address. apps/esbuild is built entirely on this pattern — see the esbuild guide.