ethereumjs-monorepo

EthereumJS - Developer Docs

This guide provides an overview of the monorepo, development tools used, shared configuration and additionally covers some advanced topics.

It is intended to be both an entrypoint for external contributors as well as a reference point for team members.

Contents

Monorepo

Structure

The EthereumJS project uses npm workspaces to manage all the packages in our monorepo and link packages together.

Key Directories

Scripts

The ./config/cli directory contains helper scripts referenced in package.json files:

Workflow

Common Commands

Working on a Specific Package

To focus on a single package (e.g., VM):

  1. Navigate to the package directory: cd packages/vm
  2. Run tests: npm run test
  3. Run a specific test: npx vitest test/path/to/test.spec.ts
  4. Build just that package: npm run build --workspace=@ethereumjs/vm

Releases

Overview

Releases are done in sync for all active packages and all libraries are always bumped to a same new version number. Library combinations with matching versions are CI tested and ensured to be compatible with each other.

Most release rounds are done as bugfix releases, including releases of non-finalized EIP versions. Minor releases are done for hardfork finalization and otherwise outstanding selected features. Major release rounds are rarely done and are reserved to bundle structural breaking changes which come along significant changes to the API.

Process

Version/Dependency Update & Publish Script

We have a release script that handles version bumping and publishing for all packages. It supports both regular releases and lightweight in-between releases (nightly, alpha).

tsx scripts/release-npm.ts [--bump-version=<version>] [--publish=<tag>] [--scope=<scope>] [--otp=<code>]

Options:

With no flags, the script publishes the current package versions to npm under the latest tag (typical flow after a manual version bump and CHANGELOG prep).

npm authentication (required for publish):

The script runs npm whoami before publishing and exits if you are not authenticated.

What the script does:

Examples:

# Publish current versions (after bump + CHANGELOG prep) — most common
tsx scripts/release-npm.ts

# Same, explicit tag
tsx scripts/release-npm.ts --publish=latest

# Bump versions only (no publish) - for preparing a release
tsx scripts/release-npm.ts --bump-version=10.1.0

# Bump versions and publish - full release in one step
tsx scripts/release-npm.ts --bump-version=10.1.0 --publish=latest

# Lightweight nightly release
tsx scripts/release-npm.ts --bump-version=10.1.1-nightly.1 --publish=nightly

# Publish with 2FA one-time password
tsx scripts/release-npm.ts --otp=123456
Fork Releases (Feel Your Protocol)

