A simple tool that add package.json({"type":"commonjs"/"module"}) to TypeScript outDir for dual package.
npm install tsconfig-to-dual-packageA 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.
``bash``
$ 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.typescript
It means that You need to install as devDependencies in your project.
- PeerDependency:
- typescript: * (any version)
- Node.js v16.17.0+
Usage
$ tsconfig-to-dual-package [Option]
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.package.json
Each generated has type field that is commonjs or module.
You can see example repository in following:
- tsconfig-to-dual-package-example
For example, This project package.json is following:
`json5package.json
{
"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 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
`json5module
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"newLine": "LF",
"outDir": "./module/", // <= Output ESM to directory`
"target": "ES2018",
"strict": true,
},
"include": [
"*/"
]
}
tsconfig.cjs.json: for CommonJS
`json5cjs
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "Node",
"outDir": "./cjs/" // <= Output CommonJS to directory`
},
"include": [
"*/"
]
}
Then, You can run tsconfig-to-dual-package after you compile both CommonJS and ES Module with following command:
`json5`
{
"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.
`markdownmodule
- package.json // { "type": "module" }
- index.ts // Node.js treat this as ESModule
- tsconfig.json // output to directorycjs
- tsconfig.cjs.json // output to 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.
- Example: tsconfig-to-dual-package-example
- Distribution files: https://www.npmjs.com/package/@azu/tsconfig-to-dual-package-example?activeTab=explore
This tool copy almost fields from package.json to generated {outDir}/package.json.main
However, it does not copy , module, exports, types fields because it points invalid file path.
It defined in OMIT_FIELDS constant.
- eventmit
- Work on CJS: https://github.com/azu/events-to-async/pull/4
- Work on ESM: https://github.com/azu/eventmit-module-env
- Work on Deno: https://github.com/azu/eventmit-deno-env
- Work on Browser: https://codesandbox.io/s/determined-poitras-yll61f?file=/index.html
- safe-marked
- Migraion PR: feat: Support CJS and ESM as dual package by azu · Pull Request #58 · azu/safe-marked
- TypeScript disallow to change file extension of generated files from .ts by Design.ts
- Feature Request: allow change file extension of generated files from · Issue #49462 · microsoft/TypeScript.mjs
- allow voluntary .ts suffix for import paths · Issue #37582 · microsoft/TypeScript
- bug(esm): TypeScript is not an ECMAScript superset post-ES2015 · Issue #50501 · microsoft/TypeScript
- Node.js require separate and .cjs if you need to get dual package in one package
As 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.package.json
It just put ({ "type": "module" } or "{ "type": "commonjs" }) to outDir for each tsconfig.json after tsc compile source codes.
@aduh95 describe the mechanism in
> For reference, the library-package/package.json contains:`
>
> json`
> {
> "name": "library-package",
> "version": "1.0.0",
> "main": "./index-cjs.js",
> "exports": {
> "import": "./index-esm.js",
> "require": "./index-cjs.js"
> },
> "type": "module"
> }
> "type": "module"
>
> Setting makes Node.js interpret all .js files as ESM, including index-cjs.js. When you remove it, all .js files will be interpreted as CJS, including index-esm.js. If you want to support both with .js extension, you should create two subfolders:`
>
> shell`
> $ 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:
>
> json`
> {
> "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.
> `json5`
> // package.json
> {
> "type": "module"
> }
> `
> bash`
> # 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.
>
> -- https://nodejs.org/api/packages.html#type
Pros
- You can use TypeScript compiler(tsc) directly
- No additional bundler, transpiler, build tool
Cons
- You need to run tsconfig-to-dual-package after tsc compilepackage.json
- This tool copy to outDir. This approach may affect path finding for package.json like read-pkg-upinstanceof
- Dual package hazard - I recommend that you should not use this approach for stateful package.
- For example, a singleton and check for user-input may cause unexpected behavior.require
- This Dual package has a risk of loading double( and import load separate resources).__diranme
- Very large package may want to prevent loading double package. For example, a large dictionary included package.
- Dual package is hard to use some API like , __filename without transpilerimport.meta.url
- Normally, you can use and new URL(..., import.meta.url) to get __dirname and __filename in ESM.import.meta.url
- On the other hands, is disallowed syntax in CJSimport.meta
- is not defined in CJS__diraname
- is not defined in ESM__dirname
- As a result, it is hard to use and __filename in dual package.
- Some package get these via Error stack trace
- bevry/filedirname: Fetch the current file and directory path, no matter your environment (Deno, Node.js, Web Browsers, ESM, CJS)
- fwh1990/this-file: Create dynamic __dirname, __filename and require method for both ESM and CJS
- JumpLink/cross-dirname: Node.js + Gjs + Deno module that returns the current script dirname. Similar to __dirname but also works in CommonJs and ES modules.
- 🆘 If you know a solution about this problem, please send pull request!
- Example repository: tsconfig-to-dual-package-example
- Pull Request: feat: support dual package by azu · Pull Request #2 · azu/tsconfig-to-dual-package-example
- Steps:
- Install tsconfig-to-dual-package: npm install --save-dev tsconfig-to-dual-package"type": "module"
- Add to package.json via npm pkg set type=moduletsconfig.json
- Add and tsconfig.cjs.jsontsconfig.json
- Create and set it to use module: "esnext"tsconfig.cjs.json
- Create and set it to use module: "commonjs"tsconfig-to-dual-package
- Add to build script"build": "tsc -p ./tsconfig.json && tsc -p ./tsconfig.cjs.json && tsconfig-to-dual-package"
- "main"
- Add /"types"(for backward compatibility)/"files"/"exports" fields to package.json"files": ["lib/", "module/"]
- (lib/ = cjs, module/ = esm)"main"
- /"types"/"exports"`
json`
{
"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
- Check Check Check
- Lint
- is helpfults-node/esm
- dependency-check@5 is useful
- Test
- use instead of ts-node for testingnpm publish
- https://github.com/TypeStrong/ts-node#node-flags-and-other-tools
- Publish!
-
- After Check!
- publint
- Load test via require/import
It is not for everyone, but I wrote a migration script for TypeScript project.
- Migration Script: Convert TypeScript project to Node.js dual package
- This script make almost migration automatic
- Use npm pkg command for change package.jsonpackage.json
- Use tsconfig-to-dual-package to build dual package
- Use eslint-cjs-to-esm to migrate source code to ESM from CJS
- Use publint to check
Example Result:
- refactor: migrate to dual package by azu · Pull Request #4 · azu/markdown-function
- If your package is a library, you should migrate to dual package if possible
- Because dual package reduce interop issues between CJS and ESM
- If your package is just logics, you can move to dual package
- If your package is a Command Line Tool(CLI), you not need to migrate to dual package
- Because CLI is not loaded from require function
- You can move to Pure ESM package
- Dual CommonJS/ES module packages
- Improve documentation on Dual Module Packages · Issue #34515 · nodejs/node
- TypeScript: Documentation - ECMAScript Modules in Node.js
- Why types fields at firstexports
- Issue: package.json resolution uses fallback conditions, unlike Node · Issue #50762 · microsoft/TypeScriptpackage.json
- why is there a package.json export for ? · Issue #1 · tsmodule/tsmodule"./package.json": "./package.json"
- Why add
- Special treatment for package.json resolution and exports? · Issue #33460 · nodejs/node
- frehner/modern-guide-to-packaging-js-library: A guide to help ensure your JavaScript library is the most compatible, fast, and efficient library you can make.
- publint: Lint your exports field in package.jsonrm -rf
- eslint-cjs-to-esm: help you to migrate CJS to ESM
- isaacs/rimraf: A util for nodejs: use same approach
See 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.
1. Fork it!
2. Create your feature branch: git checkout -b my-new-featuregit commit -am 'Add some feature'
3. Commit your changes: git push origin my-new-feature`
4. Push to the branch:
5. Submit a pull request :D
MIT © azu