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

feat: add CJS named export annotations for Transform API #2248

Closed
privatenumber opened this issue May 13, 2022 · 3 comments
Closed

feat: add CJS named export annotations for Transform API #2248

privatenumber opened this issue May 13, 2022 · 3 comments

Comments

@privatenumber
Copy link
Contributor

privatenumber commented May 13, 2022

Request

Node.js uses cjs-module-lexer to parse ComonJS named exports to interop with ESM imports.

Since esbuild compiles ESM exports to CJS using getters on module.exports, it is currently not parsable.

I would like to request a minor syntax hint to help the Node.js detect named exports in the Transform API.

Current behavior

ESM Input

// ts.ts

export const a = 1;

CJS Output

// ts.cjs

var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var ts_exports = {};
__export(ts_exports, {
  a: () => a
});
module.exports = __toCommonJS(ts_exports);
const a = 1;

Importing the output

The named export a is not detected by Node.js, and is grouped together behind the default export.

// index.mjs

const imported = await import('./ts.cjs');
console.log(imported);
// => [Module: null prototype] { default: { a: [Getter] } }

Proposal

Add a purely syntactical hint to help the lexer detect the named exports.

In this example, I add exports.<export-name> = void 0 at the end of the transformed code. This doesn't do anything because module.exports is overwritten to another object prior.

// ts.cjs

var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var ts_exports = {};
__export(ts_exports, {
  a: () => a
});
module.exports = __toCommonJS(ts_exports);
const a = 1;
exports.a = void 0; // Hint for lexer

Importing the output

The named export is successfully detected by Node.js:

// index.mjs

const imported = await import('./ts.cjs');
console.log(imported);
// => [Module: null prototype] { a: 1, default: { a: [Getter] } }

Context

We're developing a robust esbuild enhanced Node.js runtime called tsx using the transform API. And there is an edge-case for import()ing an CJS module (originally ESM, transpiled by esbuild on-demand) from a pure ESM file, but the named exports are consolidated in a default export.

Currently, I'm considering using some regex to detect the export getters and injecting the hints at the bottom of the transformed code. However, this could be slow or frail and is not an ideal long-term solution.

I believe adding these hints in esbuild would benefit all use-cases that compile ESM/TS to CJS, and also other tools that are targeted for Node.

Alternative ideas

I would also appreciate a list of exports from the transform API, similar to what es-module-lexer does. That way, the hints can be added on our end reliably.

Using es-module-lexer to detect exports is how we detect whether a module is ESM so it would also help remove double-parsing.

@hyrious
Copy link

hyrious commented May 13, 2022

There was already a hint made for nodejs when you enabled platform: "node" (only available in build mode): (see it online)

js_ast.Stmt{Data: &js_ast.SComment{Text: `// Annotate the CommonJS export names for ESM import in node:`}},

@privatenumber
Copy link
Contributor Author

privatenumber commented May 13, 2022

Thanks @hyrious. That's exactly what I was looking for! Glad it's already been implemented.

We're using the Transform API so I guess the feature request would be to make platform or export annotations available in transform mode.

@privatenumber privatenumber changed the title feat: add hints to make named exports in CJS detectable by Node feat: add CJS named export annotations for Transform API May 13, 2022
@privatenumber
Copy link
Contributor Author

privatenumber commented May 15, 2022

I investigated further and I think I may have misinterpreted the problem. Closing in favor of #2253


For reference, I was misinterpreting Node.js behavior when a CJS file uses import() to import another CJS file (transformed from ESM).

index.js (gets transformed to CJS)

import('./esm.js').then(m => console.log(m));

esm.js (gets transformed to CJS)

export const a = 1;

In this case, Node.js reads the file source directly from disk so cjs-module-lexer can't detect the transformed CJS exports because it would be checking pre-transformed ESM code. Therefore, adding named export annotations didn't actually make a difference.

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

No branches or pull requests

2 participants