Resolving multiple package.json "main" fields
See original GitHub issueTL;DR: A new compiler option mainFields
for selecting multiple fields in package.json
instead of just package.json#main
.
There are lots of related issues to this one (which I link to below), but I want to focus on just this specific proposal.
Packages often look like this:
{
"name": "my-package",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js"
"source": "src/index.ts"
}
Notice how we have multiple fields which specify multiple entry points. These entry points all refer to the same code, just in different compile states and configurations.
Many tools use these fields in order to find the entry point that they care about. For example, tools like Webpack and Rollup will use package.json#module
in order to find ES modules. Other tools will use fields like package.json#source
(or src
) for local package development.
While these fields aren’t part of the official Node module resolution algorithm. They are a community convention which has proven to be useful in lots of scenarios.
For TypeScript, one such scenario that this would be useful for is with multi-package repos or “monorepos”. These are repositories where the code for multiple npm packages exist and are symlinked together locally.
/project/
package.json
/packages/
/package-one/
package.json
/node_modules/
/package-two/ -> ../../package-two (symlink)
/package-two/
package.json
Inside each package, you’ll generally have a src/
directory that gets compiled to dist/
/package-two/
package.json
/src/
index.ts
/dist/
index.js
index.d.ts
Right now it is really painful to use TypeScript with one of these repos. This is because TypeScript will use the package.json#main
to resolve to the packages dist
folders. The problem with this is that the dist
folders might not exist and if they do exist they might not be compiled from the most recent version of src
.
To work around this today you can add a index.ts
file in the root of each of your packages to point to the right location and make sure that the root index.ts
file does not get shipped to npm.
/package-two/
index.ts
/src/index.ts
// package-two/index.ts
export * from './src/index'
It sucks that you need this file, and if you ever forget to create it in a new package, you’ll revert back to really crap behavior.
If, instead of all that, TypeScript supported a new compiler option mainFields
which looked like:
{
"compilerOptions": {
"mainFields": ["source", "main"]
}
}
You could add package.json#source
(in addition to package.json#main
) and resolve it to the right location locally.
The algorithm would look like this:
For each mainField
:
- Check if the
package.json
has a field with that name - If the package.json does not have the field, continue to next
mainField
- If it field exists, check for a file at that location.
- If no file at that location exists, continue to the next
mainField
- If the file exists, use that file as the resolved module and stop looking
I think this is the relevant code:
Related Issues:
- https://github.com/Microsoft/TypeScript/issues/21137 “Path mapping in yarn workspaces”
- https://github.com/Microsoft/TypeScript/issues/20248 “Module resolution for sub-packages picks d.ts file when .ts file is available”
- https://github.com/Microsoft/TypeScript/issues/18442 “Support
.mjs
output”
Issue Analytics
- State:
- Created 6 years ago
- Reactions:109
- Comments:20 (3 by maintainers)
Top GitHub Comments
For providing intellisense, when no declarations are available, wouldn’t it always be better to resolve to the source, if available, than to the compiled output and would that be true irrespective of the source being written in TypeScript?
Even with
--allowJs
the compiled output is usually resolved when there are no type declarations. This does not only affect developers working with monorepos, it affects anyone consuming packages without declarations.Of course, it depends on the package, but when using say, “Go to Definition”, it is very common to be taken to a UMD bundle and that is probably the least desirable result.
Having said that, there are too many of these damn fields!
Here is a totally non-exhaustive list of main fields that should be considered applicable
"main"
"browser"
"types"
"module"
"jsnext:main"
"es2015"
"unpkg"
"typings"
The solution that worked for me is keeping both uncompiled and compiled files in the same folder (
lib
) instead of having bothsrc
anddist
.TLDR: During development - all .js files are removed. During publishing - all .ts files are ignored.
Here is the flow:
For simplicity, let’s consider our package has only one,
index.ts
source file.I keep it in
<ROOT>/lib/index.ts
or<ROOT>/packages/foo/lib/index.ts
.Than in package.json I pass
"main": "./lib"
. Note I’m not passing exact file name. TS will be able to resolve toindex.ts
in development. Bundlers will be able to resolve toindex.js
in published package.During development - I have a script that clears all
.js
files inlib
(therefore you cannot use .js files in development).This makes typescript properly point to ts file.
When releasing the package - I run build which will add
.js
files next to their.ts
counterparts.In
.npmignore
- I ignore alllib/**.ts
files, so in final, published version there are only.js
files inlib
.In ‘production’ -
package.json
main
field which points to./lib
will properly resolve to./lib/index.js
.In
.gitignore
- I ignore alllib/**.js
files - so in my git repo I don’t have .js files in lib published if I forget to run my clean script before pushing.