"exports" faces outward; "imports" faces inward. It maps specifiers beginning with # to files inside your own package, visible only to code in that package -- the supported replacement for the ../../../ chains that grow in every Express 24,430 project, with no bundler, tsconfig path alias or NODE_PATH hack.
{
"name": "@shop/api", "type": "module",
"exports": { "./config": "./src/config.js" },
"imports": {
"#db": { "test": "./src/db/memory.js", "default": "./src/db/mongo.js" },
"#lib/*": "./src/*.js"
}
}import { store } from '#db';
import { port } from '#lib/config';
import { port as viaSelf } from '@shop/api/config';
console.log(store, port, viaSelf === port);MongoDB driver 3000 true
Running node -C test src/server.js prints in-memory stub 3000 true. One flag swaps the data layer for the whole process, with no dependency-injection container and no mocking library.
Two details make "imports" more capable than it looks. It accepts every condition from Export Maps, and a target may be a bare specifier rather than a file -- "#logger": "pino 18,227 " in production, "#logger": "./src/noop.js" under test. The keys must start with #, which guarantees no outside package can address them.
The third import is a self-reference: a package may import itself by its own "name", provided it declares "exports". That is how you write tests that consume your package exactly as a user will, export map included, without publishing it or linking it into node_modules.