Figma exposes an extensive API for creating plugins in javascript. While working on Relume Sitemaps, we wanted to share typescript code between our main application (outside of Figma) and our figma plugin; things like our API clients and data schemas.
At first this sounded easy. I've since learned that not all "javascript" runtimes are created equal.
Through trial and error, we worked around these oddities by reviving the "glorious" bundling practices of the 2010s.
A Figma plugin actually runs 2 separate scripts in 2 very different runtimes:
<script>
tags inline) and figma will load it as a data-url. This iframe can't access any figma
APIs, so it can't edit the user's figma design; must post, but it can post messages to your background.js script.figma
APIs, message your page script and make fetch
requests.The page runtime is relatively normal. Figma is an Electron app, so it runs a recent version of Chromium. You can write code and it'll run like in a normal browser. We did run into some CORS when requesting assets (since you're in window.location.origin = "null"
) and storage limitations (data URLs can't use cookie
s or localStorage
), but hose are pretty self explanatory.
On the other hand, the background runtime is full of surprises.
The background.js code runs inside of Figma's origin. This would pose a security risk (your plugin could steal session cookies, etc), so Figma built something to sandbox the background.js execution. Originally, Figma tried using the Realms API which would've run your code using the regular browser VM. Some might say they mucked up because following valunerability findings, they totally changed tact 2 months later. Now the background.js script runs in a custom JS VM, compiled to WASM, running inside of Figma's origin.
When Figma announced the switch in 2019, they used QuickJS as their custom VM. Either Figma hasn't updated QuickJS since, or they've moved to a different VM, because Figma's VM is missing many of the features QuickJS now supports.
figma_app.min.js.br:5 Syntax error on line 96: Unexpected token ...
..._sentry_core__WEBPACK_IMPORTED_MODULE_0__.TRACING_DEFAULTS,
^
Aside: make sure to enable "Use developer VM" from the Figma menu, as it adds the code previews to these error messages.
Cause: Many of our dependencies (from node_modules
) were compiled targeting ES6. Destructuring (e.g. x = { y, ...z }
) is an ES6 feature, and has been widely supported since 2016. However, Figma's VM can't parse it.
Solution: We added Babel to our build stack. Babel converts all the code (including node_modules
) to ES5. Specially we use @babel/preset-env
which targets ancient browsers when no configuration is given.
Error: [MobX] MobX requires global 'Symbol' to be available or polyfilled
Cause: Figma's background runtime doesn't have a global object. There's no window
(the old fashioned one), no self
(the webworker-inclusive one), no global
(the node one) nor is there a globalThis
(the modern one).
This isn't usually a problem, as most of the object you'd expect in the global object (e.g. window.Symbol
) are more commonly accessed via the global scope (e.g. just Symbol
). Figma's runtime still supports the later.
This becomes a problem when some libraries try to feature-detect. It's easier to check if globalThis.Symbol === undefined
than catch a reference error thrown by Symbol
. Libraries doing the former will erroneously detect Figma's background environment as missing the feature in question.
Solution: We create a global object with the features needed by our library. We're bundling with webpack, which compiles Node's global
into __webpack_require__.g
. So it was easy to drop in this line before our library imports:
// mobx feature detects for Symbol, Map & Set
__webpack_require__.g = { Symbol, Map, Set };
import { observable } from 'mobx';
Cause: Figma only lets you submit one background.js
script per plugin. So they don't support import
statements; there's no way to supply the other file you're importing from. I guess in the name of error-prevention / support-ticket-reduction, Figma checks for import statements in your code.
But here's the mystery: we didn't have any import statements in our code! Webpack removes import statement while bundling.
As any seasoned developer knows, a regex is the best way to parse HTML. Figma's developers are well seasoned, so they opted to use a regex on JS too:
/\bimport\s*(?:\(|\/[/*]|<!--|-->)/
I'm not sure why they use this regex as it doesn't match well formed import statement; neither import 'foo.js'
or import { bar } from 'foo.js
match. Perhaps it runs after another part of Figma's stack mangles the import statements, which would also explain the incorrect line numbers.
In our case it matched a comment in our dependency, which is clearly not an import statement:
// browser and replay packages should `@sentry/browser` import
// from `@sentry/replay` in the future
Solution: Strip out all comments. Any minification you perform on production builds probably already does this, we just needed to enable comment stripping in babel for our development builds too.
Thanks to Figma's interesting JS runtime, you can relive the glory days of JavaScript. With enough bundler & preprocessing steps, you make IE7 Figma run your ES6 (aka ES2015) code!
Now that we've figured out how to run code, let's hope their actual API doesn't have as many pitfalls...
I hope you enjoyed this article. Contact me if you have any thoughts or questions.
© 2015—2024 Sam Parkinson