blob: e91f6ff680154a5f0d567b211a531adb96d123b3 [file]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { type AnyThemeConfig } from '@apache-superset/core/theme';
import { isValidTokenName } from './antdTokenNames';
/**
* Valid algorithm values that match backend ThemeMode enum.
* These correspond to Ant Design's built-in theme algorithms.
*/
const VALID_ALGORITHM_VALUES = new Set([
'default',
'dark',
'system',
'compact',
]);
export interface ValidationIssue {
tokenName: string;
severity: 'error' | 'warning';
message: string;
}
export interface ValidationResult {
valid: boolean; // false if ANY errors exist (warnings don't affect this)
errors: ValidationIssue[];
warnings: ValidationIssue[];
}
/**
* Validates theme structure and token names.
* - ERRORS block save/apply (invalid structure, empty themes)
* - WARNINGS allow save/apply but show in editor (unknown tokens, null values)
*
* This validation does NOT check token values - Ant Design handles that at runtime.
*/
export function validateTheme(themeConfig: AnyThemeConfig): ValidationResult {
const errors: ValidationIssue[] = [];
const warnings: ValidationIssue[] = [];
// ERROR: Null/invalid config
if (!themeConfig || typeof themeConfig !== 'object') {
errors.push({
tokenName: '_root',
severity: 'error',
message: 'Theme configuration must be a valid object',
});
return { valid: false, errors, warnings };
}
// ERROR: Empty theme (no tokens, no algorithm, no components)
const hasTokens =
themeConfig.token && Object.keys(themeConfig.token).length > 0;
const hasAlgorithm = Boolean(themeConfig.algorithm);
const hasComponents =
themeConfig.components && Object.keys(themeConfig.components).length > 0;
if (!hasTokens && !hasAlgorithm && !hasComponents) {
errors.push({
tokenName: '_root',
severity: 'error',
message:
'Theme cannot be empty. Add at least one token, algorithm, or component override.',
});
return { valid: false, errors, warnings };
}
// ERROR: token must be an object if present (null is also rejected by backend)
const rawToken = themeConfig.token;
if (rawToken !== undefined) {
if (
rawToken === null ||
typeof rawToken !== 'object' ||
Array.isArray(rawToken)
) {
errors.push({
tokenName: '_root',
severity: 'error',
message:
'Token configuration must be an object, not null, array, or primitive',
});
return { valid: false, errors, warnings };
}
}
const tokens = rawToken ?? {};
// ERROR: components must be an object if present (null is also rejected by backend)
const rawComponents = themeConfig.components;
if (rawComponents !== undefined) {
if (
rawComponents === null ||
typeof rawComponents !== 'object' ||
Array.isArray(rawComponents)
) {
errors.push({
tokenName: '_root',
severity: 'error',
message:
'Components configuration must be an object, not null, array, or primitive',
});
return { valid: false, errors, warnings };
}
}
// ERROR: algorithm must be a valid string or array of valid strings if present
// Valid values: "default", "dark", "system", "compact" (matches backend ThemeMode)
const rawAlgorithm = themeConfig.algorithm;
if (rawAlgorithm !== undefined) {
// Null is rejected by backend
if (rawAlgorithm === null) {
errors.push({
tokenName: '_root',
severity: 'error',
message: 'Algorithm cannot be null',
});
return { valid: false, errors, warnings };
}
// Must be string or array of strings
const isString = typeof rawAlgorithm === 'string';
const isStringArray =
Array.isArray(rawAlgorithm) &&
rawAlgorithm.every(a => typeof a === 'string');
if (!isString && !isStringArray) {
errors.push({
tokenName: '_root',
severity: 'error',
message:
'Algorithm must be a string or array of strings (e.g., "dark" or ["dark", "compact"])',
});
return { valid: false, errors, warnings };
}
// Validate algorithm values against allowed set
const algorithms = isString ? [rawAlgorithm] : (rawAlgorithm as string[]);
const invalidAlgorithms = algorithms.filter(
a => !VALID_ALGORITHM_VALUES.has(a),
);
if (invalidAlgorithms.length > 0) {
errors.push({
tokenName: '_root',
severity: 'error',
message: `Invalid algorithm value(s): "${invalidAlgorithms.join('", "')}". Valid values are: default, dark, system, compact`,
});
return { valid: false, errors, warnings };
}
}
Object.entries(tokens).forEach(([name, value]) => {
// Null/undefined check
if (value === null || value === undefined) {
warnings.push({
tokenName: name,
severity: 'warning',
message: `Token '${name}' has null/undefined value`,
});
return;
}
// Token name validation
if (!isValidTokenName(name)) {
warnings.push({
tokenName: name,
severity: 'warning',
message: `Unknown token '${name}' - may be ignored by Ant Design`,
});
}
});
return {
valid: errors.length === 0,
errors,
warnings,
};
}