@sxl-studio/token-transformer
v1.0.0
Published
Transform DTCG design tokens to CSS, SwiftUI, and Kotlin
Downloads
121
Maintainers
Readme
SXL Studio — Token Transformer
Инструмент для трансформации дизайн-токенов из JSON (DTCG-формат) в валидный код для трёх платформ:
- Web — CSS Custom Properties
- iOS — SwiftUI
- Android — Kotlin Compose
- Vue 3 — Composition JSON → Vue 3 SFC (
.vue)
Быстрый старт
# Установка зависимостей
npm install
# Запуск с конфигом по умолчанию
npx tsx src/cli.ts
# Запуск с указанием конфига
npx tsx src/cli.ts --config=config.example.json
# Генерация конфига по умолчанию
npx tsx src/cli.ts --init
# Трансформация composition → Vue 3
npx tsx src/cli.ts --composition=path/to/composition.json --output=./output/dirСтруктура проекта
Transformer/
├── src/
│ ├── cli.ts # Точка входа CLI
│ ├── core/
│ │ ├── types.ts # Типы и интерфейсы
│ │ ├── parser.ts # Парсинг JSON, резолв алиасов, мат. выражения
│ │ ├── loader.ts # Загрузка токенов из файлов
│ │ └── writer.ts # Трансформация и запись файлов
│ ├── transformers/
│ │ ├── css.ts # Генерация CSS Custom Properties
│ │ ├── swiftui.ts # Генерация SwiftUI-кода
│ │ ├── kotlin.ts # Генерация Kotlin Compose-кода
│ │ └── vue3.ts # Composition JSON → Vue 3 SFC
│ └── utils/
│ ├── naming.ts # Утилиты именования (kebab, camel, pascal)
│ ├── color.ts # Парсинг и форматирование цветов
│ └── dimension.ts # Парсинг и форматирование размеров
├── config.example.json # Конфиг для тестовых токенов
├── config.admin-ui.json # Конфиг для admin-ui проекта
├── config.site-ui.json # Конфиг для site-ui проекта
├── project-style/ # Выходные файлы (генерируется)
└── package.jsonКонфигурация
Конфиг — JSON-файл со следующей структурой:
Полная схема
{
"source": {
"tokenDir": "../Plugin/tokens",
"configFile": "config.json",
"include": ["example/*.json", "core/*.json"],
"exclude": ["config.json", "**/diff-id*.json", "**/composition*"]
},
"platforms": {
"css": {
"outputDir": "./project-style/example",
"prefix": "ds",
"resolveAliases": false,
"splitEffects": true,
"showDescriptions": true,
"codeSyntax": {
"colorFormat": "hex",
"prefix": "ds"
},
"fileMapping": [
{
"sources": ["example/*.json"],
"output": "all-tokens.css",
"filter": {
"types": ["color", "dimension"],
"paths": ["color.", "spacing."],
"excludePaths": ["color.internal."]
}
}
]
},
"swiftui": {
"outputDir": "./project-style/example",
"prefix": "DS",
"resolveAliases": true,
"showDescriptions": true,
"fileMapping": [
{
"sources": ["example/*.json"],
"output": "AllTokens.swift"
}
]
},
"kotlin": {
"outputDir": "./project-style/example",
"prefix": "DS",
"resolveAliases": true,
"showDescriptions": true,
"fileMapping": [
{
"sources": ["example/*.json"],
"output": "AllTokens.kt"
}
]
}
},
"settings": {
"remBase": 16,
"verbose": false
}
}Описание полей конфига
source — Источник токенов
| Поле | Тип | По умолчанию | Описание |
|------|-----|-------------|----------|
| tokenDir | string | обязательный | Путь к папке с JSON-токенами |
| configFile | string | "config.json" | Путь к config.json плагина (внутри tokenDir) |
| include | string[] | все **/*.json | Glob-паттерны файлов для включения |
| exclude | string[] | ["config.json"] | Glob-паттерны файлов для исключения |
platforms — Настройки платформ
Каждая платформа (css, swiftui, kotlin) настраивается независимо:
| Поле | Тип | По умолчанию | Описание |
|------|-----|-------------|----------|
| outputDir | string | обязательный | Папка для выходных файлов |
| prefix | string | "" | Префикс для имён переменных |
| resolveAliases | boolean | CSS: false, Swift/Kotlin: true | Резолвить ли алиасы в финальные значения |
| splitEffects | boolean | true | Разделять ли эффекты на отдельные переменные (только CSS) |
| showDescriptions | boolean | true | Включать ли $description как комментарии в код |
| codeSyntax | object | — | Настройки формата кода |
| fileMapping | array | — | Правила объединения токенов в файлы |
resolveAliases — Управление алиасами
Ключевая настройка, определяющая как обрабатываются алиасные ссылки ({token.path}) в составных токенах.
CSS (resolveAliases: false — по умолчанию):
/* Алиасы сохраняются как var() */
--typography-heading-xl: var(--font-weight-bold) var(--font-size-3xl)/var(--line-height-relaxed) var(--font-family-sans);
--shadow-sm: var(--number-fx-offset-xs) var(--number-fx-offset-none) var(--number-fx-blur-xs) var(--number-fx-offset-none) var(--color-hex-alpha);CSS (resolveAliases: true):
/* Все значения полностью разрешены */
--ds-typography-heading-xl: 700 30px/24px Inter;
--ds-shadow-sm: 2px 0px 8px 0px rgba(255, 85, 0, 0.502);SwiftUI/Kotlin (resolveAliases: false):
- Чистые алиасы (весь токен = ссылка) → ссылка на константу в том же scope
- Составные токены с алиасными полями → комментарий
/// @ref//** @ref */с путями
/// @ref {color.hex-full}
static let primary = hexFull // ← прямая ссылка
/// @ref {fontFamily.sans}, {fontWeight.bold}, {fontSize.3xl}, {lineHeight.relaxed}
static let headingXl: Font = .system(size: 30, weight: .bold) // ← значения + ref-комментарийSwiftUI/Kotlin (resolveAliases: true — по умолчанию):
static let primary = Color(red: 1, green: 0.3333, blue: 0, opacity: 1) // ← полностью разрешеноsplitEffects — Разделение эффектов (только CSS)
Управляет поведением смешанных эффектов (shadow + blur + backdrop-blur).
splitEffects: true (по умолчанию):
Смешанные эффекты разделяются на отдельные переменные с суффиксами. Каждая переменная соответствует конкретному CSS-свойству:
/* box-shadow */
--effects-full-mix: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 2px rgba(255,255,255,0.5);
/* filter */
--effects-full-mix-blur: blur(2px);
/* backdrop-filter */
--effects-full-mix-backdrop-blur: blur(20px);Использование:
.card {
box-shadow: var(--effects-full-mix);
filter: var(--effects-full-mix-blur);
backdrop-filter: var(--effects-full-mix-backdrop-blur);
}splitEffects: false:
Все эффекты выводятся в одну переменную, только shadow-часть:
--effects-full-mix: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 2px rgba(255,255,255,0.5);showDescriptions — Описания токенов
Управляет выводом $description из JSON в качестве комментариев. Работает на всех платформах.
showDescriptions: true (по умолчанию):
/* Heading XL — all values raw */
--typography-heading-xl: 700 30px/24px Inter;/// Heading XL — all values raw
static let headingXl: Font = .system(size: 30, weight: .bold)/** Heading XL — all values raw */
val HeadingXl = TextStyle(...)showDescriptions: false:
--typography-heading-xl: 700 30px/24px Inter;static let headingXl: Font = .system(size: 30, weight: .bold)val HeadingXl = TextStyle(...)fileMapping — Правила объединения файлов
Позволяет собрать токены из нескольких JSON-файлов в один выходной файл.
{
"sources": ["core/palette.json", "projects/aui/colors.json", "projects/modes/themes/light.json"],
"output": "core.css",
"filter": {
"types": ["color", "dimension"],
"paths": ["color.primary"],
"excludePaths": ["color.internal"]
}
}| Поле | Описание |
|------|----------|
| sources | Массив путей к JSON-файлам (поддерживает * wildcards) |
| output | Имя выходного файла (относительно outputDir) |
| filter.types | Фильтр по типу токена |
| filter.paths | Включить только пути начинающиеся с указанных |
| filter.excludePaths | Исключить пути начинающиеся с указанных |
settings — Глобальные настройки
| Поле | Тип | По умолчанию | Описание |
|------|-----|-------------|----------|
| remBase | number | 16 | Базовое значение для вычисления rem |
| verbose | boolean | false | Подробный вывод логов |
Поддерживаемые типы токенов
Простые типы
| Тип | CSS | SwiftUI | Kotlin |
|-----|-----|---------|--------|
| color | #hex / rgba() | Color(red:green:blue:opacity:) | Color(0xAARRGGBB) |
| dimension | 16px / 1rem | CGFloat | Dp |
| spacing, sizing | 16px | CGFloat | Dp |
| borderRadius, borderWidth | 4px | CGFloat | Dp |
| opacity | 0.5 | Double | Float |
| number | 42 | числовое значение | числовое значение |
| fontFamily | "Inter" | String | String |
| fontWeight | 700 | Font.Weight | FontWeight |
| fontSize, lineHeight | 16px | CGFloat | TextUnit (sp) |
| letterSpacing | 0.5px | CGFloat | TextUnit (sp) |
| duration | 200ms | String | String |
| cubicBezier | cubic-bezier(...) | String | String |
| boolean | true | Bool | Boolean |
| text, string | "value" | String | String |
| textCase | uppercase | String | String |
| textDecoration | underline | String | String |
| strokeStyle | dashed /* ... */ | String | String |
Составные типы
| Тип | CSS | SwiftUI | Kotlin |
|-----|-----|---------|--------|
| typography | 700 16px/24px Inter | Font.system(size:weight:) | TextStyle(...) |
| shadow | 0 4px 8px rgba(...) | .shadow(color:radius:x:y:) | elevation (Dp) |
| border | 1px solid #000 | (color:width:style:) | BorderStroke(...) |
| fill | #hex / gradient(...) | Color(...) / Gradient(...) | Color(...) / Brush(...) |
| gradient | linear-gradient(...) | LinearGradient(...) | Brush.linearGradient(...) |
| effects | см. ниже | .shadow(...) | elevation (Dp) |
| blur | blur(4px) | .blur(radius:) | Modifier.blur(...) |
| backdrop-blur | blur(16px) | .blur(radius:) | Modifier.blur(...) |
| transition | all 200ms ease | String | String |
| grid | repeat(12, 1fr) | String | String |
Обработка Effects (CSS)
В CSS разные типы эффектов используют разные CSS-свойства. Трансформер автоматически создаёт отдельные переменные для каждого типа:
Один тип эффекта
/* box-shadow */
--effects-card: 0 4px 8px rgba(0,0,0,0.1);
/* filter */
--blur-sm: blur(4px);
/* backdrop-filter */
--backdrop-blur-md: blur(12px);Смешанные эффекты (shadow + blur + backdrop-blur)
Когда токен содержит несколько типов эффектов, они разделяются на отдельные переменные с суффиксами:
/* box-shadow */
--effects-full-mix: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 2px rgba(255,255,255,0.5);
/* filter */
--effects-full-mix-blur: blur(2px);
/* backdrop-filter */
--effects-full-mix-backdrop-blur: blur(20px);Использование в CSS:
.card {
box-shadow: var(--effects-full-mix);
filter: var(--effects-full-mix-blur);
backdrop-filter: var(--effects-full-mix-backdrop-blur);
}Алиасы в составных токенах
Все составные токены (typography, shadow, border, fill, gradient, effects) поддерживают алиасные ссылки {token.path} в любом подсвойстве.
Пример JSON
{
"heading": {
"xl": {
"$type": "typography",
"$value": {
"fontFamily": "{fontFamily.sans}",
"fontWeight": "{fontWeight.bold}",
"fontSize": "{fontSize.3xl}",
"lineHeight": "{lineHeight.relaxed}"
}
}
}
}Результат CSS (resolveAliases: false)
--typography-heading-xl: var(--font-weight-bold) var(--font-size-3xl)/var(--line-height-relaxed) var(--font-family-sans);Результат CSS (resolveAliases: true)
--typography-heading-xl: 700 30px/24px Inter;codeSyntax (Figma)
Если токен содержит $extensions.figma.codeSyntax, трансформер использует указанные имена переменных:
{
"primary": {
"$value": "#0066FF",
"$extensions": {
"figma.codeSyntax": {
"Web": "var(--color-primary)",
"iOS": "Color.primary",
"Android": "@color/primary"
}
}
}
}Математические выражения
Токены поддерживают математические выражения, которые вычисляются при трансформации:
{
"spacing": {
"sm": { "$value": "{spacing.base} * 2" },
"md": { "$value": "{spacing.base} * 4" }
}
}Поддерживаемые операции: +, -, *, /, round().
Выражения вычисляются рекурсивно, включая подсвойства составных токенов.
Примеры запуска
Трансформация всех проектов
# Тестовые токены (example)
npx tsx src/cli.ts --config=config.example.json
# Боевые проекты
npx tsx src/cli.ts --config=config.admin-ui.json
npx tsx src/cli.ts --config=config.site-ui.jsonПример конфига для нового проекта
{
"source": {
"tokenDir": "./path/to/tokens",
"exclude": ["config.json"]
},
"platforms": {
"css": {
"outputDir": "./dist/css",
"prefix": "my",
"resolveAliases": false,
"splitEffects": true,
"showDescriptions": true,
"fileMapping": [
{
"sources": ["core/*.json", "themes/light.json"],
"output": "core.css"
},
{
"sources": ["themes/dark.json"],
"output": "themes/dark.css"
}
]
},
"swiftui": {
"outputDir": "./dist/ios",
"prefix": "My",
"resolveAliases": false,
"showDescriptions": true,
"fileMapping": [
{
"sources": ["core/*.json", "themes/light.json"],
"output": "MyTokens.swift"
}
]
},
"kotlin": {
"outputDir": "./dist/android",
"prefix": "My",
"resolveAliases": true,
"showDescriptions": false,
"fileMapping": [
{
"sources": ["core/*.json", "themes/light.json"],
"output": "MyTokens.kt"
}
]
}
},
"settings": {
"remBase": 16
}
}Composition → Vue 3 (SFC)
Трансформер поддерживает преобразование composition-токенов из JSON в готовые Vue 3 Single File Components (.vue).
Запуск
npx tsx src/cli.ts --composition=path/to/composition.json --output=./output/dir| Параметр | Описание |
|----------|----------|
| --composition | Путь к JSON-файлу с composition-токеном |
| --output | Папка для выходного .vue файла (по умолчанию — рядом с исходником) |
Формат входного JSON
{
"$type": "composition",
"name": "WButton",
"props": {
"state": ["default", "hover", "active", "disabled"],
"style": ["accent", "secondary", "tertiary"],
"size": ["sm", "md", "lg"]
},
"structure": {
"tag": "FRAME", "class": "btn",
"children": [
{ "tag": "INSTANCE", "class": "btn-icon-left", "ref": { "component": "icon-name" } },
{ "tag": "TEXT", "class": "btn-label", "content": "Button" }
]
},
"componentProperties": {
"label": { "type": "TEXT", "layer": "btn-label", "defaultValue": "Button" },
"isIconLeft": { "type": "BOOLEAN", "layer": "btn-icon-left", "defaultValue": false }
},
"styles": { "btn": { "layoutMode": "HORIZONTAL", "fills": ["{accent.medium}"], ... } },
"adapters": {
"style=accent": { "btn": { "fills": ["{info.medium}"] } },
"size=sm": { "btn": { "paddingLeft": "12", ... } },
"state=hover": { "btn": { "opacity": "0.88" } }
}
}Что генерируется
Готовый Vue 3 SFC с тремя секциями:
<template> — HTML-структура из structure:
FRAME→<div>/<button>(определяется по имени компонента)TEXT→<span>с привязкой к props или слотамINSTANCE→ компонентный тег (<WIcon>, и т.д.)BOOLEANсвойства →v-ifдирективыINSTANCE_SWAP→ динамический:nameprop
<script setup lang="ts"> — логика:
defineProps<Props>()с типами изpropsиcomponentPropertieswithDefaults()со значениями по умолчаниюuseCssModule()для CSS Modulescomputed()для variant/size классов с exhaustiveRecord<>маппингом- Prop
styleавтоматически переименовывается вvariant(конфликт с Vue) state=disabled→ отдельныйdisabledboolean prop
<style module> — CSS:
- Базовые стили из
stylesс маппингом Figma → CSS - Modifier-классы из
adapters(.btn--accent,.btn--sm, и т.д.) state=hover/active/focus→ CSS псевдоклассы (:hover,:active,:focus)state=disabled→.btn--disabled+:disabled- Токен-ссылки
{accent.medium}→var(--accent-medium) - TEXT/INSTANCE слои:
fills→color(неbackground-color)
Маппинг Figma-свойств → CSS
| Figma | CSS |
|-------|-----|
| layoutMode: "HORIZONTAL" | display: flex |
| layoutMode: "VERTICAL" | display: flex; flex-direction: column |
| primaryAxisAlignItems | justify-content |
| counterAxisAlignItems | align-items |
| layoutSizingHorizontal: "HUG" | width: fit-content |
| layoutSizingHorizontal: "FILL" | flex: 1 |
| itemSpacing | gap |
| padding* | padding (shorthand) |
| cornerRadius | border-radius |
| fills: ["{token}"] | background-color: var(--token) / color: var(--token) |
| strokeWeight + strokes | border |
| opacity | opacity |
| clipsContent | overflow: hidden |
| fontSize | font-size |
| fontFamily | font-family |
| fontWeight | font-weight (с маппингом имён → числа) |
| textAlignHorizontal | text-align |
Пример результата
<template>
<button :class="[$style['btn'], variantClass, sizeClass, { [$style['btn--disabled']]: disabled }]"
:disabled="disabled || undefined">
<WIcon v-if="isIconLeft" :class="$style['btn-icon-left']" :name="iconLeft" />
<span :class="$style['btn-label']">{{ label }}</span>
</button>
</template>
<script setup lang="ts">
import { computed, useCssModule } from 'vue'
type Props = {
variant?: 'accent' | 'secondary' | 'tertiary'
size?: 'sm' | 'md' | 'lg'
label?: string
isIconLeft?: boolean
iconLeft?: string
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
variant: 'accent',
size: 'sm',
label: 'Button',
isIconLeft: false,
iconLeft: '',
disabled: false,
})
const $style = useCssModule()
const variantClass = computed(() => {
const map: Record<NonNullable<Props['variant']>, string> = {
'accent': $style['btn--accent'],
'secondary': $style['btn--secondary'],
'tertiary': $style['btn--tertiary'],
}
return map[props.variant ?? 'accent']
})
</script>
<style module>
.btn {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 8px;
background-color: var(--accent-medium);
}
.btn--accent { background-color: var(--info-medium); }
.btn:hover { opacity: 0.88; }
.btn--disabled, .btn:disabled { opacity: 0.4; pointer-events: none; }
</style>Справочник по настройке prefix
| Платформа | prefix | Результат именования |
|-----------|--------|---------------------|
| CSS | "" | --color-primary |
| CSS | "ds" | --ds-color-primary |
| SwiftUI | "" | Color.colorPrimary, enum Spacing |
| SwiftUI | "DS" | Color.dsColorPrimary, enum DSSpacing |
| Kotlin | "" | DSColors.ColorPrimary, DSSpacing.SpacingXs |
| Kotlin | "App" | AppColors.ColorPrimary, AppSpacing.SpacingXs |
Troubleshooting
Файл конфига не найден
Config file not found: ...
Run with --init to generate a default config.Используйте --config=путь/к/файлу.json или --init для генерации.
Токены не попадают в выходной файл
Проверьте fileMapping.sources — пути указываются относительно tokenDir.
var() не появляются в CSS
Убедитесь что "resolveAliases": false установлен для платформы css.
Алиасы не разрешаются
Убедитесь что токен-источник существует в подключённых файлах (include / sources).
Эффекты не разделяются на отдельные переменные
Убедитесь что "splitEffects": true установлен для платформы css. По умолчанию true.
Описания ($description) не отображаются
Убедитесь что "showDescriptions": true установлен для нужной платформы. По умолчанию true.
