Thanks for your interest in contributing to @baur-software/figma-to!
# Clone the repository
git clone git@github.com:Baur-Software/Figma-To.git
cd Figma-To
# Install dependencies
npm install
# Build the project
npm run build
# Run tests
npm run test:e2esrc/
├── schema/ # Type definitions
│ ├── tokens.ts # Normalized token schema (W3C DTCG aligned)
│ ├── figma.ts # Figma API types (re-exports @figma/rest-api-spec)
│ ├── ionic.ts # Ionic theme types
│ └── tailwind.ts # Tailwind output types
├── adapters/
│ ├── figma/ # Input adapter: Figma → normalized tokens
│ └── tailwind-ionic/ # Output adapter: tokens → CSS
└── index.ts # Public API exports
tests/
└── e2e/ # Playwright E2E tests
└── fixtures/ # Mock data for testing
The library uses an adapter pattern to separate concerns:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Input Source │ ──▶ │ Normalized Theme │ ──▶ │ Output Format │
│ (Figma, etc.) │ │ (ThemeFile) │ │ (CSS, etc.) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
InputAdapter ThemeFile OutputAdapter
This design allows:
- Adding new input sources without changing output logic
- Adding new output formats without changing input parsing
- Testing each adapter independently
Input adapters convert external design tool data into the normalized ThemeFile format.
// src/adapters/tokens-studio/types.ts
export interface TokensStudioInput {
tokens: Record<string, TokensStudioToken>;
// ... other fields from Tokens Studio format
}// src/adapters/tokens-studio/adapter.ts
import type { InputAdapter, ThemeFile } from '../../schema';
import type { TokensStudioInput } from './types';
export class TokensStudioAdapter implements InputAdapter<TokensStudioInput> {
/**
* Validate the input data before parsing
*/
async validate(source: TokensStudioInput): Promise<{
valid: boolean;
errors?: string[];
}> {
const errors: string[] = [];
if (!source.tokens) {
errors.push('Missing tokens object');
}
return {
valid: errors.length === 0,
errors: errors.length > 0 ? errors : undefined,
};
}
/**
* Parse input into normalized ThemeFile
*/
async parse(source: TokensStudioInput): Promise<ThemeFile> {
const validation = await this.validate(source);
if (!validation.valid) {
throw new Error(`Invalid input: ${validation.errors?.join(', ')}`);
}
return {
name: 'Tokens Studio Theme',
collections: this.parseCollections(source.tokens),
meta: {
source: 'tokens-studio',
generatedAt: new Date().toISOString(),
},
};
}
private parseCollections(tokens: Record<string, unknown>): TokenCollection[] {
// Convert Tokens Studio structure to TokenCollection[]
// ...
}
}
// Factory function for cleaner API
export function createTokensStudioAdapter(): TokensStudioAdapter {
return new TokensStudioAdapter();
}// src/adapters/tokens-studio/index.ts
export { TokensStudioAdapter, createTokensStudioAdapter } from './adapter';
export type { TokensStudioInput } from './types';// src/index.ts
export {
createTokensStudioAdapter,
type TokensStudioInput,
} from './adapters/tokens-studio';Output adapters convert the normalized ThemeFile into specific output formats.
// src/adapters/scss/types.ts
export interface ScssOutput {
variables: string; // $color-primary: #3880f6;
maps: string; // $colors: (primary: #3880f6, ...);
mixins: string; // @mixin theme-colors { ... }
files: Record<string, string>;
}
export interface ScssOptions {
prefix?: string;
generateMaps?: boolean;
generateMixins?: boolean;
}// src/adapters/scss/adapter.ts
import type { OutputAdapter, ThemeFile } from '../../schema';
import type { ScssOutput, ScssOptions } from './types';
export class ScssAdapter implements OutputAdapter<ScssOutput, ScssOptions> {
/**
* Transform normalized theme into SCSS output
*/
async transform(
theme: ThemeFile,
options?: ScssOptions
): Promise<ScssOutput> {
const variables = this.generateVariables(theme, options);
const maps = options?.generateMaps
? this.generateMaps(theme, options)
: '';
const mixins = options?.generateMixins
? this.generateMixins(theme, options)
: '';
return {
variables,
maps,
mixins,
files: {
'_variables.scss': variables,
'_maps.scss': maps,
'_mixins.scss': mixins,
'_theme.scss': [variables, maps, mixins].filter(Boolean).join('\n\n'),
},
};
}
private generateVariables(theme: ThemeFile, options?: ScssOptions): string {
const prefix = options?.prefix ?? '';
const lines: string[] = ['// Generated SCSS Variables'];
for (const collection of theme.collections) {
const tokens = collection.tokens[collection.defaultMode];
// Recursively process tokens...
this.processTokens(tokens, prefix, lines);
}
return lines.join('\n');
}
private processTokens(
tokens: Record<string, unknown>,
prefix: string,
lines: string[]
): void {
// Convert tokens to SCSS variable declarations
// $prefix-name: value;
}
private generateMaps(theme: ThemeFile, options?: ScssOptions): string {
// Generate SCSS maps like $colors: (primary: $color-primary, ...)
}
private generateMixins(theme: ThemeFile, options?: ScssOptions): string {
// Generate useful mixins
}
}
export function createScssAdapter(): ScssAdapter {
return new ScssAdapter();
}// tests/e2e/scss-output.spec.ts
import { test, expect } from '@playwright/test';
import { createFigmaAdapter } from '../../dist/adapters/figma/index.js';
import { createScssAdapter } from '../../dist/adapters/scss/index.js';
import { mockFigmaVariablesResponse } from './fixtures/figma-variables.js';
test.describe('SCSS Output Adapter', () => {
test('generates SCSS variables', async () => {
const figmaAdapter = createFigmaAdapter();
const theme = await figmaAdapter.parse({
variablesResponse: mockFigmaVariablesResponse,
});
const scssAdapter = createScssAdapter();
const output = await scssAdapter.transform(theme);
expect(output.variables).toContain('$color-primary');
expect(output.files['_variables.scss']).toBeTruthy();
});
test('generates SCSS maps when enabled', async () => {
// ...
});
});All adapters work with the ThemeFile format defined in src/schema/tokens.ts:
interface ThemeFile {
name: string;
collections: TokenCollection[];
meta?: {
source?: string;
figmaFileKey?: string;
lastSynced?: string;
generatedAt?: string;
};
}
interface TokenCollection {
name: string;
modes: string[]; // ['Light', 'Dark']
defaultMode: string; // 'Light'
tokens: Record<string, TokenGroup>; // tokens per mode
}
interface Token {
$type: TokenType; // 'color' | 'dimension' | 'fontFamily' | ...
$value: TokenValue; // The actual value
$description?: string;
$extensions?: {
'com.figma'?: FigmaExtensions;
'com.ionic'?: IonicExtensions;
};
}| Type | $value Format | Example |
|---|---|---|
color |
{ r, g, b, a } (0-1) |
{ r: 0.22, g: 0.50, b: 0.96, a: 1 } |
dimension |
{ value, unit } |
{ value: 16, unit: 'px' } |
fontFamily |
string[] |
['Inter', 'sans-serif'] |
fontWeight |
number |
700 |
string |
string |
'Inter' |
Tokens can reference other tokens:
{
$type: 'color',
$value: {
$ref: 'colors.primary.500' // Reference path
}
}- TypeScript strict mode
- Explicit return types on public functions
- JSDoc comments on exported items
- Prefer
interfaceovertypefor object shapes - Use factory functions (
createXxxAdapter()) for instantiation
We use Playwright for E2E tests that validate the full pipeline:
# Run all E2E tests
npm run test:e2e
# Run specific test file
npx playwright test tests/e2e/scss-output.spec.ts
# Run with UI
npx playwright test --uitest.describe('Adapter Name', () => {
// Setup: parse mock data once
let theme: ThemeFile;
test.beforeAll(async () => {
const figmaAdapter = createFigmaAdapter();
theme = await figmaAdapter.parse({
variablesResponse: mockFigmaVariablesResponse,
});
});
test('does something specific', async () => {
const adapter = createYourAdapter();
const output = await adapter.transform(theme);
expect(output.someProperty).toBe(expectedValue);
});
});- Fork the repository
- Create a feature branch:
git checkout -b feature/tokens-studio-adapter - Make your changes with tests
- Run the full test suite:
npm run test:e2e - Commit with a descriptive message
- Open a Pull Request
feat(adapters): add Tokens Studio input adapter
- Parse Tokens Studio JSON format
- Support nested token groups
- Handle token references
Prefixes: feat, fix, docs, test, refactor, chore
Open an issue on GitHub or start a discussion.