Design Token 的跨端契约测试:基于 JSON Schema 的全平台类型合规检验

封面信息图

在大型企业级跨端基础设施建设中,Design Token 仓库(tokens.json)是全公司设计与研发之间最底层的“视觉契约(Visual Contract)”。

一份简单的 JSON 文件,通过自动化流水线会被编译分发到 Web(CSS/SCSS)、iOS(Swift/SwiftUI)、Android(Kotlin/Jetpack Compose)以及 Flutter(Dart) 四大终端平台。

然而,在多团队协作与持续集成的现实中,设计令牌的源文件往往极其脆弱:

  • 某位设计师在 Figma 里手滑导出了一个格式错误的 Hex 色号(如 #4F46E 只有 5 位数,或者误将 RGB 写成了包含透明度的非标格式);
  • 某个层级的别名引用拼写错误(如 {color.brand.indigo.500} 写成了 {color.brand.indgo.500}),导致下游 iOS 编译时直接抛出死锁异常并阻断主工程发版;
  • 某个原本是纯数字的间距 Token(如 16),被无意修改为了带单位的字符串 "16rem",导致 Android Compose 无法将其解析为 Dp 强类型常量而全面报错!

传统的测试手段(如跑单元测试或等待 App 编译失败)耗时极长且反馈滞后。

基于 JSON Schema 与 DTCG 规范的“跨端静态契约测试(Cross-Platform Contract Testing)”,是在 Token 代码合并入主干前,毫秒级拦截所有非法类型与拓扑漏洞的最强质量护城河。

跨端契约测试的核心四维校验矩阵

一份符合全平台工业级要求的 Design Token,必须在自动化门禁中通过以下四个维度的严格契约校验:

[源头 Token 提交: tokens/**/*.json]
                 │
                 ▼ (运行静态契约测试流水线: test:tokens)
┌────────────────┴────────────────┐
├── 1. 结构与模式合规 (JSON Schema Validation)
│      - 必须包含 $value, $type, $description 规范字段
│      - 拦截非标字段与拼写错误
│
├── 2. 色彩与数值物理合法性 (Physical Validity)
│      - Hex 必须严格满足 3/6/8 位标准正规表达式
│      - 间距与尺寸必须为正数,杜绝非法负值与非标单位
│
├── 3. 别名引用拓扑闭包 (Topological Closure)
│      - 所有 {xxx.yyy} 别名必须能精准寻址到已存在的合法实体
│      - 深度遍历检测是否存在 A -> B -> A 的循环死锁引用!
│
└── 4. 跨端保留字与命名规范 (Identifier Safety)
       - 拦截 C++ / Swift / Kotlin 中的语言保留字 (如 class, default, switch)
       - 强制遵循 kebab-case 命名规范
                 │
                 ▼
[✅ 100% 校验通过 ──> 放心分发全平台编译!]

生产级 JSON Schema 契约定义规范

我们编写一套基于 Draft 2020-12 的核心 Token Schema 约束文件(token-contract.schema.json):

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "DesignSystemTokenContract",
  "type": "object",
  "patternProperties": {
    "^[a-z0-9]+(-[a-z0-9]+)*$": {
      "type": "object",
      "properties": {
        "$value": {
          "type": ["string", "number", "object"]
        },
        "$type": {
          "type": "string",
          "enum": ["color", "dimension", "fontFamily", "fontWeight", "duration", "cubicBezier", "shadow"]
        },
        "$description": {
          "type": "string",
          "minLength": 5
        }
      },
      "required": ["$value", "$type"],
      "additionalProperties": true
    }
  }
}

编写端到端契约校验引擎(Ajv + 拓扑环路检测)

// scripts/token-contract-tester.ts
import * as fs from 'fs';
import * as path from 'path';
import Ajv from 'ajv/dist/2020';
import { globSync } from 'glob';

const ajv = new Ajv({ allErrors: true, allowUnionTypes: true });

export interface ContractTestResult {
  passed: boolean;
  errors: string[];
}

export class TokenContractTester {
  private schemaValidator: any;
  private tokenRegistry: Map<string, any> = new Map();

  constructor(schemaPath: string) {
    const rawSchema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
    this.schemaValidator = ajv.compile(rawSchema);
  }

  // 1. 全量静态加载并打平 Token 路径
  public loadTokens(globPattern: string) {
    const files = globSync(globPattern);
    for (const file of files) {
      const content = JSON.parse(fs.readFileSync(file, 'utf8'));
      this.flattenNode(content, '');
    }
  }

  private flattenNode(node: any, prefix: string) {
    for (const [key, val] of Object.entries(node)) {
      const currentPath = prefix ? `${prefix}.${key}` : key;
      if (val && typeof val === 'object' && '$value' in val) {
        this.tokenRegistry.set(currentPath, val);
      } else if (val && typeof val === 'object' && !Array.isArray(val)) {
        this.flattenNode(val, currentPath);
      }
    }
  }

  // 2. 核心契约检验
  public runAudit(): ContractTestResult {
    const errors: string[] = [];

    // A. 校验 Schema 基础结构
    for (const [tokenPath, tokenObj] of this.tokenRegistry.entries()) {
      const valid = this.schemaValidator(tokenObj);
      if (!valid) {
        this.schemaValidator.errors?.forEach((err: any) => {
          errors.push(`[Schema 违规] Token \`${tokenPath}\` ${err.instancePath} ${err.message}`);
        });
      }

      // B. 针对 Color 类型的严格正则与物理合法性
      if (tokenObj.$type === 'color' && typeof tokenObj.$value === 'string' && !tokenObj.$value.startsWith('{')) {
        const hexRegex = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
        if (!hexRegex.test(tokenObj.$value)) {
          errors.push(`[非法色值] Token \`${tokenPath}\` 包含了不合法的 Hex 格式: "${tokenObj.$value}"`);
        }
      }
    }

    // C. 别名拓扑闭包与死循环引用检测 (DFS 环路检测)
    for (const tokenPath of this.tokenRegistry.keys()) {
      const cycle = this.detectCircularDependency(tokenPath, new Set());
      if (cycle) {
        errors.push(`[别名死锁] 捕获循环依赖引用链: ${cycle.join(' -> ')}`);
      }
    }

    return { passed: errors.length === 0, errors };
  }

  private detectCircularDependency(currentPath: string, visited: Set<string>): string[] | null {
    if (visited.has(currentPath)) {
      return [...Array.from(visited), currentPath];
    }

    const tokenObj = this.tokenRegistry.get(currentPath);
    if (!tokenObj || typeof tokenObj.$value !== 'string') return null;

    const aliasMatch = tokenObj.$value.match(/\{([^}]+)\}/);
    if (!aliasMatch) return null;

    const targetAlias = aliasMatch[1];
    if (!this.tokenRegistry.has(targetAlias)) {
      // 别名悬空
      return null;
    }

    visited.add(currentPath);
    const result = this.detectCircularDependency(targetAlias, new Set(visited));
    visited.delete(currentPath);

    return result;
  }
}

CI 门禁集成与阻止非法合入

在 GitHub Actions 或 GitLab CI 中,将契约测试作为绝对不可绕过的必过项(Blocking Check):

# .github/workflows/token-contract-ci.yml
name: Design Token Contract Gate
on: [pull_request]

jobs:
  validate-contract:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 18
      - name: Run Token Contract Tests
        run: |
          npm install
          npx ts-node scripts/token-contract-tester.ts

总结

跨端设计系统的稳健性,取决于最底层的类型契约有多严密。通过构建基于 JSON Schema 的全自动静态契约测试套件,在代码提交阶段把非法色号、非标单位、悬空引用与死循环别名彻底扼杀在摇篮之中,我们才能让单份 Token 资产在流向 iOS、Android、Web 与 Flutter 全端时,成为每一个终端平台都可以绝对信赖的高品质编译基石。

Logo

一站式 AI 云服务平台

更多推荐