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
Add flag to not transpile dynamic import() when module is CommonJS #43329
List of keywords you searched for before creating this issue. Write them down here so that others can find this suggestion more easily and help provide feedback.
dynamic import, commonjs, esm, node
✅ Viability Checklist
My suggestion meets these guidelines:
This wouldn't be a breaking change in existing TypeScript/JavaScript code
This wouldn't change the runtime behavior of existing JavaScript code
This could be implemented without emitting different JS based on the types of the expressions
This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
Add a tsconfig flag to not transpile dynamic imports when module is CommonJS. Something like transpileDynamicImport which would be true by default and only take effect when "module": "CommonJS".
📃 Motivating Example
Since Node 12, it is possible to use dynamic imports. Yet I'm not able to tell the TS compiler to not transpile this into Promise.resolve().then(() => __importStar(require('..')));. This prevents users from importing ES modules into CommonJS, which will become increasingly common now that Node is transitioning to ES modules.
💻 Use Cases
The main use case is to be able to import ES modules into CommonJS, which is possible to do with import(). The workaround today involves an additional build step which does replacements to hide the import() statement from TS so it does not get transpiled and re-add it later on, which is suboptimal.
Just ran into this myself and noticed that this breaks ESM interop in node. Per node's documentation it is possible to load esm files from commonjs via import(). Therefore the dynamic import statement must not be transpiled to require calls. This is currently broken when using TypeScript.
Reacted by jviide, Lemmingh, zhangwei, Kirill Groshkov, Leon Visscher, Jesse Niininen, ehcac, Philipp G., Ian VanSchooten, Matt and 30 more
I can as I've been asked about this in a few contexts. Today we assume that all requires and imports have the same end result post-transpilation, as either commonjs or esmodules.
However, it's a bit more nuanced because node supports the same keywords working differently depending on if you're in an ESM context or CommonJS context. So in a commonjs context, you normally can't use import/export, but you can use await import to import an esm module:
Which is what you want for existing cases because that was a normal feature of require statements. It's a signal that the thing you're about to grab is a ESM module.
What's tricky is that TypeScript has no way to disambiguate whether you want await import in a cjs context to continue being a = require( or now stay as a = await import(. We'd either need a flag like the above which handles it app wide, or a pragma at the import call-site.
This seems to be hitting a few big projects because people want to have config files in ESM but let the app stay in cjs.
Reacted by Shawn, ExE Boss, Kirill Groshkov, Adam, Ömer Erdinç Yağmurlu, Paul Draper, Sepehr Soltanieh, Jason Hoetger, Mike Purvis, Nir Adler and 13 more
Greets... Thanks Simon H (@dummdidumm) for filing this issue. I was about to post the same thing about a week ago.
Orta Therox (@orta) not transpiling dynamic import is an important aspect to enable for Node v12+ in regard to loading ESM for CommonJS targeted TS efforts. It will be used for a lot more than loading configuration files. I am an outside contributor attempting (ESM support was added see - oclif/core#130) to get Heroku / Salesforce to add ESM loading capabilities to their Oclif v2 CLI framework that is due for launch in the coming months. I have already worked out the essential changes with a proof of concept that is discussed in the issue linked above, but it required a workaround. Oclif is a TS project which targets CommonJS for release. One can build CLIs in CommonJS or TS; my proposed changes adds ESM to the mix which is quite relevant as the Node ecosystem moves to ESM.
The least worst workaround I could come up with is the following: const _importDynamic = new Function('modulePath', 'return import(modulePath)') which is used internally to an encapsulated addition for loading ESM via dynamic import or require for non-ESM code - module-loader.ts.
As an outside contributor I certainly could not touch the build process, so the above seems to do the trick until a flag can be added to Typescript which is certainly desirable.
Reacted by Orta Therox, Al Marks, Grunet, Xiaoxing Hu, Alvis Tang, Simon Siefke, Linus Unnebäck, Jimmy Wärting, Lemmingh, Simon Tretter and 15 more
Reacted by Tung Nguyen
Reacted by Tung Nguyen, Stefano Pigozzi and Mike Purvis
What's tricky is that TypeScript has no way to disambiguate whether you want await import in a cjs context to continue being a = require( or now stay as a = await import(. We'd either need a flag like the above which handles it app wide, or a pragma at the import call-site.
If node style module resolution is enabled, and you encounter a bare module specifier in a dynamic import, would it make sense to check the imported package's package.json for "type": "module" and preserve the dynamic import if it is present?
If node style module resolution is enabled, and you encounter a bare module specifier in a dynamic import, would it make sense to check the imported package's package.json for "type": "module" and preserve the dynamic import if it is present?
If you also checked the extension of the file being imported for ".mjs", then yes I think that would be appropriate.
This has become a big problem for using some packages in a CommonJS app that are MJS only. sindresorhus/meta#15
Reacted by Al Marks, Alvis Tang, Rhys Arkins, Simon Siefke and lego290
This is highly desirable to maintain interop as the ecosystem slowly moves towards ESM because I can import a file without knowing the module system. The current all-or-nothing approach is too painful.
We'd either need a flag like the above which handles it app wide, or a pragma at the import call-site.
I would be okay with either solution, but the pragma would give the most flexibility if it can be configured per call site.
Reacted by Pelle Wessman, Rhys Arkins, Linus Unnebäck, Lemmingh, Jimmy Wärting, Al Marks, Nate Stringham, Gautham Chandra, ExE Boss, Benjie and 10 more
For future reference: A workaround for this issue can be had by moving the import() to a non-compiled dependency, such as inclusion.
Biggest drawback of this, apart from it being confusingly non-standard, is that types won't get imported. That can be worked around by manually importing the types.
Reacted by Terry Fei, metarmask, Andrew Jefferson, Kirill Groshkov, Alexander Drozdov, Chris Pearce, Jesse Jackson, F. Levi, Phil Ting, ehcac and 13 more
Reacted by Kirill Groshkov, Jesse Jackson and Blake Mitchell
Reacted by Maciej Holyszko, Patrick Hübl-Neschkudla, Steven, ExE Boss, Kirill Groshkov, Jesse Jackson, C. T. Lin, Bernard, ehcac, 小咩 and 1 more
Unfortunately, on our use case none of the proposed workarounds work, because they are incompatible with a strict Content Security Policy. We cannot add unsafe-eval to the CSP policies due to client requirements, so doing await eval or new Function('') is not possible for us.
If you are asking why would I transpile to CJS even though I'm using the module in the browser: a) I'm already using webpack so I don't care about module formats, and b) Jest doesn't support ESM and I don't want my users to be messing up with Jest and babel transforms just to run some tests.
Is there any workaround that doesn't require to eval code, or is there a flag planned to solve this issue? Thanks
"lib": ["ES2022"],
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
// currently necessary due to ESM only imports"skipLibCheck": true,
I solved a particularly difficult case this way, by writing a module in CommonJS outside of the TypeScript source tree (so that it doesn't get compiled) that simply re-exports the desired ESM library. Here's the setup:
The project is an npm package named @expo/cli.
It transpiles the TypeScript files in src to CommonJS files in build.
We want to import the ESM package globby, but TypeScript is unfortunately transpiling all our dynamic imports into require() statements.
Not actually relevant to this issue, but does complicate the solution: We can't use relative imports that climb outside of the src folder, because the build system outputs a directory structure that's not symmetrical with the source tree (it's nested one level deeper).
Here's the project tree visually (I added the lib directory to solve the issue at hand):
First, you make a CommonJS module that dynamically imports and then returns the globby ESM module:
/** * A re-export of globby. * * globby is an ESM module, and so can only be imported into a CommonJS project * by using dynamic import(). However, TypeScript transpiles import() to CommonJS. * @see https://stackoverflow.com/questions/65265420/how-to-prevent-typescript-from-transpiling-dynamic-imports-into-require * @see https://github.com/microsoft/TypeScript/issues/43329 */asyncfunctionimportGlobby(){returnawaitimport('globby');}exports.importGlobby=importGlobby;
lib/importGlobby.d.ts
Optionally, you can write some typings alongside it.
If this issue didn't exist (i.e. if globby were a CommonJS module), we'd be writing something as simple as this:
// If this issue didn't existimport{globbyStream}from'globby';exportasyncfunctiondemo(){forawait(constpathofglobbyStream('*.tmp')){console.log(path);}}
... but as that's not the case, see our workaround code below.
If you have a symmetrical build tree
For simple projects with a symmetrical build tree, you can consume the module as easily as this:
const{ importGlobby }=require('../../lib/importGlobby');exportasyncfunctiondemo(){// Hurrah, we've imported the `globbyStream` function from the `globby` module!const{ globbyStream }=awaitimportGlobby();// Now just use `globbyStream` as usual:forawait(constpathofglobbyStream('*.tmp')){console.log(path);}}
If you have an asymmetrical build tree
However, if, like me, you have an asymmetrical build tree, then your best option to get both typings and implementation may be to import relative to the package itself, or just leave it with any type.
Otherwise, you could just use it untyped.
// Do a little dance to grab the typingsimporttypeimportGlobbyModulefrom'../../lib/importGlobby';// Import the module relative to the project (our project is @expo/cli)const{ importGlobby }=require('@expo/cli/lib/importGlobby')astypeofimportGlobbyModule;exportasyncfunctiondemo(){// Hurrah, we've imported the `globbyStream` function from the `globby` module!const{ globbyStream }=awaitimportGlobby();// Now just use `globbyStream` as usual:forawait(constpathofglobbyStream('*.tmp')){console.log(path);}}
Apparently it's much(?) faster, not sure but maybe even safer.
Hello everyone, has anyone else encountered a problem while using this workaround with Jest?
I did but not exactly your case. I faced an error using the eval() workaround.
At the end, I decided to add --experimental-vm-modules option to NODE_OPTIONS before calling Jest. For reference, I am using Jest 29.5 at the moment with node 20.13.
My script line in package.json looks like:
..."jest": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest ..."...
I'm dying of this problem.
I tried using tsconfig.json to exclude file but it didn't work
In the end, I chose to just make a simple npm package await-import-dont-compile to prevent compilation
If someone else is facing this issue but doesn't want to install an additional package, this is an ugly but possible workaround: const { Provider } = await new Function("return import('oidc-provider')")()
Activity
marvinhagemeister commentedon Mar 21, 2021
Just ran into this myself and noticed that this breaks ESM interop in node. Per node's documentation it is possible to load esm files from commonjs via
import(). Therefore the dynamic import statement must not be transpiled torequirecalls. This is currently broken when using TypeScript.RyanCavanaugh commentedon Mar 22, 2021
Wesley Wigham (@weswigham) any additional context you might provide here?
orta commentedon Mar 25, 2021
I can as I've been asked about this in a few contexts. Today we assume that all requires and imports have the same end result post-transpilation, as either commonjs or esmodules.
However, it's a bit more nuanced because node supports the same keywords working differently depending on if you're in an ESM context or CommonJS context. So in a commonjs context, you normally can't use
import/export, but you can useawait importto import an esm module:What TypeScript thinks today is that this import should be switched to a
requireWhich is what you want for existing cases because that was a normal feature of
requirestatements. It's a signal that the thing you're about to grab is a ESM module.What's tricky is that TypeScript has no way to disambiguate whether you want
await importin a cjs context to continue being a= require(or now stay as a= await import(. We'd either need a flag like the above which handles it app wide, or a pragma at the import call-site.This seems to be hitting a few big projects because people want to have config files in ESM but let the app stay in cjs.
typhonrt commentedon Apr 1, 2021
Greets... Thanks Simon H (@dummdidumm) for filing this issue. I was about to post the same thing about a week ago.
Orta Therox (@orta) not transpiling dynamic import is an important aspect to enable for Node v12+ in regard to loading ESM for CommonJS targeted TS efforts. It will be used for a lot more than loading configuration files. I am an outside contributor
attempting(ESM support was added see - oclif/core#130) to get Heroku / Salesforce to add ESM loading capabilities to their Oclif v2 CLI framework that is due for launch in the coming months. I have already worked out the essential changes with a proof of concept that is discussed in the issue linked above, but it required a workaround. Oclif is a TS project which targets CommonJS for release. One can build CLIs in CommonJS or TS; my proposed changes adds ESM to the mix which is quite relevant as the Node ecosystem moves to ESM.The least worst workaround I could come up with is the following:
const _importDynamic = new Function('modulePath', 'return import(modulePath)')which is used internally to an encapsulated addition for loading ESM via dynamic import or require for non-ESM code - module-loader.ts.As an outside contributor I certainly could not touch the build process, so the above seems to do the trick until a flag can be added to Typescript which is certainly desirable.
aomarks commentedon May 1, 2021
If node style module resolution is enabled, and you encounter a bare module specifier in a dynamic import, would it make sense to check the imported package's
package.jsonfor"type": "module"and preserve the dynamic import if it is present?yinzara commentedon May 23, 2021
If you also checked the extension of the file being imported for ".mjs", then yes I think that would be appropriate.
This has become a big problem for using some packages in a CommonJS app that are MJS only.
sindresorhus/meta#15
Refactor module import workarounds
styfle commentedon Jun 18, 2021
The beauty of
await import()is that it can import both ESM and CJS:This is highly desirable to maintain interop as the ecosystem slowly moves towards ESM because I can import a file without knowing the module system. The current all-or-nothing approach is too painful.
I would be okay with either solution, but the pragma would give the most flexibility if it can be configured per call site.
orta commentedon Jun 19, 2021
This is going to be coming in natively and without a flag/pragma I expect, when more of the node ESM support starts rolling after #44501
voxpelli commentedon Aug 7, 2021
For future reference: A workaround for this issue can be had by moving the
import()to a non-compiled dependency, such asinclusion.Biggest drawback of this, apart from it being confusingly non-standard, is that types won't get imported. That can be worked around by manually importing the types.
Complete example:
Add new lit-dev-tools-esm package
Add script for checking lit.dev redirects (#468)
nstringham commentedon Sep 19, 2021
as I workaround I use
52 remaining items
feat: Update oidc-provider to v8
feat: Update oidc-provider to v8
fcano-ut commentedon Oct 23, 2023
Unfortunately, on our use case none of the proposed workarounds work, because they are incompatible with a strict Content Security Policy. We cannot add
unsafe-evalto the CSP policies due to client requirements, so doingawait evalornew Function('')is not possible for us.If you are asking why would I transpile to CJS even though I'm using the module in the browser: a) I'm already using webpack so I don't care about module formats, and b) Jest doesn't support ESM and I don't want my users to be messing up with Jest and babel transforms just to run some tests.
Is there any workaround that doesn't require to eval code, or is there a flag planned to solve this issue? Thanks
kth-tw commentedon Oct 31, 2023
It is work for me after set tsconfig.json
compilerOptionstonode16Zielak commentedon Nov 15, 2023
That doesn't work with module CommonJS:
feat: non-breaking ESM support 🤝 (#613)
elovin commentedon Dec 21, 2023
I also had to enable
"skipLibCheck": true:shirakaba commentedon Feb 18, 2024
Solution to a challenging case
I solved a particularly difficult case this way, by writing a module in CommonJS outside of the TypeScript source tree (so that it doesn't get compiled) that simply re-exports the desired ESM library. Here's the setup:
@expo/cli.srcto CommonJS files inbuild.require()statements.srcfolder, because the build system outputs a directory structure that's not symmetrical with the source tree (it's nested one level deeper).Here's the project tree visually (I added the
libdirectory to solve the issue at hand):File contents
lib/importGlobby.jsFirst, you make a CommonJS module that dynamically imports and then returns the
globbyESM module:lib/importGlobby.d.tsOptionally, you can write some typings alongside it.
src/index.tsIf this issue didn't exist (i.e. if
globbywere a CommonJS module), we'd be writing something as simple as this:... but as that's not the case, see our workaround code below.
If you have a symmetrical build tree
For simple projects with a symmetrical build tree, you can consume the module as easily as this:
If you have an asymmetrical build tree
However, if, like me, you have an asymmetrical build tree, then your best option to get both typings and implementation may be to import relative to the package itself, or just leave it with
anytype.Otherwise, you could just use it untyped.
juliolopeztorres commentedon Jul 11, 2024
I did but not exactly your case. I faced an error using the
eval()workaround.At the end, I decided to add
--experimental-vm-modulesoption toNODE_OPTIONSbefore calling Jest. For reference, I am using Jest 29.5 at the moment with node 20.13.My script line in
package.jsonlooks like:JinYuSha0 commentedon Aug 13, 2024
I'm dying of this problem.
I tried using tsconfig.json to exclude file but it didn't work
In the end, I chose to just make a simple npm package await-import-dont-compile to prevent compilation
This is how he uses it
Hope it helps!!!
wug-ge commentedon Nov 13, 2024
If someone else is facing this issue but doesn't want to install an additional package, this is an ugly but possible workaround:
const { Provider } = await new Function("return import('oidc-provider')")()Found in this solution here:
panva/node-oidc-provider#1249
aamersaleem9999 commentedon Nov 20, 2024
end of 2024 and it is still an issue. sigh...
fix: 导入 file-type 时出错
feat: Update oidc-provider to v8
feat: precise per-city Shabbat calculation via @hebcal/core, replacin…