@sxl-studio/token-transformer
v3.4.1
Published
Transform DTCG design tokens to CSS/SCSS, Swift, Kotlin, Android XML, and JSON manifests
Downloads
594
Maintainers
Readme
SXL Token Transformer
YAML-first утилита для преобразования дизайн-токенов в:
- CSS custom properties
- SCSS variables
- Swift-константы (SwiftUI)
- Kotlin-константы (Compose)
- Android XML ресурсы (
colors.xml,dimens.xml,strings.xml,bools.xml,integers.xml)
3.4.1: smart, force и preserve
smartдобавляет, обновляет и удаляет токены по JSON; записывает только файлы, содержимое которых действительно изменилось.forceзаново генерирует и записывает все выбранные outputs. Предварительное удаление папок не требуется.--mode smart --preserve-existingдобавляет и обновляет объявления, сохраняя отсутствующие в JSON. Это флаг команды, без настройки YAML.- Стабилизированы порядок glob-файлов и повторное XML-объединение; исправлено экранирование многострочного текста и нативных спецсимволов.
3.3.1: совместимость и ограничения
- Поддержаны объектные DTCG dimension/duration, цвета
srgb,display-p3,hsl,oklch,lchи авторскийhexfallback. Это не полная реализация всех цветовых пространств DTCG; некорректные значения получают диагностику. easing: CSS/SCSS и Kotlin поддерживают пресеты, Bézier иhold. Spring в CSS — приближение с предупреждением; SwiftUI/UIKit сохраняют easing как строковый spec, Kotlin сохраняет spring как строковый spec.cubicBezierостаётся нативной кривой.- Тип в реестре не означает поддержку каждой формы Figma. Нативные outputs не воспроизводят сложную геометрию radial/conic, repeating gradients,
lineHeight: auto, video/pattern/glass fills. XML ограничен ресурсами Android. - Поля, отсутствующие в формате output (noise/texture, геометрия progressive blur, фильтры/трансформации изображений, оси шрифта и отдельные поля типографики), сопровождаются предупреждением; исходные данные сохраняет manifest. Typed output — значения/spec, а не автоматическое применение всех свойств к UI.
- Первый smart-запуск после обновления пересобирает outputs один раз. При запуске с ограниченным scope остальные outputs остаются ожидающими полного прогона. Затем неизменённые outputs снова пропускаются; standalone SCSS больше не получает импорт отсутствующего
root.scss.
Установка
npm install @sxl-studio/token-transformerКоманды
# трансформация в smart incremental режиме (по умолчанию)
npx sxl-transform sync --config ./sxl-transform.config.yaml
# принудительный полный rebuild
npx sxl-transform sync --config ./sxl-transform.config.yaml --force
# только выбранные outputs
npx sxl-transform sync --config ./sxl-transform.config.yaml --only-output css-root --only-output swift-root
# только выбранные файлы (glob)
npx sxl-transform sync --config ./sxl-transform.config.yaml --only-file "modes/dark.css"
npx sxl-transform sync --config ./sxl-transform.config.yaml --only-file "css-root:modes/dark.css"
# валидация конфига
npx sxl-transform validate-config --config ./sxl-transform.config.yaml
# генерация стартового конфига
npx sxl-transform init --path ./sxl-transform.config.yaml
# help по конкретной команде
npx sxl-transform help sync
npx sxl-transform validate-config --helpСохранение ранее сгенерированных токенов
npx sxl-transform sync --config ./sxl-transform.config.yaml --preserve-existing --issue-action debug-stopКоманда сохраняет токены, чьи JSON-файлы или значения удалены из источников, добавляет новые и обновляет совпавшие актуальными значениями. Поддерживаются CSS, SCSS, Swift, UIKit, Kotlin, Android XML и manifest. Перед запуском не удаляйте выходные файлы: именно из них читаются прежние значения.
Совпадение определяется по публичному имени и области объявления:
| Формат | Область совпадения |
|---|---|
| CSS | Имя с учётом регистра, селектор и окружающие at-правила |
| SCSS | Имя Sass-переменной внутри одного файла-модуля; дефис и подчёркивание эквивалентны |
| Swift / UIKit / Kotlin | Namespace и имя свойства; у Kotlin также package |
| Android XML | Тип ресурса, имя и квалификатор ресурсов, например values-night; drawable обновляется целиком |
| Manifest | Имя, коллекция и режим; перенос JSON между исходными файлами не создаёт дубликат |
Старые объявления, заменённые актуальными в другом файле той же области, не дублируются. Разные Sass-модули и native namespaces остаются независимыми: изменение NewTokens.size не изменяет OldTokens.size. Сохраняются устаревшие выходные файлы и их подключения через indexes.includeGenerated. Native namespace и общие вспомогательные типы получают одного владельца в пределах области компиляции.
Флаг задаётся только в команде, в YAML его нет. Он работает с --mode smart, --force, --watch, --only-output и --only-file. Запуск без флага возвращает обычную генерацию из JSON для выбранных outputs; сохранённые значения будут удалены при пересборке. options.removeStaleOutputs: false отдельно управляет сохранением целых устаревших файлов.
Если state отсутствует, прежние файлы восстанавливаются в учёте по настроенным путям генерации и split-шаблонам. Произвольные файлы вне этих шаблонов не подхватываются. Уже потерянные значения без прежнего output восстановить нельзя. Сохранённые outputs не заменяют JSON-источники при разрешении алиасов.
Повреждённый или неподдерживаемый файл останавливает запуск до записи outputs. Обработчики рассчитаны на генерируемые Transformer структуры; произвольный код custom formatter может потребовать адаптации. CSS с анонимными слоями @layer не объединяется, потому что у слоя нет устойчивого имени. Предупреждения об исходных токенах по-прежнему обрабатываются согласно --issue-action. Пустой glob после подтверждённого удаления прежних источников при сохранённых outputs становится информационным сообщением.
Smart, force и preserve
smart(по умолчанию): пересобирает только реально затронутые output-файлы. Изменения source-файлов определяются по хешу содержимого (state v2) — правки значений и алиасов ловятся даже при неизменных size/mtime, аtouch/checkout без изменения содержимого не вызывают лишней перезаписи. Дополнительно перед ранним выходом сверяется содержимое сгенерированных файлов на диске с state: удалённые или изменённые извне (git restore, ручные правки, форматтеры) output-файлы пересобираются автоматически.force: заново генерирует и записывает все выбранные output-файлы безусловно.smart --preserve-existing: сохраняет отсутствующие в JSON объявления, обновляет совпавшие и добавляет новые. Переименование сохраняет прежнее имя и добавляет новое.- В smart пересчёт из-за изменения конфига или переключения preserve не означает перезапись: одинаковые байты остаются нетронутыми. Force записывает файлы даже при одинаковом результате.
Замечания по state:
- Формат state-файла — v2 (fingerprints с
contentHash). Старый v1-state отбрасывается: первый запуск после обновления делает один полный rebuild. - Scoped-прогоны (
--only-output/--only-file) не «поглощают» изменения вне своего scope: они остаются pending и подхватываются следующим полным smart-прогоном. - Устаревшие output-файлы (source-токены которых больше не существуют) по умолчанию удаляются в smart и force. Это отключается опцией
options.removeStaleOutputs: false— тогда такие файлы остаются на диске и продолжают отслеживаться в state (при включении опции обратно они будут удалены). - Если postprocess-скрипт переписывает output-файлы, которыми управляет Transformer, после
sxl-transform sync, следующий smart-прогон пересоберёт эти outputs: их содержимое больше не совпадает со state. Держите postprocess вне managed output paths, переносите его в config/output hooks Transformer или запускайте до финального sync, который сохраняет state.
Дополнительные флаги:
--mode smart|force--force(сокращение для--mode force)--state-file <path>(переопределить путь state-файла, по умолчанию<config-name>.state.json)--only-output <id>(можно повторять, выбор output id)--only-file <glob>(можно повторять, выбор output-файлов по шаблону)--explain(печатает по каждому output, почему он пересобран или пропущен)--watch(следит за token-каталогом и конфигами, пересобирает в smart-режиме с debounce; Ctrl+C — выход; требуется Node >= 20)--no-state-lock(отключить кооперативный lock state-файла)
Параллельные прогоны и целостность state:
- State пишется атомарно (tmp + rename) — прерванный прогон не оставляет битый файл.
- На время прогона берётся lock
<state>.lock; второй одновременный прогон завершится с понятной ошибкой. Устаревшие локи (упавший процесс, старше 10 минут) перехватываются автоматически.
Формат конфига
Поддерживается только YAML. JSON-конфиг умышленно не поддерживается.
Минимальная структура:
version: 1
source:
tokenDir: ./tokens
configFile: config.json
include: ["**/*.json"]
exclude: ["config.json", "**/diff-id*.json"]
options:
remBase: 16
collisionStrategy: error # error | suffix | namespace-by-file | namespace-by-mode
removeStaleOutputs: true # false — не удалять output-файлы исчезнувших токенов
unsupportedTypes:
default: error
types:
template: skip
composition: skip
grid: skip
tokenSets:
- id: root
selectors:
- collection: Core
mode: Default
- collection: Themes
mode: Dark
refModeMap:
Projects: Customer
Core: Default
- files: ["projects/customer/*.json"]
outputs:
- id: css-root
platform: css # css | scss | swift | uikit | kotlin | xml | manifest
outputDir: ./dist/css/adminui
prefix: ds
suffix: v2
resolveAliases: false
splitEffects: true
showDescriptions: true
files:
- tokenSet: root
output: root.css
# prefix/suffix можно переопределить для конкретного mapping:
# prefix: cmp
# suffix: beta
- id: tokens-manifest
platform: manifest
outputDir: ./dist/manifest/adminui
files:
- tokenSet: root
output: tokens-manifest.json
options:
schemaVersion: "1.0"
includeResolvedValue: true
includeOriginalValue: true
includeReferences: true
includeSource: true
includeExtensions: false
includePrivate: true
groupBy: flat # flat | collection | mode | fileКаждый output должен содержать минимум один generation-блок: files, bundles, bundlesFromCollections или indexes.
CSS / SCSS Output Contract
CSS-файлы по умолчанию оборачиваются в :root. Wrapper selector можно переопределить для статичного файла, split-файла или bundle:
outputs:
- id: css-app
platform: css
outputDir: ./dist/css
files:
- tokenSet: root
output: root.css
selector: ":root"
- tokenSet: dark
output: themes/dark.css
selector: "[data-theme='dark']"
- tokenSet: components
splitBySourceFile:
include: ["components/**/*.style.json"]
outputPattern: "{component}/{component}.css"
selector:
- ":root"
- "[data-component='{component}']"selector работает только для CSS. Если указать его для scss, swift, uikit, kotlin, xml или manifest, Transformer выдаст warning и проигнорирует настройку.
CSS/SCSS entrypoint-файлы генерируются через indexes:
outputs:
- id: css-app
platform: css
outputDir: ./dist/css
files:
- tokenSet: root
output: root.css
indexes:
- output: index.css
imports:
- ./root.css
includeGenerated:
- components/**/*.css
exclude:
- index.css
- "**/*.internal.css"
sort: generated-order # generated-order | alpha
skipMissing: true
strict: false- CSS indexes генерируют
@import "...";. - SCSS indexes генерируют
@use "..." as *;. - Indexes поддержаны только для
cssиscss. - Явные
importsрезолвятся относительно index-файла. includeGeneratedматчится по файлам того же output, включая файлы из state в smart-mode.- Index-файлы хранятся в state и участвуют в stale cleanup как обычные generated files.
bundles нужны, когда несколько source token files нужно собрать в один output без создания отдельного tokenSet под каждую группу:
outputs:
- id: css-components
platform: css
outputDir: ./dist/components
files:
- tokenSet: components
splitBySourceFile:
include: ["components/**/*.style.json"]
exclude: ["components/WBanner/**/*.style.json"]
outputPattern: "{component}/{component}.css"
bundles:
- tokenSet: components
output: WBanner/WBanner.css
include:
- components/WBanner/**/*.style.json
selector: ":root"Bundles используют тот же token graph, filtering, unsupported-type policy, aliases, custom formatters и platform emitters, что и обычные files. selector внутри bundle всё равно применяется только к CSS.
bundlesFromCollections нужен, когда группы bundle уже описаны в source.configFile или top-level projectConfig:
outputs:
- id: css-components
platform: css
outputDir: ./dist/components
resolveAliases: false
files:
- tokenSet: components
splitBySourceFile:
include: ["components/**/*.style.json"]
excludeBundledSources: true
outputPattern: "{fileName}.css"
bundlesFromCollections:
- tokenSet: components
mode: Default
include:
- WBanner
- WInput
fileInclude:
- components/**/*.style.json
fileExclude:
- "**/__draft__/*.json"
outputPattern: "{collection}.css"
selector: ":root"
strict: true
indexes:
- output: index.css
includeGenerated:
- "**/*.css"
exclude:
- index.cssbundlesFromCollections читает enabled mode.files из config.json / projectConfig, фильтрует их через fileInclude / fileExclude и генерирует один bundle на matched collection. Поддержанные placeholders: {collection}, {mode}, {collectionKebab}, {collectionSnake}, {collectionLower}, {modeKebab}, {modeSnake}, {modeLower}.
Чтобы избежать двойной генерации, используйте splitBySourceFile.excludeBundledSources: true. Для явного контроля без генерации bundles используйте splitBySourceFile.excludeFromCollections.
Quality gates
npm run eslint
npm run typecheck
npm run test
npm run build
npm run checkЧто нового в 3.2.0
Цветовое ядро синхронизировано с Plugin token engine (единое поведение «что видно в Figma = что в коде»):
figma.modify: amount принимает дробь0.56, шкалу0..100(56), процент"56%"и alias{token}— нормализация как в плагине (parseModifierAmount).space: "lch"— настоящий CIE L*a*b*/LCH (D65), отдельно от OKLCH.mix: powerless hue по CSS Color 4 §12.2 — подмешивание white/black/gray больше не уводит тон (blue + white ≠ cyan). Исправлено синхронно и в плагине.- CSS named colors (полный набор CSS Color 4:
red,rebeccapurple, …) и честный Display-P3 → XYZ → sRGB. - Inline color-mutation выражения:
"$value": "rgba({color} {alpha})"(alpha — литерал/процент/alias, паритет сfigma.modifyalpha). - Math: научная нотация (
1e-3,2.5e+2; голоеe— константа Эйлера), процентная арифметика (100% - 20px,50% + 10%) без конфликта с оператором%(модуло).
Эмиттеры и типы:
- Kotlin: цветовой литерал
Color(0xFF2D6CDF)безu-суффикса (u-форма резолвилась в rawULong-конструктор Compose и давала повреждённый packed-цвет). - rem/em масштабируются через
remBaseв Swift/UIKit/Kotlin/XML (1.5rem→24). - CSS typography:
%-lineHeight валиден при любом юните fontSize (600 1rem/150% "Inter"). - Effects: смешанные эффекты (shadow + blur/backgroundBlur) в CSS/SCSS раскладываются в
--x-shadow/--x-filter/--x-backdrop-filterвместо потери данных; однородные токены сохраняют базовое имя. custom— preserved/opaque тип: значение хранится verbatim, экспортируется только в manifest, на типизированных платформах skip + warning.- Совместимость алиасов как в Figma: FLOAT-типы взаимно алиасуемы (
number → borderWidth,lineHeight → sizing, …), STRING-типы взаимно алиасуемы; плюсgradient → color,shadow ↔ effects. durationпринимает объектную форму{ "duration": "100" };cubicBezier— объектную форму{ x1, y1, x2, y2 }(в т.ч. строковые числа).- Shadow-слои:
x/yприняты как алиасыoffsetX/offsetY(Tokens Studio shape). - Android XML: дробные
numberбольше не округляются — эмитятся вfloats.xmlкак<item type="dimen" format="float">(чтение черезResourcesCompat.getFloat). outputs[].excludeHidden: true— исключить из output токены с$extensions["figma.hide"](по умолчанию false: скрытые токены часто обслуживают alias-цепочки видимых).- Диагностика:
*_COLOR_INVALID/*_DIMENSION_INVALIDи т.п. указывают неразрешённый алиас в значении ((unresolved alias in value: {…})).
Проверено на реальных проектах (2026-07-02, полные прогоны на 7 платформ по двум боевым репозиториям токенов — 436 JSON-файлов, 63 token set'а, 613 выходных файлов; машинная валидация postcss/dart-sass/XML/JSON):
- Математика гейтится по типу (парити с FLOAT-веткой плагина): строковые значения никогда не вычисляются —
text-токен"37-37"остаётся"37-37", а не превращается в0; числовые поля композитов (offsets/blur теней, размеры typography) математику сохраняют. - Инлайн-мутации цвета (
"rgba({color}, {alpha})") запекаются в вычисленный литерал на CSS/SCSS даже приresolveAliases: false—rgba(var(--hex), var(--amount))невалиден на этапе вычисления значений CSS. - SCSS: mode-файлы делают
@useна свой фактический ближайшийroot.scss(вложенные раскладки по проектам); подстановка$varограничена видимой областью (свой файл + прилинкованный root) — определения чужих проектов больше не «протекают» какUndefined variable; standalone-файлы теряют угаданный@use. - Неразрешённые
{alias}-плейсхолдеры больше не попадают в сгенерированный код строковыми литералами — новые диагностикиSWIFT/UIKIT/KOTLIN/XML_ALIAS_UNRESOLVED(в CSS — черезCSS_UNSUPPORTED_TOKEN). - Ошибки теней стали актионабельными:
Invalid shadow token value (layer 1: missing "color"; unknown key "vcolor"); debug-отчёт дедуплицирует повторы (×N) и показывает точныйfile=проблемного токена.
Важно
- Запрещённые к трансформации типы:
template,composition,grid(рекомендуемый профиль:template/composition/grid -> skip, остальные unsupported ->error). - Поддержан
figma.modify, включая chain и alias-based параметры модификаторов. - Поддержана безопасная математика (
+ - * / %,round,floor,ceil,clamp, и др.) безeval. - Для selector по
collection/modeможно управлять зависимостями:includeRefs: true|false— подключать лиref-цепочки из tokenconfig.jsonв resolver scope.refModeMap— принудительно выбрать mode для зависимых коллекций (напримерProjects: Customer).
- Аффиксы имен настраиваются в YAML:
outputs[].prefix/outputs[].suffix— для всего outputoutputs[].files[].prefix/outputs[].files[].suffix— переопределение на уровне mappingsplitBySourceFile.prefixPattern/splitBySourceFile.suffixPattern— для split-генерации по файлам
- В режиме
--only-fileутилита не делает stale-cleanup вне выбранных файлов: точечная генерация безопасна для остального output. - SCSS-выгрузка ориентирована на token-first подключение: root-файлы нужно подключать до component-файлов в entrypoint, чтобы межфайловые
$token-ссылки резолвились детерминированно. - CSS
selectorподдерживает статичные селекторы и split placeholders вроде{component}. CSS/SCSSindexesгенерируют entrypoints и участвуют в smart state/stale cleanup.bundlesиbundlesFromCollectionsгруппируют source-файлы в один output через тот же emitter pipeline, а не через конкатенацию готовых строк. - Android XML: если
colorтокен содержит CSSlinear-gradient(...), transformer генерирует нативныйdrawable/*.xml, а не невалидное значение вcolors.xml. platform: manifestгенерирует JSON-артефакт из того же resolved token graph, что и CSS/native outputs. Manifest содержитname,cssVar,path,type,value,resolvedValue,originalValue,references,sourceFileи source metadata для документации, Storybook/devtools и аудита миграций.- Manifest
levelиweightчитаются из token extensions (level,sxl.level,sxl.weight), если они есть; они не выводятся из path или названия collection. - Manifest options можно задавать на уровне
outputs[].optionsили конкретногоoutputs[].files[].options. ПоддержаныcssVarPrefix,cssVarSuffix,includePrivate,includeSource,includeExtensions,groupBy,schemaVersion.
Матрица Поддержки Типов (CSS / SCSS / Swift / UIKit / Kotlin)
Поддерживаются с ограничениями форм значений и платформ, описанными выше:
color,gradient,img,fill,opacitydimension,number,spacing,sizingborder,borderWidth,borderRadius,strokeStyletypography,fontFamily,fontWeight,fontStyle,fontSize,lineHeight,letterSpacing,paragraphSpacing,paragraphIndent,textCase,textDecorationshadow,backdrop-blur,blur,effectsboolean,texttransition,duration,cubicBezier,easing(как токены значений/spec, без auto-apply к UI-элементам)custom— preserved/opaque: verbatim в manifest, skip + warning на типизированных платформах
glass распознаётся и сохраняется в manifest. Настоящие Figma glass-объекты не имеют поддерживаемой формы в CSS/SCSS/native outputs и получают ошибку вместо подстановки blur.
Исключены из трансформации:
template,composition,grid
Manifest поддерживает те же разрешённые типы токенов и намеренно не эмитит запрещённые template, composition, grid.
Политика no-hallucination:
- Если токен невалиден для платформы, он не “додумывается”: фиксируется
error, токен не эмитится. - Если алиас неразрешим, поведение строго по
unresolvedAliases(error | warn | ignore). - Для прод-профиля рекомендуется
unsupportedTypes.default: error.
