Skip to content

Instantly share code, notes, and snippets.

@OwenMelbz
Last active May 22, 2020 08:49
Show Gist options
  • Save OwenMelbz/af6c3ebbf0df728f3633468a5ce6e3b1 to your computer and use it in GitHub Desktop.
Save OwenMelbz/af6c3ebbf0df728f3633468a5ce6e3b1 to your computer and use it in GitHub Desktop.
Adds className support to React Native for Tailwind CSS Only
{
"plugins": ["./transformwind.js"]
}
  • Install tailwind-rn with yarn add tailwind-rn css postcss tailwindcss css-to-react-native.
  • Update your babel config to point to the plugin.

Usage

<View className="w-10 bg-red-400">
    xxx
</View>

Or if you don't want to use className you can use tailwind

<View tailwind="w-10 bg-red-400">
    xxx
</View>
// yarn add css postcss tailwindcss css-to-react-native
const fs = require('fs');
const css = require('css');
const postcss = require('postcss');
const tailwind = require('tailwindcss');
const cssToReactNative = require('css-to-react-native').default;
const remToPx = value => `${Number.parseFloat(value) * 16}px`;
const getStyles = rule => {
const styles = rule.declarations
.filter(({property, value}) => {
// Skip line-height utilities without units
if (property === 'line-height' && !value.endsWith('rem')) {
return false;
}
return true;
})
.map(({property, value}) => {
if (value.endsWith('rem')) {
return [property, remToPx(value)];
}
return [property, value];
});
return cssToReactNative(styles);
};
const supportedUtilities = [
// Flexbox
/^flex/,
/^items-/,
/^content-/,
/^justify-/,
/^self-/,
// Display
'hidden',
'overflow-hidden',
'overflow-visible',
'overflow-scroll',
// Position
'absolute',
'relative',
// Top, right, bottom, left
/^(inset-0|inset-x-0|inset-y-0)/,
/^(top|bottom|left|right)-0$/,
// Z Index
/^z-\d+$/,
// Padding
/^(p.?-\d+|p.?-px)/,
// Margin
/^-?(m.?-\d+|m.?-px)/,
// Width
/^w-(\d|\/)+|^w-px|^w-full/,
// Height
/^(h-\d+|h-px|h-full)/,
// Min/Max width/height
/^(min-w-|max-w-|min-h-0|min-h-full|max-h-full)/,
// Font size
/^text-/,
// Font style
/^(not-)?italic$/,
// Font weight
/^font-(hairline|thin|light|normal|medium|semibold|bold|extrabold|black)/,
// Letter spacing
/^tracking-/,
// Line height
/^leading-\d+/,
// Text align, color, opacity
/^text-/,
// Text transform
'uppercase',
'lowercase',
'capitalize',
'normal-case',
// Background color
/^bg-(transparent|black|white|gray|red|orange|yellow|green|teal|blue|indigo|purple|pink)/,
// Background opacity
/^bg-opacity-/,
// Border color, style, width, radius, opacity
/^(border|rounded)/,
// Opacity
/^opacity-/,
// Pointer events
/^pointer-events-/
];
const isUtilitySupported = utility => {
// Skip utilities with `currentColor` values
if (['border-current', 'text-current'].includes(utility)) {
return false;
}
for (const supportedUtility of supportedUtilities) {
if (typeof supportedUtility === 'string' && supportedUtility === utility) {
return true;
}
if (supportedUtility instanceof RegExp && supportedUtility.test(utility)) {
return true;
}
}
return false;
};
const build = ({ css: source }) => {
const {stylesheet} = css.parse(source);
// Mapping of Tailwind class names to React Native styles
const styles = {};
for (const rule of stylesheet.rules) {
if (rule.type === 'rule') {
for (const selector of rule.selectors) {
const utility = selector.replace(/^\./, '').replace('\\/', '/');
if (isUtilitySupported(utility)) {
styles[utility] = getStyles(rule);
}
}
}
}
// Additional styles that we're not able to parse correctly automatically
styles.underline = {textDecorationLine: 'underline'};
styles['line-through'] = {textDecorationLine: 'line-through'};
styles['no-underline'] = {textDecorationLine: 'none'};
return styles;
};
(async () => {
const postCssPlugins = [
tailwind()
];
const css = await postcss(...postCssPlugins).process(`
@tailwind base;
@tailwind components;
@tailwind utilities;
`, {
from: undefined
});
fs.writeFileSync('tailwind-styles.json', JSON.stringify(build(css), null, '\t'));
})();
const tailwind = require('tailwind-rn');
const convertClassNameIntoTailwindStyles = ({types: t}) => {
return {
visitor: {
JSXOpeningElement(path) {
let foundAttribute = false;
let styles = {};
path.get('attributes').forEach(attribute => {
// First we need to find the attributes called className or tailwind
if (attribute.node.name.name === 'className' || attribute.node.name.name === 'tailwind') {
// We compile the style using tailwind-rn
const compiledStyle = tailwind(attribute.node.value.value);
// We then have to generate the definitions for the AST
styles = Object.keys(compiledStyle).map(cssProperty => {
const value = compiledStyle[cssProperty];
return t.ObjectProperty(
t.Identifier(cssProperty),
(isNaN(value) ? t.StringLiteral(value) : t.NumericLiteral(value))
);
});
// We don't need the element anymore, so we remove it.
attribute.remove();
foundAttribute = true;
}
})
if (foundAttribute) {
// Now we need to merge any existing styles with the classes we've just generated.
path.get('attributes').forEach((attribute) => {
if (attribute.node.name.name === 'style') {
attribute.node.value.expression.properties.forEach(property => {
styles.push(property)
});
// Remove the old style attribute so it doesn't clash.
attribute.remove();
}
})
// We can now add our new style object with our compiled classes.
path.node.attributes.push(t.JSXAttribute(
t.JSXIdentifier('style'),
t.JSXExpressionContainer(t.ObjectExpression(styles))
));
}
},
},
}
}
module.exports = convertClassNameIntoTailwindStyles;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment