@cobalt-ui/core
Advanced tools
Comparing version 0.0.2 to 0.1.0
# @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, | ||
}; |
526
src/parse.ts
@@ -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; | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
92907
3
63
0
12
1058
1
+ Added@cobalt-ui/utils@^0.1.0
+ Addedbetter-color-tools@^0.2.0
+ Addedundici@^4.12.1
+ Added@cobalt-ui/utils@0.1.0(transitive)
+ Addedbetter-color-tools@0.2.0(transitive)
+ Addedundici@4.16.0(transitive)
- Removed@cobalt-ui/plugin-json@^0.0.0
- Removed@cobalt-ui/plugin-sass@^0.0.0
- Removed@cobalt-ui/plugin-ts@^0.0.0
- Removedjs-yaml@^4.1.0
- Removedkleur@^4.1.4
- Removed@cobalt-ui/plugin-json@0.0.0(transitive)
- Removed@cobalt-ui/plugin-sass@0.0.0(transitive)
- Removed@cobalt-ui/plugin-ts@0.0.0(transitive)
- Removed@cobalt-ui/utils@0.0.0(transitive)
- Removedargparse@2.0.1(transitive)
- Removedjs-yaml@4.1.0(transitive)
- Removedkleur@4.1.5(transitive)
- Removedmime@3.0.0(transitive)