Home > Software engineering >  How to distribute the NodeJS library written in TypeScript for both BrowserJS and NodeJS with availa
How to distribute the NodeJS library written in TypeScript for both BrowserJS and NodeJS with availa

Time:10-16

Like lodash, the library @yamato-daiwa/es-extensions suggests the common functions for both BrowserJS and NodeJS, but unlike lodash it has single main file index.js with re-exports.

Requirements

This library:

  1. Must work in browser environment with Webpack (reached ✔️)
  2. Must make available the Webpack's tree shaking (by other words, the cutting off of the unused functionality on production building) for BrowserJS where each kilobyte on count (reached ✔️).
  3. Must work in NodeJS environment.
  4. Must work with ts-node

Main problem

The conflicting point is the modules type. To make the Webpack's tree shaking available, the ES modules are required, but currently NodeJS supports CommonJS modules only, and in ts-node the ES modules support is limited (especially in the library exporting case).

What we have is:

Modules type BrowserJS (Webpack) Tree shaking NodeJS ts-node
CommonJS Yes No Yes Yes
ES20XX Yes Yes No Limited

Because the tree shaking is critical for BroswerJS, it's already been decided to distribute the library by ES2020 modules. But this way, the support for NodeJS and ts-node will be lost.

Even if to build the NodeJS application with Webpack where it's not recommended to bundle the NodeJS libraries (webpack node modules externals is being used to exclude them), application will crush if don't add the @yamato-daiwa/es-extensions with it's ES modules to excluding of webpack node modules externals.

Repro

In this repro, npm run "Webpack:ProductionBuild" will build the files BrowserJS.js and NodeJS.js for appropriate environment. Because the source code using isUndefined function of "@yamato-daiwa/es-extensions" library only, in BrowserJS.js must not be any other functionality (Webpack's tree shaking):

(()=>{"use strict";console.log(!1),console.log("End of the script")})();

Well, it was the cleared challenge. The subject of this question isNodeJS.js:

(()=>{"use strict";const e=require("@yamato-daiwa/es-extensions");console.log((0,e.isUndefined)("ALPHA")),console.log("End of the script")})();

If we try to run it, we will have the error:

C:\Users\***\WebpackDoesNotCuttOfTheUnusedFunctionality\ConsumingProject\node_modules\@yamato-dai
wa\es-extensions\Distributable\index.js:6
export { default as formatNumberWith4KetaKanji } from "./Numbers/formatNumberWith4KetaKanji";

Now let's try to run it via ts-node (npm run "TSNode test"):

> ts-node index.ts

(node:18148) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
C:\Users\***\WebpackDoesNotCuttOfTheUnusedFunctionality\ConsumingProject\index.ts:1
import { isUndefined } from "@yamato-daiwa/es-extensions";

Does in mean "you must change the library"?

Note

This question has been asked before TypeScript 4.5 release. Maybe its new functionality will solve these problems?

CodePudding user response:

The "main" entry in a package.json file should always be in commonjs format. The "module" entry should always be es modules. Right now, you have "main" pointing to es modules, which will not be resolved properly (your ts-node error, for example).

Generally, if you want to give consumers the option, you would create 2 builds in the distributable.

(you'll need to remove the comments in these json files)

// tsconfig-esm.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "ES2020",

    // es module build target
    "outDir": "Distributable/esm"
  }
}
// tsconfig-cjs.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "commonjs",

    // commonjs build target
    "outDir": "Distributable/cjs",

    // the other one will build types, so not needed here
    "declaration": false
  }
}

Then, in your package.json:

"main": "./Distributable/cjs/index.js",
"module": "./Distributable/esm/index.js",
"types": "./Distributable/esm/index.d.ts",
"scripts": {
  "build": "rimraf Distributable & tsc -p tsconfig-cjs.json && tsc -p tsconfig-esm.json",
  "Lint": "eslint Source Tests"
},
"files": [
  "Distributable"
],
"devDependencies": {
  "rimraf": "^3.0.2",

Note:

  • you can call your scripts whatever you like, but build is idiomatic. The spaces would drive me nuts, as npm run Rebuild distributable wouldn't work. You have to either quote like npm run 'Rebuild distributable' or escape like npm run Rebuild\ distributable
  • if you have a "files" entry in package.json, you no longer need .npmignore. This means "package only the files specified", which is much safer than having to remember to add every new file to an ignore list.
  • I added the rimraf dependency, which is a common tool for deleting the dist directory in a platform-independent way. del-cli only works for windows.
  • Related