DEV Community

yuelinghuashu
yuelinghuashu

Posted on

Industrial‑Grade Build Scripts & DTCG Implementation

🚀 Introduction

In Part 4, we introduced the DTCG three‑layer architecture for design tokens and the concept of industrial‑grade quality checks. This part will turn those concepts into runnable engineering code, presenting the complete industrial‑grade build script of Moongate v2.2.0.

If you have already completed the basic build script from Part 2 and want to upgrade to an industrial level – supporting color normalization, WCAG contrast checking, circular reference detection, and automatic generation of CSS variables and design system documentation – then this part is for you.

💡 Note: This part involves advanced engineering practices. It is recommended to read the first three parts (Basics, Engineering, Multi‑theme) first.


📁 Directory Structure

your-theme/
├── src/
│   ├── core/
│   │   ├── primitives/
│   │   │   └── colors.yaml          # Raw color values
│   │   ├── semantics/
│   │   │   ├── dark.yaml            # Dark semantic layer
│   │   │   └── light.yaml           # Light semantic layer
│   │   └── layout.yaml              # Layout tokens (spacing, typography, breakpoints, etc.)
│   ├── languages/                   # Syntax rules per language
│   ├── workbench.yaml               # UI colors
│   └── semantic.yaml                # Semantic highlighting rules
├── scripts/
│   └── build.js                     # Industrial‑grade build script
├── themes/
│   ├── moongate-dark.json           # Generated theme JSON
│   ├── moongate-light.json
│   ├── moongate-colors.css          # Color tokens (auto‑generated)
│   └── moongate-layout.css          # Layout tokens (auto‑generated)
├── docs/
│   └── DESIGN_SYSTEM.md             # Auto‑generated design system documentation
└── package.json
Enter fullscreen mode Exit fullscreen mode

🎨 Step 1: Primitives File

src/core/primitives/colors.yaml

View full primitives file
# ==================== Raw color values ====================
# Named by hue‑lightness, used as the basic building blocks for all themes

# Blue
blue-500: "#3b82f6"
blue-600: "#2563eb"
blue-700: "#0284c7"
blue-800: "#0369a1"
blue-900: "#1e3a8a"
blue-glow: "#7dd3fc"
blue-glow-dark: "#87cefa"

# Green
green-400: "#34d399"
green-600: "#059669"
green-700: "#10b981"

# Yellow
yellow-400: "#fbbf24"
yellow-500: "#f59e0b"
yellow-600: "#d97706"
yellow-700: "#b45309"

# Red
red-400: "#f87171"
red-500: "#ef4444"
red-600: "#dc2626"
red-700: "#b91c1c"

# Cyan
cyan-400: "#22d3ee"
cyan-500: "#0891b2"
cyan-700: "#0e7490"

# Purple
purple-400: "#c084fc"
purple-500: "#9333ea"
purple-700: "#7e22ce"

# Grayscale
gray-900: "#0f172a"
gray-850: "#131c31"
gray-800: "#1e293b"
gray-750: "#252e40"
gray-700: "#2d3748"
gray-600: "#475569"
gray-550: "#7a8c9e"
gray-500: "#94a3b8"
gray-525: "#a5b4cb"
gray-400: "#94a3b8"
gray-300: "#cbd5e1"
gray-200: "#e2e8f0"
gray-100: "#f1f5f9"
gray-50: "#f9fafb"

# Pure
white: "#ffffff"
black: "#000000"
Enter fullscreen mode Exit fullscreen mode


🌙 Step 2: Semantic Layer Files

Dark Semantic Layer src/core/semantics/dark.yaml

View full dark semantic layer
# ==================== Dark semantic layer ====================

primary: "{blue-500}"
success: "{green-400}"
warning: "{yellow-400}"
error: "{red-400}"
highlight: "{blue-glow}"
cyan: "{cyan-400}"
purple: "{purple-400}"

function: "{blue-glow-dark}"
operator: "{gray-600}"
comment: "{gray-525}"
variable: "{gray-200}"
variableDim: "{gray-300}"
textMuted: "{gray-400}"
punctuation: "{gray-400}"

bg: "{gray-900}"
bgElevated: "{gray-850}"
bgMuted: "{gray-800}"
bgHover: "{blue-500}20"
bgActive: "{blue-500}40"
hoverBg: "{gray-750}"
selectedBg: "{blue-600}"

surfaceGround: "{gray-900}"
surfaceRaised: "{gray-850}"
surfaceFloating: "{gray-800}"
surfaceTooltip: "{gray-750}"
borderFloating: "{blue-500}40"

text: "{gray-200}"
textDim: "{gray-300}"
textInactive: "{gray-400}"

border: "{gray-700}"
borderHover: "{blue-500}"
borderDim: "{gray-600}"

buttonHoverBg: "{blue-600}"
white: "{white}"

ansiBlack: "{gray-800}"
ansiRed: "{red-400}"
ansiGreen: "{green-400}"
ansiYellow: "{yellow-400}"
ansiBlue: "{blue-500}"
ansiMagenta: "{purple-400}"
ansiCyan: "{cyan-400}"
ansiWhite: "{gray-200}"
ansiBrightBlack: "{gray-700}"
ansiBrightRed: "{red-400}"
ansiBrightGreen: "{green-400}"
ansiBrightYellow: "{yellow-400}"
ansiBrightBlue: "{blue-500}"
ansiBrightMagenta: "{purple-400}"
ansiBrightCyan: "{cyan-400}"
ansiBrightWhite: "{white}"

bracket1: "{blue-glow}"
bracket2: "{green-400}"
bracket3: "{yellow-400}"
bracket4: "{purple-400}"
bracket5: "{blue-500}"
bracket6: "{gray-400}"

scrollbar: "{blue-500}"
gitAdded: "{green-400}"
gitModified: "{yellow-400}"
gitDeleted: "{red-400}"
gitUntracked: "{gray-400}"
gitIgnored: "{gray-700}"
debugStart: "{green-400}"
debugPause: "{yellow-400}"
debugStop: "{red-400}"
Enter fullscreen mode Exit fullscreen mode

Light Semantic Layer src/core/semantics/light.yaml

View full light semantic layer
# ==================== Light semantic layer ====================

primary: "{blue-700}"
success: "{green-600}"
warning: "{yellow-700}"
error: "{red-700}"
highlight: "{blue-800}"
cyan: "{cyan-700}"
purple: "{purple-700}"

function: "{blue-800}"
operator: "{gray-600}"
comment: "{gray-600}"
variable: "{gray-900}"
variableDim: "{gray-600}"
textMuted: "{gray-600}"
punctuation: "{gray-400}"

bg: "{gray-50}"
bgElevated: "{white}"
bgMuted: "{gray-100}"
bgHover: "{blue-700}15"
bgActive: "{blue-700}25"
hoverBg: "{gray-100}"
selectedBg: "{gray-300}"

surfaceGround: "{gray-50}"
surfaceRaised: "{white}"
surfaceFloating: "{gray-100}"
surfaceTooltip: "{gray-200}"
borderFloating: "{blue-700}80"

text: "{gray-900}"
textDim: "{gray-600}"
textInactive: "{gray-400}"

border: "{gray-300}"
borderHover: "{blue-700}"
borderDim: "{gray-400}"

buttonHoverBg: "{blue-700}"
white: "{white}"

ansiBlack: "{gray-900}"
ansiRed: "{red-700}"
ansiGreen: "{green-600}"
ansiYellow: "{yellow-700}"
ansiBlue: "{blue-700}"
ansiMagenta: "{purple-700}"
ansiCyan: "{cyan-700}"
ansiWhite: "{gray-200}"
ansiBrightBlack: "{gray-500}"
ansiBrightRed: "{red-400}"
ansiBrightGreen: "{green-400}"
ansiBrightYellow: "{yellow-400}"
ansiBrightBlue: "{blue-600}"
ansiBrightMagenta: "{purple-400}"
ansiBrightCyan: "{cyan-400}"
ansiBrightWhite: "{white}"

bracket1: "{blue-700}"
bracket2: "{green-600}"
bracket3: "{yellow-700}"
bracket4: "{purple-700}"
bracket5: "{cyan-700}"
bracket6: "{gray-500}"

scrollbar: "{blue-700}"
gitAdded: "{green-600}"
gitModified: "{yellow-700}"
gitDeleted: "{red-700}"
gitUntracked: "{gray-500}"
gitIgnored: "{gray-400}"
debugStart: "{green-600}"
debugPause: "{yellow-700}"
debugStop: "{red-700}"
Enter fullscreen mode Exit fullscreen mode


📐 Step 3: Layout Tokens File

src/core/layout.yaml

View full layout tokens file
# ==================== Layout tokens ====================
# Spacing, radius, shadow, typography, breakpoints, z‑index

spacing:
  base: "4px"
  xs: "4px"
  sm: "8px"
  md: "16px"
  lg: "24px"
  xl: "32px"
  "2xl": "48px"

radius:
  none: "0px"
  sm: "0px"
  md: "0px"
  lg: "0px"

shadow:
  none: "none"
  border: "0 0 0 1px"

