import cjs strange behaviour (interop with babel)
I have tried to update my library that depends on other cjs libraries.
But I encountered a problem, that import * as foo from 'foo' treats it as
{default: {bar, baz}}
when foo/index.js contains
exports = {bar, baz}
As I know such module representation does not match current implementations, e.g. Babel, which doesn't wrap module content into {default: ...}.
So if I write
import * as foo from 'foo'
foo.bar()
Node will complain "bar is not a function", because foo.default.bar should be used
And if I write
import foo from 'foo'
foo.bar()
Babel will complain "Cannot read property 'bar' of undefined", because default export is undefined, so foo is undefined too
I guess TS it works like Babel here. I don't understand why module-as-default representation of a module was implemented, I think it is very weird. So how I should write my library so it can be used with native Node and can be transpiled too?
Yeah, this is really confusing.
As I know such module representation does not match current implementations, e.g. Babel, which doesn't wrap module content into
{default: ...}.
The issue at the core of this is: The module syntax in babel is just sugar for CommonJS. Which means that the code often doesn't work like a "real" JavaScript module. Node, just like browsers, implements JavaScript modules as they appear in the JavaScript spec. And loading CommonJS (the code babel generates) from ESM (JavaScript modules) is currently only really possible as the default export.
If the file you are loading is CommonJS, it only has a default export (the value of module.exports). CommonJS doesn't have static named exports. Babel's interop behavior should reflect that, assuming that for interop files (exports. _interopRequireDefault), you never load the CommonJS version from ESM. Instead you should be loading the original ESM source.
See: https://babeljs.io/docs/en/babel-plugin-transform-modules-commonjs#nointerop (should not be set)
Example:
// foo.mjs
export function bar() {}
// foo.cjs, compiled by babel from foo.mjs
exports.bar = function bar() {}
Object.defineProperty(exports, "__esModule", { value: true });
// consumer.mjs
import { bar } from './foo.mjs';
bar();
// consumer.cjs, compiled by babel from consumer.mjs
var _foo = _interopRequireDefault(require("./foo.cjs"));
_foo.bar();
Note: The example above assumes that there's a babel plugin to rewrite mjs imports to the appropriate cjs path.
The solution to this is in the package that's transpiled: their entry points should not be transpiled, they should module.exports = require('./lib').default or similar. (an alternative is to use the add-module-exports babel transform, which does module.exports = when there's only a default export).
This is a common problem in the TS and Babel ecosystems where the packages are published in such a way as to expose this interop implementation detail.
@jkrems Thank you for detailed response!
But I still don't get it.
add-module-exports should handle .default during import/require, when module actually has default export.
But here cjs libraries don't export default property, but node.js forces it, I don't understand why it assigns module content to the default field and can't just put everything in the root of the imported object (how it's done with require: no default is exported and no default is imported)
So, for now, Babel and Node.js has no interop, thus library authors can't start to update their libraries (they can update only if they support node 13+)
Interop is needed because Node >= 13 users should be able to use same code with native Node
and older Node users should run the same code with transpiler.
As I understand, I should wait for some major release of Babel, in which they align their import foo from 'foo.cjs' behavior with Node?
But here cjs libraries don't export default property, but node.js forces it, I don't understand why it assigns module content to the default field and can't just put everything in the root of the imported object.
In modules, there is no assignable module content object. The closest thing is the namespace object (import * as namespaceObject from 'x') but it can't be set to arbitrary values. The properties of the namespace object are the individual exports of the imported module. Every value exported by a module has to be given a name. default is somewhat special among those names but in many ways it's just yet another name. So - if a CJS file wants to export a value (the value of module.exports), it needs to be put into some export name.
When those names are determined, no code has executed yet - so it's impossible to tell what properties the CJS file will set on exports. So from an ES module perspective, CJS only has a single export: The value of module.exports. We have to put that value into some binding key and default was the most obvious one. We could also have used import { cjsExports } from './foo.cjs'.
As I understand, I should wait for some major release of Babel, in which they align their
import foo from 'foo.cjs'behavior with Node?
I'm not sure I follow - the example above should work..? Can you clarify which aspect of it doesn't work for you? If you can set up a repo or gist with a repro, I'm happy to take a look as well. :)
When porting libraries from babel/CommonJS to modules, you'll likely have to start from deeper dependencies and work your way outwards. Or, as @ljharb hinted at, you can update the libraries to make sure they only expose the default export to outside users instead of leaking the babel module interop wrappers.
@shrpne, if none of the solutions above work, you may have success with the following.
import { default as namespace } from 'cjs-package-specifier';
The above being the desugared form of:
import cjsExportsObject from 'cjs-package-specifier';
Small nit: Please don't call the exports object a "namespace" in the context of ESM. There's already a namespace in ESM and it's something else. :)
import { default as exportsValue } from 'cjs-package-specifier';
import exportsValue from 'cjs-package-specifier';
import * as namespace from 'cjs-package-specifier';
const exportsValue = namespace.default;
I'm wondering if there's anything we should add to the docs regarding this. I don't want to give advice specific to Babel or using Babel-transpiled packages, though; is there something about .default that's universal and that we can advise users about?
I think we approach the limits of what belongs in the node reference docs vs. what could be a dedicated guide. I think both Typescript and Babel are important enough in the wider ecosystem to warrant dedicated how-tos.
Not universal - it applies to Babel’s interop, and typescript’s (when their module system isn’t broken, by enabling esModuleInterop and synthetic imports, which tsc init enables by default).
I guess these are the same issues: https://github.com/babel/babel/issues/7294 https://github.com/babel/babel/issues/7998
And they are somehow should be resolved by Babel to make it interoperable with Node's approach
We do have a place on the repo for "guides".
Maybe a "all you need to know as a module author" guide would be appropriate
On Fri, Jan 31, 2020, 12:57 PM shrpne [email protected] wrote:
I guess these are the same issues: babel/babel#7294 https://github.com/babel/babel/issues/7294 babel/babel#7998 https://github.com/babel/babel/issues/7998
And they are somehow should be resolved by Babel to make it interoperable with Node's approach
— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/nodejs/modules/issues/480?email_source=notifications&email_token=AADZYV4A5PMNVIZMBONJ5C3RARRBTA5CNFSM4KMXK3YKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEKPO4SQ#issuecomment-580841034, or unsubscribe https://github.com/notifications/unsubscribe-auth/AADZYV273KZ7HRVQZAWNSALRARRBTANCNFSM4KMXK3YA .
Please don't call the exports object a "namespace" in the context of ESM.
A couple links about ES module namespace objects from the ES2020 spec: