Typescript library for Node as hybrid package in NPM

Viewed 809

How to properly configure typescript tsconfig.json and npm package.json to create a hybrid npm package?

I mostly followed this article (but I also tried other methods)

This is the alpha version of my library gomshal on github. This last state that I describe here is in the branch hybrid-2.3. I wanted to use ESM as a basic import and a CommonJS as option. This is described in chapter 2.3 of the article. My lib files exports through index.ts class Gomshal, enums and interfaces.

Project structure

My sources are in "./src/lib". I have two tsconfig.json files: tsconfig.esm.json and tsconfig.cjs.json. Both includes same "./src/lib/**/*.ts" files as a source. Differecnes are in compilerOptions:

// tsconfig.cjs.json for CommonJS
"compilerOptions": {
  "outDir": "./build/lib/cjs",
  "target": "ES5",
  "module": "CommonJS",
  ...
}

// tsconfig.esm.json for ES6 module
"compilerOptions": {
    "outDir": "./build/lib/esm",
    "target": "ES2015",
    "module": "ES2015",
  ...
}

Besides, I also have a file package.json specific for commonjs with content: { "type": "commonjs" }. Important keys from my root package.json are:

{
  "type": "module",
  "main": "./build/lib/esm/index.js",
  "module": "./build/lib/esm/index.js",
  "typings": "./build/lib/esm/index.d.ts",
  "files": [ "build/lib/**/*" ],
  "exports": {
    "./esm": "./build/lib/esm/index.js",
    "./cjs": "./build/lib/cjs/index.js"
  },
  ...
}

So everything here is targeting to ES6 module by default. After compilation (tsc -p tsconfig.esm.json and tsc -p tsconfig.cjs.json) two folders are created in the build directory (build/lib/cjs and build/lib/esm). Package json file from src/cjs is coppied to build/lib/cjs and everything looks ready to publish. Now when I try npm pack with this settings, copy generated tgz file and npm install into demo project (part of package.json: "type": "module", "dependencies": {"gomshal": "file:gomshal-1.4.2.tgz"}), minimal demo index.js:

import { Gomshal } from 'gomshal';
gomshal = new Gomshal();

The error ERR_MODULE_NOT_FOUND is returned:

node .\index.js
(node:23404) ExperimentalWarning: The ESM module loader is experimental.
internal/modules/esm/resolve.js:58
  let url = moduleWrapResolve(specifier, parentURL);
            ^

Error: Cannot find module D:\Workspace\gomshal\npmtest\node_modules\gomshal\build\lib\esm\gomshal imported from D:\Workspace\gomshal\npmtest\node_modules\gomshal\build\lib\esm\index.js
    at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:58:13)
    at Loader.resolve (internal/modules/esm/loader.js:85:40)
    at Loader.getModuleJob (internal/modules/esm/loader.js:188:40)
    at ModuleWrap.<anonymous> (internal/modules/esm/module_job.js:42:40)
    at link (internal/modules/esm/module_job.js:41:36) {
  code: 'ERR_MODULE_NOT_FOUND'

I tried other variants in root package.json ("main" targeting to cjs and "module" targeting esm compiled code and other combinations in "exports" key) but in this case I am probably closest to the hybrid package. My node version is 13.8.0, npm 6.13.6, tsc version 3.8.3.

However, I still can't configure my npm package to make the named ESM import import { Gomshal } from 'gomshal'; usable without error. What did i miss?

0 Answers
Related