-
Notifications
You must be signed in to change notification settings - Fork 30.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support type stripping in node_modules
#57215
Comments
You can use a loader/module hook to workaround that. /cc @JakobJingleheimer |
I believe this was specifically addressed, the package just needs
Yes, you can use a loader like |
I think a flag to enable it would be reasonable |
For private true only packages, this seems fine to me; bit i don’t think node should ever provide anything that directly allows stripping types from published packages (not counting the tools so a loader can be easily built) |
I created a PR that read the package.json private property. But private apparently does not allow publishing at all |
Correct, that’s why it’s a viable escape hatch - because nothing can ever be published with private true, so the only way to have one of those in node_modules is if it’s bundled or linked in. |
… on the public registry of npm, I’m not sure it makes sense to rely on it and assume it’s always going to be the case |
Certainly an unknown npm client, or an unknown private registry, could violate that rule (all known ones comply with it afaik), but the concern is package ecosystem leakage, and anything in those buckets isn't part of the package ecosystem. |
Node.js doesn't read the |
Id create a flag and keep it opt in forever. |
As long as the flag didn't work in NODE_OPTIONS or any committable config file, that'd be fine - otherwise, it would still cause ecosystem leakage since package authors who want to ship TS directly will just tell people to commit that config somewhere. |
Hi! Thanks everyone for your answers. 🙏 @JakobJingleheimer, I have I suspected it was because of the symlink, but even adding |
By the way, @aduh95 seems to be right: Node doesn't check the References: node/lib/internal/modules/typescript.js Lines 164 to 171 in 269c851
Lines 516 to 518 in 269c851
|
This is the PR that would have allowed it (I closed it) #55385 |
Ah, my mistake. I thought Marco's PR had landed. A loader like |
I don't think any of the concerns we raised last time have good answers, and adding a flag just adds pressure to main-lining this into Node.js. From the user-side, ensuring every invocation of So can we continue with the current recommendation of using a loader? |
What if gave it a try as a pure experiment? Maybe IDE can find a workaround and we can start publishing TypeScript source code in the future? The Node.js syntax constraint are strict enough that only a very small and stable subset is allowed |
It would never be a good idea to allow people to easily publish untranspiled TypeScript code, unless TypeScript reached a point where it would never make a breaking change or change its recommended configuration ever again, which I suspect it's unlikely to ever do. |
It is already possible to publish ts. |
With But by the same token, the intentional choice there was to not include stripping in What's the goal in adding a second not-very-different way to do the same thing? The only endgame I can see here is allowing TS-only packages in the registry, and that's a conversation that should be had directly if we want to revisit it. |
I think the value is to publish source code that does not need a build step. |
I'm not gonna push on this direction because I trust your judgment of knowing whats the best for typescript ecosystem. My point is that maybe it wouldnt be too bad as we think and it could unlock some possibilities |
I hear you. We really want TS authors to build into I think there's a reasonable argument to say that e.g. |
This is an interesting analysis. The way I read it, it's saying that publishing The gist points to a potential solution: the |
Let me list some of the obstacles here; this is not an exhaustive list. The local developer might be using an older version of TS than what the published author was using, so the .ts file in the registry might contain syntax that can't even be parsed. It just doesn't work. The local developer might be using a newer version of TS than what the published author was using, so the .ts file in the registry might generate an invalid (i.e. produced in the presence of type errors) declaration file due to bugs fixed in the newer version. Things will be unexpectedly Most projects out there can and do have custom build steps that are beyond just running I don't know where on disk these Correctly building a .d.ts from a .ts file requires knowing all its dependencies, and those dependencies can and do include things like a dev dependency on Because you need the identical external .d.ts files that the author had, which can come from anywhere, you need to be using an identical package manager to the one the package author was using, and be invoking it the same way that the package author does, at the same version. If the package author was using e.g. a The caching strategy here is also fraught. Correctly building a .d.ts from a .ts file requires knowing all its dependencies including everything that goes into the global scope, so the cache key here is not just "what is content of the input file", but "what are the contents of the entire transitive graph of files that this file causes to load", so each file really requires global knowledge of everything that was originally present during authoring. In this world, in a monorepo setting you don't really know if something from It gets worse from there, because you can't share this process between different packages, since it's not correct to just build one giant global scope - the global scope has to be computed for each project. especially because of config differences. So the first time you do this process, you're not just building the project you depend on, you're building all its dependencies, from scratch, in graph dependency order. Assuming all these problems can be solved or ignored, I just think this is placing the burden in the wrong place from a numerical standpoint. We're talking about doing this on every dev machine on every install forever, instead of once on each publish. For most packages, the install to publish ratio is conservatively 200,000:1, maybe more? A normal In fact, given the constraints, it's much cleaner to recommend "run tsc then publish" than to explain to them the entire set of caveats that would need to be met for "you don't need to run tsc" to work. You would need a tool that would validate that your npm package could be correctly .d.ts'd on the fly, and at the point you're running that tool, why not just run tsc instead and publish the .d.ts file that you know is correct (and save your downstream users the laptop fan spin)? |
In this scenario, the .d.ts file is basically a result of parsing something else. The fact that To which I can hear to argue, why should we want to support that. Besides the monorepo use cases, there are those who simply want to live in a world where .ts replaces .js and has the same level of convenience: it's standardized and there's no need for configuration or sidecar files like .d.ts. That's part of the point of type stripping: to provide a way forward toward that future. |
The file load overhead thing is possibly solvable in ways you've described (though the idea that we'd make every machine out there do a compile unnecessarily isn't something I would advocate for, even if we could cache, and even through every effort to make TS faster). There are still lots of problems we've mentioned around compatibility which declaration files are good for because they hide some certain details and provide maximal compatibility guarantees. |
Id say we should come back to this topic when the typescript support in Node.js is more stable. For now I'd mark it as wont fix. |
I confirm that using a loader works, and I strongly recommend However, I'm not fully convinced of this path:
I personally would like the experimental flag to allow people to ship TS packages to see how it goes, but I hear your concerns about the risks for the ecosystem, and I respect that. Can you instead reconsider @marco-ippolito's PR #55385? |
Loaders should not be used in production because you pay the cost every time. That makes sense in development where files are changing every time. For production, they do not change at all, so you should instead pre-compile with a tool like esbuild. You should also not use them in production because many of them provide only an inexpensive facsimile that is sufficient in their target environment. For example,
That's not what that text means, but sure. I'm not sure whether amaro/strip will go into node_modules though (I haven't checked, but I expect it to have the same restriction as node itself because it's the tool mode is using to do this internally).
Because you need to do different things. Unfortunate to have to pay the hard disk space twice. |
It does, see daquinoaldo/pnpm-monorepo-type-stripping#1. The reason is that Node.js type stripping is a wrapper around
Sorry, @JakobJingleheimer, I'm not very knowledgeable on loaders. Can you explain why I need to do different things? |
Ah, good :) By the way, the code in node you cited is not where the check is. It's here: node/lib/internal/modules/typescript.js Lines 169 to 171 in 269c851
Sure! I'm happy to improve the documentation. Please let me know (perhaps a DM so we avoid cluttering this issue) what parts are unclear or need expanding 🙂 I could potentially also write up a Learn article if it's more about consumption. |
I'm trying to use Why is running Node.js type stripping or a loader in production discouraged? From @GeoffreyBooth's comment, I understood that the overhead of type stripping is minimal. |
The problems you're encountering with esbuild are likely due to misconfiguration and/or misconsumption. I have almost never needed to mark a dependency as "external" for esbuild (basically, only for OS-specific packages). Esbuild is probably not recommending you mark them as external (I have never seen it do that), but rather suggesting that may be appropriate (it probably isn't). Loaders in general are a development API, not a production API. Type stripping is relatively cheap but not free, and the only reason to pay¹ that cost in production is laziness 😜 (we're all as lazy as we can be, but this is not the place to be lazy). So amaro is just as inappropriate for production as nodejs-loaders 😉 ¹ it will literally cost you money |
Great discussion, I learned a lot with this issue. My conclusion is that typescript inside Just wanted to bring attention to an alternative type-stripping compiler that doesn't seem to be much talked about: https://github.com/bloomberg/ts-blank-space This thing works great and can also be used as a loader. A quick test: mkdir -p ./node_modules/the-package
echo "let x: number = 123; console.log({ x });" > ./node_modules/the-package/index.ts
node --import="ts-blank-space/register" ./node_modules/the-package/index.ts One interesting thing about |
@paulovieira pretty sure ts-blank-space is what node uses, via amaro. |
Amaro is just SWC with some options set, and that's bundled with Node (but also on npm). ts-blank-space has the same idea (replacing types with spaces + ASI fixups etc) but using TS's APIs, largely for simplicity but also because it plugs well into Bloomberg's build system where they need the ASTs anyway. But they are different. (I think ts-blank-space was being worked on before strip types?) |
ah, fair - but i'm still pretty sure that SWC's implementation was inspired by ts-blank-space. |
Yes, I think ts-blank-space was the first one with this approach (released to the public in september/2024) and SWC quickly followed. This thread has some context: https://x.com/acutmore/status/1836762324452975021 Another interesting thing: this benckmark by @acutmore shows it is 2x faster than SWC/wasm (which is what |
What is the problem this feature will solve?
The
ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING
arises if you import a TypeScript file from another package. I understood this is to discourage having TypeScript-only packages. However, this also applies to private packages, where the organization decides whether a TS-only ecosystem fits for them. This is particularly annoying in monorepos, where imports are local, because it forces you to build the changed package before using it instead of changes just propagating naturally.What is the feature you are proposing to solve the problem?
We could have an opt-in flag, environment variable, or
package.json
field that enables type stripping innode_modules
. In this way, we keep discouraging TypeScript-only public packages, while allowing their usage in contexts where it makes sense.What alternatives have you considered?
pnpm Workspaces: In a monorepo, pnpm symlinks local packages, so development works fine. But when building an artifact (like a container image),
pnpm prune
copies local packages intonode_modules
, forcing you to copy the entire monorepo—which increases the artifact size.Turbo.build: Turbo lets you selectively copy only the necessary packages, avoiding the need to hardcode local packages in
node_modules
. The trade-off is that it introduces extra tooling and only works in monorepos; using local packages from cloned repositories still poses challenges (TS in development vs. JS in builds).File Watcher: A file watcher can rebuild changed dependencies and restart the server on file changes. However, this requires complex logic for monitoring files and managing dependencies.
The text was updated successfully, but these errors were encountered: