skip to Main Content

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


  1. Chosen as BEST ANSWER

    What I ended up doing was adding this to my package.json

    "exports": {
        "./*.js": {
            "types": "./*.d.ts",
            "require": "./*.js",
            "import": "./*.js"
        }
    }
    

    Now I can import any file, and I don't need to maintain the exports field.

    import { Logger } from "@org/runtime/logger/index.js"
    

  2. First – define the full path in your package.json under export section:

    "exports": {
    "./logger": "path/to/logger/index.js"
    }
    

    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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search