Skip to content

Latest commit

 

History

History
610 lines (468 loc) · 13.9 KB

README.md

File metadata and controls

610 lines (468 loc) · 13.9 KB

Design Tokens Format Module

— A Typescript implementation of the Design Tokens Format Module specification along with some utility functions

Abstract

This repository aims to provide comprehensive tooling to work with the Design Tokens Format Module specification when it comes to parse, validate, resolve aliasing and (maybe) manage design token files.

⚠️ Please note, neither the DTCG specification nor this library are stable yet. The DTCG specification is currently under draft phase and the library might integrate unstable APIs for the sake of research.

Introducing Design Tokens

According to the DTCG, Design Tokens are described using a JSON object made of arbitrary nested groups, organizing tokens.

While tokens are responsible to carry the actual value ($value), we also distinguish aliases that help reference another token within the same JSON object using the {dot.path} notation.

The DTCG does not give a name (yet?) to this very JSON object. For the sake of clarity, this library refers to it as DesignTokenTree.

The Design Token Tree

At the top level definition we encounter the DesignTokenTree. A simple recursive structure allowing deep group nesting where Design Tokens can be defined.

type DesignTokenTree = {
  [name: string]: DesignToken | DesignTokenGroup | DesignTokenTree;
};
{
  "colors": {
    "primary": {
      "$type": "color",
      "$value": "#ff0000"
    }
  }
}

The Design Token Group

A DesignTokenGroup is a simple object that can be used to group Design Tokens together. It is used to provide an arbitrary semantic meaning to a set of Design Tokens.

It can also define a $type property, which will be inherited by all Design Tokens within the group.

export type DesignTokenGroup = {
  $type?: DesignTokenType;
  $description?: string;
};
{
  "some": {
    "nested": {
      "groups": {}
    }
  }
}

The (actual) Design Token

A Design Token is recognized by the presence of a $value property. It can be a simple primitive value, a complex object or an alias to another token of the same type.

The type must be specified most of the time, but the library will try to infer against JSONTypeName from the value if not specified.

export type DesignToken = {
  $value: DesignTokenValue;
  $type?: DesignTokenType;
  $description?: string;
  $extensions?: JSONValue;
};
{
  "border-default": {
    "$type": "border",
    "$value": {
      "width": "1px",
      "style": "solid",
      "color": "#000000"
    }
  }
}

Design Token types

The DTCG specification defines a set of Design Token types that can be used to describe the value of a Design Token. The library provides a set of constants to use as $type value.

As of today (Oct 2022), the following types are supported:

type DesignTokenType =
  // JSON types
  | 'string'
  | 'number'
  | 'boolean'
  | 'null'
  | 'object'
  | 'array'
  // DTCG types
  | 'color'
  | 'dimension'
  | 'fontFamily'
  | 'fontWeight'
  | 'duration'
  | 'cubicBezier'
  // Composite types
  | 'shadow'
  | 'strokeStyle'
  | 'border'
  | 'transition'
  | 'gradient'
  | 'typography'

More details in the API section down below 👇

Design Token Alias

The DesignTokenAlias is a special type of Design Token that references another token within the same Design Token Tree using the {dot.path} notation.

type DesignTokenAlias = `{${string}}`;
{
  "color": {
    "base": {
      "$type": "color",
      "$value": "#ff0000"
    },
    "some-other-color": {
      "$type": "color",
      "$value": "{color.primary.base}"
    }
  }
}

(Smart) $type resolution over groups

To avoid having to specify the $type Design Token of a given Group, the DTCG allows to define group-level $types.

{
  "colors": {
    "$type": "color",
    "primary": {
      "$value": "#ff0000"
    },
    "secondary": {
      "$value": "#00ff00"
    }
  }
}

colors.primary and colors.secondary will both be resolved with type color.

Usage

Installation

$ npm install design-tokens-format-module

Validate a Design Token Tree

The first usage of the parser is to validate a Design Token Tree against the DTCG specification.

import { parseDesignTokens, DesignTokenTree } from "design-tokens-format-module";

const tokens: DesignTokenTree = {
  "colors": {
    "$type": "color",
    "primary": {
      "$value": "#ff0000"
    }
  }
};

const parsedTokens = parseDesignTokens(tokens);

This outputs almost the same structure:

// parsedTokens =
{
  "colors": {
    "$type": "color",
    "primary": {
      "$type": "color", // <-- Resolved from the group-level $type
      "$value": "#ff0000"
    }
  }
}

But where tokens was a DesignTokenTree, parsedTokens is now a ConcreteDesignTokenTree.

The library provides a ConcreteDesignTokenTypeValueGuard type offering type safety in consumer code. This is a special type of DesignTokenTree that has been validated against the DTCG specification and defines only valid $type<>$value combinations.

Resolve aliases

In order to fully validate the tree and make it ready for further serialization operations, the parser takes an optional resolveAliases parameter that will resolve all aliases in the tree.

import { parseDesignTokens, DesignTokenTree } from "design-tokens-format-module";

const tokens: DesignTokenTree = {
  colors: {
    $type: 'color',
    primary: {
      $value: '#000000',
      $description: 'This is a primary color',
    },
    secondary: {
      $value: '{colors.primary}',
    },
  },
};
const parsedTokens = parseDesignTokens(tokens, { resolveAliases: true });

parsedTokens.colors.secondary now holds the content of its alias.

// parsedTokens =
{
  "colors": {
    "$type": "color",
    "primary": {
      "$type": "color",
      "$value": "#000000",
      "$description": "This is a primary color"
    },
    "secondary": {
      "$type": "color",
      "$value": {
        "$type": "color",
        "$value": "#000000",
        "$description": "This is a primary color"
      }
    }
  }
}

Get additional metadata

Along the development of this library, some metadata information, such as the path to a design token or the kind of a tree node. the parser takes an optional publishMetadata parameter that will add metadata to the concrete tree.

Additions are:

  • _kind property on each node ('group' | 'token' | 'alias')
  • _path property on each node (Array<string>)
  • _name property on ConcreteDesignTokenAlias only to reference the original name of the alias
import { parseDesignTokens, DesignTokenTree } from "design-tokens-format-module";

const input: DesignTokenTree = {
  'a-color': {
    $type: 'color',
    $value: '#000000',
    $description: 'This is a color',
  },
};
const parsedTokens = parseDesignTokens(input, { publishMetadata: true });

parsedTokens has now _ prefixed properties.

// parsedTokens =
{
  "a-color": {
    "$type": "color",
    "$value": "#000000",
    "$description": "This is a color",
    "_kind": "token",
    "_path": ["a-color"]
  }
}

A full example with aliases and metadata

import { parseDesignTokens, DesignTokenTree } from "design-tokens-format-module";

const tokens: DesignTokenTree = {
  colors: {
    $type: 'color',
    primary: {
      $value: '#000000',
      $description: 'This is a primary color',
    },
    secondary: {
      $value: '{colors.primary}',
    },
  },
};
const parsedTokens = parseDesignTokens(tokens, { resolveAliases: true, publishMetadata: true });
// parsedTokens =
{
  "colors": {
    "$type": "color",
    "_kind": "group",
    "_path": ["colors"],
    "primary": {
      "$type": "color",
      "$value": "#000000",
      "$description": "This is a primary color",
      "_kind": "token",
      "_path": ["colors", "primary"]
    },
    "secondary": {
      "$type": "color",
      "$value": {
        "$type": "color",
        "$value": "#000000",
        "$description": "This is a primary color",
        "_kind": "alias",
        "_path": ["colors", "primary"],
        "_name": "primary" // <-- The original name of the alias
      },
      "_kind": "token",
      "_path": ["colors", "secondary"]
    }
  }
}

API

Token tree

The DesignTokenTree is the JSON Object data structure that represents the entire design tokens document.

type DesignTokenTree = {
  [name: string]: DesignToken | DesignTokenGroup | DesignTokenTree;
};

At each level of the token tree, we can have either an actual design token, a token group or yet another token tree, recursively.

Aliasing

In order to avoid duplication of declarations, a DesignToken can reference another DesignToken using the $value property and the {token.path} syntax.

type DesignTokenAlias = `{${string}}` // e.g. {colors.primary}

Token types

We distinguish 3 categories of design tokens:

  • JSON values (string, number, boolean, ...)
  • Primitive Design Tokens (color, duration, ...)
  • Composite Design Tokens (border, shadow, ...)

JSON types

They are the only types we might infer if not provided explicitly in the given TokenTree.

type JSONTokenType = 'string' | 'number' | 'boolean' | 'null' | 'array' | 'object'

Color

type name: 'color'

type ColorValue = `#${string}` | DesignTokenAlias

Dimension

type name: 'dimension'

type DimensionValue = `${number}px` | `${number}rem` | DesignTokenAlias; // 1px | 1rem

Font Family

type name: 'fontFamily'

type FontFamilyValue = string | string[] | DesignTokenAlias; // "Helvetica" | ["Helvetica", "Arial", sans-serif]

Font Weight

type name: 'fontWeight'

type FontWeightValue =
  | number // [1-1000]
  | FontWeightNomenclature[keyof FontWeightNomenclature]['value'] // 'thin' | 'hairline' | 'extra-light' | 'ultra-light' | 'light' | 'normal' | 'regular' | 'book' | 'medium' | 'semi-bold' | 'demi-bold' | 'bold' | 'extra-bold' | 'ultra-bold' | 'black' | 'heavy' | 'extra-black' | 'ultra-black'
  | DesignTokenAlias;

Duration

type name: 'duration'

type DurationValue = `${number}ms` | DesignTokenAlias; // 100ms

Cubic Bezier

type name: 'cubicBezier'

type CubicBezierValue =
  | [P1x: number, P1y: number, P2x: number, P2y: number]
  | DesignTokenAlias;

Shadow

type name: 'shadow'

type ShadowValue =
  | {
  color: ColorValue;
  offsetX: DimensionValue;
  offsetY: DimensionValue;
  blur: DimensionValue;
  spread: DimensionValue;
}
  | DesignTokenAlias;

Stroke Style

type name: 'strokeStyle'

type StrokeStyleValue =
  | 'solid'
  | 'dashed'
  | 'dotted'
  | 'double'
  | 'groove'
  | 'ridge'
  | 'outset'
  | 'inset'
  | {
  dashArray: DimensionValue[];
  lineCap: 'round' | 'butt' | 'square';
}
  | DesignTokenAlias;

Border

type name: 'border'

type BorderValue =
  | {
  color: ColorValue;
  width: DimensionValue;
  style: StrokeStyleValue;
}
  | DesignTokenAlias;

Transition

type name: 'transition'

type TransitionValue =
  | {
  duration: DurationValue;
  delay: DurationValue;
  timingFunction: CubicBezierValue;
}
  | DesignTokenAlias;

Gradient

type name: 'gradient'

type GradientValue =
  | Array<{
  color: ColorValue;
  position: JSONNumberValue;
}>
  | DesignTokenAlias;

Typography

type name: 'typography'

type TypographyValue =
  | {
  fontFamily: FontFamilyValue;
  fontSize: DimensionValue;
  fontWeight: FontWeightValue;
  letterSpacing: DimensionValue;
  lineHeight: JSONStringValue;
}
  | DesignTokenAlias;

Utility functions

parseDesignTokens

declare function parseDesignTokens<A extends boolean, M extends boolean>(
  input: TokenTree,
  parserOptions: {
    publishMetadata?: M; // defaults to false
    resolveAliases?: A;  // defaults to false
  }
): ConcreteTokenTree<A, M>;

The function handles :

  • Validation of the input against the Design Token spec
  • Resolution of the $type field for any token
  • Optional resolution of aliases
  • Optional population of _ prefixed metadata

⚠️ Limitation: The detection of circular aliasing does not work without parserOptions.resolveAliases set to true.

validateDesignTokenValue

While constructing a Design Token Tree, you may need to validate token values individually. The library provides a validateDesignTokenValue based on the zod validation library.

declare function validateDesignTokenValue(
  tokenType: DesignTokenType,
  tokenValue: DesignTokenValue
): any

Note: return type must be improved

validateDesignTokenAndGroupName

Since token names and group names are meant to be used in {dot.path} notation, we need to ensure they avoid the 3 characters: ., {, }.

declare function validateDesignTokenAndGroupName(name: string): string

matchIsDesignTokenAlias

Used to deduce if a given string is an alias following the {dot.path} notation.

declare function matchIsDesignTokenAlias(value: any): boolean

Types

JSONTypeName

type JSONTypeName = 'string' | 'number' | 'boolean' | 'null' | 'array' | 'object'

Roadmap

  • Implement the latest specification Typescript type definition
  • Add a parse function to traverse any given token tree
  • Resolve aliases
  • Add validation logic for all known token types
  • Expose validation only API
  • Expose parse API with options for alias resolution and metadata population
  • Expose proper types for external consumers
  • Add support for circular aliasing detection when not resolving aliases