menu
search
...

Color Management with Single Source of Truth

eye-

Background and Problem

In wechat mini app, color definitions are typically needed in multiple places:

The traditional approach is to define them separately in two places:

// _variables.scss
$Primary: #228891;
$Gray100: #f7f7f7;
// colors.js
export const COLORS = {
  Primary: "#228891",
  Gray100: "#f7f7f7",
}

Problems:

  1. Duplicate definitions: The same color value needs to be maintained in multiple places
  2. Easy to be inconsistent: May miss some places when modifying
  3. High maintenance cost: Adding or modifying colors requires synchronizing multiple files
  4. Error-prone: Manual synchronization is prone to errors

Solution

Adopt the Single Source of Truth (SSOT) principle, using a single data source + build-time code generation approach:

Single Source of Truth (colors.js)
Build-time Auto-generation
┌─────────────────────┬─────────────────────┐
│ _colors-generated.scss │ colors-generated.js (SCSS variables)     (JS constants)└─────────────────────┴─────────────────────┘

Technical Implementation

1. Single Source of Truth: colors.js

All color definitions in one JavaScript file, supporting comments and grouping:

/**
 * Color definitions - single source of truth
 * This file is used to generate SCSS variables and JS constants via build/generateColors.js
 */

// Base colors
const white = "#fff"
const black = "#000"

// Primary colors
const Primary = "#228891"
const Primary100 = "#eff6fa"
const Primary200 = "#c1dbe7"

// Composite colors (using variables for clarity)
// These are calculated from base colors above, making the relationship clear
const Outline = "rgba(255, 255, 255, 0.08)" // rgba(white, 0.08)
const Overlay1 = "rgba(0, 0, 0, 0.1)" // rgba(black, 0.1)

// Export all colors as a flat object for the generator
module.exports = {
  Primary,
  black,
  white,
  Primary100,
  Primary200,
  Outline,
  Overlay1,
  // ... more colors
}

Advantages:

2. Code Generator: generateColors.js

Build-time script that reads colors.js and generates two files:

/**
 * Generate color files at build time
 * Generate SCSS variable file and JS constants file from colors.js
 */
const path = require("path")

const colorsJsPath = path.resolve(__dirname, "../src/style/colors.js")
const scssOutputPath = path.resolve(
  __dirname,
  "../src/style/_colors-generated.scss"
)
const jsOutputPath = path.resolve(__dirname, "../src/style/colors-generated.js")

function generateColorFiles() {
  // Read JS file (use require to execute JS code)
  // Clear require cache to ensure we always get the latest content
  delete require.cache[require.resolve(colorsJsPath)]
  const colors = require(colorsJsPath)

  // Generate SCSS variable file
  let scssContent =
    "// Auto-generated file by build/generateColors.js, do not edit manually\n"
  scssContent += "// Color definitions are in src/style/colors.js\n\n"

  Object.entries(colors).forEach(([key, value]) => {
    scssContent += `$${key}: ${value};\n`
  })

  // Generate JS constants file
  let jsContent = "/**\n"
  jsContent +=
    " * Auto-generated file by build/generateColors.js, do not edit manually\n"
  jsContent += " * Color definitions are in src/style/colors.js\n"
  jsContent += " */\n\n"
  jsContent += "export const COLORS = {\n"

  Object.entries(colors).forEach(([key, value]) => {
    jsContent += `  ${key}: "${value}",\n`
  })

  jsContent += "};\n"

  // Write files
  const fs = require("fs")
  fs.writeFileSync(scssOutputPath, scssContent, "utf8")
  fs.writeFileSync(jsOutputPath, jsContent, "utf8")

  console.log("✅ Color files generated successfully:")
  console.log(`   - ${scssOutputPath}`)
  console.log(`   - ${jsOutputPath}`)
}

// Execute if this script is run directly
if (require.main === module) {
  generateColorFiles()
}

module.exports = generateColorFiles

Key Points:

