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

Allow plugin resolve callbacks to mark a module as side-effect free #1009

Closed
buffaybu opened this issue Mar 19, 2021 · 4 comments · Fixed by #1313
Closed

Allow plugin resolve callbacks to mark a module as side-effect free #1009

buffaybu opened this issue Mar 19, 2021 · 4 comments · Fixed by #1313

Comments

@buffaybu
Copy link

I'm using esbuild to eliminate unneeded code by bundling a virtual entry that only re-exports the needed part and mark every imports as external.

An example that transforms originalCode to only keep client side code:

const { build } = require("esbuild");

const originalCode = `
  import { serverFn } from "./server-side";
  import { clientFn } from "./client-side";

  export function server() {
    serverFn();
  }
  export function client() {
    clientFn();
  }
`;

async function main() {
  const result = await build({
    stdin: {
      contents: `export { client } from 'ORIGINAL'`,
      loader: "ts",
    },
    bundle: true,
    format: "esm",
    write: false,
    plugins: [
      {
        name: "only-export-client",
        setup: (build) => {
          build.onResolve({ filter: /.*/ }, (args) => {
            if (args.path === "ORIGINAL") {
              return {
                path: "ORIGINAL",
                namespace: "proxy",
              };
            }
            return {
              external: true,
            };
          });
          build.onLoad({ filter: /.*/, namespace: "proxy" }, async (args) => {
            return {
              contents: originalCode,
              loader: "ts",
            };
          });
        },
      },
    ],
  });

  console.log(result.outputFiles[0].text);
}
main();

The output is:

// proxy:ORIGINAL
import {serverFn} from "./server-side";
import {clientFn} from "./client-side";
function client() {
  clientFn();
}
export {
  client
};

./server-side is still imported and I guess the reason is that esbuild doesn't know whether the module is side-effect free or not.

So if it's about side-effect, is it possible to allow plugin resolve callbacks to mark a module as side-effect free? Or is there any other solution to solve this problem?

@buffaybu
Copy link
Author

I'm temporally solving this by transforming the output again by ts loader (leaving importsNotUsedAsValues as default which is "remove").

const { transformSync } = require("esbuild");

const code = `
// proxy:ORIGINAL
import {serverFn} from "./server-side";
import {clientFn} from "./client-side";
function client() {
  clientFn();
}
export {
  client
};
`;

const result = transformSync(code, {
  format: "esm",
  loader: "ts",
});

console.log(result.code);
import {clientFn} from "./client-side";
function client() {
  clientFn();
}
export {
  client
};

@ryanflorence
Copy link

@buffaybu we have a nearly identical plugin. If you set package.json sideEffects: false in the app directory of the code you're bundling, you'll get what you're looking for.

@chriscasola
Copy link
Contributor

@evanw I can implement this, were you thinking of adding a field like SideEffectFree to OnResolveResult?

chriscasola pushed a commit to chriscasola/esbuild that referenced this issue May 26, 2021
Adding a SideEffectFree property to OnResolve that plugins
can set to indicate that the resolved module is side effect
free.

Fixes evanw#1009

Co-authored-by: Adam Gaynor <adamsg9425@gmail.com>
@buffaybu
Copy link
Author

buffaybu commented Jun 17, 2021

With v0.12.9 and sideEffects: false, the example of this issue still outputs the same result.

const esbuild = require("esbuild");

const originalCode = `
  import { serverFn } from "./server-side";
  import { clientFn } from "./client-side";

  export function server() {
    serverFn();
  }
  export function client() {
    clientFn();
  }
`;

async function main() {
  const result = await esbuild.build({
    stdin: {
      contents: `export { client } from 'ORIGINAL'`,
      loader: "js",
    },
    bundle: true,
    format: "esm",
    write: false,
    plugins: [
      {
        name: "only-export-client",
        setup: (build) => {
          build.onResolve({ filter: /.*/ }, (args) => {
            if (args.path === "ORIGINAL") {
              return {
                path: "ORIGINAL",
                namespace: "proxy",
                sideEffects: false,
              };
            }
            return {
              external: true,
              sideEffects: false,
            };
          });
          build.onLoad({ filter: /.*/, namespace: "proxy" }, async (args) => {
            return {
              contents: originalCode,
              loader: "js",
            };
          });
        },
      },
    ],
  });

  console.log(`// v${esbuild.version}\n`);
  console.log(result.outputFiles[0].text);
}
main();
// v0.12.9

// proxy:ORIGINAL
import { serverFn } from "./server-side";
import { clientFn } from "./client-side";
function client() {
  clientFn();
}
export {
  client
};

Import of ./server-side is not shaken off.


Update: After reading #1241 (comment) the above behavior now makes sense to me that sideEffects: false means don't bundle unused code from the importee, instead of don't import it. And by #705 external imports are reserved.

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

Successfully merging a pull request may close this issue.

3 participants