mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
131 lines
3.5 KiB
JavaScript
131 lines
3.5 KiB
JavaScript
import _ from 'lodash'
|
|
import functions from 'postcss-functions'
|
|
import didYouMean from 'didyoumean'
|
|
import transformThemeValue from '../util/transformThemeValue'
|
|
|
|
function findClosestExistingPath(theme, path) {
|
|
const parts = _.toPath(path)
|
|
do {
|
|
parts.pop()
|
|
|
|
if (_.hasIn(theme, parts)) break
|
|
} while (parts.length)
|
|
|
|
return parts.length ? parts : undefined
|
|
}
|
|
|
|
function pathToString(path) {
|
|
if (typeof path === 'string') return path
|
|
return path.reduce((acc, cur, i) => {
|
|
if (cur.includes('.')) return `${acc}[${cur}]`
|
|
return i === 0 ? cur : `${acc}.${cur}`
|
|
}, '')
|
|
}
|
|
|
|
function list(items) {
|
|
return items.map((key) => `'${key}'`).join(', ')
|
|
}
|
|
|
|
function listKeys(obj) {
|
|
return list(Object.keys(obj))
|
|
}
|
|
|
|
function validatePath(config, path, defaultValue) {
|
|
const pathString = Array.isArray(path) ? pathToString(path) : _.trim(path, `'"`)
|
|
const pathSegments = Array.isArray(path) ? path : _.toPath(pathString)
|
|
const value = _.get(config.theme, pathString, defaultValue)
|
|
|
|
if (typeof value === 'undefined') {
|
|
let error = `'${pathString}' does not exist in your theme config.`
|
|
const parentSegments = pathSegments.slice(0, -1)
|
|
const parentValue = _.get(config.theme, parentSegments)
|
|
|
|
if (_.isObject(parentValue)) {
|
|
const validKeys = Object.keys(parentValue).filter(
|
|
(key) => validatePath(config, [...parentSegments, key]).isValid
|
|
)
|
|
const suggestion = didYouMean(_.last(pathSegments), validKeys)
|
|
if (suggestion) {
|
|
error += ` Did you mean '${pathToString([...parentSegments, suggestion])}'?`
|
|
} else if (validKeys.length > 0) {
|
|
error += ` '${pathToString(parentSegments)}' has the following valid keys: ${list(
|
|
validKeys
|
|
)}`
|
|
}
|
|
} else {
|
|
const closestPath = findClosestExistingPath(config.theme, pathString)
|
|
if (closestPath) {
|
|
const closestValue = _.get(config.theme, closestPath)
|
|
if (_.isObject(closestValue)) {
|
|
error += ` '${pathToString(closestPath)}' has the following keys: ${listKeys(
|
|
closestValue
|
|
)}`
|
|
} else {
|
|
error += ` '${pathToString(closestPath)}' is not an object.`
|
|
}
|
|
} else {
|
|
error += ` Your theme has the following top-level keys: ${listKeys(config.theme)}`
|
|
}
|
|
}
|
|
|
|
return {
|
|
isValid: false,
|
|
error,
|
|
}
|
|
}
|
|
|
|
if (
|
|
!(
|
|
typeof value === 'string' ||
|
|
typeof value === 'number' ||
|
|
value instanceof String ||
|
|
value instanceof Number ||
|
|
Array.isArray(value)
|
|
)
|
|
) {
|
|
let error = `'${pathString}' was found but does not resolve to a string.`
|
|
|
|
if (_.isObject(value)) {
|
|
let validKeys = Object.keys(value).filter(
|
|
(key) => validatePath(config, [...pathSegments, key]).isValid
|
|
)
|
|
if (validKeys.length) {
|
|
error += ` Did you mean something like '${pathToString([...pathSegments, validKeys[0]])}'?`
|
|
}
|
|
}
|
|
|
|
return {
|
|
isValid: false,
|
|
error,
|
|
}
|
|
}
|
|
|
|
const [themeSection] = pathSegments
|
|
|
|
return {
|
|
isValid: true,
|
|
value: transformThemeValue(themeSection)(value),
|
|
}
|
|
}
|
|
|
|
export default function (config) {
|
|
return (root) =>
|
|
functions({
|
|
functions: {
|
|
theme: (path, ...defaultValue) => {
|
|
const { isValid, value, error } = validatePath(
|
|
config,
|
|
path,
|
|
defaultValue.length ? defaultValue : undefined
|
|
)
|
|
|
|
if (!isValid) {
|
|
throw root.error(error)
|
|
}
|
|
|
|
return value
|
|
},
|
|
},
|
|
})(root)
|
|
}
|