Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@cobalt-ui/core

Package Overview
Dependencies
Maintainers
1
Versions
49
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@cobalt-ui/core - npm Package Compare versions

Comparing version 0.0.2 to 0.1.0

dist/index.min.js

12

CHANGELOG.md
# @cobalt-ui/core
## 0.0.2
## 0.1.0
### Minor Changes
### Patch Changes
- 5748e72: Use JSON to align with the Design Tokens W3C spec
- 6bd02b5: Add image fetching from Figma
## 0.0.1
### Patch Changes
- 21c653b: Add Figma support
- Updated dependencies [5748e72]
- @cobalt-ui/utils@0.1.0

@@ -1,4 +0,56 @@

export * from './build.js';
export * from './config.js';
export * from './parse.js';
export * from './validate.js';
/// <reference types="node" />
import type { TokenType, ParseResult, Schema } from './parse.js';
export type { ColorToken, ConicGradientToken, CubicBezierToken, DimensionToken, FileToken, FontToken, Group, LinearGradientToken, Mode, ParsedMetadata, ParsedToken, ParseResult, RadialGradientToken, Schema, ShadowToken, Token, TokenBase, TokenOrGroup, TokenType, } from './parse.js';
import { parse } from './parse.js';
export { parse } from './parse.js';
export interface BuildResult {
/** File to output inside config.outDir (ex: ./tokens.sass) */
fileName: string;
/** File contents */
contents: string | Buffer;
}
export interface FigmaComponent {
component: string;
token: string;
type: TokenType;
file?: string;
}
export interface FigmaStyle {
style: string;
token: string;
type: TokenType;
file?: string;
}
export interface FigmaMapping {
[url: string]: (FigmaStyle | FigmaComponent)[];
}
export interface Config {
tokens: URL;
outDir: URL;
plugins: Plugin[];
figma?: FigmaMapping;
}
export interface Plugin {
name: string;
/** (optional) load config */
config?: (config: Config) => void;
/** main build fn */
build(options: {
schema: ParseResult['result'];
rawSchema: Schema;
}): Promise<BuildResult[]>;
}
export interface UserConfig {
/** path to tokens.json (default: "./tokens.json") */
tokens?: string;
/** output directory (default: "./tokens/") */
outDir?: string;
/** specify plugins (default: @cobalt-ui/plugin-json, @cobalt-ui/plugin-sass, @cobalt-ui/plugin-ts) */
plugins: Plugin[];
/** add figma keys */
figma?: FigmaMapping;
}
declare const _default: {
parse: typeof parse;
};
export default _default;

@@ -1,4 +0,5 @@

export * from './build.js';
export * from './config.js';
export * from './parse.js';
export * from './validate.js';
import { parse } from './parse.js';
export { parse } from './parse.js';
export default {
parse,
};

@@ -1,110 +0,73 @@

