tshy

Hybrid (CommonJS/ESM) TypeScript node package builder. Write modules that J...

README

tshy - TypeScript HYbridizer


Hybrid (CommonJS/ESM) TypeScript node package builder. Write
modules that Just Work in ESM and CommonJS, in easy mode.

This tool manages the exports in your package.json file, and
builds your TypeScript program using tsc 5.2+, emitting both ESM
and CommonJS variants, providing the full strength of TypeScript’s checking for both output formats.

USAGE


Install tshy:

  1. ```
  2. npm i -D tshy
  3. ```

Put this in your package.json to use it with the default configs:

  1. ```json
  2. {
  3.   "files": ["dist"],
  4.   "scripts": {
  5.     "prepare": "tshy"
  6.   }
  7. }
  8. ```

Put your source code in ./src.

The built files will end up in ./dist/esm (ESM) and
./dist/commonjs (CommonJS).

Your exports will be edited to reflect the correct module entry
points.

Dual Package Hazards


If you are exporting both CommonJS and ESM forms of a package,
then it is possible for both versions to be loaded at run-time.
However, the CommonJS build is a different module from the ESM
build, and thus a _different thing_ from the point of view of the
JavaScript interpreter in Node.js.

Consider this contrived example:

  1. ```js
  2. // import the class from ESM
  3. import { SomeClass } from 'module-built-by-tshy'
  4. import { createRequire } from 'node:module'
  5. const require = createRequire(import.meta.url)

  6. // create an object using the commonjs version
  7. function getObject() {
  8.   const { SomeClass } = require('module-built-by-tshy')
  9.   return new SomeClass()
  10. }

  11. const obj = getObject()
  12. console.log(obj instanceof SomeClass) // false!!
  13. ```

In a real program, this might happen because one part of the code
loads the package using require() and another loads it using
import.

The Node.js documentation
exporting an ESM wrapper that re-exports the CommonJS code, or
isolating state into a single module used by both CommonJS and
ESM. While these strategies do work, they are not what tshy does.

What Does tshy Do Instead?


It builds your program twice, into two separate folders, and sets
up exports. By default, the ESM and CommonJS forms live in
separate universes, unaware of one another, and treats the "Dual
Module Hazard" as a simple fact of life.

Which it is.

"Dual Module Hazard" is a fact of life anyway


Since the advent of npm, circa 2010, module in node have been
potentially duplicated in the dependency graph. Node's nested
node_modules resolution algorithm, added in Node 0.4, made this
even easier to leverage, and more likely to occur.

So: as a package author, you cannot safely rely on there being
exactly one copy of your library loaded at run-time.

This doesn't mean you shouldn't care about it. It means that you
_should_ take it into consideration always, whether you are using
a hybrid build or not.

If you need to ensure that exactly one copy of something exists
at run-time, whether using a hybrid build or not, you need to
guard this with a check that is not dependent on the dependency
graph, such as a global variable.

  1. ```js
  2. const ThereCanBeOnlyOne = Symbol.for('there can be only one')
  3. const g = globalThis as typeof globalThis & {
  4.   [ThereCanBeOnlyOne]?: Thing
  5. }
  6. import { Thing } from './thing.js'
  7. g[ThereCanBeOnlyOne] ??= new Thing
  8. export const thing = g[ThereCanBeOnlyOne]
  9. ```

If you find yourself doing this, it's a good idea to pause and
consider if you would be better off with a type check function or
something other than relying on instanceof. There are certainly
cases where it's unavoidable, but it can be tricky to work with.