diff --git a/__tests__/themeFunction.test.js b/__tests__/themeFunction.test.js index d795a44ddd84..4cfdc0800110 100644 --- a/__tests__/themeFunction.test.js +++ b/__tests__/themeFunction.test.js @@ -110,6 +110,61 @@ 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 tailwind theme.\nValid keys for "theme" are: "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 tailwind theme.\nValid keys for "colors" are: "blue", "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 tailwind theme.\n"colors.yellow" exists but is not an object' + ) +}) + test('array values are joined by default', () => { const input = ` .heading { font-family: theme('fontFamily.sans'); } diff --git a/src/lib/evaluateTailwindFunctions.js b/src/lib/evaluateTailwindFunctions.js index 80333456e79d..bf3f95e38eb5 100644 --- a/src/lib/evaluateTailwindFunctions.js +++ b/src/lib/evaluateTailwindFunctions.js @@ -1,6 +1,34 @@ import _ from 'lodash' import functions from 'postcss-functions' +function findClosestExistingPath(theme, path) { + const parts = _.toPath(path) + do { + parts.pop() + + if (_.hasIn(theme, parts)) break + } while (parts.length) + + return parts +} + +function buildError(root, theme, path) { + const closestPath = findClosestExistingPath(theme, path).join('.') || 'theme' + const closestValue = _.get(theme, closestPath, theme) + + let message = `"${path}" does not exist in your tailwind theme.\n` + + if (typeof closestValue === 'object') { + message += `Valid keys for "${closestPath}" are: ${Object.keys(closestValue) + .map(k => `"${k}"`) + .join(', ')}` + } else { + message += `"${closestPath}" exists but is not an object.` + } + + return root.error(message) +} + const themeTransforms = { fontSize(value) { return Array.isArray(value) ? value[0] : value @@ -12,16 +40,22 @@ function defaultTransform(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('.') - - return _.get(themeTransforms, themeSection, defaultTransform)(value) - }) + return root => + functions({ + functions: { + theme: (path, ...defaultValue) => { + const trimmedPath = _.trim(path, `'"`) + + if (!defaultValue.length && !_.hasIn(config.theme, trimmedPath)) { + throw buildError(root, config.theme, trimmedPath) + } + + return _.thru(_.get(config.theme, trimmedPath, defaultValue), value => { + const [themeSection] = trimmedPath.split('.') + + return _.get(themeTransforms, themeSection, defaultTransform)(value) + }) + }, }, - }, - }) + })(root) }