evanw / esbuild

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

Support tree-shaking unused properties on an object literal #2855

Open fabiospampinato opened 1 year ago

fabiospampinato commented 1 year ago

It'd be interesting if default exports could be tree-shaken too, perhaps on a global or per-module opt-in basis.

The problem is that, for example, I want the DX that the following allows, where I can then just do a super clean import Lib from 'lib', rather than having to write * as or having to import each little thing individually.

import foo from './foo';
import bar from './bar';
import baz from './baz';

const Lib = { foo, bar, baz };

export default Lib;

But I want the tree-shaking that this allows:

import foo from './foo';
import bar from './bar';
import baz from './baz';

export {foo, bar, baz};

Basically I think there should be some mechanism for treating simple trivially analyzable default exports as tree-shakeable. Maybe if an ESM package says explicitly that it has "sideEffects": false this optimization could be performed? Maybe some other flag or #PURE-like annotation could be introduced?

Alternatively the simplest workaround I can think about is to have two entry points of your library, one exports things individually and the other imports everything from that and exports them together as the default export, then you import the default export, but rewrite the code with a simple transform to actually import everything from the other entry point with * as. It should work, and it's simple enough to be practical as a one-off, but if I need to modify every library entry point and account for each of them with a transform it gets impractical pretty quick, it'd be nice if this Just Worked™️

evanw commented 1 year ago

Are you saying if you do import Lib from 'lib' and then lib.foo() that esbuild would remove bar and baz? One problem is that lib.foo() hands foo the entire value of lib via this, which it could then use to access bar and baz. So this is not "trivially analyzable" because removing bar and baz would be unsafe and incorrect in some cases.

fabiospampinato commented 1 year ago

There will be cases where analyzing the code would be complicated enough, if not impossible, but I'd argue it would still be useful to implement this, if there's a reasonable mechanism for opting into this that doesn't break the world. Maybe somebody should just make a userland plugin for this.

In you example specifically if foo is an arrow function that problem doesn't exist immediately, and at least I almost always use arrow functions, though I understand most people don't, in which case one could check if this is ever used inside the body of the function, or if the function contains some runtime code generation (eval/Function/setInterval...).

ghost commented 3 months ago

If you only supported a strict form for this feature, where export * as is used to build the namespace [1], would that be a useful signal from the developer to say "I'm opting into this", could that also guarantee that the namespace doesn't get modified and does it make the feature any easier to implement if you did that?

For example, if I built the namespace like this:

lib/public.ts

import foo from './foo';
import bar from './bar';
import baz from './baz';

export {foo, bar, baz};

// export * as qux from "./qux"; // [1] and maybe also if the file only contains imports/exports?

lib/index.ts

export * as Lib from "./public";

main.ts

import { Lib } from "./lib";

Lib.foo();
Lib.baz();
// Lib.qux.quux();
evanw commented 3 months ago

I don't think that would make this easier to implement as that's pretty much a completely separate feature from what is being requested here. The export statement may look like an object literal because its syntax also uses the { and } tokens, but no object is involved and it does something pretty different. The request for the feature you are asking about is already tracked here: #1420