How a Weekend MVP Became inDrive’s Cross-Platform Design Token Export Tool

inDrive built ExFig, an open-source Swift CLI that exports Figma design tokens and assets across iOS, Android, Flutter, Web, and Penpot. It started as a weekend fork of figma-export and grew into a production tool with Pkl configs, platform plugins, granular caching, MCP support, DocC docs, and a GitHub Action. In production, ExFig cut iOS illustration export from 154s to 37s, with cache-hit runs around 3s, and reduced Android export from 576s to 84s.


This content originally appeared on HackerNoon and was authored by inDrive.Tech

By Alexey, iOS Engineer, inDrive Design Systems Team

If you work on a shared design system across iOS, Android, and Flutter, you already know the drill: a designer updates a color in Figma, and three platform teams update their files by hand. It works fine at ten tokens. At 393 illustrations (each exported at @2x and @3x, plus dark variants — 840 PNG per run), dozens of semantic colors, a dark theme, and RTL support, it stops working entirely.

For a long time, we solved this at inDrive with figma-export, an excellent open-source Swift CLI from Red Mad Robot. I’d been contributing to it for about 18 months. But by late 2025, the gap between what we needed and what the upstream tool did had grown wide enough that no amount of workarounds would close it cleanly. So I took a weekend and built a fork.

That fork is ExFig. Four months later, it’s in production across iOS, Android, and Flutter at inDrive. This is the story of why we forked, what we built, and what the numbers looked like.

Why a Fork, Not a PR

The figma-export codebase is well-structured and actively used — and that’s exactly why some of our changes didn’t belong upstream. Several of them broke backward compatibility or changed the config format in ways that would disrupt existing users. That’s a reasonable position for a live open-source tool to take. It’s just not compatible with our timeline.

Here’s specifically what had outpaced the upstream tool by late 2025:

  • Figma Variables had become our primary token source. Figma’s Variables API matured significantly after Config 2024, and Code Connect hit stable. Our tokens now lived in Variable collections, and we needed the export tool to speak that language natively.
  • Page-level filtering was missing. figma-export filters assets by frame name only. We had three pages in a single Figma file, each containing a frame called “Icons” — one for Outlined, one for Filled, one for Colored. The upstream tool couldn’t distinguish between them.
  • Sequential downloads were too slow. figma-export fetches assets one at a time with a forced delay to avoid hitting Figma’s rate limit. That’s a reasonable default — but with 840 PNG per run, “reasonable” meant 154 seconds at the median.
  • Flutter was now in scope. We’d added a Flutter product to the design system, and the upstream tool had no Flutter support.
  • RTL handling was fragile. Our RTL flag lived as a string in the description field of a Figma component. We wanted to read it as a proper component property with explicit rtlActiveValues — but that was a schema change.
  • Automatic icon recoloring via Variable Modes was our own feature. Some icon sets used exactly two colors, both from Variable collections. We wanted ExFig to fetch the icon once, substitute dark-mode variable values, and generate the dark variant automatically.

Several of these together — especially the config format change and the breaking changes — made a fork the cleaner path. Not “rewrite from scratch” (18 months of upstream work is real value), not “plugins on top of upstream” (we’d still be dependent on their release pace), but a fork with permission to break our own backward compatibility.

The alternatives didn’t fully close the gap either. Style Dictionary is excellent for tokens but doesn’t handle images or RTL-aware SVG icons. Tokens Studio covers Figma → tokens but not Figma → .xcassets with PDF vectors. Commercial tools like Supernova work, but our scenario — one CI pipeline for four platforms from a single Pkl config — loses flexibility when wrapped in a SaaS.

v1: Clean Base, Page Filters, and Parallel Downloads

The first release (December 2025) preserved figma-export’s architecture and command structure. The migration path needed to be low-friction, so I added an exfig migrate command that converts an existing figma-export.yaml into ExFig’s format.

Page-Level Filters

The fix is a single optional field, figmaPageName, that disambiguates frames when multiple pages share the same frame name:

