Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CJS named export "default" is not recognized as the default export entry #48899

Closed
legendecas opened this issue Jul 24, 2023 · 9 comments
Closed
Labels
module Issues and PRs related to the module subsystem.

Comments

@legendecas
Copy link
Member

legendecas commented Jul 24, 2023

Version

v20.5.0

Platform

Darwin my.local 21.6.0 Darwin Kernel Version 21.6.0: Sat Jun 18 17:07:22 PDT 2022; root:xnu-8020.140.41~1/RELEASE_ARM64_T6000 arm64

Subsystem

module

What steps will reproduce the bug?

With two scripts, namely "my-mod.mjs" and "my-cjs.cjs", and their contents as:

// my-mod.mjs

import foo from './my-cjs.cjs'
console.log(foo);
// my-cjs.cjs
exports.default = "foo";

Run with node my-mod.mjs.

How often does it reproduce? Is there a required condition?

Always

What is the expected behavior? Why is that the expected behavior?

Output foo.

What do you see instead?

Output { default: 'foo' }

Additional information

The original problem here is that I found a typescript module written as:

const foo = 'foo';
export {
    foo as default
};

is compiled as the following when targeting CJS: (playground)

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = void 0;
const foo = 'foo';
exports.default = foo;

If the module is imported from an ESM module in Node.js, the default export entry would not be the expected one written originally in typescript.

The spec https://tc39.es/ecma262/#sec-static-semantics-importentriesformodule states that ImportedDefaultBinding imports the "default" entry from the requested module.

When using with vm.SyntheticModule, the following script outputs the expected result:

import vm from 'vm';

const context = vm.createContext();
context.print = console.log;

const mod = new vm.SourceTextModule(`
import foo from 'foo';
console.log(foo);
`);
await mod.link(async () => {
  const mod = new vm.SyntheticModule(['default'], () => {
    mod.setExport('default', 'foo');
  });

  return mod;
});
await mod.evaluate(); // outputs "foo"
@legendecas legendecas added the module Issues and PRs related to the module subsystem. label Jul 24, 2023
@bnoordhuis
Copy link
Member

Why is that the expected behavior?

☝️ - because it sure seems like unexpected - even magical - behavior to me.

If nothing else, your expected behavior is a backwards incompatible change. The interpretation of the CJS export object becomes context-dependent on whether the importer is a CJS or ESM module; a bit like the wantarray keyword in perl.

Confusing at best, but more likely downright wrong. Consider what happens when you cp my-mod.mjs my-mod.cjs.

@legendecas
Copy link
Member Author

legendecas commented Jul 24, 2023

The original problem here is that I found a typescript module written as:

const foo = 'foo';
export {
    foo as default
};

is compiled as the following when targeting CJS: (playground)

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = void 0;
const foo = 'foo';
exports.default = foo;

If the module is imported from an ESM module in Node.js, the default export entry would not be the expected one written originally in typescript.

@bnoordhuis
Copy link
Member

I sympathize, but TS bugs shouldn't result in Node hacks.

@targos
Copy link
Member

targos commented Jul 24, 2023

I think this is set in stone anyway. This behavior has been released for too long to be changed. It's certain that people now rely on it.

@legendecas
Copy link
Member Author

legendecas commented Jul 24, 2023

I sympathize, but TS bugs shouldn't result in Node hacks.

I don't understand why this is a TS bug. Its source and compiled products are valid as ESM and CJS sources, respectively. There isn't a mechanism that allows tools like TS to transform the compiled CJS "default" export entry as an ESM default entry.

I think this is set in stone anyway. This behavior has been released for too long to be changed. It's certain that people now rely on it.

I understand the concern here. As TS is one of the tools that is largely used in the wild, I'd also like to know if this is a problem worth addressing for CJS/ESM interoperability in Node.js.

@bnoordhuis
Copy link
Member

Not if it breaks backward compatibility (which it would.)

@targos
Copy link
Member

targos commented Jul 24, 2023

The behavior is the same for all CJS modules imported from ESM:

  • default export always contains the module.exports object
  • other named exports may contain some of properties of module.exports, if they could be statically detected and do not contain characters that are invalid in JS identifiers

I understand your problem here, but changing the behavior when there is an export named "default" would be breaking and inconsistent.

@aduh95
Copy link
Contributor

aduh95 commented Jul 24, 2023

  • default export always contains the module.exports object

☝️ – that's the key behavior that never changed (and cannot ever change at this point) when consuming CJS from ESM. This is a known limitation from the get go, you can check out the discussions over at #35249 and the related issues for more context.

Closing as Won't fix.

I sympathize, but TS bugs shouldn't result in Node hacks.

I don't understand why this is a TS bug.

You're right, it's not, the same thing would happen without TS; there's simply no way to have a module.exports.default to be interpreted as the default object (unless it's a self reference of course).

@aduh95 aduh95 closed this as not planned Won't fix, can't repro, duplicate, stale Jul 24, 2023
@legendecas
Copy link
Member Author

Continuing discussion at #50981.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
module Issues and PRs related to the module subsystem.
Projects
None yet
Development

No branches or pull requests

4 participants