export declare type NodeType = 'token' | 'group' | 'file' | 'url';
export interface RawTokenSchema {
/** Manifest name */
name?: string;
/** Metadata. Useful for any arbitrary data */
metadata?: Record<string, any>;
/** Version. Only useful for the design system */
version?: number;
/** Tokens. Required */
tokens: {
[tokensOrGroup: string]: RawSchemaNode;
};
export interface ColorToken extends TokenBase<string> {
type: 'color';
}
export interface TokenSchema extends RawTokenSchema {
tokens: {
[tokensOrGroup: string]: SchemaNode;
};
export interface ConicGradientToken extends TokenBase<string> {
type: 'conic-gradient';
}
/** An arbitrary grouping of tokens. Groups can be nested infinitely to form namespaces. */
export interface RawGroupNode<T = string> {
type: 'group';
/** (optional) User-friendly name of the group */
name?: string;
/** (optional) Longer descripton of this group */
description?: string;
/** (optional) Enforce that all child tokens have values for all modes */
modes?: string[];
tokens: {
[tokensOrGroup: string]: RawSchemaNode<T>;
};
export interface CubicBezierToken extends TokenBase<[number, number, number, number]> {
type: 'cubic-bezier';
}
/** An arbitrary grouping of tokens. Groups can be nested infinitely to form namespaces. */
export interface GroupNode<T = string> extends RawGroupNode<T> {
/** unique identifier (e.g. "color.gray") */
id: string;
/** id within group (e.g. "gray") */
localID: string;
/** group reference (yes, groups can be nested!) */
group?: GroupNode;
tokens: {
[tokensOrGroup: string]: SchemaNode<T>;
};
export interface DimensionToken extends TokenBase<string> {
type: 'dimension';
}
/** A design token. */
export interface RawTokenNode<T = string> {
type: 'token' | undefined;
/** (optional) User-friendly name of this token */
name?: string;
/** (optional) Longer description of this token */
description?: string;
value: T | T[] | TokenValue<T>;
export interface FontToken extends TokenBase<string[]> {
type: 'font';
}
/** A design token. */
export interface TokenNode<T = string> extends RawTokenNode<T> {
/** unique identifier (e.g. "color.gray") */
id: string;
type: 'token';
/** id within group (e.g. "gray") */
localID: string;
/** group reference */
group?: GroupNode;
value: TokenValue<T>;
export interface FileToken extends TokenBase<string> {
type: 'file';
}
/** A local file on disk. */
export interface RawFileNode {
type: 'file';
/** (optional) User-friendly name of this token */
export declare type Group = {
metadata?: Record<string, unknown>;
} & {
[childNode: string]: TokenOrGroup;
};
export interface LinearGradientToken extends TokenBase<string> {
type: 'linear-gradient';
}
export declare type Mode<T = string> = Record<string, T>;
export interface RadialGradientToken extends TokenBase<string> {
type: 'radial-gradient';
}
export interface ShadowToken extends TokenBase<string[]> {
type: 'shadow';
}
export interface TokenBase<T = string> {
/** User-friendly name */
name?: string;
/** (optional) Longer description of this token */
/** Token description */
description?: string;
value: string | string[] | TokenValue<string>;
/** Token value */
value: T;
/** Mode variants */
mode: Mode<T>;
}
/** A local file on disk. */
export interface FileNode extends RawFileNode {
/** unique identifier (e.g. "color.gray") */
id: string;
/** id within group (e.g. "gray") */
localID: string;
/** group reference */
group?: GroupNode;
value: TokenValue<string>;
export declare type Token = ColorToken | ConicGradientToken | CubicBezierToken | DimensionToken | FileToken | FontToken | LinearGradientToken | RadialGradientToken | ShadowToken | URLToken;
export declare type TokenType = Token['type'];
export declare type TokenOrGroup = Group | Token;
export interface URLToken extends TokenBase<string> {
type: 'url';
value: string;
}
/** A URL reference */
export interface RawURLNode {
type: 'url';
/** (optional) User-friendly name of this token */
export interface ParsedMetadata {
name?: string;
/** (optional) Longer description of this token */
description?: string;
value: string | string[] | TokenValue<string>;
version?: string;
metadata?: Record<string, unknown>;
}
/** A URL reference */
export interface URLNode extends RawURLNode {
/** unique identifier (e.g. "color.gray") */
export interface ParseResult {
errors?: string[];
warnings?: string[];
result: {
metadata: ParsedMetadata;
tokens: ParsedToken[];
};
}
export declare type ParsedToken = {
id: string;
/** id within group (e.g. "gray") */
localID: string;
/** group reference */
group?: GroupNode;
value: TokenValue<string>;
} & Token;
export interface Schema {
name?: string;
version?: string;
metadata?: Record<string, unknown>;
tokens: Record<string, TokenOrGroup>;
}
export declare type RawSchemaNode<T = string> = RawGroupNode<T> | RawTokenNode<T> | RawFileNode | RawURLNode;
export declare type SchemaNode<T = string> = GroupNode<T> | TokenNode<T> | FileNode | URLNode;
export declare type TokenValue<T = string> = {
/** Required */
default: T;
/** Additional modes */
[mode: string]: T;
};
export declare function parse(source: string): RawTokenSchema;
export declare function parse(schema: Schema): ParseResult;

@@ -1,4 +0,377 @@

import yaml from 'js-yaml';
export function parse(source) {
return yaml.load(source);
import color from 'better-color-tools';
const VALID_TOP_LEVEL_KEYS = new Set(['name', 'version', 'metadata', 'tokens']);
const ALIAS_RE = /^\{.*\}$/;
export function parse(schema) {
const errors = [];
const warnings = [];
const result = { result: { metadata: {}, tokens: [] } };
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
errors.push(`Invalid schema type. Expected object, received "${Array.isArray(schema) ? 'Array' : typeof schema}"`);
result.errors = errors;
return result;
}
// 1. check top-level
for (const k of Object.keys(schema)) {
if (VALID_TOP_LEVEL_KEYS.has(k)) {
if (k !== 'tokens')
result.result.metadata[k] = schema[k];
}
else {
errors.push(`Invalid top-level name "${k}". Place arbitrary data inside "metadata".`);
}
}
if (errors.length) {
result.errors = errors;
return result;
}
if (!schema.tokens || typeof schema.tokens !== 'object' || !Object.keys(schema.tokens).length) {
errors.push('"tokens" is empty!');
result.errors = errors;
return result;
}
// 2. collect tokens
const tokens = {};
function walk(node, chain = [], group = { modes: [] }) {
for (const [k, v] of Object.entries(node)) {
if (!v || Array.isArray(v) || typeof v !== 'object') {
errors.push(`${k}: unexpected token format "${v}"`);
continue;
}
if (k.includes('.') || k.includes('#')) {
errors.push(`${k}: invalid name. Names can’t include "." or "#".`);
continue;
}
// token
const isToken = v.type || (group.type && !!v.value);
if (isToken) {
const id = chain.concat(k).join('.');
const nodeType = v.type || group.type;
if (!v.value) {
errors.push(`${id}: missing value`);
continue;
}
const token = v;
let mode = token.mode || {};
if (typeof mode !== 'object' || Array.isArray(mode)) {
errors.push(`${id}: "mode" must be an object`);
mode = {};
}
if (group.modes.length) {
for (const modeID of group.modes) {
if (!mode[modeID])
errors.push(`${id}: missing mode "${modeID}" set on parent group`);
}
}
for (const [modeID, modeValue] of Object.entries(mode)) {
if (!checkStrVal(modeValue))
errors.push(`${id}#${modeID}: bad value "${modeValue}"`);
}
tokens[id] = {
id,
...token,
type: nodeType,
mode: mode,
};
}
// group
else {
const { metadata, ...groupTokens } = v; // "metadata" only reserved word on group
const nextGroup = { ...group };
if (metadata) {
if (metadata.type)
nextGroup.type = metadata.type;
if (Array.isArray(metadata.modes))
nextGroup.modes = metadata.modes;
}
if (Object.keys(groupTokens).length) {
walk(groupTokens, [...chain, k], nextGroup);
}
}
}
}
walk(schema.tokens);
if (errors.length) {
result.errors = errors;
return result;
}
// 3. resolve aliases
const values = {};
// 3a. pass 1: gather all IDs & values
for (const token of Object.values(tokens)) {
values[token.id] = token.value;
for (const [k, v] of Object.entries(token.mode)) {
values[`${token.id}#${k}`] = v;
}
}
// 3b. pass 2: resolve simple aliases
aliasLoop: while (Object.values(values).some((t) => typeof t === 'string' && ALIAS_RE.test(t))) {
for (const [k, v] of Object.entries(values)) {
if (typeof v !== 'string' || !ALIAS_RE.test(v))
continue;
const id = v.substring(1, v.length - 1);
if (!values[id]) {
errors.push(`${k}: can’t find ${v}`);
break aliasLoop;
}
// check for circular references
const ref = values[id];
if (typeof ref === 'string' && ALIAS_RE.test(ref) && id === ref.substring(1, ref.length - 1)) {
errors.push(`${k}: can’t reference circular alias ${v}`);
break aliasLoop;
}
values[k] = values[id];
}
}
if (errors.length) {
result.errors = errors;
return result;
}
// 3c. pass 3: resolve embedded aliases from simple aliases
while (Object.values(values).some((t) => (typeof t === 'string' && findAliases(t).length) || (Array.isArray(t) && t.some((v) => typeof v === 'string' && findAliases(v).length)))) {
for (const [k, v] of Object.entries(values)) {
if (typeof v === 'string') {
let value = v;
for (const alias of findAliases(v)) {
const aliasedID = alias.substring(1, alias.length - 1);
values[k] = value.replace(alias, values[aliasedID]);
}
}
if (Array.isArray(v) && v.every((s) => typeof s === 'string')) {
values[k] = v.map((s) => {
let value = s;
for (const alias of findAliases(s)) {
const aliasedID = alias.substring(1, alias.length - 1);
value = value.replace(alias, values[aliasedID]);
}
return value;
});
}
}
}
// 4. validate values & replace aliases
for (const id of Object.keys(tokens)) {
const token = tokens[id];
try {
switch (token.type) {
// string tokens can all be validated together
case 'dimension':
case 'file':
case 'linear-gradient':
case 'radial-gradient':
case 'conic-gradient': {
const val = values[id];
// ✔ valid string
if (checkStrVal(val))
tokens[id].value = val;
// ✘ invalid string
else
errors.push(`${id}: bad value "${val}"`);
for (const modeID of Object.keys(token.mode)) {
const modeVal = values[`${id}#${modeID}`];
// ✔ valid mode
if (checkStrVal(modeVal))
tokens[id].mode[modeID] = modeVal;
// ✘ invalid mode
else
errors.push(`${id}: bad value "${modeVal}"`);
}
break;
}
case 'color': {
const val = values[id];
// ✔ valid color
if (checkColor(val))
tokens[id].value = val;
// ✘ invalid color
else
errors.push(`${id}: invalid color "${val}"`);
for (const modeID of Object.keys(token.mode)) {
const modeVal = values[`${id}#${modeID}`];
// ✔ valid mode
if (checkColor(modeVal))
tokens[id].mode[modeID] = modeVal;
// ✘ invalid mode
else
errors.push(`${id}: invalid color "${modeVal}"`);
}
break;
}
case 'font': {
const rawVal = values[id];
const val = Array.isArray(rawVal) ? rawVal : [rawVal];
// ✔ valid font
if (checkFont(val))
tokens[id].value = val;
// ✘ invalid font
else
errors.push(`${id}: expected string or array of strings, received ${typeof token.value}`);
for (const modeID of Object.keys(token.mode)) {
const rawModeVal = values[`${id}#${modeID}`];
const modeVal = Array.isArray(rawModeVal) ? rawModeVal : [rawModeVal];
// ✔ valid mode
if (checkFont(modeVal))
tokens[id].mode[modeID] = modeVal;
// ✘ invalid mode
else
errors.push(`${id}: expected string or array of strings, received ${typeof rawModeVal}`);
}
break;
}
case 'cubic-bezier': {
const val = values[id];
// ✔ valid cubic-bezier
if (checkCubicBezier(val)) {
val[0] = Math.max(0, Math.min(1, val[0]));
val[2] = Math.max(0, Math.min(1, val[2]));
tokens[id].value = val;
}
// ✘ invalid cubic-bezier
else
errors.push(`${id}: expected [x1, y1, x2, y2], received "${val}"`);
for (const modeID of Object.keys(token.mode)) {
const modeVal = values[`${id}#${modeID}`];
// ✔ valid mode
if (checkCubicBezier(modeVal)) {
modeVal[0] = Math.max(0, Math.min(1, modeVal[0]));
modeVal[2] = Math.max(0, Math.min(1, modeVal[2]));
tokens[id].mode[modeID] = modeVal;
}
// ✘ invalid mode
else
errors.push(`${id}: expected [x1, y1, x2, y2], received "${modeVal}"`);
}
break;
}
case 'url': {
const val = values[id];
// ✔ valid url
if (checkURL(val))
tokens[id].value = val;
// ✘ invalid url
else
errors.push(`${id}: invalid url "${val}" (if this is relative, use type: "file")`);
for (const modeID of Object.keys(token.mode)) {
const modeVal = values[`${id}#${modeID}`];
// ✔ valid mode
if (checkURL(modeVal))
tokens[id].mode[modeID] = modeVal;
// ✘ invalid mode
else
errors.push(`${id}: invalid url "${modeVal}" (if this is relative, use type: "file")`);
}
break;
}
case 'shadow': {
const val = values[id];
// ✔ valid shadow (validate before aliasing, as aliases may be inside array)
if (checkShadow(val))
tokens[id].value = val;
// ✘ invalid shadow
else
errors.push(`${id}: expected array, received "${token.value}"`);
for (const modeID of Object.keys(token.mode)) {
const modeVal = values[`${id}#${modeID}`];
// ✔ valid mode
if (checkShadow(modeVal))
tokens[id].mode[modeID] = modeVal;
// ✘ invalid mode
else
errors.push(`${id}: expected array, received "${modeVal}"`);
}
break;
}
// custom/other
default: {
tokens[id].value = values[id];
for (const modeID of Object.keys(token.mode)) {
tokens[id].mode[modeID] = values[`${id}#${modeID}`];
}
break;
}
}
}
catch (err) {
errors.push(`${id}: ${err.message || err}`);
}
}
// 4. return
if (errors.length)
result.errors = errors;
if (warnings.length)
result.warnings = warnings;
result.result.tokens = Object.values(tokens);
return result;
}
function checkColor(val) {
if (!val)
return false;
if (typeof val !== 'string')
return false;
try {
color.from(val);
return true;
}
catch {
return false;
}
}
function checkCubicBezier(val) {
if (!val)
return false;
return Array.isArray(val) && val.length === 4 && val.every((v) => typeof v === 'number' && !Number.isNaN(v));
}
function checkStrVal(val) {
return !!val && typeof val === 'string';
}
function checkFont(val) {
if (!val)
return false;
if (typeof val === 'string')
return true;
return Array.isArray(val) && val.every((v) => typeof v === 'string' && v.length);
}
function checkShadow(val) {
if (!val)
return false;
return Array.isArray(val) && val.every((v) => typeof v === 'string' && v.length);
}
function checkURL(val) {
if (!val || typeof val !== 'string')
return false;
try {
new URL(val);
return true;
}
catch {
return false;
}
}
/** given a string, find all {aliases} */
function findAliases(input) {
const matches = [];
if (!input.includes('{'))
return matches;
let lastI = -1;
for (let n = 0; n < input.length; n++) {
switch (input[n]) {
case '\\': {
// if '\{' or '\}' encountered, skip
if (input[n + 1] == '{' || input[n + 1] == '}')
n += 1;
break;
}
case '{': {
lastI = n; // '{' encountered; keep going until '}' (below)
break;
}
case '}': {
if (lastI === -1)
continue; // ignore '}' if no '{'
matches.push(input.substring(lastI, n + 1));
lastI = -1; // reset last index
break;
}
}
}
return matches;
}
{
"name": "@cobalt-ui/core",
"description": "Schemas and tools for managing design tokens",
"version": "0.0.2",
"version": "0.1.0",
"author": {

@@ -17,19 +17,23 @@ "name": "Drew Powers",

"type": "module",
"main": "./dist/index.js",
"main": "./dist/index.min.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"dependencies": {
"@cobalt-ui/plugin-json": "^0.0.0",
"@cobalt-ui/plugin-sass": "^0.0.0",
"@cobalt-ui/plugin-ts": "^0.0.0",
"js-yaml": "^4.1.0",
"kleur": "^4.1.4"
"@cobalt-ui/utils": "^0.1.0",
"better-color-tools": "^0.2.0",
"undici": "^4.12.1"
},
"devDependencies": {
"@types/js-yaml": "^4.0.4",
"@types/node": "^16.11.7"
"chai": "^4.3.4",
"mocha": "^9.1.3"
},
"scripts": {
"build": "rm -rf dist && tsc",
"dev": "tsc -w"
}
"build": "tsc && npm run bundle",
"dev": "run-p dev:*",
"dev:ts": "tsc --watch",
"dev:bundle": "npm run bundle -- --watch",
"bundle": "esbuild --format=esm --bundle --minify dist/index.js --outfile=dist/index.min.js --sourcemap",
"test": "mocha --parallel"
},
"readme": "# @cobalt-ui/core\n\nGenerate code from your design tokens, and sync your design tokens with Figma.\n\n## Install\n\n```\nnpm install @cobalt-ui/core\n```\n\n## Usage\n\n### Parse\n\nParse a `tokens.json` file into a JS object\n\n```js\nimport co from \"@cobalt-ui/core\";\nimport fs from \"fs\";\n\nconst schema = JSON.parse(co.parse(fs.readFileSync(\"./tokens.json\", \"utf8\")));\n```\n\n### Build\n\nGenerate code from `tokens.json` schema\n\n```js\nimport co from \"@cobalt-ui/core\";\nimport sass from \"@cobalt-ui/sass\";\nimport css from \"@cobalt-ui/css\";\nimport fs from \"fs\";\n\nconst schema = JSON.parse(co.parse(fs.readFileSync(\"./tokens.json\", \"utf8\")));\nconst files = co.build(schema, {\n plugins: [sass(), css()],\n});\n```\n\n### Sync\n\nSync `tokens.json` with Figma\n\n```js\nimport co from \"@cobalt-ui/core\";\nimport fs from \"fs\";\nimport deepmerge from 'deepmerge'\n\nconst schema = Jco.parse(JSON.parse(fs.readFileSync(\"./tokens.json\", \"utf8\")));\nconst updates = co.sync({\n 'https://figma.com/file/ABC123?node_id=123': {\n styles: {\n Black: {type: 'color', id: 'color.black'},\n },\n components: {\n 'Font / Body': {type: 'font', id: 'font.family.body'},\n },\n }\n});\n\nfs.writeFileSync('./tokens.json', deepmerge(schema, updates, {arrayMerge(a, b) => b}));\n```\n"
}

@@ -1,32 +0,62 @@

# 💎 Cobalt UI
# @cobalt-ui/core
Schemas and tools for managing design tokens
Generate code from your design tokens, and sync your design tokens with Figma.
## Getting Started
## Install
```
npm install @cobalt-ui/cli
npm install @cobalt-ui/core
```
Create a `tokens.yaml` file with your tokens (docs)
## Usage
Add to `package.json`:
### Parse
Parse a `tokens.json` file into a JS object
```js
import co from "@cobalt-ui/core";
import fs from "fs";
const schema = JSON.parse(co.parse(fs.readFileSync("./tokens.json", "utf8")));
```
"scripts": {
"tokens:build": "cobalt build",
"tokens:validate": "cobalt validate tokens.yaml"
}
```
### Building
### Build
Generate code from `tokens.json` schema
```js
import co from "@cobalt-ui/core";
import sass from "@cobalt-ui/sass";
import css from "@cobalt-ui/css";
import fs from "fs";
const schema = JSON.parse(co.parse(fs.readFileSync("./tokens.json", "utf8")));
const files = co.build(schema, {
plugins: [sass(), css()],
});
```
npm run tokens:build
```
### Validating
### Sync
Sync `tokens.json` with Figma
```js
import co from "@cobalt-ui/core";
import fs from "fs";
import deepmerge from 'deepmerge'
const schema = Jco.parse(JSON.parse(fs.readFileSync("./tokens.json", "utf8")));
const updates = co.sync({
'https://figma.com/file/ABC123?node_id=123': {
styles: {
Black: {type: 'color', id: 'color.black'},
},
components: {
'Font / Body': {type: 'font', id: 'font.family.body'},
},
}
});
fs.writeFileSync('./tokens.json', deepmerge(schema, updates, {arrayMerge(a, b) => b}));
```
npm run tokens:validate
```

@@ -1,4 +0,79 @@

export * from './build.js';
export * from './config.js';
export * from './parse.js';
export * from './validate.js';
import type { TokenType, ParseResult, Schema } from './parse.js';
export type {
ColorToken,
ConicGradientToken,
CubicBezierToken,
DimensionToken,
FileToken,
FontToken,
Group,
LinearGradientToken,
Mode,
ParsedMetadata,
ParsedToken,
ParseResult,
RadialGradientToken,
Schema,
ShadowToken,
Token,
TokenBase,
TokenOrGroup,
TokenType,
} from './parse.js';
import { parse } from './parse.js';
export { parse } from './parse.js';
export interface BuildResult {
/** File to output inside config.outDir (ex: ./tokens.sass) */
fileName: string;
/** File contents */
contents: string | Buffer;
}
export interface FigmaComponent {
component: string;
token: string;
type: TokenType;
file?: string;
}
export interface FigmaStyle {
style: string;
token: string;
type: TokenType;
file?: string;
}
export interface FigmaMapping {
[url: string]: (FigmaStyle | FigmaComponent)[];
}
export interface Config {
tokens: URL;
outDir: URL;
plugins: Plugin[];
figma?: FigmaMapping;
}
export interface Plugin {
name: string;
/** (optional) load config */
config?: (config: Config) => void;
/** main build fn */
build(options: { schema: ParseResult['result']; rawSchema: Schema }): Promise<BuildResult[]>;
}
export interface UserConfig {
/** path to tokens.json (default: "./tokens.json") */
tokens?: string;
/** output directory (default: "./tokens/") */
outDir?: string;
/** specify plugins (default: @cobalt-ui/plugin-json, @cobalt-ui/plugin-sass, @cobalt-ui/plugin-ts) */
plugins: Plugin[];
/** add figma keys */
figma?: FigmaMapping;
}
export default {
parse,
};

@@ -1,128 +0,458 @@

import yaml from 'js-yaml';
import color from 'better-color-tools';
export type NodeType = 'token' | 'group' | 'file' | 'url';
export interface ColorToken extends TokenBase<string> {
type: 'color';
}
export interface RawTokenSchema {
/** Manifest name */
name?: string;
/** Metadata. Useful for any arbitrary data */
metadata?: Record<string, any>;
/** Version. Only useful for the design system */
version?: number;
/** Tokens. Required */
tokens: {
[tokensOrGroup: string]: RawSchemaNode;
};
export interface ConicGradientToken extends TokenBase<string> {
type: 'conic-gradient';
}
export interface TokenSchema extends RawTokenSchema {
tokens: {
[tokensOrGroup: string]: SchemaNode;
};
export interface CubicBezierToken extends TokenBase<[number, number, number, number]> {
type: 'cubic-bezier';
}
/** An arbitrary grouping of tokens. Groups can be nested infinitely to form namespaces. */
export interface RawGroupNode<T = string> {
type: 'group';
/** (optional) User-friendly name of the group */
name?: string;
/** (optional) Longer descripton of this group */
description?: string;
/** (optional) Enforce that all child tokens have values for all modes */
modes?: string[];
tokens: {
[tokensOrGroup: string]: RawSchemaNode<T>;
};
export interface DimensionToken extends TokenBase<string> {
type: 'dimension';
}
/** An arbitrary grouping of tokens. Groups can be nested infinitely to form namespaces. */
export interface GroupNode<T = string> extends RawGroupNode<T> {
/** unique identifier (e.g. "color.gray") */
id: string;
/** id within group (e.g. "gray") */
localID: string;
/** group reference (yes, groups can be nested!) */
group?: GroupNode;
tokens: {
[tokensOrGroup: string]: SchemaNode<T>;
};
export interface FontToken extends TokenBase<string[]> {
type: 'font';
}
/** A design token. */
export interface RawTokenNode<T = string> {
type: 'token' | undefined;
/** (optional) User-friendly name of this token */
export interface FileToken extends TokenBase<string> {
type: 'file';
}
export type Group = {
metadata?: Record<string, unknown>;
} & {
[childNode: string]: TokenOrGroup;
};
export interface LinearGradientToken extends TokenBase<string> {
type: 'linear-gradient';
}
export type Mode<T = string> = Record<string, T>;
export interface RadialGradientToken extends TokenBase<string> {
type: 'radial-gradient';
}
export interface ShadowToken extends TokenBase<string[]> {
type: 'shadow';
}
export interface TokenBase<T = string> {
/** User-friendly name */
name?: string;
/** (optional) Longer description of this token */
/** Token description */
description?: string;
value: T | T[] | TokenValue<T>;
/** Token value */
value: T;
/** Mode variants */
mode: Mode<T>;
}
/** A design token. */
export interface TokenNode<T = string> extends RawTokenNode<T> {
/** unique identifier (e.g. "color.gray") */
id: string;
type: 'token';
/** id within group (e.g. "gray") */
localID: string;
/** group reference */
group?: GroupNode;
value: TokenValue<T>;
export type Token =
| ColorToken
| ConicGradientToken
| CubicBezierToken
| DimensionToken
| FileToken
| FontToken
| LinearGradientToken
| RadialGradientToken
| ShadowToken
| URLToken;
export type TokenType = Token['type'];
export type TokenOrGroup = Group | Token;
export interface URLToken extends TokenBase<string> {
type: 'url';
value: string;
}
/** A local file on disk. */
export interface RawFileNode {
type: 'file';
/** (optional) User-friendly name of this token */
export interface ParsedMetadata {
name?: string;
/** (optional) Longer description of this token */
description?: string;
value: string | string[] | TokenValue<string>;
version?: string;
metadata?: Record<string, unknown>;
}
/** A local file on disk. */
export interface FileNode extends RawFileNode {
/** unique identifier (e.g. "color.gray") */
id: string;
/** id within group (e.g. "gray") */
localID: string;
/** group reference */
group?: GroupNode;
value: TokenValue<string>;
export interface ParseResult {
errors?: string[];
warnings?: string[];
result: {
metadata: ParsedMetadata;
tokens: ParsedToken[];
};
}
/** A URL reference */
export interface RawURLNode {
type: 'url';
/** (optional) User-friendly name of this token */
export type ParsedToken = { id: string } & Token;
export interface Schema {
name?: string;
/** (optional) Longer description of this token */
description?: string;
value: string | string[] | TokenValue<string>;
version?: string;
metadata?: Record<string, unknown>;
tokens: Record<string, TokenOrGroup>;
}
/** A URL reference */
export interface URLNode extends RawURLNode {
/** unique identifier (e.g. "color.gray") */
id: string;
/** id within group (e.g. "gray") */
localID: string;
/** group reference */
group?: GroupNode;
value: TokenValue<string>;
const VALID_TOP_LEVEL_KEYS = new Set(['name', 'version', 'metadata', 'tokens']);
const ALIAS_RE = /^\{.*\}$/;
export function parse(schema: Schema): ParseResult {
const errors: string[] = [];
const warnings: string[] = [];
const result: ParseResult = { result: { metadata: {}, tokens: [] } };
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
errors.push(`Invalid schema type. Expected object, received "${Array.isArray(schema) ? 'Array' : typeof schema}"`);
result.errors = errors;
return result;
}
// 1. check top-level
for (const k of Object.keys(schema)) {
if (VALID_TOP_LEVEL_KEYS.has(k)) {
if (k !== 'tokens') (result.result.metadata as any)[k] = (schema as any)[k];
} else {
errors.push(`Invalid top-level name "${k}". Place arbitrary data inside "metadata".`);
}
}
if (errors.length) {
result.errors = errors;
return result;
}
if (!schema.tokens || typeof schema.tokens !== 'object' || !Object.keys(schema.tokens).length) {
errors.push('"tokens" is empty!');
result.errors = errors;
return result;
}
interface InheritedGroup {
type?: TokenType;
modes: string[];
}
// 2. collect tokens
const tokens: Record<string, ParsedToken> = {};
function walk(node: TokenOrGroup, chain: string[] = [], group: InheritedGroup = { modes: [] }): void {
for (const [k, v] of Object.entries(node)) {
if (!v || Array.isArray(v) || typeof v !== 'object') {
errors.push(`${k}: unexpected token format "${v}"`);
continue;
}
if (k.includes('.') || k.includes('#')) {
errors.push(`${k}: invalid name. Names can’t include "." or "#".`);
continue;
}
// token
const isToken = v.type || (group.type && !!v.value);
if (isToken) {
const id = chain.concat(k).join('.');
const nodeType: TokenType = v.type || group.type;
if (!v.value) {
errors.push(`${id}: missing value`);
continue;
}
const token = v as Token;
let mode = token.mode || {};
if (typeof mode !== 'object' || Array.isArray(mode)) {
errors.push(`${id}: "mode" must be an object`);
mode = {};
}
if (group.modes.length) {
for (const modeID of group.modes) {
if (!mode[modeID]) errors.push(`${id}: missing mode "${modeID}" set on parent group`);
}
}
for (const [modeID, modeValue] of Object.entries(mode)) {
if (!checkStrVal(modeValue)) errors.push(`${id}#${modeID}: bad value "${modeValue}"`);
}
tokens[id] = {
id,
...(token as any),
type: nodeType,
mode: mode as any,
};
}
// group
else {
const { metadata, ...groupTokens } = v; // "metadata" only reserved word on group
const nextGroup = { ...group };
if (metadata) {
if (metadata.type) nextGroup.type = metadata.type;
if (Array.isArray(metadata.modes)) nextGroup.modes = metadata.modes;
}
if (Object.keys(groupTokens).length) {
walk(groupTokens, [...chain, k], nextGroup);
}
}
}
}
walk(schema.tokens);
if (errors.length) {
result.errors = errors;
return result;
}
// 3. resolve aliases
const values: Record<string, unknown> = {};
// 3a. pass 1: gather all IDs & values
for (const token of Object.values(tokens)) {
values[token.id] = token.value;
for (const [k, v] of Object.entries(token.mode)) {
values[`${token.id}#${k}`] = v;
}
}
// 3b. pass 2: resolve simple aliases
aliasLoop: while (Object.values(values).some((t) => typeof t === 'string' && ALIAS_RE.test(t))) {
for (const [k, v] of Object.entries(values)) {
if (typeof v !== 'string' || !ALIAS_RE.test(v)) continue;
const id = v.substring(1, v.length - 1);
if (!values[id]) {
errors.push(`${k}: can’t find ${v}`);
break aliasLoop;
}
// check for circular references
const ref = values[id] as string;
if (typeof ref === 'string' && ALIAS_RE.test(ref) && id === ref.substring(1, ref.length - 1)) {
errors.push(`${k}: can’t reference circular alias ${v}`);
break aliasLoop;
}
values[k] = values[id];
}
}
if (errors.length) {
result.errors = errors;
return result;
}
// 3c. pass 3: resolve embedded aliases from simple aliases
while (
Object.values(values).some(
(t) => (typeof t === 'string' && findAliases(t).length) || (Array.isArray(t) && t.some((v) => typeof v === 'string' && findAliases(v).length))
)
) {
for (const [k, v] of Object.entries(values)) {
if (typeof v === 'string') {
let value = v;
for (const alias of findAliases(v)) {
const aliasedID = alias.substring(1, alias.length - 1);
values[k] = value.replace(alias, values[aliasedID] as any);
}
}
if (Array.isArray(v) && v.every((s) => typeof s === 'string')) {
values[k] = v.map((s) => {
let value = s;
for (const alias of findAliases(s)) {
const aliasedID = alias.substring(1, alias.length - 1);
value = value.replace(alias, values[aliasedID]);
}
return value;
});
}
}
}
// 4. validate values & replace aliases
for (const id of Object.keys(tokens)) {
const token = tokens[id];
try {
switch (token.type) {
// string tokens can all be validated together
case 'dimension':
case 'file':
case 'linear-gradient':
case 'radial-gradient':
case 'conic-gradient': {
const val = values[id] as string;
// ✔ valid string
if (checkStrVal(val)) tokens[id].value = val;
// ✘ invalid string
else errors.push(`${id}: bad value "${val}"`);
for (const modeID of Object.keys(token.mode)) {
const modeVal = values[`${id}#${modeID}`];
// ✔ valid mode
if (checkStrVal(modeVal)) tokens[id].mode[modeID] = modeVal as string;
// ✘ invalid mode
else errors.push(`${id}: bad value "${modeVal}"`);
}
break;
}
case 'color': {
const val = values[id] as ColorToken['value'];
// ✔ valid color
if (checkColor(val)) tokens[id].value = val;
// ✘ invalid color
else errors.push(`${id}: invalid color "${val}"`);
for (const modeID of Object.keys(token.mode)) {
const modeVal = values[`${id}#${modeID}`] as ColorToken['value'];
// ✔ valid mode
if (checkColor(modeVal)) tokens[id].mode[modeID] = modeVal;
// ✘ invalid mode
else errors.push(`${id}: invalid color "${modeVal}"`);
}
break;
}
case 'font': {
const rawVal = values[id];
const val = Array.isArray(rawVal) ? rawVal : [rawVal];
// ✔ valid font
if (checkFont(val)) tokens[id].value = val;
// ✘ invalid font
else errors.push(`${id}: expected string or array of strings, received ${typeof token.value}`);
for (const modeID of Object.keys(token.mode)) {
const rawModeVal = values[`${id}#${modeID}`];
const modeVal = Array.isArray(rawModeVal) ? rawModeVal : [rawModeVal];
// ✔ valid mode
if (checkFont(modeVal)) tokens[id].mode[modeID] = modeVal;
// ✘ invalid mode
else errors.push(`${id}: expected string or array of strings, received ${typeof rawModeVal}`);
}
break;
}
case 'cubic-bezier': {
const val = values[id] as CubicBezierToken['value'];
// ✔ valid cubic-bezier
if (checkCubicBezier(val)) {
val[0] = Math.max(0, Math.min(1, val[0]));
val[2] = Math.max(0, Math.min(1, val[2]));
tokens[id].value = val;
}
// ✘ invalid cubic-bezier
else errors.push(`${id}: expected [x1, y1, x2, y2], received "${val}"`);
for (const modeID of Object.keys(token.mode)) {
const modeVal = values[`${id}#${modeID}`] as CubicBezierToken['value'];
// ✔ valid mode
if (checkCubicBezier(modeVal)) {
modeVal[0] = Math.max(0, Math.min(1, modeVal[0]));
modeVal[2] = Math.max(0, Math.min(1, modeVal[2]));
tokens[id].mode[modeID] = modeVal;
}
// ✘ invalid mode
else errors.push(`${id}: expected [x1, y1, x2, y2], received "${modeVal}"`);
}
break;
}
case 'url': {
const val = values[id] as URLToken['value'];
// ✔ valid url
if (checkURL(val)) tokens[id].value = val;
// ✘ invalid url
else errors.push(`${id}: invalid url "${val}" (if this is relative, use type: "file")`);
for (const modeID of Object.keys(token.mode)) {
const modeVal = values[`${id}#${modeID}`] as URLToken['value'];
// ✔ valid mode
if (checkURL(modeVal)) tokens[id].mode[modeID] = modeVal;
// ✘ invalid mode
else errors.push(`${id}: invalid url "${modeVal}" (if this is relative, use type: "file")`);
}
break;
}
case 'shadow': {
const val = values[id] as ShadowToken['value'];
// ✔ valid shadow (validate before aliasing, as aliases may be inside array)
if (checkShadow(val)) tokens[id].value = val;
// ✘ invalid shadow
else errors.push(`${id}: expected array, received "${token.value}"`);
for (const modeID of Object.keys(token.mode)) {
const modeVal = values[`${id}#${modeID}`] as ShadowToken['value'];
// ✔ valid mode
if (checkShadow(modeVal)) tokens[id].mode[modeID] = modeVal;
// ✘ invalid mode
else errors.push(`${id}: expected array, received "${modeVal}"`);
}
break;
}
// custom/other
default: {
tokens[id].value = values[id] as any;
for (const modeID of Object.keys((token as any).mode)) {
(tokens[id] as any).mode[modeID] = values[`${id}#${modeID}`];
}
break;
}
}
} catch (err: any) {
errors.push(`${id}: ${err.message || err}`);
}
}
// 4. return
if (errors.length) result.errors = errors;
if (warnings.length) result.warnings = warnings;
result.result.tokens = Object.values(tokens);
return result;
}
export type RawSchemaNode<T = string> = RawGroupNode<T> | RawTokenNode<T> | RawFileNode | RawURLNode;
function checkColor(val: unknown): boolean {
if (!val) return false;
if (typeof val !== 'string') return false;
try {
color.from(val);
return true;
} catch {
return false;
}
}
export type SchemaNode<T = string> = GroupNode<T> | TokenNode<T> | FileNode | URLNode;
function checkCubicBezier(val: unknown): boolean {
if (!val) return false;
return Array.isArray(val) && val.length === 4 && val.every((v) => typeof v === 'number' && !Number.isNaN(v));
}
export type TokenValue<T = string> = {
/** Required */
default: T;
/** Additional modes */
[mode: string]: T;
};
function checkStrVal(val: unknown): boolean {
return !!val && typeof val === 'string';
}
export function parse(source: string): RawTokenSchema {
return yaml.load(source) as RawTokenSchema;
function checkFont(val: unknown): boolean {
if (!val) return false;
if (typeof val === 'string') return true;
return Array.isArray(val) && val.every((v) => typeof v === 'string' && v.length);
}
function checkShadow(val: unknown): boolean {
if (!val) return false;
return Array.isArray(val) && val.every((v) => typeof v === 'string' && v.length);
}
function checkURL(val: unknown): boolean {
if (!val || typeof val !== 'string') return false;
try {
new URL(val);
return true;
} catch {
return false;
}
}
/** given a string, find all {aliases} */
function findAliases(input: string): string[] {
const matches: string[] = [];
if (!input.includes('{')) return matches;
let lastI = -1;
for (let n = 0; n < input.length; n++) {
switch (input[n]) {
case '\\': {
// if '\{' or '\}' encountered, skip
if (input[n + 1] == '{' || input[n + 1] == '}') n += 1;
break;
}
case '{': {
lastI = n; // '{' encountered; keep going until '}' (below)
break;
}
case '}': {
if (lastI === -1) continue; // ignore '}' if no '{'
matches.push(input.substring(lastI, n + 1));
lastI = -1; // reset last index
break;
}
}
}
return matches;
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc