skip to content

Build an esbuild plugin

The speed of esbuild is incredible, but what happens if you need it to do more? Chance Strickland will teach us how to extend esbuild with custom plugins in this episode.

Full Transcript

Click to toggle the visibility of the transcript

Captions provided by White Coat Captioning (https://whitecoatcaptioning.com/). Communication Access Realtime Translation (CART) is provided in order to facilitate communication accessibility and may not be a totally verbatim record of the proceedings.

JASON: Hello, everyone. And welcome to another episode of Learn with Jason! Today on the show we are bringing in Chance Strickland. How are you doing, Chance?

CHANCE: Doing swell. How about you, Jason?

JASON: I'm also doing swell. I'm super-happy to have you on the show. It is the first time you have been on the show, but I feel like I have learned a lot from you already, I'm really pleased to be able to bring all my friends in the chat in and learn from you again. So, for folk who is aren't familiar with you and your work, do you want to give us a bit of a background?

CHANCE: Sure, yeah. I have been playing this Dev game for several years now. I sort of got my start working in agencies. WordPress sites in agencies. Did that for several years. Went and did my own thing, contracting for a while, with that, and then I learned about React and took off in the JavaScript ecosystem ever since. That's what I have been chasing after and focused on. And in, I don't know? What was it? 3, 4 years ago now I started working with Michael Florence and Michael Jackson who ran React training. I was working on Reach UI, a accessibility-focused UI. I started training with them and teaching teams how to use React effectively. That was all well and good until pandemics. We had one of those. I don't know if you heard about it. But it kind of changed a lot of stuff.

JASON: Sure.

CHANCE: React training, we will a lot of in-person stuff that had to stop. I worked with a team from there called Modules, writing a visual editor for the web, you're doing great work over there. We did rayx-ui library. It's similar to reach UI. Is expanded scope and had more resources to hone in on the components we built. It's cool. It's a great library and check it out if you're not familiar with it. It's awesome and those folks are awesome. Really love the modules game. And then I started working with Michael and Ryan again not too long ago and they started building Remix. I have been working on Remix with the lovely new React framework that is all of the rage with the kids these days. We can talk more about Remix. But, yeah, Remix.run, check it out if you haven't already. I think it's great and it will help you build better websites. That's what I'm you were to these days.

JASON: Yeah, we've got a couple episodes on Remix. We're talking today about esbuild. And the reason we're talking about esbuild is that's what's powering Remix, is that right?

CHANCE: That's our compiler.

JASON: Gotcha.

CHANCE: All the JavaScript you write in Remix is powered by esbuild. We build two, a client and a server build. But esbuild is responsible for both of those.

JASON: Nice. That kind of sent you down a rabbit hole, I imagine. Because it's, you know, building anything like that is always challenging. But especially when you're working on it as a, you know, when you got a framework. Frameworks have to be flexible. Not just for like solving an individual problem. Like if I'm building something at work, I'm trying to solve my team's problem. When you're building a framework, you're trying to solve a community's problem. Those are very shape-shifty things. How did you find yourself in the esbuild extending game?

CHANCE: Yeah. That's an interesting question. I knew nothing about compilers at all. I found myself in the esbuild game by really pushing hard on Remix supporting various CSS styling solutions that would require a compiler. One of which is CSS Modules which I'm still working on. Which we have been working on for a while. And it's something I really hope to get in fairly soon. But that requires a build step. Because when you import a CSS module, it's not JavaScript, right? The compiler needs to know what to do with that import. It doesn't just give you a JavaScript thing. You have to do something with it. And in relation to Remix sites, what you have to do is process the CSS. There's a lot of CSS module syntax that's going to change. You have to process the class name to that they're hashed.

JASON: Can y'all hear this jet flying over my house right now?

CHANCE: I did hear the jet. I wasn't sure that was about.

JASON: Sorry, everyone. I think there's an air show in Portland today. There's a lot going on out there. Sorry about that. And I interrupted you.

CHANCE: You're fine. I think Jason literally got off a plane 20 minutes ago or something and jumped right on to stream. Any excess noises are not his fault. We're doing it live! As they say.

JASON: Yeah.

CHANCE: Yeah, esbuild is responsible for -- for taking all of those imports and make sense of them and trying to do something with them. I started working on this plugin, dove deep on esbuild and compilers in general and learned a lot. Yeah, learned how to write some plugins and excited to talk about it because esbuild is really cool.

JASON: Yeah. I feel like esbuild sort of it kind of exploded on to the scene where it was like everything was -- there was only Webpack. Like the whole world was Webpack. And then all of a sudden, esbuild came out and it like blew the world open. I feel like we started seeing a lot of innovation in the space again. And I don't think esbuild may not have even been the first. Because there was like SWC, there was parcel, there were a few that just started exploring. What does it look like to bundle without Webpack? But esbuild came in and it was so fast. So, fast that it's kind of startling. The first time I used it, I thought it was broken. There's no way, right?

CHANCE: Yeah.

JASON: But one of the reasons that esbuild is so fast is that it does a whole lot less than what Webpack is trying to do out of the box, right? Esbuild is bundling ES modules. It can do that quickly. Webpack is transpiling, running Babel, running various extensions and plugins, various forms. It's not exactly an apples to apples comparison. But it was interesting to see how much esbuild changed the game for innovation. I feel like it opened the door for a lot of these new frameworks and meta frameworks that we're seeing because all of a sudden there was a new way to solve a problem. So, the legacy frameworks like the Next and the Gatsbys and the ones that are kind of loaded up with a lot of things that were pre-this new wave of innovation, there was a run for --

CHANCE: I want to say -- sorry, I didn't mean to interrupt. I want to say how I loved you called technologies that are like 2 or 3 years old max legacy. Always cracks me up in anything in the JavaScript ecosystem. As soon as it turns a year old, it's like, it's stale. What are we gonna do next?

JASON: Yeah. I should probably use a different word. But it just feels the pace of the --

CHANCE: I get it.

JASON: Development it's so -- it's moving, right? Things get really -- it's very wild. So, I think I'm -- I think I'm clipping. And all right, y'all, I'll come clean with you. I have been out of town. I got in last night. And I have not set up my office yet. So, I like have my computer. But I like plugged things in right before the stream and I'm pretty sure that something in my audio chain is not quite right yet. I apologize for the crackling. If it gets really subtracting, let me know and I'll switch to the MacBook pro mic. I think I'm clipping and I'm too hot. I will get that fixed, but I can't fix it right now without unplugging things. I apologize for that. All right. So, let's talk a little bit about -- I know that we're gonna do a little bit of a faster episode today. We're going to try to get done within an hour. We have about 40 minutes between now and when we're gonna call this thing done. No, 50 minutes. 50.

CHANCE: I think we can do it. Yeah.

JASON: I think we can do it. Let's talk little bit about what the goal is. When would you want to extend esbuild? You mentioned CSSModels as being one case. Where would someone find themselves needing to extend esbuild?

CHANCE: Sure. So, essentially, if you have any sorted of not-JavaScript in your JavaScript, rights? So, like which is really common for us these days. We got JSX in React, and esbuild does handle JSX by default, which is great. But anything else, it's very hands off. Which allows it to be super-fast, which is great. I like to tell people, go run some code with esbuild and then run some code with Webpack and look at the output. They're radically different. So, that should tell you how much one is doing versus how much the other is doing. So, esbuild is very hands off in a lot of ways. But we do rely on a lot of not-technically JavaScript in our JavaScript code these days. So, any time you need to do anything, any sort of transformations with your imports or with any of the code you encounter, you are gonna need to extend esbuild. So, one quick caveat to that is that unlike Webpack and Rollup and Parcel and -- I don't know about Parcel -- but a lot of bundlers have plugins that allow you to sort of jump into the AST a little bit, right? AST for those who don't know, abstract syntax tree. It's the base of the compiler. It's a data model that takes our previous cold -- our previous code -- and uses that information to transform it into code that runs the same way that we would expect. A lot of these tools in the past have let us reach into that AST to make transformations on individual nodes within the code. Esbuild doesn't do that, at least not with JavaScript. The JavaScript API. Because it's written in Go, not JavaScript. And that's a big reason why it's so freakin' fast. If you allowed folks to jump in there in JavaScript land and start tinkering, that's going to bottle Mack. And at least at the moment, you cannot jump in and do individual transforms on stuff within the code.

JASON: Right.

CHANCE: If you want to do that, you would use a plugin to intercept the import, take the import, take the file contents and manipulate it however you want to spit it back out. One note on that limitation. There are tradeoffs with esbuild as there are with any software. Something to keep in mind.

JASON: For sure.

CHANCE: That's the case for extending esbuild.

JASON: Gotcha. The lack of ability to transform nodes is not impossible with esbuild. That's when you would introduce a plugin. You need to build all of those transforms yourself. Which and if I remember correctly, that's kind of how Webpack started and then really the game with Webpack was like, we can build transforms for everything. And over time it just kind of became like, if you needed to change anything at all about your app, there was a web transforms for it. A lot of them came pre-baked in. A lot of people thought Webpack did all of that stuff. And reconfiguring Webpack and you didn't use create -- you have the bare config and then import all of the different Babel transforms and your Webpack plugins and all these different things so you could actually put it together. It was 15, 20, 30 different npm packages to run the build processes. We just abstracted that away with React app or next or Gatsby or whatever it was. That's where the slowdown starts to come. Each of the things, they don't work together, they're working on a separate piece of work and that's not always happening in parallel or cleanly, right? So --

CHANCE: Most of the time you don't know what's happening. Most people like myself configure Webpack and then never look at it. That's kind of the MO with Webpack. You get it to work exactly how you want and then shove it in the corner somewhere and pray you never have to go back and actually inspect anything in it. That's my approach.

JASON: Yeah, exactly, right? And this is sort of -- I think this is what's been interesting is like it felt like for the last 5 years or so the name of the game was make it so that when we write JavaScript it's possible to run that anywhere. And so, the work that was done with Babel to get just universal compatibility. And the work that was done with -- with Webpack to be able to do these transforms for different non-spec but very common things like we have, you know, or Webpack transforms for CSS-in-JS. We've got CSS modules, SaaS, stylus, whatever you want to use. All of these different things come with plugins. But what we've also seen is that the browser has been advancing. And we've seen the community settle around ES modules. We're still coming to the end of that where we're not quite 100% supported. But we have seen a lot of new browser APIs come up as well that eliminate what some of the stuff that was happening 5 years ago was needed for. We don't necessarily need as many of those tools. And when I call things "Legacy," that's kind of what I mean. They're there because we needed them before. But we don't really need them now. And esbuild feels a little bit like, okay, let's start with what's true today and build another layer on top that have to get the best possible starting point. We're getting I would say a refresh of how we build for modern tooling. But before I get too far off on a rabbit hole here, knowing like trying to be mindful of time. I want to make sure we've got plenty of time to build. So, did you have anything to add to my little rant before I jump us over into writing code?

CHANCE: Sure. Real quick, I just want to make it clear. I am not like hating on Webpack. Webpack did so much for us. And it still does so much for us, so many tools are still powered by it. I learned the other day about the I think it's module federation in Webpack. And it was bonkers and I had literally no idea what it was for until two days ago. It changed my outlook of the low level tool like Webpack. My biggest problem with Webpack is I never learned to use it. If that's your approach -- sometimes it has to be. We have to build stuff and ship it. You don't have the time to learn the nuances of every single little thing. All that to say, I have no hate for Webpack or Rollup or any of the tools. I really love esbuild. To your point, it's been able to simplify so much because the language and the tooling around JavaScript has progressed so much in just a few short years.

JASON: Absolutely.

CHANCE: Yeah. That is pro-esbuild, not an anti-Webpack situation.

JASON: Yeah. And I think the -- maybe a way to think about it too just for anybody in the chat who was maybe getting the impression that we're hating on Webpack, it's more that we're standing on the shoulders of giants here. JQuery normalized so much on the browser so that we could build something like Grunt and Gulp which gave us build systems so that we could build Webpack and gave us new flexibility and new way of thinking about build so good that we could build esbuild. It's all -- it's the progression and evolution. It's not that one thing is bad. It's that things need to exist before other things can be built. And I think we see that through a lot of web Devs. Very much agree. There's no hate here. This is just celebrating progress, right? All right. So, let's get into a little bit of -- of coding here. So, I'm gonna -- I'm gonna jump us over to the pair programming view. And I'm gonna do a shoutout to our live captioning. We have Amanda with us today. Taking down all these words, thank you very much, Amanda. Amanda's coming over from White Coat Captioning. And that is made possible through the generous contributions of our sponsors. We've got Netlify, NX, and Backlight all kickin' in to make the show more accessible and generally allow us to do cool thing on Learn with Jason that I would never be able to do without that support. Head over to the home page, check out the transcription and learn about the sponsor links to learn about them. And we are talking to Chance today, head over to Twitter and give him a follow. I know that you were -- you were hesitant to get more followers. But too bad. Everybody, go follow him. We are talking today about esbuild. So, esbuild is -- you can see here, this is a comparison at build time. They're running ten copies of default JS with minification. Esbuild is doing .3, Webpack is taking 41 seconds, Parcel is -- you can see like the first time they ran this, I was like, it's broken. It happens so fast, you assume no work was done. But it's a stark difference. Like you said, it's built in Go. It's extremely efficient, extremely fast. But part of that is because it's a very limited API. Which brings us to with a we're gonna do today. Which is build a plugin. Chance, where should we start?

CHANCE: Let's start in the repo. So, I'm going to pop a link in the chat. And now I'm gone -- I'm getting that echo in your mic again. Sorry. Not the -- oh, wait, hold on. I think it's gone again. I think you're good. All right. There's some magical devil sitting in your room like throwing darts and just knocking your audio off today.

JASON: It's killin' me.

CHANCE: But yeah. Let's start at the repo. We got the repo open now. This is just a quick little React app that I through together that is bundled with esbuild. Very, very straightforward and simple. Let's go ahead and I think you got that cloned locally so we're gonna start working with that. This is called the Boops with Jason app. We all know how Jason loves his boops. If we could drop some boops in the stream here, that would be awesome. Because, I don't know. I just love that little effect that you got going on when we fill the void with just endless boops.

JASON: Yeah. We fill that empty place in my heart with boops is really how that goes.

CHANCE: That's right. There we are. We're getting the boops. Let's bring it on. Cloned the Boop with Jason repo, and got that opened locally. Run npm and Dev, open up the URL. You have to install first. Did you install everything?

JASON: Let me open this up. Here's our VSCode. And then I'll use the terminal in VSCode from here on out.

CHANCE: Sweet.

JASON: Let's run npm run.

CHANCE: Dev.

JASON: Oh, npm run Dev. Okay. And see? It's so dang fast, that --

CHANCE: It's just basically instant, yeah.

JASON: Yeah.

CHANCE: I should have put a time log on there. I should have put a log on there to show you the time. It's probably like 10 milliseconds or something ridiculous. Anyway, here's our boop. We've got a couple pages, we've got a home page. We're ready for some boop. We have the home page. Let's see this lil guy! I don't see a lil guy. We're going back into the code. Take a look -- in the source directory, we've got pages here and using React router. Let's look at the boop component. That's our boop route. So, we've got a couple things in here. We're importing this thing called "Corgi" from corgi.boop. What the heck is a boop? Let's uncomment that really fast and let's render that component. Let's comment that component there. And just save. And let's reload the page. See what happens? Nothing's going to happen. Because your app crashed? Why did your app crash? Because we have no idea on Earth what the heck corgi.boop is or means. This is a weird little thing. What are we doing here? What is corgi.boop giving us? Do you see a file called corgi.boop? I don't.

JASON: Wait a minute.

CHANCE: Found a boops folder. Open up corgi.json. Let's look at that guy. We've got a little data here.

JASON: Okay.

CHANCE: We've got the data object. Looks like data for an image. Source URL and an alt tag. I think this is probably going to be important to render an image with. Say you have data coming from an outside source and you wanted to find a way to import that data directly into your code as say a React component or some data -- some transform data that you would use in your React components, right? So, we would -- we could potentially create here what I would call I guess what the ecosystem would call a virtual module, right? It's not a real file, but it's a module that represents something that can happen somewhere else in your system. Right? We use virtual modules in the Remix compiler for a small handful of things.

JASON: Okay.

CHANCE: If you've not used Remix, we have loaders and action functions that you export from routes. We use export functions to extract stuff you would need on the server and run it only for the server build. And run things from the client only on the browser build. Esbuild is really powerful for this sort of thing. For manipulating imports and turning them into some other form of code that you can actually execute. So, what I want to do here is I want to build a small esbuild plugin that is going to let us take this data and turn it into a React component.

JASON: Okay.

CHANCE: So, if we go into our scripts directory, you can see a few scripts. Plain old node scripts. Open a build, that's what we're interested in. In our build, we have a couple things. We're calling the build directly if we're executing it from the root. But let's take a look at the actual build function. Build is responsible. We've got some JavaScript -- I think below that we've got a CSS builder. But I want to build JSX. I want to look at the JavaScript builder. Down there. Yes. Build JS is calling esbuild. Build function takes care, you've got an entry point. And for reference, that entry point is relative to the directory that you execute the script. No this particular file. Since you're executing from the root, it's going to look at the source directory and spit out an out file in the public.disk.jsx. That's in the browser. If you look at that file, we talked about the simplicity of the CS build and the output. If you have seen a compiled Webpack thing, you've got a lot of stuff happening. This looks like there's a lot of stuff happening. But this is all React. Like all of -- like React is a big library. And so, like there's a lot of code. But it's still just -- it's readable JavaScript, right? Like this is just JavaScript stuff. And it's very like if you parse it and you get through like this is React. I think there's some comments even that will tell you, this is React and then this is so and so script, right? This is your entry point. But, you know, it's all just JavaScript. It's very readable JavaScript. They're not doing anything fancy here. See, there's React router. Yeah. There's our app code, right? Those are our transforms. React components. We're just calling create element, right? We don't get JSX in our output. That's what esbuild is really doing for you. Compiling it down into a single React file to consume in the browser. Done with this file. Go back to the builder in our build script. In our esbuild we want to add a plugin. Right after app file, go ahead and add the plugins key. And we are going to pass it an array. And inside this array, we don't have it yet. But we're gonna pass a function. That's our esbuild plugin, right? So, we want to go ahead -- let's go ahead and create a new plugin. Let's call it the Boop plugin. Just do a new file just to -- so, it's --

JASON: Okay.

CHANCE: Get a clean view to focus on that.

JASON: Where does one keep -- would you put it in here as like a plugin?

CHANCE: Throw it in scripts. Boop plugin. And you can create another directory for plugins if you want. We'll just do it here. So, let's go -- we'll need a few modules here because we're going to be working with the file system. So, go ahead and remember we're working in Node right now. So, this is a common JS. Let's import the file system module, FS, as well as the path module. And let's declare -- actually, yeah, let's go ahead and declare a plugin. Call it -- just make it a constant variable. We'll call it "Boop plugin." And should be an object. So, yeah, I said function, I think. But it's just gonna be an object. And look at -- you got Copilot going on. It's telling you exactly --

JASON: Copilot's killer.

CHANCE: What am I doing here? Just let Copilot write the code. I don't need to tell you anything. I just installed Copilot yesterday. And it scared the crap out of me. I'm gonna be out of a job in a year if this keeps getting smarter. It's so ridiculous. Yeah, that's what I would name the plugin, actually. Go ahead and hit tab and do the work. It knows the next step. Create an asynchronous function called build. The build key. It's an async function, take the config option. Good lord, this is scary. All right. So, inside of this function, let me see if Copilot knows what's next.

JASON: Doesn't know anything.

CHANCE: Come on!

JASON: Oh, look at it try now.

CHANCE: All right. Don't worry about all that. It's actually -- oh, my gosh, it's actually giving you my code from the final branch which tells you exactly what we're doing.

JASON: Does it really?

CHANCE: Yeah, that's terrifying. It's looking at the other branches in Git and pulling my code. Oh, my gosh. That is wild. Anyway, let's ignore that for now. So, we're gonna do a couple things. Call config. I would actually call it instead of config, let's rename it variable. That's confusing to me. What you get is the argument, it's not your config. It's actually the build itself. Just call it build. That is the DCR that's literally the build. And the function actually, I was wrong. I get too excited about Copilot. The function is called setup. You're setting up your plugin and what you get is the argument for that setup is the build itself, right?

JASON: Gotcha, gotcha.

CHANCE: Build is an object. It's got a few properties. And the properties are essentially hooks. They're hooks into the process, right? So, you've got a hook for onStart which fire when is esbuild starts the build process. I think some people get con us food with the onStart and it's an on End. And some get confused because thing onStart -- I said some people, that's me. I got confused.

JASON: It's me. I am some people.

CHANCE: Yeah. I thought onStart was firing when we first encountered it. OnStart and on end is triggered when it starts and ends. A lesson to all the developers, read your docs and you don't be confused like me. We have build.onStart, and build.onEnd. We're not going it use onStart. These are the life cycles you have available to you. We're going call onResolve. The first step to take to handle this on boop import is to resolve the import. That file doesn't exist. We didn't have an onBoop file. We need to resolve it. Use the resolver to manipulate to a file that does exist. Do something with can that, right. Inside onResolve, it's an object. It's like a config object. And by default whether you call onResolve, it's going resolve literally every single import you ever see in your app. We don't need that. We only care about the onBoops. And pass a filter key. It's going to be a regX. How to you verbally communicate a regX.

JASON: Fortunately I know enough about regX to handle that. What do you need to find?

CHANCE: Find any string that ends in dot boop.

JASON: A glob or a legit?

CHANCE: A regX. Just let Copilot do it and change CSS to boop. There you go. Boom! There it is.

JASON: Don't need it to be case sensitive.

CHANCE: No. Any dot boop. Don't worry about capital dot boops. That's a user problem. And now we pass to the callback function.

JASON: Okay.

CHANCE: Sorry, second argument to onResolve. You're still in keys on that object.

JASON: Oh, gotcha.

CHANCE: Pass just an asynchronous function. Yep. There you go. And inside -- so, we're gonna get some arguments in that. Just the argument, just say args. What we're gonna do in this async function is we're gonna return a new object. And this object is going to instruct us what we need to do with that import, right? So, we're going to pass it a -- what is the key? I think it's path. So, go ahead and pass it a path key. And the path is how we resolve this import, right? So, we're not -- we don't have a dot boop file. Go ahead and say path.resolve. Just let this guy do it. Copilot. It knows what's up. We need to resolve, dear name, dear name, underscore, underscore, dear name, dot, dot, back out, we need to go up a level, and we need to go into the boops directory. Say boops. And then lastly we're going to resolve the base name of our -- the actual import that we said. So, say call path.basename.

JASON: Path.basename.

CHANCE: And inside there, say args.path. It's the original path name we started with. And all we care about in that case is the base name. It's called corgi.json. Say path base name, args.path. And then plus JSON.

JASON: Okay. Just to kind of do a quick like understanding of how this would work. We do corgi.json. Or corgi.boop. And when you run path.baseName, the baseName is gonna be "Corgi," right? It leaves out the extension and the folders and all that?

CHANCE: It doesn't leave out the extension. You would need a second argument for that. Instead of a second argument, we're going to simplify it. After the base name function, dot replace. Replace it using the same regX. Dot boop is fine. I want to be clear there in case there's a .boop somewhere else. And say .JSON. That's what we've done. We've we constructed the path to where we live, right? We're almost done here. The second key in our return object is going to be a namespace. So, the namespace -- just so you know when you're writing these plugins and hooks, you can have multiple unresolved and hooks in the plugin. You plight want to do more than just transform dot boops, transform something else. It's related logic and you throw additional calls in there to deal it with. And the namespace allows us to differentiate between those hooks.

JASON: Gotcha.

CHANCE: Boop slash namespace. Needs to be a unique identifier. So, call it boop/namespace. I think you can use slashes. We're done with this hook. We have resolved our import. We know where the file is and we're done. But now we have a JSON file. We don't want the JSON. We want to use that JSON to give us a component. Remember in that --

JASON: Right.

CHANCE: We were trying to render a component. So, the next hook we're going to use is the onLoad hook. Look, Copilot was actually wrong. I love it. It's not perfect.

JASON: Now it's trying to copy me.

CHANCE: Now it's right, actually.

JASON: Oh, it's the same?

CHANCE: Yeah. It's the same.

JASON: Okay.

CHANCE: With one exception. Go ahead and close that function off real quick. One exception, in that options object, pass an additional argument. This one is called namespace. We use the same exact namespace here. We're loading what we just resolved, right?

JASON: Gotcha.

CHANCE: Resolved it here, load it down here. Again, if you have multiples of these things, it's important. We're only concerned with loading files in this namespace. And now we're gonna do some more work to tell it what to give us once we load this information. So, what we're gonna do here is we're just gonna construct a React component. We're going to -- go ahead and call -- actually, let's put this in a try cache. We're gonna do some file systems stuff. We might to want handle errors. We'll come back to the errors in a minute. Actually, let's just do the errors. In case we catch an error, we want to tell esbuild about the error.

JASON: We want to tell esbuild, all right.

CHANCE: Correct. Return an object here. And inside that object I think it takes a key called errors. Errors, plural. It's an array. And just say another object. And then text. And yeah. I think it's just error.message. I think that's what you're gonna use. Whatever the error message is, just pass it along to the text here. And now we've got some error handling that won't completely kill our build process. It will --

JASON: Gotcha. This is a built in behavior for all of the hooks. If I return an object with an errors array, it will pick that up and use it?

CHANCE: You can pass whatever errors you want. You can pass multiple errors. It's nice if you want to customize, I know I read the file system wrong. Instead of this nasty Node error, hey, back up in this plugin. We know where it happened. Human-readable error message if you wanted to. We're not doing that here. But you could. It's good for that, I think.

JASON: Cool.

CHANCE: The core logic here is we are going to take the path that we have resolved and turn it into a React component. First we need to, since we're dealing with JSON, we need to parse the contents of that file. So, go ahead and start a new variable. Just call it JSON. And then call JSON.parse. And what we're gonna parse is the result of the -- of reading the file systems. So, since this is async, let's go ahead and call await. Yeah, that's fine. Read asynchronous is fine. Doesn't matter. We're blocking either way. So, pass in args.path. Now we have -- this is the path from our resolve hook, right? This is --

JASON: What we -- so, what we've done is we have set up a chain. So --

CHANCE: Yes.

JASON: Our script, request corgi.boop. Esbuild says what do I do with corgi.boop? And our plugin says get corgi.json which is now in the on-lead.

CHANCE: Yes. Now args path is the resolve path constructed up above.

JASON: Gotcha.

CHANCE: Don't need to do it twice. I think the default coding is --

JASON: I think so. I drop it in anyways.

CHANCE: I always do it anyway. Because I don't memorize these things. And off of JSON, let's just destructure a couple things. We check our JSON. Remember, we had a source, an SRC and we had an alt, right? That's what we got in that JSON file. Now we've got those things. And let's construct a template literal. Going to create a React component, right? It's just a string. So, we insert a -- yeah. There we go. Component. And inside that, just write a React component. You need to import React. We're all used to React 17 telling us we don't have to import React anymore. I think there's a configuration option in esbuild to enforce that. But I didn't mess with it. Yeah, got React imported. That's great. Now just say export default React function. Yeah. There you go. And --

JASON: Actually, I'll go simpler. We'll just do one of these.

CHANCE: Yep. And we're gonna build an image. You don't even need to finish -- yeah, don't finish your -- there you go. There you go. You'll need your little dollar sign in front of those.

JASON: Right. Because we're doing the...

CHANCE: Yeah, you'll need to... well, you'll still need the -- well, no, you won't. Those are gonna be read as a string. You just need the quotes. There you go. Perfect. And that is pretty much it. You've got your content there, right?

JASON: All right.

CHANCE: We know what content is supposed to look like. And this -- so, when we load something from this file, instead of the file itself, this is the content we're expecting, right?

JASON: Gotcha.

CHANCE: Now we're gonna return an object. And that object is going to have a content's key. And the contents of this file --

JASON: Contents.

CHANCE: Yep, contents. And the contents is our component.

JASON: Okay.

CHANCE: And then we're gonna have a loader. So, the loader -- there are several different builds in loaders. You can actually build your own loader I believe for esbuild. But there are built-in loaders which know how to handle certain data types differently. So, in this case, we're loading JavaScript. Or actually loading JSX. There's a different here. Go ahead and say JSX. Use esbuild's built-in JSX loader. So, it knows when it sees our JSX what to do with it. And we're also gonna pass in this case a resolve directory. That's because this is going to be resolving from the core -- I think it's the boops folder. Because that's where it's encountered all that, right? It's resolving from there. We are gonna resolve it from the root. We need to import React from the root Node modules. Actually, I think it's the parent name, we need to go up a path. Because right now --

JASON: Yeah, yeah. So, we need --

CHANCE: Path dot resolve or join. I think it doesn't matter in this case?

JASON: TWD?

CHANCE: Go ahead and do that. Where we're calling it from is fine. Sorry, not path. It's process.cwd.

JASON: Okay. Why did this look wrong to me? Good.

CHANCE: That's your root. And I think we're done. Go ahead and save that. And then go back in your build. Let's go ahead and import this plugin.

JASON: Okay. So, I'm going to const boop plugin --

CHANCE: Did we export it? I don't think we exported it.

JASON: We didn't. So, wow. Wow. Jeez. Okay.

CHANCE: So, we're in common JS, remember, you have to do module.exports here.

JASON: Okay.

CHANCE: Yeah. So, now we can just -- now we can import it in our build. So, require. I think we just -- it's the default, actually. Because module.exports --

JASON: The exports is default. Man, I have not written common JS in a while.

CHANCE: Yeah. Every time I do it, I have to think twice about everything. All right. Cool. Now we're just gonna pop that into our plugins. There you go.

JASON: Like that?

CHANCE: Yeah. And as long as I didn't goof up on any of the instructions, I think if you restart your server. It might have restarted it already. It did not. Since it crashed, you'll have to restart it.

JASON: Wait, still doesn't like to. I have done something wrong. Do not know how to load path, boop slash namespace, corgi.json.

CHANCE: Keep going, keep going. What did we do wrong? Let's see here. Go back and inspect stuff. It doesn't know how to load. We built a loader. Let me look at the loader again. Our on-load. We're filtering correctly. That's what we should do. We're namespacing correctly. That's what we should do. I don't know if the slash is allowed in esbuild. Let's just change that to a dash just out of... I have no idea if this will make a difference, actually. But let's try it. Because I always use dashes. Yeah. That just makes me feel better.

JASON: Okay.

CHANCE: So, we got our arguments, we are getting our JSON. Let's double check and make sure we got that file path right. It should give us a more specific error, though. Because we're definitely resolving it. We're resolving from the current directory, going up a directory which is what we want because we're in the scripts directory now. We want to go back to the root. Yeah. When in doubt, log everything. Sometimes it -- sometimes it will fail before you try to log anything.

JASON: Boops, corgi JSON. So, that's correct. Yeah, there it is. I can click right into it.

CHANCE: Yeah. We definitely know that's right. Keep going down. Keep playing with it.

JASON: Okay.

CHANCE: That's --

JASON: So, we get it resolved. And then get into the loader. And once in the loader, try to read our file sync. So, let's... console log, our source and alt.

CHANCE: Yeah, I'm wondering if it didn't fail on the read file. Which I don't know why it would. Because that file should exist. Go back and actually log the file. Just to make sure for some reason that we did something wrong on our namespace. Like right above there. The args -- just log args.path. Anyone in the chat, let me know. This was not on my list of things to debug. Weird. it's not getting called. Did we call it build.onLoad? Did we name it right? I don't know that we're even encountering our loader, which is odd to me.

JASON: We're not.

CHANCE: The only reason --

JASON: Did I screw up the name, is it onLoad?

CHANCE: That's right, the boop filter.

JASON: It's a .JSON now, should the filter --

CHANCE: You're right! I didn't think about that. That's what happens when you let autopilot -- or Copilot do everything. Sloppiness here. We don't want to do this with every JSON file. Just the things that used to be boops. Just call it corgi.json. Just a straight, yeah, corgi.json. There grow. The reason I'm saying there is because I'm lazy. But in reality, you want to name it something specific with a key that you can recognize down there in your filter. But yeah, I think that's it. Good eye on that one. All right.

CHANCE: Boop plugin was triggered, but something else has gone wrong. Boop from the onload, same from the onLoad. Invalid option from onLoad, callback and plugin resolve.

CHANCE: process.cw...

JASON: Apparently we can't use process -- so, how did you do it?

CHANCE: I just constructed the path like up above with the root directory. Say path.resolve, and then dear name and then up one.

JASON: That's all?

CHANCE: I think so, yeah. Oh, god --

JASON: Still unhappy --

CHANCE: I was not prepared for all of these problems.

JASON: It's all right. This is the best part of the show when we have no idea what's going on and then we go and fix it. Okay. So, now it doesn't like new error text summary. Which means...

CHANCE: That's an internal esbuild error. So, what is that?

JASON: Invalid option. It doesn't like resolve as an option. Is it resolver?

CHANCE: Yeah, resolved -- capital DIR. I'm not good at spotting tie pose. Look at boop. Look at this little guy! We rendered an image that's the React component. It just has that data in there. This is -- obviously, this is a little silly. But it's very fun. That's why we do silly things. But it's a real -- you know, it is a real use case for building an esbuild plugin. You have some data somewhere. You want to do something with it in your JavaScript that isn't normal JavaScript. And so, we -- we take that data. We manipulate it, we resolve the path and build a component out of it. Like I said, there's lots of loads that are you get in esbuild in the CSS modules plugin we're building. We have a loader for the JSON to the original class name and the hash class name. And we have the CSS file that we have access to, we use the file for that. There's different loaders that give you different pieces of data and you use them for different things. In a nutshell, that's how you build an esbuild plugin.

JASON: What we did was firmly in the proof of concept camp.

CHANCE: Absolutely.

JASON: You can imagine, there are interesting things that you could do with this. I know, for example, that I have spent a lot of time writing out YAML files or JSON file where is we've got a bunch of data that's exactly the same. Like I'll give you a real world example. On my website, I have a JSON file that is articles I have written on other websites, right? So, my list of like lead Dev articles and CSS tricks articles and stuff like that. I have to keep those somewhere. And I don't know where to keep them. I put them in a JSON file. I import that, run a loop and build these things. So, I can see like a plugin like this would allow me to just import the article as a -- like, you know, just import it and use it as a component. And then use esbuild to do the plugin to like parse all that for me. Or even stuff that would be like proprietary things like if you need to scan for secrets or something. You could build the parser to look at any of your files and check to see if you like included something that resembles an API key and then failed if you did that. There's some really interesting stuff that you could do here that --

CHANCE: MDX is a great example. If you have a markdown file, MDX and you want to process that, and you can't do that with JavaScript. You can process it with MDX and process it and spit out an actual JavaScript file. That's what we do in Remix. We support MDX routes and use an esbuild plugin to support those.

JASON: Cool.

CHANCE: Any data type. Any shared data like that in some sort of configuration. You can take that and transform it however you need. X at least read it and then spit it out to your actual JavaScript components. So, you don't have to create a whole new component. You can just give the user some data, right? You can turn your YAML into JSON.

JASON: Right.

CHANCE: Now you have a JSON object you can use. There's loads of really cool examples like that.

JASON: I feel like this is a really, really powerful tool to have in your tool belt. Especially as we're seeing a lot of the ecosystems starting to my grade to esbuild as a default. If you see something using vite. It uses esbuild under the hood. If you're use the new create X app and it's got vite under the hood, esbuild is what's under the hood. I wish I could automate this and make it easier. Esbuild plugins might be the way that you that. I do think, though, this probably ought to come with a bit of a caveat which is the reason Webpack takes a long time to build is because people run 5 million plugins in Webpack. Webpack is not itself slow. As you start dreaming up ideas for esbuild. Bear in mind the tradeoffs. As you add more plugins, you add more work, which means that esbuild is gonna slow down. And that bloat is not esbuild's fault. We got to be careful that as we're adding tools that we're adding tools that add enough value to justify the time.

CHANCE: Sure. And there's a caveat to your caveat, by the way.

JASON: A caveat to the caveat.

CHANCE: I would argue you have to have a whole loft plugins doing a whole lot of work -- it's not just the number of plugins, it's what the plugins are doing. Remember, the plugins never go in and parse your AST, which is where the bulk of where a lot of compiler work happens.

JASON: Sure.

CHANCE: Even though we're running in JavaScript and running a little slower than the source Go code that's executing everything. First of all, we have the filter, a nice escape hatch. We're only dealing with these certain type of imports. We ignore everything else. So, with the filter, we get to really speed things up a lot too by just ignoring things so, yeah. So, like I'm not saying to go and build a gillion plugins. We do a fair bit of work in Remix and it's still super-fast. That's just how fast esbuild is. There's a lot of wiggle room there. And you can also use caching by the way so. There's a lot of work you can do to build caching. So, setting a cache key. You know, checking a store somewhere if you're doing anything too expensive to make sure nothing has changed. There's a lot of things you can do to optimize your plugins as well.

JASON: Yeah, yeah. Absolutely. And I feel like somebody asked if this would minify -- yeah, here it is. Asked, is the code further processed by esbuild? Will it be minified? And I looked at build to see and it looks like it certainly would be.

CHANCE: There are options. Esbuild has a bunch of different options, and you can minify the code. By default it does it. We used a very minimal esbuild configuration. It does that. It uses love transforms out of the box. Don't quote me on this, I think they support browsers list and stuff like that. You don't have to worry about configuring your .env or whatever. What did we do back in the day? You had to like install it 5 million Babel plugins and then individually what future features to support? We don't do that anymore. It -- esbuild just does that. So, it's already back -- it's gonna transform your code back a few years and make sure the browsers can use that.

JASON: If you don't want to do that, you can write a browsers list config. I only want to support the latest browser. And then do a lot less transpilation then.

CHANCE: If they support the browsers --

JASON: I think I read. Maybe don't quote us on that, chat. But I'm pretty sure it's in here. But yeah. You can find all those details in here. It's under the API somewhere. But yeah. There's a lot -- a lot to love about esbuild. It's a very exciting project. I feel like it has been part of a new wave of really exciting stuff going on in the JavaScript ecosystem. Chance, I know you -- you wanted to keep it to an hour today. Which means that we are out of time. So, where should people go if they want to learn more?

CHANCE: Twitter is a good place. I yap my mouth on Twitter a fair bit. So, you can follow me there. Chancethedev on Twitter. You can join the Remix Discord channel, Remix.run and there is a link at the bottom to join our Discord channel, I believe. And a lot of good stuff happening there. You can ping me on Discord. If you want to never, ever get in touch with me, you can send me a message on LinkedIn. I'll never read it. Yeah. That's about it, I think. Otherwise, just catch me around the Internets and try Remix. It's a lot of fun. I think you'll enjoy it.

JASON: Absolutely. All right, y'all. Well, let's do one more quick shoutout. We've had Amanda here from White Coat Captioning here all day doing the live captioning for us. Thank you very much, Amanda. That's made possible through contributions from our sponsors, Netlify, NX and Backlight, all making this show what it is today through financial support. Thank you very much for that. While you are on the site, go and check out the schedule. We've got a lot of really good stuff coming up. We are going to build a dynamic image with Node canvas. And then I've got more episodes that I haven't put up on the site yet. I apparently need to get them up today because we're just about to the end of the list that's on the website now.

CHANCE: Have you been busy this week?

JASON: It's been a bit of a week. But let's... let's do this as well, chat. Who do you want to see on the show, right? And I'm gonna shut this down. So, Tweet at me. Go tag your favorite creators and tell them you want to see them on the show so that we can get more -- more of these episodes lined up. Also, let me know what you want to learn about. Because there's a lot going on out there. I love learning new things, it's always fun to see what people are building and innovating on. Hit me up. What do you want to learn? I want to learn everything. Got to focus. With that, I think we're gonna call this one a success. I had a lot of fun. I feel like I have a much better understanding of how esbuild is working under the hood and what we can do to extend it a bit. Chance, thank you so much for spending some time with us today. Do you have any parting words for the chat?

CHANCE: No. The chat was great. I was following along on another tab, watching on Twitch as folks were enjoying dropping the boops on. This is a great show. Always love watching it. Really awesome to be a part. Thank you, Jason. Appreciate it.

CHANCE: Thank you, Chance. We are going to find somebody to raid. We will see you all next time.

CHANCE: Peace!

Learn With Jason is made possible by our sponsors: