Imports

esdev finds ./util for util.ts, a directory for its index.ts, and ./util.js for the util.ts that TypeScript tells you to spell that way. That is what esdev build already does, so a source tree written for a build step runs under the runner without being rewritten first.

esrun resolves only what the module spec says — which is Node's answer too, since an extensionless specifier was never valid ESM and extension guessing is a bundler convention. The artifact you deploy has been through a build, which resolved all of it, so ship the build rather than the source.

import { two } from "./util"
esdev, Bun, a bundlerresolves util.ts
esrun, NodeERR_MODULE_NOT_FOUND

Aliases

A test resolves an import the way the build does. Two places can name an alias, and both apply, in this order:

esdev.json resolve.alias{ "resolve": { "alias": { "@": "./src" } } } — the longest name wins; a path or a package name. See Aliases
tsconfig.json paths"paths": { "~/*": ["src/*"] }, with baseUrl and extends, from the tsconfig that owns the importing file. A mapping is used only when it names a file
jsconfig.json pathsA JavaScript project's, at the project root, read when there is no tsconfig.json beside it — by the build too

mock.module("@/db", …) resolves through the same aliases, so it replaces the module an import reaches. A dependency in node_modules is not under the project's tsconfig.json, but esdev.json's resolve.alias applies to it, as in a build: "react": "preact/compat" under resolve.alias is for dependencies too.

esdev <file> resolves the same way, and so do --changed and --related, so a change to src/db.ts selects the test that imports it as @/db.

CommonJS packages

esdev runs a CommonJS package without a build step. The first run that imports one converts it to an ES module under node_modules/.esdev/deps/; later runs reuse the result until the package or the lockfile changes. A new install's first conversion removes the previous install's, so the directory holds one install's worth.

JavaScript
import IntlMessageFormat, { IntlMessageFormat as Named } from "intl-messageformat";
Named importsRead from the source, as Node reads them: exports.x =, Object.defineProperty(exports, "x", …), and re-exported files
The default importmodule.exports, or its default when the package was compiled from ES modules — the same as esdev build
Shared packagesA package another one requires is loaded once, so react-dom and your test use the same React
Mockingmock.module("pkg", …) and mock.importActual work on a converted package
Node builtinsA require("fs") fails where it runs, naming the package, not at import

The same conversion applies to esdev app.ts. esrun still loads ES module packages only, so ship the output of esdev build.

Last updated on
Edit this page