typography:
  family-mono: "'JetBrains Mono', 'Fira Code', monospace"
  family-sans: "Inter, system-ui, -apple-system, sans-serif"
  size-code: "13px"
  size-body: "14px"
  size-small: "12px"
  line-height: "1.5"

breakpoints:
  mobile: "640px"
  tablet: "768px"
  desktop: "1024px"
  wide: "1280px"

z-index:
  base: 1
  sticky: 100
  overlay: 500
  modal: 1000
  tooltip: 1500
Enter fullscreen mode Exit fullscreen mode


🛠️ Step 4: Industrial‑Grade Build Script

scripts/build.js

View full industrial‑grade build script
const fs = require("fs");
const yaml = require("js-yaml");
const path = require("path");
const wcag = require("wcag-contrast");

const ROOT_DIR = path.resolve(__dirname, "..");

// ==================== Path Configuration ====================
const PATHS = {
  primitives: path.join(ROOT_DIR, "src", "core", "primitives", "colors.yaml"),
  semanticsDir: path.join(ROOT_DIR, "src", "core", "semantics"),
  layout: path.join(ROOT_DIR, "src", "core", "layout.yaml"),
  workbench: path.join(ROOT_DIR, "src", "workbench.yaml"),
  semantic: path.join(ROOT_DIR, "src", "semantic.yaml"),
  langDir: path.join(ROOT_DIR, "src", "languages"),
  specialDir: path.join(ROOT_DIR, "src", "special"),
  outputDir: path.join(ROOT_DIR, "themes"),
  docsDir: path.join(ROOT_DIR, "docs"),
};

// ==================== Helper Functions ====================

function ensureFileExists(filePath, description) {
  if (!fs.existsSync(filePath)) {
    throw new Error(`❌ ${description} file not found: ${filePath}`);
  }
}

function safeLoadYaml(filePath, description) {
  try {
    return yaml.load(fs.readFileSync(filePath, "utf8"));
  } catch (err) {
    console.error(`❌ Failed to parse ${description} (${filePath}):`, err.message);
    return null;
  }
}

/**
 * Normalize hex color
 */
function normalizeHex(color, tokenName) {
  if (typeof color !== "string" || !color.startsWith("#")) {
    if (color && !color.startsWith("#")) {
      console.warn(`⚠️ Skipping non‑hex color: ${tokenName} = ${color}`);
    }
    return color;
  }

  let hex = color.replace("#", "");

  if (hex.length === 3) {
    hex = hex.split("").map((c) => c + c).join("");
  } else if (hex.length === 4) {
    hex = hex.split("").map((c) => c + c).join("");
  }

  if (!/^[0-9a-fA-F]{6}$|^[0-9a-fA-F]{8}$/.test(hex)) {
    console.error(`❌ Fatal error: token "${tokenName}" value "${color}" does not conform to industrial specification.`);
    console.error(`   Requirement: 6‑digit (#RRGGBB) or 8‑digit (#RRGGBBAA) hex`);
    process.exit(1);
  }

  return `#${hex.toLowerCase()}`;
}

/**
 * Recursively resolve token references {token-name}
 */
function resolveTokens(obj, tokenMap, depth = 0, path = []) {
  const MAX_DEPTH = 20;
  if (depth > MAX_DEPTH) {
    throw new Error(`[ENGINEERING_FATAL] Circular reference detected: ${path.join("")}`);
  }

  if (typeof obj === "string") {
    const resolveOne = (str) => {
      return str.replace(/\{([a-zA-Z0-9_-]+)\}/g, (match, key) => {
        const value = tokenMap[key];
        if (value === undefined) {
          console.warn(`⚠️ Warning: token "${key}" not defined, keeping placeholder`);
          return match;
        }
        return resolveOne(value);
      });
    };
    return resolveOne(obj);
  }
  if (Array.isArray(obj)) {
    return obj.map((item) => resolveTokens(item, tokenMap, depth + 1, path));
  }
  if (obj && typeof obj === "object") {
    const result = {};
    for (const [k, v] of Object.entries(obj)) {
      result[k] = resolveTokens(v, tokenMap, depth + 1, [...path, k]);
    }
    return result;
  }
  return obj;
}

/**
 * Normalize all color values
 */
function normalizeColors(obj, tokenName) {
  if (typeof obj === "string") {
    return normalizeHex(obj, tokenName);
  }
  if (Array.isArray(obj)) {
    return obj.map((item) => normalizeColors(item, tokenName));
  }
  if (obj && typeof obj === "object") {
    const result = {};
    for (const [k, v] of Object.entries(obj)) {
      result[k] = normalizeColors(v, `${tokenName}.${k}`);
    }
    return result;
  }
  return obj;
}

