-
-
Notifications
You must be signed in to change notification settings - Fork 532
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
ESM support: soliciting feedback #1007
Comments
TODO: turns out, users can tell the language service to include the The option is passed to the language service in a |
I was trying to figure out if ts-node needs to automatically switch the Today, ts-node respects the tsconfig's Alternatively, we can automatically override the After thinking about this, it doesn't make sense. Users will choose either ESM or CommonJS via their package.json file. They won't do a mix of both. Also, this would get pretty messy since we'd be doing something that doesn't match Nevertheless, if we wanted to implement this: If the TypeScript has an internal |
We have released an experimental implementation of this in v8.10.1. Please test and share your feedback here. |
Thanks @cspotcode for the release! Everything seems be working minus one snafu. Importing named exports don't seem to be working, but this may be a Node module quirk. For example in import { graphql } from 'graphql'; will cause a syntax error of:
but this can be solved by using destructuring: import Graphql from 'graphql';
const { graphql } = Graphql; Any way to support importing named exports in ts files? |
@chpeters I'd guess that would be because |
Using mocha and TypeScript with ES modules I am facing an issue and I don't quite understand it. Running this cmd as my test cmd : node --experimental-modules --loader ts-node/esm.mjs ./node_modules/mocha/bin/mocha --extension ts I get this error : import './unit/authentication.js';
^^^^^^
SyntaxError: Cannot use import statement outside a module What did I do wrong ? PS: I have my |
Please send me a minimal reproduction and I'll be able to tell you.
…On Fri, May 8, 2020, 12:01 PM Julien Collard ***@***.***> wrote:
Using mocha and TypeScript with ES modules I am facing an issue and I
don't quite understand it.
Running this cmd as my test cmd :
node --experimental-modules --loader ts-node/esm.mjs ./node_modules/mocha/bin/mocha --extension ts
I get this error :
import './unit/authentication.js';^^^^^^
SyntaxError: Cannot use import statement outside a module
What did I do wrong ?
PS: I have my tsconfig.json module attribute set to "ES2015", my
package.json type attribute to "module", ts-node installed locally
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#1007 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAC35OCOQXHWRG4OZI2UC63RQQUFZANCNFSM4MGJCWPA>
.
|
This is my project architecture :
My {
"name": "my-project",
"version": "1.0.0",
"description": "My project",
"main": "lib/index",
"type": "module",
"files": [
"lib/**/*"
],
"directories": {
"test": "test"
},
"scripts": {
"build": "tsc",
"test": "node --experimental-modules --loader ts-node/esm.mjs ./node_modules/mocha/bin/mocha --extension ts"
},
"devDependencies": {
"@types/chai": "^4.2.11",
"@types/mocha": "^7.0.2",
"@types/node": "^13.13.5",
"chai": "^4.2.0",
"mocha": "^7.1.2",
"ts-node": "^8.10.1",
"typescript": "^3.8.3"
}
} My {
"compilerOptions": {
"target": "ES2015",
"module": "ES2015",
"lib": ["es6"],
"declaration": true,
"outDir": "lib",
"rootDir": "src",
"strict": true,
"noImplicitAny": true,
"moduleResolution": "node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"exclude": [
"test/"
]
} My import './unit/authentication.js'
Do you need more context ? |
@NeilujD this is perfect, thanks. It looks like, due to missing features in node's ESM support, mocha is using a hack to figure out whether a file should be loaded as ESM or CJS. ts-node's At first I thought mocha could simply |
I think I can hack this by delegating to node's built-in
At the cost of a failed
EDIT I've shared this hack with the node folks nodejs/modules#351 (comment) to see if they have any interest in exposing this API natively. |
Responding to #1007 (comment)
Actually... it should be possible, by providing a very simple posix shell script wrapping the whole thing. That would be ideal when using shebangs, as it's not allowed to pass options to shebangs in Linux (although it's possible in Mac). |
@castarco the problem is cross-platform support that matches the npm ecosystem without adding complexity to ts-node. Typically this is handled by the package manager: npm, yarn, pnpm, etc. We set up our package.json "bin" field, pointing to a file with a shebang, and it takes care of creating a symlink, |
@NeilujD The mocha issue you were seeing should be fixed by #1031 which has been merged to master. If you're feeling adventurous, you can install ts-node directly from master.
Or you can download and install the tarball artifact produced by CI.
|
* Fix TypeScript issues Commander can automatically import files for us by specifying 'executableFile' for commands. This unfortunately doesn't work well with TypeScript, which was why we were using `node -r ts-node` to run the CLI instead of `ts-node`. This setup to get commander working was also causing other issues with file imports. This commit updates so we now manually import files for commands, which enables us to revert the less-conventional TypeScript config. * Refactor file structure, update how commands run Since the tool is only React Native for now, it didn't seem necessary to have the 'react-native' directory. Moved to 'src'. Also created utility for importing commands for commander * Run ts-node with esm loader This fixes the issue where we are unable to run packages that only export ES Modules. It's possible we will still run into some issues with this configuration since ESM support is still experimental on the TypeScript side. Relevant issue: TypeStrong/ts-node#1007 ESM loader docs: https://typestrong.org/ts-node/docs/imports/#native-ecmascript-modules
Environment: Explanation:
I ultimately need to do 2 things:
I hopefullly added only the revelation-data that allowed me to finally put this dying horse to sleep for the night. My transition to Deno can't come soon enough; then I can shoot this poor horse out of its misery and TypeScript can run as a first-class citizen as God intended. # package.json
{
"type": "module", # Removing led to the revelation as `my-oneoff` executed wholly and completely, but... I NEEDS IT!
"scripts": {
"my-oneoff": "TS_NODE_PROJECT=custom.tsconfig.json node --loader ts-node/esm src/backend/my-oneoff.ts",
"dev:backend": "npx nodemon",
"build:backend": "tsc --project custom.tsconfig.json",
"postbuild:backend": "tsc-esm-fix --tsconfig='custom.tsconfig.json' --ext='.js'"
}
} # nodemon.json
{
"watch": [
"src/shared",
"src/backend"
],
"ext": "ts",
"exec": "ts-node --project custom.tsconfig.json src/backend"
} # custom.tsconfig.json
{
"ts-node": {
"esm": true, # suspiciously unequivalent to `ts-node --esm`, which no longer seems to work anyway. ¯\_(⊙︿⊙)_/¯
"experimentalSpecifierResolution": "node", # or `node --loader ts-node/esm --experimental-specifier-resolution=node`
},
"compilerOptions": {
"moduleResolution": "node",
"module": "esnext",
"target": "esnext",
"types": ["node"],
"esModuleInterop": true,
}
} I pray this helps but 1 person save the nightmarish day I've had--hopefully it will never be helpful to me again after I transition to Deno. :* |
https://nodejs.org/api/repl.html#repl TypeStrong/ts-node#1007 some real jank around ESM vs CommonJS - we seem to have to use .js file extensions in src/repl.ts ;; let's check if we CAN use .js everywhere in our .ts files at least that would be consistent.
@karts-with-freaking-lasers This works! Thanks, you did save from me a nightmarish day :) |
The Rename happened quite some time ago (Node 12.11.1, in October 2019). Deprecation warning is printed when running with Node 20. Documentation: It means:
|
node --import "data:text/javascript,import {register} from 'node:module'; import {pathToFileURL} from 'node:url'; register('ts-node/esm', pathToFileURL('./'))" my-script.ts It works on Node.js v20.10.0. 😢 But you can create a file named import {register} from 'node:module'
import {pathToFileURL} from 'node:url'
register('ts-node/esm', pathToFileURL('./')) And then: node --import ./ts-loader.js my-script.ts Remember: Don't write the loader path as |
Note that as of Node 20 you'll need to name it |
This solution led to an error for me:
Node.js v18.19.0 |
I used the graphql plugin of jetbrains idea editor, and the following error occurred. Node.js v20.11.0 Error
When I changed it to import
|
When using With this flag, it's now easy for CommonJS projects/code to slowly transition to ESM code dependency by dependency. The only issue is that |
@pksunkara i need to point out that one of the implementation details of require('es-module') is that it does not support and will never support for internal reasons top level await to shim that and even add support for it to ts-node you can go with -r esm the -r -i flags of node allow to import or requeire stuff before intrinsics are froozen the esm package ads cross interop in userland. until node 22.4~ will unflag that require-module thing. |
Of course, but many other use cases exist where the flag is enough. |
To sum up.
I am working with Quite amazed I will have now to switch to another client library |
Please use this ticket to provide feedback on our native ESM support. Your involvement is greatly appreciated to ensure the feature works on real-world projects.
Experimental warning
Node's loader hooks are EXPERIMENTAL and subject to change. ts-node's ESM support is as stable as it can be, but it relies on APIs which node can and will break in new versions of node.
When node breaks their APIs, it breaks loaders using their APIs. You have been warned!
Third-party docs: "Guide: ES Modules in NodeJS"
Someone has been maintaining a great reference document explaining how to use ts-node's ESM loader.
Guide: ES Modules in NodeJS
First-party docs
Our website explains the basics:
CommonJS vs native ECMAScript modules
Options: esm
Usage
Requirements
"module": "ESNext"
or"ES2015"
so that TypeScript emits import/export syntax."type": "module"
in yourpackage.json
, which is required to tell node that .js files are ESM instead of CommonJS. To be compatible with editors, the compiler, and the TypeScript ecosystem, we cannot name our source files.mts
nor.mjs
.import
statements, or pass--experimental-specifier-resolution=node
Idiomatic TypeScript should importfoo.ts
asimport 'foo.js';
TypeScript understands this.Invocation
ts-node-esm
/--esm
/"esm": true
work by spawning a subprocess and passing it the--loader
flag.Configuration
When running
ts-node --esm
,ts-node-esm
, orts-node
all CLI flags and configuration are parsed as normal. However, when passing--loader ts-node/esm
, the following limitations apply:tsconfig.json
is parsed.ts-node
must be installed locally, not globally.npm install ts-node
oryarn add ts-node
.tsconfig will be resolved relative to
process.cwd()
or toTS_NODE_PROJECT
. Specify ts-node options in your tsconfig file. For details, see our docs.Use
TS_NODE_PROJECT
to tell ts-node to use a specific tsconfig, and put all ts-node options into this config file.Versioning
As long as node's APIs are experimental, all changes to ESM support in ts-node, including breaking changes, will be released as minor or patch versions, NOT major versions. This conforms to semantic versioning's philosophy for version numbers lower than 1.0. Stable features will continue to be versioned as normal.
node's API change: v16.12.0, v17.0.0
Node made a breaking change in their ESM API in version 17, backported to 16.12.0. It may also be backported to 14 and 12.
This is the change: nodejs/node#37468
ts-node automatically supports both APIs, thanks to #1457. This relies on hard-coded version number checks. If/when this is backported to node 14 and 12, we will publish a new version of ts-node with the appropriate version number checks. Be sure you are always using the latest version of ts-node to avoid problems.
Note: things below this line may be out-of-date or inaccurate. These notes were used during initial implementation, but have not been updated since
Pending development work
process.argv
for config resolution?require('ts-node').esmImport(module, 'import-path')
The proposal
Below is the official proposal, explaining our implementation in detail.
I am asking node's modules team questions here: nodejs/modules#351
I was reading the threads about ESM support in ts-node, e.g. #935.
The @K-FOSS/TS-ESNode implementation is unfortunately incomplete; it does not attempt to typecheck. (it uses transpileModule)
So I did some research. Below is a proposal for ESM support in
ts-node
, describing the required behavior in detail.This doesn't feel like an urgent feature to me, but I like having an official proposal we can work on.
Usage
Cannot be invoked as
ts-node
because it requires node flags; hooks cannot be enabled at runtime. This is unavoidable.For simplicity,
--require ts-node/register
can be eliminated, becausets-node/esm
automatically does that.Alternatively, we publish an experimental
ts-node-esm
entry-point which invokes anode
subprocess.Don't forget
allowJs
! Affects the treatment of.js
files. (Not.mjs
nor.cjs
because the TS language service won't look at them)ESM hooks
Must implement ESM hooks to resolve extensionless imports to .ts files, resolve .js to .ts, classify .ts(x) and .jsx files as CJS or MJS, and compile .ts(x) and .jsx files.
resolve()
hook:Match additional file extensions:
.ts
,.tsx
,.jsx
.Resolve
.ts
,.tsx
, and.jsx
if the import specifier says.js
. Obey preferTsExts when doing this._
[Good idea?] Always ask default resolver first. If it finds something, we should not interfere.
--experimental-specifier-resolution=node
does not obeyrequire.extensions
, unfortunately, so we can't use that.getFormat
hook:If the resolved file is
.ts
,.tsx
, or.jsx
, behave as if extension was.js
: use node'spackage.json
discovery behavior to figure out if ESM or CJS.This can be accomplished by appending
.js
to the URL path and delegating to built-ingetFormat
hook.transformSource
hook:Same as today's code transformer. Relies on projects to be configured correctly for
import
/export
emit.Changes to existing functionality
require()
hookgetFormat
logic to determine if node will treat file as CJS or ESM.require.resolve
points to a.ts
file, we need to make the determination.require()
code transformimport()
calls.require('ts-node').esmImport(module, 'import-path')
?ts-node
bin entry-pointts-node
CLI does NOT need to supportimport()
ing ESM.WHY? Because ESM hooks are an experimental feature which must be enabled via node CLI flag.
Thus we will be loaded via
--require
, and Node is responsible for loading the entry-point, either triggering our hook or ourrequire.extensions
.Allow
import()
in CJSIf
"module": "commonjs"
, compiler transformsimport()
into__importStar
No way to change this without a custom transformer, which IMO is too much complexity at this time.
Users should run their code as ESM.
If they can't do that, we can recommend the following workaround:
Emit considerations
NOTE we have not implemented the following, although initially I thought we might. Instead, we assume tsconfig is configured for either ESM or CJS as needed
We could intelligently emit both
"module": "esnext"
and"module": "commonjs"
depending on the classification of a file.In
transpile-only
mode this is simple. CalltranspileModule
with different options.When typechecking, we can pull
SourceFile
ASTs from the language service / incremental compiler.We'll need a second compiler, one for each emit format. Or we can hack it by using
transpileModule
for all ESM output.transpileModule
is incompatible with certain kinds of TS code, (can't do const enums) but it might work for a first-pass implementation.The text was updated successfully, but these errors were encountered: