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 · 166 comments
Open

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

DanielRosenwasser opened this issue Oct 20, 2021 · 166 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.

@weswigham
Copy link
Member

@weswigham weswigham commented Jun 9, 2022

Realistically, there is no incremental strategy for migrating to node esm, because you can't import node esm from node cjs (synchronously). (Best you can do is provide both an esm and cjs package and use conditional exports to direct consumers to the one they need)

@pbadenski
Copy link

@pbadenski pbadenski commented Jun 10, 2022

Let me clarify - I can convert my TypeScript *.ts files to *.mts one by one while preserving the rest of the project in cjs. However there doesn't seem to be a way to do similar for *.tsx files.

@frank-dspeed
Copy link

@frank-dspeed frank-dspeed commented Jun 10, 2022

@weswigham maybe it will be possible in future with help of the new vm.Module api that can at last lead into the sync direction but i found a other little nice thing i can transpil ESM on the fly to require via spawnSync this way the code gets transpiled in sync via a async bundler and the process stays sync i can work with ESM only files as long as i use my loader.

@milesj
Copy link

@milesj milesj commented Jun 10, 2022

@pbadenski Are you trying to convert .tsx to .mts? Someone correct me if I'm wrong, but cts/mts do not support tsx, as they are Node.js runtimes (cjs/mjs) and JSX is a browser-side feature.

@fbartho
Copy link

@fbartho fbartho commented Jun 10, 2022

@milesj does that mean .tsx files must only and always be imported from CJS contexts? -- we have whole codebases with lots of .tsx files and would like to incrementally migrate our projects to ESM. #confused 😕

@milesj
Copy link

@milesj milesj commented Jun 10, 2022

I'll let the TS team clarify, but this is my understanding of how everything currently works.

  • .cts compiles to .cjs files, which is only usable in Node.js.
  • .mts compiles to .mjs files, which is also only Node.js.
  • .ts and .tsx should be used to compile to normal .js, which can be used in the browser or Node.js.

You don't need to only use .mts to support modules/ESM, you use the module setting, ideally using nodenext or ES2020 or higher (for import.meta). This should also be coupled with the target setting for down-leveling specific features. If you're using Babel/swc to compile your TS files, then those must be configured to output modules.

While those 2 settings compile your files to ESM, you'll still need handle the new Node.js module resolution logic (package exports and imports), which is done through the moduleResolution setting, which should be nodenext. However, do note that the npm ecosystem is not ready for this, as there are many packages with straight broken exports and types.

@cspotcode
Copy link

@cspotcode cspotcode commented Jun 10, 2022

I am also not on the TS team, but can clarify a couple things:

With NodeNext, .tsx follows the same rules as .ts: it obeys package.json "type"

In past discussions, hypothetical file extensions .mtsx and .ctsx were discussed which would compile to .mjs and .cjs respectively and would follow the same rules as .mts and .cts, with the notable exception that they would allow JSX syntax. However, those two hypothetical extensions are not supported today.

It is worth remembering that there are use-cases for JSX on nodejs, notably server-side rendering of JSX.

For incrementally migrating tsx from ESM to CJS, assuming you fully understand the issues you will face if you ever try to import ESM from CJS, your most pragmatic option might be to migrate directory-by-directory, creating tiny package.json files with {"type": "module"}

@andrewbranch
Copy link
Member

@andrewbranch andrewbranch commented Jun 10, 2022

@cspotcode is correct here. I will also add that we reserved JSX-ambiguous syntax in .mts and .cts files so we have the option in the future of allowing those files to contain JSX. So this is indeed a topic where we’re listening to feedback.

@fbartho
Copy link

@fbartho fbartho commented Jun 10, 2022

