Skip to content
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

Concerns with TypeScript 4.5's Node 12+ ESM Support #46452

Open
DanielRosenwasser opened this issue Oct 20, 2021 · 136 comments
Open

Concerns with TypeScript 4.5's Node 12+ ESM Support #46452

DanielRosenwasser opened this issue Oct 20, 2021 · 136 comments
Labels
Discussion

Comments

@DanielRosenwasser
Copy link
Member

@DanielRosenwasser DanielRosenwasser commented Oct 20, 2021

For TypeScript 4.5, we've added a new module mode called node12 to better-support running ECMAScript modules in Node.js. Conceptually, the feature is simple - for whatever Node.js does, either match it or overlay something TypeScript-specific that mirrors thes same functionality. This is what TypeScript did for our initial Node.js support (i.e. --moduleResolution node and --module commonjs); however, the feature is much more expansive, and over the past few weeks, a few of us have grown a bit concerned about the complexity.

I recently put together a list of user scenarios and possibly useful scripts for a few people on the team to run through, and we found a few sources of concerns.

  • Bugs
  • UX
  • Ecosystem
  • User Guidance

Bugs

Most complex software ships with a few bugs. Obviously, we want to avoid them, but the more complex a feature is, the harder it is to cover all the use-cases. As we get closer to our RC date, do we feel confident that what we're shipping has as few blocking bugs as possible?

I would like to say we're close, but the truth is I have no idea. It feels like we'll have to keep trying the features for a bit until we don't run into anything - but we have less than 3 weeks before the RC ships.

Here's a few surprising bugs that need to get fixed before I would feel comfortable shipping node12 in stable.

  • Code changes breaking module resolution
  • Auto-imports not working: #46332 (technically present in CommonJS)
  • resolveJsonModule can't be used with node12: #46362
  • No errors on extensionless imports from .ts and .tsx files in node12 (unfiled, reported by @andrewbranch)
  • Strange errors under pnpm (unfiled, reported by @DanielRosenwasser)
  • package.json changes in packages not tracked (unfiled, reported by @DanielRosenwasser)

UX Concerns

In addition to bugs we found, there are just several UX concerns. Package authoring is already a source of confusion in the TypeScript ecosystem. It's too easy to accidentally shoot yourself in the foot as a package author, and it's too hard to correctly consume misconfigured packages. The node12 mode makes this a whole lot worse. Two filed examples of user confusion:

  • It's too hard to tell whether you're in an ESM or a CJS file: #46408
  • The export field is confusing to configure and diagnose: #46334
  • Poor errors on extensionless imports: #46152

While there might be a lot of "working as intended" behavior here, the question is not about whether it works, but how it works - how do we tell users when something went wrong. I think the current implementation leaves a lot of room for some polish.

But there are some questions about this behavior, and we've had several questions about whether we can simplify it. One motivating question I have is:

When a user creates a new TypeScript project with this mode, when would they not want "type": "module"? Why? Should that be required by default?

We've discussed this a bit, and it seems a bit strange that because we want to cover the "mixed mode" case so much, every Node 12+ user will have to avoid this foot-gun.

I would like to see a world where we say "under this mode, .ts files must be covered by a "type": "module"". .cts can do their own CommonJS thing, but they need to be in a .cts file.

Another motivating question is:

Why would I use node12 today instead of nodenext?

Node 14.8 added top-level await, but Node 12 doesn't have it. I think this omission is enough of a wart that starting at Node 12 is the wrong move.

Ecosystem

The ecosystem is CONFUSING here. Here's a taste of what we've found:

  • ts-node, Webpack, and Vite don't like imports with .js extensions, but TypeScript expects them. Not all of these can be configured with a plugin.
  • ts-node, Webpack, and Vite, and Deno are fine with .ts extensions, but TypeScript doesn't allow them!
  • Many packages that ship types have started supporting export fields, but don't have a types sub-field within export (e.g. RxJS, Vue 3).
  • Many packages have started supporting export fields, but their @types package might not reflect that.

The last two can be easily fixed over time, though it would be nice to have the team pitch in and help prepare a bit here, especially because it's something that affects our tooling for JavaScript users as well (see #46339)

However, the first two are real issues with no obvious solutions that fall within our scope.

