Shared conventions
These rules are enforced by CI or by review. Follow them when adding an app or changing an existing one.
Dependencies
- npm only. Tevm packages are installed from npm with caret ranges (
^1.0.0-next.149).workspace:,file:, andlink:protocols pointing attevm-monorepoare not allowed — they defeat the purpose of this repository (why). tevmis a peer dependency. Apps that import the node declare"tevm": ">=1.0.0"inpeerDependenciesand install a concrete version indevDependenciesfor local validation.- The one internal link is
apps/mud/packages/client→apps/mud/packages/contracts, because both live here. - pnpm 9.15.9, pinned by
packageManagerand enabled viacorepack enable. Installing with another major produces a lockfile diff CI rejects.
Every app must have
{
"scripts": {
"build": "…",
"test": "…",
"typecheck": "tsc --noEmit"
}
}The root build, test, and typecheck scripts fan out to every app by package name, and CI runs
pnpm lint && pnpm typecheck && pnpm build && pnpm test. An app with a missing script breaks the fan-out. Use
vitest run --passWithNoTests if there is genuinely nothing to test yet.
Network access in tests
Tests that need a real RPC endpoint must skip, not fail, when the endpoint is absent. CI has no RPC credentials.
import { expect, test } from 'vitest'
import { readContract } from './readContract'
// This integration test needs a caller-provided fork endpoint; CI has no RPC credentials.
const testWithRpc = process.env.RPC_URL ? test : test.skip
testWithRpc(readContract.name, async () => {
expect(await readContract()).toBe(BigInt(1))
})Document the variable in the app's README.md and, where useful, ship an example.env. Current variables:
| Variable | App |
|---|---|
RPC_URL | apps/bun |
RPC_URL_MAINNET | apps/esbuild |
VITE_RPC_URL_1, VITE_RPC_URL_420, VITE_ALCHEMY_API_KEY | apps/vite |
Formatting and linting
Biome, configured at the repo root (biome.json): tabs, single quotes, no semicolons in TypeScript.
pnpm lint # biome lint .
biome check . --write --unsafeapps/next is the exception — it uses Prettier with an import-sort plugin, run through its own
pnpm --filter @tevm/example-next format.
JSDoc
When you touch an exported symbol that has no JSDoc, add it. A complete block has a one-line summary, @param for
each parameter, @returns, @throws for every failure mode a caller can hit, and a runnable @example:
/**
* Reads `ownerOf(tokenId)` on the mainnet deployment of `ExampleContract`.
*
* @param {bigint} [tokenId] - Token id to look up. Defaults to `1n`.
* @returns {Promise<`0x${string}`>} The owner address.
* @throws If `RPC_URL_MAINNET` points at an unreachable endpoint, or if the token does
* not exist — ERC721 `ownerOf` reverts and viem throws a `ContractFunctionExecutionError`.
* @example
* ```js
* import { ownerOf } from './ownerOf.js'
*
* console.log(await ownerOf())
* ```
*/Examples in JSDoc must be complete and runnable. No ... elisions — someone will paste it.
TypeScript
@tevm/ts-pluginincompilerOptions.pluginsfor any app importing.sol.- A
solidity.d.tsambient declaration alongside it, sotscandsvelte-checkwork without the plugin. tsc --noEmit; nothing inapps/emits declarations.
Adding a new app
- Create
apps/<name>/with apackage.jsonfollowing the rules above. apps/*is already a workspace glob — nopnpm-workspace.yamlchange is needed unless you nest packages.- Add the package name to the root
build,test, andtypecheckscripts. - Add a
README.md, and a guide page underdocs/src/pages/guides/plus a sidebar entry indocs/vocs.config.ts. - Add a changeset if the change is user-visible.

