esbuild icon indicating copy to clipboard operation
esbuild copied to clipboard

bundles .mjs code when .cjs (commonjs) is expected

Open snebjorn opened this issue 2 years ago • 3 comments

I'm new to esbuild, so I'm not sure if this is intended, but I'm at least confused 😄

I have the following

// package.json
{
  "scripts": {
    "build": "esbuild main.cts --bundle --platform=node --format=cjs --outfile=dist/out.js"
  },
  "dependencies": {
    "esbuild": "^0.19.2",
    "locter": "^1.2.2"
  }
}
// main.cts
import { isFilePath } from "locter";

isFilePath("foo")

I'm expecting dist/out.js to be commonjs and bundled commonjs code from locter. Which have package.json exports defined as follows:

// loctor's package.json exports
"exports": {
    "./package.json": "./package.json",
    ".": {
        "require": "./dist/index.cjs",
        "import": "./dist/index.mjs"
    }
},

However dist/out.js have this

// node_modules/locter/dist/index.mjs
var import_jiti = __toESM(require_lib(), 1);
var import_url3 = __toESM(require("url"), 1);
var import_path2 = __toESM(require("path"), 1);
var import_module = __toESM(require("module"), 1);
var import_meta = {};
var __filename = import_url3.default.fileURLToPath(import_meta.url);
var __dirname = import_path2.default.dirname(__filename);
var require2 = import_module.default.createRequire(import_meta.url);
function isFilePath(input) {
  const extension = import_node_path.default.extname(input);
  return extension && extension !== "";
}

I was expecting // node_modules/locter/dist/index.cjs and the code from that file. The .mjs code contains import.meta.url which breaks my code runtime. But that's besides the point.

Am I doing something wrong here?

snebjorn avatar Sep 05 '23 14:09 snebjorn

Using an import statement matches the import condition, not the require condition. If you want to match the require condition instead then you should use require instead of import:

const { isFilePath } = require("locter");

evanw avatar Sep 14 '23 00:09 evanw

I thought that this typescript

import { isFilePath } from "locter";

was turned into

const { isFilePath } = require("locter");

if the compile target is commonjs

From TS docs https://www.typescriptlang.org/tsconfig/#module

// @filename: index.ts
import { valueOfPi } from "./constants";
 
export const twoPi = valueOfPi * 2;

CommonJS

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.twoPi = void 0;
const constants_1 = require("./constants");
exports.twoPi = constants_1.valueOfPi * 2;

ESNext

import { valueOfPi } from "./constants";
export const twoPi = valueOfPi * 2;

snebjorn avatar Sep 14 '23 10:09 snebjorn

I agree, you shouldn't need to use require() in typescript files. The file extension of .cts should be enough to indicate that the target is commonjs, and therefore the require condition should be used.

IanVS avatar Oct 04 '23 15:10 IanVS

@evanw would love your input on this. Is some kind of preprocessing supposed to be done on TypeScript files before it's ready for esbuild?

Reading from https://esbuild.github.io/content-types/#es-module-interop

import * as foo from 'foo' is compiled to const foo = require('foo')

Given that esModuleInterop: false

However shouldn't esModuleInterop: true and "module": "commonjs" (tsconfig.json) also do

import * as foo from 'foo' is compiled to const foo = require('foo')

I did some more testing and I can't get esbuild to convert import to require no matter what I set in tsconfig. I must be missing something here.

snebjorn avatar Feb 12 '24 12:02 snebjorn

From my personal perspective, I do not want esbuild to convert ts import to js require by tsconfig.

  • It is only a historical behavior. Now the whole js community is moving forward to ESM. It is very strange to suddenly step back and give users CJS when they are writing code using import.

  • Complexity. The bundler now has to consider these configs / filenames, which is hard to figure out by a normal developer:

    • package.json > type: module
    • filename ends with .mjs .cjs (mts cts)
    • file's relative tsconfig.json
      • module: commonjs
      • target: es6 (which indicates module: es6)
      • but esbuild actually cannot handle target: es5 when the code uses es6 syntax, which is the default config, should it throw many errors to stop you from bundling?
      • files / includes
    • esModuleInterop indicates different wrappers to handle external modules, however it should always be configured to true for modern projects
  • By using require you're preventing esbuild's tree shaking from taking effect.

hyrious avatar Feb 12 '24 15:02 hyrious

I'm closing this as it doesn't seem to be going anywhere.

I must be misunderstanding something. I mean a lot of projects use esbuild and the internet isn't on fire 😃

snebjorn avatar Mar 04 '24 08:03 snebjorn