You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Feature] Support transformers written as ES modules #58
It is not currently possible to import a transformer that is written a standard ES module. Only transformers written as CommonJS modules work currently.
Node has supported standard ES modules since v12, so that's all currently supported versions of Node: https://nodejs.org/en/about/releases/. Node packages are increasingly being published as ES modules instead of CommonJS, so it would be great to be able to use them here too! One huge advantage of ES modules is that they can run directly both in Node and browsers.
Note this doesn't require writing ts-patch itself in ES modules, since there is bi-directional interoperability in Node. To import an ES module from a CommonJS module, use const mod = await import("./some-module.js") instead of require. To check if a module should be imported as an ES module, look in its package.json for the {"type": "module"} field.
I see a new version is being written at #40 -- I think this would be an awesome thing to include in the rewrite.
Thanks for the report. ESModule support is definitely important. I'm not sure if I'm missing something, but I'm not sure how you could implement this into CJS code without dramatic rewrites.
For example, the typescript compiler is a synchronous cjs file. In the case of tsc.js, you could probably make it work because you can pre-resolve the transformer files early (as you know exactly what tsconfig you're working with at execution, and you're only doing one). Even still, you'd still have to wrap the actual executable portion and only call it after you've resolved the promise for resolution.
In the case of the API (ie. typescript.js), it becomes much more difficult. You'd need to convert createProgram to an async function, and thus modify everything that leads to it, and the whole chain that use those, etc. Am I misunderstanding something? Is there a way to synchronously import via expression?
Edit:
I think the simple answer here is that typescript, itself, doesn't support esm transformers. Rewiring it to do so on our end wouldn't be a viable or sustainable route to take.
That said, however, our current support for .ts transformers is due to pre-transpiling. I think we could reasonably achieve esm support through the same means, by either:
Detecting if transformer uses esm programmatically (via package.json if provided, or perhaps catch, if not)
Adding a flag to indicate transformer is esm
Then, if it is necessary, we simply transpile ahead of time, like we do for .ts transformers
Again, I don't have a lot of knowledge around the actual esm spec or any recent developments thereof, so if there's anything I'm not seeing, I'd be glad to be wrong! From what I currently know, I think this may be the best means of achieving support.
Ah, interesting. It didn't occur to me that because you are patching TypeScript, it is not easy for you to add arbitrary asynchronous setup work. It is true that there is no synchronous dynamic import for ES modules. I can see that this would be quite hard to support!
I'm coming over from ttypescript (since it seems a bit abandoned). I'm guessing it might be easier to do this in the model it uses, because it's got an alternate frontend -- maybe it could just do arbitrary work before invoking any TypeScript APIs (in this case waiting for all the modules specified in compilerOptions.plugins.transform to be imported)?
We're essentially doing the same thing as ttypescript, with the main differences being that we add a few more features and ours is done once, where tts does it each time it's loaded.
Both libraries patch ts.createProgram, which gets called from many places throughout the compiler. If we made that an async function, it would affect everything that calls it.
Actually just supporting tsc would be relatively simple. As you mentioned, it could do the resolution work before executing the compilation. It's easy for us to add that in. The issue is that we also support the TypeScript compiler API, which can be called multiple times, with different configs. But as I mentioned, the real issue is that the typescript library (which natively handles transformers) doesn't support esm transformers.
But, this is really just me thinking out loud. I don't think any of that is relevant to the ask. The right move is to add support further upstream, like we did to natively support .ts transformers.
Have you tried using --loader ts-node/esm as a node parameter (or specifying in NODE_OPTIONS env)?
plugins: "..." does not have an export "default": {}`.
It seems when loadPlugin is called, it tries to require(transformerPath). However, require does not support ESM as far as I know. It's also worth noting that transformerPath is "" anyway, so require returns {}. transformerPath is empty because it's assigned using require.resolve.
Unless I'm totally mistaken here, using import instead of require would fix the issue. However, that would require the use of async/await. I'm not sure about require.resolve, though, because using import.meta.resolve can only be called inside of a module, itself.
Esm requires the esm package, but it should warn you of that. The plugin path should be relative file path to the plugin file.
transformerPath should not be empty, as it's resolved from your file path in your plugin config entry in tsconfig. ESM wouldn't play a factor at that point.
did you ever get to the bottom of this? I'm using ESM and continue to get:
evalmachine.:385
throw new tsp.TsPatchError(tsconfig.json > plugins: "${configTransformValue}" does not have an export "${importKey}": +
^
TsPatchError: tsconfig.json > plugins: "test" does not have an export "default": {}
Not sure how to get around it or where the issue is
Activity
nonara commentedon Aug 21, 2021
Thanks for the report. ESModule support is definitely important. I'm not sure if I'm missing something, but I'm not sure how you could implement this into CJS code without dramatic rewrites.
For example, the typescript compiler is a synchronous cjs file. In the case of
tsc.js, you could probably make it work because you can pre-resolve the transformer files early (as you know exactly what tsconfig you're working with at execution, and you're only doing one). Even still, you'd still have to wrap the actual executable portion and only call it after you've resolved the promise for resolution.In the case of the API (ie.
typescript.js), it becomes much more difficult. You'd need to convertcreateProgramto an async function, and thus modify everything that leads to it, and the whole chain that use those, etc. Am I misunderstanding something? Is there a way to synchronously import via expression?Edit:
I think the simple answer here is that typescript, itself, doesn't support esm transformers. Rewiring it to do so on our end wouldn't be a viable or sustainable route to take.
That said, however, our current support for
.tstransformers is due to pre-transpiling. I think we could reasonably achieve esm support through the same means, by either:package.jsonif provided, or perhaps catch, if not)Then, if it is necessary, we simply transpile ahead of time, like we do for
.tstransformersAgain, I don't have a lot of knowledge around the actual esm spec or any recent developments thereof, so if there's anything I'm not seeing, I'd be glad to be wrong! From what I currently know, I think this may be the best means of achieving support.
aomarks commentedon Aug 22, 2021
Ah, interesting. It didn't occur to me that because you are patching TypeScript, it is not easy for you to add arbitrary asynchronous setup work. It is true that there is no synchronous dynamic import for ES modules. I can see that this would be quite hard to support!
I'm coming over from
ttypescript(since it seems a bit abandoned). I'm guessing it might be easier to do this in the model it uses, because it's got an alternate frontend -- maybe it could just do arbitrary work before invoking any TypeScript APIs (in this case waiting for all the modules specified incompilerOptions.plugins.transformto be imported)?nonara commentedon Aug 22, 2021
We're essentially doing the same thing as
ttypescript, with the main differences being that we add a few more features and ours is done once, where tts does it each time it's loaded.Both libraries patch
ts.createProgram, which gets called from many places throughout the compiler. If we made that an async function, it would affect everything that calls it.Actually just supporting
tscwould be relatively simple. As you mentioned, it could do the resolution work before executing the compilation. It's easy for us to add that in. The issue is that we also support the TypeScript compiler API, which can be called multiple times, with different configs. But as I mentioned, the real issue is that the typescript library (which natively handles transformers) doesn't support esm transformers.But, this is really just me thinking out loud. I don't think any of that is relevant to the ask. The right move is to add support further upstream, like we did to natively support
.tstransformers.Have you tried using
--loader ts-node/esmas a node parameter (or specifying inNODE_OPTIONSenv)?[-]Support transformers written as ES modules[/-][+][Feature] Support transformers written as ES modules[/+]clearfram3 commentedon Jan 12, 2023
Does TS 5.0 being mostly ESM now change this at all?
nonara commentedon Apr 20, 2023
This is now supported in v3
refactor!: Rewrote for new major v3 (see detail)
colecrouter commentedon Aug 17, 2023
I'm on v3.0.2, and I'm unable to get ESM working.
It seems when
loadPluginis called, it tries torequire(transformerPath). However,requiredoes not support ESM as far as I know. It's also worth noting thattransformerPathis""anyway, sorequirereturns{}.transformerPathis empty because it's assigned usingrequire.resolve.Unless I'm totally mistaken here, using
importinstead ofrequirewould fix the issue. However, that would require the use of async/await. I'm not sure aboutrequire.resolve, though, because usingimport.meta.resolvecan only be called inside of a module, itself.nonara commentedon Aug 18, 2023
@Mexican-Man Can you make a repro?
Esm requires the esm package, but it should warn you of that. The plugin path should be relative file path to the plugin file.
transformerPathshould not be empty, as it's resolved from your file path in your plugin config entry in tsconfig. ESM wouldn't play a factor at that point.Sounds like your config had an issue somewhere
colecrouter commentedon Aug 18, 2023
@nonara It's more than likely I'm messing something up. Here's a reproduction with instructions. Please let me know if you spot anything.
zero1zero commentedon Nov 21, 2024
did you ever get to the bottom of this? I'm using ESM and continue to get:
Not sure how to get around it or where the issue is
zero1zero commentedon Nov 21, 2024
Opened a new issue here: #172