|
| 1 | +/** |
| 2 | + * Copyright (c) HashiCorp, Inc. |
| 3 | + * SPDX-License-Identifier: MPL-2.0 |
| 4 | + */ |
| 5 | + |
| 6 | +import chalk from 'chalk'; |
| 7 | + |
| 8 | +import type { Dictionary, PlatformConfig } from 'style-dictionary'; |
| 9 | + |
| 10 | +import { getSourceFromFileWithRootSelector } from './getSourceFromFileWithRootSelector.ts'; |
| 11 | +import type { Mode } from './getStyleDictionaryConfig.ts'; |
| 12 | +import { modes } from './getStyleDictionaryConfig.ts'; |
| 13 | + |
| 14 | +export async function validateThemingCssFiles(_dictionary: Dictionary, config: PlatformConfig): Promise<void> { |
| 15 | + |
| 16 | + // store all the sources in memory |
| 17 | + const allSources = {} as Record<Mode, Record<string, string>>; |
| 18 | + for (const mode of modes) { |
| 19 | + const commonSource = await getSourceFromFileWithRootSelector(config, mode, 'common-tokens.css'); |
| 20 | + const themedSource = await getSourceFromFileWithRootSelector(config, mode, 'themed-tokens.css'); |
| 21 | + allSources[mode] = { commonSource, themedSource } |
| 22 | + } |
| 23 | + |
| 24 | + // first validation: make sure that all the common files have actually the same content |
| 25 | + const comparisonModes = modes.filter(mode => mode !== 'default'); |
| 26 | + comparisonModes.forEach((comparisonMode: Mode) => { |
| 27 | + if (allSources[comparisonMode].commonSource !== allSources.default.commonSource) { |
| 28 | + // we want to interrupt the execution of the script if one of the generated "common" files is different from the others |
| 29 | + // note: comment this out if you need to debug why they differ, so the files are saved with the different content |
| 30 | + throw new Error(`❌ ${chalk.red.bold('ERROR')} - Generated "common" tokens for mode '${comparisonMode}' differ from the ones generated for the 'default' mode (expected to be identical)`); |
| 31 | + } |
| 32 | + }); |
| 33 | + |
| 34 | + // second validation: make sure there are no orphans CSS variables when "common" and "themed" files are used together |
| 35 | + for (const mode of modes) { |
| 36 | + const { partialDefinitions: commonDefinitions, partialUsages: commonUsages } = extractAllCssVariables(allSources[mode].commonSource); |
| 37 | + const { partialDefinitions: themedDefinitions, partialUsages: themedUsages } = extractAllCssVariables(allSources[mode].themedSource); |
| 38 | + const allDefinitions = new Set([...commonDefinitions, ...themedDefinitions]); |
| 39 | + const allUsages = new Set([...commonUsages, ...themedUsages]); |
| 40 | + const undefinedVariables = [...allUsages].filter(usage => !allDefinitions.has(usage)); |
| 41 | + if (undefinedVariables.length > 0) { |
| 42 | + throw new Error(`❌ ${chalk.red.bold('ERROR')} - Generated "common/themed" token files for mode '${mode} contain CSS variables that not defined in any of the generated files: ${undefinedVariables.map((variable: string) => `\`--token-${variable}\``).join(', ')}`); |
| 43 | + } |
| 44 | + } |
| 45 | +} |
| 46 | + |
| 47 | +// regex for variable definition (`--token-***: ***`) and usage: (`var(--token-***)`) |
| 48 | +const varDefRegex = /--token-([a-zA-Z0-9-_]+)\s*:/g; |
| 49 | +const varUsageRegex = /var\(\s*--token-([a-zA-Z0-9-_]+)\s*\)/g; |
| 50 | + |
| 51 | +function extractAllCssVariables(source: string) { |
| 52 | + const cleanSource = stripCssComments(source); |
| 53 | + const partialDefinitions = []; |
| 54 | + const partialUsages = []; |
| 55 | + |
| 56 | + // find all definitions and usages in this file |
| 57 | + let match; |
| 58 | + while ((match = varDefRegex.exec(cleanSource))) { |
| 59 | + partialDefinitions.push(match[1]); |
| 60 | + } |
| 61 | + while ((match = varUsageRegex.exec(cleanSource))) { |
| 62 | + partialUsages.push(match[1]); |
| 63 | + } |
| 64 | + |
| 65 | + return { partialDefinitions, partialUsages }; |
| 66 | +} |
| 67 | + |
| 68 | +function stripCssComments(source: string) { |
| 69 | + return source.replace(/\/\*[\s\S]*?\*\//g, ''); |
| 70 | +} |
0 commit comments