new iOS.IconsEntry {
    figmaFileId = "..."
    figmaPageName = "Color Icons [ready-A11y]" // page-level filter
    figmaFrameName = "Action"
}

One Figma file, multiple products split across pages — each CI job pulls only its own assets.

Parallel Downloads with Adaptive Rate Limiting

The rewrite replaces sequential fetching with a shared concurrency pool. --concurrent-downloads defaults to 20; we run it at 50 in CI. An adaptive rate limiter handles Figma’s 429 responses with exponential backoff, honoring Retry-After headers. On our 840-PNG set, the benchmark results are in the numbers section — the short version is a ×4 improvement at the median.

Web Export

Three artifacts: CSS custom properties with @media (prefers-color-scheme: dark), TypeScript constants for type-safe access, and React TSX components via the SVGR pattern. Barrel files (index.ts) are generated automatically — at 200+ icons, that’s a thousand lines of manual imports you don’t write.

Note: Web export isn’t used inside inDrive — our frontend team has its own implementation. But the feature is complete and available for teams on a React stack.

v2: Pkl Config, Plugin Architecture, and Everything That Followed

YAML worked until it didn’t. When our iOS config grew past 70 entries — 66 icon categories, plus colors and illustrations — and identical configs lived in separate Android and Flutter repositories with their own path conventions and code templates, the config files became maintenance burdens: repetitive, error-prone, and untyped.

For the 2.0 release (February 2026), I rewrote the configuration layer in Apple Pkl.

Why Pkl

Pkl is a configuration language from Apple that compiles to JSON, YAML, or properties files. It’s typed, supports imports, has a for loop, and ships official IDE plugins for IntelliJ, VSCode, and Cursor. The key properties for our use case:

  • Typed schemas. The nameStyle field accepts only camelCase or snakeCase. An invalid config fails fast, before any Figma API call.
  • Imports and amends. Common settings live in common.pkl; project configs extend them. You can reference schemas by package URL and never carry copies around.
  • IDE support. Autocomplete and inline validation in any of the three supported editors. exfig schemas extracts Pkl schemas locally for offline use.

Here’s the practical difference. Previously, 21 categories of Colored icons required 21 separate IconsEntry blocks — copy-pasted and diverged over time. In Pkl:

local coloredCategories: Mapping<String, String> = new {
  ["Action"] = "Action"
  ["Finance"] = "Finance"
  ["Communication, Media, Art"] = "CommunicationMediaArt"
  // ... 18 more pairs
}

icons = new Listing {
  for (frameName, folder in coloredCategories) {
    new iOS.IconsEntry {
      figmaFileId = "<your-figma-file-id>"
      figmaPageName = "Color Icons [ready-A11y]"
      figmaFrameName = frameName
      format = "svg"
      assetsFolder = "\(folder)Colored"
      renderMode = "default"
      imageSwift = "./Generated/\(folder)ColoredIcons.generated.swift"
    }
  }
}

Adding a 22nd category is one line. All paths regenerate via \(folder) interpolation. The equivalent YAML change is a find-and-replace across hundreds of lines with easy room to miss one.

Code Generation with swift-jinja Templates

The upstream tool used Stencil for code generation. Stencil is unmaintained. I moved to swift-jinja from Hugging Face — compatible with Jinja2, with proper filters, extensions, and documentation.

