Modules in Node are very different than they were only a Month ago, and even more so than 18-20 months ago. As of JUNE 2022 this is the most current means for inspecting a TS-Node package, and inferring the module-type of the package.
The inspected package can be, cjs
, esm
or both (cjs
& esm
)
Its now possible to use TypeScript to build a CJS module, and an ESM module from one TS-Node code-base (or project if you will). In-fact, if your going to maintain both module types, its almost a necessity to have a transpiler (unfortunately I can't go further into detail, as such a topic extends past the scope of the original question).
It Should be Noted: Knowing how to infer a module type from a TS-Node package, is close to the same as knowing how to configure TS-Node packages as different mod0ule-types.
The Module-type of a TS-Node Package can be Inferred from 2 Files
- The
package.json
- The
tsconfig.json
In the package.json
File
...the "type"
property is where the module type is defined. Its important to note, that this is where Node.js
will also infer what the modules type is, and how it should be expected to resolve modules with-in the project. TypeScript isn't present during Runtime, where Node.js is the Runtime Environment. Because Node.js is the RTE, its a bit more concrete to look at the package.json
, and infer the module type from it, however, the story is far from over. JavaScript Modules make for a complex topic now.
Before I move on, lets look at a package.json
for an ESM configuration.
ESM Configured package.json
// FILE: "./package.json"
{
"name": "foo-pkg-bar"
"version": "1.23.4",
"type": "module", // <-- You're Looking for this K/V pair
"license": "MIT",
"desc": "some description here...",
/*
...rest of the JSON-file's properties...
*/
}
When the module is an ES-Module
or (ESM
), it will have its package.json
property set to "module"
.
In a CommonJS Module
(or CJS Module
) the configuration isn't required to be explicit. This means that you can have the "type"
property completely left out of the package.json
file, and in that very common situation the module-type will default to CJS
. The "type"
property can be configured to implicitly declare the module-type as CJS
though, which in that case the "type"
property will look like this:
{
name: "foobar-foofoo-head",
version: 1.23.4,
type: "commonjs"
}
Multiple Package.json Files
Often times there can be, and in dual-module-typed packages (packages that are built to support both ESM & CJS environments), there is going to be, more than a single package.json
file. In a dual-module-typed package, the ESM
build will have a package.json
file that has the single property added to it as shown bellow.
// package.json for the ESM build
{
"type": "module"
}
CJS
will also have its own type package.json file, and it will also have a single property added to it (see below).
// package.json for the CJS build
{
"type": "commonjs"
}
The rest of the package.json file will be in the ROOT-package.json file. In such a situation the file-structure will look somthing like this.
├── build
│ ├── cjs
│ │ ├── lib
│ │ │ ├── project-stuff.d.ts
│ │ │ └── project-stuff.js
│ │ ├── main.cjs
│ │ ├── main.d.cts
│ │ └── package.json // <-- CJS "package.json"
│ └── esm
│ ├── lib
│ │ ├── project-stuff.d.ts
│ │ └── project-stuff.js
│ ├── main.d.mts
│ ├── main.mjs
│ └── package.json // <-- ESM "package.json"
├── package.json // <-- ROOT "package.json"
├── package-lock.json
├── src
│ ├── lib
│ │ └── project-stuff.ts
│ ├── main.cts
│ └── main.mts
├── tsconfig.base.json
├── tsconfig.cjs.json
└── tsconfig.esm.json
The above is actually from the project I built to teach myself how to trans-pile to both module types.
You probably noticed the many tsconfig.json
files?
Honestly, if you see tsconfig files with that naming convention, you don't need to inspect any further, it definitely can be used as either a CJS
, or an ESM
module (assuming its a working package).
Inspecting the TSConfig for the Package's Module-type
The TSConfig can also be used to infer which module-type any given TS/Node project is. Open the TSConfig, and look for the Module & Module resolution properties.
TS v4.7, eased support for ESM in Node/TS projects
The support for ESM in Node packages that are trans-piled using TypeScript was eased by the addition of valid values that can be passed to the tsconfig.json
files configuration properties "module"
and "moduleResolution"
"module"
& module resolution can now accept the new values
node12
(no top level await),
node16
(ESM standard implemented for the LTS node16 version) &
nodenext
(most current ESM standard)
If you see the module
& moduleResolution
properties set to these values then the project is being transpiled as an ES-Module.
CJS & ESM
Modules can be both, CJS & ESM. If a package is being trans piled into a CJS build & ESM build, you should see two separate directories for the builds.
There should be separate package.json
files for each build, that take advantage of the cascading file structure that package.json
files are able to be placed in (e.g. a tree-like hierarchy).
- There should be a base
package.json
file.
- One
package.json
file should have its "type"
property set to module
- while the other
package.json
file should have its "type"
property set to commonjs
this is a sign that the package, not only supports both ESM & CJS, but is both ESM & CJS
You can, once again, Infer the Module-type by Inspecting the tsconfig.json
files.
There will also be multiple TSConfig files. The thing about the TS Configuration files, is that there is not any one way that they will be named, or structurally set up. From what I see, most use a tsconfig.base
, then extend it to two other tsconfig.*.json
files.
Something like this:
- There will be the tsconfig.base.json file (I have seen this also named
tsconfig-base.json
as well)
- There should be a
tsconfig.*.json
file for CJS
- Then there should be a
tsconfig.*.json
file for ESM as well.
The naming conventions are not static, there highly configurable, and customizable (which is what we all love about TS right?). Getting use to this can be difficult for some. It was hard for me at first.
The key is to read each TSConfig, and see what the module
& moduleResolution
properties are set to. If you see a tsconfig with the module property set to "commonjs", and another tsconfig
has its module
property set to NodeNext
, that means the project trans-piles to both ESM
& CJS
module types.