/**
 * Detect direct primitive references (e.g., {blue-500})
 */
function detectPrimitiveReference(value, context) {
  if (typeof value === "string" && /\{([a-zA-Z0-9_-]+)\}/.test(value)) {
    const match = value.match(/\{([a-zA-Z0-9_-]+)\}/)[1];
    const primitivePrefixes = [
      "blue-", "green-", "yellow-", "red-", "cyan-", "purple-", "gray-",
      "white", "black",
    ];
    if (primitivePrefixes.some((prefix) => match.startsWith(prefix))) {
      console.warn(`[Architecture reminder] ${context} directly references primitive value "${match}". Use semantic layer instead.`);
    }
  }
}

/**
 * Replace ${var} with final color value (supports opacity suffix)
 * Opacity suffix format: two‑digit hex (00–FF), e.g., 20 ≈ 12.5% opacity, 80 ≈ 50%, FF = opaque.
 */
function replaceVariables(obj, colors, context = "") {
  if (typeof obj === "string") {
    if (context) detectPrimitiveReference(obj, context);

    return obj.replace(/\$\{([a-zA-Z0-9_-]+)\}([0-9a-fA-F]{2})?/g, (match, key, alpha) => {
      const value = colors[key];
      if (value === undefined) {
        console.warn(`⚠️ Warning: variable "${key}" not defined, keeping placeholder`);
        return match;
      }
      if (alpha) {
        if (/^rgba?\(/.test(value)) {
          console.warn(`⚠️ Warning: variable "${key}" value ${value} is already rgba, ignoring suffix`);
          return value;
        }
        if (/^#[0-9a-fA-F]{8}$/.test(value)) {
          console.warn(`⚠️ Warning: variable "${key}" value ${value} already contains opacity, ignoring suffix "${alpha}"`);
          return value;
        }
        if (/^#[0-9a-fA-F]{6}$/.test(value)) {
          return value + alpha;
        }
        console.warn(`⚠️ Warning: variable "${key}" value ${value} has unsupported format, cannot apply opacity`);
        return value;
      }
      return value;
    });
  }
  if (Array.isArray(obj)) {
    return obj.map((item) => replaceVariables(item, colors, context));
  }
  if (obj && typeof obj === "object") {
    const result = {};
    for (const [k, v] of Object.entries(obj)) {
      result[k] = replaceVariables(v, colors, context);
    }
    return result;
  }
  return obj;
}

/**
 * WCAG contrast check (graduated standard)
 */
function checkContrast(color1, color2, role, themeType) {
  if (!color1 || !color2) return;
  const ratio = wcag.hex(color1, color2);

  let minRatio = 4.5;
  if (role === "textDim" || role === "comment") {
    minRatio = 4.0;
  }
  if (role === "textMuted") {
    minRatio = 3.0;
  }

  if (ratio < minRatio) {
    if (role === "textMuted") {
      console.warn(`⚠️ Contrast slightly low: ${themeType} · ${role} (${color1}) vs background (${color2}) = ${ratio.toFixed(2)}:1`);
      console.warn(`   Recommended ≥3.0:1, currently meets minimum.`);
    } else {
      console.error(`❌ Insufficient contrast: ${themeType} · ${role} (${color1}) vs background (${color2}) = ${ratio.toFixed(2)}:1`);
      console.error(`   WCAG requires ≥${minRatio}:1, below standard.`);
      process.exit(1);
    }
  } else {
    console.log(`✅ ${themeType} · ${role}: ${ratio.toFixed(2)}:1`);
  }
}

/**
 * Generate CSS variables for colors (includes light/dark mode)
 */
function generateColorCss(lightColors, darkColors) {
  let css = `/* ===== Moongate Color Tokens – Auto‑generated ===== */\n`;
  css += `/* Source: VS Code theme build script */\n`;
  css += `/* Do not edit manually – edit primitives/ and semantics/ directories instead */\n\n`;

  css += `/* Light mode */\n:root,\n.light {\n`;
  Object.entries(lightColors).forEach(([key, val]) => {
    const cssKey = key.replace(/([A-Z])/g, "-$1").toLowerCase();
    css += `  --ui-${cssKey}: ${val};\n`;
  });
  css += `}\n\n`;

  css += `/* Dark mode */\n.dark {\n`;
  Object.entries(darkColors).forEach(([key, val]) => {
    const cssKey = key.replace(/([A-Z])/g, "-$1").toLowerCase();
    css += `  --ui-${cssKey}: ${val};\n`;
  });
  css += `}\n`;

  const cssPath = path.join(PATHS.outputDir, "moongate-colors.css");
  fs.writeFileSync(cssPath, css);
  console.log(`✅ Color tokens generated: ${cssPath}`);
}

/**
 * Generate CSS variables for layout tokens (supports nested objects)
 */
function generateLayoutCss(layoutTokens) {
  let css = `/* ===== Moongate Layout Tokens – Auto‑generated ===== */\n`;
  css += `/* Includes: spacing, radius, shadow, typography, breakpoints, z‑index */\n`;
  css += `/* Do not edit manually – edit src/core/layout.yaml instead */\n\n`;

  css += `:root {\n`;

  function flattenObject(obj, prefix = "") {
    Object.entries(obj).forEach(([key, val]) => {
      const fullKey = prefix ? `${prefix}-${key}` : key;
      if (val && typeof val === "object" && !Array.isArray(val)) {
        flattenObject(val, fullKey);
      } else {
        let formattedVal = val;
        if (typeof formattedVal === "string") {
          if ((formattedVal.startsWith("'") && formattedVal.endsWith("'")) ||
              (formattedVal.startsWith('"') && formattedVal.endsWith('"'))) {
            formattedVal = formattedVal.slice(1, -1);
          }
        }
        css += `  --ui-${fullKey}: ${formattedVal};\n`;
      }
    });
  }

  flattenObject(layoutTokens);

  css += `}\n`;

  const cssPath = path.join(PATHS.outputDir, "moongate-layout.css");
  fs.writeFileSync(cssPath, css);
  console.log(`✅ Layout tokens generated: ${cssPath}`);
}

/**
 * Generate design system documentation (enhanced)
 * Includes variable selection protocol, primitive colors, elevation system, semantic layer contrast
 */
function generateDesignSystemDoc(primitives, lightColors, darkColors) {
  const md = [];

  md.push("# Moongate Design System\n");
  md.push("## 🧭 Moongate Variable Selection Protocol\n");
  md.push("To ensure long‑term maintainability and semantic consistency, follow this decision path:\n");
  md.push("| Scenario | Lookup Location | Prohibited Action |");
  md.push("|----------|----------------|-------------------|");
  md.push("| **I need to define a new primitive color** (e.g., `blue-600`) | `primitives/colors.yaml` | ❌ Do not write raw color values in semantics or components |");
  md.push("| **I need to assign a value to a semantic role** (e.g., `primary`) | `semantics/*.yaml` (reference primitives) | ❌ Do not reference primitives directly in components |");
  md.push("| **I need to style a UI component** (e.g., `sideBar.background`) | Reference semantic variables (e.g., `${surfaceRaised}`) | ❌ Do not use `${blue-500}` directly or hard‑code colors |");
  md.push("| **I need a new semantic role** | Add a logical role in semantics (e.g., `actionHover`), then reference it in components | ❌ Never invent new variables inside components |");
  md.push("\n> **Core principle**: All colors must flow through the chain `primitives → semantics → components`. Any cross‑layer direct reference is **architecture pollution**.\n");

  md.push("## 🎨 Primitive Colors\n");

  const colorGroups = {
    blue: ["blue-500", "blue-600", "blue-700", "blue-800", "blue-900", "blue-glow", "blue-glow-dark"],
    green: ["green-400", "green-600", "green-700"],
    yellow: ["yellow-400", "yellow-500", "yellow-600", "yellow-700"],
    red: ["red-400", "red-500", "red-600", "red-700"],
    cyan: ["cyan-400", "cyan-500", "cyan-700"],
    purple: ["purple-400", "purple-500", "purple-700"],
    gray: Object.keys(primitives).filter((k) => k.startsWith("gray-")),
    special: ["white", "black"],
  };

  for (const [group, keys] of Object.entries(colorGroups)) {
    if (keys.length === 0) continue;
    md.push(`### ${group.charAt(0).toUpperCase() + group.slice(1)} Series\n`);
    md.push("| Token | Value | Preview |");
    md.push("|-------|-------|---------|");
    for (const key of keys) {
      if (!primitives[key]) continue;
      const val = primitives[key];
      const preview = `![](https://placehold.co/20x20/${val.slice(1)}/${val.slice(1)}?text=+)`;
      md.push(`| \`--moongate-${key}\` | \`${val}\` | ${preview} |`);
    }
    md.push("");
  }

  md.push("## 🏔️ Elevation System\n");
  md.push("The elevation system expresses physical depth through lightness differences, following Material Design elevation guidelines.\n");
  md.push("| Variable | Light Mode | Dark Mode | Description |");
  md.push("|----------|------------|-----------|-------------|");
  md.push(`| \`surfaceGround\` | \`${lightColors.surfaceGround}\` | \`${darkColors.surfaceGround}\` | Ground layer (0dp) – editor background |`);
  md.push(`| \`surfaceRaised\` | \`${lightColors.surfaceRaised}\` | \`${darkColors.surfaceRaised}\` | Raised layer (2dp) – sidebar, activity bar |`);
  md.push(`| \`surfaceFloating\` | \`${lightColors.surfaceFloating}\` | \`${darkColors.surfaceFloating}\` | Floating layer (8dp) – popups, menus |`);
  md.push(`| \`surfaceTooltip\` | \`${lightColors.surfaceTooltip}\` | \`${darkColors.surfaceTooltip}\` | Tooltip layer (12dp) – tooltips |`);
  md.push(`| \`borderFloating\` | \`${lightColors.borderFloating}\` | \`${darkColors.borderFloating}\` | Floating layer border (translucent primary) |\n`);

  const contrast = (color1, color2) => {
    if (!color1 || !color2) return null;
    try {
      return wcag.hex(color1, color2).toFixed(2);
    } catch {
      return null;
    }
  };

  md.push("## 🌙 Light Mode Semantics\n");
  md.push("| Semantic Variable | Value | Preview | WCAG Contrast (vs `bg`) |");
  md.push("|------------------|-------|---------|--------------------------|");
  const lightBg = lightColors.bg;
  const importantKeys = ["text", "textDim", "textMuted", "comment", "primary", "success", "warning", "error"];
  for (const [key, val] of Object.entries(lightColors)) {
    const preview = `![](https://placehold.co/20x20/${val.slice(1)}/${val.slice(1)}?text=+)`;
    let contrastRatio = "-";
    if (importantKeys.includes(key) && lightBg) {
      const ratio = contrast(val, lightBg);
      if (ratio) contrastRatio = `${ratio}:1`;
    }
    md.push(`| \`${key}\` | \`${val}\` | ${preview} | ${contrastRatio} |`);
  }

  md.push("\n## 🌑 Dark Mode Semantics\n");
  md.push("| Semantic Variable | Value | Preview | WCAG Contrast (vs `bg`) |");
  md.push("|------------------|-------|---------|--------------------------|");
  const darkBg = darkColors.bg;
  for (const [key, val] of Object.entries(darkColors)) {
    const preview = `![](https://placehold.co/20x20/${val.slice(1)}/${val.slice(1)}?text=+)`;
    let contrastRatio = "-";
    if (importantKeys.includes(key) && darkBg) {
      const ratio = contrast(val, darkBg);
      if (ratio) contrastRatio = `${ratio}:1`;
    }
    md.push(`| \`${key}\` | \`${val}\` | ${preview} | ${contrastRatio} |`);
  }

  const mdPath = path.join(PATHS.docsDir, "DESIGN_SYSTEM.md");
  if (!fs.existsSync(PATHS.docsDir)) {
    fs.mkdirSync(PATHS.docsDir, { recursive: true });
  }
  fs.writeFileSync(mdPath, md.join("\n"), "utf8");
  console.log(`✅ Design system documentation generated: ${mdPath}`);
}

function getThemeInfo() {
  const pkgPath = path.join(ROOT_DIR, "package.json");
  if (!fs.existsSync(pkgPath)) {
    return { name: "your-theme", displayName: "Your Theme" };
  }
  try {
    const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
    let displayName = pkg.displayName || pkg.name || "Your Theme";
    if (displayName.startsWith("@") && displayName.includes("/")) {
      displayName = displayName.split("/")[1];
    }
    return {
      name: pkg.name || "your-theme",
      displayName,
    };
  } catch {
    return { name: "your-theme", displayName: "Your Theme" };
  }
}

// ==================== Main ====================
function main() {
  console.log("🚀 Building theme (DTCG standard + industrial quality checks)...\n");

  try {
    ensureFileExists(PATHS.primitives, "primitives");
    ensureFileExists(PATHS.semanticsDir, "semantics directory");
    ensureFileExists(PATHS.layout, "layout tokens");
    ensureFileExists(PATHS.workbench, "workbench");
    ensureFileExists(PATHS.semantic, "semantic");
  } catch (err) {
    console.error(err.message);
    process.exit(1);
  }

  console.log("📦 Loading primitives...");
  const primitivesRaw = safeLoadYaml(PATHS.primitives, "primitives.yaml");
  if (!primitivesRaw) process.exit(1);

  const primitives = {};
  Object.entries(primitivesRaw).forEach(([key, val]) => {
    primitives[key] = normalizeHex(val, `primitives.${key}`);
  });

  console.log("📦 Loading layout tokens...");
  const layoutTokens = safeLoadYaml(PATHS.layout, "layout.yaml");
  if (layoutTokens) {
    generateLayoutCss(layoutTokens);
  } else {
    console.error("❌ Failed to load layout.yaml, build aborted");
    process.exit(1);
  }

  console.log("📦 Loading shared rules...");
  const workbenchRaw = safeLoadYaml(PATHS.workbench, "workbench.yaml");
  const semanticRaw = safeLoadYaml(PATHS.semantic, "semantic.yaml");
  if (!workbenchRaw || !semanticRaw) process.exit(1);

  console.log("📚 Loading language rules...");
  let tokenColorsRaw = [];
  if (fs.existsSync(PATHS.langDir)) {
    const langFiles = fs.readdirSync(PATHS.langDir).filter((f) => f.endsWith(".yaml")).sort();
    langFiles.forEach((file) => {
      const filePath = path.join(PATHS.langDir, file);
      const langRules = safeLoadYaml(filePath, `language rules ${file}`);
      if (langRules?.tokenColors) {
        tokenColorsRaw = tokenColorsRaw.concat(langRules.tokenColors);
        console.log(`   ✅ Loaded: ${file}`);
      }
    });
  }

  console.log("✨ Loading special rules...");
  if (fs.existsSync(PATHS.specialDir)) {
    const specialFiles = fs.readdirSync(PATHS.specialDir).filter((f) => f.endsWith(".yaml"));
    specialFiles.forEach((file) => {
      const filePath = path.join(PATHS.specialDir, file);
      const specialRules = safeLoadYaml(filePath, `special rules ${file}`);
      if (specialRules?.tokenColors) {
        tokenColorsRaw = tokenColorsRaw.concat(specialRules.tokenColors);
        console.log(`   ✅ Loaded: ${file}`);
      }
    });
  }

  console.log("\n🎨 Scanning semantic files...");
  const semanticFiles = fs.readdirSync(PATHS.semanticsDir).filter((f) => f.endsWith(".yaml"));
  if (semanticFiles.length === 0) {
    console.error("❌ No .yaml files found in semantics directory");
    process.exit(1);
  }

  const themeInfo = getThemeInfo();
  let baseName = themeInfo.name.replace(/[^a-z0-9-]/gi, "-").toLowerCase();

  let lightSemantics, darkSemantics;

  console.log(`\n🔨 Building themes...\n`);
  semanticFiles.forEach((semanticFile) => {
    const themeType = path.basename(semanticFile, ".yaml");
    const outputFile = path.join(PATHS.outputDir, `${baseName}-${themeType}.json`);

    const semanticsPath = path.join(PATHS.semanticsDir, semanticFile);
    const semantics = safeLoadYaml(semanticsPath, `semantic layer ${semanticFile}`);
    if (!semantics) {
      console.error(`   ❌ Skipping ${semanticFile}`);
      return;
    }

    const resolved = resolveTokens(semantics, primitives);
    const normalized = normalizeColors(resolved, `semantics.${semanticFile}`);

    if (themeType === "light") lightSemantics = normalized;
    if (themeType === "dark") darkSemantics = normalized;

    const uiColors = replaceVariables(workbenchRaw, normalized, `workbench`);
    const semanticColors = replaceVariables(semanticRaw, normalized, `semantic`);
    const tokenColors = replaceVariables(tokenColorsRaw, normalized, `tokenColors`);

    const type = themeType.includes("light") ? "light" : "dark";
    const displaySuffix = themeType === "dark" ? "Dark" : "Light";

    const theme = {
      name: `${themeInfo.displayName} ${displaySuffix}`,
      type: type,
      colors: uiColors,
      tokenColors: tokenColors,
      semanticTokenColors: semanticColors,
    };

    if (!fs.existsSync(PATHS.outputDir)) {
      fs.mkdirSync(PATHS.outputDir, { recursive: true });
    }

    fs.writeFileSync(outputFile, JSON.stringify(theme, null, 2));
    console.log(`   ✅ Built: ${outputFile}`);

    if (normalized.bg && normalized.text) {
      checkContrast(normalized.text, normalized.bg, "text", themeType);
    }
    if (normalized.bg && normalized.textDim) {
      checkContrast(normalized.textDim, normalized.bg, "textDim", themeType);
    }
    if (normalized.bg && normalized.textMuted) {
      checkContrast(normalized.textMuted, normalized.bg, "textMuted", themeType);
    }
  });

  if (lightSemantics && darkSemantics) {
    generateColorCss(lightSemantics, darkSemantics);
    generateDesignSystemDoc(primitives, lightSemantics, darkSemantics);
  }

  console.log("\n🎉 All themes built successfully!");
}

main();
Enter fullscreen mode Exit fullscreen mode


⚙️ Step 5: Configure package.json

{
  "name": "moongate-theme",
  "version": "2.2.0",
  "scripts": {
    "build": "node scripts/build.js",
    "watch": "nodemon --watch src -e yaml --exec \"pnpm run build\"",
    "prepublishOnly": "pnpm run build"
  },
  "devDependencies": {
    "js-yaml": "^4.1.1",
    "nodemon": "^3.1.14",
    "wcag-contrast": "^3.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Note: Make sure to run pnpm install or npm install to install all dependencies, otherwise the script will fail because wcag-contrast is missing.


🚀 Step 6: Run the Build

pnpm run build
Enter fullscreen mode Exit fullscreen mode

A successful build will produce output similar to:

🚀 Building theme (DTCG standard + industrial quality checks)...

📦 Loading primitives...
📦 Loading layout tokens...
✅ Layout tokens generated: themes/moongate-layout.css
📦 Loading shared rules...
📚 Loading language rules...
   ✅ Loaded: base.yaml
   ...
✨ Loading special rules...
   ✅ Loaded: better-comments.yaml

🎨 Scanning semantic files...

🔨 Building themes...
   ✅ Built: themes/moongate-dark.json
✅ dark · text: 14.48:1
✅ dark · textDim: 12.02:1
✅ dark · textMuted: 6.96:1
   ✅ Built: themes/moongate-light.json
✅ light · text: 17.08:1
✅ light · textDim: 7.25:1
✅ light · textMuted: 7.25:1
✅ Color tokens generated: themes/moongate-colors.css
✅ Design system documentation generated: docs/DESIGN_SYSTEM.md

🎉 All themes built successfully!
Enter fullscreen mode Exit fullscreen mode

Generated files:

  • themes/moongate-dark.json, themes/moongate-light.json – VS Code themes
  • themes/moongate-colors.css – Color tokens (light/dark)
  • themes/moongate-layout.css – Layout tokens (spacing, typography, breakpoints, etc.)
  • docs/DESIGN_SYSTEM.md – Complete design system documentation with variable selection protocol

📊 Step 7: Using the Generated Assets

CSS Variable Naming Rules

Generated CSS variables use the --ui- prefix, converting camelCase semantic variable names to kebab‑case. For example:

Semantic Variable Generated CSS Variable Description
bg --ui-bg Editor background
surfaceRaised --ui-surface-raised Raised surface background
textMuted --ui-text-muted Muted text color
spacing.md --ui-spacing-md Medium spacing

Using CSS Variables in a Blog

<link rel="stylesheet" href="/themes/moongate-colors.css">
<link rel="stylesheet" href="/themes/moongate-layout.css">
Enter fullscreen mode Exit fullscreen mode
body {
  background: var(--ui-bg);
  color: var(--ui-text);
  font-family: var(--ui-typography-family-sans);
  font-size: var(--ui-typography-size-body);
}

.card {
  background: var(--ui-surface-raised);
  border: var(--ui-shadow-border) var(--ui-border);
  padding: var(--ui-spacing-md);
  border-radius: var(--ui-radius-none);
}

.button-primary:hover {
  /* State composition: base + overlay */
  background-image: linear-gradient(var(--ui-action-hover), var(--ui-action-hover));
  background-color: var(--ui-primary);
}

@media (min-width: var(--ui-breakpoint-tablet)) {
  .container {
    padding: var(--ui-spacing-lg);
  }
}
Enter fullscreen mode Exit fullscreen mode

Switching Between Light and Dark Modes

Add or remove the .dark class on the root element:

document.documentElement.classList.toggle('dark');
Enter fullscreen mode Exit fullscreen mode

📝 Summary

With this part, you have fully implemented an industrial‑grade build script:

  • ✅ DTCG three‑layer architecture (primitives, semantics, components)
  • ✅ Color normalization and circular reference detection
  • ✅ WCAG contrast checking
  • ✅ Automatic generation of CSS variables for colors
  • ✅ Automatic generation of CSS variables for layout (spacing, typography, breakpoints, etc.)
  • ✅ Automatic generation of design system documentation (including variable selection protocol)

This script not only serves the Moongate theme but can also be the engineering foundation for all your future design system projects. You now have a complete design system – from design philosophy to engineering code to cross‑platform assets.

Explore endlessly, code without limits.

⬆ Back to top


This article is part of the **VS Code Theme Development Guide* series. The original Chinese version is available on my blog: moongate.top.*

© 2026 yuelinghuashu. This work is licensed under CC BY-NC 4.0.

Top comments (0)