diff --git a/README.md b/README.md index 619dc506..fc3eb4d1 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,14 @@ ml-8 [2rem] / ml-10 [2.5rem] / ml-12 [3rem] / ml-16 [4rem] / ml-20 [5rem] / ml-2 ml-40 [10rem] / ml-48 [12rem] / ml-56 [14rem] / ml-64 [16rem] / ml-auto [auto] / ml-px [1px] ``` +**🖌️ Use the theme import to add values from your tailwind config** + +```js +import { theme, css } from 'twin.macro' + +const Input = () => +``` + **💥 Go important with a bang** - Add important to any class with a trailing bang! ```js diff --git a/src/logging.js b/src/logging.js index ade276f5..d5e86fdb 100644 --- a/src/logging.js +++ b/src/logging.js @@ -142,6 +142,43 @@ const errorSuggestions = properties => { return spaced(`${textNotFound}\n\n${suggestionText}`) } +const themeErrorNotString = ({ themeValue, input }) => { + const textNotFound = warning( + `${color.errorLight(input)} didn’t bring back a string theme value` + ) + const suggestionText = `Try adding one of these values after a dot:\n${formatSuggestions( + Object.entries(themeValue).map(([k, v]) => ({ + target: k, + value: typeof v === 'string' ? v : '...', + })) + )}` + + return spaced(`${textNotFound}\n\n${suggestionText}`) +} + +const themeErrorNotFound = ({ theme, input, trimInput }) => { + if (typeof theme === 'string') { + return spaced(logBadGood(input, trimInput)) + } + + const textNotFound = warning( + `${color.errorLight(input)} was not found in your theme` + ) + + if (!theme) { + return spaced(textNotFound) + } + + const suggestionText = `Try one of these values:\n${formatSuggestions( + Object.entries(theme).map(([k, v]) => ({ + target: k, + value: typeof v === 'string' ? v : '...', + })) + )}` + + return spaced(`${textNotFound}\n\n${suggestionText}`) +} + export { logNoVariant, logNoClass, @@ -152,4 +189,6 @@ export { debugPlugins, inOutPlugins, errorSuggestions, + themeErrorNotString, + themeErrorNotFound, } diff --git a/src/macro.js b/src/macro.js index 01fb9327..2b62e12e 100644 --- a/src/macro.js +++ b/src/macro.js @@ -13,6 +13,7 @@ import { updateStyledReferences, addStyledImport, } from './macro/styled' +import { handleThemeFunction } from './macro/theme' import { handleTwProperty, handleTwFunction } from './macro/tw' import getUserPluginData from './utils/getUserPluginData' import { debugPlugins } from './logging' @@ -104,6 +105,9 @@ const twinMacro = ({ babel: { types: t }, references, state, config }) => { addStyledImport({ program, t, styledImport, state }) } + // Theme import + handleThemeFunction({ references, t, state }) + // Auto add css prop for styled components if ( state.hasTwProp && diff --git a/src/macro/theme.js b/src/macro/theme.js new file mode 100644 index 00000000..f4bfbd48 --- /dev/null +++ b/src/macro/theme.js @@ -0,0 +1,83 @@ +import dlv from 'dlv' +import { replaceWithLocation, astify } from './../macroHelpers' +import { getTheme, assert } from './../utils' +import { + logGeneralError, + themeErrorNotString, + themeErrorNotFound, +} from './../logging' + +const getFunctionValue = path => { + if (path.parent.type !== 'CallExpression') return + + const parent = path.findParent(x => x.isCallExpression()) + if (!parent) return + + const argument = parent.get('arguments')[0] || '' + return { parent, input: argument.evaluate().value } +} + +const getTaggedTemplateValue = path => { + if (path.parent.type !== 'TaggedTemplateExpression') return + + const parent = path.findParent(x => x.isTaggedTemplateExpression()) + if (!parent) return + if (parent.node.tag.type !== 'Identifier') return + + return { parent, input: parent.get('quasi').evaluate().value } +} + +const normalizeThemeValue = foundValue => + Array.isArray(foundValue) + ? foundValue.join(', ') + : typeof foundValue === 'string' + ? foundValue.trim() + : foundValue + +const trimInput = themeValue => { + const arrayValues = themeValue.split('.').filter(Boolean) + if (arrayValues.length === 1) { + return arrayValues[0] + } + + return arrayValues.slice(0, -1).join('.') +} + +const handleThemeFunction = ({ references, t, state }) => { + if (!references.theme) return + + const theme = getTheme(state.config.theme) + + references.theme.forEach(path => { + const { input, parent } = + getTaggedTemplateValue(path) || getFunctionValue(path) + + if (input === '') { + return replaceWithLocation(parent, astify('', t)) + } + + assert(!parent || !input, () => + logGeneralError( + "The theme value doesn’t look right\n\nTry using it like this: theme`colors.black` or theme('colors.black')" + ) + ) + + const themeValue = dlv(theme(), input) + assert(!themeValue, () => + themeErrorNotFound({ + theme: input.includes('.') ? dlv(theme(), trimInput(input)) : theme(), + input, + trimInput: trimInput(input), + }) + ) + + const normalizedValue = normalizeThemeValue(themeValue) + assert(typeof normalizedValue !== 'string', () => + themeErrorNotString({ themeValue, input }) + ) + + return replaceWithLocation(parent, astify(normalizedValue, t)) + }) +} + +export { handleThemeFunction } diff --git a/src/macroHelpers.js b/src/macroHelpers.js index 233781c9..65ecf287 100644 --- a/src/macroHelpers.js +++ b/src/macroHelpers.js @@ -191,7 +191,14 @@ function replaceWithLocation(path, replacement) { return newPaths } -const validImports = new Set(['default', 'styled', 'css', 'TwStyle']) +const validImports = new Set([ + 'default', + 'styled', + 'css', + 'theme', + 'TwStyle', + 'ThemeStyle', +]) const validateImports = imports => { const unsupportedImport = Object.keys(imports).find( reference => !validImports.has(reference) @@ -206,7 +213,7 @@ const validateImports = imports => { }) assert(unsupportedImport, () => logGeneralError( - `Twin doesn't recognize { ${unsupportedImport} }\n\nTry one of these imports:\nimport tw, { styled, css } from 'twin.macro'` + `Twin doesn't recognize { ${unsupportedImport} }\n\nTry one of these imports:\nimport tw, { styled, css, theme } from 'twin.macro'` ) ) } diff --git a/types/index.d.ts b/types/index.d.ts index 54b500c8..1cecc57d 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -5,6 +5,10 @@ export interface TwStyle { [key: string]: string | number | TwStyle } +export interface ThemeStyle { + [key: string]: string | number | ThemeStyle +} + export type TemplateFn = ( strings: Readonly, ...values: readonly string[] @@ -12,6 +16,14 @@ export type TemplateFn = ( export type TwFn = TemplateFn +export type ThemeSearchFn = (...values: readonly string[]) => R +export type ThemeSearchTaggedFn = ( + strings: Readonly +) => R + +export type ThemeFn = ThemeSearchFn & + ThemeSearchTaggedFn + export type TwComponent = ( props: JSX.IntrinsicElements[K] ) => JSX.Element @@ -36,3 +48,6 @@ declare global { } } } + +declare const theme: ThemeFn +export { theme }