@cspotcode I assume you meant "from CJS to ESM" (we're all assuming that ESM is the future right?).

your most pragmatic option might be to migrate directory-by-directory, creating tiny package.json files with {"type": "module"}

How exactly does this work?

Here's the real problem: we've got a non-trivial amount of code. An increasingly large number of critical dependencies are going ESM-only. The ecosystem and TypeScript just don't seem to be ready to help us proceed?

What are we supposed to do? We can't ignore ESM, but we also can't move without the rest of the ecosystem moving?

My dream: TypeScript could have a compatibility mode that figures out how to import the package based on what it discovers during parsing. Our codebase isn't trying to do anything special at the module level. Linter rules or compiler deprecation warnings should flag the few cases we need to update for the module changes. This compatibility mode could take 2x longer to compile, and it still would be worth it, if it makes it easy for people to incrementally migrate to ESM. Allow any imports to include ".ts" extension and have TSC rewrite it during emit.

@cspotcode
Copy link

@cspotcode cspotcode commented Jun 10, 2022

assuming you fully understand the issues you will face if you ever try to import ESM from CJS

I assume you meant "from CJS to ESM"

I meant that if a CJS file attempts to import an ESM file, it will likely break.
For example, supposing you have two files, foo.tsx which is CommonJS, and bar.tsx which is ESM, and foo attempts to import {functionality} from './hypothetical/components/bar';

@fbartho
Copy link

@fbartho fbartho commented Jun 10, 2022

I meant that if a CJS file attempts to import an ESM file, it will likely break.

right @cspotcode I hear you. That's the hypothetical "compatibility mode" "migration mode" I'm looking for. Can't something automatically shim an ESM module to make it importable from a CJS module?

I totally understand if this breaks certain performance benefits of ESM or whatever, I'm just baffled as to why this detail can't be hidden from the caller if the calling source-code won't need changes. I can't help that my upstreams are going ESM only, they're transitive dependencies in some or many cases.

@andrewbranch
Copy link
Member

@andrewbranch andrewbranch commented Jun 10, 2022

@fbartho I don’t quite understand your use case. Why is it that you want to migrate from CJS to ESM file-by-file? Are you writing a self-contained app or a library that others will consume? If the latter, are you trying to provide entrypoints in CJS, ESM, or both?

An increasingly large number of critical dependencies are going ESM-only.

This is definitely a pain, but to be clear, it has nothing to do with TypeScript. There is no way any kind of compatibility mode TypeScript could dream up can get you out of the fundamental async pickle of needing ESM-only dependencies in a CJS context.

Can't something automatically shim an ESM module to make it importable from a CJS module?

No. That’s literally asking to convert an async operation to a synchronous one. This is the interop model that Node chose, which @weswigham (among others) advocated strongly against. But at this point it’s 100% out of our hands. All we can do now is relfect the reality that Node gave us.

@fbartho
Copy link

@fbartho fbartho commented Jun 10, 2022

Maybe I'm asking the wrong question.

We have a small dozen of nodejs backend applications (cloud functions, express servers, local tools, graphql servers, and frontend react sites), and another small dozen of library packages in a monorepo. We depend on libraries from npmjs some of which are going ESM-only.

We don't give a shit about CJS or ESM. But we do feel responsible for keeping our dependencies up to date, and weekly it seems like we have to mark more and more dependencies as "can't update this one anymore".

What's the right move here? How do we go from "everything is CJS" to whatever future allows our dependencies to stay up to date (via dependabot or similar)?

@andrewbranch
Copy link
Member

@andrewbranch andrewbranch commented Jun 10, 2022

Gotcha. So, just in case it wasn’t clear, ESM is importable from CJS, but it must be done with dynamic imports, which return a Promise. So, if all of your stuff is already async, you may be able to get away with awaiting for your ESM dependencies to be imported with relatively little fuss. (Well, I don’t want to predict how much fuss that will be, but it can be done.)

Migrating some, but not all, of your files to ESM will not help you with this, because as long as you have any CJS that depends on anything ESM, you’re going to have the infective async problem. Migrating all of your code to ESM will theoretically get you out of trouble, so I suppose you’re wanting to migrate piecemeal just because migrating all in one go is impractical. It is true that TypeScript is currently going to make it difficult for you to migrate TSX files to ESM one at a time because we lack format-specific extensions that support JSX syntax at the moment. Like @cspotcode said, the only way you can control the module format of TSX files is with the "module" key of package.json files. This is something that could change in TypeScript in response to community feedback.

That said, if you are set on eventually migrating everything to ESM, it might not be the worst to do big batches (whole sites/component libraries) of TSX files at one time, since these components surely depend on each other a lot—migrating them file by file might actually be more work than doing them all together.

@fbartho
Copy link

@fbartho fbartho commented Jun 10, 2022

if you are set on eventually migrating everything to ESM,

You say "if" like I have a choice, but I don't really think I do, dependencies are updating in breaking ways, and I can't exclude them forever, and I don't control the ecosystem packages that rely on them. ¯_(ツ)_/¯

Libs so far:
libs that went esm only in our dependencies/transitive dependencies


So let's say hypothetically, I'm willing to do this in batches. (And let's ignore .tsx files for this question). What are the steps?

Everything shares a top-level tsconfig. Each application/library has its own package.json.

  • What tsconfig changes are needed? I can create a second top-level config for ESM-ready packages to import from as we do the incremental migration if that's helpful.
  • What package.json changes are needed?
  • What standard source-level changes are needed?
    • I know about __dirname -> import.meta.url,
    • do all local imports need to be migrated to include a .js? (Is that still a requirement?, how do directory-index imports work?)

@weswigham
Copy link
Member

@weswigham weswigham commented Jun 10, 2022

We depend on libraries from npmjs some of which are going ESM-only.

This is a conscious choice by some package maintainers to try to use their package's popularity to lurch the ecosystem forwards, regardless of how much it harms their users. The node maintainers assumed package authors wouldn't be user hostile and would be willing to ship so called "dual mode" packages that had both esm and cjs sources for a time (or that most users still using cjs would be fine with old package versions), and that those would bridge the gap. In reality, there's little incentive for a library author to do so beyond perceived "legacy user" demand.

@andrewbranch
Copy link
Member

@andrewbranch andrewbranch commented Jun 10, 2022

For packages that are going to run in Node, setting module to node16 or nodenext should be the only tsconfig change needed. The whole point of this mode is that we model the interplay between CJS and ESM the same way Node does, so there’s certainly no need to try to split your code under two separate tsconfigs.

Assuming that all your code is currently compiling to CJS, you shouldn’t make any package.json changes—this will keep .ts files CJS by default, and you can start converting files to ESM by renaming them .mts. Then someday, you would have the option of reversing this—setting "type": "module" in package.json, which will make .ts files ESM by default, and you can use .cts to have outliers be CJS. Or don’t. You can use all explicit extensions if you like.

For your frontend apps and libraries, you should really consult with whatever bundler you’re using. It may be compiling your dependencies’ ESM code down to something synchronously available by merit of being bundled. These new modes in TypeScript are made specifically for Node. When bundlers do magic stuff, all bets are off.

do all local imports need to be migrated to include a .js? (Is that still a requirement?, how do directory-index imports work?)

In ESM files, yes. There are no directory-index imports in ESM.

Let me stress again—while I’m happy to give you advice, even in this thread here, beyond setting nodenext in your tsconfig.json, none of this has anything to do with TypeScript whatsoever. For working out the logistics of this migration, you would possibly be better served by searching for guides that are specifically about converting Node apps from CJS to ESM.

@andrewbranch
Copy link
Member

@andrewbranch andrewbranch commented Jun 10, 2022

setting module to node16 or nodenext should be the only tsconfig change needed.

Clarification: this implies correspondingly named values for moduleResolution. If you have explicitly set moduleResolution, you’ll need to either change or delete that too.

@thetutlage
Copy link

@thetutlage thetutlage commented Jun 11, 2022

I cannot even get Node16 to work

ts-socery.mov

@Hazmi35
Copy link

@Hazmi35 Hazmi35 commented Jun 11, 2022

I cannot even get Node16 to work

ts-socery.mov

Are you using bundled TypeScript from vxc (It could be outdated)? Make sure to use your workspace tsdk with this settings:

{
    "typescript.tsdk": "node_modules/typescript/lib",
}

Place it in .vscode/settings.json or anywhere, really, or you could use globally installed typescript

@thetutlage
Copy link

@thetutlage thetutlage commented Jun 12, 2022

Are you using bundled TypeScript from vxc (It could be outdated)? Make sure to use your workspace tsdk with this settings:

Even compiling with tsc wasn't working. But re-installing TypeScript did fix it. Thanks :)

@IgnacioFDM
Copy link

@IgnacioFDM IgnacioFDM commented Jun 27, 2022

What is the recommended way to upgrade large codebases to ESM? I haven't found anything in the docs. The most painful part is the no extensions imports to .js imports. Are there any codemods or tools to migrate? Ideally some transpiler or setting that allowed both .js and no extension so you can gradually migrate large codebases.

By the way there should be a much bigger and clearer warning that you should use .js and not .ts, since this is really counterintuitive. Especially if you use ts-node.

@weswigham
Copy link
Member

@weswigham weswigham commented Jun 27, 2022

What is the recommended way to upgrade large codebases to ESM?

There isn't one. "Upgrading" to esm is a breaking change for a node package, no way around it.

@IgnacioFDM
Copy link

@IgnacioFDM IgnacioFDM commented Jun 27, 2022

Yes, but a codemod or similar could exist to add the .js to imports to aid migration. Or a setting to allow imports without extension to work alongside .js so you can gradually migrate imports.

Both of these things are completely feasible and I'm asking if there's already something like that. There's no reason to manually add .js to a million imports if you can automate that.

@weswigham
Copy link
Member

@weswigham weswigham commented Jun 27, 2022

I mean, there are a bunch of community codemod tools that do stuff like that. We don't exactly bundle tools to make moving from target to target easier; it's just not a thing we do.

@IgnacioFDM
Copy link

@IgnacioFDM IgnacioFDM commented Jun 27, 2022

I understand not bundling codemods, but I was wondering if there was an official or unofficial recommendation on how to migrate to ESM.

Now that some popular packages are moving to ESM only, you can probably expect more and more people going from CJS to ESM. Expanding the docs with a page featuring a guideline or recommendations for migrating to ESM would be very helpful, it will avoid many future Github Issues.

Currently this page covers the technical basics regarding TS and ESM, but a page full on dedicated to migrating would be helpful to many people in the future.

Such page could mention the existence of tools that aid migration, or TS settings that help. From what I've seen the TS team has decided against any setting that allows .js and no extension imports from coexisting. Which is a pity and I hope you reconsider since this would be extremely useful for migrations.

After all the vast majority TS codebases currently use CJS, and in the future if they want to keep up with the npm ecosystem they'll presumably move to ESM. So good docs on this would help numerous projects.

@andrewbranch
Copy link
Member

@andrewbranch andrewbranch commented Jun 28, 2022

TS team has decided against any setting that allows .js and no extension imports from coexisting

There is a setting, and it’s called “using CommonJS” 🙃. We allow this wherever your module loader does. ESM in Node does not, so we do not.

It may sound like we’re being pedantic here, but our point of view is derived from two principles which may not be obvious to many people reading up on the new nodenext mode:

  • We don’t believe you should migrate to ESM for the sake of migrating to ESM. (Hence Wesley’s use of scare quotes on your characterization of this as an “upgrade.”)
  • If you actually have reasons to migrate to ESM, TypeScript should get out of your way. There should basically be no TypeScript-specific steps in your migration beyond using --module nodenext. Once you’ve done that, literally everything you do to make TypeScript happy (including putting .js extensions on your relative imports) is precisely what you would have to do to make Node happy.

This is core to why we don’t have a migration guide. If folks are set on migrating to ESM and seeking guidance, I would encourage them to look for general JS/Node guides on the subject—it shouldn’t have to be specific to TypeScript.

If you hit pain-points that are TypeScript specific (reminder: this does not include adding extensions to module specifiers; this is literally a Node requirement), we’re interested in hearing those—they may be good candidates for docs (if not actual fixes in our code). (FWIW, the area I’m most concerned with here is dealing with dependencies—it’s going to take some time before all the widely used npm packages who use conditional exports get their typings properly configured. For a user migrating to ESM who runs into dependency issues of this nature, there’s not much they can do beyond recognize what the problem is and file an issue with the library, but maybe we can help with that diagnostic step somehow.)

@IgnacioFDM
Copy link

@IgnacioFDM IgnacioFDM commented Jun 28, 2022

We don’t believe you should migrate to ESM for the sake of migrating to ESM.

I don't think anybody with a large codebase wants to migrate for the sake of migration. TS already gives you import. It's because of dependencies becoming ESM only that people might want to migrate. In fact I assume most people would prefer a flag that was "emit whatever makes Node ESM happy so I can use my ESM dependencies, without changing my current TS imports", because that would be the least amount of work.

But I understand TS is against that and I don't want to start another flamewar with something that has been discussed many times.

So given that some people will need to migrate to ESM, and such people will need to change their imports, I think there should be docs regarding this. And these docs could rephrase what you just explained, that you must change your imports so that they are imports that make Node happy.

But it is a typescript specific step because people are used to TS handling their imports. Sure, once you know how TS with ESM works you can say imports are now not a step specific to TS. But where are you going to get that knowledge?

People looking at general JS guides won't know how TS decided to handle ESM. They can at most make assumptions. And a lot of people, if they assume that now you have to use extensions with tsc too, they might assume that you have to use .ts instead of .js.

I assumed that and many people will assume that too. I'm not arguing against that decision, just stating that there should be good documentation so people don't make these wrong assumptions.

What I propose is having some documentation with something like a checklist for migrating, something like

  • Make the following changes to your package.json and tsconfig.json.
  • Change your imports so they have a .js file extension (NOT .ts). Consider using some tools to automate most of this process
  • Some smaller things to take into account

I see no downside to having such documentation, and while you can argue that you can infer all that from here, having this info concisely presented to those wanting to migrate will help a lot of people, and we'll avoid a lot of erroneous github issues.

@frank-dspeed
Copy link

@frank-dspeed frank-dspeed commented Jun 28, 2022

In NodeJS ESM is only a good Authoring format not a good Runtime format at all. The Pros of ESM while coding

  • static analyzeable imports.
  • easy transpilable / downleveling
  • when used as is it works in the browser most of the time and even inside nodejs

The parsing and loading speeds of ESM and CJS do total depend on the overall project variables like size module graph time of instantiation.

Cons ESM:

  • The ESM System to be more exact the specifier lookup is done in the Host that embeds the ES Engine so out of control without engine hooks or modifications. This is defined in the Specs will never change it was a mistake. Workarounds get invented that allow you to create user space ESM Systems.
    see: https://nodejs.org/api/vm.html#class-vmmodule there comes next support for interaction with the Module Loader see also other methods containing the word Module.
  • no access to the Module Cache without Restarting the engine no cleanup possible by design.
  • inside nodejs internal loading of dlls and .node modules so interaction with native code is always Sync in the CJS context
  • the engine instation is always Sync and in CJS even with ESM as Entrypoint that results in
    • v8::isolate create context (CJS) => run nothing on first run and start reading ESM Imports => Secund loop iteration of the engine starts to init ESM Code see: https://nodejs.org/api/vm.html#class-vmmodule again for the 3 phases that a ESM Module walks through by design ever phase gets runned after the whole engine stack is empty

Update because thumbs down

I did not design that module system that was ECMA i only say it . Thats how it is. i do not say that ESM Got well designed.

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