Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unused top-level code is not removed with format:esm but is removed with format:iife #1551

Closed
marc136 opened this issue Aug 25, 2021 · 3 comments

Comments

@marc136
Copy link

marc136 commented Aug 25, 2021

Hello,

If I use an IIFE as output format, the dead top-level code is removed. I would expect the same behavior for ESM (as the global definitions also cannot leak outside the module).

This seems to be an unintended bug, as @evanw wrote on #1518 (comment)

ESM or IIFE. Neither of these formats allow module-level variables to leak into the global scope, so it’s ok for esbuild to remove unused code as it cannot possibly be referenced.

Or is there maybe a technical reason why I need to use the build API to get DCE when using ESM as an output format? But I think it would still be inconsistent behavior.

I have this code to check for the behavior

const esbuild = require('esbuild');
const fs = require('fs');
const os = require('os');

function print(caption, code) {
    console.log(caption, '\n```js\n', code.toString().trim(), '\n```\n');
}

const esm = `// as ESM
function unusedFn(a) {
    return a + 1;
}

const a = 5;
let b = 7;

export const c = 2;
`;

const iife = `// as IIFE
function unusedFn(a) {
    return a + 1;
}

const a = 5;
let b = 7;

window.c = 2;
`;

const options = {
    target: "es2020",
    format: "esm",
};

let result = esbuild.transformSync(iife, Object.assign({}, options, { format: "iife" }));
print('Unused code is removed with format:iife', result.code);

result = esbuild.transformSync(esm, options);
print('Unused code is kept with format:esm', result.code);

const tmpFile = `${os.tmpdir()}/esbuild-test-${Date.now()}.js`
fs.writeFileSync(tmpFile, esm, { encoding: 'utf8' });
esbuild.buildSync(Object.assign(options, {
    entryPoints: [tmpFile],
    outfile: tmpFile,
    allowOverwrite: true,
    bundle: true,
}));
result = fs.readFileSync(tmpFile, { encoding: 'utf8' });
fs.rmSync(tmpFile);
print('Unused code is removed with format:esm and bundle:true', result);

Which yields with esbuild 0.12.22:

❯ node esbuild-bug.js

Unused code is removed with format:iife

(() => {
  window.c = 2;
})();

Unused code is kept with format:esm

function unusedFn(a2) {
  return a2 + 1;
}
const a = 5;
let b = 7;
const c = 2;
export {
  c
};

Unused code is removed with format:esm and bundle:true

 // ../../../../../tmp/esbuild-test-1629875946567.js
var c = 2;
export {
  c
};
marc136 added a commit to marc136/elm-optimize-test that referenced this issue Aug 25, 2021
@evanw
Copy link
Owner

evanw commented Aug 26, 2021

This is intended behavior. People sometimes use the transform API and then expect to be able to stick extra code on the end and have it work (i.e. provide a partial input). For example, the Svelte people need it to work this way to be able to use esbuild for their TypeScript support: #604 (comment). And automatic DCE messes that up.

DCE is enabled for IIFE output but not for ESM output because adding extra code at the end can't access inside the IIFE closure but can access inside the top-level ESM scope. However, DCE is always enabled when bundling because the assumption is that all of the code to be bundled is included in the input files.

I agree that what the Svelte people want to do is weird. For example, they need to hack into the TypeScript compiler to get it to have this behavior. But esbuild's current behavior enables their use case while still making both things possible. So that's why it currently works that way.

@marc136
Copy link
Author

marc136 commented Aug 26, 2021

Thank you for this explanation.

Would it be possible to add a configuration setting to enable DCE in an ESM using the transform API?
Sth like removeUnusedCode:false or removeDeadCode:true that is implicitly set to true if an IIFE is used or if bundle:true is given and can be activated for ESM?
I assume this would mean exposing a switch that already exists but is not yet directly changeable via configuration.

So the default behavior for Svelte does not change, and the docs could tell about this behavior (or maybe I'm just blind and missed this info about requiring bundle to enable DCE for target:esm in the docs).

marc136 added a commit to marc136/elm-optimize-test that referenced this issue Aug 26, 2021
marc136 added a commit to marc136/elm-optimize-test that referenced this issue Aug 27, 2021
marc136 added a commit to marc136/elm-optimize-test that referenced this issue Aug 27, 2021
marc136 added a commit to marc136/elm-optimize-test that referenced this issue Aug 27, 2021
@evanw
Copy link
Owner

evanw commented Jan 22, 2022

You should now be able to use treeShaking: true with format: 'esm' to do this. This was added in version 0.13.0 last September. Closing as fixed.

@evanw evanw closed this as completed Jan 22, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants