oven-sh / bun

Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one
https://bun.sh
Other
74.53k stars 2.79k forks source link

Bun.build commonjs named exports conversion to ESM is broken #12463

Open Guibi1 opened 4 months ago

Guibi1 commented 4 months ago

What version of Bun is running?

1.1.18+5a0b93523

What platform is your computer?

Ubuntu WSL2 on Win11

What steps can reproduce the bug?

Compile a commonjs file using Bun.

This bug breaks the use of Bun.build, as the resulting file CANNOT be used for named import interchangeably with the original non-compiled file.

In this example, I compile the react package from node_modules (make sure to install it), and I then compare the imported module of the compiled react and the untouched react. I expect both to be identical. However, the compiled module has and additionnal depth.

await Bun.build({
    entrypoints: ["./node_modules/react/index.js"],
    outdir: ".",
    naming: {
        entry: "compiled-react.js",
    },
});

const react = await import("react");
const compiledReact = (await import("./compiled-react.js")) as typeof react;

console.log("Default react import:", react.default);
console.log("Default compiled import:", compiledReact.default);

console.log("Named react import (useState):", react.useState);
console.log("Named compiled import (useState):", compiledReact.useState);

export type {}; // Makes it a module

What is the expected behavior?

// Module react import:
const react = {
  Children: {
    map: [Function: mapChildren],
    forEach: [Function: forEach],
    count: [Function: count],
    toArray: [Function: toArray],
    only: [Function: only],
  },
  Component: [Function: Component],
  Fragment: Symbol(react.fragment),
  Profiler: Symbol(react.profiler),
  PureComponent: [Function: PureComponent],
  StrictMode: Symbol(react.strict_mode),
  Suspense: Symbol(react.suspense),
  __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE: {
    H: null,
    A: null,
    T: null,
    S: null,
    actQueue: null,
    isBatchingLegacy: false,
    didScheduleLegacyUpdate: false,
    didUsePromise: false,
    thrownErrors: [],
    getCurrentStack: null,
  },
  act: [Function],
  cache: [Function],
  cloneElement: [Function],
  createContext: [Function],
  createElement: [Function],
  createRef: [Function],
  forwardRef: [Function],
  isValidElement: [Function: isValidElement],
  lazy: [Function],
  memo: [Function],
  startTransition: [Function],
  unstable_useCacheRefresh: [Function],
  use: [Function],
  useActionState: [Function],
  useCallback: [Function],
  useContext: [Function],
  useDebugValue: [Function],
  useDeferredValue: [Function],
  useEffect: [Function],
  useId: [Function],
  useImperativeHandle: [Function],
  useInsertionEffect: [Function],
  useLayoutEffect: [Function],
  useMemo: [Function],
  useOptimistic: [Function],
  useReducer: [Function],
  useRef: [Function],
  useState: [Function],
  useSyncExternalStore: [Function],
  useTransition: [Function],
  version: "19.0.0-rc-f38c22b244-20240704",
}

What do you see instead?

// Module compiled import:
const compiled = {
  default: {
    Children: {
      map: [Function: mapChildren],
      forEach: [Function: forEach],
      count: [Function: count],
      toArray: [Function: toArray],
      only: [Function: only],
    },
    Component: [Function: Component],
    Fragment: Symbol(react.fragment),
    Profiler: Symbol(react.profiler),
    PureComponent: [Function: PureComponent],
    StrictMode: Symbol(react.strict_mode),
    Suspense: Symbol(react.suspense),
    __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE: {
      H: null,
      A: null,
      T: null,
      S: null,
      actQueue: null,
      isBatchingLegacy: false,
      didScheduleLegacyUpdate: false,
      didUsePromise: false,
      thrownErrors: [],
      getCurrentStack: null,
    },
    act: [Function],
    cache: [Function],
    cloneElement: [Function],
    createContext: [Function],
    createElement: [Function],
    createRef: [Function],
    forwardRef: [Function],
    isValidElement: [Function: isValidElement],
    lazy: [Function],
    memo: [Function],
    startTransition: [Function],
    unstable_useCacheRefresh: [Function],
    use: [Function],
    useActionState: [Function],
    useCallback: [Function],
    useContext: [Function],
    useDebugValue: [Function],
    useDeferredValue: [Function],
    useEffect: [Function],
    useId: [Function],
    useImperativeHandle: [Function],
    useInsertionEffect: [Function],
    useLayoutEffect: [Function],
    useMemo: [Function],
    useOptimistic: [Function],
    useReducer: [Function],
    useRef: [Function],
    useState: [Function],
    useSyncExternalStore: [Function],
    useTransition: [Function],
    version: "19.0.0-rc-f38c22b244-20240704",
  },
  Children: [Getter],
  Component: [Getter],
  Fragment: [Getter],
  Profiler: [Getter],
  PureComponent: [Getter],
  StrictMode: [Getter],
  Suspense: [Getter],
  __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE: [Getter],
  act: [Getter],
  cache: [Getter],
  cloneElement: [Getter],
  createContext: [Getter],
  createElement: [Getter],
  createRef: [Getter],
  forwardRef: [Getter],
  isValidElement: [Getter],
  lazy: [Getter],
  memo: [Getter],
  startTransition: [Getter],
  unstable_useCacheRefresh: [Getter],
  use: [Getter],
  useActionState: [Getter],
  useCallback: [Getter],
  useContext: [Getter],
  useDebugValue: [Getter],
  useDeferredValue: [Getter],
  useEffect: [Getter],
  useId: [Getter],
  useImperativeHandle: [Getter],
  useInsertionEffect: [Getter],
  useLayoutEffect: [Getter],
  useMemo: [Getter],
  useOptimistic: [Getter],
  useReducer: [Getter],
  useRef: [Getter],
  useState: [Getter],
  useSyncExternalStore: [Getter],
  useTransition: [Getter],
  version: [Getter],
}

Additional information

I modified the first line of the outputs to allow proper highlighting.

Possible fix

This issue can probably be fixed by modifying the functions added by the compiler at the start of the file:

var __toESM = (mod, isNodeMode, target) => {
  target = mod != null ? __create(__getProtoOf(mod)) : {};
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
  for (let key of __getOwnPropNames(mod))
    if (!__hasOwnProp.call(to, key))
      __defProp(to, key, {
        get: () => mod[key],
        enumerable: true
      });
  return to;
};
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
Guibi1 commented 4 months ago

After more digging, it seems like the final require_react() is working as intended in the compiled file. However, the only export is export default require_react();, which only exports a { default: require_react() }. This explains the additional nesting that occurs when the file is imported. I was unable to find a reliable solution that doesn't imply exporting each keys one by one. (export const { default, version, useState, [...] } = require_react();)

gyf304 commented 3 months ago

I managed to work around this by replacing isNodeMode || !mod || !mod.__esModule to isNodeMode && (!mod || !mod.__esModule). Since having the __esModule marker means that the CJS module is already ESM compatible, and does not need to be wrapped again with a default.

Relevant stackoverflow answer: https://stackoverflow.com/questions/50943704/whats-the-purpose-of-object-definepropertyexports-esmodule-value-0