module: add --experimental-strip-private-modules #63869
Conversation
|
Review requested:
|
003fda2 to
88c3881
Compare
aduh95
left a comment
There was a problem hiding this comment.
We'd need to document private in https://nodejs.org/api/packages.html#nodejs-packagejson-field-definitions
88c3881 to
5c48075
Compare
|
Adding the |
aduh95
left a comment
There was a problem hiding this comment.
I don't think it's worth it, also I don't think that we can use private to reliably guess if a package was downloaded from a registry or not, and it adds arguably a lot of complexity (in comparison for e.g. adding a loader, or not putting the code inside node_modules)
|
Weighing in from the TypeScript team: We're OK with this, and don't have any concerns about this eventually going unflagged either. That said, we wanted to re-emphasize @ljharb's and others' comments in the prior PR thread. Our top priority in this space remains ensuring that un-compiled .ts is never the entry point for a package in the public registry. The fundamental technical motivations behind that haven't changed, and we don't see any path forward where they could change. Keeping this restricted to |
9f68ed3 to
2cfa6d1
Compare
To be explicit for @nodejs/typescript: this is not a stepping stone to public-registry TS. @aduh95 I kindly ask you to reconsider. |
Signed-off-by: Marco Ippolito <marcoippolito54@gmail.com>
2cfa6d1 to
76740fc
Compare
andrewbranch
left a comment
There was a problem hiding this comment.
It would also be good to have one of the test fixtures' "private": true packages include a relative import of another TypeScript file in the same package, so it's clear that finding the "private": true package.json that allows type stripping doesn't depend on that same package.json being looked up as part of module resolution. I think the implementation already works this way, but it wasn't clear from reading the tests.
Just to be clear: I don't think Node has any technical issues with TypeScript packages in And if I remember correctly, the performance problem had to do with tsc parsing all the types of those packages, and their potential publishing with different tsconfig settings. And if they're published to the public registry as plain JavaScript, they're parsed as such and so no problem. Correct so far? So wouldn't then a potential solution to the tsc performance problem be for tsc to treat TypeScript packages under |
|
Sorry, I don't understand that suggestion and how it would address any of the concerns we've raised every time this discussion comes up. |
I think it's a bit premature. Let's see what the TypeScript team replies to my last comment. I've never found the 'the ecosystem will suffer' argument all that convincing because it relies on a series of hypotheticals all going a certain way, which I don't think is inevitable. I think there's probably still room for consensus here. |
|
Correct me if I'm wrong, but I tihnk there's no tool out there that will strip out the |
kdy1
left a comment
There was a problem hiding this comment.
I think private-only is a good direction, and will be useful enough
|
What if I publish my packages but to a private registry to be used on a project of ours, just like a monorepo? I'm sorry but this is just a bandaid. |
|
It depends on the |
|
You can't publish anything with Certainly you could go to some lengths to add private:true to things postinstall, but at that point you've spent more effort than just using a loader. As for the "ecosystem will suffer" argument, it's already empirically demonstrated by React Native - the entire RN ecosystem has to always be on the latest version of Metro or everything breaks. I'll happily bet real wads of cash that it will happen if we create a scenario where raw TS can be published, and I'll make out like a bandit. Bear in mind that tsconfigs vary far more than Metro configs can, so the problem will be a thousand times worse here. |
|
@aduh95 This doesn't incentivize anyone to make code private. The flag changes nothing about the publish decision, there's no upside to marking a package you want public as The use case is when a workspace package is copied into node_modules rather than linked: Let's not let perfect be the enemy of good. |
Yeah but let's not discriminate open-source software in favor of closed-source, I'm strongly -1 for a solution that leaves open-source out. I suppose that upon copying, the monorepo tool could add |
A software can have |
Couldn't it be a regular GitHub repo? And those can be installed directly. |
Maybe yes I havent tried. |
Sure but it doesn’t need to be published to the public registry. Like you can do |
|
True, but as of npm 12, you won't be able to do that by default (non-registry deps are highly discouraged and insecure). |
By default, Node.js refuses to strip types from `.ts`/`.mts`/`.cts` files under `node_modules`, throwing `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`. This protects editor and `tsc` performance: a dependency that ships raw TypeScript without declarations forces consumers to infer types from its source. But the folder-based rule also blocks legitimate cases where trusted first-party TypeScript ends up under `node_modules`, such as monorepo deploys (`pnpm deploy`), packages from a private registry or a Git URL, and globally installed TypeScript CLIs. Add an experimental, opt-in flag `--experimental-strip-types-in-node-modules-with-declarations`. Under it, a TypeScript file under `node_modules` is stripped and executed when a co-located declaration file sits beside it (e.g. `foo.d.ts` next to `foo.ts`), the default layout emitted by `tsc --emitDeclarationOnly`. The declaration's presence signals that the author pre-computed the type boundaries downstream tooling relies on, so editors read declarations instead of inferring from raw source. The check is a single `stat`, and the flag is rejected unless type-stripping is enabled. Refs: nodejs#63853 Refs: nodejs#63869 Signed-off-by: Geoffrey Booth <webadmin@geoffreybooth.com>
By default, Node.js refuses to strip types from `.ts`/`.mts`/`.cts` files under `node_modules`, throwing `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`. This protects editor and `tsc` performance: a dependency that ships raw TypeScript without declarations forces consumers to infer types from its source. But the folder-based rule also blocks legitimate cases where trusted first-party TypeScript ends up under `node_modules`, such as monorepo deploys (`pnpm deploy`), packages from a private registry or a Git URL, and globally installed TypeScript CLIs. Add an experimental, opt-in flag `--experimental-strip-types-in-node-modules-with-declarations`. Under it, a TypeScript file under `node_modules` is stripped and executed when a co-located declaration file sits beside it (e.g. `foo.d.ts` next to `foo.ts`), the default layout emitted by `tsc --emitDeclarationOnly`. The declaration's presence signals that the author pre-computed the type boundaries downstream tooling relies on, so editors read declarations instead of inferring from raw source. The check is a single `stat`, and the flag is rejected unless type-stripping is enabled. Refs: nodejs#63853 Refs: nodejs#63869 Signed-off-by: Geoffrey Booth <webadmin@geoffreybooth.com>
Great. Let's circle back to figuring out what problem we're trying to solve. If you never publish your package, then you can set If you don't publish your package, what's gained by publishing un-stripped TS (with the necessary .d.ts)? Missing upsides:
Actual downsides:
What's the scenario that concretely benefits from type stripping in published packages?
|
I think there are several use cases that would benefit from TypeScript files in published packages, and I think that requiring |
|
There's a lot of chat here about TS being unable to make breaking changes, which is a concern and avoided by people not shipping TS for sure, but the problems are deeper. A main concern was the prospect of us loading way more code in places like the editor or compilation, because that's what we'd read out of these packages. We've linked the analysis every time (I'm sure it's somewhere in the chain of many threads about this), but the impact would be pretty bad. A method like:
Does not work, because the whole point of TS is to parse out those types and do something with them. Imagine a lib whose function returns are inferred; we have to do the analysis! Then, on top of that are the differing compiler options and how loading someone else's TS impacts that; if your tsconfig bans unused variables, but you load someone else's code that has those, your compilation is now broken. The same goes for other more serious type checking things like strictFunctionTypes, the lib directives, etc. These are all of the concerns we raised when type stripping was added those years ago and was the reason why we pushed so hard to not create any scenario by which TS source would make it onto the registry. The "adjacent |
|
(fwiw i hand-write dts files, but that's because i use tsdoc to import their types into .js files - if i was authoring ts files, i can't imagine ever hand-writing a dts) |
Can you explain what they are? Maybe there are other ways to accomplish this. |
Sure. There are the ones I listed in #63936: first-party code shared across repos (private registry, Git deps, internal CLIs), and TypeScript-first packages that Deno and Bun run directly from source, where the author wants them to run on Node too without adding a transpile step solely for Node. Regarding other ways, I can think of a few:
|
|
Git deps are an antipattern and a security risk, which is why npm >= 12 will refuse to install them by default, so I think we can scratch that one off the list. The "run directly from source" pattern in Bun and Deno is very brittle, since "the ideal TS config" constantly changes over time, and TS itself sometimes ships breaking changes even in minor releases - I'm not sure we want to endorse/encourage that pattern by making it easier for people to do an unwise thing? |
Followup of #63853
Adds a flag to allow type stripping inside
node_modulesONLY if private.I think I covered #63853 (comment) concerns about smuggling private package.json's in a non private package