Add theme helper function errors (#2615)
* feat: throw when theme does not have requested path (#1799) * Add suggestions to theme function errors Co-authored-by: Jason Quense <monastic.panic@gmail.com>
This commit is contained in:
parent
d8841ffa43
commit
5bf4699a35
4 changed files with 247 additions and 12 deletions
|
|
@ -110,6 +110,127 @@ test('an unquoted list is valid as a default value', () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('a missing root theme value throws', () => {
|
||||
const input = `
|
||||
.heading { color: theme('colours.gray.100'); }
|
||||
`
|
||||
|
||||
return expect(
|
||||
run(input, {
|
||||
theme: {
|
||||
colors: {
|
||||
yellow: '#f7cc50',
|
||||
},
|
||||
},
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
`'colours.gray.100' does not exist in your theme config. Your theme has the following top-level keys: 'colors'`
|
||||
)
|
||||
})
|
||||
|
||||
test('a missing nested theme property throws', () => {
|
||||
const input = `
|
||||
.heading { color: theme('colors.red'); }
|
||||
`
|
||||
|
||||
return expect(
|
||||
run(input, {
|
||||
theme: {
|
||||
colors: {
|
||||
blue: 'blue',
|
||||
yellow: '#f7cc50',
|
||||
},
|
||||
},
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
`'colors.red' does not exist in your theme config. 'colors' has the following valid keys: 'blue', 'yellow'`
|
||||
)
|
||||
})
|
||||
|
||||
test('a missing nested theme property with a close alternative throws with a suggestion', () => {
|
||||
const input = `
|
||||
.heading { color: theme('colors.yellw'); }
|
||||
`
|
||||
|
||||
return expect(
|
||||
run(input, {
|
||||
theme: {
|
||||
colors: {
|
||||
yellow: '#f7cc50',
|
||||
},
|
||||
},
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
`'colors.yellw' does not exist in your theme config. Did you mean 'colors.yellow'?`
|
||||
)
|
||||
})
|
||||
|
||||
test('a path through a non-object throws', () => {
|
||||
const input = `
|
||||
.heading { color: theme('colors.yellow.100'); }
|
||||
`
|
||||
|
||||
return expect(
|
||||
run(input, {
|
||||
theme: {
|
||||
colors: {
|
||||
yellow: '#f7cc50',
|
||||
},
|
||||
},
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
`'colors.yellow.100' does not exist in your theme config. 'colors.yellow' is not an object.`
|
||||
)
|
||||
})
|
||||
|
||||
test('a path which exists but is not a string throws', () => {
|
||||
const input = `
|
||||
.heading { color: theme('colors.yellow'); }
|
||||
`
|
||||
|
||||
return expect(
|
||||
run(input, {
|
||||
theme: {
|
||||
colors: {
|
||||
yellow: Symbol(),
|
||||
},
|
||||
},
|
||||
})
|
||||
).rejects.toThrowError(`'colors.yellow' was found but does not resolve to a string.`)
|
||||
})
|
||||
|
||||
test('a path which exists but is invalid throws', () => {
|
||||
const input = `
|
||||
.heading { color: theme('colors'); }
|
||||
`
|
||||
|
||||
return expect(
|
||||
run(input, {
|
||||
theme: {
|
||||
colors: {},
|
||||
},
|
||||
})
|
||||
).rejects.toThrowError(`'colors' was found but does not resolve to a string.`)
|
||||
})
|
||||
|
||||
test('a path which is an object throws with a suggested key', () => {
|
||||
const input = `
|
||||
.heading { color: theme('colors'); }
|
||||
`
|
||||
|
||||
return expect(
|
||||
run(input, {
|
||||
theme: {
|
||||
colors: {
|
||||
yellow: '#f7cc50',
|
||||
},
|
||||
},
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
`'colors' was found but does not resolve to a string. Did you mean something like 'colors.yellow'?`
|
||||
)
|
||||
})
|
||||
|
||||
test('array values are joined by default', () => {
|
||||
const input = `
|
||||
.heading { font-family: theme('fontFamily.sans'); }
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@
|
|||
"chalk": "^4.1.0",
|
||||
"color": "^3.1.3",
|
||||
"detective": "^5.2.0",
|
||||
"didyoumean": "^1.2.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
"html-tags": "^3.1.0",
|
||||
"lodash": "^4.17.20",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import _ from 'lodash'
|
||||
import functions from 'postcss-functions'
|
||||
import didYouMean from 'didyoumean'
|
||||
|
||||
const themeTransforms = {
|
||||
fontSize(value) {
|
||||
|
|
@ -11,17 +12,124 @@ function defaultTransform(value) {
|
|||
return Array.isArray(value) ? value.join(', ') : value
|
||||
}
|
||||
|
||||
export default function (config) {
|
||||
return functions({
|
||||
functions: {
|
||||
theme: (path, ...defaultValue) => {
|
||||
const trimmedPath = _.trim(path, `'"`)
|
||||
return _.thru(_.get(config.theme, trimmedPath, defaultValue), (value) => {
|
||||
const [themeSection] = trimmedPath.split('.')
|
||||
function findClosestExistingPath(theme, path) {
|
||||
const parts = _.toPath(path)
|
||||
do {
|
||||
parts.pop()
|
||||
|
||||
return _.get(themeTransforms, themeSection, defaultTransform)(value)
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
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: _.get(themeTransforms, themeSection, defaultTransform)(value),
|
||||
}
|
||||
}
|
||||
|
||||
export default function (config) {
|
||||
return (root) =>
|
||||
functions({
|
||||
functions: {
|
||||
theme: (path, ...defaultValue) => {
|
||||
return _.thru(
|
||||
validatePath(config, path, defaultValue.length ? defaultValue : undefined),
|
||||
({ isValid, value, error }) => {
|
||||
if (isValid) return value
|
||||
throw root.error(error)
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
})(root)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2158,6 +2158,11 @@ detective@^5.2.0:
|
|||
defined "^1.0.0"
|
||||
minimist "^1.1.1"
|
||||
|
||||
didyoumean@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.1.tgz#e92edfdada6537d484d73c0172fd1eba0c4976ff"
|
||||
integrity sha1-6S7f2tplN9SE1zwBcv0eugxJdv8=
|
||||
|
||||
diff-sequences@^26.5.0:
|
||||
version "26.5.0"
|
||||
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.5.0.tgz#ef766cf09d43ed40406611f11c6d8d9dd8b2fefd"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue