@agent-scope/tokens
v1.20.0
Published
Design token file parser, validator, and resolution engine for Scope
Readme
@agent-scope/tokens
Design token file parser, validator, and resolution engine for Scope.
Parses reactscope.tokens.json / .yaml files into a flat, fully-resolved Token[], and provides lookup, nearest-match search, compliance auditing, impact analysis, theme overlays, and multi-format export.
Installation
npm install @agent-scope/tokensWhat it does / when to use it
| Need | Use |
|------|-----|
| Parse a token file and resolve all {path.to.token} references | parseTokenFile / parseTokenFileSync |
| Look up a token by path | TokenResolver.resolve |
| Find the token that matches a CSS value | TokenResolver.match / TokenResolver.nearest |
| Audit component styles against the token set | ComplianceEngine |
| Predict which components break when a token changes | ImpactAnalyzer |
| Export tokens to CSS / TypeScript / SCSS / Tailwind / Figma | exportTokens |
| Resolve values across dark/brand themes | ThemeResolver |
| Validate a raw token file object | validateTokenFile |
Token file format
Token files are JSON or YAML. The top-level shape is:
{
"$schema": "https://reactscope.dev/token-schema.json", // optional
"version": "0.1", // required
"meta": { // optional
"name": "My Design Tokens",
"lastUpdated": "2024-01-01",
"updatedBy": "[email protected]"
},
"tokens": { ... }, // required
"themes": { ... } // optional
}Token tree
The tokens object is a nested tree. Every leaf node must have value and type:
{
"version": "0.1",
"tokens": {
"color": {
"primary": {
"500": { "value": "#3B82F6", "type": "color" },
"600": { "value": "#2563EB", "type": "color" }
},
"neutral": {
"0": { "value": "#FFFFFF", "type": "color" },
"900": { "value": "#111827", "type": "color" }
},
"alias": {
"brand": { "value": "{color.primary.500}", "type": "color" }
}
},
"spacing": {
"4": { "value": "16px", "type": "dimension" },
"8": { "value": "32px", "type": "dimension" }
},
"typography": {
"fontFamily": {
"sans": { "value": "Inter, sans-serif", "type": "fontFamily", "description": "Primary font" }
},
"fontWeight": {
"bold": { "value": 700, "type": "fontWeight" },
"normal": { "value": 400, "type": "fontWeight" }
}
},
"radius": { "md": { "value": "6px", "type": "dimension" } },
"shadow": { "md": { "value": "0 4px 6px rgba(0,0,0,0.1)", "type": "shadow" } },
"motion": {
"duration": { "fast": { "value": "150ms", "type": "duration" } },
"easing": { "standard": { "value": "cubic-bezier(0.4, 0, 0.2, 1)", "type": "cubicBezier" } }
}
}
}Leaf node fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| value | string \| number | Yes | Raw value. May be a {path.to.token} reference. |
| type | TokenType | Yes | One of the 8 supported types (see below). |
| description | string | No | Human-readable annotation. Passed through to exports. |
Token types
| Type | Example value |
|------|---------------|
| color | "#3B82F6" |
| dimension | "16px", "1.5rem" |
| fontFamily | "Inter, sans-serif" |
| fontWeight | 700 |
| number | 1.5 |
| shadow | "0 4px 6px rgba(0,0,0,0.1)" |
| duration | "150ms" |
| cubicBezier | "cubic-bezier(0.4, 0, 0.2, 1)" |
Reference syntax — {path.to.token}
A token's value can point to another token using curly-brace dot-notation:
"color": {
"primary": { "500": { "value": "#3B82F6", "type": "color" } },
"alias": {
"brand": { "value": "{color.primary.500}", "type": "color" }
}
}- The parser resolves
{color.primary.500}→"#3B82F6"and stores it inresolvedValue. - References can chain:
A → B → C(all fully resolved). - Circular references (e.g.
A → B → A) throwTokenParseErrorwith code"CIRCULAR_REFERENCE". - References to non-existent paths throw
TokenParseErrorwith code"INVALID_REFERENCE". resolvedValueis always a string, even for numeric tokens (fontWeight: 700→"700").
API reference
parseTokenFile(input, format?) — async
async function parseTokenFile(
input: string,
format?: "json" | "yaml",
): Promise<ParsedTokens>Parses a token file (JSON or YAML) and returns a flat resolved token array.
- If
formatis omitted, JSON is tried first, then YAML as a fallback. - Throws
TokenParseErroron reference or circular reference errors. - Throws
TokenValidationErroron schema violations.
import { parseTokenFile } from "@agent-scope/tokens";
import { readFileSync } from "node:fs";
const source = readFileSync("tokens.json", "utf8");
const { tokens, rawFile } = await parseTokenFile(source);
// tokens: Token[] — flat, resolved
// rawFile: TokenFile — the validated raw file objectparseTokenFileSync(input) — sync, JSON only
function parseTokenFileSync(input: string): ParsedTokensSynchronous version. Only supports JSON (YAML requires an async import).
import { parseTokenFileSync } from "@agent-scope/tokens";
const { tokens } = parseTokenFileSync(source);
// tokens[0] → { path: "color.primary.500", value: "#3B82F6", resolvedValue: "#3B82F6", type: "color" }ParsedTokens
interface ParsedTokens {
tokens: Token[];
rawFile: TokenFile;
}Token
interface Token {
path: string; // dot-notation, e.g. "color.primary.500"
value: string | number; // raw value from the file (may be a reference)
resolvedValue: string; // fully resolved, always a string
type: TokenType;
description?: string;
}TokenResolver
Wraps a Token[] for fast path-based lookup and nearest-match search.
import { TokenResolver } from "@agent-scope/tokens";
const resolver = new TokenResolver(tokens);resolve(path)
resolve(path: string): stringReturns the resolved value for a known token path. Throws TokenParseError ("INVALID_REFERENCE") if not found.
resolver.resolve("color.primary.500"); // "#3B82F6"
resolver.resolve("spacing.4"); // "16px"
resolver.resolve("typography.fontWeight.bold"); // "700"
resolver.resolve("color.alias.brand"); // "#3B82F6" (alias resolved)match(value, type)
match(value: string, type: TokenType): TokenMatch | nullReturns an exact-match TokenMatch if any token of the given type has a resolvedValue equal to value (case-insensitive for colors). Returns null if no exact match exists.
resolver.match("#3B82F6", "color");
// { token: { path: "color.primary.500", ... }, exact: true, distance: 0 }
resolver.match("#3b82f6", "color"); // case-insensitive — same result
resolver.match("16px", "dimension");
// { token: { path: "spacing.4", ... }, exact: true, distance: 0 }
resolver.match("#AABBCC", "color"); // null — no matchnearest(value, type)
nearest(value: string, type: TokenType): TokenMatchReturns the TokenMatch with the smallest computed distance to value among all tokens of the given type. Always returns a result (never null); throws if no tokens of the specified type exist.
Distance computation per type:
color— Euclidean distance in CIE Lab space (perceptual)dimension/duration—|parsed numeric difference|fontWeight/number—|numeric difference|shadow/fontFamily/cubicBezier— string equality (0 or 1)
// #3A82F5 is perceptually very close to #3B82F6
resolver.nearest("#3A82F5", "color");
// { token: { path: "color.primary.500", ... }, exact: false, distance: 0.42 }
// 15px — closest to 16px (spacing.4)
resolver.nearest("15px", "dimension");
// { token: { path: "spacing.4", resolvedValue: "16px", ... }, exact: false, distance: 1 }list(type?, category?)
list(type?: TokenType, category?: string): Token[]Returns all tokens, optionally filtered by type and/or category (the first path segment, e.g. "color" in "color.primary.500").
resolver.list(); // all tokens
resolver.list("color"); // only color tokens
resolver.list(undefined, "spacing"); // all tokens in the "spacing" category
resolver.list("dimension", "spacing"); // dimension tokens in "spacing"TokenMatch
interface TokenMatch {
token: Token;
exact: boolean; // true when resolvedValue === queried value
distance: number; // 0 for exact; computed distance otherwise
}ComplianceEngine
Audits rendered component CSS styles against the resolved token set. Reports per-property compliance status and an aggregate compliance percentage.
import { ComplianceEngine } from "@agent-scope/tokens";
const engine = new ComplianceEngine(resolver);
// With custom tolerances:
const engine = new ComplianceEngine(resolver, {
colorTolerance: 3, // default: max CIE Lab distance for on-system
dimensionTolerance: 2, // default: max px difference for on-system
fontWeightTolerance: 0, // default: exact match only
});audit(styles)
audit(styles: ComputedStyles): ComplianceReportAudits a single component's computed styles.
const report = engine.audit({
colors: { background: "#3B82F6", color: "#ffffff" },
spacing: { paddingTop: "16px", gap: "8px" },
typography: { fontFamily: "Inter, sans-serif", fontSize: "14px", fontWeight: "700" },
borders: { borderRadius: "4px" },
shadows: { boxShadow: "0 1px 3px rgba(0,0,0,0.1)" },
});
report.compliance; // 0.83 (fraction of on-system properties)
report.total; // 8 (properties audited, excluding skipped values)
report.onSystem; // 7
report.offSystem; // 1ComputedStyles
type ComputedStyles = {
colors: Record<string, string>; // e.g. { background: "#3B82F6" }
spacing: Record<string, string>; // e.g. { paddingTop: "16px" }
typography: Record<string, string>; // fontFamily, fontSize, fontWeight, lineHeight
borders: Record<string, string>; // borderRadius, borderWidth
shadows: Record<string, string>; // boxShadow
};Skipped values (not counted toward totals): "none", "inherit", "initial", "unset", "auto", "transparent", "currentColor", "", "normal".
ComplianceReport
interface ComplianceReport {
properties: Record<string, PropertyResult>; // per-property result
total: number; // properties audited
onSystem: number;
offSystem: number;
compliance: number; // onSystem / total (1 when total === 0)
auditedAt: string; // ISO timestamp
}PropertyResult — on_system example
// report.properties["background"] when background: "#3B82F6" (exact token match)
{
property: "background",
value: "#3B82F6",
status: "on_system",
token: "color.primary.500",
nearest: { token: "color.primary.500", value: "#3B82F6", distance: 0 }
}PropertyResult — OFF_SYSTEM example
// report.properties["background"] when background: "#FF0000" (no match)
{
property: "background",
value: "#FF0000",
status: "OFF_SYSTEM",
// token is absent (undefined) for OFF_SYSTEM results
nearest: { token: "color.neutral.900", value: "#111827", distance: 74.3 }
}auditBatch(components)
auditBatch(components: Map<string, ComputedStyles>): BatchReportAudits multiple components at once:
const batch = engine.auditBatch(new Map([
["Button", { colors: { background: "#3B82F6" }, spacing: {}, typography: {}, borders: {}, shadows: {} }],
["Input", { colors: { background: "#FF0000" }, spacing: {}, typography: {}, borders: {}, shadows: {} }],
]));
batch.aggregateCompliance; // 0.5
batch.components.Button.compliance; // 1
batch.components.Input.compliance; // 0ComplianceEngine.toJSON(report)
static toJSON(report: ComplianceReport | BatchReport): stringSerializes a report to indented JSON (2-space).
ImpactAnalyzer
Analyses the downstream effects of a design token change on audited components.
import { ImpactAnalyzer } from "@agent-scope/tokens";
const analyzer = new ImpactAnalyzer(resolver, componentReports);
// componentReports: Map<string, ComplianceReport> — from ComplianceEngine.auditBatchimpactOf(tokenPath, newValue)
impactOf(tokenPath: string, newValue: string): ImpactReportReturns an ImpactReport describing which components and properties would be affected by changing the specified token to newValue.
const report = analyzer.impactOf("color.primary.500", "#1D4ED8");
report.tokenPath; // "color.primary.500"
report.oldValue; // "#3B82F6"
report.newValue; // "#1D4ED8"
report.tokenType; // "color"
report.colorDelta; // CIE Lab distance (only for color tokens)
report.affectedComponentCount; // e.g. 2
report.overallSeverity; // "subtle" | "moderate" | "significant" | "none"
report.components; // AffectedComponent[]ImpactReport
interface ImpactReport {
tokenPath: string;
oldValue: string;
newValue: string;
tokenType: TokenType;
affectedComponentCount: number;
components: AffectedComponent[];
overallSeverity: VisualSeverity; // max severity across all components
colorDelta?: number; // CIE Lab distance (color tokens only)
}
interface AffectedComponent {
name: string;
affectedProperties: string[]; // e.g. ["background", "borderColor"]
severity: VisualSeverity;
}
type VisualSeverity = "none" | "subtle" | "moderate" | "significant";Severity thresholds for color tokens (CIE Lab distance):
"none"— distance 0 (no change)"subtle"— distance < 5"moderate"— distance < 20"significant"— distance ≥ 20
For dimension tokens: ≤2px → subtle, ≤8px → moderate, >8px → significant.
exportTokens(tokens, format, options?)
function exportTokens(
tokens: Token[],
format: ExportFormat,
options?: ExportOptions,
): stringExports a resolved token set to the specified format.
type ExportFormat = "css" | "ts" | "scss" | "tailwind" | "flat-json" | "figma";
interface ExportOptions {
themes?: Map<string, Map<string, string>>; // theme name → (tokenPath → overrideValue)
prefix?: string; // CSS/SCSS: prefix for custom property / variable names
rootSelector?: string; // CSS: override ":root" selector
}CSS export
exportTokens(tokens, "css");
// :root {
// --color-primary-500: #3B82F6;
// --color-primary-600: #2563EB;
// --spacing-4: 16px;
// ...
// }
exportTokens(tokens, "css", { prefix: "scope" });
// :root { --scope-color-primary-500: #3B82F6; ... }
exportTokens(tokens, "css", { rootSelector: "html" });
// html { --color-primary-500: #3B82F6; ... }
// With themes:
exportTokens(tokens, "css", { themes: themeMap });
// :root { --color-primary-500: #3B82F6; ... }
// [data-theme="dark"] { --color-primary-500: #60A5FA; ... }
// [data-theme="brand-b"] { --color-primary-500: #8B5CF6; ... }TypeScript export
exportTokens(tokens, "ts");
// // Auto-generated design tokens — do not edit manually
//
// export const colorPrimary500 = "#3B82F6" as const;
// export const colorPrimary600 = "#2563EB" as const;
// export const spacing4 = "16px" as const;
// ...
// With themes:
// export const themes = {
// "dark": { colorPrimary500: "#60A5FA" as const, ... },
// "brand-b": { colorPrimary500: "#8B5CF6" as const, ... },
// } as const;SCSS export
exportTokens(tokens, "scss");
// // Auto-generated design tokens — do not edit manually
//
// $color-primary-500: #3B82F6;
// $spacing-4: 16px;
// ...
exportTokens(tokens, "scss", { prefix: "tok" });
// $tok-color-primary-500: #3B82F6;
// With themes — emits [data-theme] blocks using CSS custom properties:
// [data-theme="dark"] { --color-primary-500: #60A5FA; }Tailwind export
exportTokens(tokens, "tailwind");
// // Auto-generated design tokens — do not edit manually
// module.exports = {
// "theme": {
// "extend": {
// "color": { "primary": { "500": "#3B82F6", "600": "#2563EB" } },
// "spacing": { "4": "16px", "8": "32px" }
// }
// }
// };flat-json export
exportTokens(tokens, "flat-json");
// {
// "color.primary.500": "#3B82F6",
// "color.primary.600": "#2563EB",
// "spacing.4": "16px"
// }Figma export
exportTokens(tokens, "figma");
// {
// "global": {
// "color": {
// "primary": {
// "500": { "value": "#3B82F6", "type": "color" }
// }
// },
// "typography": {
// "fontFamily": {
// "sans": { "value": "Inter, sans-serif", "type": "fontFamily", "description": "Primary font" }
// }
// }
// },
// "dark": { "color": { "primary": { "500": { "value": "#60A5FA", "type": "color" } } } }
// }ThemeResolver
Extends TokenResolver with named theme overlays.
Token file format — themes
Two supported theme formats:
Flat override map (original format):
{
"version": "0.1",
"tokens": { ... },
"themes": {
"dark": {
"color.primary.500": "#60A5FA",
"color.neutral.0": "#0F172A"
},
"brand-b": {
"color.primary.500": "#8B5CF6"
}
}
}Nested DTCG-style (structured format):
{
"version": "0.1",
"tokens": { ... },
"themes": {
"dark": {
"color": {
"primary": { "500": { "$value": "#60A5FA" } },
"neutral": { "0": { "$value": "#0F172A" } }
}
}
}
}ThemeResolver.fromTokenFile(baseResolver, rawFile)
static fromTokenFile(baseResolver: TokenResolver, rawFile: ThemedTokenFile): ThemeResolverConstructs a ThemeResolver from a TokenResolver and a raw token file (supports both flat and nested formats).
import { ThemeResolver } from "@agent-scope/tokens";
const { tokens, rawFile } = parseTokenFileSync(source);
const resolver = new TokenResolver(tokens);
const themeResolver = ThemeResolver.fromTokenFile(resolver, rawFile);
themeResolver.listThemes(); // ["dark", "brand-b"]
themeResolver.resolveThemed("color.primary.500", "dark"); // "#60A5FA"
themeResolver.resolveThemed("spacing.4", "dark"); // "16px" (falls back to base)
themeResolver.resolveAllThemes("color.primary.500");
// { base: "#3B82F6", dark: "#60A5FA", "brand-b": "#8B5CF6" }ThemeResolver.fromThemeMap(baseResolver, themes)
static fromThemeMap(
baseResolver: TokenResolver,
themes: Map<string, Map<string, string>>,
): ThemeResolverProgrammatic construction from a pre-built theme map.
resolveThemed(path, themeName)
Returns the value for the path in the given theme, falling back to base if the theme doesn't override it. Throws if the theme name is not registered.
resolveAllThemes(path)
Returns { base: string, [themeName]: string, ... } — the resolved value in every theme.
buildThemedTokens(themeName)
Returns a full Token[] with the theme overrides applied (base values for non-overridden tokens).
Delegated methods
ThemeResolver also exposes resolve(path) and list(type?, category?) which delegate to the underlying TokenResolver.
validateTokenFile(raw)
function validateTokenFile(raw: unknown): asserts raw is TokenFileValidates a raw parsed object against the TokenFile schema. Throws TokenValidationError with all collected issues if validation fails (collects all errors before throwing, not just the first).
Validation rules:
- Root value must be a non-null object
versionmust be a string fieldtokensmust be an object field- Every leaf node inside
tokensmust havevalue(string or number) andtype(one of the 8 valid types) meta, if present, must be an objectthemes, if present, must be aRecord<string, Record<string, string>>
import { validateTokenFile, TokenValidationError } from "@agent-scope/tokens";
try {
validateTokenFile(raw);
// raw is now asserted as TokenFile
} catch (err) {
if (err instanceof TokenValidationError) {
for (const error of err.errors) {
console.error(`${error.path}: ${error.message} [${error.code}]`);
}
}
}Error types
class TokenParseError extends Error {
readonly code: "CIRCULAR_REFERENCE" | "INVALID_REFERENCE" | "INVALID_SCHEMA" | "PARSE_ERROR";
readonly path?: string;
}
class TokenValidationError extends Error {
readonly errors: ValidationError[];
}
interface ValidationError {
path: string;
message: string;
code: string;
}Complete example
import {
parseTokenFileSync,
TokenResolver,
ComplianceEngine,
ImpactAnalyzer,
exportTokens,
ThemeResolver,
} from "@agent-scope/tokens";
import { readFileSync } from "node:fs";
// 1. Parse token file
const source = readFileSync("reactscope.tokens.json", "utf8");
const { tokens, rawFile } = parseTokenFileSync(source);
// 2. Build resolver
const resolver = new TokenResolver(tokens);
resolver.resolve("color.primary.500"); // "#3B82F6"
resolver.match("#3B82F6", "color"); // exact match → TokenMatch
resolver.nearest("#3A80F0", "color"); // perceptually closest color
// 3. Export tokens
const css = exportTokens(tokens, "css");
const ts = exportTokens(tokens, "ts");
const scss = exportTokens(tokens, "scss");
// 4. Compliance audit
const engine = new ComplianceEngine(resolver);
const report = engine.audit({
colors: { background: "#3B82F6" },
spacing: { paddingTop: "16px" },
typography: { fontFamily: "Inter, sans-serif" },
borders: { borderRadius: "6px" },
shadows: { boxShadow: "0 4px 6px rgba(0,0,0,0.1)" },
});
console.log(report.compliance); // e.g. 1
// 5. Batch audit + impact analysis
const batchReport = engine.auditBatch(new Map([
["Button", { colors: { background: "#3B82F6", color: "#FFFFFF" }, spacing: {}, typography: {}, borders: {}, shadows: {} }],
["Card", { colors: { background: "#FFFFFF" }, spacing: { gap: "32px" }, typography: {}, borders: { borderRadius: "6px" }, shadows: {} }],
]));
const analyzer = new ImpactAnalyzer(resolver, new Map(Object.entries(batchReport.components)));
const impact = analyzer.impactOf("color.primary.500", "#1D4ED8");
console.log(impact.affectedComponentCount); // 1
console.log(impact.overallSeverity); // "moderate"
// 6. Theme resolution
const themeResolver = ThemeResolver.fromTokenFile(resolver, rawFile);
themeResolver.resolveThemed("color.primary.500", "dark"); // "#60A5FA"
themeResolver.resolveAllThemes("color.primary.500"); // { base, dark, "brand-b" }Internal architecture
| Module | Responsibility |
|--------|----------------|
| types.ts | All TypeScript types and error classes |
| validator.ts | Schema validation — collects all errors before throwing |
| parser.ts | JSON/YAML parsing → flattenTokens → resolveValue (DFS with cycle detection) |
| resolver.ts | TokenResolver — path lookup, match, nearest, list |
| compliance.ts | ComplianceEngine — style auditing against the token set |
| impact.ts | ImpactAnalyzer — downstream change analysis |
| export.ts | exportTokens — CSS, TS, SCSS, Tailwind, flat-JSON, Figma |
| themes.ts | ThemeResolver — flat and DTCG-style theme overlay resolution |
| color-utils.ts | hexToLab, labDistance, parseColorToLab — perceptual color math |
Used by
@agent-scope/cli— token compliance commands (scope tokens compliance,scope tokens export,scope tokens impact,scope tokens preview)@agent-scope/site— type imports for the Scope web UI
