Package foundations
Design package exports
Expose a small supported entry-point map and keep internal files private to consumers.
Without an exports map, Node can resolve deep imports into files you never meant to expose. A consumer might reach into dist/internal/normalize.js today, and your refactor tomorrow breaks their build.
The exports field defines the only import paths you support. Everything else should fail on purpose.
For @acme/slugify-title, we want two public entry points and nothing else:
{
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./strict": {
"import": "./dist/strict.js",
"require": "./dist/strict.cjs",
"types": "./dist/strict.d.ts"
}
}
}
A consumer writes:
import { slugifyTitle } from '@acme/slugify-title'
import { slugifyTitleStrict } from '@acme/slugify-title/strict'
They do not write:
import x from '@acme/slugify-title/dist/internal.js'
Prove the boundary
Create a tiny consumer file outside the package:
// consumer.mjs
import { slugifyTitle } from '@acme/slugify-title'
console.log(slugifyTitle('Hello World!'))
Run it after installing the packed tarball (we cover that flow in a later lesson). You should see:
hello-world
Now try the internal path:
import x from '@acme/slugify-title/dist/internal.js'
Node should throw an error like Package subpath './dist/internal.js' is not defined by "exports". That failure is good. It means your map is doing its job.
Keep the map small
My advice is to add a subpath only when you would document it in the README and test it from a consumer fixture. Two entry points beat ten accidental ones.
If you need a separate ./node or ./browser target later, exports supports import conditions. Start without them until you have a real runtime split to support.
Legacy fields like "main" still matter for older tooling. When you adopt exports, keep main, module, and types aligned with the primary entry until you know every consumer resolves through exports.
Document each supported import path in the README with a copy-paste example. Undocumented paths are not part of the contract, even if Node could resolve them before you added the map.
When you remove a subpath in a major release, list it in the changelog under Removed so search hits find the migration note quickly.
Lesson completed