Repository navigation
Uncovered cases explored from a project migration聽#123
Description
Hey there! First off, thanks for this awesome tool鈥攊t's been a huge time-saver! 馃檶
I recently migrated a TypeScript project to ESM and used ts2esm to correctly add .js extensions in imports. While it worked well overall, I ran into a few challenges that I think could improve the tool even further.
1锔忊儯 Partial Support for tsconfig Paths
I saw that tsconfig path support works to some extent, but I had issues in an Nx monorepo with /apps/* and /libs/*. These are linked using tsconfig paths, and ts2esm only resolved the imports correctly after I moved libs inside an apps folder. My workaround looked like this:
- Move
libs/*intoapps/myapp/libs/* - Update
tsconfig.jsonpaths accordingly - Run
ts2esm - Move
libsback to their original location - Repeat for each app
If ts2esm could resolve paths of aliases outside the current project dir, that would be amazing!
2锔忊儯 Handling Package Exports and Subpath Imports
My project had a mix of modern packages (with package.json exports) and older ones using subpath imports. It would be great if ts2esm could leverage TypeScript's built-in resolver to automatically handle these cases.
Example 1: Legacy Package (lodash)
lodash doesn't have an exports field, so it requires .js in the import path:
// Before (CommonJS)
import omit from 'lodash/omit';
// After (ESM)
import omit from 'lodash/omit.js';Currently, ts2esm doesn鈥檛 handle this, so I had to fix it manually.
Example 2: Modern Package (firebase-functions)
firebase-functions has an exports field, so no changes are needed:
// This works in both CJS and ESM
import { HttpsError } from 'firebase-functions/v1/https';However, in cases where an import incorrectly targets a private subpath, it breaks in ESM:
// This works in CJS but fails in ESM
import type { ObjectMetadata } from 'firebase-functions/lib/v1/providers/storage';Since TypeScript correctly flags these issues, maybe ts2esm could warn or ignore them?
3锔忊儯 Dynamic Imports Are Not Converted
I also noticed that dynamic imports aren鈥檛 updated with .js extensions:
const module = await import('./my-function.function');
// Expected:
const module = await import('./my-function.function.js'); Handling this would make ts2esm even more powerful!
Would love to hear your thoughts! Let me know if I can help test anything. 馃槉
Activity
bennycode commentedon Feb 3, 2025
Hey @timofei-iatsenko, thanks a lot for putting in the effort to write such a detailed report! 馃檹 Seeing others engage so deeply with projects I maintain gives me a real boost of motivation to keep working on them. 馃挭
Let's go through the topics...
1锔忊儯 Partial Support for
tsconfigPathsIs this the structure of your Nx Monorepo?
apps/myapp/tsconfig.jsonapps/myapp/apps/*apps/myapp/libs/*Do you have a single main
tsconfig.json, or are there multiple? I ask because I usesourceFile.getProject()from ts-morph](https://github.com/dsherret/ts-morph) to locate TS configs. @fgarcia mentioned thatsourceFileshould be a.d.tsfile to properly find references from co-located packages: dsherret/ts-morph#1242 (comment)2锔忊儯 Handling Package Exports and Subpath Imports
Right now,
ts2esmsimply checks if a JS file is inside thenode_modulesdirectory of the referenced package. It doesn鈥檛 parse thepackage.jsonfile of installed modules at all. It sounds like supporting this would require reading theexportsfield frompackage.json. Might take some effort to implement.Relevant docs: https://devblogs.microsoft.com/typescript/announcing-typescript-4-5-beta/#package.json-exports-imports-and-self-referencing
Currently, my isNodeModuleRoot can already locate the relevant
package.jsonfile. That would be a good entrypoint to build the parsing functionality. 馃挕3锔忊儯 Dynamic Imports Are Not Converted
Great catch! I tested the following code in the TypeScript AST Viewer:
It shows that this call expression is of type
SyntaxKind.ImportKeyword, which isn't currently handled in convertFile.This snippet could help detect it:
It should be relatively easy to implement. I'll see when I can get to it. 馃
Thanks again for your report!
Best from Berlin,
Benny
timofei-iatsenko commentedon Feb 3, 2025
/tsconfig.json- root tsconfig, has path mappings such as@mylib - > /libs/mylib/src/index.ts/libs/*/libs/mylib/tsconfig.jsonlib tsconfig extends root one and has anincludefield forsrc/*/libs/mylib/src/*/apps/myapp/tsconfig.jsonapp tsconfig extends root one and has anincludefield forsrc/*/apps/myapp/src/*I started the script in each project in monorepo one by one eq:
cd ./apps/myapp && npx ts2esmI'm thinking might be utilizing real typescript resolver would help here?
So the logic might be like that:
firebase-functions/v1/https, try to resolve it with typescript'sts.resolveModuleName(see example here https://github.com/nrwl/nx/blob/master/packages/jest/plugins/resolver.ts#L69-L74)moduleResolution: NodeNext+type: module, means this external import is ok. left it as isbennycode commentedon Feb 24, 2025
Thanks for your insights @timofei-iatsenko!
I implemented support for dynamic imports (#130) and I created myself a test case for "2锔忊儯 Handling Package Exports and Subpath Imports":
Using the current version of ts2esm it gets converted to this:
.jsextension is assigned to a "Legacy Package"Isn't that exactly what you wanted?
I also tried to use the built-in TypeScript resolver using this utility method:
However, it returns a resolution to the
.d.tsfile when asking it for'lodash/omit':I am not yet sure how I can use this information to decide if I should add a
.jsextension to'lodash/omit'or not. 馃timofei-iatsenko commentedon Feb 24, 2025
Looks awesome!
That's a bummer, hoped it will free you from implementing this logic yourself.
albjeremias commentedon May 7, 2025
after I ran ts2esm on my project i got this error:
I have no idea what is wrong cause im not an esm expert.
here is the branch i pushed the ts2esm changed code
any support is very welcome
bennycode commentedon Oct 1, 2026
All three points from this report are fixed now:
exportsfield stay untouched and legacy packages likelodash/omitget.js.@albjeremias the
Cannot find package '@src/app'error happens becausetscnever rewrites path aliases in the emitted JavaScript.pathsin tsconfig only tells the type checker where to look, so Node.js sees@src/appat runtime and treats it as a package name. ts2esm can't change that. Two common ways to fix it:tscto rewrite the aliases into relative paths in your build output.package.json, e.g."imports": { "#src/*": "./dist/*" }, and import from#src/app.js. Node.js and TypeScript both understand these natively.Closing this, since the individual issues are resolved. Feel free to open a new issue if something else comes up during a migration.