CINXE.COM
esbuild - Plugins
<!DOCTYPE html><html lang="en"><head><meta charset="utf8"><title>esbuild - Plugins</title><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta property="og:title" content="esbuild - Plugins"/><meta property="og:type" content="website"/><style>html:not([data-theme=dark]){--bg-grad: rgba(255, 255, 255, 0) 0%, #fff 25%, #fff 75%, rgba(255, 255, 255, 0) 100%;--bg: #fff;--dark: none;--fg-on: #000;--fg: #222;--lite: block;--menu-bg: #fff;--menu-fg: #222;--menu-a: .25;--pre-dim: #777;--pre-val: #870;--pre: #222;--svg: #444;--td: #ddd;--tr: #f7f7f7;--warn-info: #222}html[data-theme=dark]{color-scheme:dark;--bg-grad: rgba(25, 25, 25, 0) 0%, #191919 25%, #191919 75%, rgba(25, 25, 25, 0) 100%;--bg: #191919;--dark: block;--fg-on: #ddd;--fg: #aaa;--lite: none;--menu-bg: #222;--menu-fg: #aaa;--menu-a: .65;--pre-dim: #999;--pre-val: #cb8;--pre: #ccc;--svg: #aaa;--td: #333;--tr: #151515;--warn-info: #ddd}@media (prefers-color-scheme: dark){html:not([data-theme=light]){color-scheme:dark;--bg-grad: rgba(25, 25, 25, 0) 0%, #191919 25%, #191919 75%, rgba(25, 25, 25, 0) 100%;--bg: #191919;--dark: block;--fg-on: #ddd;--fg: #aaa;--lite: none;--menu-bg: #222;--menu-fg: #aaa;--menu-a: .65;--pre-dim: #999;--pre-val: #cb8;--pre: #ccc;--svg: #aaa;--td: #333;--tr: #151515;--warn-info: #ddd}}body{font:16px/140% sans-serif}a{color:inherit}b,strong{color:var(--fg-on)}footer{font-size:80%;line-height:140%;font-style:italic}sup{vertical-align:top;font-size:70%;line-height:100%;font-style:normal}footer>sup[id]{position:relative;padding-top:20px}code{font-family:Noto Sans Mono,monospace;background:rgba(159,159,159,.15);padding:1px 4px;margin:-1px 0;border-radius:5px;white-space:nowrap}.dl{display:block;width:22px;height:22px}.dl:after{content:"";display:block;width:22px;height:22px;mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center}@-moz-document url-prefix(){code{white-space:pre-wrap}}@keyframes scale-bar{0%{transform:scaleX(.0000001)}to{transform:scale(1)}}.logo{display:inline-block;font-size:200%;font-weight:700;margin-top:10px;margin-bottom:10px;line-height:40px;padding-left:50px;background-image:url(/favicon.svg);background-repeat:no-repeat;background-size:40px 40px}nav a{text-decoration:none}nav a:hover{text-decoration:underline}nav ul{margin:0;padding:0}nav li{list-style:none}#menu>ul>li{padding-top:20px}nav li.current{font-weight:700}nav ul.h2{padding-left:10px;font-size:80%;line-height:150%}nav ul.h3{padding:0 0 0 10px}nav li.current>ul.h3{font-weight:400}#icons a{display:inline-block}body:not(.has-js) #theme{display:none}.index h1{text-align:center}.index blockquote{text-align:center;font-style:italic;margin:0 0 100px}.bench a{text-decoration:none}.bench a:hover{text-decoration:underline}figure{margin:60px auto}figcaption{font-weight:700;text-align:center;margin:40px 0 -40px}figure+figcaption{font-weight:400;text-align:left;margin:-40px auto 60px;font-size:13px;line-height:140%;font-style:italic;max-width:800px;padding:0 15%;box-sizing:border-box}h1{margin:10px 0 20px;font-size:200%;line-height:40px}h2{font-size:160%;border-bottom:2px solid #FFCF00;padding-top:63px;line-height:40px}h3{font-size:140%;padding-top:67px;line-height:32px}h4{font-size:100%;padding-top:70px;line-height:26px}h2,h3,h4{position:relative;margin:-10px 0 20px}h2 .permalink,h3 .permalink,h4 .permalink{position:absolute;width:25px;left:-25px;text-align:center;text-decoration:none;opacity:50%;-khtml-user-select:none;-moz-user-select:-moz-none;-ms-user-select:none;user-select:none;-webkit-user-select:none}h2:not(:hover) .permalink:not(:focus),h3:not(:hover) .permalink:not(:focus),h4:not(:hover) .permalink:not(:focus){color:transparent}p{margin:20px 0}.warning{border:1px solid #a90;border-radius:10px;padding:20px 20px 20px 50px;background-color:rgba(255,223,0,.2)}.warning:before{content:"!";font:700 43px/43px serif;margin-left:-30px;display:block;float:left}.info{border:1px solid var(--fg-on);border-radius:10px;padding:20px 20px 20px 60px}.info:before{content:"\24d8";font:700 43px/43px serif;margin-left:-43px;display:block;float:left}table{font-size:inherit;margin:30px auto;border-collapse:collapse;display:block;width:max-content;max-width:100%;overflow-x:auto}td,th{padding:7px 10px;white-space:nowrap;vertical-align:top}pre{position:relative;font:14px/140% Noto Sans Mono,monospace;background:rgba(127,127,127,.1);border:1px solid rgba(127,127,127,.5);border-radius:10px;padding:8px 10px;margin:30px 0;overflow-x:auto}pre:before{position:absolute;display:block;right:0;top:0;padding:5px 8px;font:12px/140% sans-serif}pre.cli3:before{content:"CLI"}pre.js3:before,pre.js2:before{content:"JS"}pre.go3:before,pre.go2:before{content:"Go"}pre.unix2:before{content:"Unix"}pre.windows2:before{content:"Windows"}.has-js pre.switchable{margin-top:0;display:none;border-top-right-radius:0}body[data-mode3=cli] pre.cli3,body[data-mode3=js] pre.js3,body[data-mode2=js] pre.js2,body[data-mode3=go] pre.go3,body[data-mode2=go] pre.go2,body[data-os2=unix] pre.unix2,body[data-os2=windows] pre.windows2{display:block}.has-js pre:before{display:none}.color-bold{font-weight:700;color:var(--fg-on)}.color-underline{text-decoration:underline;color:var(--fg-on)}.bg-red{background:#e24834}.color-red{color:#e24834}.bg-yellow{background:#f2d42d}.color-yellow{color:#f2d42d}.color-green{color:#58a549}.color-black{color:#000}.color-white{color:#fff}.color-dim{color:#777}.repl-in{margin-left:15px;display:block}.repl-in:before{position:absolute;display:block;left:10px;color:#7f7f7f}.cli1 .repl-in:before,.unix1 .repl-in:before,.cli2 .repl-in:before,.unix2 .repl-in:before,.cli3 .repl-in:before,.unix3 .repl-in:before{content:"$"}.windows1 .repl-in:before,.windows2 .repl-in:before,.windows3 .repl-in:before{content:">"}.js1 .repl-in:before,.js2 .repl-in:before,.js3 .repl-in:before{content:"";display:block;width:16px;height:20px}.repl-out{color:#7f7f7f}.repl-in+.repl-in,.repl-out+.repl-in{margin-top:20px}.switcher{display:none}.has-js .switcher{display:flex;justify-content:flex-end}.switcher a{background:rgba(127,127,127,.1);border:1px solid rgba(127,127,127,.5);border-bottom:none;border-right:none;text-decoration:none;-khtml-user-select:none;-moz-user-select:-moz-none;-ms-user-select:none;user-select:none;-webkit-user-select:none;padding:5px 8px;font:12px/140% sans-serif}.switcher a:first-child{border-top-left-radius:10px}.switcher a:last-child{border-top-right-radius:10px;border-right:1px solid rgba(127,127,127,.5)}body[data-mode3=cli] .switcher .cli3,body[data-mode3=js] .switcher .js3,body[data-mode2=js] .switcher .js2,body[data-mode3=go] .switcher .go3,body[data-mode2=go] .switcher .go2,body[data-os2=unix] .switcher .unix2,body[data-os2=windows] .switcher .windows2{background:rgba(127,127,127,.25)}body{margin:0;padding:30px}nav{margin-bottom:50px}#menubar{display:none;position:fixed;left:-20px;top:0;right:-20px;height:50px;padding:0 20px;z-index:2}#menutoggle{display:block;width:50px;height:50px}body.has-js{padding-top:60px}.has-js #menubar{display:block}.has-js #shadow{position:fixed;left:0;top:0;right:0;bottom:0;z-index:3;background:rgba(0,0,0,0);visibility:hidden;transition:visibility .25s,background-color .25s}.has-js .open #shadow{background:rgba(0,0,0,.5);visibility:visible}.has-js #menu{position:fixed;left:0;top:0;bottom:0;width:220px;box-sizing:border-box;overflow-y:auto;padding:30px 0 0 30px;z-index:4;transition:transform .25s;transform:translate(-220px);box-shadow:none}.has-js .open #menu{transform:translate(0);box-shadow:0 0 20px rgba(0,0,0,.5)}#icons{position:static;margin:20px 0 20px 50px}.index h1{font-size:75px;line-height:40px}.index blockquote{font-size:16px}.available-options{margin-top:50px;display:flex;flex-flow:wrap;flex-direction:row}.option-group{padding-right:20px}.option-group ul{margin:0;padding:0 0 0 20px}@media (min-width: 800px){body,body.has-js{position:relative;max-width:1000px;margin:0 auto;padding:50px 50px 500px 250px}.index h1{font-size:100px;line-height:70px}.index blockquote{font-size:21px}main{position:relative;z-index:1}.has-js #menubar,.has-js #shadow{display:none}nav,.has-js nav{position:fixed;left:0;right:0;top:0;bottom:0;max-width:1300px;margin:0 auto;display:block}#menu,.has-js #menu,.has-js .open #menu{position:absolute;left:0;top:0;bottom:0;padding:50px 0 30px 30px;overflow-y:auto;transform:none;box-shadow:none}pre{margin-left:30px;margin-right:30px}li pre{margin-left:0;margin-right:0}.switcher{margin-right:30px}.available-options{justify-content:center}}body,#menu,nav ul ul{background:var(--bg);color:var(--fg)}#menubar{box-shadow:0 0 10px rgba(0,0,0,var(--menu-a));background:var(--menu-bg);color:var(--menu-fg)}.progress{background:linear-gradient(to right,var(--bg-grad))}path,rect,circle{fill:var(--svg);stroke:var(--svg)}h1,h2,h3,.logo,nav ul,nav li.current{color:var(--fg-on)}a:hover path,a:hover rect,a:hover circle{fill:var(--fg-on);stroke:var(--fg-on)}pre,.hljs-tag .hljs-string{color:var(--pre)}.hljs-comment,.hljs-keyword,.hljs-literal{color:var(--pre-dim)}.hljs-regexp,.hljs-string,.hljs-number,.hljs-tag{color:var(--pre-val)}#theme-dark{display:var(--dark)}#theme-light{display:var(--lite)}td,th{border:1px solid var(--td)}tr:hover{background:var(--tr)}.warning,.info{color:var(--warn-info)}.dl:after{background:var(--fg)}.dl:hover:after{background:var(--fg-on)}.js2 .repl-in:before,.js3 .repl-in:before{background-image:url('data:image/svg+xml,<svg width="16" height="20" xmlns="http://www.w3.org/2000/svg"><path d="M2 5.5L6 9.5 2 13.5" stroke-width="1.5" fill="none" stroke="%237f7f7f"/></svg>')}.dl:after{mask-image:url('data:image/svg+xml,<svg width="22" height="22" xmlns="http://www.w3.org/2000/svg"><path d="M11 2V15M4 8L11.7071 15.7071M18 8L10.2929 15.7071M2 19H20" stroke="black" stroke-width="2"/></svg>');-webkit-mask-image:url('data:image/svg+xml,<svg width="22" height="22" xmlns="http://www.w3.org/2000/svg"><path d="M11 2V15M4 8L11.7071 15.7071M18 8L10.2929 15.7071M2 19H20" stroke="black" stroke-width="2"/></svg>')}</style><meta name="viewport" content="width=device-width, initial-scale=1"></head><body><script>(function(){var s=document,L=s.body,r=s.documentElement,c=r.dataset,l=L.dataset,i=s.getElementById.bind(s),T=navigator.platform==="Win32"?"windows":"unix";try{l.mode3=localStorage.getItem("mode3")||"cli",l.mode2=localStorage.getItem("mode2")||"js",l.os2=localStorage.getItem("os2")||T,c.theme=localStorage.getItem("theme")}catch(t){l.mode3="cli",l.mode2="js",l.os2=T,c.theme=null}L.classList.add("has-js"),addEventListener("click",function(t){if(t.target.parentElement&&t.target.parentElement.className==="switcher"){var o=t.target.className;if(/^(cli|js|go)[23]$/.test(o)){for(var m=t.target.offsetTop-r.scrollTop,d=o==="cli3"?3:2;d<=3;d++)l["mode"+d]=o.slice(0,-1),localStorage.setItem("mode"+d,o.slice(0,-1));var h=t.target.offsetTop-r.scrollTop;r.scrollTop+=h-m}if(/^(unix|windows)2$/.test(o)){var m=t.target.offsetTop-r.scrollTop;l.os2=o.slice(0,-1),localStorage.setItem("os2",o.slice(0,-1));var h=t.target.offsetTop-r.scrollTop;r.scrollTop+=h-m}}}),addEventListener("DOMContentLoaded",function(){function t(){for(var a,u,g,n=0;n<v.length;n++){var e=v[n];if(e.getBoundingClientRect().top>10)break;e.tagName==="H2"?(a=e,u=g=null):e.tagName==="H3"?(u=e,g=null):g=e}for(var n=0;n<v.length;n++){var e=v[n];e.tagName!=="H4"&&i("nav-"+e.id).classList.toggle("current",e===(u||a))}var e=g||u||a;f=location.pathname+(e?"#"+e.id:""),p===null&&(p=setTimeout(o,300))}function o(){p=null,location.pathname+location.hash!==f&&history.replaceState(null,"",f)}var m=i("menutoggle"),d=i("shadow"),h=i("menu"),S=s.querySelector("nav");m.addEventListener("click",function(){h.scrollTop=0,S.classList.add("open")}),d.addEventListener("click",function(a){S.classList.remove("open")});var f,p=null,v=s.querySelectorAll("h2,h3,h4");addEventListener("scroll",t,{passive:!0}),addEventListener("load",t);function y(){return E.matches?"light":"dark"}function I(a){localStorage.setItem("theme",a),c.theme=a}addEventListener("storage",function(){c.theme=localStorage.getItem("theme")}),i("theme").addEventListener("click",function(){var a=y();I(c.theme===a?null:a)});var E=matchMedia("(prefers-color-scheme: dark)");function k(){c.theme!==y()&&I(null)}try{E.addEventListener("change",k)}catch(a){E.addListener(k)}})})();</script><div id="menubar"><a id="menutoggle" href="javascript:void 0" aria-label="Toggle the menu"><svg width="50" height="50" xmlns="http://www.w3.org/2000/svg"><rect x="15" y="18" width="20" height="2" stroke-width="0"></rect><rect x="15" y="24" width="20" height="2" stroke-width="0"></rect><rect x="15" y="30" width="20" height="2" stroke-width="0"></rect></svg></a></div><nav><div id="shadow"></div><div id="menu"><a href="/" class="logo">esbuild</a><ul><li><a href="/try/">Try in the browser</a></li><li><a href="/getting-started/">Getting Started</a><ul class="h2"><li><a href="/getting-started/#install-esbuild">Install esbuild</a></li><li><a href="/getting-started/#your-first-bundle">Your first bundle</a></li><li><a href="/getting-started/#build-scripts">Build scripts</a></li><li><a href="/getting-started/#bundling-for-the-browser">Bundling for the browser</a></li><li><a href="/getting-started/#bundling-for-node">Bundling for node</a></li><li><a href="/getting-started/#simultaneous-platforms">Simultaneous platforms</a></li><li><a href="/getting-started/#yarn-pnp">Using Yarn Plug'n'Play</a></li><li><a href="/getting-started/#other-ways-to-install">Other ways to install</a></li></ul></li><li><a href="/api/">API</a><ul class="h2"><li><a href="/api/#overview">Overview</a></li><li><a href="/api/#general-options">General options</a></li><li><a href="/api/#input">Input</a></li><li><a href="/api/#output-contents">Output contents</a></li><li><a href="/api/#output-location">Output location</a></li><li><a href="/api/#path-resolution">Path resolution</a></li><li><a href="/api/#transformation">Transformation</a></li><li><a href="/api/#optimization">Optimization</a></li><li><a href="/api/#source-maps">Source maps</a></li><li><a href="/api/#build-metadata">Build metadata</a></li><li><a href="/api/#logging">Logging</a></li></ul></li><li><a href="/content-types/">Content Types</a><ul class="h2"><li><a href="/content-types/#javascript">JavaScript</a></li><li><a href="/content-types/#typescript">TypeScript</a></li><li><a href="/content-types/#jsx">JSX</a></li><li><a href="/content-types/#json">JSON</a></li><li><a href="/content-types/#css">CSS</a></li><li><a href="/content-types/#text">Text</a></li><li><a href="/content-types/#binary">Binary</a></li><li><a href="/content-types/#base64">Base64</a></li><li><a href="/content-types/#data-url">Data URL</a></li><li><a href="/content-types/#external-file">External file</a></li><li><a href="/content-types/#empty-file">Empty file</a></li></ul></li><li><a href="/plugins/">Plugins</a><ul class="h2"><li id="nav-finding-plugins"><a href="#finding-plugins">Finding plugins</a></li><li id="nav-using-plugins"><a href="#using-plugins">Using plugins</a></li><li id="nav-concepts"><a href="#concepts">Concepts</a><ul class="h3"><li id="nav-namespaces"><a href="#namespaces">Namespaces</a></li><li id="nav-filters"><a href="#filters">Filters</a></li></ul></li><li id="nav-on-resolve"><a href="#on-resolve">On-resolve callbacks</a><ul class="h3"><li id="nav-on-resolve-options"><a href="#on-resolve-options">On-resolve options</a></li><li id="nav-on-resolve-arguments"><a href="#on-resolve-arguments">On-resolve arguments</a></li><li id="nav-on-resolve-results"><a href="#on-resolve-results">On-resolve results</a></li></ul></li><li id="nav-on-load"><a href="#on-load">On-load callbacks</a><ul class="h3"><li id="nav-on-load-options"><a href="#on-load-options">On-load options</a></li><li id="nav-on-load-arguments"><a href="#on-load-arguments">On-load arguments</a></li><li id="nav-on-load-results"><a href="#on-load-results">On-load results</a></li><li id="nav-caching-your-plugin"><a href="#caching-your-plugin">Caching your plugin</a></li></ul></li><li id="nav-on-start"><a href="#on-start">On-start callbacks</a></li><li id="nav-on-end"><a href="#on-end">On-end callbacks</a></li><li id="nav-on-dispose"><a href="#on-dispose">On-dispose callbacks</a></li><li id="nav-build-options"><a href="#build-options">Accessing build options</a></li><li id="nav-resolve"><a href="#resolve">Resolving paths</a><ul class="h3"><li id="nav-resolve-options"><a href="#resolve-options">Resolve options</a></li><li id="nav-resolve-results"><a href="#resolve-results">Resolve results</a></li></ul></li><li id="nav-example-plugins"><a href="#example-plugins">Example plugins</a><ul class="h3"><li id="nav-http-plugin"><a href="#http-plugin">HTTP plugin</a></li><li id="nav-webassembly-plugin"><a href="#webassembly-plugin">WebAssembly plugin</a></li><li id="nav-svelte-plugin"><a href="#svelte-plugin">Svelte plugin</a></li></ul></li><li id="nav-plugin-api-limitations"><a href="#plugin-api-limitations">Plugin API limitations</a></li></ul></li><li><a href="/faq/">FAQ</a><ul class="h2"><li><a href="/faq/#why-is-esbuild-fast">Why is esbuild fast?</a></li><li><a href="/faq/#benchmark-details">Benchmark details</a></li><li><a href="/faq/#upcoming-roadmap">Upcoming roadmap</a></li><li><a href="/faq/#production-readiness">Production readiness</a></li><li><a href="/faq/#anti-virus-software">Anti-virus software</a></li><li><a href="/faq/#old-go-version">Outdated version of Go</a></li><li><a href="/faq/#minified-newlines">Minified newlines</a></li><li><a href="/faq/#top-level-name-collisions">Avoiding name collisions</a></li><li><a href="/faq/#top-level-var">Top-level <code>var</code></a></li></ul></li><li><a href="/analyze/">Bundle Size Analyzer</a></li></ul><div id="icons"><a href="https://github.com/evanw/esbuild" aria-label="View this project on GitHub"><svg xmlns="http://www.w3.org/2000/svg" width="25" height="25"><path fill-rule="evenodd" stroke-width="0" d="M13 5a8 8 0 00-2.53 15.59c.4.07.55-.17.55-.38l-.01-1.49c-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82a7.42 7.42 0 014 0c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48l-.01 2.2c0 .21.15.46.55.38A8.01 8.01 0 0021 13a8 8 0 00-8-8z"></path></svg></a><a href="javascript:void 0" id="theme" aria-label="Toggle dark mode"><svg id="theme-light" width="25" height="25" xmlns="http://www.w3.org/2000/svg"><path d="M13.5 4v3m9.5 6.5h-3M13.5 23v-3M7 13.5H4M9 9L7 7m13 0l-2 2m2 11l-2-2M7 20l2-2"></path><circle cx="13.5" cy="13.5" r="4.5" stroke-width="0"></circle></svg><svg id="theme-dark" width="25" height="25" xmlns="http://www.w3.org/2000/svg"><path d="M10.1 6.6a8.08 8.08 0 00.24 11.06 8.08 8.08 0 0011.06.24c-6.46.9-12.2-4.84-11.3-11.3z" stroke-width="0"></path></svg></a></div></div></nav><main><h1>Plugins</h1><p>The plugin API allows you to inject code into various parts of the build process. Unlike the rest of the API, it's not available from the command line. You must write either JavaScript or Go code to use the plugin API. Plugins can also only be used with the <a href="/api/#build">build</a> API, not with the <a href="/api/#transform">transform</a> API.</p><h2 id="finding-plugins"><a class="permalink" href="#finding-plugins">#</a>Finding plugins</h2><p>If you're looking for an existing esbuild plugin, you should check out the <a href="https://github.com/esbuild/community-plugins">list of existing esbuild plugins</a>. Plugins on this list have been deliberately added by the author and are intended to be used by others in the esbuild community.</p><p>If you want to share your esbuild plugin, you should:</p><ol><li><a href="https://docs.npmjs.com/creating-and-publishing-unscoped-public-packages">Publish it to npm</a> so others can install it.</li><li>Add it to the <a href="https://github.com/esbuild/community-plugins">list of existing esbuild plugins</a> so others can find it.</li></ol><h2 id="using-plugins"><a class="permalink" href="#using-plugins">#</a>Using plugins</h2><p>An esbuild plugin is an object with a <code>name</code> and a <code>setup</code> function. They are passed in an array to the <a href="/api/#build">build</a> API call. The <code>setup</code> function is run once for each build API call.</p><p>Here's a simple plugin example that allows you to import the current environment variables at build time:</p><div class="switcher"> <a href="javascript:void 0" class="js2">JS</a> <a href="javascript:void 0" class="go2">Go</a> </div> <pre class="switchable js2"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> esbuild <span class="hljs-keyword">from</span> <span class="hljs-string">'esbuild'</span> <span class="hljs-keyword">let</span> envPlugin = { <span class="hljs-attr">name</span>: <span class="hljs-string">'env'</span>, <span class="hljs-function"><span class="hljs-title">setup</span>(<span class="hljs-params">build</span>)</span> { <span class="hljs-comment">// Intercept import paths called "env" so esbuild doesn't attempt</span> <span class="hljs-comment">// to map them to a file system location. Tag them with the "env-ns"</span> <span class="hljs-comment">// namespace to reserve them for this plugin.</span> build.onResolve({ <span class="hljs-attr">filter</span>: <span class="hljs-regexp">/^env$/</span> }, <span class="hljs-function"><span class="hljs-params">args</span> =></span> ({ <span class="hljs-attr">path</span>: args.path, <span class="hljs-attr">namespace</span>: <span class="hljs-string">'env-ns'</span>, })) <span class="hljs-comment">// Load paths tagged with the "env-ns" namespace and behave as if</span> <span class="hljs-comment">// they point to a JSON file containing the environment variables.</span> build.onLoad({ <span class="hljs-attr">filter</span>: <span class="hljs-regexp">/.*/</span>, namespace: <span class="hljs-string">'env-ns'</span> }, <span class="hljs-function">() =></span> ({ <span class="hljs-attr">contents</span>: <span class="hljs-built_in">JSON</span>.stringify(process.env), <span class="hljs-attr">loader</span>: <span class="hljs-string">'json'</span>, })) }, } <span class="hljs-keyword">await</span> esbuild.build({ <span class="hljs-attr">entryPoints</span>: [<span class="hljs-string">'app.js'</span>], <span class="hljs-attr">bundle</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">outfile</span>: <span class="hljs-string">'out.js'</span>, <span class="hljs-attr">plugins</span>: [envPlugin], }) </pre> <pre class="switchable go2"><span class="hljs-keyword">package</span> main <span class="hljs-keyword">import</span> <span class="hljs-string">"encoding/json"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"os"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"strings"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"github.com/evanw/esbuild/pkg/api"</span> <span class="hljs-keyword">var</span> envPlugin = api.Plugin{ Name: <span class="hljs-string">"env"</span>, Setup: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(build api.PluginBuild)</span></span> { <span class="hljs-comment">// Intercept import paths called "env" so esbuild doesn't attempt</span> <span class="hljs-comment">// to map them to a file system location. Tag them with the "env-ns"</span> <span class="hljs-comment">// namespace to reserve them for this plugin.</span> build.OnResolve(api.OnResolveOptions{Filter: <span class="hljs-string">`^env$`</span>}, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(args api.OnResolveArgs)</span> <span class="hljs-params">(api.OnResolveResult, error)</span></span> { <span class="hljs-keyword">return</span> api.OnResolveResult{ Path: args.Path, Namespace: <span class="hljs-string">"env-ns"</span>, }, <span class="hljs-literal">nil</span> }) <span class="hljs-comment">// Load paths tagged with the "env-ns" namespace and behave as if</span> <span class="hljs-comment">// they point to a JSON file containing the environment variables.</span> build.OnLoad(api.OnLoadOptions{Filter: <span class="hljs-string">`.*`</span>, Namespace: <span class="hljs-string">"env-ns"</span>}, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(args api.OnLoadArgs)</span> <span class="hljs-params">(api.OnLoadResult, error)</span></span> { mappings := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>) <span class="hljs-keyword">for</span> _, item := <span class="hljs-keyword">range</span> os.Environ() { <span class="hljs-keyword">if</span> equals := strings.IndexByte(item, <span class="hljs-string">'='</span>); equals != <span class="hljs-number">-1</span> { mappings[item[:equals]] = item[equals+<span class="hljs-number">1</span>:] } } bytes, err := json.Marshal(mappings) <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> api.OnLoadResult{}, err } contents := <span class="hljs-keyword">string</span>(bytes) <span class="hljs-keyword">return</span> api.OnLoadResult{ Contents: &contents, Loader: api.LoaderJSON, }, <span class="hljs-literal">nil</span> }) }, } <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> { result := api.Build(api.BuildOptions{ EntryPoints: []<span class="hljs-keyword">string</span>{<span class="hljs-string">"app.js"</span>}, Bundle: <span class="hljs-literal">true</span>, Outfile: <span class="hljs-string">"out.js"</span>, Plugins: []api.Plugin{envPlugin}, Write: <span class="hljs-literal">true</span>, }) <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(result.Errors) > <span class="hljs-number">0</span> { os.Exit(<span class="hljs-number">1</span>) } } </pre><p>You would use it like this:</p><pre><span class="hljs-keyword">import</span> { PATH } <span class="hljs-keyword">from</span> <span class="hljs-string">'env'</span> <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`PATH is <span class="hljs-subst">${PATH}</span>`</span>)</pre><h2 id="concepts"><a class="permalink" href="#concepts">#</a>Concepts</h2><p>Writing a plugin for esbuild works a little differently than writing a plugin for other bundlers. The concepts below are important to understand before developing your plugin:</p><h3 id="namespaces"><a class="permalink" href="#namespaces">#</a>Namespaces</h3><p>Every module has an associated namespace. By default esbuild operates in the <code>file</code> namespace, which corresponds to files on the file system. But esbuild can also handle "virtual" modules that don't have a corresponding location on the file system. One case when this happens is when a module is provided using <a href="/api/#stdin">stdin</a>.</p><p>Plugins can be used to create virtual modules. Virtual modules usually use a namespace other than <code>file</code> to distinguish them from file system modules. Usually the namespace is specific to the plugin that created them. For example, the sample <a href="#http-plugin">HTTP plugin</a> below uses the <code>http-url</code> namespace for downloaded files.</p><h3 id="filters"><a class="permalink" href="#filters">#</a>Filters</h3><p>Every callback must provide a regular expression as a filter. This is used by esbuild to skip calling the callback when the path doesn't match its filter, which is done for performance. Calling from esbuild's highly-parallel internals into single-threaded JavaScript code is expensive and should be avoided whenever possible for maximum speed.</p><p>You should try to use the filter regular expression instead of using JavaScript code for filtering whenever you can. This is faster because the regular expression is evaluated inside of esbuild without calling out to JavaScript at all. For example, the sample <a href="#http-plugin">HTTP plugin</a> below uses a filter of <code>^https?://</code> to ensure that the performance overhead of running the plugin is only incurred for paths that start with <code>http://</code> or <code>https://</code>.</p><p>The allowed regular expression syntax is the syntax supported by Go's <a href="https://pkg.go.dev/regexp/">regular expression engine</a>. This is slightly different than JavaScript. Specifically, look-ahead, look-behind, and backreferences are not supported. Go's regular expression engine is designed to avoid the catastrophic exponential-time worst case performance issues that can affect JavaScript regular expressions.</p><p>Note that namespaces can also be used for filtering. Callbacks must provide a filter regular expression but can optionally also provide a namespace to further restrict what paths are matched. This can be useful for "remembering" where a virtual module came from. Keep in mind that namespaces are matched using an exact string equality test instead of a regular expression, so unlike module paths they are not intended for storing arbitrary data.</p><h2 id="on-resolve"><a class="permalink" href="#on-resolve">#</a>On-resolve callbacks</h2><p>A callback added using <code>onResolve</code> will be run on each import path in each module that esbuild builds. The callback can customize how esbuild does path resolution. For example, it can intercept import paths and redirect them somewhere else. It can also mark paths as external. Here is an example:</p><div class="switcher"> <a href="javascript:void 0" class="js2">JS</a> <a href="javascript:void 0" class="go2">Go</a> </div> <pre class="switchable js2"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> esbuild <span class="hljs-keyword">from</span> <span class="hljs-string">'esbuild'</span> <span class="hljs-keyword">import</span> path <span class="hljs-keyword">from</span> <span class="hljs-string">'node:path'</span> <span class="hljs-keyword">let</span> exampleOnResolvePlugin = { <span class="hljs-attr">name</span>: <span class="hljs-string">'example'</span>, <span class="hljs-function"><span class="hljs-title">setup</span>(<span class="hljs-params">build</span>)</span> { <span class="hljs-comment">// Redirect all paths starting with "images/" to "./public/images/"</span> build.onResolve({ <span class="hljs-attr">filter</span>: <span class="hljs-regexp">/^images\//</span> }, <span class="hljs-function"><span class="hljs-params">args</span> =></span> { <span class="hljs-keyword">return</span> { <span class="hljs-attr">path</span>: path.join(args.resolveDir, <span class="hljs-string">'public'</span>, args.path) } }) <span class="hljs-comment">// Mark all paths starting with "http://" or "https://" as external</span> build.onResolve({ <span class="hljs-attr">filter</span>: <span class="hljs-regexp">/^https?:\/\//</span> }, <span class="hljs-function"><span class="hljs-params">args</span> =></span> { <span class="hljs-keyword">return</span> { <span class="hljs-attr">path</span>: args.path, <span class="hljs-attr">external</span>: <span class="hljs-literal">true</span> } }) }, } <span class="hljs-keyword">await</span> esbuild.build({ <span class="hljs-attr">entryPoints</span>: [<span class="hljs-string">'app.js'</span>], <span class="hljs-attr">bundle</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">outfile</span>: <span class="hljs-string">'out.js'</span>, <span class="hljs-attr">plugins</span>: [exampleOnResolvePlugin], <span class="hljs-attr">loader</span>: { <span class="hljs-string">'.png'</span>: <span class="hljs-string">'binary'</span> }, }) </pre> <pre class="switchable go2"><span class="hljs-keyword">package</span> main <span class="hljs-keyword">import</span> <span class="hljs-string">"os"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"path/filepath"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"github.com/evanw/esbuild/pkg/api"</span> <span class="hljs-keyword">var</span> exampleOnResolvePlugin = api.Plugin{ Name: <span class="hljs-string">"example"</span>, Setup: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(build api.PluginBuild)</span></span> { <span class="hljs-comment">// Redirect all paths starting with "images/" to "./public/images/"</span> build.OnResolve(api.OnResolveOptions{Filter: <span class="hljs-string">`^images/`</span>}, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(args api.OnResolveArgs)</span> <span class="hljs-params">(api.OnResolveResult, error)</span></span> { <span class="hljs-keyword">return</span> api.OnResolveResult{ Path: filepath.Join(args.ResolveDir, <span class="hljs-string">"public"</span>, args.Path), }, <span class="hljs-literal">nil</span> }) <span class="hljs-comment">// Mark all paths starting with "http://" or "https://" as external</span> build.OnResolve(api.OnResolveOptions{Filter: <span class="hljs-string">`^https?://`</span>}, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(args api.OnResolveArgs)</span> <span class="hljs-params">(api.OnResolveResult, error)</span></span> { <span class="hljs-keyword">return</span> api.OnResolveResult{ Path: args.Path, External: <span class="hljs-literal">true</span>, }, <span class="hljs-literal">nil</span> }) }, } <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> { result := api.Build(api.BuildOptions{ EntryPoints: []<span class="hljs-keyword">string</span>{<span class="hljs-string">"app.js"</span>}, Bundle: <span class="hljs-literal">true</span>, Outfile: <span class="hljs-string">"out.js"</span>, Plugins: []api.Plugin{exampleOnResolvePlugin}, Write: <span class="hljs-literal">true</span>, Loader: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]api.Loader{ <span class="hljs-string">".png"</span>: api.LoaderBinary, }, }) <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(result.Errors) > <span class="hljs-number">0</span> { os.Exit(<span class="hljs-number">1</span>) } } </pre><p>The callback can return without providing a path to pass on responsibility for path resolution to the next callback. For a given import path, all <code>onResolve</code> callbacks from all plugins will be run in the order they were registered until one takes responsibility for path resolution. If no callback returns a path, esbuild will run its default path resolution logic.</p><p>Keep in mind that many callbacks may be running concurrently. In JavaScript, if your callback does expensive work that can run on another thread such as <code>fs.<wbr>existsSync()</code>, you should make the callback <code>async</code> and use <code>await</code> (in this case with <code>fs.<wbr>promises.<wbr>exists()</code>) to allow other code to run in the meantime. In Go, each callback may be run on a separate goroutine. Make sure you have appropriate synchronization in place if your plugin uses any shared data structures.</p><h3 id="on-resolve-options"><a class="permalink" href="#on-resolve-options">#</a>On-resolve options</h3><p>The <code>onResolve</code> API is meant to be called within the <code>setup</code> function and registers a callback to be triggered in certain situations. It takes a few options:</p><div class="switcher"> <a href="javascript:void 0" class="js2">JS</a> <a href="javascript:void 0" class="go2">Go</a> </div> <pre class="switchable js2">interface OnResolveOptions { <span class="hljs-attr">filter</span>: <span class="hljs-built_in">RegExp</span>; namespace?: string; } </pre> <pre class="switchable go2"><span class="hljs-keyword">type</span> OnResolveOptions <span class="hljs-keyword">struct</span> { Filter <span class="hljs-keyword">string</span> Namespace <span class="hljs-keyword">string</span> } </pre><ul><li><code>filter</code> <p> Every callback must provide a filter, which is a regular expression. The registered callback will be skipped when the path doesn't match this filter. You can read more about filters <a href="#filters">here</a>. </p></li><li><code>namespace</code> <p> This is optional. If provided, the callback is only run on paths within modules in the provided namespace. You can read more about namespaces <a href="#namespaces">here</a>. </p></li></ul><h3 id="on-resolve-arguments"><a class="permalink" href="#on-resolve-arguments">#</a>On-resolve arguments</h3><p>When esbuild calls the callback registered by <code>onResolve</code>, it will provide these arguments with information about the imported path:</p><div class="switcher"> <a href="javascript:void 0" class="js2">JS</a> <a href="javascript:void 0" class="go2">Go</a> </div> <pre class="switchable js2">interface OnResolveArgs { <span class="hljs-attr">path</span>: string; importer: string; namespace: string; resolveDir: string; kind: ResolveKind; pluginData: any; <span class="hljs-keyword">with</span>: Record<string, string>; } type ResolveKind = | <span class="hljs-string">'entry-point'</span> | <span class="hljs-string">'import-statement'</span> | <span class="hljs-string">'require-call'</span> | <span class="hljs-string">'dynamic-import'</span> | <span class="hljs-string">'require-resolve'</span> | <span class="hljs-string">'import-rule'</span> | <span class="hljs-string">'composes-from'</span> | <span class="hljs-string">'url-token'</span> </pre> <pre class="switchable go2"><span class="hljs-keyword">type</span> OnResolveArgs <span class="hljs-keyword">struct</span> { Path <span class="hljs-keyword">string</span> Importer <span class="hljs-keyword">string</span> Namespace <span class="hljs-keyword">string</span> ResolveDir <span class="hljs-keyword">string</span> Kind ResolveKind PluginData <span class="hljs-keyword">interface</span>{} With <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span> } <span class="hljs-keyword">const</span> ( ResolveEntryPoint ResolveKind ResolveJSImportStatement ResolveKind ResolveJSRequireCall ResolveKind ResolveJSDynamicImport ResolveKind ResolveJSRequireResolve ResolveKind ResolveCSSImportRule ResolveKind ResolveCSSComposesFrom ResolveKind ResolveCSSURLToken ResolveKind ) </pre><ul><li><code>path</code> <p> This is the verbatim unresolved path from the underlying module's source code. It can take any form. While esbuild's default behavior is to interpret import paths as either a relative path or a package name, plugins can be used to introduce new path forms. For example, the sample <a href="#http-plugin">HTTP plugin</a> below gives special meaning to paths starting with <code>http://</code>. </p></li><li><code>importer</code> <p> This is the path of the module containing this import to be resolved. Note that this path is only guaranteed to be a file system path if the namespace is <code>file</code>. If you want to resolve a path relative to the directory containing the importer module, you should use <code>resolveDir</code> instead since that also works for virtual modules. </p></li><li><code>namespace</code> <p> This is the namespace of the module containing this import to be resolved, as set by the <a href="#on-load">on-load callback</a> that loaded this file. This defaults to the <code>file</code> namespace for modules loaded with esbuild's default behavior. You can read more about namespaces <a href="#namespaces">here</a>. </p></li><li><code>resolveDir</code> <p> This is the file system directory to use when resolving an import path to a real path on the file system. For modules in the <code>file</code> namespace, this value defaults to the directory part of the module path. For virtual modules this value defaults to empty but <a href="#on-load">on-load callbacks</a> can optionally give virtual modules a resolve directory too. If that happens, it will be provided to resolve callbacks for unresolved paths in that file. </p></li><li><code>kind</code> <p> This says how the path to be resolved is being imported. For example, <code>'entry-<wbr>point'</code> means the path was provided to the API as an entry point path, <code>'import-<wbr>statement'</code> means the path is from a JavaScript <code>import</code> or <code>export</code> statement, and <code>'import-<wbr>rule'</code> means the path is from a CSS <code>@import</code> rule. </p></li><li><code>pluginData</code> <p> This property is passed from the previous plugin, as set by the <a href="#on-load">on-load callback</a> that loaded this file. </p></li><li><code>with</code> <p> This contains a map of the <a href="https://github.com/tc39/proposal-import-attributes">import attributes</a> that were present on the import statement used to import this module. For example, a module imported using <code>with { <wbr>type: <wbr>'json' }</code> will provide a <code>with</code> value of <code>{ type: <wbr>'json' }</code> to plugins. You can use this to resolve to a different path depending on the import attributes. </p></li></ul><h3 id="on-resolve-results"><a class="permalink" href="#on-resolve-results">#</a>On-resolve results</h3><p>This is the object that can be returned by a callback added using <code>onResolve</code> to provide a custom path resolution. If you would like to return from the callback without providing a path, just return the default value (so <code>undefined</code> in JavaScript and <code>OnResolveResult{}</code> in Go). Here are the optional properties that can be returned:</p><div class="switcher"> <a href="javascript:void 0" class="js2">JS</a> <a href="javascript:void 0" class="go2">Go</a> </div> <pre class="switchable js2">interface OnResolveResult { errors?: Message[]; external?: boolean; namespace?: string; path?: string; pluginData?: any; pluginName?: string; sideEffects?: boolean; suffix?: string; warnings?: Message[]; watchDirs?: string[]; watchFiles?: string[]; } interface Message { <span class="hljs-attr">text</span>: string; location: Location | <span class="hljs-literal">null</span>; detail: any; <span class="hljs-comment">// The original error from a JavaScript plugin, if applicable</span> } interface Location { <span class="hljs-attr">file</span>: string; namespace: string; line: number; <span class="hljs-comment">// 1-based</span> column: number; <span class="hljs-comment">// 0-based, in bytes</span> length: number; <span class="hljs-comment">// in bytes</span> lineText: string; } </pre> <pre class="switchable go2"><span class="hljs-keyword">type</span> OnResolveResult <span class="hljs-keyword">struct</span> { Errors []Message External <span class="hljs-keyword">bool</span> Namespace <span class="hljs-keyword">string</span> Path <span class="hljs-keyword">string</span> PluginData <span class="hljs-keyword">interface</span>{} PluginName <span class="hljs-keyword">string</span> SideEffects SideEffects Suffix <span class="hljs-keyword">string</span> Warnings []Message WatchDirs []<span class="hljs-keyword">string</span> WatchFiles []<span class="hljs-keyword">string</span> } <span class="hljs-keyword">type</span> Message <span class="hljs-keyword">struct</span> { Text <span class="hljs-keyword">string</span> Location *Location Detail <span class="hljs-keyword">interface</span>{} <span class="hljs-comment">// The original error from a Go plugin, if applicable</span> } <span class="hljs-keyword">type</span> Location <span class="hljs-keyword">struct</span> { File <span class="hljs-keyword">string</span> Namespace <span class="hljs-keyword">string</span> Line <span class="hljs-keyword">int</span> <span class="hljs-comment">// 1-based</span> Column <span class="hljs-keyword">int</span> <span class="hljs-comment">// 0-based, in bytes</span> Length <span class="hljs-keyword">int</span> <span class="hljs-comment">// in bytes</span> LineText <span class="hljs-keyword">string</span> } </pre><ul><li><code>path</code> <p> Set this to a non-empty string to resolve the import to a specific path. If this is set, no more on-resolve callbacks will be run for this import path in this module. If this is not set, esbuild will continue to run on-resolve callbacks that were registered after the current one. Then, if the path still isn't resolved, esbuild will default to resolving the path relative to the resolve directory of the current module. </p></li><li><code>external</code> <p> Set this to <code>true</code> to mark the module as <a href="/api/#external">external</a>, which means it will not be included in the bundle and will instead be imported at run-time. </p></li><li><code>namespace</code> <p> This is the namespace associated with the resolved path. If left empty, it will default to the <code>file</code> namespace for non-external paths. Paths in the file namespace must be an absolute path for the current file system (so starting with a forward slash on Unix and with a drive letter on Windows). </p> <p> If you want to resolve to a path that isn't a file system path, you should set the namespace to something other than <code>file</code> or an empty string. This tells esbuild to not treat the path as pointing to something on the file system. </p></li><li><code>errors</code> and <code>warnings</code> <p> These properties let you pass any log messages generated during path resolution to esbuild where they will be displayed in the terminal according to the current <a href="/api/#log-level">log level</a> and end up in the final build result. For example, if you are calling a library and that library can return errors and/or warnings, you will want to forward them using these properties. </p> <p> If you only have a single error to return, you don't have to pass it via <code>errors</code>. You can simply throw the error in JavaScript or return the <code>error</code> object as the second return value in Go. </p></li><li><code>watchFiles</code> and <code>watchDirs</code> <p> These properties let you return additional file system paths for esbuild's <a href="/api/#watch">watch mode</a> to scan. By default esbuild will only scan the path provided to <code>onLoad</code> plugins, and only if the namespace is <code>file</code>. If your plugin needs to react to additional changes in the file system, it needs to use one of these properties. </p> <p> A rebuild will be triggered if any file in the <code>watchFiles</code> array has been changed since the last build. Change detection is somewhat complicated and may check the file contents and/or the file's metadata. </p> <p> A rebuild will also be triggered if the list of directory entries for any directory in the <code>watchDirs</code> array has been changed since the last build. Note that this does not check anything about the contents of any file in these directories, and it also does not check any subdirectories. Think of this as checking the output of the Unix <code>ls</code> command. </p> <p> For robustness, you should include all file system paths that were used during the evaluation of the plugin. For example, if your plugin does something equivalent to <code>require.resolve()</code>, you'll need to include the paths of all "does this file exist" checks, not just the final path. Otherwise a new file could be created that causes the build to become outdated, but esbuild doesn't detect it because that path wasn't listed. </p></li><li><code>pluginName</code> <p> This property lets you replace this plugin's name with another name for this path resolution operation. It's useful for proxying another plugin through this plugin. For example, it lets you have a single plugin that forwards to a child process containing multiple plugins. You probably won't need to use this. </p></li><li><code>pluginData</code> <p> This property will be passed to the next plugin that runs in the plugin chain. If you return it from an <code>onLoad</code> plugin, it will be passed to the <code>onResolve</code> plugins for any imports in that file, and if you return it from an <code>onResolve</code> plugin, an arbitrary one will be passed to the <code>onLoad</code> plugin when it loads the file (it's arbitrary since the relationship is many-to-one). This is useful to pass data between different plugins without them having to coordinate directly. </p></li><li><code>sideEffects</code> <p> Setting this property to false tells esbuild that imports of this module can be removed if the imported names are unused. This behaves as if <code>"sideEffects": false</code> was specified the corresponding <code>package.json</code> file. For example, <code>import { x } <wbr>from "y"</code> may be completely removed if <code>x</code> is unused and <code>y</code> has been marked as <code>sideEffects: false</code>. You can read more about what <code>sideEffects</code> means in <a href="https://webpack.js.org/guides/tree-shaking/#mark-the-file-as-side-effect-free">Webpack's documentation about the feature</a>. </p></li><li><code>suffix</code> <p> Returning a value here lets you pass along an optional URL query or hash to append to the path that is not included in the path itself. Storing this separately is beneficial in cases when the path is processed by something that is not aware of the suffix, either by esbuild itself or by another plugin. </p> <p> For example, an on-resolve plugin might return a suffix of <code>?#iefix</code> for a <code>.eot</code> file in a build with a different on-load plugin for paths ending in <code>.eot</code>. Keeping the suffix separate means the suffix is still associated with the path but the <code>.eot</code> plugin will still match the file without needing to know anything about suffixes. </p> <p> If you do set a suffix, it must begin with either <code>?</code> or <code>#</code> because it's intended to be a URL query or hash. This feature has certain obscure uses such as hacking around bugs in IE8's CSS parser and may not be that useful otherwise. If you do use it, keep in mind that each unique namespace, path, and suffix combination is considered by esbuild to be a unique module identifier so by returning a different suffix for the same path, you are telling esbuild to create another copy of the module. </p></li></ul><h2 id="on-load"><a class="permalink" href="#on-load">#</a>On-load callbacks</h2><p>A callback added using <code>onLoad</code> will be run for each unique path/<wbr>namespace pair that has not been marked as external. Its job is to return the contents of the module and to tell esbuild how to interpret it. Here's an example plugin that converts <code>.txt</code> files into an array of words:</p><div class="switcher"> <a href="javascript:void 0" class="js2">JS</a> <a href="javascript:void 0" class="go2">Go</a> </div> <pre class="switchable js2"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> esbuild <span class="hljs-keyword">from</span> <span class="hljs-string">'esbuild'</span> <span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">'node:fs'</span> <span class="hljs-keyword">let</span> exampleOnLoadPlugin = { <span class="hljs-attr">name</span>: <span class="hljs-string">'example'</span>, <span class="hljs-function"><span class="hljs-title">setup</span>(<span class="hljs-params">build</span>)</span> { <span class="hljs-comment">// Load ".txt" files and return an array of words</span> build.onLoad({ <span class="hljs-attr">filter</span>: <span class="hljs-regexp">/\.txt$/</span> }, <span class="hljs-keyword">async</span> (args) => { <span class="hljs-keyword">let</span> text = <span class="hljs-keyword">await</span> fs.promises.readFile(args.path, <span class="hljs-string">'utf8'</span>) <span class="hljs-keyword">return</span> { <span class="hljs-attr">contents</span>: <span class="hljs-built_in">JSON</span>.stringify(text.split(<span class="hljs-regexp">/\s+/</span>)), <span class="hljs-attr">loader</span>: <span class="hljs-string">'json'</span>, } }) }, } <span class="hljs-keyword">await</span> esbuild.build({ <span class="hljs-attr">entryPoints</span>: [<span class="hljs-string">'app.js'</span>], <span class="hljs-attr">bundle</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">outfile</span>: <span class="hljs-string">'out.js'</span>, <span class="hljs-attr">plugins</span>: [exampleOnLoadPlugin], }) </pre> <pre class="switchable go2"><span class="hljs-keyword">package</span> main <span class="hljs-keyword">import</span> <span class="hljs-string">"encoding/json"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"io/ioutil"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"os"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"strings"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"github.com/evanw/esbuild/pkg/api"</span> <span class="hljs-keyword">var</span> exampleOnLoadPlugin = api.Plugin{ Name: <span class="hljs-string">"example"</span>, Setup: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(build api.PluginBuild)</span></span> { <span class="hljs-comment">// Load ".txt" files and return an array of words</span> build.OnLoad(api.OnLoadOptions{Filter: <span class="hljs-string">`\.txt$`</span>}, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(args api.OnLoadArgs)</span> <span class="hljs-params">(api.OnLoadResult, error)</span></span> { text, err := ioutil.ReadFile(args.Path) <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> api.OnLoadResult{}, err } bytes, err := json.Marshal(strings.Fields(<span class="hljs-keyword">string</span>(text))) <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> api.OnLoadResult{}, err } contents := <span class="hljs-keyword">string</span>(bytes) <span class="hljs-keyword">return</span> api.OnLoadResult{ Contents: &contents, Loader: api.LoaderJSON, }, <span class="hljs-literal">nil</span> }) }, } <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> { result := api.Build(api.BuildOptions{ EntryPoints: []<span class="hljs-keyword">string</span>{<span class="hljs-string">"app.js"</span>}, Bundle: <span class="hljs-literal">true</span>, Outfile: <span class="hljs-string">"out.js"</span>, Plugins: []api.Plugin{exampleOnLoadPlugin}, Write: <span class="hljs-literal">true</span>, }) <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(result.Errors) > <span class="hljs-number">0</span> { os.Exit(<span class="hljs-number">1</span>) } } </pre><p>The callback can return without providing the contents of the module. In that case the responsibility for loading the module is passed to the next registered callback. For a given module, all <code>onLoad</code> callbacks from all plugins will be run in the order they were registered until one takes responsibility for loading the module. If no callback returns contents for the module, esbuild will run its default module loading logic.</p><p>Keep in mind that many callbacks may be running concurrently. In JavaScript, if your callback does expensive work that can run on another thread such as <code>fs.<wbr>readFileSync()</code>, you should make the callback <code>async</code> and use <code>await</code> (in this case with <code>fs.<wbr>promises.<wbr>readFile()</code>) to allow other code to run in the meantime. In Go, each callback may be run on a separate goroutine. Make sure you have appropriate synchronization in place if your plugin uses any shared data structures.</p><h3 id="on-load-options"><a class="permalink" href="#on-load-options">#</a>On-load options</h3><p>The <code>onLoad</code> API is meant to be called within the <code>setup</code> function and registers a callback to be triggered in certain situations. It takes a few options:</p><div class="switcher"> <a href="javascript:void 0" class="js2">JS</a> <a href="javascript:void 0" class="go2">Go</a> </div> <pre class="switchable js2">interface OnLoadOptions { <span class="hljs-attr">filter</span>: <span class="hljs-built_in">RegExp</span>; namespace?: string; } </pre> <pre class="switchable go2"><span class="hljs-keyword">type</span> OnLoadOptions <span class="hljs-keyword">struct</span> { Filter <span class="hljs-keyword">string</span> Namespace <span class="hljs-keyword">string</span> } </pre><ul><li><code>filter</code> <p> Every callback must provide a filter, which is a regular expression. The registered callback will be skipped when the path doesn't match this filter. You can read more about filters <a href="#filters">here</a>. </p></li><li><code>namespace</code> <p> This is optional. If provided, the callback is only run on paths within modules in the provided namespace. You can read more about namespaces <a href="#namespaces">here</a>. </p></li></ul><h3 id="on-load-arguments"><a class="permalink" href="#on-load-arguments">#</a>On-load arguments</h3><p>When esbuild calls the callback registered by <code>onLoad</code>, it will provide these arguments with information about the module to load:</p><div class="switcher"> <a href="javascript:void 0" class="js2">JS</a> <a href="javascript:void 0" class="go2">Go</a> </div> <pre class="switchable js2">interface OnLoadArgs { <span class="hljs-attr">path</span>: string; namespace: string; suffix: string; pluginData: any; <span class="hljs-keyword">with</span>: Record<string, string>; } </pre> <pre class="switchable go2"><span class="hljs-keyword">type</span> OnLoadArgs <span class="hljs-keyword">struct</span> { Path <span class="hljs-keyword">string</span> Namespace <span class="hljs-keyword">string</span> Suffix <span class="hljs-keyword">string</span> PluginData <span class="hljs-keyword">interface</span>{} With <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span> } </pre><ul><li><code>path</code> <p> This is the fully-resolved path to the module. It should be considered a file system path if the namespace is <code>file</code>, but otherwise the path can take any form. For example, the sample <a href="#http-plugin">HTTP plugin</a> below gives special meaning to paths starting with <code>http://</code>. </p></li><li><code>namespace</code> <p> This is the namespace that the module path is in, as set by the <a href="#on-resolve">on-resolve callback</a> that resolved this file. It defaults to the <code>file</code> namespace for modules loaded with esbuild's default behavior. You can read more about namespaces <a href="#namespaces">here</a>. </p></li><li><code>suffix</code> <p> This is the URL query and/or hash at the end of the file path, if there is one. It's either filled in by esbuild's native path resolution behavior or returned by the <a href="#on-resolve">on-resolve callback</a> that resolved this file. This is stored separately from the path so that most plugins can just deal with the path and ignore the suffix. The on-load behavior that's built into esbuild just ignores the suffix and loads the file from its path alone. </p> <p> For context, IE8's CSS parser has a bug where it considers certain URLs to extend to the last <code>)</code> instead of the first <code>)</code>. So the CSS code <code>url('Foo.eot') <wbr>format('eot')</code> is incorrectly considered to have a URL of <code>Foo.eot') <wbr>format('eot</code>. To avoid this, people typically add something like <code>?#iefix</code> so that IE8 sees the URL as <code>Foo.eot?#iefix') <wbr>format('eot</code>. Then the path part of the URL is <code>Foo.eot</code> and the query part is <code>?#iefix') <wbr>format('eot</code>, which means IE8 can find the file <code>Foo.eot</code> by discarding the query. </p> <p> The suffix feature was added to esbuild to handle CSS files containing these hacks. A URL of <code>Foo.eot?#iefix</code> should be considered <a href="/api/#external">external</a> if all files matching <code>*.eot</code> have been marked as external, but the <code>?#iefix</code> suffix should still be present in the final output file. </p></li><li><code>pluginData</code> <p> This property is passed from the previous plugin, as set by the <a href="#on-resolve">on-resolve callback</a> that runs in the plugin chain. </p></li><li><code>with</code> <p> This contains a map of the <a href="https://github.com/tc39/proposal-import-attributes">import attributes</a> that were present on the import statement used to import this module. For example, a module imported using <code>with { <wbr>type: <wbr>'json' }</code> will provide a <code>with</code> value of <code>{ type: <wbr>'json' }</code> to plugins. A given module is loaded separately for each unique combination of import attributes, so these attributes are guaranteed to have been provided by all import statements used to import this module. That means they can be used by the plugin to alter the content of this module. </p></li></ul><h3 id="on-load-results"><a class="permalink" href="#on-load-results">#</a>On-load results</h3><p>This is the object that can be returned by a callback added using <code>onLoad</code> to provide the contents of a module. If you would like to return from the callback without providing any contents, just return the default value (so <code>undefined</code> in JavaScript and <code>OnLoadResult{}</code> in Go). Here are the optional properties that can be returned:</p><div class="switcher"> <a href="javascript:void 0" class="js2">JS</a> <a href="javascript:void 0" class="go2">Go</a> </div> <pre class="switchable js2">interface OnLoadResult { contents?: string | <span class="hljs-built_in">Uint8Array</span>; errors?: Message[]; loader?: Loader; pluginData?: any; pluginName?: string; resolveDir?: string; warnings?: Message[]; watchDirs?: string[]; watchFiles?: string[]; } interface Message { <span class="hljs-attr">text</span>: string; location: Location | <span class="hljs-literal">null</span>; detail: any; <span class="hljs-comment">// The original error from a JavaScript plugin, if applicable</span> } interface Location { <span class="hljs-attr">file</span>: string; namespace: string; line: number; <span class="hljs-comment">// 1-based</span> column: number; <span class="hljs-comment">// 0-based, in bytes</span> length: number; <span class="hljs-comment">// in bytes</span> lineText: string; } </pre> <pre class="switchable go2"><span class="hljs-keyword">type</span> OnLoadResult <span class="hljs-keyword">struct</span> { Contents *<span class="hljs-keyword">string</span> Errors []Message Loader Loader PluginData <span class="hljs-keyword">interface</span>{} PluginName <span class="hljs-keyword">string</span> ResolveDir <span class="hljs-keyword">string</span> Warnings []Message WatchDirs []<span class="hljs-keyword">string</span> WatchFiles []<span class="hljs-keyword">string</span> } <span class="hljs-keyword">type</span> Message <span class="hljs-keyword">struct</span> { Text <span class="hljs-keyword">string</span> Location *Location Detail <span class="hljs-keyword">interface</span>{} <span class="hljs-comment">// The original error from a Go plugin, if applicable</span> } <span class="hljs-keyword">type</span> Location <span class="hljs-keyword">struct</span> { File <span class="hljs-keyword">string</span> Namespace <span class="hljs-keyword">string</span> Line <span class="hljs-keyword">int</span> <span class="hljs-comment">// 1-based</span> Column <span class="hljs-keyword">int</span> <span class="hljs-comment">// 0-based, in bytes</span> Length <span class="hljs-keyword">int</span> <span class="hljs-comment">// in bytes</span> LineText <span class="hljs-keyword">string</span> } </pre><ul><li><code>contents</code> <p> Set this to a string to specify the contents of the module. If this is set, no more on-load callbacks will be run for this resolved path. If this is not set, esbuild will continue to run on-load callbacks that were registered after the current one. Then, if the contents are still not set, esbuild will default to loading the contents from the file system if the resolved path is in the <code>file</code> namespace. </p></li><li><code>loader</code> <p> This tells esbuild how to interpret the contents. For example, the <a href="/content-types/#javascript"><code>js</code></a> loader interprets the contents as JavaScript and the <a href="/content-types/#css"><code>css</code></a> loader interprets the contents as CSS. The loader defaults to <code>js</code> if it's not specified. See the <a href="/content-types/">content types</a> page for a complete list of all built-in loaders. </p></li><li><code>resolveDir</code> <p> This is the file system directory to use when resolving an import path in this module to a real path on the file system. For modules in the <code>file</code> namespace, this value defaults to the directory part of the module path. Otherwise this value defaults to empty unless the plugin provides one. If the plugin doesn't provide one, esbuild's default behavior won't resolve any imports in this module. This directory will be passed to any <a href="#on-resolve">on-resolve callbacks</a> that run on unresolved import paths in this module. </p></li><li><code>errors</code> and <code>warnings</code> <p> These properties let you pass any log messages generated during path resolution to esbuild where they will be displayed in the terminal according to the current <a href="/api/#log-level">log level</a> and end up in the final build result. For example, if you are calling a library and that library can return errors and/or warnings, you will want to forward them using these properties. </p> <p> If you only have a single error to return, you don't have to pass it via <code>errors</code>. You can simply throw the error in JavaScript or return the <code>error</code> object as the second return value in Go. </p></li><li><code>watchFiles</code> and <code>watchDirs</code> <p> These properties let you return additional file system paths for esbuild's <a href="/api/#watch">watch mode</a> to scan. By default esbuild will only scan the path provided to <code>onLoad</code> plugins, and only if the namespace is <code>file</code>. If your plugin needs to react to additional changes in the file system, it needs to use one of these properties. </p> <p> A rebuild will be triggered if any file in the <code>watchFiles</code> array has been changed since the last build. Change detection is somewhat complicated and may check the file contents and/or the file's metadata. </p> <p> A rebuild will also be triggered if the list of directory entries for any directory in the <code>watchDirs</code> array has been changed since the last build. Note that this does not check anything about the contents of any file in these directories, and it also does not check any subdirectories. Think of this as checking the output of the Unix <code>ls</code> command. </p> <p> For robustness, you should include all file system paths that were used during the evaluation of the plugin. For example, if your plugin does something equivalent to <code>require.resolve()</code>, you'll need to include the paths of all "does this file exist" checks, not just the final path. Otherwise a new file could be created that causes the build to become outdated, but esbuild doesn't detect it because that path wasn't listed. </p></li><li><code>pluginName</code> <p> This property lets you replace this plugin's name with another name for this module load operation. It's useful for proxying another plugin through this plugin. For example, it lets you have a single plugin that forwards to a child process containing multiple plugins. You probably won't need to use this. </p></li><li><code>pluginData</code> <p> This property will be passed to the next plugin that runs in the plugin chain. If you return it from an <code>onLoad</code> plugin, it will be passed to the <code>onResolve</code> plugins for any imports in that file, and if you return it from an <code>onResolve</code> plugin, an arbitrary one will be passed to the <code>onLoad</code> plugin when it loads the file (it's arbitrary since the relationship is many-to-one). This is useful to pass data between different plugins without them having to coordinate directly. </p></li></ul><h3 id="caching-your-plugin"><a class="permalink" href="#caching-your-plugin">#</a>Caching your plugin</h3><p>Since esbuild is so fast, it's often the case that plugin evaluation is the main bottleneck when building with esbuild. Caching of plugin evaluation is left up to each plugin instead of being a part of esbuild itself because cache invalidation is plugin-specific. If you are writing a slow plugin that needs a cache to be fast, you will have to write the cache logic yourself.</p><p>A cache is essentially a map that memoizes the transform function that represents your plugin. The keys of the map usually contain the inputs to your transform function and the values of the map usually contain the outputs of your transform function. In addition, the map usually has some form of least-recently-used cache eviction policy to avoid continually growing larger in size over time.</p><p>The cache can either be stored in memory (beneficial for use with esbuild's <a href="/api/#rebuild">rebuild</a> API), on disk (beneficial for caching across separate build script invocations), or even on a server (beneficial for really slow transforms that can be shared between different developer machines). Where to store the cache is case-specific and depends on your plugin.</p><p>Here is a simple caching example. Say we want to cache the function <code>slowTransform()</code> that takes as input the contents of a file in the <code>*.example</code> format and transforms it to JavaScript. An in-memory cache that avoids redundant calls to this function when used with esbuild's <a href="/api/#rebuild">rebuild</a> API) might look something like this:</p><pre><span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">'node:fs'</span> <span class="hljs-keyword">let</span> examplePlugin = { <span class="hljs-attr">name</span>: <span class="hljs-string">'example'</span>, <span class="hljs-function"><span class="hljs-title">setup</span>(<span class="hljs-params">build</span>)</span> { <span class="hljs-keyword">let</span> cache = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Map</span> build.onLoad({ <span class="hljs-attr">filter</span>: <span class="hljs-regexp">/\.example$/</span> }, <span class="hljs-keyword">async</span> (args) => { <span class="hljs-keyword">let</span> input = <span class="hljs-keyword">await</span> fs.promises.readFile(args.path, <span class="hljs-string">'utf8'</span>) <span class="hljs-keyword">let</span> key = args.path <span class="hljs-keyword">let</span> value = cache.get(key) <span class="hljs-keyword">if</span> (!value || value.input !== input) { <span class="hljs-keyword">let</span> contents = slowTransform(input) value = { input, <span class="hljs-attr">output</span>: { contents } } cache.set(key, value) } <span class="hljs-keyword">return</span> value.output }) } }</pre><p>Some important caveats about the caching code above:</p><ul><li><p> There is no cache eviction policy present in the code above. Memory usage will continue to grow if more and more keys are added to the cache map. <p> </p> To combat this limitation somewhat, the <code>input</code> value is stored in the cache <code>value</code> instead of in the cache <code>key</code>. This means that changing the contents of a file will not leak memory because the key only includes the file path, not the file contents. Changing the file contents only overwrites the previous cache entry. This is probably fine for common usage where someone repeatedly edits the same file in between incremental rebuilds and only occasionally adds or renames files. <p> </p> But the cache will continue to grow in size if each build contains new unique path names (e.g. perhaps an auto-generated temporary file path containing the current time). A more advanced version might use a least-recently-used eviction policy. </p></li><li><p> Cache invalidation only works if <code>slowTransform()</code> is a <a href="https://en.wikipedia.org/wiki/Pure_function">pure function</a> (meaning that the output of the function <em>only</em> depends on the inputs to the function) and if all of the inputs to the function are somehow captured in the lookup to the cache map. For example if the transform function automatically reads the contents of some other files and the output depends on the contents of those files too, then the cache would fail to be invalidated when those files are changed because they are not included in the cache key. </p> <p> This part is easy to mess up so it's worth going through a specific example. Consider a plugin that implements a compile-to-CSS language. If that plugin implements <code>@import</code> rules itself by parsing imported files and either bundles them or makes any exported variable declarations available to the importing code, your plugin will not be correct if it only checks that the importing file's contents haven't changed because a change to the imported file could also invalidate the cache. </p> <p> You may be thinking that you could just add the contents of the imported file to the cache key to fix this problem. However, even that may be incorrect. Say for example this plugin uses <a href="https://nodejs.org/api/modules.html#modules_require_resolve_request_options"><code>require.resolve()</code></a> to resolve the import path to an absolute file path. This is a common approach because it uses node's built-in path resolution that can resolve to a path inside a package. This function usually does many checks for files in different locations before returning the resolved path. For example, importing the path <code>pkg/file</code> from the file <code>src/entry.css</code> might check the following locations (yes, node's package resolution algorithm is very inefficient): </p> <pre> src/node_modules/pkg/file src/node_modules/pkg/file.css src/node_modules/pkg/file/package.json src/node_modules/pkg/file/main src/node_modules/pkg/file/main.css src/node_modules/pkg/file/main/index.css src/node_modules/pkg/file/index.css node_modules/pkg/file node_modules/pkg/file.css node_modules/pkg/file/package.json node_modules/pkg/file/main node_modules/pkg/file/main.css node_modules/pkg/file/main/index.css node_modules/pkg/file/index.css </pre> <p> Say the import <code>pkg/file</code> was ultimately resolved to the absolute path <code>node_modules/<wbr>pkg/<wbr>file/<wbr>index.css</code>. Even if you cache the contents of both the importing file and the imported file and verify that the contents of both files are still the same before reusing the cache entry, the cache entry could still be stale if one of the other files that <code>require.resolve()</code> checks for has either been created or deleted since the cache entry was added. Caching this correctly essentially involves always re-running all such path resolutions even when none of the input files have been changed and verifying that none of the path resolutions have changed either. </p></li><li><p> These cache keys are only correct for an in-memory cache. It would be incorrect to implement a file system cache using the same cache keys. While an in-memory cache is guaranteed to always run the same code for every build because the code is also stored in memory, a file system cache could potentially be accessed by two separate builds that each contain different code. Specifically the code for the <code>slowTransform()</code> function may have been changed in between builds. </p> <p> This can happen in various cases. The package containing the function <code>slowTransform()</code> may have been updated, or one of its transitive dependencies may have been updated even if you have pinned the package's version due to how npm handles semver, or someone may have <a href="https://www.npmjs.com/package/patch-package">mutated the package contents</a> on the file system in the meantime, or the transform function may be calling a node API and different builds could be running on different node versions. </p> <p> If you want to store your cache on the file system, you should guard against changes to the code for the transform function by storing some representation of the code for the transform function in the cache key. This is usually some form of <a href="https://nodejs.org/api/crypto.html#crypto_class_hash">hash</a> that contains the contents of all relevant files in all relevant packages as well as potentially other details such as which node version you are currently running on. Getting all of this to be correct is non-trivial. </p></li></ul><h2 id="on-start"><a class="permalink" href="#on-start">#</a>On-start callbacks</h2><p>Register an on-start callback to be notified when a new build starts. This triggers for all builds, not just the initial build, so it's especially useful for <a href="/api/#rebuild">rebuilds</a>, <a href="/api/#watch">watch mode</a>, and <a href="/api/#serve">serve mode</a>. Here's how to add an on-start callback:</p><div class="switcher"> <a href="javascript:void 0" class="js2">JS</a> <a href="javascript:void 0" class="go2">Go</a> </div> <pre class="switchable js2"><span class="hljs-keyword">let</span> examplePlugin = { <span class="hljs-attr">name</span>: <span class="hljs-string">'example'</span>, <span class="hljs-function"><span class="hljs-title">setup</span>(<span class="hljs-params">build</span>)</span> { build.onStart(<span class="hljs-function">() =></span> { <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'build started'</span>) }) }, } </pre> <pre class="switchable go2"><span class="hljs-keyword">package</span> main <span class="hljs-keyword">import</span> <span class="hljs-string">"fmt"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"github.com/evanw/esbuild/pkg/api"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"os"</span> <span class="hljs-keyword">var</span> examplePlugin = api.Plugin{ Name: <span class="hljs-string">"example"</span>, Setup: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(build api.PluginBuild)</span></span> { build.OnStart(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span> <span class="hljs-params">(api.OnStartResult, error)</span></span> { fmt.Fprintf(os.Stderr, <span class="hljs-string">"build started\n"</span>) <span class="hljs-keyword">return</span> api.OnStartResult{}, <span class="hljs-literal">nil</span> }) }, } <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> { } </pre><p>You should not use an on-start callback for initialization since it can be run multiple times. If you want to initialize something, just put your plugin initialization code directly inside the <code>setup</code> function instead.</p><p>The on-start callback can be <code>async</code> and can return a promise. All on-start callbacks from all plugins are run concurrently, and then the build waits for all on-start callbacks to finish before proceeding. On-start callbacks can optionally return errors and/or warnings to be included with the build.</p><p>Note that on-start callbacks do not have the ability to mutate the <a href="#build-options">build options</a>. The initial build options can only be modified within the <code>setup</code> function and are consumed once <code>setup</code> returns. All builds after the first one reuse the same initial options so the initial options are never re-consumed, and modifications to <code>build.initialOptions</code> that are done within the start callback are ignored.</p><h2 id="on-end"><a class="permalink" href="#on-end">#</a>On-end callbacks</h2><p>Register an on-end callback to be notified when a new build ends. This triggers for all builds, not just the initial build, so it's especially useful for <a href="/api/#rebuild">rebuilds</a>, <a href="/api/#watch">watch mode</a>, and <a href="/api/#serve">serve mode</a>. Here's how to add an on-end callback:</p><div class="switcher"> <a href="javascript:void 0" class="js2">JS</a> <a href="javascript:void 0" class="go2">Go</a> </div> <pre class="switchable js2"><span class="hljs-keyword">let</span> examplePlugin = { <span class="hljs-attr">name</span>: <span class="hljs-string">'example'</span>, <span class="hljs-function"><span class="hljs-title">setup</span>(<span class="hljs-params">build</span>)</span> { build.onEnd(<span class="hljs-function"><span class="hljs-params">result</span> =></span> { <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`build ended with <span class="hljs-subst">${result.errors.length}</span> errors`</span>) }) }, } </pre> <pre class="switchable go2"><span class="hljs-keyword">package</span> main <span class="hljs-keyword">import</span> <span class="hljs-string">"fmt"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"github.com/evanw/esbuild/pkg/api"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"os"</span> <span class="hljs-keyword">var</span> examplePlugin = api.Plugin{ Name: <span class="hljs-string">"example"</span>, Setup: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(build api.PluginBuild)</span></span> { build.OnEnd(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(result *api.BuildResult)</span> <span class="hljs-params">(api.OnEndResult, error)</span></span> { fmt.Fprintf(os.Stderr, <span class="hljs-string">"build ended with %d errors\n"</span>, <span class="hljs-built_in">len</span>(result.Errors)) <span class="hljs-keyword">return</span> api.OnEndResult{}, <span class="hljs-literal">nil</span> }) }, } <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> { } </pre><p>All on-end callbacks are run in serial and each callback is given access to the final build result. It can modify the build result before returning and can delay the end of the build by returning a promise. If you want to be able to inspect the build graph, you should enable the <a href="/api/#metafile">metafile</a> setting on the <a href="#build-options">initial options</a> and the build graph will be returned as the <code>metafile</code> property on the build result object.</p><h2 id="on-dispose"><a class="permalink" href="#on-dispose">#</a>On-dispose callbacks</h2><p>Register an on-dispose callback to perform cleanup when the plugin is no longer used. It will be called after every <code>build()</code> call regardless of whether the build failed or not, as well as after the first <code>dispose()</code> call on a given build context. Here's how to add an on-dispose callback:</p><div class="switcher"> <a href="javascript:void 0" class="js2">JS</a> <a href="javascript:void 0" class="go2">Go</a> </div> <pre class="switchable js2"><span class="hljs-keyword">let</span> examplePlugin = { <span class="hljs-attr">name</span>: <span class="hljs-string">'example'</span>, <span class="hljs-function"><span class="hljs-title">setup</span>(<span class="hljs-params">build</span>)</span> { build.onDispose(<span class="hljs-function">() =></span> { <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'This plugin is no longer used'</span>) }) }, } </pre> <pre class="switchable go2"><span class="hljs-keyword">package</span> main <span class="hljs-keyword">import</span> <span class="hljs-string">"fmt"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"github.com/evanw/esbuild/pkg/api"</span> <span class="hljs-keyword">var</span> examplePlugin = api.Plugin{ Name: <span class="hljs-string">"example"</span>, Setup: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(build api.PluginBuild)</span></span> { build.OnDispose(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> { fmt.Println(<span class="hljs-string">"This plugin is no longer used"</span>) }) }, } <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> { } </pre><h2 id="build-options"><a class="permalink" href="#build-options">#</a>Accessing build options</h2><p>Plugins can access the initial build options from within the <code>setup</code> method. This lets you inspect how the build is configured as well as modify the build options before the build starts. Here is an example:</p><div class="switcher"> <a href="javascript:void 0" class="js2">JS</a> <a href="javascript:void 0" class="go2">Go</a> </div> <pre class="switchable js2"><span class="hljs-keyword">let</span> examplePlugin = { <span class="hljs-attr">name</span>: <span class="hljs-string">'auto-node-env'</span>, <span class="hljs-function"><span class="hljs-title">setup</span>(<span class="hljs-params">build</span>)</span> { <span class="hljs-keyword">const</span> options = build.initialOptions options.define = options.define || {} options.define[<span class="hljs-string">'process.env.NODE_ENV'</span>] = options.minify ? <span class="hljs-string">'"production"'</span> : <span class="hljs-string">'"development"'</span> }, } </pre> <pre class="switchable go2"><span class="hljs-keyword">package</span> main <span class="hljs-keyword">import</span> <span class="hljs-string">"github.com/evanw/esbuild/pkg/api"</span> <span class="hljs-keyword">var</span> examplePlugin = api.Plugin{ Name: <span class="hljs-string">"auto-node-env"</span>, Setup: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(build api.PluginBuild)</span></span> { options := build.InitialOptions <span class="hljs-keyword">if</span> options.Define == <span class="hljs-literal">nil</span> { options.Define = <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>{} } <span class="hljs-keyword">if</span> options.MinifyWhitespace && options.MinifyIdentifiers && options.MinifySyntax { options.Define[<span class="hljs-string">`process.env.NODE_ENV`</span>] = <span class="hljs-string">`"production"`</span> } <span class="hljs-keyword">else</span> { options.Define[<span class="hljs-string">`process.env.NODE_ENV`</span>] = <span class="hljs-string">`"development"`</span> } }, } <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> { } </pre><p>Note that modifications to the build options after the build starts do not affect the build. In particular, <a href="/api/#rebuild">rebuilds</a>, <a href="/api/#watch">watch mode</a>, and <a href="/api/#serve">serve mode</a> do not update their build options if plugins mutate the build options object after the first build has started.</p><h2 id="resolve"><a class="permalink" href="#resolve">#</a>Resolving paths</h2><p>When a plugin returns a result from an <a href="#on-resolve">on-resolve callback</a>, the result completely replaces esbuild's built-in path resolution. This gives the plugin complete control over how path resolution works, but it means that the plugin may have to reimplement some of the behavior that esbuild already has built-in if it wants to have similar behavior. For example, a plugin may want to search for a package in the user's <code>node_modules</code> directory, which is something esbuild already implements.</p><p>Instead of reimplementing esbuild's built-in behavior, plugins have the option of running esbuild's path resolution manually and inspecting the result. This lets you adjust the inputs and/or the outputs of esbuild's path resolution. Here's an example:</p><div class="switcher"> <a href="javascript:void 0" class="js2">JS</a> <a href="javascript:void 0" class="go2">Go</a> </div> <pre class="switchable js2"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> esbuild <span class="hljs-keyword">from</span> <span class="hljs-string">'esbuild'</span> <span class="hljs-keyword">let</span> examplePlugin = { <span class="hljs-attr">name</span>: <span class="hljs-string">'example'</span>, <span class="hljs-function"><span class="hljs-title">setup</span>(<span class="hljs-params">build</span>)</span> { build.onResolve({ <span class="hljs-attr">filter</span>: <span class="hljs-regexp">/^example$/</span> }, <span class="hljs-keyword">async</span> () => { <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> build.resolve(<span class="hljs-string">'./foo'</span>, { <span class="hljs-attr">kind</span>: <span class="hljs-string">'import-statement'</span>, <span class="hljs-attr">resolveDir</span>: <span class="hljs-string">'./bar'</span>, }) <span class="hljs-keyword">if</span> (result.errors.length > <span class="hljs-number">0</span>) { <span class="hljs-keyword">return</span> { <span class="hljs-attr">errors</span>: result.errors } } <span class="hljs-keyword">return</span> { <span class="hljs-attr">path</span>: result.path, <span class="hljs-attr">external</span>: <span class="hljs-literal">true</span> } }) }, } <span class="hljs-keyword">await</span> esbuild.build({ <span class="hljs-attr">entryPoints</span>: [<span class="hljs-string">'app.js'</span>], <span class="hljs-attr">bundle</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">outfile</span>: <span class="hljs-string">'out.js'</span>, <span class="hljs-attr">plugins</span>: [examplePlugin], }) </pre> <pre class="switchable go2"><span class="hljs-keyword">package</span> main <span class="hljs-keyword">import</span> <span class="hljs-string">"os"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"github.com/evanw/esbuild/pkg/api"</span> <span class="hljs-keyword">var</span> examplePlugin = api.Plugin{ Name: <span class="hljs-string">"example"</span>, Setup: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(build api.PluginBuild)</span></span> { build.OnResolve(api.OnResolveOptions{Filter: <span class="hljs-string">`^example$`</span>}, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(api.OnResolveArgs)</span> <span class="hljs-params">(api.OnResolveResult, error)</span></span> { result := build.Resolve(<span class="hljs-string">"./foo"</span>, api.ResolveOptions{ Kind: api.ResolveJSImportStatement, ResolveDir: <span class="hljs-string">"./bar"</span>, }) <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(result.Errors) > <span class="hljs-number">0</span> { <span class="hljs-keyword">return</span> api.OnResolveResult{Errors: result.Errors}, <span class="hljs-literal">nil</span> } <span class="hljs-keyword">return</span> api.OnResolveResult{Path: result.Path, External: <span class="hljs-literal">true</span>}, <span class="hljs-literal">nil</span> }) }, } <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> { result := api.Build(api.BuildOptions{ EntryPoints: []<span class="hljs-keyword">string</span>{<span class="hljs-string">"app.js"</span>}, Bundle: <span class="hljs-literal">true</span>, Outfile: <span class="hljs-string">"out.js"</span>, Plugins: []api.Plugin{examplePlugin}, Write: <span class="hljs-literal">true</span>, }) <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(result.Errors) > <span class="hljs-number">0</span> { os.Exit(<span class="hljs-number">1</span>) } } </pre><p>This plugin intercepts imports to the path <code>example</code>, tells esbuild to resolve the import <code>./foo</code> in the directory <code>./bar</code>, forces whatever path esbuild returns to be considered external, and maps the import for <code>example</code> to that external path.</p><p>Here are some additional things to know about this API:</p><ul><li><p> If you don't pass the optional <code>resolveDir</code> parameter, esbuild will still run <code>onResolve</code> plugin callbacks but will not attempt any path resolution itself. All of esbuild's path resolution logic depends on the <code>resolveDir</code> parameter including looking for packages in <code>node_modules</code> directories (since it needs to know where those <code>node_modules</code> directories might be). </p></li><li><p> If you want to resolve a file name in a specific directory, make sure the input path starts with <code>./</code>. Otherwise the input path will be treated as a package path instead of a relative path. This behavior is identical to esbuild's normal path resolution logic. </p></li><li><p> If path resolution fails, the <code>errors</code> property on the returned object will be a non-empty array containing the error information. This function does not always throw an error when it fails. You need to check for errors after calling it. </p></li><li><p> The behavior of this function depends on the build configuration. That's why it's a property of the <code>build</code> object instead of being a top-level API call. This also means you can't call it until all plugin <code>setup</code> functions have finished since these give plugins the opportunity to adjust the build configuration before it's frozen at the start of the build. So the <code>resolve</code> function is going to be most useful inside your <code>onResolve</code> and/or <code>onLoad</code> callbacks. </p></li><li><p> There is currently no attempt made to detect infinite path resolution loops. Calling <code>resolve</code> from within <code>onResolve</code> with the same parameters is almost certainly a bad idea. </p></li></ul><h3 id="resolve-options"><a class="permalink" href="#resolve-options">#</a>Resolve options</h3><p>The <code>resolve</code> function takes the path to resolve as the first argument and an object with optional properties as the second argument. This options object is very similar to the <a href="#on-resolve-arguments">arguments that are passed to <code>onResolve</code></a>. Here are the available options:</p><div class="switcher"> <a href="javascript:void 0" class="js2">JS</a> <a href="javascript:void 0" class="go2">Go</a> </div> <pre class="switchable js2">interface ResolveOptions { <span class="hljs-attr">kind</span>: ResolveKind; importer?: string; namespace?: string; resolveDir?: string; pluginData?: any; <span class="hljs-keyword">with</span>?: Record<string, string>; } type ResolveKind = | <span class="hljs-string">'entry-point'</span> | <span class="hljs-string">'import-statement'</span> | <span class="hljs-string">'require-call'</span> | <span class="hljs-string">'dynamic-import'</span> | <span class="hljs-string">'require-resolve'</span> | <span class="hljs-string">'import-rule'</span> | <span class="hljs-string">'url-token'</span> </pre> <pre class="switchable go2"><span class="hljs-keyword">type</span> ResolveOptions <span class="hljs-keyword">struct</span> { Kind ResolveKind Importer <span class="hljs-keyword">string</span> Namespace <span class="hljs-keyword">string</span> ResolveDir <span class="hljs-keyword">string</span> PluginData <span class="hljs-keyword">interface</span>{} With <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span> } <span class="hljs-keyword">const</span> ( ResolveEntryPoint ResolveKind ResolveJSImportStatement ResolveKind ResolveJSRequireCall ResolveKind ResolveJSDynamicImport ResolveKind ResolveJSRequireResolve ResolveKind ResolveCSSImportRule ResolveKind ResolveCSSURLToken ResolveKind ) </pre><ul><li><code>kind</code> <p> This tells esbuild how the path was imported, which can affect path resolution. For example, <a href="https://nodejs.org/api/packages.html#conditional-exports">node's path resolution rules</a> say that paths imported using <code>'require-call'</code> should respect <a href="/api/#conditions">conditional package imports</a> in the <code>"require"</code> section in <code>package.json</code> while paths imported using <code>'import-statement'</code> should respect conditional package imports in the <code>"import"</code> section instead. </p></li><li><code>importer</code> <p> If set, this is interpreted as the path of the module containing this import to be resolved. This affects plugins with <code>onResolve</code> callbacks that check the <code>importer</code> value. </p></li><li><code>namespace</code> <p> If set, this is interpreted as the namespace of the module containing this import to be resolved. This affects plugins with <code>onResolve</code> callbacks that check the <code>namespace</code> value. You can read more about namespaces <a href="#namespaces">here</a>. </p></li><li><code>resolveDir</code> <p> This is the file system directory to use when resolving an import path to a real path on the file system. This must be set for esbuild's built-in path resolution to be able to find a given file, even for non-relative package paths (since esbuild needs to know where the <code>node_modules</code> directory is). </p></li><li><code>pluginData</code> <p> This property can be used to pass custom data to whatever <a href="#on-resolve">on-resolve callbacks</a> match this import path. The meaning of this data is left entirely up to you. </p></li><li><code>with</code> <p> This is the <a href="https://github.com/tc39/proposal-import-attributes">import attributes</a> assocated with the import statement for this path. For example, a <code>with</code> value of <code>{ type: <wbr>'json' }</code> would be appropriate for a module imported using <code>with { <wbr>type: <wbr>'json' }</code> attributes on the import statement. This information isn't used by esbuild but may be used by <a href="#on-resolve">on-resolve callbacks</a>. </p></li></ul><h3 id="resolve-results"><a class="permalink" href="#resolve-results">#</a>Resolve results</h3><p>The <code>resolve</code> function returns an object that's very similar to what plugins can <a href="#on-resolve-results">return from an <code>onResolve</code> callback</a>. It has the following properties:</p><div class="switcher"> <a href="javascript:void 0" class="js2">JS</a> <a href="javascript:void 0" class="go2">Go</a> </div> <pre class="switchable js2"><span class="hljs-keyword">export</span> interface ResolveResult { <span class="hljs-attr">errors</span>: Message[]; external: boolean; namespace: string; path: string; pluginData: any; sideEffects: boolean; suffix: string; warnings: Message[]; } interface Message { <span class="hljs-attr">text</span>: string; location: Location | <span class="hljs-literal">null</span>; detail: any; <span class="hljs-comment">// The original error from a JavaScript plugin, if applicable</span> } interface Location { <span class="hljs-attr">file</span>: string; namespace: string; line: number; <span class="hljs-comment">// 1-based</span> column: number; <span class="hljs-comment">// 0-based, in bytes</span> length: number; <span class="hljs-comment">// in bytes</span> lineText: string; } </pre> <pre class="switchable go2"><span class="hljs-keyword">type</span> ResolveResult <span class="hljs-keyword">struct</span> { Errors []Message External <span class="hljs-keyword">bool</span> Namespace <span class="hljs-keyword">string</span> Path <span class="hljs-keyword">string</span> PluginData <span class="hljs-keyword">interface</span>{} SideEffects <span class="hljs-keyword">bool</span> Suffix <span class="hljs-keyword">string</span> Warnings []Message } <span class="hljs-keyword">type</span> Message <span class="hljs-keyword">struct</span> { Text <span class="hljs-keyword">string</span> Location *Location Detail <span class="hljs-keyword">interface</span>{} <span class="hljs-comment">// The original error from a Go plugin, if applicable</span> } <span class="hljs-keyword">type</span> Location <span class="hljs-keyword">struct</span> { File <span class="hljs-keyword">string</span> Namespace <span class="hljs-keyword">string</span> Line <span class="hljs-keyword">int</span> <span class="hljs-comment">// 1-based</span> Column <span class="hljs-keyword">int</span> <span class="hljs-comment">// 0-based, in bytes</span> Length <span class="hljs-keyword">int</span> <span class="hljs-comment">// in bytes</span> LineText <span class="hljs-keyword">string</span> } </pre><ul><li><code>path</code> <p> This is the result of path resolution, or an empty string if path resolution failed. </p></li><li><code>external</code> <p> This will be <code>true</code> if the path was marked as <a href="/api/#external">external</a>, which means it will not be included in the bundle and will instead be imported at run-time. </p></li><li><code>namespace</code> <p> This is the namespace associated with the resolved path. You can read more about namespaces <a href="#namespaces">here</a>. </p></li><li><code>errors</code> and <code>warnings</code> <p> These properties hold any log messages generated during path resolution, either by any plugins that responded to this path resolution operation or by esbuild itself. These log messages are not automatically included in the log, so they will be completely invisible if you discard them. If you want them to be included in the log, you'll need to return them from either <code>onResolve</code> or <code>onLoad</code>. </p></li><li><code>pluginData</code> <p> If a plugin responded to this path resolution operation and returned <code>pluginData</code> from its <code>onResolve</code> callback, that data will end up here. This is useful to pass data between different plugins without them having to coordinate directly. </p></li><li><code>sideEffects</code> <p> This property will be <code>true</code> unless the module is somehow annotated as having no side effects, in which case it will be <code>false</code>. This will be <code>false</code> for packages that have <code>"sideEffects": false</code> in the corresponding <code>package.json</code> file, and also if a plugin responds to this path resolution operation and returns <code>sideEffects: false</code>. You can read more about what <code>sideEffects</code> means in <a href="https://webpack.js.org/guides/tree-shaking/#mark-the-file-as-side-effect-free">Webpack's documentation about the feature</a>. </p></li><li><code>suffix</code> <p> This can contain an optional URL query or hash if there was one at the end of the path to be resolved and if removing it was required for the path to resolve successfully. </p></li></ul><h2 id="example-plugins"><a class="permalink" href="#example-plugins">#</a>Example plugins</h2><p>The example plugins below are meant to give you an idea of the different types of things you can do with the plugin API.</p><h3 id="http-plugin"><a class="permalink" href="#http-plugin">#</a>HTTP plugin</h3><p><em>This example demonstrates: using a path format other than file system paths, namespace-specific path resolution, using resolve and load callbacks together.</em></p><p>This plugin allows you to import HTTP URLs into JavaScript code. The code will automatically be downloaded at build time. It enables the following workflow:</p><pre><span class="hljs-keyword">import</span> { zip } <span class="hljs-keyword">from</span> <span class="hljs-string">'https://unpkg.com/lodash-es@4.17.15/lodash.js'</span> <span class="hljs-built_in">console</span>.log(zip([<span class="hljs-number">1</span>, <span class="hljs-number">2</span>], [<span class="hljs-string">'a'</span>, <span class="hljs-string">'b'</span>]))</pre><p>This can be accomplished with the following plugin. Note that for real usage the downloads should be cached, but caching has been omitted from this example for brevity:</p><div class="switcher"> <a href="javascript:void 0" class="js2">JS</a> <a href="javascript:void 0" class="go2">Go</a> </div> <pre class="switchable js2"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> esbuild <span class="hljs-keyword">from</span> <span class="hljs-string">'esbuild'</span> <span class="hljs-keyword">import</span> https <span class="hljs-keyword">from</span> <span class="hljs-string">'node:https'</span> <span class="hljs-keyword">import</span> http <span class="hljs-keyword">from</span> <span class="hljs-string">'node:http'</span> <span class="hljs-keyword">let</span> httpPlugin = { <span class="hljs-attr">name</span>: <span class="hljs-string">'http'</span>, <span class="hljs-function"><span class="hljs-title">setup</span>(<span class="hljs-params">build</span>)</span> { <span class="hljs-comment">// Intercept import paths starting with "http:" and "https:" so</span> <span class="hljs-comment">// esbuild doesn't attempt to map them to a file system location.</span> <span class="hljs-comment">// Tag them with the "http-url" namespace to associate them with</span> <span class="hljs-comment">// this plugin.</span> build.onResolve({ <span class="hljs-attr">filter</span>: <span class="hljs-regexp">/^https?:\/\//</span> }, <span class="hljs-function"><span class="hljs-params">args</span> =></span> ({ <span class="hljs-attr">path</span>: args.path, <span class="hljs-attr">namespace</span>: <span class="hljs-string">'http-url'</span>, })) <span class="hljs-comment">// We also want to intercept all import paths inside downloaded</span> <span class="hljs-comment">// files and resolve them against the original URL. All of these</span> <span class="hljs-comment">// files will be in the "http-url" namespace. Make sure to keep</span> <span class="hljs-comment">// the newly resolved URL in the "http-url" namespace so imports</span> <span class="hljs-comment">// inside it will also be resolved as URLs recursively.</span> build.onResolve({ <span class="hljs-attr">filter</span>: <span class="hljs-regexp">/.*/</span>, namespace: <span class="hljs-string">'http-url'</span> }, <span class="hljs-function"><span class="hljs-params">args</span> =></span> ({ <span class="hljs-attr">path</span>: <span class="hljs-keyword">new</span> URL(args.path, args.importer).toString(), <span class="hljs-attr">namespace</span>: <span class="hljs-string">'http-url'</span>, })) <span class="hljs-comment">// When a URL is loaded, we want to actually download the content</span> <span class="hljs-comment">// from the internet. This has just enough logic to be able to</span> <span class="hljs-comment">// handle the example import from unpkg.com but in reality this</span> <span class="hljs-comment">// would probably need to be more complex.</span> build.onLoad({ <span class="hljs-attr">filter</span>: <span class="hljs-regexp">/.*/</span>, namespace: <span class="hljs-string">'http-url'</span> }, <span class="hljs-keyword">async</span> (args) => { <span class="hljs-keyword">let</span> contents = <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =></span> { <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fetch</span>(<span class="hljs-params">url</span>) </span>{ <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Downloading: <span class="hljs-subst">${url}</span>`</span>) <span class="hljs-keyword">let</span> lib = url.startsWith(<span class="hljs-string">'https'</span>) ? https : http <span class="hljs-keyword">let</span> req = lib.get(url, <span class="hljs-function"><span class="hljs-params">res</span> =></span> { <span class="hljs-keyword">if</span> ([<span class="hljs-number">301</span>, <span class="hljs-number">302</span>, <span class="hljs-number">307</span>].includes(res.statusCode)) { fetch(<span class="hljs-keyword">new</span> URL(res.headers.location, url).toString()) req.abort() } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (res.statusCode === <span class="hljs-number">200</span>) { <span class="hljs-keyword">let</span> chunks = [] res.on(<span class="hljs-string">'data'</span>, <span class="hljs-function"><span class="hljs-params">chunk</span> =></span> chunks.push(chunk)) res.on(<span class="hljs-string">'end'</span>, <span class="hljs-function">() =></span> resolve(Buffer.concat(chunks))) } <span class="hljs-keyword">else</span> { reject(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`GET <span class="hljs-subst">${url}</span> failed: status <span class="hljs-subst">${res.statusCode}</span>`</span>)) } }).on(<span class="hljs-string">'error'</span>, reject) } fetch(args.path) }) <span class="hljs-keyword">return</span> { contents } }) }, } <span class="hljs-keyword">await</span> esbuild.build({ <span class="hljs-attr">entryPoints</span>: [<span class="hljs-string">'app.js'</span>], <span class="hljs-attr">bundle</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">outfile</span>: <span class="hljs-string">'out.js'</span>, <span class="hljs-attr">plugins</span>: [httpPlugin], }) </pre> <pre class="switchable go2"><span class="hljs-keyword">package</span> main <span class="hljs-keyword">import</span> <span class="hljs-string">"io/ioutil"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"net/http"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"net/url"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"os"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"github.com/evanw/esbuild/pkg/api"</span> <span class="hljs-keyword">var</span> httpPlugin = api.Plugin{ Name: <span class="hljs-string">"http"</span>, Setup: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(build api.PluginBuild)</span></span> { <span class="hljs-comment">// Intercept import paths starting with "http:" and "https:" so</span> <span class="hljs-comment">// esbuild doesn't attempt to map them to a file system location.</span> <span class="hljs-comment">// Tag them with the "http-url" namespace to associate them with</span> <span class="hljs-comment">// this plugin.</span> build.OnResolve(api.OnResolveOptions{Filter: <span class="hljs-string">`^https?://`</span>}, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(args api.OnResolveArgs)</span> <span class="hljs-params">(api.OnResolveResult, error)</span></span> { <span class="hljs-keyword">return</span> api.OnResolveResult{ Path: args.Path, Namespace: <span class="hljs-string">"http-url"</span>, }, <span class="hljs-literal">nil</span> }) <span class="hljs-comment">// We also want to intercept all import paths inside downloaded</span> <span class="hljs-comment">// files and resolve them against the original URL. All of these</span> <span class="hljs-comment">// files will be in the "http-url" namespace. Make sure to keep</span> <span class="hljs-comment">// the newly resolved URL in the "http-url" namespace so imports</span> <span class="hljs-comment">// inside it will also be resolved as URLs recursively.</span> build.OnResolve(api.OnResolveOptions{Filter: <span class="hljs-string">".*"</span>, Namespace: <span class="hljs-string">"http-url"</span>}, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(args api.OnResolveArgs)</span> <span class="hljs-params">(api.OnResolveResult, error)</span></span> { base, err := url.Parse(args.Importer) <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> api.OnResolveResult{}, err } relative, err := url.Parse(args.Path) <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> api.OnResolveResult{}, err } <span class="hljs-keyword">return</span> api.OnResolveResult{ Path: base.ResolveReference(relative).String(), Namespace: <span class="hljs-string">"http-url"</span>, }, <span class="hljs-literal">nil</span> }) <span class="hljs-comment">// When a URL is loaded, we want to actually download the content</span> <span class="hljs-comment">// from the internet. This has just enough logic to be able to</span> <span class="hljs-comment">// handle the example import from unpkg.com but in reality this</span> <span class="hljs-comment">// would probably need to be more complex.</span> build.OnLoad(api.OnLoadOptions{Filter: <span class="hljs-string">".*"</span>, Namespace: <span class="hljs-string">"http-url"</span>}, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(args api.OnLoadArgs)</span> <span class="hljs-params">(api.OnLoadResult, error)</span></span> { res, err := http.Get(args.Path) <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> api.OnLoadResult{}, err } <span class="hljs-keyword">defer</span> res.Body.Close() bytes, err := ioutil.ReadAll(res.Body) <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> api.OnLoadResult{}, err } contents := <span class="hljs-keyword">string</span>(bytes) <span class="hljs-keyword">return</span> api.OnLoadResult{Contents: &contents}, <span class="hljs-literal">nil</span> }) }, } <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> { result := api.Build(api.BuildOptions{ EntryPoints: []<span class="hljs-keyword">string</span>{<span class="hljs-string">"app.js"</span>}, Bundle: <span class="hljs-literal">true</span>, Outfile: <span class="hljs-string">"out.js"</span>, Plugins: []api.Plugin{httpPlugin}, Write: <span class="hljs-literal">true</span>, }) <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(result.Errors) > <span class="hljs-number">0</span> { os.Exit(<span class="hljs-number">1</span>) } } </pre><p>The plugin first uses a resolver to move <code>http://</code> and <code>https://</code> URLs to the <code>http-url</code> namespace. Setting the namespace tells esbuild to not treat these paths as file system paths. Then, a loader for the <code>http-url</code> namespace downloads the module and returns the contents to esbuild. From there, another resolver for import paths inside modules in the <code>http-url</code> namespace picks up relative paths and translates them into full URLs by resolving them against the importing module's URL. That then feeds back into the loader allowing downloaded modules to download additional modules recursively.</p><h3 id="webassembly-plugin"><a class="permalink" href="#webassembly-plugin">#</a>WebAssembly plugin</h3><p><em>This example demonstrates: working with binary data, creating virtual modules using import statements, re-using the same path with different namespaces.</em></p><p>This plugin allows you to import <code>.wasm</code> files into JavaScript code. It does not generate the WebAssembly files themselves; that can either be done by another tool or by modifying this example plugin to suit your needs. It enables the following workflow:</p><pre><span class="hljs-keyword">import</span> load <span class="hljs-keyword">from</span> <span class="hljs-string">'./example.wasm'</span> load(imports).then(<span class="hljs-function"><span class="hljs-params">exports</span> =></span> { ... })</pre><p>When you import a <code>.wasm</code> file, this plugin generates a virtual JavaScript module in the <code>wasm-stub</code> namespace with a single function that loads the WebAssembly module exported as the default export. That stub module looks something like this:</p><pre><span class="hljs-keyword">import</span> wasm <span class="hljs-keyword">from</span> <span class="hljs-string">'/path/to/example.wasm'</span> <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> (imports) => WebAssembly.instantiate(wasm, imports).then( <span class="hljs-function"><span class="hljs-params">result</span> =></span> result.instance.exports)</pre><p>Then that stub module imports the WebAssembly file itself as another module in the <code>wasm-binary</code> namespace using esbuild's built-in <a href="/content-types/#binary">binary</a> loader. This means importing a <code>.wasm</code> file actually generates two virtual modules. Here's the code for the plugin:</p><div class="switcher"> <a href="javascript:void 0" class="js2">JS</a> <a href="javascript:void 0" class="go2">Go</a> </div> <pre class="switchable js2"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> esbuild <span class="hljs-keyword">from</span> <span class="hljs-string">'esbuild'</span> <span class="hljs-keyword">import</span> path <span class="hljs-keyword">from</span> <span class="hljs-string">'node:path'</span> <span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">'node:fs'</span> <span class="hljs-keyword">let</span> wasmPlugin = { <span class="hljs-attr">name</span>: <span class="hljs-string">'wasm'</span>, <span class="hljs-function"><span class="hljs-title">setup</span>(<span class="hljs-params">build</span>)</span> { <span class="hljs-comment">// Resolve ".wasm" files to a path with a namespace</span> build.onResolve({ <span class="hljs-attr">filter</span>: <span class="hljs-regexp">/\.wasm$/</span> }, <span class="hljs-function"><span class="hljs-params">args</span> =></span> { <span class="hljs-comment">// If this is the import inside the stub module, import the</span> <span class="hljs-comment">// binary itself. Put the path in the "wasm-binary" namespace</span> <span class="hljs-comment">// to tell our binary load callback to load the binary file.</span> <span class="hljs-keyword">if</span> (args.namespace === <span class="hljs-string">'wasm-stub'</span>) { <span class="hljs-keyword">return</span> { <span class="hljs-attr">path</span>: args.path, <span class="hljs-attr">namespace</span>: <span class="hljs-string">'wasm-binary'</span>, } } <span class="hljs-comment">// Otherwise, generate the JavaScript stub module for this</span> <span class="hljs-comment">// ".wasm" file. Put it in the "wasm-stub" namespace to tell</span> <span class="hljs-comment">// our stub load callback to fill it with JavaScript.</span> <span class="hljs-comment">//</span> <span class="hljs-comment">// Resolve relative paths to absolute paths here since this</span> <span class="hljs-comment">// resolve callback is given "resolveDir", the directory to</span> <span class="hljs-comment">// resolve imports against.</span> <span class="hljs-keyword">if</span> (args.resolveDir === <span class="hljs-string">''</span>) { <span class="hljs-keyword">return</span> <span class="hljs-comment">// Ignore unresolvable paths</span> } <span class="hljs-keyword">return</span> { <span class="hljs-attr">path</span>: path.isAbsolute(args.path) ? args.path : path.join(args.resolveDir, args.path), <span class="hljs-attr">namespace</span>: <span class="hljs-string">'wasm-stub'</span>, } }) <span class="hljs-comment">// Virtual modules in the "wasm-stub" namespace are filled with</span> <span class="hljs-comment">// the JavaScript code for compiling the WebAssembly binary. The</span> <span class="hljs-comment">// binary itself is imported from a second virtual module.</span> build.onLoad({ <span class="hljs-attr">filter</span>: <span class="hljs-regexp">/.*/</span>, namespace: <span class="hljs-string">'wasm-stub'</span> }, <span class="hljs-keyword">async</span> (args) => ({ <span class="hljs-attr">contents</span>: <span class="hljs-string">`import wasm from <span class="hljs-subst">${<span class="hljs-built_in">JSON</span>.stringify(args.path)}</span> export default (imports) => WebAssembly.instantiate(wasm, imports).then( result => result.instance.exports)`</span>, })) <span class="hljs-comment">// Virtual modules in the "wasm-binary" namespace contain the</span> <span class="hljs-comment">// actual bytes of the WebAssembly file. This uses esbuild's</span> <span class="hljs-comment">// built-in "binary" loader instead of manually embedding the</span> <span class="hljs-comment">// binary data inside JavaScript code ourselves.</span> build.onLoad({ <span class="hljs-attr">filter</span>: <span class="hljs-regexp">/.*/</span>, namespace: <span class="hljs-string">'wasm-binary'</span> }, <span class="hljs-keyword">async</span> (args) => ({ <span class="hljs-attr">contents</span>: <span class="hljs-keyword">await</span> fs.promises.readFile(args.path), <span class="hljs-attr">loader</span>: <span class="hljs-string">'binary'</span>, })) }, } <span class="hljs-keyword">await</span> esbuild.build({ <span class="hljs-attr">entryPoints</span>: [<span class="hljs-string">'app.js'</span>], <span class="hljs-attr">bundle</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">outfile</span>: <span class="hljs-string">'out.js'</span>, <span class="hljs-attr">plugins</span>: [wasmPlugin], }) </pre> <pre class="switchable go2"><span class="hljs-keyword">package</span> main <span class="hljs-keyword">import</span> <span class="hljs-string">"encoding/json"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"io/ioutil"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"os"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"path/filepath"</span> <span class="hljs-keyword">import</span> <span class="hljs-string">"github.com/evanw/esbuild/pkg/api"</span> <span class="hljs-keyword">var</span> wasmPlugin = api.Plugin{ Name: <span class="hljs-string">"wasm"</span>, Setup: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(build api.PluginBuild)</span></span> { <span class="hljs-comment">// Resolve ".wasm" files to a path with a namespace</span> build.OnResolve(api.OnResolveOptions{Filter: <span class="hljs-string">`\.wasm$`</span>}, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(args api.OnResolveArgs)</span> <span class="hljs-params">(api.OnResolveResult, error)</span></span> { <span class="hljs-comment">// If this is the import inside the stub module, import the</span> <span class="hljs-comment">// binary itself. Put the path in the "wasm-binary" namespace</span> <span class="hljs-comment">// to tell our binary load callback to load the binary file.</span> <span class="hljs-keyword">if</span> args.Namespace == <span class="hljs-string">"wasm-stub"</span> { <span class="hljs-keyword">return</span> api.OnResolveResult{ Path: args.Path, Namespace: <span class="hljs-string">"wasm-binary"</span>, }, <span class="hljs-literal">nil</span> } <span class="hljs-comment">// Otherwise, generate the JavaScript stub module for this</span> <span class="hljs-comment">// ".wasm" file. Put it in the "wasm-stub" namespace to tell</span> <span class="hljs-comment">// our stub load callback to fill it with JavaScript.</span> <span class="hljs-comment">//</span> <span class="hljs-comment">// Resolve relative paths to absolute paths here since this</span> <span class="hljs-comment">// resolve callback is given "resolveDir", the directory to</span> <span class="hljs-comment">// resolve imports against.</span> <span class="hljs-keyword">if</span> args.ResolveDir == <span class="hljs-string">""</span> { <span class="hljs-keyword">return</span> api.OnResolveResult{}, <span class="hljs-literal">nil</span> <span class="hljs-comment">// Ignore unresolvable paths</span> } <span class="hljs-keyword">if</span> !filepath.IsAbs(args.Path) { args.Path = filepath.Join(args.ResolveDir, args.Path) } <span class="hljs-keyword">return</span> api.OnResolveResult{ Path: args.Path, Namespace: <span class="hljs-string">"wasm-stub"</span>, }, <span class="hljs-literal">nil</span> }) <span class="hljs-comment">// Virtual modules in the "wasm-stub" namespace are filled with</span> <span class="hljs-comment">// the JavaScript code for compiling the WebAssembly binary. The</span> <span class="hljs-comment">// binary itself is imported from a second virtual module.</span> build.OnLoad(api.OnLoadOptions{Filter: <span class="hljs-string">`.*`</span>, Namespace: <span class="hljs-string">"wasm-stub"</span>}, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(args api.OnLoadArgs)</span> <span class="hljs-params">(api.OnLoadResult, error)</span></span> { bytes, err := json.Marshal(args.Path) <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> api.OnLoadResult{}, err } contents := <span class="hljs-string">`import wasm from `</span> + <span class="hljs-keyword">string</span>(bytes) + <span class="hljs-string">` export default (imports) => WebAssembly.instantiate(wasm, imports).then( result => result.instance.exports)`</span> <span class="hljs-keyword">return</span> api.OnLoadResult{Contents: &contents}, <span class="hljs-literal">nil</span> }) <span class="hljs-comment">// Virtual modules in the "wasm-binary" namespace contain the</span> <span class="hljs-comment">// actual bytes of the WebAssembly file. This uses esbuild's</span> <span class="hljs-comment">// built-in "binary" loader instead of manually embedding the</span> <span class="hljs-comment">// binary data inside JavaScript code ourselves.</span> build.OnLoad(api.OnLoadOptions{Filter: <span class="hljs-string">`.*`</span>, Namespace: <span class="hljs-string">"wasm-binary"</span>}, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(args api.OnLoadArgs)</span> <span class="hljs-params">(api.OnLoadResult, error)</span></span> { bytes, err := ioutil.ReadFile(args.Path) <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> api.OnLoadResult{}, err } contents := <span class="hljs-keyword">string</span>(bytes) <span class="hljs-keyword">return</span> api.OnLoadResult{ Contents: &contents, Loader: api.LoaderBinary, }, <span class="hljs-literal">nil</span> }) }, } <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> { result := api.Build(api.BuildOptions{ EntryPoints: []<span class="hljs-keyword">string</span>{<span class="hljs-string">"app.js"</span>}, Bundle: <span class="hljs-literal">true</span>, Outfile: <span class="hljs-string">"out.js"</span>, Plugins: []api.Plugin{wasmPlugin}, Write: <span class="hljs-literal">true</span>, }) <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(result.Errors) > <span class="hljs-number">0</span> { os.Exit(<span class="hljs-number">1</span>) } } </pre><p>The plugin works in multiple steps. First, a resolve callback captures <code>.wasm</code> paths in normal modules and moves them to the <code>wasm-stub</code> namespace. Then load callback for the <code>wasm-stub</code> namespace generates a JavaScript stub module that exports the loader function and imports the <code>.wasm</code> path. This invokes the resolve callback again which this time moves the path to the <code>wasm-binary</code> namespace. Then the second load callback for the <code>wasm-binary</code> namespace causes the WebAssembly file to be loaded using the <code>binary</code> loader, which tells esbuild to embed the file itself in the bundle.</p><h3 id="svelte-plugin"><a class="permalink" href="#svelte-plugin">#</a>Svelte plugin</h3><p><em>This example demonstrates: supporting a compile-to-JavaScript language, reporting warnings and errors, integrating source maps.</em></p><p>This plugin allows you to bundle <code>.svelte</code> files, which are from the <a href="https://svelte.dev/">Svelte</a> framework. You write code in an HTML-like syntax that is then converted to JavaScript by the Svelte compiler. Svelte code looks something like this:</p><pre><span class="hljs-tag"><<span class="hljs-name">script</span>></span><span class="javascript"> <span class="hljs-keyword">let</span> a = <span class="hljs-number">1</span>; <span class="hljs-keyword">let</span> b = <span class="hljs-number">2</span>; </span><span class="hljs-tag"></<span class="hljs-name">script</span>></span> <span class="hljs-tag"><<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"number"</span> <span class="hljs-attr">bind:value</span>=<span class="hljs-string">{a}</span>></span> <span class="hljs-tag"><<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"number"</span> <span class="hljs-attr">bind:value</span>=<span class="hljs-string">{b}</span>></span> <span class="hljs-tag"><<span class="hljs-name">p</span>></span>{a} + {b} = {a + b}<span class="hljs-tag"></<span class="hljs-name">p</span>></span></pre><p>Compiling this code with the Svelte compiler generates a JavaScript module that depends on the <code>svelte/internal</code> package and that exports the component as a a single class using the <code>default</code> export. This means <code>.svelte</code> files can be compiled independently, which makes Svelte a good fit for an esbuild plugin. This plugin is triggered by importing a <code>.svelte</code> file like this:</p><pre><span class="hljs-keyword">import</span> Button <span class="hljs-keyword">from</span> <span class="hljs-string">'./button.svelte'</span></pre><p>Here's the code for the plugin (there is no Go version of this plugin because the Svelte compiler is written in JavaScript):</p><pre class="js1"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> esbuild <span class="hljs-keyword">from</span> <span class="hljs-string">'esbuild'</span> <span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> svelte <span class="hljs-keyword">from</span> <span class="hljs-string">'svelte/compiler'</span> <span class="hljs-keyword">import</span> path <span class="hljs-keyword">from</span> <span class="hljs-string">'node:path'</span> <span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">'node:fs'</span> <span class="hljs-keyword">let</span> sveltePlugin = { <span class="hljs-attr">name</span>: <span class="hljs-string">'svelte'</span>, <span class="hljs-function"><span class="hljs-title">setup</span>(<span class="hljs-params">build</span>)</span> { build.onLoad({ <span class="hljs-attr">filter</span>: <span class="hljs-regexp">/\.svelte$/</span> }, <span class="hljs-keyword">async</span> (args) => { <span class="hljs-comment">// This converts a message in Svelte's format to esbuild's format</span> <span class="hljs-keyword">let</span> convertMessage = <span class="hljs-function">(<span class="hljs-params">{ message, start, end }</span>) =></span> { <span class="hljs-keyword">let</span> location <span class="hljs-keyword">if</span> (start && end) { <span class="hljs-keyword">let</span> lineText = source.split(<span class="hljs-regexp">/\r\n|\r|\n/g</span>)[start.line - <span class="hljs-number">1</span>] <span class="hljs-keyword">let</span> lineEnd = start.line === end.line ? end.column : lineText.length location = { <span class="hljs-attr">file</span>: filename, <span class="hljs-attr">line</span>: start.line, <span class="hljs-attr">column</span>: start.column, <span class="hljs-attr">length</span>: lineEnd - start.column, lineText, } } <span class="hljs-keyword">return</span> { <span class="hljs-attr">text</span>: message, location } } <span class="hljs-comment">// Load the file from the file system</span> <span class="hljs-keyword">let</span> source = <span class="hljs-keyword">await</span> fs.promises.readFile(args.path, <span class="hljs-string">'utf8'</span>) <span class="hljs-keyword">let</span> filename = path.relative(process.cwd(), args.path) <span class="hljs-comment">// Convert Svelte syntax to JavaScript</span> <span class="hljs-keyword">try</span> { <span class="hljs-keyword">let</span> { js, warnings } = svelte.compile(source, { filename }) <span class="hljs-keyword">let</span> contents = js.code + <span class="hljs-string">`//# sourceMappingURL=`</span> + js.map.toUrl() <span class="hljs-keyword">return</span> { contents, <span class="hljs-attr">warnings</span>: warnings.map(convertMessage) } } <span class="hljs-keyword">catch</span> (e) { <span class="hljs-keyword">return</span> { <span class="hljs-attr">errors</span>: [convertMessage(e)] } } }) } } <span class="hljs-keyword">await</span> esbuild.build({ <span class="hljs-attr">entryPoints</span>: [<span class="hljs-string">'app.js'</span>], <span class="hljs-attr">bundle</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">outfile</span>: <span class="hljs-string">'out.js'</span>, <span class="hljs-attr">plugins</span>: [sveltePlugin], }) </pre><p>This plugin only needs a load callback, not a resolve callback, because it's simple enough that it just needs to transform the loaded code into JavaScript without worrying about where the code comes from.</p><p>It appends a <code>//# sourceMappingURL=</code> comment to the generated JavaScript to tell esbuild how to map the generated JavaScript back to the original source code. If source maps are enabled during the build, esbuild will use this to ensure that the generated positions in the final source map are mapped all the way back to the original Svelte file instead of to the intermediate JavaScript code.</p><h2 id="plugin-api-limitations"><a class="permalink" href="#plugin-api-limitations">#</a>Plugin API limitations</h2><p>This API does not intend to cover all use cases. It's not possible to hook into every part of the bundling process. For example, it's not currently possible to modify the AST directly. This restriction exists to preserve the excellent performance characteristics of esbuild as well as to avoid exposing too much API surface which would be a maintenance burden and would prevent improvements that involve changing the AST.</p><p>One way to think about esbuild is as a "linker" for the web. Just like a linker for native code, esbuild's job is to take a set of files, resolve and bind references between them, and generate a single file containing all of the code linked together. A plugin's job is to generate the individual files that end up being linked.</p><p>Plugins in esbuild work best when they are relatively scoped and only customize a small aspect of the build. For example, a plugin for a special configuration file in a custom format (e.g. YAML) is very appropriate. The more plugins you use, the slower your build will get, especially if your plugin is written in JavaScript. If a plugin applies to every file in your build, then your build will likely be very slow. If caching is applicable, it must be done by the plugin itself.</p></main></body></html>