Modules
A file containing import/export runs as an ES module; a plain file runs as a classic script (and ff -e code is a classic script too — no top-level await there). import is the only loader — there is no require(), no CommonJS, and no URL imports. Three specifier shapes resolve: relative (./, ../), absolute (/), and bare (lodash).
Quick look
Top-level await works in every module — no async wrapper needed:
Relative imports: ./ and ../
Joined onto the importer's directory. An extensionless specifier probes
X.js, then X/index.js; anything written with an extension loads
literally:
Note: The probe only tries
X.jsandX/index.js— other extensions (.mjs,.json, …) must be written out. If neither probe hits, the loader error namesX.js.
.. segments normalize against the importer dir (./a/../b.js → ./b.js); escaping past the filesystem root resolves to the joined remainder and fails to load.
Bare imports: packages
Anything not starting with ., /, or containing :// is bare, and walks up the directory tree through node_modules/<pkg>:
For each level (starting at the importer's dir, then each parent), the first node_modules/<pkg> hit wins via these rules, in order:
Then the walk continues to the parent directory. Misses everywhere and the loader prints its standard filename error for the first candidate.
Two sharp edges, both fail loud:
- A manifest entry naming a file that isn't on disk is a broken install:
could not load module filename '…'and a hard error. It never silently falls through to a higher copy (same strictness as npm). - Subpath imports (
pkg/feature) need anexportsmap entry ("./feature") or a matching literal file. No map + no file = keep walking, then fail.
Install packages first — resolution needs the files:
Absolute paths
A leading / loads literally from the filesystem root:
No lookup, no fallback. Missing file = load error.
Practical example: tiny multi-file API
Reference
Troubleshooting
could not load module filename '…' — the specifier resolved to a path with no file. For relative imports: check the spelling and the extension. For bare imports: run ff imprint <pkg> and confirm node_modules/<pkg> exists from the importer's directory upward.
Extensionless relative import fails — the probe only tries X.js and X/index.js. For .mjs (or any other extension), write it out.
require is not defined — ESM only. Convert to import, or pick an ESM package.
Works locally, fails after move — bare resolution walks up from the importer's dir. Moving the importer without its node_modules ancestry breaks it; keep project files under the installed root.
Wrong file loaded from a package — the manifest's exports/module/main chain picked it. Inspect node_modules/<pkg>/package.json to see which entry won.