The release script supports publishing all packages under a different npm scope via the --scope flag. This is used by Feel Your Protocol to publish fork releases from feature branches (e.g. EIP prototype implementations) that can be integrated as separate dependencies alongside the official @ethereumjs/* packages.

# Publish all packages under @feelyourprotocol scope
tsx scripts/release-npm.ts --scope=feelyourprotocol --bump-version=8141.0.0 --publish=latest

When --scope is set to a value other than ethereumjs, the script:

The --scope flag is fully generic and not tied to any specific npm org.

CHANGELOG Preparation

The following prompt has been tested with Cursor IDE to work well for CHANGELOG updates (please update placeholders in the first paragraph accordingly):

I want to do a new release round for all active packages listed in @README.md. Version bump has already been done, see an exemplary [ REFERENCE package.json FILE ] file, release is target for today. Last release round has been done on [ ENTER DATE IN FORMAT: April 29 2025 ] along commit [ ENTER COMMIT HASH, e.g.: 9e461f54312bf20c710b43ab73f7d3ad753f8765 ]. An exemplary CHANGELOG.md file is [ REFERENCE e.g. block CHANGELOG.md file ].

Can you please add new sections in the CHANGELOG files and add one-line summaries for the user-facing changes? For this please go for the commits since last release, one commit represents one PR due to our (squash) merge policy. You can leave out PRs only updating documentation, code in the examples folder or tests. Also tooling infrastructure (linting,...) and CI updating PRs can be left out. New support for new and deprecation for older Node.js as well as TypeScript versions should be added. Version updates for external dependencies - so not from within the monorepo - should be added as well.

Here is an example for the format of a change/PR entry:

- New default hardfork: `Shanghai` -> `Cancun`, see PR [#3566](https://github.com/ethereumjs/ethereumjs-monorepo/pull/3566)

For the CHANGELOG files you have not added lines in this step please nevertheless add a CHANGELOG entry (we do releases for all active packages no matter the changeset) and enter the following sentence: Maintenance release, no active changes.

Windows Users Note

Windows users might encounter errors with script paths. To fix, configure Git bash as the script shell:

npm config set script-shell "C:\\Program Files (x86)\\git\\bin\\bash.exe"

To reset this setting:

npm config delete script-shell

Conventions

This chapter documents the recurring code patterns across the active packages as they exist today — it is descriptive, not aspirational. Some inconsistencies are called out honestly; a few are being actively worked on.

Constructor Functions (createX)

Most objects are not created with a public new X(...) call. Instead each package exposes free-standing factory functions named createX…, typically grouped in a constructors.ts file. The class constructor is generally intended for internal/advanced use; the createX functions are the public entry points and often do async setup or input normalization the bare constructor cannot.

Examples (verified):

A common sub-pattern is createXFromY for alternate input formats (…FromRLP, …FromRPC, …FromBytesArray, …FromString, …FromPrivateKey).

Per-Package File Layout (types.ts, constructors.ts, params.ts)

Active packages converge on a small set of conventionally named modules:

Options-Object Arguments

Constructors and createX functions take an options object rather than a positional argument list, named XOpts or XOptions. This keeps call sites readable and lets new optional fields be added without breaking existing callers. Examples: EVMOpts (packages/evm/src/types.ts:220), VMOpts (packages/vm/src/types.ts:101), CommonOpts (packages/common/src/types.ts:144), BlockOptions (packages/block/src/types.ts:20).

Events (eventemitter3)

Packages that emit events expose an events property typed with eventemitter3 and a corresponding event-map type in types.ts:

Note one inconsistency: on the concrete classes events is always defined, but in the corresponding option interfaces it is events? optional (e.g. packages/evm/src/types.ts:182, packages/blockchain/src/types.ts:91). Event handlers receive an optional resolve callback so emission can await async listeners.

Errors

Error handling is not yet uniform across the monorepo (this is known and being addressed):

When adding new errors, prefer EthereumJSError with a real code. The commented-out error-code machinery in packages/util/src/errors.ts is a work in progress; do not extend it ad hoc.

Where EIP / Parameter Config Lives

There are two complementary mechanisms:

  1. Hardfork / EIP activation is owned by @ethereumjs/common: which hardforks exist (packages/common/src/hardforks.ts), which EIPs they activate (packages/common/src/eips.ts), and per-chain config (packages/common/src/chains.ts). Code gates behavior with common.isActivatedEIP(n) and common.gteHardfork(...).
  2. Numeric parameter values (gas costs, limits, …) are decentralized into each package in a params.ts file exporting an EIP-indexed ParamsDict: paramsEVM (packages/evm/src/params.ts), paramsVM (packages/vm/src/params.ts), paramsTx (packages/tx/src/params.ts), paramsBlock (packages/block/src/params.ts). At construction time a package merges its dictionary into the shared Common via common.updateParams(opts.params ?? paramsX) (e.g. packages/evm/src/evm.ts:384, packages/vm/src/vm.ts:72). Values are then read with common.param('name'), resolved against the active hardfork/EIP set.

So: what is active lives in common; what each value is lives next to the code that uses it, and is injected into common at runtime. Consumers can override parameters by passing a custom params option.

Naming Rules (createX, xToY, isX)

.ts-Extension ESM Imports

Relative imports inside src/ are written with explicit .ts extensions, e.g. import { createEIP7708TransferLog } from './eip7708.ts' (packages/evm/src/evm.ts:25). This is enabled by allowImportingTsExtensions + rewriteRelativeImportExtensions in the root tsconfig.json; the compiler rewrites the extension to .js in emitted output, so published code imports .js as normal. Always include the extension on relative imports.

Development Tools

TypeScript

All packages use TypeScript with a shared base configuration.

Configuration Files

Each package should have:

Example tsconfig.json:

{
  "extends": "../../config/tsconfig.json",
  "include": ["src/**/*.ts", "test/**/*.ts"]
}

