Try filling exports field in package.json. Documentation addresses the exact use case you describe:
Subpath exports
When using the "exports" field, custom subpaths can be defined along
with the main entry point by treating the main entry point as the "."
subpath:
{
"main": "./main.js",
"exports": {
".": "./main.js",
"./submodule": "./src/submodule.js"
}
}
Now only the defined subpath in "exports" can be imported by a
consumer:
import submodule from 'es-module-package/submodule';
// Loads ./node_modules/es-module-package/src/submodule.js
While other subpaths will error:
import submodule from 'es-module-package/private-module.js';
// Throws ERR_PACKAGE_PATH_NOT_EXPORTED
Further info:
Demo scenario
Folder structure:
├── package // the package we want to distribute
│ ├── package.json
│ ├── submodule1.js
│ └── submodule2.js
├── main.js // consumer script
└── package.json
submodule contents:
// ./package/submodule1.js
export default 'submodule1.js'
// ./package/submodule2.js
export default 'submodule2.js'
export const name = 'second submodule'
inside ./package/package.json, I defined submodule exports for these files:
{
"name": "my-package",
"type": "module", // enable ESM imports
"exports": {
"./a": "./submodule1.js",
"./b": "./submodule2.js"
}
}
then I installed the ./package directory in consumer project using npm install ./package which added an entry in ./package.json
{
"name": "consumer",
"type": "module",
"dependencies": {
"my-package": "file:package"
}
}
Now, I can import submodules (both default and named exports) from ./main.js:
import a from 'my-package/a'
import b, {name as bName} from 'my-package/b'
console.log(a);
console.log(b);
console.log(bName);
which prints:
submodule1.js
submodule2.js
second submodule
I can go as far as to prevent importing the base package and only allowing submodules by explicitly setting root package export to null (or by simply omitting it altogether):
{
"name": "my-package",
"main": "main.js",
"type": "module",
"exports": {
".": null, // prevent importing root package
"./a": "./submodule1.js",
"./b": "./submodule2.js"
}
}
which causes this code to throw error
import mypackage from 'my-package'
node:internal/process/esm_loader:74
internalBinding('errors').triggerUncaughtException(
^
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: No "exports" main defined in /path/to/demo/node_modules/my-package/package.json imported from /path/to/demo/index.js
at new NodeError (node:internal/errors:329:5)
at throwExportsNotFound (node:internal/modules/esm/resolve:337:9)
...
at link (node:internal/modules/esm/module_job:56:36) {
code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'
}