esbuild icon indicating copy to clipboard operation
esbuild copied to clipboard

Namespace export results in empty object across monorepo

Open zaidarain1 opened this issue 1 year ago • 1 comments

Hi,

We're currently migrating from rollup to esbuild and are facing an issue regarding namespace imports/exports.

The monorepo has two packages, package metrics and package passport which has metrics as a dependency. In the src/index.ts, metrics imports a file via a namespace import and then attempts to re-export it within an object. When this object is called by passport, the value of that import is just an empty object, {}.

The code is open source and is viewable below. (Metrics package) The metrics index.ts file that does the import/export here The file that metrics is attempting to export via a namespace import here

(Passport package) The passport package attemping to use the utils object here Note that adding a console log on line 6 for the utils import results in the following output:

{ localStorage: {} }

Below is also the relevant output javascript code for the metrics package produced with esbuild. dist/index.js

var __defProp = Object.defineProperty;
var __export = (target, all3) => {
  for (var name in all3)
    __defProp(target, name, { get: all3[name], enumerable: true });
};

// src/utils/localStorage.ts
var localStorage_exports = {};
__export(localStorage_exports, {
  deleteItem: () => deleteItem,
  getItem: () => getItem,
  setItem: () => setItem
});

// Block of code

// src/index.ts
var utils = {
  localStorage: localStorage_exports
};
export {
  // other exports
  utils
};

Tried many different approached regarding the command line arguments, as well as attempting to export the localStorage namespace import in different creative ways that workaround the issue, however none of those are backwards compatible. I've also been facing this same issue with tsup, which is powered by esbuild.

Is there any way to get these imports working across packages like they are with rollup? In case it's relevant, the rollup config currently used can be found here

Esbuild version: 0.23.0 Command: esbuild src/index.ts --platform=node --outfile=dist/index.js --format=esm --target=es2022 --bundle

zaidarain1 avatar Jul 09 '24 07:07 zaidarain1

The empty object log is fine here since its fields will be computed at the first time you access to it.

However if you do catch an empty object at runtime, it is likely that some code are run before variables like deleteItem being defined. In that case you can check if some of your code is re-ordered after bunding and try to get a minimal reproduction.

esbuild lifts the namespace object definition up (before this module's contents) mainly for compatible with circular dependencies, where a module may import itself while being executed. This is not a good code pattern and should be avoided by code authors. On the other and, this definition also strictly ensures ESM's semantic of namespace (i.e. immutable).

So here are 2 workarounds to it:

  • Lazily initialize everything to prevent code reordered breaks at runtime. For example instead of const { deleteItem } = someModule at top-level, use that name inside the function you really need it.
  • Instead of using namespaces, you can export a single object which ensures its running order is after its dependencies: export const localStorage = { deleteItem }.

hyrious avatar Aug 10 '24 01:08 hyrious