module: allow .ts in node_modules when .d.ts is present#63936
module: allow .ts in node_modules when .d.ts is present#63936GeoffreyBooth wants to merge 1 commit into
Conversation
|
Review requested:
|
e0bbcf0 to
1826070
Compare
|
I think this is a far better solution than any alternatives. Thank you. |
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>
1826070 to
789b878
Compare
|
Anything that allows or encourages publishing of untranspiled TS is a disaster for the ecosystem. At the very least, put this behind a flag? |
|
Also, for d.ts to be present with TS, tsc has been ran. What’s the point of not stripping the types at publish time? The monorepo use case is for no build process - meaning no d.ts files. |
Claude: There are two different things being conflated: The local, no-build monorepo loop already works — and this PR doesn’t touch it. When you run a workspace package in place, the package manager symlinks it into The restriction only bites when first-party code is copied into
Every one of those is a publish/deploy boundary — a CI or packaging step that already exists. Running On “why not just emit
|
|
That doesn’t really explain it. Of those 3 bullet points, only the first is something we’d want to support - the other two we actively want to be UNsupported. Enabling the first use case must not enable others. |
If I’m reading you right, your position is that The other objections I can find to TypeScript files in the public registry were semver (an author’s newer syntax breaking an older consumer) and So these cases don’t need to be kept apart. TypeScript with co-located declarations is fine in the public registry, so the private-registry, Git-URL, and global-CLI cases, which are all more controlled, are fine too. |
|
Performance is a very minor factor. The issue is that if .ts ends up in the registry, TypeScript will find itself unable to evolve. d.ts files do not eliminate config drift; each package's config matters quite a lot. DefinitelyTyped mandates that types be config-agnostic, certainly, but no other package has that guarantee. There is no circumstance where runnable TypeScript in the public registry is a good outcome, full stop. |
@ljharb can you clarify if this PR changes that? I'm not sure if you're saying that's already the sorry state of the ecosystem, or if it's a concern introduced by this PR.
I've seen this discussion point come up again and again, so there seems to be some communication gap, maybe an example would help get the point across. Is there by any chance a precedent where erasable TS syntax evolved in a non-backward compatible way? |
To clarify, you mean an example where old erasable TS syntax is no longer valid in a newer TS version? |
A module's emitted |
|
@aduh95 i'm saying it's a normal and expected state of the ecosystem, and not something that node can possibly affect in any way. |
ljharb
left a comment
There was a problem hiding this comment.
I don't think any TS-related feature should land over the objections of the TS team; putting a block to ensure this doesn't land until such time as there are confirmed to be none (feel free to dismiss it at that time, if it occurs)
|
I don't think that this detection method works. TypeScript prefers For example, a project may be switching to TS from JS or otherwise not set If you have that situation (even in (rene jinx, oops) |
@Renegade334 @jakebailey Thank you for this; I had always assumed #63869′s While looking into this I considered another idea: strip a |
Problem
By default, Node.js refuses to strip types from
.ts/.mts/.ctsfiles undernode_modules, throwingERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING. The restriction protects editor/tscperformance: a dependency that ships raw TypeScript without declarations forces consumers to infer types from its source (“slow types”).But using the folder as the boundary also blocks legitimate cases where trusted first-party TypeScript ends up under
node_modulesand does ship declarations:node_modules(pnpm deploy),Solution
Add an experimental, opt-in flag,
--experimental-strip-types-in-node-modules-with-declarations. Under it, a TypeScript file undernode_modulesis stripped and executed when a co-located declaration sits beside it (foo.d.tsnext tofoo.ts) — the default layout emitted bytsc --emitDeclarationOnly. The gate is a singlestat, and requires type-stripping to be enabled (rejected with--no-strip-types). Otherwise the existing error is thrown.Each of the blocked workflows already runs a build or publish step, so producing declarations is cheap: the same CI that assembles a
pnpm deploy, publishes to a private registry, or packs a global CLI can runtsc --emitDeclarationOnlyto emit.d.tsfiles beside the sources. That one artifact then both satisfies Node’s gate and ships ready-to-consume types, so the raw.tsruns with no separate JavaScript build — which is the thing these users are actually asking for.It also keeps the TypeScript team’s guarantee intact. The restriction exists to stop a dependency’s raw, undeclared source from dragging down a consumer’s editor and
tsc; by tying execution to the presence of a declaration, Node will only run raw.tsfrom a package that already provides one, so the consumer’s tooling always resolves a.d.tsand never falls back to inferring types across a dependency. The runtime permission is bound to the same artifact that keeps editing fast, rather than to where the code happens to live.Notes
exports"types"condition) is not recognized in this initial version; adding that is a small follow-up if the layout proves common..d.tsfiles.private: true(which can’t cover private-registry packages and doesn’t address editor performance).@nodejs/typescript @anonrig @marco-ippolito @RyanCavanaugh