privatenumber / tsx

⚡️ TypeScript Execute | The easiest way to run TypeScript in Node.js
https://tsx.is
MIT License
8.64k stars 132 forks source link

Unexpected `top-level await error` for `type: module` + top level await in `node_modules` #552

Open JounQin opened 1 month ago

JounQin commented 1 month ago

Acknowledgements

Minimal reproduction URL

https://github.com/JounQin/test/tree/tsx

Version

4.10.1

Node.js version

v20.11.0

Package manager

yarn

Operating system

macOS

Problem & expected behavior (under 200 words)

// test.ts
import prettier from 'prettier'

const main = async () => {
  await prettier.resolveConfig('README.md')
}

main()

prettier@v3 is a dual package, I have type: module in my package.json, it should be run in ESM module, and it will resolve its own configs with import(), @1stg/prettier-config is a pure ESM package with top level await used, it should just work without runtime exception

But it just throws like the following: ```log node:internal/process/promises:289 triggerUncaughtException(err, true /* fromPromise */); ^ Error: Transform failed with 1 error: /Users/JounQin/Workspaces/GitHub/test/node_modules/@1stg/prettier-config/base.js:20:11: ERROR: Top-level await is currently not supported with the "cjs" output format at failureErrorWithLog (/Users/JounQin/Workspaces/GitHub/test/node_modules/esbuild/lib/main.js:1651:15) at /Users/JounQin/Workspaces/GitHub/test/node_modules/esbuild/lib/main.js:849:29 at responseCallbacks. (/Users/JounQin/Workspaces/GitHub/test/node_modules/esbuild/lib/main.js:704:9) at handleIncomingPacket (/Users/JounQin/Workspaces/GitHub/test/node_modules/esbuild/lib/main.js:764:9) at Socket.readFromStdout (/Users/JounQin/Workspaces/GitHub/test/node_modules/esbuild/lib/main.js:680:7) at Socket.emit (node:events:518:28) at addChunk (node:internal/streams/readable:559:12) at readableAddChunkPushByteMode (node:internal/streams/readable:510:3) at Readable.push (node:internal/streams/readable:390:5) at Pipe.onStreamRead (node:internal/stream_base_commons:190:23) { name: 'TransformError' } Node.js v20.11.0 ```

I'm not for sure why node_modules packages are also transformed by esbuild, is that expected to be transformed or configurable?

Contributions

privatenumber commented 1 month ago

Thanks for the bug report @JounQin

tsx supports loading ESM in Node's CommonJS mode by compiling it to CommonJS syntax... so if a module file (.mjs or package.json#type: 'module') gets require()'d, it gets converted to CommonJS. Since require() is sync, it cannot handle top-level await. Basically, this is the same limitation as the recent improvement in Node: https://github.com/nodejs/node/pull/51977

Looking at Prettier's code, it looks like it tries require() first, and if it fails, it tries import(). Because we want it to go to import() (which supports TLA), you can opt-in only to the ESM enhancement:

node --import tsx/esm scripts/test

This way, the require() will fail, and skip to the import() and the TLA file will get loaded expectedly.

Since it catches the ERR_REQUIRE_ESM error, it might be a good idea to make tsx emit that instead of or in addition to the TLA error.


By the way, if you have any sway in how Prettier loads the config files, the new tsImport() function might be useful for supporting TypeScript configs.

It's being experimented in ESLint via https://github.com/eslint/eslint/pull/18440