There's also other considerations like "what about import maps?" Does TypeScript ever see itself leveraging those in some way, and will package managers ever support generating them?

Guidance

With --moduleResolution node, it became clear over time that everyone should use this mode. It made sense for Node.js apps/scripts, and it made sense for front-end apps that were going to go through a bundler. Even apps that didn't actually load from node_modules could take advantage of @types in a fairly straightforward way.

Now we have an ecosystem mismatch between Node.js and bundlers. No bundler is compatible with this new TypeScript mode (and keep in mind, back-end code also occasionally uses a bundler).

Here's some questions I wouldn't know how to answer confidently:

  • Is our guidance to always use this mode for plain Node.js apps, and let tools "catch up"?
  • Should new projects that use this mode pick node12 or nodenext?
  • There's a big foot-gun with forgetting "type": "module" - should we always recommend .mts?

Next Steps

I see us having 3 options on the table:

  • A mad dash to fix everything - I think this is too hard in the next 2 weeks to pull off.
  • Keep the feature in, but make it inaccessible until we're ready - I think temporarily removing the feature would be impractical. It would probably take more work to remove it than to fix the issues I've already mentioned. So we might as well keep it in.
  • Ship with some "experimental" labeling - I think this makes the most sense, but with some caveats to what I mean by "ship". It would make sense to just ship this as "experimental", but I think we should make this feature only work in nightly releases so that people can continue to easily use it, but not depend on a stable version until it's ready.
@frank-dspeed
Copy link

@frank-dspeed frank-dspeed commented Oct 24, 2021

I think this issue is a good example for the long needed plugin (hook) system.

The Solution to the first 2 Problems is rollup at present you can use it with plugin typescript to resolve anything correct and then inject it into the typescript program.

i am already researching how i could maintain and release a typescript-rollup version which would be typescript + rollup hooks and plugins.

Conclusion

a Plugin/Hook System is the Solution for the Resolve Problem. The Only one that is flexible and adjustable enough to cover every case.

@rayfoss
Copy link

@rayfoss rayfoss commented Oct 25, 2021

Conclusion

a Plugin/Hook System is the Solution for the Resolve Problem. The Only one that is flexible and adjustable enough to cover every case.

There is already a hooks system built into package.json... it's bad, but the whole point is to get rid of it as soon as dependencies merge the PR's to fix the issues.

I have the following install hook as a bandaid while upstream applies the fixes to default imports and reachable types/modules.

#!/bin/bash
set -euo pipefail

sed -i '2s/import express/import \* as express/' node_modules/\@feathersjs/express/index.d.ts
sed -i '1s/import http/import \* as http/' node_modules/\@feathersjs/socketio/index.d.ts
sed -i '2s/import io/import \* as io/' node_modules/\@feathersjs/socketio/index.d.ts
sed -i '8s/"source",/"\.\/source\/index\.js",\n"types": "\.\/index\.d\.ts",/' node_modules/chalk/package.json

I'd rather have 4.5 stable sooner, than to wait for yet another workaround.

@frank-dspeed
Copy link

@frank-dspeed frank-dspeed commented Oct 25, 2021

@rayfoss we can take your example to again show that a plugin/hook system like the one from rollup is badly neeeded.

you mixed linux shell script into your package.json as workaround.

@Jamesernator
Copy link

@Jamesernator Jamesernator commented Oct 25, 2021

Now we have an ecosystem mismatch between Node.js and bundlers. No bundler is compatible with this new TypeScript mode (and keep in mind, back-end code also occasionally uses a bundler).
Is our guidance to always use this mode for plain Node.js apps, and let tools "catch up"?

Yeah, so the trouble is if TypeScript doesn't encourage the use of node12/nodenext then the package author will be unable to use any packages that use Node ESM. So no matter which choice the author makes some things will be invariably broken.

This is something I've mentioned in the past on some related issue, but has it been considered not to have a distinct node and node12/nodenext mode at all, but rather just use --module node and require the presence of "type": "module" or "type": "commonjs" in package.json? (And regardless of this being present, .mts/.mjs/.cts/.cjs would work always, this would only be required for using .ts/.js in the new mode).

