javascriptnode.jstypescriptes6-modules

ERR_UNSUPPORTED_DIR_IMPORT when importing my own package


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/vitest@2.0.5_@types+node@20.16.1/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.


Solution

  • 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"