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
This plugin (and Rollup in extension) work as seamlessly as possible with output generated by itself and tools in the ecosystem.
Actual Behavior / Situation
There are a whole lot of interop issues, see details below. This is meant as an overview over how this plugin actually works and a more or less complete picture of all interop situations and the current behaviour. The idea is to use this as a base for discussion and to identify sub-issues that can be solved with reasonable effort. I will gladly try to take care of all changes to Rollup core but am very happy if I get support improving the plugin.
This list is VERY long, and I will try to update it based on suggestions. I will also try to add a compiled summary at the end soon.
As I strongly believe in testing over believing, I added actual output code samples for almost everything. To generate the pre-formatted code samples, I used the following
Rollup config
// "rollup.config.js"importpathfrom"path";importcjsfrom"@rollup/plugin-commonjs";constinputFiles=Object.create(null);consttransformedFiles=Object.create(null);constformatFiles=(files)=>Object.keys(files).map((id)=>`// ${JSON.stringify(id)}\n${files[id].code}\n`).join("\n");constformatId=(id)=>{const[,prefix,modulePath]=/(\0)?(.*)/.exec(id);return`${prefix||""}${path.relative(".",modulePath)}`;};exportdefault{input: "main",plugins: [{name: "collect-input-files",transform(code,id){if(id[0]!=="\0"){inputFiles[formatId(id)]={code: code.trim()};}},},cjs(),{name: "collect-output",transform(code,id){// Never display the helpers fileif(id!=="\0commonjsHelpers.js"){transformedFiles[formatId(id)]={code: code.trim()};}},generateBundle(options,bundle){console.log(`<details><summary>Input</summary>\`\`\`js${formatFiles(inputFiles)}\`\`\`</details><details><summary>Transformed</summary>\`\`\`js${formatFiles(transformedFiles)}\`\`\`</details><details><summary>Output</summary>\`\`\`js${formatFiles(bundle)}\`\`\`</details>`);},},],output: {format: "es",file: "bundle.js",},};
Importing CJS from CJS
The plugin needs to take care that this works seamlessly, resolving require statements with module.exports of the required module. This is not really an interop problem, the goal of this section is rather to highlight how the plugin actually works and handles certain scenarios and where it could be improved before moving on to the actual interop situations.
Assignment to module.exports
In the importing file, we generate two imports: An empty imports of the original module while the actual binding is imported from a proxy module. The reason for the empty import of the original file is to trigger loading and transforming the original file so that we know if it is CJS or ESM when building the proxy file.
The actual module renders two exports: What is assigned to module.exports is exported as both default and __moduleExports. The proxy again exports __moduleExports as default (for situations where the proxy does slightly different things, look into the section where ESM is imported from CJS).
Assignments to properties of exports or module.exports
In this scenario, Rollup creates an artificial module.exports object that is created with all properties inline. This is very efficient as opposed to assigning the properties to an object one by one as the runtime engine can immediately optimize such an object for quick access. This object is again then exported as both default and __moduleExports. Additionally, all assigned properties are also exported as named exports.
🐞 Bug 1: Assigning to the same property twice will generate two exports of the same name, causing Rollup to throw a "Duplicate export " error.
Handling unsupported use of module.exports or exports
There are a lot of cases where the plugin deoptimizes, e.g. when a property is read instead of assigned. In such situations, createCommonjsModule helper is used to create a wrapper to execute the module more or less like Node would execute it without detecting any named exports.
// "bundle.js"functioncreateCommonjsModule(fn,basedir,module){return((module={path: basedir,exports: {},require: function(path,base){returncommonjsRequire(path,base===undefined||base===null ? module.path : base);},}),fn(module,module.exports),module.exports);}functioncommonjsRequire(){thrownewError("Dynamic requires are not currently supported by @rollup/plugin-commonjs");}vardep=createCommonjsModule(function(module,exports){{exports.foo="foo";}});console.log(dep);
Inline require calls
At the moment, the plugin is NOT capable of maintaining exact execution order. Rather, even nested and conditionally executed require statements (unless they are written via an if-statement in a particular way) are always hoisted to the top. This is a separate situation that could be improved by drastic changes, i.e. wrapping modules in function enclosures and calling them when they are first used. This will however have a negative effect on the efficiency of the generated code as compared to the status quo so this should only happen when it is really necessary. Unfortunately, it is not possible to tell if a module is required in a non-standard way until the whole module graph is built so in the worst case, all modules might need to be wrapped. Rollup could help here a little by implementing some inlining algorithm, however this is very much future talk (say, up to a year in the future) and will likely only apply anyway if a module is used exactly in one place. Other approaches could be for the plugin to analyze the actual execution order to see if it can ensure that the first usage does not need wrapping so that it does not matter if there are dynamic requires later on but this feels complicated and error-prone.
Anyway, this is mostly listed for completeness here as it does not really touch on the subject of interop but warrants its own issue. Here is a sample to illustrate:
Input
// "main.js"console.log("first");require("./dep.js");console.log("third");false&&require("./broken-conditional.js");// There is special logic to handle this exact case, which is why// "working-conditional.js" is not in the module graph, but it is not easily// generalized.if(false){require("./working-conditional.js");}// "dep.js"console.log("second");// "broken-conditional.js"console.log("not executed");
Transformed
// "main.js"import"./dep.js";import"./broken-conditional.js";import"./dep.js?commonjs-proxy";importrequire$$1from"./broken-conditional.js?commonjs-proxy";console.log("first");console.log("third");false&&require$$1;// There is special logic to handle this exact case, which is why// "working-conditional.js" is not in the module graph, but it is not easily// generalized.if(false){require("./working-conditional.js");}// "dep.js"console.log("second");// "\u0000dep.js?commonjs-proxy"import*asdepfrom"/Users/lukastaegert/Github/rollup-playground/dep.js";exportdefaultdep;// "broken-conditional.js"console.log("not executed");// "\u0000broken-conditional.js?commonjs-proxy"import*asbrokenConditionalfrom"/Users/lukastaegert/Github/rollup-playground/broken-conditional.js";exportdefaultbrokenConditional;
In Node, CJS modules only expose a default export that corresponds to module.exports. This important pattern should always work.
For the plugin, the main difference here is that instead of the proxy, the actual module is imported directly. Here just one example to illustrate, in general everything works similar to the CJS-to-CJS case.
This is supported by Webpack but (previously in part but now fully) also by this plugin. In addition to the default import resolving to module.exports, named imports will resolve to properties on module.exports. Previously, this would only work for named exports that the plugin could auto-detect (and only if the module was not deoptimized to use createCommonjsModule) or which were listed by the user. The use of Rollup's syntheticNamedExports property in its current form now enables arbitrary named imports to be resolved without additional configuration while even maintaining live-bindings.
A Note about Webpack .mjs semantics and better NodeJS interop
Note that Webpack currently either disallows or warns about this pattern when used from modules with the .mjs extension.
🚧 TODO: Would be nice to confirm this
The intention here is that this extension signals we want to enter some sort of strict NodeJS interop mode. We could do something similar, but then I would love to have the ESM/CJS detection in @rollup/plugin-node-resolve and establish a communication channel to get this information from that plugin. Then we might add a switch to @rollup/plugin-commonjs to use "strict NodeJS interop" that
Does not auto-detect module types but uses NodeJS semantics (extension + package.type), which could even give a slight speed boost
Disallows non-default imports from CJS files
This could become an advanced feature we add after solving the more pressing issues we have at the moment.
Here we just assign an object to module.exports. Note how we retain live-bindings by using property accesses: If the object at module.exports would be mutated later on, accessing our named variable would always provide the current value.
This is the tricky one. The problem here is to maintain isomorphic behaviour between the original ES module and the CJS module. Named exports are handled mostly correctly when using the "NodeJS with named imports" pattern (except we do not throw for missing exports) however the default export should not be module.exports but module.exports.default. This is incompatible with the previously listed interop patterns.
At the moment most tools implement a runtime detection pattern for this by adding an __esModule property to module.exports to signify this is a transpiled ES module. Then the algorithm when getting the default import is
If this property is present, use module.exports.default as the default export
Otherwise use module.exports
Example importing a named export when a default export is present
🐞 Bug 2: This is not working correctly as instead of the named export, it tries to return a property on the default export. The reason is that in this situation, the interop pattern in unwrapExports kind of correctly extracts the default export and exports it as default, but syntheticNamedExports should not use that to extract named exports.
// "bundle.js"functionunwrapExports(x){returnx&&x.__esModule&&Object.prototype.hasOwnProperty.call(x,"default")
? x["default"]
: x;}functioncreateCommonjsModule(fn,basedir,module){return((module={path: basedir,exports: {},require: function(path,base){returncommonjsRequire(path,base===undefined||base===null ? module.path : base);},}),fn(module,module.exports),module.exports);}functioncommonjsRequire(){thrownewError("Dynamic requires are not currently supported by @rollup/plugin-commonjs");}vardep=createCommonjsModule(function(module,exports){Object.defineProperty(exports,"__esModule",{value: true});exports.foo="foo";exports.default="default";});vardep$1=/*@__PURE__*/unwrapExports(dep);console.log(dep$1.foo);
This is quite difficult to fix, especially if we want to maintain live bindings for named exports. My first thought was to extend syntheticNamedExports to allow an additional value to indicate that the default export is also picked as default property from the actual default export. This would mean however that auto-detecting interop becomes slow and difficult and destroys likely all live-bindings for the non ESM case because we need to build a new object, i.e.
📈 Improvement 1: A better idea I had is to allow specifying an arbitrary string as value of syntheticNamedExports, e.g. syntheticNamedExports: "__moduleExports". The meaning would be that missing named (and even the default) exports are no taken from the default export but from a named export of the given name. Then the interop would just be
This is rather efficient, though it could still be put into an interop function getDefault if we want to save a few bytes. Of course we still do not get live-bindings for the default export in the transpiled ESM case, but even this is fixable if in a second step, we implement static detection for __esModule:
📈 Improvement 2: If we come across the Object.defineProperty(exports, "__esModule", { value: true }) line (or !0 instead of true for the minified case) on the top level of a module, then we can just mark this module as being transpiled and can even get rid of this line in the transformer, making the code more efficient and removing the need for any interop, i.e. above we do not add the export default at all in that case. There is also no longer a need to wrap the code in createCommonjsModule if this property definition is ignored.
Example importing a non-existing default export
🐞 Bug 3: It is not really surprising that this case is not working correctly as it should actually throw either at build or at runtime. Otherwise at the very least the default export should be undefined while here it is actually the namespace.
// "bundle.js"functionunwrapExports(x){returnx&&x.__esModule&&Object.prototype.hasOwnProperty.call(x,"default")
? x["default"]
: x;}functioncreateCommonjsModule(fn,basedir,module){return((module={path: basedir,exports: {},require: function(path,base){returncommonjsRequire(path,base===undefined||base===null ? module.path : base);},}),fn(module,module.exports),module.exports);}functioncommonjsRequire(){thrownewError("Dynamic requires are not currently supported by @rollup/plugin-commonjs");}vardep=createCommonjsModule(function(module,exports){Object.defineProperty(exports,"__esModule",{value: true});exports.foo="foo";});varfoo=/*@__PURE__*/unwrapExports(dep);console.log(foo);
To fix this, I would like to reduce the interop pattern to only look for the presence of __esModule and nothing else. This would be covered by the suggested Improvement 2.
Importing ESM from CJS
To my knowledge, there is no engine that supports this at the moment, so the question is what the "correct" return value should be. For NodeJS, I heard that there is actually some interest in supporting this eventually but the road blocks are technical here (mostly that the ESM loader is async + TLA handling and similar things). However if this is ever supported, a require of an ES module will likely return the namespace. Otherwise Webpack supports this of course, and to my knowledge here you always get the namespace as well. Looking at TypeScript it will always add the __esModule property on transpiled ESM modules and use the default as a property which in turn means that if you require an ES module and use CJS as output target, you always get the namespace. The same goes for Babel.
For Rollup, it is more complicated, and the reason is in part rooted in that fact that Rollup was first and foremost designed for libraries, and here you want to be able to create CJS output where everything, e.g. a function, is directly assigned to module.exports to be able to create a nice interface for "one-trick-pony" libraries. So Rollup core by default uses its "auto" mode when generating CJS output, which means
module.exports contains the namespace unless
there is exactly one default export, in which case that one is assigned to module.exports
Now of course this is about converting ESM to CJS but one idea is that in such a situation, the CJS and ESM versions should be interchangeable when I require them. So for internal imports, it could make sense to work like Rollup's auto mode in reverse, giving you the default export when there are no named exports and the namespace otherwise. But I understand that in the long term, we should likely align with the rest of the tooling world even if it generates less efficient code, so my suggestion on this front is:
📈 Improvement 3: We switch to always returning the namespace on require by default
📈 Improvement 4a: We add a flag to switch either all modules or some modules (via glob/include/exclude pattern or maybe something similar to how the external option in Rollup works) to work like auto mode as outlined above to make existing mixed ESM CJS code-bases work. The reason I think we need this is that the requiring module can easily be a third party dependency itself and thus not under your direct control.
📈 Improvement 5: Rollup itself will be adjusted to display a warning when using "auto" mode without specifying it explicitly to explain the problems one might run into when the output is meant to be interchangeable with an ES module, explaining how to change the interface.
To view the full interop story here, one has to look both at what this plugin generates and what Rollup generates as CJS output.
External imports in the CommonJS plugin
🐞 Bug 5: For external imports, this plugin will always require the default import.
📈 Improvement 6: With the arguments given above, this should actually be the namespace by default (import * as external from 'external') as this is technically equivalent to requiring an ES module.
📈 Improvement 4b: Again we should add an option to specify when an external require should just return the default export instead. It could even be the same option. So here is some room for discussion.
The interop function just checks for the presence of a default property instead of checking the __esModule property. I always thought there was a reason buried in some old issue but going back in history it seems it has always been that way. This should change as it has all sorts of adverse effects: If the file was not an ES module but assigns something to exports.default, it will be mistaken for a default export; if it was an ES module but does not have a default export, this will wrongly return the namespace instead.
Live-bindings of default imports will not be preserved. This could cause issues if there ever were circular dependencies with external modules.
Entry point which assigns to module.exports but requires itself
🐞 Bug 7: Basically it looks for a __moduleExports export that does not exist instead of giving the namespace. My suggestion above for how to rework syntheticNamedExports in Improvement 1 should also fix this from within Rollup. Similar problems arise when another module imports our entry point or when the module just adds properties to exports.
Ideally, the output should have the same exports as the original entry. At the moment, this will not be the case as the Object.defineProperty call will always cause the createCommonjsModule wrapper to be used and no named exports will be detected. There are several ways to improve this:
Remove the __esModule property definition and do not treat it as a reason for deoptimization, see Improvement 2
📈 Improvement 8: Add a new option similar to the now removed namedExports that specifically lists exposed exports for entries. We could also use this to activate or deactive the default export and decide if the default export should be whatever is assigned to exports.default or rather module.exports. This could be handled by simply creating a wrapper file that reexports these exports. If we do it that way, Rollup tree-shaking might even remove some unused export code.
// "bundle.js"functionunwrapExports(x){returnx&&x.__esModule&&Object.prototype.hasOwnProperty.call(x,"default")
? x["default"]
: x;}functioncreateCommonjsModule(fn,basedir,module){return((module={path: basedir,exports: {},require: function(path,base){returncommonjsRequire(path,base===undefined||base===null ? module.path : base);},}),fn(module,module.exports),module.exports);}functioncommonjsRequire(){thrownewError("Dynamic requires are not currently supported by @rollup/plugin-commonjs");}varmain=createCommonjsModule(function(module,exports){Object.defineProperty(exports,"__esModule",{value: true});exports.default="foo";exports.bar="bar";});varmain$1=/*@__PURE__*/unwrapExports(main);exportdefaultmain$1;
Summary and possible action plan
In Rollup:
I will gladly take care of the necessary improvements here:
Implement Improvement 5: Add a descriptive warning when using auto mode without specifying it explicitly and there is a package that has only a default export. Explain how this package will not be interchangeable with its ESM version in many tools and suggest to use named exports mode with all its consequences or better, do not use default exports.
► Warn when implicitly using default export mode rollup#3659 ✅
Implement the Rollup part of Improvement 1: syntheticNamedExports should support receiving a string value that corresponds to an export name from which to pick missing exports. The value of true would correspond to "default" except that for entry points when using a string, the listed property name would not be part of the public interface. This will fix issues where suddenly __moduleExports is introduced into an interface.
► Add basic support for using a non-default export for syntheticNamedExports rollup#3657 ✅
In Rollup 3 (or behind a flag in Rollup 2 as this is a breaking change): Implement Improvement 7: Change how the default is imported in CJS output to work like Babel.
► Rework interop handling rollup#3710 ✅
In this plugin:
Implement Improvement 3, Improvement 4a, Improvement 4b and Improvement 6: Always return the namespace when requiring an ES module except when a particular option is set. What is returned is configured in the proxy module. At the moment, we only return the namespace if the module does not use a default export. There are many ways how such an option might be done, here is one suggestion:
name: requireReturnsDefault
supported values:
false (the default): All ES modules return their namespace when required. External imports are rendered as import * as external from 'external' without any interop.
true: All ES modules that have a default export should return their default when required. Only if there is no default export, the namespace is used. This would be like the current behaviour. For external imports, the following interop pattern is used:
It might make sense to extract this into a helper function that is reused where possible.
"auto": All modules return their namespace if required unless they have only a default export, in which case that one is returned. This is the inverse of Rollup's auto mode. For external imports, the following interop pattern is used:
Again it might make sense to extract this into a helper function.
an array of module ids. For convenience, these ids should be put through this.resolve so that the user can just use the name of a node_module. In that case if the module is not an ES module or does not have a default export, the plugin should throw, explaining the error. External imports are just rendered as import external from 'external' without any interop.
a function that is called for each ES module that does have a default export and is required from CJS. The function would return true or false|undefined. It should be ensured that this function is only called once per resolved module id. This is equivalent to specifying an array of all module ids for which we returned true.
Implement Improvement 2: If a property definition for __esModule is encountered, remove it, do not treat it as a cause for using the createCommonjsModule wrapper and do not add the interop default export to the module. Ideally, in this scenario, assignments to exports.default should be treated like assignments to exports.foo in that it generates an explicit export. So this:
// Note that we need to escape the variable name as `default` is not allowedvar_default='default';varfoo='foo';vardep={default: _default,foo: foo};export{depas__moduleExports};// This is a different way to generate a default export that even allows for live-bindingsexport{foo,_defaultasdefault};
Implement Improvement 8: Allow specifying exposed exports. My suggestion is:
name: exposedExports
value: An object where keys correspond to module ids. For convenience, the keys should be put through this.resolve by the plugin. The value is an array of named exports. Internally, this could just mean we resolve the module not to itself but rather to a reexport file, e.g.
// for exposedExports: {'my-module': ['foo', 'default']}export{foo,default}from' \0my-module?commonjsEntry'
The new ' \0my-module?commonjsEntry' would correspond to that we usually render here. Commonjs proxies should also import from that file.
Expected Behavior / Situation
This plugin (and Rollup in extension) work as seamlessly as possible with output generated by itself and tools in the ecosystem.
Actual Behavior / Situation
There are a whole lot of interop issues, see details below. This is meant as an overview over how this plugin actually works and a more or less complete picture of all interop situations and the current behaviour. The idea is to use this as a base for discussion and to identify sub-issues that can be solved with reasonable effort. I will gladly try to take care of all changes to Rollup core but am very happy if I get support improving the plugin.
This list is VERY long, and I will try to update it based on suggestions. I will also try to add a compiled summary at the end soon.
As I strongly believe in testing over believing, I added actual output code samples for almost everything. To generate the pre-formatted code samples, I used the following
Rollup config
Importing CJS from CJS
The plugin needs to take care that this works seamlessly, resolving require statements with
module.exportsof the required module. This is not really an interop problem, the goal of this section is rather to highlight how the plugin actually works and handles certain scenarios and where it could be improved before moving on to the actual interop situations.Assignment to
module.exportsIn the importing file, we generate two imports: An empty imports of the original module while the actual binding is imported from a proxy module. The reason for the empty import of the original file is to trigger loading and transforming the original file so that we know if it is CJS or ESM when building the proxy file.
The actual module renders two exports: What is assigned to
module.exportsis exported as bothdefaultand__moduleExports. The proxy again exports__moduleExportsasdefault(for situations where the proxy does slightly different things, look into the section where ESM is imported from CJS).Input
Transformed
Output
Assignments to properties of
exportsormodule.exportsIn this scenario, Rollup creates an artificial
module.exportsobject that is created with all properties inline. This is very efficient as opposed to assigning the properties to an object one by one as the runtime engine can immediately optimize such an object for quick access. This object is again then exported as bothdefaultand__moduleExports. Additionally, all assigned properties are also exported as named exports.Input
Transformed
Output
🐞 Bug 1: Assigning to the same property twice will generate two exports of the same name, causing Rollup to throw a "Duplicate export " error.
Handling unsupported use of
module.exportsorexportsThere are a lot of cases where the plugin deoptimizes, e.g. when a property is read instead of assigned. In such situations,
createCommonjsModulehelper is used to create a wrapper to execute the module more or less like Node would execute it without detecting any named exports.Input
Transformed
Output
Inline
requirecallsAt the moment, the plugin is NOT capable of maintaining exact execution order. Rather, even nested and conditionally executed require statements (unless they are written via an if-statement in a particular way) are always hoisted to the top. This is a separate situation that could be improved by drastic changes, i.e. wrapping modules in function enclosures and calling them when they are first used. This will however have a negative effect on the efficiency of the generated code as compared to the status quo so this should only happen when it is really necessary. Unfortunately, it is not possible to tell if a module is required in a non-standard way until the whole module graph is built so in the worst case, all modules might need to be wrapped. Rollup could help here a little by implementing some inlining algorithm, however this is very much future talk (say, up to a year in the future) and will likely only apply anyway if a module is used exactly in one place. Other approaches could be for the plugin to analyze the actual execution order to see if it can ensure that the first usage does not need wrapping so that it does not matter if there are dynamic requires later on but this feels complicated and error-prone.
Anyway, this is mostly listed for completeness here as it does not really touch on the subject of interop but warrants its own issue. Here is a sample to illustrate:
Input
Transformed
Output
Now let us get to the actual interop patterns:
Importing CJS from ESM
NodeJS style
In Node, CJS modules only expose a default export that corresponds to
module.exports. This important pattern should always work.For the plugin, the main difference here is that instead of the proxy, the actual module is imported directly. Here just one example to illustrate, in general everything works similar to the CJS-to-CJS case.
Input
Transformed
Output
NodeJS style with named imports
This is supported by Webpack but (previously in part but now fully) also by this plugin. In addition to the default import resolving to
module.exports, named imports will resolve to properties onmodule.exports. Previously, this would only work for named exports that the plugin could auto-detect (and only if the module was not deoptimized to usecreateCommonjsModule) or which were listed by the user. The use of Rollup'ssyntheticNamedExportsproperty in its current form now enables arbitrary named imports to be resolved without additional configuration while even maintaining live-bindings.A Note about Webpack .mjs semantics and better NodeJS interop
Note that Webpack currently either disallows or warns about this pattern when used from modules with the
.mjsextension.🚧 TODO: Would be nice to confirm this
The intention here is that this extension signals we want to enter some sort of strict NodeJS interop mode. We could do something similar, but then I would love to have the ESM/CJS detection in
@rollup/plugin-node-resolveand establish a communication channel to get this information from that plugin. Then we might add a switch to@rollup/plugin-commonjsto use "strict NodeJS interop" thatThis could become an advanced feature we add after solving the more pressing issues we have at the moment.
Example with statically detected named exports
Input
Transformed
Output
Example that relies on synthetic named exports
Here we just assign an object to
module.exports. Note how we retain live-bindings by using property accesses: If the object atmodule.exportswould be mutated later on, accessing our named variable would always provide the current value.Input
Transformed
Output
ESM that was transpiled to CJS
This is the tricky one. The problem here is to maintain isomorphic behaviour between the original ES module and the CJS module. Named exports are handled mostly correctly when using the "NodeJS with named imports" pattern (except we do not throw for missing exports) however the default export should not be
module.exportsbutmodule.exports.default. This is incompatible with the previously listed interop patterns.At the moment most tools implement a runtime detection pattern for this by adding an
__esModuleproperty tomodule.exportsto signify this is a transpiled ES module. Then the algorithm when getting the default import ismodule.exports.defaultas the default exportmodule.exportsExample importing a named export when a default export is present
🐞 Bug 2: This is not working correctly as instead of the named export, it tries to return a property on the default export. The reason is that in this situation, the interop pattern in
unwrapExportskind of correctly extracts the default export and exports it as default, butsyntheticNamedExportsshould not use that to extract named exports.Input
Transformed
Output
This is quite difficult to fix, especially if we want to maintain live bindings for named exports. My first thought was to extend
syntheticNamedExportsto allow an additional value to indicate that the default export is also picked asdefaultproperty from the actual default export. This would mean however that auto-detecting interop becomes slow and difficult and destroys likely all live-bindings for the non ESM case because we need to build a new object, i.e.📈 Improvement 1: A better idea I had is to allow specifying an arbitrary string as value of
syntheticNamedExports, e.g.syntheticNamedExports: "__moduleExports". The meaning would be that missing named (and even the default) exports are no taken from the default export but from a named export of the given name. Then the interop would just beThis is rather efficient, though it could still be put into an interop function
getDefaultif we want to save a few bytes. Of course we still do not get live-bindings for thedefaultexport in the transpiled ESM case, but even this is fixable if in a second step, we implement static detection for__esModule:📈 Improvement 2: If we come across the
Object.defineProperty(exports, "__esModule", { value: true })line (or!0instead oftruefor the minified case) on the top level of a module, then we can just mark this module as being transpiled and can even get rid of this line in the transformer, making the code more efficient and removing the need for any interop, i.e. above we do not add theexport defaultat all in that case. There is also no longer a need to wrap the code increateCommonjsModuleif this property definition is ignored.Example importing a non-existing default export
🐞 Bug 3: It is not really surprising that this case is not working correctly as it should actually throw either at build or at runtime. Otherwise at the very least the default export should be
undefinedwhile here it is actually the namespace.Input
Transformed
Output
To fix this, I would like to reduce the interop pattern to only look for the presence of
__esModuleand nothing else. This would be covered by the suggested Improvement 2.Importing ESM from CJS
To my knowledge, there is no engine that supports this at the moment, so the question is what the "correct" return value should be. For NodeJS, I heard that there is actually some interest in supporting this eventually but the road blocks are technical here (mostly that the ESM loader is async + TLA handling and similar things). However if this is ever supported, a
requireof an ES module will likely return the namespace. Otherwise Webpack supports this of course, and to my knowledge here you always get the namespace as well. Looking at TypeScript it will always add the__esModuleproperty on transpiled ESM modules and use thedefaultas a property which in turn means that if yourequirean ES module and use CJS as output target, you always get the namespace. The same goes for Babel.For Rollup, it is more complicated, and the reason is in part rooted in that fact that Rollup was first and foremost designed for libraries, and here you want to be able to create CJS output where everything, e.g. a function, is directly assigned to
module.exportsto be able to create a nice interface for "one-trick-pony" libraries. So Rollup core by default uses its "auto" mode when generating CJS output, which meansmodule.exportscontains the namespace unlessmodule.exportsNow of course this is about converting ESM to CJS but one idea is that in such a situation, the CJS and ESM versions should be interchangeable when I
requirethem. So for internal imports, it could make sense to work like Rollup's auto mode in reverse, giving you the default export when there are no named exports and the namespace otherwise. But I understand that in the long term, we should likely align with the rest of the tooling world even if it generates less efficient code, so my suggestion on this front is:requireby defaultexternaloption in Rollup works) to work like auto mode as outlined above to make existing mixed ESM CJS code-bases work. The reason I think we need this is that the requiring module can easily be a third party dependency itself and thus not under your direct control.Requiring ESM with only named exports
This works as intended.
Input
Transformed
Output
Requiring ESM with only a default export
This one is working correctly for auto mode but with the arguments above, it should likely be changed, see Improvement 3 and Improvement 4.
Input
Transformed
Output
Requiring ESM with mixed exports
🐞 Bug 4: This one is broken—it should definitely return the namespace here but instead it returns the default export.
Input
Transformed
Output
External imports
To view the full interop story here, one has to look both at what this plugin generates and what Rollup generates as CJS output.
External imports in the CommonJS plugin
🐞 Bug 5: For external imports, this plugin will always require the default import.
📈 Improvement 6: With the arguments given above, this should actually be the namespace by default (
import * as external from 'external') as this is technically equivalent to requiring an ES module.📈 Improvement 4b: Again we should add an option to specify when an external require should just return the default export instead. It could even be the same option. So here is some room for discussion.
Input
Transformed
Output
External imports in Rollup's CJS output
Importing a namespace
Ideally, this should be converted to a simple
requirestatement asrequirewould again become a simplerequire.And this is indeed the case:
Input
Output
Importing named bindings
These should just be converted to properties on whatever
requirereturns as that should be equivalent to a namespace. And that is again the case:Input
Output
Importing the default export
🐞 Bug 6: Several things are broken here:
defaultproperty instead of checking the__esModuleproperty. I always thought there was a reason buried in some old issue but going back in history it seems it has always been that way. This should change as it has all sorts of adverse effects: If the file was not an ES module but assigns something toexports.default, it will be mistaken for a default export; if it was an ES module but does not have a default export, this will wrongly return the namespace instead.Input
Output
📈 Improvement 7: To fix this, I think I would try to go for how Babel does it.
Entry points
Entry point which assigns to
module.exportsThis will be turned into a default export which is probably the best we can hope for.
Input
Transformed
Output
Entry point which adds properties to
exportsThis appears to be working sensibly.
Input
Transformed
Output
Entry point which assigns to
module.exportsbut requires itself🐞 Bug 7: Basically it looks for a
__moduleExportsexport that does not exist instead of giving the namespace. My suggestion above for how to reworksyntheticNamedExportsin Improvement 1 should also fix this from within Rollup. Similar problems arise when another module imports our entry point or when the module just adds properties toexports.Input
Transformed
Output
Entry point which assigns to
exports.default🐞 Bug 8: Here no output is generated while it should likely default export the namespace unless the
__esModuleproperty is present.Input
Transformed
Output
// "bundle.js"Entry point that is a transpiled ES module
Ideally, the output should have the same exports as the original entry. At the moment, this will not be the case as the
Object.definePropertycall will always cause thecreateCommonjsModulewrapper to be used and no named exports will be detected. There are several ways to improve this:__esModuleproperty definition and do not treat it as a reason for deoptimization, see Improvement 2namedExportsthat specifically lists exposed exports for entries. We could also use this to activate or deactive the default export and decide if the default export should be whatever is assigned toexports.defaultor rathermodule.exports. This could be handled by simply creating a wrapper file that reexports these exports. If we do it that way, Rollup tree-shaking might even remove some unused export code.Input
Transformed
Output
Summary and possible action plan
In Rollup:
I will gladly take care of the necessary improvements here:
automode without specifying it explicitly and there is a package that has only a default export. Explain how this package will not be interchangeable with its ESM version in many tools and suggest to use named exports mode with all its consequences or better, do not use default exports.► Warn when implicitly using default export mode rollup#3659 ✅
syntheticNamedExportsshould support receiving a string value that corresponds to an export name from which to pick missing exports. The value oftruewould correspond to"default"except that for entry points when using a string, the listed property name would not be part of the public interface. This will fix issues where suddenly__moduleExportsis introduced into an interface.► Add basic support for using a non-default export for syntheticNamedExports rollup#3657 ✅
► Rework interop handling rollup#3710 ✅
In this plugin:
Implement Improvement 3, Improvement 4a, Improvement 4b and Improvement 6: Always return the namespace when requiring an ES module except when a particular option is set. What is returned is configured in the proxy module. At the moment, we only return the namespace if the module does not use a default export. There are many ways how such an option might be done, here is one suggestion:
name:
requireReturnsDefaultsupported values:
false(the default): All ES modules return their namespace when required. External imports are rendered asimport * as external from 'external'without any interop.true: All ES modules that have a default export should return their default when required. Only if there is no default export, the namespace is used. This would be like the current behaviour. For external imports, the following interop pattern is used:It might make sense to extract this into a helper function that is reused where possible.
"auto": All modules return their namespace if required unless they have only a default export, in which case that one is returned. This is the inverse of Rollup's auto mode. For external imports, the following interop pattern is used:Again it might make sense to extract this into a helper function.
this.resolveso that the user can just use the name of a node_module. In that case if the module is not an ES module or does not have a default export, the plugin should throw, explaining the error. External imports are just rendered asimport external from 'external'without any interop.trueorfalse|undefined. It should be ensured that this function is only called once per resolved module id. This is equivalent to specifying an array of all module ids for which we returnedtrue.► Return the namespace by default when requiring ESM #507 ✅
Once it is done on Rollup core side, implement the plugin part of Improvement 1: Use the new value for
syntheticNamedExportsand add the default export corresponding to the suggested simplified interop pattern.► fix(commonjs): fix interop when importing CJS that is a transpiled ES module from an actual ES module #501 ✅
Implement Improvement 2: If a property definition for
__esModuleis encountered, remove it, do not treat it as a cause for using thecreateCommonjsModulewrapper and do not add the interop default export to the module. Ideally, in this scenario, assignments toexports.defaultshould be treated like assignments toexports.fooin that it generates an explicit export. So this:should be converted to
► feat(commonjs): reconstruct real es module from __esModule marker #537 ✅
Implement Improvement 8: Allow specifying exposed exports. My suggestion is:
name:
exposedExportsvalue: An object where keys correspond to module ids. For convenience, the keys should be put through
this.resolveby the plugin. The value is an array of named exports. Internally, this could just mean we resolve the module not to itself but rather to a reexport file, e.g.The new
' \0my-module?commonjsEntry'would correspond to that we usually render here. Commonjs proxies should also import from that file.