evanw / esbuild

An extremely fast bundler for the web
https://esbuild.github.io/
MIT License
38.16k stars 1.15k forks source link

ESM import in ESM modules results into TypeError: (0 , import_....default) is not a function #2480

Open CSchulz opened 2 years ago

CSchulz commented 2 years ago

I have issues with ESM imports in ESM modules. I am using esbuild@0.13.23 but also esbuild@0.15.5 fails.

I think it could be related to #2384 and #2026.

lodash-es/isPlainObject is a .js file.

TypeError: (0 , import_isPlainObject.default) is not a function
    at isPlainObject (commons-testing\fesm2015\commons-testing.mjs:185:10)

Here are the specific file parts:

Original mjs file

  import isPlainObject from 'lodash-es/isPlainObject';

esbuild output

var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));

var import_isPlainObject = __toESM(require("lodash-es/isPlainObject"), 1);

if (!(0, import_isPlainObject.default)(methods)) {
  return typedJestCreateSpyObj(objType, methods);
}
hyrious commented 2 years ago

It looks like you're bundling your ESM code into CommonJS (or IIFE), but with "lodash-es" being externalized. (i.e. esbuild index.mjs --bundle --format=cjs --external:lodash-es) There are a few problems:

  1. In this mode, esbuild will convert all import to require calls to external modules.
  2. Importing an ESM-only module with require() is not allowed in any environment.
  3. Your source code ends with .mjs, which indicates esbuild to generate { default: module }, you can find more details in issues you've mentioned above.

The simplest workaround is do not externalize lodash-es, just remove it from your extenral settings.

CSchulz commented 2 years ago

I am not sure if I can match all your arguments with the call I have found in the jest preset:

 esbuild.transformSync(fileContent, {
    loader: 'js',
    format: 'cjs',
    target: 'es2016',
    sourcemap: compilerOpts.sourceMap,
    sourcefile: filePath,
    sourcesContent: true,
    sourceRoot: compilerOpts.sourceRoot,
});

https://github.com/thymikee/jest-preset-angular/blob/1ece7674231f5c422df4d2cae12ce3920a7346b9/src/ng-jest-transformer.ts#L61-L69

hyrious commented 2 years ago
format: 'cjs',

That's the cause. While as I know jest cannot run in ESM mode, so you may not be able to use lodash-es with jest.

CSchulz commented 2 years ago

We are using it in conjuction with jest-resolve. I was expecting jest-resolve will detect the correct order of the dependencies and will transform first lodash-es before trying to transform / load our library using lodash-es. Is that not true?

edisonLzy commented 1 year ago

i have a question , why this callExpression second parameter is 1 in esbuild output result ? 🤔️

var import_isPlainObject = __toESM(require("lodash-es/isPlainObject"), 1);

evanw commented 1 year ago

As you can see from the source code, the second parameter of __toESM is called isNodeMode. Node compatibility mode (including when and why it happens) is documented here: https://esbuild.github.io/content-types/#default-interop.

edisonLzy commented 1 year ago

As you can see from the source code, the second parameter of __toESM is called isNodeMode. Node compatibility mode (including when and why it happens) is documented here: https://esbuild.github.io/content-types/#default-interop.

i got a issue maybe related isNodeMode . here is a third dependence entry file in my project, it seem mark as ESM format already.

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(
    exports, "__esModule", { value: true });
const Graphemer_1 = __importDefault(require("./Graphemer"));
exports.default = Graphemer_1.default;

here is the output result, the isNodeMode has been marked as 1, this will cause __toESM unexpected working. check the comment in __toESM below.

var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // 🔧  it will hit this branch to define default props again, 
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));

var import_graphemer = __toESM(require_lib(), 1);
//  ❌  runtime error
var gaphemer = new import_graphemer.default();
ApplY3D commented 11 months ago

The same problem occurs when using import() with Angular 17 after building. It's strange because I didn't encounter this problem while serving locally.

export const exportExcel = () => import('xlsx-js-style').then(xlsx => {})

Workaround:

const safeESModule = <T>(a: T | { default: T }): T => {
  const b = a as any;
  return b.__esModule || b[Symbol.toStringTag] === 'Module' ? b.default : b;
};

export const exportExcel = () => import('xlsx-js-style').then(safeESModule).then(xlsx => {})
brookback commented 8 months ago

I have this problem when bundling code for the browser and having type: module set in package.json.

I do:

import xs from 'xstream';

console.log(xs.never());

This fails when running in the browser with never() doesn't exist on undefined.

Removing type: module from package.json fixes this issue, but prevents us from doing ESM on the server.

The problem

  1. I want type: module for doing native ESM on the server in Node.
  2. esbuild notices this, and uses "node mode". This will set the second argument to truthy in __toESM:
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));

which in turn messes up the default import of xstream.

I've scanned the excellent docs and read the explanations in https://esbuild.github.io/content-types/#default-interop, but it feels like I'm between a rock and a hard place: I want ESM on the server, but don't want this to affect esbuild's bundling of client code.

Workaround

Using @ApplY3D's workaround above worked:

// safe-xstream.ts
import _xs from "xstream";

const safeESModule = <T,>(a: T | { default: T }): T => {
    const b = a as any;
    return b.__esModule || b[Symbol.toStringTag] === "Module" ? b.default : b;
};

const xs = safeESModule(_xs);

export default xs;
// index.ts
import xs from './safe-xstream';

console.log(xs.never());
Minimal repro ```ts // src/index.ts import xs from 'xstream'; console.log(xs.never()); ``` ```json // package.json { "name": "tmp", "version": "1.0.0", "type": "module", "scripts": { "build": "./build-js.sh", }, "dependencies": { "esbuild": "^0.20.1", "xstream": "^11" } } ``` ```bash #!/bin/sh # build-js.sh set -eo pipefail npx esbuild \ --color=true \ --bundle \ --target=es2018 \ --format=esm \ --outfile=build/bundle-dev.js \ ./src/index.ts ``` ```html ```