Skip to content
This repository has been archived by the owner on Sep 2, 2023. It is now read-only.

import cjs strange behaviour (interop with babel) #480

Open
shrpne opened this issue Jan 28, 2020 · 14 comments
Open

import cjs strange behaviour (interop with babel) #480

shrpne opened this issue Jan 28, 2020 · 14 comments

Comments

@shrpne
Copy link

shrpne commented Jan 28, 2020

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?

@jkrems
Copy link
Contributor

jkrems commented Jan 28, 2020

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)

@jkrems
Copy link
Contributor

jkrems commented Jan 28, 2020

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.

@ljharb
Copy link
Member

ljharb commented Jan 28, 2020

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.

@shrpne
Copy link
Author

shrpne commented Jan 28, 2020

@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?

@jkrems
Copy link
Contributor

jkrems commented Jan 28, 2020

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.

@DerekNonGeneric
Copy link
Contributor

@shrpne, if none of the solutions above work, you may have success with the following.

import { default as namespace } from 'cjs-package-specifier';

@ExE-Boss
Copy link

ExE-Boss commented Jan 31, 2020

The above being the desugared form of:

import cjsExportsObject from 'cjs-package-specifier';

@jkrems
Copy link
Contributor

jkrems commented Jan 31, 2020

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;

@GeoffreyBooth
Copy link
Member

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?

@jkrems
Copy link
Contributor

jkrems commented Jan 31, 2020

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.

@ljharb
Copy link
Member

ljharb commented Jan 31, 2020

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).

@shrpne
Copy link
Author

shrpne commented Jan 31, 2020

I guess these are the same issues:
babel/babel#7294
babel/babel#7998

And they are somehow should be resolved by Babel to make it interoperable with Node's approach

@MylesBorins
Copy link
Member

MylesBorins commented Jan 31, 2020 via email

@DerekNonGeneric
Copy link
Contributor

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:

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants