Transpile TypeScript code inside `node_modules`.
What is the problem this feature will solve?
I do not propose allowing transforms for all packages in node_modules. That would encourage publishing TypeScript directly to npm, which comes with significant downsides for the ecosystem.
With the introduction of --experimental-strip-types and --experimental-transform-types, it is now feasible to run TypeScript code directly without a separate build step.
However, in monorepo environments, this feature is limited by how Node.js currently handles node_modules resolution. In a typical monorepo setup, you might have a structure like:
graph TD
libA --> server
libB --> server
libC --> server
libD --> libB
Tools like pnpm (and similarly yarn and npm workspaces) install local packages via symlinks into the node_modules folder. For example, if server depends on libA, pnpm will symlink (or copy, depending on configuration) libA into server/node_modules/libA.
The issue is that Node.js currently does not apply --experimental-strip-types or --experimental-transform-types to anything inside a node_modules folder, even if it’s a symlink to a local workspace package.
This forces developers to pre-transpile any local dependencies, which essentially defeats the purpose of the feature in monorepos, where the server package often represents only a small percentage of the codebase compared to its locally installed dependencies.
This might introduce some complexity to the current implementation, since each package could have its own tsconfig.json with potentially different options. That said, I’m not deeply familiar with the internals of --experimental-strip-types and --experimental-transform-types, so this is just an assumption.
Bun and Deno come to mind as the major runtimes that support TypeScript out of the box. A study might be needed to determine how to handle tsconfig resolution when transpiling packages inside node_modules. Should it respect each subpackage's own tsconfig? Or should it transpile everything according to the server’s tsconfig? I’m also unsure what the best or most correct approach would be here.
What is the feature you are proposing to solve the problem?
I'm still unsure what the best decision would be here, as there are multiple ways to distinguish which folders inside node_modules come from local/owned packages and which do not. What about private registries? There's still a lot to decide.
What alternatives have you considered?
Some options have come to mind:
- Transpile all packages in
node_modulesthat have"private": true. This ensures those packages did not come from a public registry. - Add a flag like
--transpile-packages=@mycompany/*to explicitly opt into transforming specific scoped packages. - Automatically transpile all symlinked packages.
- Require each package that needs to be transpiled to include a field like
"source": "./src/index.ts"or a custom flag such as"needsTranspilation": true.
A flag like the latter would be especially useful, as it would enable users to publish TypeScript packages to private or internal registries while still opting into transformation behavior.
Since this restriction was intentionally designed to prevent publishing uncompiled TypeScript to npm, perhaps a more consistent approach would be to only block transpilation for packages that clearly come from the public registry. Again, there's a lot to discuss here...
I don’t think there’s any reliable way to ensure people don’t publish untranspiled code except never having this feature by default - people will find workarounds if they need to. Postinstall scripts can add private true, or use symlinks, or make any change that would distinguish - it’s just not an achievable guarantee.
Postinstall scripts can add private true, or use symlinks, or make any change that would distinguish - it’s just not an achievable guarantee.
That same argument applies to publishing untranspiled TypeScript to npm today, there’s nothing preventing someone from doing it now, even without this feature.
While npm may have become a de facto standard and can enforce certain norms within its ecosystem, Node.js itself cannot (and arguably should not) enforce the same rules across all private or self-hosted registries. In short, preventing misuse entirely isn’t a realistic goal. Instead, Node.js should aim to enable powerful, ergonomic workflows for legitimate use cases, like monorepos, while still discouraging misuse through clear defaults and documentation.
FYI this was already suggested (and rejected) in #57215.
Hm. https://github.com/nodejs/node/issues/57215 links to https://github.com/nodejs/node/pull/55385, which says
I said this on https://github.com/nodejs/typescript/issues/14#issuecomment-2411759437, but I don't understand the use case here; if you have a package like this, you can't have published it because it's marked private. How did you get the package, then? For monorepos, I would really expect that you'd get it via a symlink, in which case the
realpathof the files will not prevent the transform from happening.
But this thread implies that packages that resolve via a symlink in node_modules to something outside node_modules are not being transformed, contrary to my understanding of the above comment. What's the intended behavior for that case?
I have a monorepo that supports both tsc-ed code on prod and node's type stripping in canary/local. Getting both to work required a workaround on my end.
- Adjust all the local package.jsons that need to be imported with:
{
"exports": {
"import": "index.js",
"importts": "index.ts"
}
}
- Run node with
--conditions=importtswhen using type stripping.
Is it pretty? No. Does it work? Yes.
The issue is that Node.js currently does not apply --experimental-strip-types or --experimental-transform-types to anything inside a node_modules folder, even if it’s a symlink to a local workspace package.
Given the above, I don't think this is true 🙈
@slagiewka that's quite interesting... I wasn't aware this was possible at all. I wonder if Node's maintainers were also aware of it or if they will take any actions against it.
If this is possible today, why not just document this interesting behavior as something like:
{
"exports": {
"import": "index.js",
"typescript": "index.ts"
}
}
node -C typescript
This use case is documented in Amaro https://github.com/nodejs/amaro. I think it should also be documented in Node, PR welcome. Pinging @nodejs/typescript for visibility (This does not allow to run in node_modules anyways)
FWIW this condition based approach was what we had recommended previously; prior art is:
- https://colinhacks.com/essays/live-types-typescript-monorepo
- https://www.npmjs.com/package/tshy
- https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/4a8afcde6b0981a5fad2365e50e0ba25ac78e2f8/packages/core/package.json#L32
That being said, I'm not sure why the realpath-ing is not working in this case, unless I'm not understanding the problem. Would need a real repo for an example.
That being said, I'm not sure why the realpath-ing is not working in this case, unless I'm not understanding the problem. Would need a real repo for an example.
It's not bypassing the node_modules restriction afaik. Realpath-ing should be working as expected.
Node.js: developers are discouraged to upload TypeScript to npmjs.com.
But,
npmjs.com: developers are free to upload their favorite movies.
The .ts files are MPEG transport stream files :laughing:
Actually node_modules can contain arbitrary files and come from anywhere, file system symlinks, private registry, not just public packages from npmjs.com.
If a library's downstream developers really want TypeScript code, re-transpiled with their favorite bundler, then the library's developer does have reason to upload TypeScript to npmjs.com.
I also don't understand why Node.js insists on this limitation.
I also don't understand why Node.js insists on this limitation.
You say that when you just gave an example of why we cannot assume we'd be able to transpile .ts files. Note that as far as Node.js is concerned, publishing source files is perfectly fine, as long as there's also a transpiled version alongside.
Even though I don't agree with the current situation of monorepos support, I still believe we should at least try to centralize all this information and document it so other people, when facing out the same problem as I, at least are able to better understand workable solutions.
What paths should I take to improve it? Maybe a PR to node's documentation website?
Yes I think it belongs to https://github.com/nodejs/nodejs.org/blob/main/apps%2Fsite%2Fpages%2Fen%2Flearn%2Ftypescript%2Frun-natively.md
Maybe I'm late to the party, but why shouldn't we be allowed to set "main": "./index.mts" or "exports": { "import": "./index.mts" } again?
I'm using private packages on my own server, so it's not even published to npm, an extra build step with extra tools just to please this rule is so unnecessary.
Seems obvious that we should be able to configure this as we want, no?
Seems obvious that we should be able to configure this as we want, no?
You could use a loader (module hooks), that way you could force Node.js to transpile those files even inside a node_modules folder
There are several loaders that will do it out of the box, such as @nodejs-loaders/tsx
There are several loaders that will do it out of the box, such as
@nodejs-loaders/tsx
I was hoping I wouldn't have to include a bunch of config files and mess around with things just to run a script, this should be possible out of the box with NodeJS imho.
I was hoping I wouldn't have to include a bunch of config files
You don't (at least with @nodejs-loaders/tsx). Take a quick look at its README.
We are aware of the request. More requests do not move us any closer to solving the issues preventing the request 😉
Take a quick look at its README.
Thanks for making that, but I did, and it says something about eslint config + it's a separate tool / command + it says it can etc... seems hackish for what I'm trying to accomplish. I'll look at the source code, maybe it can be adapted to this use case.
I think more requests is exactly what helps move priorities up 🥰
I am not enturely sure where you're basing your experience, but I don't think (2) is related to TS, and (3) is possible with rewriteRelativeImportExtensions as of TS 5.7.
I'm not sure what you mean by (1), you just need those for local dev?
(2) isn't correct - anything can read .mjs files unrelated to the type field. The only thing the type field ever does is changes how .js is parsed.