Node.js dual package tool that add package.json to tsconfig's `outDir`
MIT License
A Node.js dual package tool for TypeScript.
You can support CommonJS and ESModules in one package via this tool.
This tool add package.json
which is { "type": "module" }
or { "type": "commonjs" }
based on tsconfig's module
and outDir
option.
You can use this tool with tsc
command.
$ tsc -p . && tsc -p ./tsconfig.cjs.json && tsconfig-to-dual-package # add "{ourDir}/package.json"
Install with npm:
npm install tsconfig-to-dual-package --save-dev
Requirements: This tool depended on typescript
package for parsing tsconfig.json
file.
It means that You need to install typescript
as devDependencies in your project.
typescript
: *
(any version)Usage
$ tsconfig-to-dual-package [Option] <tsconfig.json>
Options
--cwd [String] current working directory. Default: process.cwd()
--debug [Boolean] Enable debug output
--help [Boolean] show help
Examples
# Find tsconfig*.json in cwd and convert to dual package
$ tsconfig-to-dual-package
# Convert specified tsconfig.json to dual package
$ tsconfig-to-dual-package ./config/tsconfig.esm.json ./config/tsconfig.cjs.json
This tool adds package.json
to tsconfig's outDir
for dual package.
Each generated package.json
has type
field that is commonjs
or module
.
You can see example repository in following:
For example, This project package.json
is following:
{
"name": "my-package",
"version": "1.0.0",
"type": "module",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"module": "./module/index.js",
// Note: Normally same .js extension can not be used as dual package
// but this tool add custom `package.json` to each outDir(=lib/, module/) and resolve it.
"exports": {
".": {
"import": {
"types": "./module/index.d.ts",
"default": "./module/index.js"
},
"require": {
"types": "./lib/index.d.ts",
"default": "./lib/index.js"
},
"default": "./lib/index.js"
}
}
}
And, This project has tsconfig.json
and tsconfig.cjs.json
:
tsconfig.json
: for ES Module
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"newLine": "LF",
"outDir": "./module/", // <= Output ESM to `module` directory
"target": "ES2018",
"strict": true,
},
"include": [
"**/*"
]
}
tsconfig.cjs.json
: for CommonJS
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "Node",
"outDir": "./cjs/" // <= Output CommonJS to `cjs` directory
},
"include": [
"**/*"
]
}
Then, You can run tsconfig-to-dual-package
after you compile both CommonJS and ES Module with following command:
{
"scripts": {
"build": "tsc -p ./tsconfig.json && tsc -p ./tsconfig.cjs.json && tsconfig-to-dual-package",
}
}
tsconfig-to-dual-package
command adds package.json
to module
and cjs
directory.
As a result, you can publish both CommonJS and ESModule in a single package. It is called dual package.
- package.json // { "type": "module" }
- index.ts // Node.js treat this as ESModule
- tsconfig.json // output to `module` directory
- tsconfig.cjs.json // output to `cjs` directory
- cjs/
- package.json // { "type": "commonjs" }
- index.js // Node.js treat it as CommonJS module
- module/
- package.json // { "type": "module" }
- index.js // Node.js treat it as ESModule
For more details, please see Dual CommonJS/ES module packages in Node.js official document.
This tool copy almost fields from package.json
to generated {outDir}/package.json
.
However, it does not copy main
, module
, exports
, types
fields because it points invalid file path.
It defined in OMIT_FIELDS constant.
.ts
by Design
.mjs
and .cjs
if you need to get dual package in one packageAs a result, TypeScript and Node.js ESM support is conflicting.
It is hard that you can support dual package with same .js
extension.
Of course, you can use tsc-multi or Packemon to support dual packages.
However, These are build tools. I want to use TypeScript compiler(tsc
) directly.
tsconfig-to-dual-package
do not touch TypeScript compiler(tsc
) process.
It just put package.json
({ "type": "module" }
or "{ "type": "commonjs" }
) to outDir
for each tsconfig.json after tsc
compile source codes.
@aduh95 describe the mechanism in https://github.com/nodejs/node/issues/34515#issuecomment-664209714
For reference, the
library-package/package.json
contains:{ "name": "library-package", "version": "1.0.0", "main": "./index-cjs.js", "exports": { "import": "./index-esm.js", "require": "./index-cjs.js" }, "type": "module" }
Setting
"type": "module"
makes Node.js interpret all.js
files as ESM, includingindex-cjs.js
. When you remove it, all.js
files will be interpreted as CJS, includingindex-esm.js
. If you want to support both with.js
extension, you should create two subfolders:$ mkdir ./cjs ./esm $ echo '{"type":"commonjs"}' > cjs/package.json $ echo '{"type":"module"}' > esm/package.json $ git mv index-cjs.js cjs/index.js $ git mv index-esm.js esm/index.js
And then have your package exports point to those subfolders:
{ "name": "library-package", "version": "1.0.0", "main": "./cjs/index.js", "exports": { "import": "./esm/index.js", "require": "./cjs/index.js" }, "type": "module" }
Also, Node.js documentation describe this behavior as follows
The nearest parent package.json is defined as the first package.json found when searching in the current folder, that folder's parent, and so on up until a node_modules folder or the volume root is reached.
// package.json { "type": "module" }
# In same folder as preceding package.json node my-app.js # Runs as ES module
If the nearest parent package.json lacks a "type" field, or contains "type": "commonjs", .js files are treated as CommonJS. If the volume root is reached and no package.json is found, .js files are treated as CommonJS.
Pros
tsc
) directly
Cons
tsconfig-to-dual-package
after tsc
compilepackage.json
to outDir
. This approach may affect path finding for package.json
like read-pkg-up
instanceof
check for user-input may cause unexpected behavior.require
and import
load separate resources).__diranme
, __filename
without transpiler
import.meta.url
and new URL(..., import.meta.url)
to get __dirname
and __filename
in ESM.import.meta.url
is disallowed syntax in CJSimport.meta
is not defined in CJS__diraname
is not defined in ESM__dirname
and __filename
in dual package.tsconfig-to-dual-package
: npm install --save-dev tsconfig-to-dual-package
"type": "module"
to package.json via npm pkg set type=module
tsconfig.json
and tsconfig.cjs.json
tsconfig.json
and set it to use module: "esnext"
tsconfig.cjs.json
and set it to use module: "commonjs"
tsconfig-to-dual-package
to build script
"build": "tsc -p ./tsconfig.json && tsc -p ./tsconfig.cjs.json && tsconfig-to-dual-package"
"main"
/"types"
(for backward compatibility)/"files"
/"exports"
fields to package.json
"files": ["lib/", "module/"]
(lib/ = cjs, module/ = esm)"main"
/"types"
/"exports"
{
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"exports": {
".": {
"import": {
"types": "./module/index.d.ts",
"default": "./module/index.js"
},
"require": {
"types": "./lib/index.d.ts",
"default": "./lib/index.js"
},
"default": "./module/index.js"
},
"./package.json": "./package.json"
}
}
npx publint
is helpfults-node/esm
instead of ts-node
for testingnpm publish
It is not for everyone, but I wrote a migration script for TypeScript project.
npm pkg
command for change package.json
package.json
Example Result:
require
functiontypes
fields at firstexports
resolution uses fallback conditions, unlike Node · Issue #50762 · microsoft/TypeScript
package.json
? · Issue #1 · tsmodule/tsmodule
"./package.json": "./package.json"
exports
field in package.json
rm -rf
util for nodejs: use same approachSee Releases page.
Install devDependencies and Run npm test
:
npm test
Pull requests and stars are always welcome.
For bugs and feature requests, please create an issue.
git checkout -b my-new-feature
git commit -am 'Add some feature'
git push origin my-new-feature
MIT © azu