evanw / esbuild

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

Question: writing a plugin to decorate module content #3303

Closed JakobJingleheimer closed 1 year ago

JakobJingleheimer commented 1 year ago

I would like to make a plugin to append some content to a module, specifically:

Input:

function Foobar() {}

Additional/new output:

n.displayName="Foobar";

The problem I'm trying to solve is making the outputted code compatible with React devtools' Components inspector, which is broken when format=esm + minify are set.

ESBuild → Try

Refs: https://github.com/evanw/esbuild/issues/2475

This is a very common feature in past build systems but is understandably outside the scope of ESBuild core. I'm hoping for some guidance on writing this plugin (which I would happily contribute back to the plugin registry).

ESBuild's built-in JSX loader is already doing the vast majority (probably ~99%) of the setup work I would need, so I'm hoping to somehow tie into that, but I don't see any way to do it. I think onLoad callback is the appropriate one to use.

Inqnuam commented 1 year ago

What about enabling sourcemap?

In production you can set --sourcemap=external , Then add sourcemap manually to Chrome when you want to inspect it with React Dev tools

hyrious commented 1 year ago

I'm not sure about the "auto add property" behavior. Did you see some other tools (babel plugin or webpack plugin) doing the same thing?

JakobJingleheimer commented 1 year ago

What about enabling sourcemap?

In production you can set --sourcemap=external ,

Then add sourcemap manually to Chrome when you want to inspect it with React Dev tools

I already have source-maps, and they do not address the issue.

JakobJingleheimer commented 1 year ago

I'm not sure about the "auto add property" behavior. Did you see some other tools (babel plugin or webpack plugin) doing the same thing?

Yes, the reffed issue cites the babel plugin that did it 🙂

hyrious commented 1 year ago

Yes, the reffed issue cites the babel plugin that did it.

Thanks, I missed that. With that babel plugin you can easily write an esbuild plugin based on that:

First of all, download that plugin and prepare the project

$ mkdir test && cd test
$ pnpm init && pnpm add -D @babel/core @babel/helper-plugin-utils @babel/preset-react esbuild
$ curl https://raw.githubusercontent.com/zendesk/babel-plugin-react-displayname/master/src/index.js -o react-displayname.cjs
$ touch build.js

Here's the build.js content:

import fs from "fs";
import reactDisplayName from "./react-displayname.cjs";
import { transformSync } from "@babel/core";
import { build } from "esbuild";

await build({
  entryPoints: ["./main.jsx"],
  bundle: true,
  plugins: [
    {
      name: "react-displayname",
      setup({ onLoad }) {
        onLoad({ filter: /\.jsx$/ }, (args) => {
          var code = fs.readFileSync(args.path, "utf8");
          // https://github.com/zendesk/babel-plugin-react-displayname/blob/master/src/index.test.js
          code = transformSync(code, {
            babelrc: false,
            configFile: false,
            plugins: [reactDisplayName],
            presets: [["@babel/preset-react", { pure: false }]],
          }).code;

          return { contents: code, loader: "default" };
        });
      },
    },
  ],
}).catch(() => process.exit(1));
evanw commented 1 year ago

Thanks for your answer @hyrious. I'm closing this because this has been answered.

JakobJingleheimer commented 1 year ago

Thanks @hyrious! Won't that basically inject babel core + that plugin into the build process? I'm not otherwise using babel, so ideally it wouldn't literally use babel for this.

hyrious commented 1 year ago

@JakobJingleheimer Yes, it uses babel. This is because the babel plugin you referenced to implements the function by analyzing the AST node of JSX files and guess if a normal function is a component. esbuild does not allow you to access AST so you cannot do something based on that, so to achieve the same effect you have to invoke babel core to run that plugin.

As you may already looked into the esbuild plugin's API, the onLoad callback asks you to provide the code contents using the resolved path. You can do anything during this step to achieve your goals. For example, if you decide to enforce some kind of code style like that all components should use titlecase on a function name, you can write a simple string-search based plugin too.

onLoad({ filter: /\.jsx$/ }, (args) => {
  var code = fs.readFileSync(args.path, "utf8"), names = new Set();
  // let's say that 'function Titlecase(' must be a component
  code.replace(/^function ([A-Z][^\s\(]+)/g, (_, name) => {
    names.add(name);
  });
  // append '{name}.displayName = "{name}"' to the end
  for (var name of names)
    code += "\n" + name + ".displayName = " + JSON.stringify(name);
  return { contents: code, loader: "default" };
})