@sxl-studio/export-icons
v1.1.5
Published
Export icons and assets from Figma with diff sync, fonts, and SF Symbols
Downloads
90
Maintainers
Readme
SXL Export Icons
@sxl-studio/export-icons — installable CLI for exporting icon assets from Figma with incremental diff, flexible multi-config, font generation, SF Symbols pipeline, and optional Bridge/MCP source mode.
Install
npm install --save-dev @sxl-studio/export-iconsEnvironment and secrets
Create .env in your repository root and provide your Figma credentials there:
FIGMA_TOKEN=figd_xxxxxxxxxxxxxxxxx
SXL_ICONS_FILE_KEY=xxxxxxxxxxxxRequired Figma token scopes:
file_content:readfile_metadata:read
No write scopes are required for this CLI.
Add .env to .gitignore and never commit tokens:
.env
.env.localCLI commands
# If package is installed in repo (recommended)
pnpm exec sxl-export-icons init
pnpm exec sxl-export-icons init --wizard
# One-shot run without install
pnpm dlx @sxl-studio/export-icons init
npx @sxl-studio/export-icons init
# Validate one or many configs
pnpm exec sxl-export-icons validate-config --config sxl-export-icons.config.yaml
pnpm exec sxl-export-icons validate-config --config-glob "packages/*/sxl-export-icons*.yaml"
# Sync (default command)
pnpm exec sxl-export-icons sync --config sxl-export-icons.config.yaml
pnpm exec sxl-export-icons sync --config sxl-export-icons.config.yaml --adopt-existing
pnpm exec sxl-export-icons sync --config sxl-export-icons.config.yaml --target font-subset-local
pnpm exec sxl-export-icons --config sxl-export-icons.config.yaml --dry-run
pnpm exec sxl-export-icons --config sxl-export-icons.config.yaml --full
pnpm exec sxl-export-icons --config-glob "packages/*/sxl-export-icons*.yaml" --report .reports/icons-sync.json
# Enable fallback if source mode=mcp failed
pnpm exec sxl-export-icons sync --config ./icons.yaml --allow-fallback-rest
# Clean generated files from state/config scopes
pnpm exec sxl-export-icons clean --config ./icons.yaml
pnpm exec sxl-export-icons clean --config ./icons.yaml --dry-run
# Print state coverage report
pnpm exec sxl-export-icons report --config ./icons.yaml
# Probe Figma Motion runtime export support through Bridge/Remote Connect
pnpm exec sxl-export-icons probe-motion --format MP4,WEBM,GIF,SVG,SVG_STRING --inspect-only
pnpm exec sxl-export-icons probe-motion --node-id 1:2 --format MP4,WEBM,GIF,SVG --ignore-overlapping-layers --include-id-attribute--full resets the complete state cache before applying any --target filter. Prefer a full run with all targets. With --full --target <id>, non-selected assets remain on disk when pruneUntracked: false, but their next sync is recalculated from an empty state and reports them as new.
Do not use
pnpx sxl-export-icons ...in registries with private proxy mapping.pnpx/dlxcan try to resolve unscoped packagesxl-export-iconsand return 404.
Running from a project terminal
cd ./packages/design-system/tokens
# First run: adopt already downloaded files as baseline state
pnpm exec sxl-export-icons sync --config ../sxl-export-icons.config.yaml --adopt-existing
# Regular sync
pnpm exec sxl-export-icons sync --config ../sxl-export-icons.config.yaml
# Local subset conversion only
pnpm exec sxl-export-icons sync --config ../sxl-export-icons.config.yaml --target font-subset-localNotes:
- CLI auto-loads
.env.local/.envfrom current and parent folders. --adopt-existingis bootstrap mode for empty scope state.- During REST source loading, CLI shows live progress stages/pages
(
REST 1/3,REST 2/3,REST 3/3) so long runs are observable.
Config v3
Canonical filename: sxl-export-icons.config.yaml.
Legacy config version: 2 (icons.config.yaml) is supported via auto-migration.
When v2 config is loaded, relative paths are resolved from the current workspace root to keep legacy behavior.
version: 3
env:
figmaTokenVar: FIGMA_TOKEN
bridge:
baseUrl: http://127.0.0.1:37830
authTokenVar: BRIDGE_AUTH_TOKEN
state:
file: .sxl/cache/icons-state.json
safety:
allowOutsideWorkspace: false
sources:
- id: ds-icons
mode: rest # rest | mcp
fileKeyVar: SXL_ICONS_FILE_KEY
pageName: Icons
downloadSpeed: 20
descriptionExportMarker:
key: sxl-studio-export-icon
includeWhenMissing: true
selectors:
includeComponentSetNames: []
includeComponentNames: []
variantMatch:
style: outline
targets:
- id: web-svg
mode: sync # sync | convert-local
sourceIds: [ds-icons]
download:
enabled: true
pruneUntracked: false
output:
path: assets/icons/svg
format: svg # static: svg | png | jpg | webp | gif | pdf; motion.enabled: svg | gif | mp4 | webm
scale: 1
quality: 100
lossless: false
unsafeSvgPolicy: fail # fail | embed-image-fills | embed-png
qualitySize: null
width: null
height: null
modeWH: null # null | cover | proportion | fill
naming:
case: kebab # kebab | snake | camel | pascal
separator: "-"
pattern: "{prefix}{variant.style}{sectionName}{baseName}{suffix}"
prefix: null
suffix: null
includeSectionName: false
collisionStrategy: add-nodeid # error | add-nodeid | add-index
font:
enabled: true
path: assets/icons/font
formats: [woff2, woff, ttf, eot, svg]
fontName: icons
engine: webfonts-generator # webfonts-generator | advanced
includeNames: [] # optional subset for font/SF generation
excludeNames: [] # optional exclusion list
sfSymbols:
enabled: true
path: assets/icons/ios
overrides:
- name: illustrations-lossless-webp-128
match:
sectionName: Illustrations
patch:
output:
format: webp
width: 128
height: 128
quality: 100
lossless: true
sourceExport:
transport: plugin
format: png
constraint:
type: WIDTH
value: 128Source mode: rest and mcp
mode: rest— direct Figma REST API.mode: mcp— read via SXL Bridge/Remote Connect (/api/commandwith page/tree traversal).- If
mode: mcpfails and--allow-fallback-restis passed, CLI auto-falls back to REST for sources with resolvedfileKey.
The bridge block is used for mode: mcp sources, motion.enabled targets, and static items whose effective sourceExport.transport is plugin. A static REST source does not use Bridge unless its target or matching override selects Plugin transport.
SVG safety for embedded image fills
output.unsafeSvgPolicy controls static SVG export when the indexed Figma structure contains a visible IMAGE fill inside a VECTOR node:
failis the default. A structural preflight stops the current config before its output and state writes. Fix the source when the result must remain fully vector.embed-image-fillsis an opt-in REST-only repair forsource.mode: restwithsourceExport.transport: restor the default transport. Export Icons keeps the Figma REST SVG and embeds only IMAGE/STRETCH fills missing from it, using image URLs returned byGET /files/:key/imagesand references fromfillOverrideTable. Plugin, Bridge, and Remote Connect are not required.embed-pngkeeps the explicit Plugin full-node PNG contract. It requires effectivesourceExport.transport: pluginandsourceExport.format: png, then embeds the full rendered node in an SVG wrapper.
The REST repair accepts only an unambiguous mapping to one opaque IMAGE/STRETCH paint with an invertible transform. Unsupported or mixed paints, effects, masks, non-STRETCH modes, ambiguous SVG path mapping, missing metadata, and Figma file version races fail closed before repaired output is committed. Normal SVGs without missing image fills keep the Figma REST SVG unchanged.
targets:
- id: media-svg
sourceIds: [icons]
output:
path: assets/icons/media-svg
format: svg
unsafeSvgPolicy: embed-image-fills--dry-run describes this path as REST SVG + embedded IMAGE fill repair and reports the number of image regions. The repaired SVG embeds raster image fills, so it cannot be used for icon-font generation or SF Symbols.
The legacy embed-png path remains available when a full-node Plugin render is explicitly required.
Plugin-backed SVG is always represented as embedded PNG even when structural risk metadata is absent. sourceExport.constraint is optional and defaults effectively to SCALE:1; when authored, it controls the Plugin PNG pixel dimensions. output.scale does not control this SVG path. The wrapper width, height, and viewBox come from indexed logical render bounds: absoluteRenderBounds first, then absoluteBoundingBox. Position changes do not alter the item fingerprint, but logical resize does. Missing, non-finite, or non-positive bounds fail preflight before Plugin SVG output. Because the content is raster, it can blur when enlarged. JPG, Motion, icon-font, and SF Symbols combinations are rejected; use a separate target without vector-only auxiliary pipelines.
The current MCP page/tree payload does not guarantee those absolute bounds. Export Icons preserves real absoluteRenderBounds or absoluteBoundingBox when the payload contains them, but it does not reinterpret local width/height as absolute bounds. For the raster-backed SVG path, use mode: rest to index the Figma file and sourceExport.transport: plugin to obtain the PNG bytes. An MCP-indexed item without real absolute bounds fails preflight.
targets:
- id: media-raster-backed-svg
sourceIds: [icons]
selectors:
includeComponentNames: [media-preview]
output:
path: assets/icons/media-svg
format: svg
unsafeSvgPolicy: embed-png
sourceExport:
transport: plugin
format: png
constraint:
type: SCALE
value: 4For embed-png, --dry-run identifies every indexed asset that resolves to the embedded-PNG representation and prints Plugin transport, PNG source format, constraint, and logical dimensions. When no constraint was authored, dry-run shows its effective Plugin default as SCALE:1. The sync JSON report records only items planned after diffing under sourceExport.plugin and svgExport.embeddedPng, together with rasterBacked: true.
For mode: mcp with REST-backed static downloads, the CLI also reads the exact SVG node metadata and file version through Figma REST before export. This requires the configured Figma token and fileKey/fileKeyVar; the sync fails instead of treating missing paint metadata as safe.
Static raster export through Plugin
sourceExport is optional at target level and in overrides[].patch. It controls how Figma supplies the intermediate static raster:
sourceExport:
transport: plugin # rest | plugin
format: png # png | jpg
constraint:
type: WIDTH # SCALE | WIDTH | HEIGHT
value: 128 # positive; SXL limit: SCALE <= 16, WIDTH/HEIGHT <= 4096When omitted, transport remains rest. output.scale remains the REST export scale and accepts values from 0.01 through 4.
Plugin transport:
- supports static
png,jpg,webp, andgifoutputs, plus only the explicit raster-backed SVG tuple shown above; it cannot be combined withmotion.enabled; - calls Figma node
exportAsyncthrough the current compatible SXL Studio Plugin, Bridge, and Remote Connect session; - requires the configured Figma file to be open;
- requires
fileKey/fileKeyVaron the source, includingmode: mcp, and verifies that key against the active Plugin document before filesystem operations; - exports a
pngorjpgintermediate usingSCALE,WIDTH, orHEIGHT, then applies local output conversion; - rejects local upscaling: the Plugin intermediate must already contain enough pixels for the requested output size.
In Community Plugin builds, figma.fileKey may be unavailable. In that case open Repository connection in SXL Studio, set Figma file URL or key, and save it once for this document. This fallback compares the configured key with document plugin data; it is a manual document assertion, not an independent Figma identity lookup. Re-save the correct key after duplicating a file. Sync fails safely when the identity is absent or does not match. A single sync may use Plugin transport for only one Figma file; split multi-file exports by config or --target.
SXL safety limits are SCALE <= 16, WIDTH/HEIGHT <= 4096, projected raster sides no larger than 4096 px, a 16 million pixel render budget, and 16 MiB of encoded PNG/JPG bytes per node. Nodes without finite render bounds are rejected before exportAsync. These are SXL memory/transport limits, not documented Figma API limits. Plugin intermediates are staged and published only after the Plugin batch succeeds, so a disconnect does not replace earlier outputs in that batch; the following filesystem commit is sequential rather than atomic.
For example, this override takes a square 24x24 vector node from the Illustrations section, asks Figma for a 128x128 PNG, and writes a lossless WebP that remains 128x128:
overrides:
- name: illustrations-lossless-webp-128
match:
sectionName: Illustrations
patch:
output:
format: webp
width: 128
height: 128
quality: 100
lossless: true
sourceExport:
transport: plugin
format: png
constraint:
type: WIDTH
value: 128output.lossless defaults to false. For WebP, true keeps Sharp in lossless encoding, including when qualitySize is checked; the CLI does not silently switch to lossy output. This describes the encoding mode, not metadata or byte-for-byte identity with the Plugin intermediate. Embedded bitmaps cannot gain detail that is missing in their source pixels.
--dry-run prints both the pre-diff SVG safety candidate inventory described above and the number and names of Plugin exports planned after diffing. A sync JSON report includes only each planned Plugin node, intermediate format, and authored constraint (null when omitted). Effective output and source-export settings are hashed per item, so changing an override re-exports matching items without forcing unrelated matches to export again.
Motion animated icon export
Animated icons authored with Figma Motion can be exported by normal sync when a target explicitly enables motion.enabled.
Supported Motion output formats:
svg-> animated SVG generated from Motion keyframes;gif-> native Figma Motion GIF export through the plugin runtime;mp4-> native Figma Motion MP4 export through the plugin runtime;webm-> native Figma Motion WebM export through the plugin runtime.
Motion export requires:
- SXL Studio Bridge running;
- SXL Studio plugin launched in the same Figma file with Remote Connect enabled;
- a
restsource withfileKey/fileKeyVarfor normal file indexing, or anmcpsource when the file is already open in Figma.
targets:
- id: motion-webm
sourceIds: [default-source]
output:
path: assets/motion
format: webm
scale: 1
quality: 90
motion:
enabled: true
fps: 30
loop: true
ignoreOverlappingLayers: true
includeIdAttribute: truemotion.enabled is deliberately opt-in. Static targets keep the existing REST/SVG/raster export path. Motion targets are re-exported on every sync because Figma Motion keyframes are read through the plugin runtime and are not present in the REST file JSON fingerprint.
Figma's public Plugin typings/docs do not expose MP4/WebM/GIF Motion formats, but current Figma Motion runtime accepts native exportAsync formats MP4, WEBM, and GIF. Export Icons uses that native path for media formats and uses plugin SVG_STRING plus Motion keyframes for animated SVG. ignoreOverlappingLayers maps to contentsOnly for SVG/SVG_STRING; the current native Motion media runtime rejects that key, so GIF/MP4/WebM use Figma runtime defaults for overlap handling.
Motion runtime probe
probe-motion is a diagnostic command for checking what the current Figma plugin runtime exposes for Figma Motion nodes.
It requires SXL Studio Bridge and an active Remote Connect session in Figma. Without --node-id, it probes the current Figma selection. It returns Motion metadata, available figma.motion runtime keys, existing export settings, and optional exportAsync probe results without writing files.
pnpm exec sxl-export-icons probe-motion \
--format MP4,WEBM,GIF,SVG,SVG_STRING \
--ignore-overlapping-layers \
--include-id-attributeUseful options:
--inspect-only— read metadata without callingexportAsync;--format <list>— comma-separated or repeatable formats to probe;--ignore-overlapping-layers— usescontentsOnly: trueforSVG/SVG_STRING; native Motion media formats use Figma runtime defaults;--include-descendants— includes descendant Motion keyframes used by animated icon export;--include-id-attribute— passessvgIdAttribute: trueforSVG/SVG_STRING.
Target mode: sync and convert-local
mode: sync(default) — reads Figma sources, downloads/updates assets, then runs aux pipelines.mode: convert-local— does not use Figma sources; reads existing local SVG files fromoutput.pathand runs font/SF pipelines.
Rules for convert-local:
sourceIdsmust be empty;download.enabledmust befalse;output.formatmust besvg;- at least one of
font.enabledorsfSymbols.enabledmust betrue.
To run only a local-conversion target from mixed config:
pnpm exec sxl-export-icons sync --config ./sxl-export-icons.config.yaml --target font-subset-localDescription marker filter
You can control export in Figma description of COMPONENT / COMPONENT_SET:
sxl-studio-export-icon: trueor
sxl-studio-export-icon: falseConfig:
sources:
- id: ds-icons
descriptionExportMarker:
key: sxl-studio-export-icon
includeWhenMissing: trueRules:
- marker
true-> include - marker
false-> exclude - marker missing -> use
includeWhenMissing
What is exported
- Only
COMPONENTnodes. - For
COMPONENT_SET, exports eachCOMPONENTvariant child as a separate item. - Variant matching works with arbitrary property names (for example
styleMode,platform,state, ...), not onlystyle. - If source sets
sectionName/sectionId, items outside that section are skipped.
One or multiple formats
target.output.formatis a single format per target.- To export the same source in multiple formats, add multiple targets with shared
sourceIds.
targets:
- id: icons-svg
sourceIds: [ds-icons]
output:
path: assets/icons/svg
format: svg
scale: 1
quality: 100
qualitySize: null
width: null
height: null
modeWH: null
- id: icons-webp
sourceIds: [ds-icons]
output:
path: assets/icons/webp
format: webp
scale: 1
quality: 90
qualitySize: null
width: null
height: null
modeWH: nullIncremental diff behavior
Scope identity: sourceId::targetId.
NEW— node appears first time in scope.UPDATED—updatedAt/fingerprint changes, file missing, format changed, or scope config hash changed.DELETED— node removed from source scope.RENAMED— canonical name/path changes while format stays same.UNCHANGED— no content and no config-impact changes.
State file stores scope metadata, config hash, and exported item hashes.
For REST sources, node fingerprints are computed from render-relevant node data (geometry=paths), so pure vector-shape edits are detected even when metadata timestamps stay unchanged.
Bootstrap behavior (first run without scope state):
- every matched icon is treated as
NEWand re-exported; - this guarantees replacement/update even when files already exist on disk.
- with
--adopt-existing, existing files are adopted asUNCHANGEDbaseline instead of forced re-download.
Optional strict cleanup:
- set
download.pruneUntracked: trueto remove files with the same target format inoutput.paththat are not present in current Figma scope. - enable it only for dedicated output directories, because cleanup is path+format based.
Output policies
- Paths are resolved relative to config file location.
- Export outside workspace is blocked by default.
- To allow external absolute/relative destinations set:
safety:
allowOutsideWorkspace: truePost-processing
svg-> SVGO optimizationpng/jpg-> Sharp optimization + optional resizewebp-> export PNG from Figma, then convert to WebP- static
gif-> export PNG from Figma, then convert to GIF - Motion
svg-> export SVG with node IDs and apply Motion keyframes as CSS animation - Motion
gif/mp4/webm-> native Figma Motion export through Bridge/Remote Connect pdf-> direct Figma export
Convert-only targets (no download)
If icons already exist locally and you only need font/SF conversion:
targets:
- id: icons-font-only
mode: convert-local
sourceIds: []
download:
enabled: false
output:
path: assets/icons/svg
format: svg
scale: 1
quality: 100
qualitySize: null
width: null
height: null
modeWH: null
naming:
case: kebab
separator: "-"
pattern: "{variant.style}{baseName}"
includeSectionName: false
collisionStrategy: add-nodeid
font:
enabled: true
path: assets/icons/font
formats: [woff2, woff, ttf, eot]
fontName: ds-icons
includeNames: [filled-casino-menu, outline-casino-menu]Notes:
download.enabled: falseskips file mutation (no download/rename/delete);font.includeNameslimits conversion to a selected subset;- names in
font.includeNames/excludeNamescan be provided with or without.svg; - source SVG files must already exist in
output.path.
Font pipeline
- Adapter-based interface with
webfonts-generatordefault engine. - Supports
woff2,woff,ttf,svg,eot. - Preserves stable codepoints (
codepoints.json) between runs. - Generates preview (
preview.html) + stylesheet.
SF Symbols pipeline
- Generates
Assets.xcassets/*.symbolset. - Uses
symbol-rendering-intentin generatedContents.json. - Rejects complex SVG features (gradients, filters, masks, embedded images).
Multi-config usage
You can run many configs in one call:
pnpm exec sxl-export-icons sync \
--config packages/ds-a/sxl-export-icons.config.yaml \
--config packages/ds-b/sxl-export-icons.config.yaml \
--report .reports/icons-mono.jsonOr by glob:
pnpm exec sxl-export-icons sync --config-glob "packages/*/sxl-export-icons.config.yaml"For multiple pages/sections from the same Figma file, define separate sources with the same fileKey/fileKeyVar and different pageName/sectionName.
Naming token notes (variant.*)
variantis a reserved token namespace innaming.pattern.variant.<propName>uses any Figma variant property name (for examplestyle,type,size,state).- Multi-prop naming is supported by chaining tokens:
"{prefix}{pageName}{variant.style}{variant.type}{baseName}"
- If a property is missing for a node, that segment is skipped.
In mixed sets this can cause collisions, so use
collisionStrategyand/or add disambiguators like{sectionName}or{nodeId}.
Duplicate preflight (before download)
When duplicate output names are detected, CLI prints the full duplicate list (name + node id + variant props) and asks for action:
refresh— re-read the source, then repeat SVG safety and Plugin readiness preflight before the refreshed scope resumes duplicate planning. Outputs from earlier completed scopes or configs are not rolled back if this refreshed preflight fails.skip— keep one node and skip duplicate entriesall— download all duplicates using rename strategy (add-nodeid/add-index)abort— stop sync
Interactive TTY mode uses keyboard selection (↑/↓ + Enter) instead of manual text input.
In non-interactive mode (CI), prompts are skipped and configured collisionStrategy is applied automatically.
Troubleshooting
Missing Figma token-> set env var fromenv.figmaTokenVar.Path ... is outside workspace-> enablesafety.allowOutsideWorkspace.Bridge session is not connected-> startUtils/bridge, open plugin, enable Remote Connect.Naming collision detected-> changenaming.patternor usecollisionStrategy: add-nodeid/add-index.- Too many 429 responses -> lower
downloadSpeedfor source.
Contract references used in implementation
- Figma REST:
GET /v1/files/:key,GET /v1/images/:key, components/component_sets metadata endpoints. - Sharp conversion/encoding docs for WebP/GIF/resize.
webfonts-generatorformats and options.
This utility keeps those contracts explicit in code and docs to reduce drift and regression risk.