3. Webpack Plugin: ColorGeneratorPlugin.js

Integrate into the build process for automation:

/**
 * Webpack plugin: Auto-generate SCSS and JS color files from colors.js
 */
const fs = require("fs")
const path = require("path")
const generateColorFiles = require("./generateColors")

class ColorGeneratorPlugin {
  apply(compiler) {
    // Generate color files before compilation starts
    compiler.hooks.beforeRun.tap("ColorGeneratorPlugin", () => {
      generateColorFiles()
    })

    // Watch colors.js file changes
    compiler.hooks.afterCompile.tap("ColorGeneratorPlugin", (compilation) => {
      const colorsJsPath = path.resolve(__dirname, "../src/style/colors.js")
      if (fs.existsSync(colorsJsPath)) {
        compilation.fileDependencies.add(colorsJsPath)
      }
    })

    // Regenerate when colors.js changes
    compiler.hooks.invalid.tap("ColorGeneratorPlugin", (fileName) => {
      if (fileName && fileName.includes("colors.js")) {
        generateColorFiles()
      }
    })
  }
}

module.exports = ColorGeneratorPlugin

Webpack Hooks Explanation:

  1. beforeRun: Generate color files before compilation starts, ensuring files exist
  2. afterCompile: Add colors.js to file dependencies so Webpack watches for changes
  3. invalid: When colors.js changes, immediately regenerate color files

Workflow:

User modifies colors.js
Webpack detects changes (via dependencies added in afterCompile)
Triggers invalid hook
Regenerate color files
Webpack starts recompilation

4. Integration into Project

Configure in webpack.config.js:

const ColorGeneratorPlugin = require("./build/ColorGeneratorPlugin")

module.exports = {
  configureWebpack(config) {
    const plugins = [
      new ColorGeneratorPlugin(), // Add plugin
      // ... other plugins
    ]
    return { plugins }
  },
}

Usage

Using in SCSS

// _variables.scss
@import "./_colors-generated";

// Direct usage
.my-class {
  color: $Primary;
  background: $Gray100;
  border: 1px solid $Border;
}

Using in JavaScript/Templates

// 1. Import color constants
import { COLORS } from "@style/colors-generated"

// 2. Use in components
createPage({
  data() {
    return {
      primaryColor: COLORS.Primary,
    }
  },
})
<!-- 3. Use in templates -->
<Custom-Com color="{{primaryColor}}" />

Advantages Summary

AdvantageDescription
Single SourceAll colors defined in one place, avoiding duplicates
Auto SyncAutomatically generate all formats after modifying colors.js
Type SafeReduces errors from manual synchronization
Easy MaintenanceAdding colors only requires adding in one place
Comment SupportJS files support comments to explain color usage
Build-time GenerationNo runtime overhead
Hot Reload SupportAuto-regenerate when modifying colors.js during development

Notes

  1. ⚠️ Do not manually edit generated files (_colors-generated.scss and colors-generated.js)
  2. ✅ All color modifications should be done in colors.js
  3. ✅ Generated files are added to .gitignore and won't be committed to the repository
  4. ✅ After modifying colors.js, Webpack will automatically regenerate files

Manual Generation

If you need to manually generate color files, you can run:

npm run generate:colors

Project Structure

project/
├── src/
│   └── style/
│       ├── colors.js                    # Single source of truth (manually maintained)
│       ├── _colors-generated.scss       # Auto-generated (do not edit manually)
│       ├── colors-generated.js          # Auto-generated (do not edit manually)
│       └── _variables.scss              # Import generated files
├── build/
│   ├── generateColors.js                # Code generator
│   └── ColorGeneratorPlugin.js          # Webpack plugin
└── webpack.config.js                        # Configure plugin

Summary

Through the single source of truth + build-time code generation solution, we achieved:

This is a typical practice of the DRY (Don't Repeat Yourself) principle, solving the multi-format synchronization problem through build tool automation.


Related Files:

abbreviationhow to create image carousel