By having both --module nodenext and "type": "module" people are gonna be essentially double-configuring in a lot of cases anyway (most cases?), given people have to add --module nodenext to their tsconfig to enter the mode anyway, I don't see that it would be significantly worse to have them add "type": "module"/"commonjs" to their package.json instead.

@TheThing
Copy link

@TheThing TheThing commented Oct 25, 2021

Conclusion

a Plugin/Hook System is the Solution for the Resolve Problem. The Only one that is flexible and adjustable enough to cover every case.

This seems like the absolute worst conclusion to ever reach. I'm really sorry for butting in on this issue as not a huge avid typescript users but the one thing I like about typescript is precisely it doesn't require 9001 packages like soo many other builders and bundlers to actually get into a working state.

Typescript is standalone that just-works and doesn't require the end programmer to have to build-their-own-compiler themselves. Add plugins is just gonna be that, making it more and more complicated while adding nothing really. Especially something like this issue that REALLY needs to work out of the box but isn't. And saying to people "oh you're using typescript but typescript is dumb like bundlers, you need to hook plugins to get it working" is something you don't want to say to developers or people.

I really strongly advice against any case of adding plugins to this package. If it doesn't work, we can fix it. If it works, why would you need a plugin just to make configging even more complicated?

Best regards:
TT

P.S.
what is it with developers and wanting plugins in literally everything?

@frank-dspeed
Copy link

@frank-dspeed frank-dspeed commented Oct 25, 2021

@TheThing in my case it is simply needed because there are many package authors with total diffrent opinions and i do not want to get blocked by them. I also do not want to hardfork everything and so on.

The only Alternativ to a plugin system in typescript is the usage of dev bundels that are typescript compatible.

also my conclusion is driven from the fact that there are tons of other environments not only nodejs

i only vote for resolve hooks and plugins because of all the diffrent environments as also package managers.

@Jack-Works
Copy link
Contributor

@Jack-Works Jack-Works commented Oct 27, 2021

Node 14.8 added top-level await, but Node 12 doesn't have it. I think this omission is enough of a wart that starting at Node 12 is the wrong move.

What about removing Node12 and starts from Node 14?

@andrewbranch
Copy link
Member

@andrewbranch andrewbranch commented Oct 29, 2021

One note here from https://github.com/microsoft/TypeScript/issues/46550#issuecomment-954348769—for users who have declaration emit enabled, error messages like this are probably going to be a common symptom of a dependency that needs to update their export map to include "types" conditions:

error TS2742: The inferred type of 'T' cannot be named without a reference to '../../../node_modules/async-call-rpc/out/full'. This is likely not portable. A type annotation is necessary.

It feels pretty non-obvious that that’s what’s going on so I wanted to make a note of it here. Basically, the declaration emitter wants to print a type like import("async-call-rpc/out/full").T, but it can’t because the package has an export map that doesn’t specify any "types". So the only way it can reach that module is with a relative import through node_modules, which is not allowed to be synthesized by declaration emit. If you look at the package’s package.json, you can see that the actual specifier that should be generated will probably be "async-call-rpc/full", but each of those entrypoints in the export map needs a "types" if it’s going to be resolvable by TypeScript.

@andrewbranch
Copy link
Member

@andrewbranch andrewbranch commented Oct 30, 2021

☝️ Actually, I was wrong about this particular example. This may be a common symptom for some packages if their typings are stored in a separate folder from the JS that the export maps point to, like RxJs does. However, if the .d.ts files are colocated with the .js/.mjs/.cjs files pointed to by the export map, this should “just work.” This is the case for async-call-rpc, so the fact that the declaration emitter is complaining here is probably indicative of a module specifier resolution bug. (cc @Jack-Works)

@Jack-Works
Copy link
Contributor

@Jack-Works Jack-Works commented Oct 30, 2021

Oh. Yes, I found that package actually exports the correct typing and JS file at /full, not /out/full.

@demurgos
Copy link

@demurgos demurgos commented Oct 30, 2021

There are some rough edges with the new resolution mode. I've been trying out this mode for two weeks.

