Photo by Uillian Vargas on Unsplash
I recently published a new open source plugin to export Figma frames to DeckDeckGo slides.
As I like to benefit from my experiences to learn and try new concept, instead of using a bundler as described in the Figma documentation, I decided to give a try to esbuild.
The least I can say, I loved it ❤️.
Foreword
Following solution is the one I set up for my plugin. It does work like a charm but, notably because it was the first time I used esbuild, it might need some improvements. If you notice improvements or issues, let me know, I would like to hear from you!
Contributions to my plugin and PR are also welcomed 😉.
Setup
In a Figma plugin, install both esbuild
and rimraf
.
npm i esbuild rimraf --save-dev
rimraf
might not be needed, if you only build your project in a CI, nevertheless, for a local build, I think it is safer to delete the output directory before any new build.
In package.json
add, or modify, the build
script.
"scripts": {
"build": "rimraf dist && node ./esbuild.js"
}
You might notice that the last command target a script called esbuild.js
. This file will contain our bundling steps, therefore create such a new file at the root of your project.
touch esbuild.js
Finally, in this newly created file, import esbuild
.
const esbuild = require("esbuild");
Sandbox
A Figma plugin run (see documentation) in a combination of a sandbox, to access to the Figma nodes, and an iFrame, for the presentation layer. We set up firstly the sandbox’s build.
// sandbox
esbuild
.build({
entryPoints: ["src/plugin.ts"],
bundle: true,
platform: "node",
target: ["node10.4"],
outfile: "dist/plugin.js"
})
.catch(() => process.exit(1));
In the above script, we bundle the plugin.ts
, the sandbox’s code, to its JavaScript counterpart plugin.js
. As configuration, we tell esbuild
to treat it as a NodeJS platform and we target the version 10.4.
I configured my plugin to handle sources from a folder
src
. I also bundle the outcome to another one calleddist
. If you do not have the same structure, modify the solution accordingly.
UI
In comparison to the previous chapter, we are going to gather the results of the build instead of telling esbuild
to write directly to a file. For such reason, we import NodeJS fs
to interact with the file system.
const { readFile, writeFile } = require("fs").promises;
We also install html-minifier-terser
to minify the resulting HTML code.
npm i html-minifier-terser --save-dev
Once installed, we add a related import to our build script too.
const minify = require("html-minifier-terser").minify;
These imports set, we implement the bundling.
// iframe UI
(async () => {
const script = esbuild.buildSync({
entryPoints: ["src/ui.ts"],
bundle: true,
minify: true,
write: false,
target: ["chrome58", "firefox57", "safari11", "edge16"]
});
const html = await readFile("src/ui.html", "utf8");
const minifyOptions = {
collapseWhitespace: true,
keepClosingSlash: true,
removeComments: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true,
minifyCSS: true
};
await writeFile(
"dist/ui.html",
`<script>${script.outputFiles[0].text}</script>${await minify(html, minifyOptions)}`
);
})();
In the above script, we compile the ui.ts
, our TypeScript code related to the UI, with esbuild
. We instruct it to inline any imported dependencies into the file itself with the option bundle, we minify the JavaScript code and, we do not write to the file system. Instead of such step, we gather the outcome in a variable I called script
.
We read the ui.html
source file, define some options for the HTML minification and, finally, write both compiled code and HTML to the output (dist/ui.html
in this example).
Web Components
Of course, I had to create some Web Components for my projects 😉. Integrating these follow same logic as previously, except that we use the esm format.
const buildWebComponents = (entryPoints) =>
entryPoints
.map((entryPoint) =>
esbuild.buildSync({
entryPoints: [entryPoint],
bundle: true,
minify: true,
write: false,
target: ["chrome58", "firefox57", "safari11", "edge16"],
format: "esm"
})
)
.map((componentScript) => componentScript.outputFiles[0].text)
.join("");
(async () => {
const componentsScript = buildWebComponents([
"src/components/checkbox.ts",
"src/components/button.ts",
"src/components/spinner.ts",
"src/components/fonts.ts"
]);
// Same as previous chapter
await writeFile(
"dist/ui.html",
`<script>${
script.outputFiles[0].text
}</script><script type="module">${componentsScript}</script>${await minify(
html,
minifyOptions
)}`
);
})();
I created more than one Web Component (checkbox.ts
, button.ts
, etc.), that’s why the buildWebComponents
function. It takes an array, a list of files, as parameter and, concat all bundle together to a single value.
And...that’s it 😃. Sandbox, UI and Web Components are bundled faster than ever ⚡️.
Repo
You can find above solution and, other fun stuff in the open source repo of my plugin: https://github.com/deckgo/figma-deckdeckgo-plugin
Summary
Setting up a project with esbuild was a pleasant developer experience. Writing a JS script to bundle my project, without many dependencies and with much flexibility, definitely matches my current inspiration. In addition, the outcome, the bundling itself, is faaaaaaaaaaaaaast! I am looking forward to use this compiler in other projects 👍.
To infinity and beyond!
David