evanw / esbuild

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

onEnd cannot be used to modify file output #2999

Open MageJohn opened 1 year ago

MageJohn commented 1 year ago

I would like to modify the ouput format of my bundle before esbuild writes it to disk. I thought this would be possible with the onEnd hook, because the documentation says it "can modify the build result before returning". Indeed, this seems to have been possible in the past (see how the callback is written here: https://github.com/marvinhagemeister/karma-esbuild/issues/33#issuecomment-1093042415). However, it looks like you changed the way the hook works in version 0.17.0, and now the files are written before the hook is run.

While I can manually overwrite the files in the hook, this makes the plugin integrate less nicely into esbuild. For correctness, the plugin must manually respect the write option, and the write option should be turned off by the plugin to avoid writing outputs twice. Also, the printed summary shows the size of the output before the onEnd hook runs, making it innaccurate.

For context, my use case is that I'm bundling some bookmarklets, so prefixing my output with javascript: and URI encoding it. My plugin looks like this:

const bookmarkletOutput = {
  name: "bookmarklet-output",
  setup(build) {
    const options = build.initialOptions;

    const write = options.write ?? true;
    options.write = false;

    build.onEnd(async ({ errors, outputFiles }) => {
      if (!errors.length && outputFiles?.length && write) {
        await Promise.all(
          outputFiles.map(async (out) => {
            await outputFile(out.path, encodeURI(`javascript:${out.text}`));
          })
        );
      }
    });
  },
};

What I would like to do is modify the file contents directly, with a plugin that looks like this:

const bookmarkletOutput = {
  name: "bookmarklet-output",
  setup(build) {
    const encoder = new TextEncoder();
    const encodeUTF8 = (text) => encoder.encode(text);

    build.onEnd(({ errors, outputFiles }) => {
      if (!errors.length && outputFiles?.length) {
        outputFiles.forEach((out) => {
          out.contents = encodeUTF8(encodeURI(`javascript:${out.text}`));
        });
      }
    });
  },
};

However, as described above, this doesn't work because the files are seemingly already written, so modifying out.contents has no effect.

For completeness, the plugin is being used with a config that looks like this:

await esbuild.build({
  entryPoints: ["src/some-input.js"],
  bundle: true,
  format: "iife",
  minify: true,
  outdir: "dist/",
  logLevel: "info",
  plugins: [bookmarkletOutput],
});
KonnorRogers commented 1 year ago

Would something like this work?

const bookmarkletOutput = {
  name: "bookmarklet-output",
  setup(build) {
    const encoder = new TextEncoder();
    const encodeUTF8 = (text) => encoder.encode(text);

    build.onLoad({ filter: /.*/ }, (args) => {
      return {
        contents: encodeUTF8(encodeURI(`javascript:${fs.readFileSync(args.path)}`));
      }
    });
  },
};
MageJohn commented 1 year ago

I don't think so; if I understand that correctly, that will operate on every file that's loaded, whereas I want to operate on the bundled output.

mayank1513 commented 1 year ago

I was facing same issue, I fixed it using following approach

setup(build) {
    const write = build.initialOptions.write;   
    build.initialOptions.write = false;
    ...
    build.onEnd(result => {
       outputFiles.forEach(f =>
         ... 
          f.contents = new TextEncoder().encode(newContents);
         ...
       }
       ...
       if (write === undefined || write) {
           result.outputFiles?.forEach(file => {
           fs.mkdirSync(path.dirname(file.path), { recursive: true });
           fs.writeFileSync(file.path, file.contents);
       });  
    }

Checkout the plugin here - https://github.com/mayank1513/esbuild-plugin-react18

mihailik commented 2 weeks ago

How can I modify the output result in the --serve case?