The main issues I've hit are:

  • unstable import resolution issues in IntelliJ IDEA (looks to be related to #46396 )
  • libraries missing a types declaration (@types/koa-route, raw-body)
  • libraries with an export field but no types condition while the type declarations are in their own directory (rxjs)

Despite all of these issues, I still hope that the new resolution algorithm becomes available on stable TypeScript: the core seems good and bugs/UX can be iterated. The new resolution mode lets me get rid of tons hacks and makes it finally ergonomic to use ESM natively.

  • I no longer have to manually watch out for explicit extensions in relative imports
  • I can use exports with subpath mapping to expose my outputDir directly for deep-imports; while being fully compatible with npm|yarn link and project references, and without having to abuse typeVersions.
  • For complex cases where types are generated directly in the build directory (e.g. using wasm-bindgen), I am now able to use import maps and remove dummy files used just so TypeScript does not complain about missing files.
  • Other various quality of life improvements such as proper handling of import.meta or __dirname.

I was able to use node12 with Node, webpack, mocha, wasm-bindgen, native node modules and Angular 13.

In my personal experience, all of these improvements strongly outweight the current issues: it makes things so much simpler as it realigns TS behavior's with Node's. I hope that the current issues get fixed soon and I hope that the concerns will not delay the new resolution too much.

@andrewbranch
Copy link
Member

@andrewbranch andrewbranch commented Nov 1, 2021

unstable import resolution issues in IntelliJ IDEA (looks to be related to #46389)

@demurgos did you get the right issue number here? This doesn't sound like it would be related to what you're talking about 🤔

@demurgos
Copy link

@demurgos demurgos commented Nov 1, 2021

I wanted to link the issue 46396: "nodenext alternates between finding and not finding the imported package"

I've double checked my message: I linked it as #46396 but it looks like GitHub failed to resolve it properly. Editing my message to add a single space to force a refresh fixed the rendering.

@Exac
Copy link

@Exac Exac commented Nov 2, 2021

What if TypeScript 5 assumes "type": "module" by default?

It would be regrettable if we have to add "type": "module" to our package.json in the far future, when many have migrated.

@Jamesernator
Copy link

@Jamesernator Jamesernator commented Nov 2, 2021

What if TypeScript 5 assumes "type": "module" by default?

It would be regrettable if we have to add "type": "module" to our package.json in the far future, when many have migrated.

Just to be clear it is Node (not TypeScript) that requires "type": "module" in package.json and that isn't likely to change probably ever. TypeScript just reads the value to understand how Node will run the module, TypeScript doesn't control how the module is run.

@frank-dspeed
Copy link

@frank-dspeed frank-dspeed commented Nov 3, 2021

at present i think the type fild in the package.json is less relevant as that is only a switch for the .js extension inside NodeJs Typescript at present 4.5+ detects the module type via import and export statments inside the .js files this should not change.

Typescript is not a NodeJS only Product at last i guess that.

ps i still have Javascript Projects without a package.json at all and i use the global installed typescript to typecheck them it works great and it should stay working.

@orta
Copy link
Contributor

@orta orta commented Nov 3, 2021

For folks who are interested in testing esm-node out:

npm add typescript@4.5.0-dev.20211101--save-dev
yarn add typescript@4.5.0-dev.20211101 --dev
pnpm add typescript@4.5.0-dev.20211101 --dev

This is the closest nightly npm release to the RC, so can act as "4.5 but with ESM enabled" for your projects.

@frank-dspeed
Copy link

@frank-dspeed frank-dspeed commented Mar 20, 2022

@weswigham why is there this createRequire logic=? out of my view that is the last resort of edge case but by no way a default.

top level import can import CJS by default without createRequire.

@glen-84
Copy link

@glen-84 glen-84 commented Apr 9, 2022

From the release announcement:

As a result, it will have to be rewritten to use the extension of the output of foo.ts – so bar.ts will instead have to import from ./foo.js.

I wanted to echo this comment:

On the subject of imports, why couldn't Typescript convert file extensions from *.js to *.ts during compilation? So that a programmer who works in a Typescript project imports files with the *.ts extension.
Importing *.js files in Typescript projects where you work on *.ts files looks... very strange, very aesthetically unpleasant

(I think the author meant from *.ts to *.js)

I have the same question.

@weswigham
Copy link
Member

@weswigham weswigham commented Apr 9, 2022

We've started this elsewhere (many times), but it's because we can't guarantee a rewrite of all imports, because dynamic import exists, so in our view it's better to continue to normalize writing imports like you're working in the built js (after all, ts extension imports have never been valid in ts). Also makes migrating between ts and js a bit easier - less to change.

@glen-84
Copy link

@glen-84 glen-84 commented Apr 10, 2022

We've started this elsewhere (many times) [sic]

I apologize for asking again, but I did search both the PR and this issue, and I couldn't find the answer.

dynamic import exists

Good point. Could it be allowed for static imports only? Dynamic imports would require a JS extension because they're dynamic and cannot always be statically analysed.

You're importing a TS file, not a JS file. Ctrl-clicking the import takes you to a TS file, etc. It just doesn't seem right.

@andrewbranch
Copy link
Member

@andrewbranch andrewbranch commented Apr 11, 2022

We are not going to discuss module specifier transformation in this issue. See #16577 and the duplicates that reference it.

@justinfagnani
Copy link

@justinfagnani justinfagnani commented Apr 11, 2022

We've started this elsewhere (many times), but it's because we can't guarantee a rewrite of all imports, because dynamic import exists, so in our view it's better to continue to normalize writing imports like you're working in the built js (after all, ts extension imports have never been valid in ts). Also makes migrating between ts and js a bit easier - less to change.

@weswigham it would be really great IMO if this were an official recommendation visible to TS embedders like ts-node. When those support .ts imports it makes TypeScript sources less portable.

@andrewbranch
Copy link
Member

@andrewbranch andrewbranch commented Apr 12, 2022

I think something like #37582 is more likely than us convincing runtimes and bundlers to artificially disallow imports of .ts. But we can continue that conversation on #37582.

@cspotcode
Copy link

@cspotcode cspotcode commented May 17, 2022

Picking up from #46452 (comment), where we talked about how to tell ts.transpileModule to emit this new style of ESM:

If a project has package.json type: module and we need to transform a .ts or .js file, how can we tell ts.transpileModule to emit this esm flavor? Is our only option to pass a filename that ends in .mts or .mjs?

I've found that this trick will not work for .tsx files, since they emit differently from .mts. There is no file extension I can use to make it emit like a .tsx file in a type=module directory.

.tsx files in package.json type=module directories support:

  • emitting import foo = require('foo') as import {createRequire as _createRequire} ... (not possible with tsconfig.json module=esnext)
  • emitting JSX syntax (not possible when replacing file extension with .mts)

@Jack-Works
Copy link
Contributor

@Jack-Works Jack-Works commented May 17, 2022

Do you know there are also .mtsx and .ctsx? 🤔

@cspotcode
Copy link

@cspotcode cspotcode commented May 17, 2022

Those extensions are not working for me in TS 4.7.1-rc, are you saying that they are supposed to be supported?

When trying to use an .mtsx file, I get a diagnostic from TS saying "The only supported extensions are '.ts', '.tsx', '.d.ts', '.cts', '.d.cts', '.mts', '.d.mts'."

@Jack-Works
Copy link
Contributor

@Jack-Works Jack-Works commented May 17, 2022

Oh... I recall those extensions but they don't exist. Strange.

I can found discussion about .mtsx in #44442 but I don't know the conclusion

@orta
Copy link
Contributor

@orta orta commented May 17, 2022

It was a long time ago, but I think the conclusion was that we keep the mts / cts parser available to handle JSX code and don't add support for mtsx and ctsx until it's a very obvious pressing need

@cspotcode
Copy link

@cspotcode cspotcode commented May 17, 2022

I tested to confirm: <Foo>foo type assertion syntax is reserved/forbidden in mts/cts. So although they do not support JSX today, the possibility was kept open.

@frank-dspeed
Copy link

@frank-dspeed frank-dspeed commented May 18, 2022

@andrewbranch @weswigham @DanielRosenwasser this is my answer and final conclusion about Module Systems in Typescript lets fallback to classic resolve what do you think about that https://github.com/frank-dspeed/tc39-proposal-importScripts/blob/main/README.md#typescript it is only a little usecase example but that is the one related to module system chaos.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Discussion
Projects
None yet
Development

No branches or pull requests