tailwindcss/src/lib/evaluateTailwindFunctions.js
2020-10-19 11:32:22 -04:00

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)
}