Skip to content

module: add __esModule to require()'d ESM#52166

Closed
joyeecheung wants to merge 4 commits into
nodejs:mainfrom
joyeecheung:namespace
Closed

module: add __esModule to require()'d ESM#52166
joyeecheung wants to merge 4 commits into
nodejs:mainfrom
joyeecheung:namespace

Conversation

@joyeecheung
Copy link
Copy Markdown
Member

@joyeecheung joyeecheung commented Mar 20, 2024

Before this PR, trying to load real ESM from transpiled ESM would throw errors like this with --experimental-require-module

// 'logger' package being loaded as real ESM
export default class Logger { log(val) { console.log(val); } }
export function log(logger, val) { logger.log(val) };
// Consuming code originally authored in ESM, but transpiled to CommonJS before being loaded by Node.js
import Logger, { log } from 'logger';
log(new Logger(), 'import both');
/Users/joyee/projects/node/test/fixtures/es-modules/transpiled-cjs-require-module/dist/import-both.cjs:27
(0, logger_1.log)(new logger_1.default(), 'import both');
                  ^

TypeError: logger_1.default is not a constructor
    at Object.<anonymous> (/Users/joyee/projects/node/test/fixtures/es-modules/transpiled-cjs-require-module/dist/import-both.cjs:27:19)
    at Module._compile (node:internal/modules/cjs/loader:1460:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1544:10)
    at Module.load (node:internal/modules/cjs/loader:1275:32)
    at Module._load (node:internal/modules/cjs/loader:1091:12)
    at wrapModuleLoad (node:internal/modules/cjs/loader:212:19)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:158:5)
    at node:internal/main/run_main_module:30:49

After this PR it logs 'import both'.

Tooling in the ecosystem have been using the __esModule property to
recognize transpiled ESM in consuming code. For example, a 'log'
package written in ESM:

export function log(val) { console.log(val); }

Can be transpiled as:

exports.__esModule = true;
exports.default = function log(val) { console.log(val); }

The consuming code may be written like this in ESM:

import log from 'log'

Which gets transpiled to:

const _mod = require('log');
const log = _mod.__esModule ? _mod.default : _mod;

So to allow transpiled consuming code to recognize require()'d real ESM
as ESM and pick up the default exports, we add a __esModule property by
building a source text module facade for any module that has a default
export and add .__esModule = true to the exports. We don't do this to
modules that don't have default exports to avoid the unnecessary
overhead. This maintains the enumerability of the re-exported names
and the live binding of the exports.

The source of the facade is defined as a constant per-isolate property
required_module_facade_source_string, which looks like this

export * from 'original';
export { default } from 'original';
export const __esModule = true;

And the 'original' module request is always resolved by
createRequiredModuleFacade() to wrap which is a ModuleWrap wrapping
over the original module.

This PR originally used the same trick that Bun did (h/t @Jarred-Sumner) by putting the original module namespace in the prototype chain. Upon closer examination this now switched to use a SourceTextModule facade only when the exports contain default to 1) reduce the performance impact 2) ensure that the exported names are still enumerable and can be copied by tools.

Refs: #51977 (comment)
Refs: #52134

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author ready PRs that have at least one approval, no pending requests for changes, and a CI started. commit-queue-rebase Add this label to allow the Commit Queue to land a PR in several commits. esm Issues and PRs related to the ECMAScript Modules implementation. experimental Issues and PRs related to experimental features. needs-ci PRs that need a full CI run. notable-change PRs with changes that should be highlighted in changelogs. semver-minor PRs that contain new features and should be released in the next minor version.

Projects

None yet

Development

Successfully merging this pull request may close these issues.