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 bundler | resolves util.ts |
esrun, Node | ERR_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 paths | A 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.
import IntlMessageFormat, { IntlMessageFormat as Named } from "intl-messageformat";
| Named imports | Read from the source, as Node reads them: exports.x =, Object.defineProperty(exports, "x", …), and re-exported files |
| The default import | module.exports, or its default when the package was compiled from ES modules — the same as esdev build |
| Shared packages | A package another one requires is loaded once, so react-dom and your test use the same React |
| Mocking | mock.module("pkg", …) and mock.importActual work on a converted package |
| Node builtins | A 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.