I created an ESM-only package that expose all the files instead of defining exports
in package.json
{
"name": "@org/runtime",
"version": "0.0.0-development",
"description": "Runtime library",
"module": "index.js",
"type": "module",
"scripts": {
"b": "pnpm build",
"prebuild": "rm -rf dist",
"build": "tsc --project tsconfig.build.json && pnpm build:copy-dts && tsc-alias && cp package.json dist/",
"build:copy-dts": "copyfiles -V -u 1 src/**/*.d.ts src/*.d.ts dist",
"lint": "eslint .",
"test": "vitest",
"test:ci": "vitest --coverage",
"release": "semantic-release"
},
"devDependencies": {
"tsc-alias": "^1.8.10",
"typescript": "^5.5.3",
},
"dependencies": {
},
"engines": {
"node": ">=22"
}
}
I get a file structure like this:
├── background-job
│ ├── index.d.ts
│ └── index.js
├── logger
│ ├── formatter
│ │ ├── console-formatter.d.ts
│ │ ├── console-formatter.js
│ │ ├── formatter.d.ts
│ │ ├── formatter.js
│ │ ├── index.d.ts
│ │ ├── index.js
│ │ ├── json-formatter.d.ts
│ │ └── json-formatter.js
│ ├── index.d.ts
│ ├── index.js
│ ├── shared.d.ts
│ └── shared.js
├── package.json
├── request-context
│ ├── fastify.d.ts
│ ├── fastify.js
│ ├── index.d.ts
│ ├── index.js
│ ├── plugin.d.ts
│ └── plugin.js
├── result.d.ts
├── result.js
├── tests
│ ├── setup.d.ts
│ └── setup.js
├── types.d.ts
├── validations
│ ├── is-truthy.d.ts
│ └── is-truthy.js
└── validator
├── index.d.ts
├── index.js
├── schema-extensions.d.ts
├── schema-extensions.js
└── yup.d.ts
In my main project I have this tsconfig.json:
{
"compilerOptions": {
"target": "ES2023",
"module": "NodeNext",
"esModuleInterop": true,
"moduleResolution": "NodeNext",
"lib": [
"ESNext"
],
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"outDir": "dist",
"rootDir": "src",
"baseUrl": ".",
"typeRoots": [
"node_modules/@types"
],
"types": [
"node"
],
"declaration": false,
"sourceMap": false,
"paths": {
"@/*": [
"src/*"
]
}
},
"include": [
"src/**/*.ts",
"node_modules/@types/node/globals.d.ts",
"node_modules/vitest/globals.d.ts",
"node_modules/.pnpm/[email protected]_@[email protected]/node_modules/vitest/globals.d.ts"
],
"exclude": [
"node_modules",
"dist"
],
"tsc-alias": {
"resolveFullPaths": true,
"verbose": true
}
}
After installing my package, I try to import it like this:
import { Logger } from '@org/runtime/logger';
ESlint, my IDE and tsc work without any warning/error on that import statement. I can lint and build with no issues. Then, when I run the transpiled code I get the error
Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import '/my-project/node_modules/@org/runtime/logger' is not supported resolving ES modules imported from /my-project/index.js
Did you mean to import "@org/runtime/logger/index.js"?
If I add index.js
to my import statement, I can run the project without issues. How can I make TSC catch these issues during build-time? Or how can I make the IDE aware of this issue?
I would prefer to avoid depending on eslint, so I can catch this when I build the project.
2
Answers
What I ended up doing was adding this to my package.json
Now I can import any file, and I don't need to maintain the exports field.
First – define the full path in your package.json under export section:
Then it will be a bit more pretty, and also this way ts and the ide can catch it since you use explicitly defined pathes.
Second – i dont think ts does that without using the mention above, its more style and types, its assume that when you import you do it right.
Although you said you prefer not to, the option i know is to use
eslint-plugin-import
which does exactly that.Third – you can add an eslint check to your build (actually its recommended) then you catch it during build.
Lastly, there’s probably an extension for that in your ide, just search it.