Example tsconfig.prod.json:

{
  "extends": "../../config/tsconfig.prod.json",
  "include": ["src/**/*.ts"],
  "compilerOptions": {
    "outDir": "./dist"
  }
}

Build Commands

Use these commands in your package scripts:

{
  "scripts": {
    "tsc": "../../config/cli/ts-compile.sh",
    "build": "../../config/cli/ts-build.sh"
  }
}

Linting

We use ESLint v9 and Biome for code style enforcement and linting.

Configuration Files

Each package includes:

Commands

Commands area available on both root and package levels.

Run npm run lint to find lint issues and npm run lint:fix to fix fixable lint issues.

Spellcheck

We use cspell to do spellchecking.

Configuration Files

The following two configuration files include a list of allowed words (add yours if you have one necessary) as well as some additional configuration, separate for docs and code.

Commands

Commands area available on both root and package levels.

{
  "scripts": {
    "sc": "npm run spellcheck",
    "spellcheck": "npm run spellcheck:ts && npm run spellcheck:md",
    "spellcheck:ts": "npx cspell --gitignore -c ../../config/cspell-ts.json ...",
    "spellcheck:md": "npx cspell --gitignore -c ../../config/cspell-md.json ..."
  }
}

Testing

The project uses Vitest for testing with c8 for code coverage.

PR CI (Build) runs package tests for the lowest-touched workspace packages and everything that depends on them (scripts/ci-affected.mjs). Lint and typecheck always run. Examples and browser tests run for affected packages. Coverage is collected only for packages with direct changes; dependents run tests without coverage. Pushes to master, workflow dispatch, and shared-config changes (.github/, config/, lockfile, …) still run the full matrix. Nightly runs are unchanged.

Set the required status check to Build / CI. Per-package jobs may be skipped; that summary job is what must pass. In-flux EST dev suites (vm-est-dev) run when VM is affected but are not part of that gate.

General

Each package includes one or more test scripts. To run all tests in any package, use npm run test. Refer to the package.json for more specifics.

To run a specific test and watch for changes:

npx vitest test/path/to/test.spec.ts

Browser

We use vitest with playwright to run browser tests in real Chromium (headless).

Local: browser tests are optional unless you are working on bundling or browser-specific behaviour. Install Chromium once (Chromium only — not the full Playwright browser set):

npm run install-browser-deps
# equivalent to: npx playwright install chromium

Then run npm run test:browser in a package, or npm run test:browser from the monorepo root.

CI: deps are restored on the host runner (same cache as other jobs). Browser tests run in the official Playwright Docker image (mcr.microsoft.com/playwright:v1.60.0-noble) via docker run — preinstalled browsers, no Chromium download per run. Keep the image tag in .github/workflows/browser.yml in sync with the playwright version in package-lock.json.

Advanced Topics

Linking to an External Library

Quick Summary

To test packages with an external project locally, use npm link:

  1. Build the package you want to test:
    cd packages/package-name
    npm run build
    
  2. Link the package globally:
    npm link
    
  3. In your test project, link to the local package:
    cd path/to/your/project
    npm link @ethereumjs/package-name
    
  4. When you make changes to your package, rebuild it for the changes to be reflected.

  5. When done testing, unlink: ```sh

    In your test project

    npm unlink –no-save @ethereumjs/package-name

In the package directory

npm unlink ```

When making changes to the linked package, rebuild it for the changes to be reflected in your test project.

Shared Dependencies

Common development dependencies (e.g. eslint, biome) are defined in the root package.json.

Additional Docs

There are selected additional developer docs available to get more deep on certain topics. The following is an overview.

VM

VM Docs for testing, debugging and VM/EVM profiling.

Client

Client Docs for running Hive tests.