Templates for Swift, Kotlin, Dart, and TypeScript live alongside the config in templates/{colors,icons,images}/*.jinja. Here’s a real snippet from templates/colors/inDrive/UIColor+extension.swift.jinja:

{{ header }}
import UIKit
import SwiftUI

public extension Color {{ '{' }}{% for color in colors %}
    static let {{ color.name }}: Color = ThemeCompatable.colors.{{ color.name }}
{% endfor %}}

extension UIColor {{ '{' }}{% for color in colors %}
    static var {{ color.name }}: UIColor { .init(named: #function) }
{% endfor %}}

ExFig runs the parsed token data through this template. Output: Color.swift with a hundred static fields. A designer adds a new color in Figma, runs exfig batch, and it’s immediately available as Color.backgroundPrimary.

Granular Cache by Figma nodeId

ExFig stores a hash of each node’s content and metadata after every run. If a node hasn’t changed, ExFig skips the API call and the disk write. On our 840-PNG asset set, a cache-hit run takes about 3 seconds instead of 154. In CI terms: open a PR with no design changes, and the export step costs almost nothing. Enabled with --experimental-granular-cache.

Three Dark Theme Modes

figma-export supported two dark theme approaches: two separate Figma files, or a suffix in the layer name. ExFig adds a third: Variable Modes. For our newer icon sets — those that use exactly two colors, both from Variable collections — designers now draw only the light version. ExFig substitutes the dark-mode variable values and assembles the dark variant automatically. Previously, designers had to draw every icon twice.

Android-Specific Handling

AAPT (Android’s asset packaging tool) will reject any pathData value longer than 32,767 bytes. Long icons silently render as empty circles; on older devices, they crash. ExFig validates this before export (strictPathValidation) and can reduce pathPrecision (1–6 decimal places) to shorten SVG path commands. It also manages Android themeAttributes blocks, handling sections in attrs.xml, styles.xml, and styles-night.xml between configurable markers.

W3C DTCG Token Export

exfig download tokens --format w3c --w3c-version v2025 exports tokens in the W3C Design Token Community Group format, readable by Style Dictionary, Tokens Studio, and other DTCG tools without converters. Both v2025 and legacy hex (v1) are supported. exfig tokens convert filters an already-generated .tokens.json by group and type.

v3: Penpot, an MCP Server, and exfig lint

By spring 2026, the ExFig GitHub issues had a recurring request from the open-source community: does this work with anything besides Figma? Internally at inDrive, we don’t use Penpot. But supporting it as a community contribution was a reasonable use of a well-motivated abstraction. More importantly, it gave us a clean reason to decouple the “design source” concept properly.

Inside ExFig, there’s no longer a hardcoded “Figma” layer. Instead, there’s a design source protocol:

protocol ColorsProvider { func fetchColors() async throws -> [Color] }
protocol IconsProvider  { func fetchIcons()  async throws -> [Icon]  }
protocol ImagesProvider { func fetchImages() async throws -> [Image] }

FigmaSource and PenpotSource implement the same protocols. exfig batch exfig.pkl accepts either — you change one field in the config. Plugins don’t know which source they’re working with; they operate on the parsed data model.

There’s also a local W3C DTCG file source for teams whose tokens are already exported separately:

tokensFile = new TokensFile {
    path = "./design-tokens/colors.tokens.json"
}

No Figma token required at all.

MCP Server

exfig mcp starts a JSON-RPC server over stdio. Claude Code and similar AI coding assistants connect to it via .mcp.json. The tool surface includes: exfigvalidate, exfiginspect (what’s in the Figma file), exfigexport, exfigdownload (inline DTCG), exfigtokensinfo; all Pkl schemas and config templates as Resources; and setup-config and troubleshoot-export Prompts.

A companion plugin set for the Claude Code marketplace (DesignPipe/exfig-plugins) adds slash commands: /export-colors, /export-icons, /export-all, /exfig-config-review, /exfig-troubleshooting.

The practical upshot: connect ExFig to Claude Code, write “update the icons on the home screen,” and the agent calls exfig_export through the MCP tool and places the assets in the project. The MCP server isn’t a toy feature — it’s the reason we didn’t add a REST API. stdio JSON-RPC is simpler to maintain and directly supported by Claude Code’s extension protocol.

exfig lint

exfig lint validates a Figma file before export and catches common mistakes that otherwise surface as confusing build failures. Current rules:

  • frame-page-match — the frame isn’t on the page specified in the config
  • naming-convention — names don’t match the configured nameValidateRegexp
  • component-not-frame — an icon is a frame instead of a component (loses the variant API)
  • duplicate-component-names — the same ic_arrow appears under two names in a variant set
  • path-data-length — SVG path > 32,767 bytes (will fail on Android)
  • dark-mode-variables / dark-mode-suffix — dark theme configuration conflicts
  • alias-chain-integrity — Variable aliases point to deleted nodes
  • invalid-rtl-variant-value — RTL flag value not in rtlActiveValues
  • deleted-variables — a token in code references a deleted Variable

Flags: --rules , --format json, --severity error. JSON mode returns a non-zero exit code on errors, making it easy to plug into pre-commit hooks and CI.

Internally, this lint runs on a Figma publish webhook. [Editor’s note: worth a sentence or two explaining the n8n + GitHub Actions setup here for readers unfamiliar with the inDrive CI stack.] When designers publish a new library version, a GitHub Action (06figmalint.yml) runs exfig lint and posts the report to Slack. Frame conflicts, naming violations, and dark theme mismatches land in the design team’s Slack before any engineer has tried to run an export. Previously, we caught those errors in Android Studio, where the symptom is “empty circle instead of arrow”.

Other Commands

exfig fetch is an interactive wizard for pulling a single asset without a config file. exfig init -p ios | -p android | -p flutter | -p web is non-interactive platform scaffolding. Small conveniences, but they close the gap for the frequent question: “how do I grab one icon on the fly?”

The Stack: Swift 6.3, Cross-Platform, Noora, and DocC

ExFig builds on macOS, Linux, and Windows. macOS is the primary development platform. Linux exists for CI: Linux runners on GitHub Actions cost roughly one-tenth of macOS runners, so lint, core tests, and non-Xcode builds run there. Windows was added in v3.0: some dependencies are conditionally excluded via #if !os(Windows), and the rest builds as-is.

Internal stack: Swift 6.3 with strict concurrency, swift-argument-parser for the CLI, pkl-swift for config, swift-jinja for templates, swift-yyjson for Figma API response parsing. For terminal UX: Noora from the Tuist team — progress bars, spinners, colored output, and interactive prompts in a consistent style.

Documentation in DocC

The ExFig docs are built as a DocC archive — the same format Apple uses for its own developer documentation. Deployed to GitHub Pages. Live at DesignPipe.github.io/exfig/documentation/exfigcli. Sections: Getting Started, Why ExFig, CLI reference, Pkl Configuration Guide, Custom Templates, MCP Server, Design Tokens, CI/CD Integration.

For a CLI tool aimed at iOS engineers, DocC is the honest signal that the project takes documentation seriously.

CI Without Shell Scripts: exfig-action

DesignPipe/exfig-action is the official GitHub Action wrapping the CLI. Inside inDrive, we use it instead of manually scripting mise install + exfig batch. Supports Ubuntu (Linux x64) and macOS (universal — arm64 + x86_64).

What it handles:

  • Two-level caching. Binary cache (per OS + version) and incremental asset cache. Cache is saved even on failed runs — next time, --resume picks up from .exfig-checkpoint.json (TTL: 24 hours).
  • Structured outputs. assetsexported, changedfiles (newline-delimited list), cachehit, duration, errorcategory (RATELIMIT, TIMEOUT, AUTH…). Useful for conditional steps: if: steps.exfig.outputs.errorcategory == 'RATE_LIMIT' can page an admin instead of silently failing the pipeline.
  • Slack notifications. Three states: success (✅), failure (❌ with @mention only on error), cache-hit (💨). Customizable via Slack Block Kit templates with {{ASSETS}}, {{DURATION}}, {{RUN_URL}} placeholders.
  • Rate limit and concurrency knobs. ratelimit, maxretries, concurrentdownloads, granularcache, fail_fast — all exposed as Action inputs.

A typical workflow step:

- uses: DesignPipe/exfig-action@v3
  id: exfig
  with:
    figma_token: ${{ secrets.FIGMA_PERSONAL_TOKEN }}
    command: batch
    config: exfig.pkl
    cache: true
    granular_cache: true
    rate_limit: 25
    slack_webhook: ${{ secrets.SLACK_WEBHOOK }}

Without the action, this would be 30+ lines of bash per product — binary installation, cache setup, exit code handling, and Slack payload assembly. Repeated across three products.

Extracted SPM Packages

Several pieces of functionality were extracted into standalone Swift Package Manager libraries, reusable outside ExFig and available under MIT:

  • swift-figma-api — Swift client for the Figma REST API. 46 endpoints, async/await, token-bucket rate limiter with round-robin, exponential retry honoring Retry-After, Variables API. Swift 6.1+, macOS and Linux.
  • swift-svgkit — SVG parsing and Android format generation: VectorDrawable (XML) and ImageVector (Jetpack Compose). Native Swift, no Android SDK on CI.
  • swift-penpot-api — Penpot RPC API client with SVG reconstruction from the shape tree. Linux-compatible.
  • swift-resvg — Swift wrapper around resvg for rendering SVG to PNG or PDF.

Benchmarks

What a Production Config Looks Like

To put the numbers in context, here’s the scale of our iOS exfig.pkl:

  • 3 color entries: InDrive Base (4 modes: Light / Dark / Contrast Light / Contrast Dark), InDrive Statement (same 4 modes), DS3 (Light / Dark only). Each with its own Figma tokensFileId and code template.
  • 66 icon entries: 19 Outlined, 10 Filled, 21 Colored, 1 Flags, 15 Double Color (dark theme via Variable Modes). All 66 generated via a Pkl for loop — the config is ~250 lines instead of ~1,500.
  • 1 illustration entry: 393 imagesets × @2x/@3x, plus dark variants for a subset — 840 PNG per run.
  • Code Connect: Generated for all 66 icon sets and for illustrations. Designers in Figma Dev Mode see the real Swift code for each icon.

iOS: 840 PNG into Xcode

Benchmark conditions: 393 illustrations × @2x/@3x + dark variants = 840 PNG per run. Runner: macOS self-hosted (CY-M-6224). Timing includes Figma API download, xcassets write, and Swift file generation. pngquant runs in a separate step and is excluded.

Three things worth noting:

  • ExFig is ×4 faster on the same work. 37s vs 154s at the median. The difference is --concurrent-downloads 50 against figma-export’s sequential fetching with forced delays.
  • ExFig’s variance is dramatically lower. figma-export ranged 32–339s, a 10× spread. ExFig caps at 40s cold.
  • Granular cache changes the economics. If nothing changed in Figma, the step costs 3 seconds. figma-export always re-downloaded everything, every run.

Android: Same Figma File, Different Pipeline

Android uses the same illustration set, but the pipeline differs. Assets live as .webp files (for APK size), while Figma exports SVG. With figma-export, this was two CI steps: Download, then Convert to webp. With ExFig, it’s one: format = "webp" + sourceFormat = "svg" in the Pkl config, and the tool handles download, scaling, and encoding in a single pass.

The full jump is 576s → 84s (×6.9). The Android gain is larger than iOS (×6.9 vs ×4) partly because the Android pipeline had two steps that collapsed into one. Sometimes the biggest win isn’t making something faster — it’s eliminating a step.

The figma-export n=138 is six months of production data, not a lab benchmark; the median is statistically stable. The ExFig cache-hit sample (n=2) is just two runs where designers hadn’t touched Figma that day — treat it as an order-of-magnitude indicator. The iOS cache-hit data (n=11, 1–5s) is the more meaningful reference.

What We Learned

The right time to fork a tool is when the gap between its pace and yours has become wider than the cost of maintaining your own fork. Fork too early and you’re debugging someone else’s code that already works. Fork too late and the workarounds cost more than the fork would have.

A few notes on the technology choices:

  • Pkl instead of YAML because a 66-entry YAML config stops being a config and becomes a liability. The inflection point was around 30–40 entries.
  • swift-jinja instead of Stencil because Stencil is unmaintained.
  • Noora instead of raw ANSI because terminal UX matters and manual escape codes don’t scale.
  • MCP instead of a REST API because exfig mcp over stdio is simpler to maintain and directly integrates with Claude Code.
  • DocC instead of a /docs folder because for a CLI tool in the iOS ecosystem, it’s the credibility signal that the project is serious about documentation.

These were all simple, practical reasons. They tended to work better than the architectural ones.

One more thing: ExFig at this scale, in four months, happened because I built it with an AI assistant in the loop. Pkl schemas, platform templates, core tests, DocC sections, GitHub Action — all followed the same cycle: design the structure, ask for a draft, refactor, review. This isn’t “AI wrote my code.” The design decisions, architecture choices, and code review remain the engineer’s job. The AI compresses the time between those steps. A task that used to take three evenings now takes one. Without that workflow, four platform plugins, an MCP server, cross-platform Swift, and four SPM packages would not have fit in one engineer’s free time.

What’s Next

Today, our design system’s source of truth is Figma, and ExFig pulls assets from it into code. But Figma is expensive, moves slower than AI-assisted design workflows, and tying the entire design system to a single SaaS vendor carries real risk.

The next step we’re thinking about is a self-hosted DS service: a design system knowledge base that lives independently from any tool and syncs with Figma, Penpot, or whatever comes next on demand. In that picture, ExFig becomes one of several adapters. Migration away from Figma stops being an architectural decision and becomes a config change.

That’s a longer project. For now, ExFig is open under MIT and in production at inDrive.

Links

ExFig: github.com/DesignPipe/exfig

GitHub Action: github.com/DesignPipe/exfig-action

Documentation: DesignPipe.github.io/exfig/documentation/exfigcli

Issues and questions are welcome.


This content originally appeared on HackerNoon and was authored by inDrive.Tech


Print Share Comment Cite Upload Translate Updates
APA

inDrive.Tech | Sciencx (2026-06-12T05:46:46+00:00) How a Weekend MVP Became inDrive’s Cross-Platform Design Token Export Tool. Retrieved from https://www.scien.cx/2026/06/12/how-a-weekend-mvp-became-indrives-cross-platform-design-token-export-tool/

MLA
" » How a Weekend MVP Became inDrive’s Cross-Platform Design Token Export Tool." inDrive.Tech | Sciencx - Friday June 12, 2026, https://www.scien.cx/2026/06/12/how-a-weekend-mvp-became-indrives-cross-platform-design-token-export-tool/
HARVARD
inDrive.Tech | Sciencx Friday June 12, 2026 » How a Weekend MVP Became inDrive’s Cross-Platform Design Token Export Tool., viewed ,<https://www.scien.cx/2026/06/12/how-a-weekend-mvp-became-indrives-cross-platform-design-token-export-tool/>
VANCOUVER
inDrive.Tech | Sciencx - » How a Weekend MVP Became inDrive’s Cross-Platform Design Token Export Tool. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2026/06/12/how-a-weekend-mvp-became-indrives-cross-platform-design-token-export-tool/
CHICAGO
" » How a Weekend MVP Became inDrive’s Cross-Platform Design Token Export Tool." inDrive.Tech | Sciencx - Accessed . https://www.scien.cx/2026/06/12/how-a-weekend-mvp-became-indrives-cross-platform-design-token-export-tool/
IEEE
" » How a Weekend MVP Became inDrive’s Cross-Platform Design Token Export Tool." inDrive.Tech | Sciencx [Online]. Available: https://www.scien.cx/2026/06/12/how-a-weekend-mvp-became-indrives-cross-platform-design-token-export-tool/. [Accessed: ]
rf:citation
» How a Weekend MVP Became inDrive’s Cross-Platform Design Token Export Tool | inDrive.Tech | Sciencx | https://www.scien.cx/2026/06/12/how-a-weekend-mvp-became-indrives-cross-platform-design-token-export-tool/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.