feat: options to control what is transformed

refactor: convert everything to typescript
This commit is contained in:
Mark Lawlor
2022-04-06 18:56:41 +10:00
parent 3d2212a8ca
commit eb4100a68f
75 changed files with 1411 additions and 1318 deletions

View File

@@ -4,12 +4,12 @@ Use [Tailwindcss](https://tailwindcss.com/) in your cross platform [React Native
> This library is currently under active development.
* :sparkles: native support for multiple platforms
* :sparkles: respects tailwind.config.js
* :sparkles: fast refresh compatible
* :sparkles: supports dark mode / media queries / arbitrary classes
* :sparkles: compatible with existing styles
* :sparkles: Server Side Rendering (SSR) on Web (including responsive styles)
- :sparkles: native support for multiple platforms
- :sparkles: respects tailwind.config.js
- :sparkles: fast refresh compatible
- :sparkles: supports dark mode / media queries / arbitrary classes
- :sparkles: compatible with existing styles
- :sparkles: Server Side Rendering (SSR) on Web (including responsive styles)
Already using another RN library for Tailwind? [Find out why you should switch.](https://github.com/marklawlor/tailwindcss-react-native/blob/main/docs/library-comparision.md)
@@ -19,15 +19,13 @@ Install the library
`npm install tailwindcss-react-native` or `yarn add tailwindcss-react-native`
Add `tailwindcss-react-native/babel` to your babel plugins
Add `tailwindcss-react-native/babel` to your babel plugins
```js
// babel.config.js
module.exports = {
plugins: [
'tailwindcss-react-native/babel'
],
}
plugins: ["tailwindcss-react-native/babel"],
};
```
Add the `TailwindProvider` to your application
@@ -51,7 +49,7 @@ This package has a peerDependency of `tailwindcss@3.x.x`. You can install it wit
Create a file (eg. `src/tailwindcss-react-native.d.ts`) and paste this line
```js
import "tailwindcss-react-native/types.d"
import "tailwindcss-react-native/types.d";
```
#### Web only
@@ -60,8 +58,6 @@ import "tailwindcss-react-native/types.d"
If using `{ platform: 'web' }` you will need to follow the follow the [TailwindCSS installation steps](https://tailwindcss.com/docs/installation) to include it's styles in the application.
## Usage
Simply add a `className` attribute to your existing `react-native` components
@@ -91,7 +87,6 @@ export function Test({ isBold, isUnderline }) {
}
```
## Options
Options can be provided via the babel config
@@ -99,17 +94,14 @@ Options can be provided via the babel config
```js
// babel.config.js
module.exports = {
plugins: [
['tailwindcss-react-native/babel', { platform: 'native' }]
],
}
plugins: [["tailwindcss-react-native/babel", { platform: "native" }]],
};
```
Prop | Values | Default | Description
----------|----------------------|----------|----------------------
platform | `native`, `web`, `native-inline`, `native-context` | `native` | Specifies how the className is transformed (see [platforms](https://github.com/marklawlor/tailwindcss-react-native/blob/main/docs/platforms.md)
tailwindConfig | Path relative to `cwd` | `tailwind.config.js` | Provide a custom `tailwind.config.js`. Useful for setting different settings per platform.
| Prop | Values | Default | Description |
| -------------- | -------------------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| platform | `native`, `web`, `native-inline`, `native-context` | `native` | Specifies how the className is transformed (see [platforms](https://github.com/marklawlor/tailwindcss-react-native/blob/main/docs/platforms.md) |
| tailwindConfig | Path relative to `cwd` | `tailwind.config.js` | Provide a custom `tailwind.config.js`. Useful for setting different settings per platform. |
## How it works
@@ -120,4 +112,4 @@ Under the hood, `tailwindcss-react-native` performs these general steps
1. Remove the `className` attribute and replace/merge it with the `style` attribute
1. Utilises a `react` hook for matching media queries.
See [the platforms documentation](https://github.com/marklawlor/tailwindcss-react-native/blob/main/docs/platforms.md) for a more detailed explaination)
See [the platforms documentation](https://github.com/marklawlor/tailwindcss-react-native/blob/main/docs/platforms.md) for a more detailed explaination)

View File

@@ -1,24 +1,28 @@
import path from "path";
import pluginTester from "babel-plugin-tester";
import testPlugin from "../babel";
import testPlugin from "../src/babel";
const babelOptions = {
plugins: ["@babel/plugin-syntax-jsx"],
};
pluginTester({
pluginName: "native-inline",
plugin: testPlugin,
fixtures: path.join(__dirname, "native-inline-fixtures"),
babelOptions: require("../babel.config.js"),
babelOptions,
});
pluginTester({
pluginName: "native-context",
plugin: testPlugin,
fixtures: path.join(__dirname, "native-context-fixtures"),
babelOptions: require("../babel.config.js"),
babelOptions,
});
pluginTester({
pluginName: "web",
plugin: testPlugin,
fixtures: path.join(__dirname, "web-fixtures"),
babelOptions: require("../babel.config.js"),
babelOptions,
});

View File

@@ -1,20 +0,0 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.Test = Test;
var _reactNative = require("react-native");
var _tailwindcssReactNative = require("tailwindcss-react-native");
var _src = require("../../../src");
var _jsxRuntime = require("react/jsx-runtime");
function Test() {
return (0, _jsxRuntime.jsx)(_src.TailwindProvider, {
styles: __tailwindStyles,
media: __tailwindMedia,
children: (0, _jsxRuntime.jsx)(_reactNative.Text, {
style: (0, _tailwindcssReactNative.__useParseTailwind)("font-bold"),
children: "Hello world!",
}),
});
}
const __tailwindStyles = _reactNative.StyleSheet.create({
"font-bold": { fontWeight: "700" },
});
const __tailwindMedia = {};

View File

@@ -0,0 +1,19 @@
import { StyleSheet } from "react-native";
import { __useParseTailwind } from "tailwindcss-react-native";
import { Text } from "react-native";
import { TailwindProvider } from "../../../src";
export function Test() {
return (
<TailwindProvider styles={__tailwindStyles} media={__tailwindMedia}>
<Text style={__useParseTailwind("font-bold")}>Hello world!</Text>
</TailwindProvider>
);
}
const __tailwindStyles = StyleSheet.create({
"font-bold": {
fontWeight: "700",
},
});
const __tailwindMedia = {};

View File

@@ -1,4 +1,3 @@
{
"tailwindConfigPath": "./__tests__/native-inline-fixtures/basic/tailwind.config.js",
"platform": "native-inline"
}

View File

@@ -1,21 +0,0 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.Test = Test;
var _tailwindcssReactNative = require("tailwindcss-react-native");
var _reactNative = require("react-native");
var _src = require("../../../src");
var _jsxRuntime = require("react/jsx-runtime");
function Test() {
return (0, _jsxRuntime.jsx)(_src.TailwindProvider, {
children: (0, _jsxRuntime.jsx)(_reactNative.Text, {
style: (0, _tailwindcssReactNative.__useParseTailwind)("font-bold", {
styles: __tailwindStyles,
media: __tailwindMedia,
}),
children: "Hello world!",
}),
});
}
const __tailwindStyles = _reactNative.StyleSheet.create({
"font-bold": { fontWeight: "700" },
});
const __tailwindMedia = {};

View File

@@ -0,0 +1,26 @@
import { StyleSheet } from "react-native";
import { __useParseTailwind } from "tailwindcss-react-native";
import { Text } from "react-native";
import { TailwindProvider } from "../../../src";
export function Test() {
return (
<TailwindProvider>
<Text
style={__useParseTailwind("font-bold", {
styles: __tailwindStyles,
media: __tailwindMedia,
})}
>
Hello world!
</Text>
</TailwindProvider>
);
}
const __tailwindStyles = StyleSheet.create({
"font-bold": {
fontWeight: "700",
},
});
const __tailwindMedia = {};

View File

@@ -1,3 +0,0 @@
module.exports = {
content: [`${__dirname}/*.{js,ts,jsx,tsx}`],
};

View File

@@ -1,4 +1,3 @@
{
"tailwindConfigPath": "./__tests__/native-inline-fixtures/conditionals/tailwind.config.js",
"platform": "native-inline"
}

View File

@@ -1,27 +0,0 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.Test = Test;
var _tailwindcssReactNative = require("tailwindcss-react-native");
var _reactNative = require("react-native");
var _src = require("../../../src");
var _jsxRuntime = require("react/jsx-runtime");
function Test(_ref) {
var isBold = _ref.isBold,
isUnderline = _ref.isUnderline;
var classNames = [];
if (isBold) classNames.push("font-bold");
if (isUnderline) classNames.push("underline");
return (0, _jsxRuntime.jsx)(_src.TailwindProvider, {
children: (0, _jsxRuntime.jsx)(_reactNative.Text, {
style: (0, _tailwindcssReactNative.__useParseTailwind)(
classNames.join(" "),
{ styles: __tailwindStyles, media: __tailwindMedia }
),
children: "Hello world!",
}),
});
}
const __tailwindStyles = _reactNative.StyleSheet.create({
"font-bold": { fontWeight: "700" },
underline: { textDecorationLine: "underline" },
});
const __tailwindMedia = {};

View File

@@ -0,0 +1,32 @@
import { StyleSheet } from "react-native";
import { __useParseTailwind } from "tailwindcss-react-native";
import { Text } from "react-native";
import { TailwindProvider } from "../../../src";
export function Test({ isBold, isUnderline }) {
const classNames = [];
if (isBold) classNames.push("font-bold");
if (isUnderline) classNames.push("underline");
return (
<TailwindProvider>
<Text
style={__useParseTailwind(classNames.join(" "), {
styles: __tailwindStyles,
media: __tailwindMedia,
})}
>
Hello world!
</Text>
</TailwindProvider>
);
}
const __tailwindStyles = StyleSheet.create({
"font-bold": {
fontWeight: "700",
},
underline: {
textDecorationLine: "underline",
},
});
const __tailwindMedia = {};

View File

@@ -1,3 +0,0 @@
module.exports = {
content: [`${__dirname}/*.{js,ts,jsx,tsx}`],
};

View File

@@ -1,4 +1,3 @@
{
"tailwindConfigPath": "./__tests__/native-inline-fixtures/empty-className/tailwind.config.js",
"platform": "native-inline"
}

View File

@@ -1,19 +0,0 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.Test = Test;
var _tailwindcssReactNative = require("tailwindcss-react-native");
var _reactNative = require("react-native");
var _src = require("../../../src");
var _jsxRuntime = require("react/jsx-runtime");
function Test() {
return (0, _jsxRuntime.jsx)(_src.TailwindProvider, {
children: (0, _jsxRuntime.jsx)(_reactNative.Text, {
style: (0, _tailwindcssReactNative.__useParseTailwind)("", {
styles: __tailwindStyles,
media: __tailwindMedia,
}),
children: "Hello world!",
}),
});
}
const __tailwindStyles = _reactNative.StyleSheet.create({});
const __tailwindMedia = {};

View File

@@ -0,0 +1,22 @@
import { StyleSheet } from "react-native";
import { __useParseTailwind } from "tailwindcss-react-native";
import { Text } from "react-native";
import { TailwindProvider } from "../../../src";
export function Test() {
return (
<TailwindProvider>
<Text
style={__useParseTailwind("", {
styles: __tailwindStyles,
media: __tailwindMedia,
})}
>
Hello world!
</Text>
</TailwindProvider>
);
}
const __tailwindStyles = StyleSheet.create({});
const __tailwindMedia = {};

View File

@@ -1,3 +0,0 @@
module.exports = {
content: [`${__dirname}/*.{js,ts,jsx,tsx}`],
};

View File

@@ -1,4 +1,3 @@
{
"tailwindConfigPath": "./__tests__/native-inline-fixtures/existing-style/tailwind.config.js",
"platform": "native-inline"
}

View File

@@ -1,25 +0,0 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.Test = Test;
var _tailwindcssReactNative = require("tailwindcss-react-native");
var _reactNative = require("react-native");
var _src = require("../../../src");
var _jsxRuntime = require("react/jsx-runtime");
function Test() {
return (0, _jsxRuntime.jsx)(_src.TailwindProvider, {
children: (0, _jsxRuntime.jsx)(_reactNative.Text, {
style: [
(0, _tailwindcssReactNative.__useParseTailwind)("font-bold", {
styles: __tailwindStyles,
media: __tailwindMedia,
}),
styles.test,
],
children: "Hello world!",
}),
});
}
var styles = _reactNative.StyleSheet.create({ test: { color: "blue" } });
const __tailwindStyles = _reactNative.StyleSheet.create({
"font-bold": { fontWeight: "700" },
});
const __tailwindMedia = {};

View File

@@ -0,0 +1,34 @@
import { StyleSheet } from "react-native";
import { __useParseTailwind } from "tailwindcss-react-native";
import { Text } from "react-native";
import { TailwindProvider } from "../../../src";
export function Test() {
return (
<TailwindProvider>
<Text
style={[
__useParseTailwind("font-bold", {
styles: __tailwindStyles,
media: __tailwindMedia,
}),
styles.test,
]}
>
Hello world!
</Text>
</TailwindProvider>
);
}
const styles = StyleSheet.create({
test: {
color: "blue",
},
});
const __tailwindStyles = StyleSheet.create({
"font-bold": {
fontWeight: "700",
},
});
const __tailwindMedia = {};

View File

@@ -1,3 +0,0 @@
module.exports = {
content: [`${__dirname}/*.{js,ts,jsx,tsx}`],
};

View File

@@ -13,5 +13,5 @@ export function Test() {
const styles = StyleSheet.create({
test: { color: "blue" },
test2: { color: "blue" },
test2: { color: "red" },
});

View File

@@ -1,4 +1,3 @@
{
"tailwindConfigPath": "./__tests__/native-inline-fixtures/existing-styles/tailwind.config.js",
"platform": "native-inline"
}

View File

@@ -1,29 +0,0 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.Test = Test;
var _tailwindcssReactNative = require("tailwindcss-react-native");
var _reactNative = require("react-native");
var _src = require("../../../src");
var _jsxRuntime = require("react/jsx-runtime");
function Test() {
return (0, _jsxRuntime.jsx)(_src.TailwindProvider, {
children: (0, _jsxRuntime.jsx)(_reactNative.Text, {
style: [
(0, _tailwindcssReactNative.__useParseTailwind)("font-bold", {
styles: __tailwindStyles,
media: __tailwindMedia,
}),
styles.test,
styles.test2,
],
children: "Hello world!",
}),
});
}
var styles = _reactNative.StyleSheet.create({
test: { color: "blue" },
test2: { color: "blue" },
});
const __tailwindStyles = _reactNative.StyleSheet.create({
"font-bold": { fontWeight: "700" },
});
const __tailwindMedia = {};

View File

@@ -0,0 +1,38 @@
import { StyleSheet } from "react-native";
import { __useParseTailwind } from "tailwindcss-react-native";
import { Text } from "react-native";
import { TailwindProvider } from "../../../src";
export function Test() {
return (
<TailwindProvider>
<Text
style={[
__useParseTailwind("font-bold", {
styles: __tailwindStyles,
media: __tailwindMedia,
}),
styles.test,
styles.test2,
]}
>
Hello world!
</Text>
</TailwindProvider>
);
}
const styles = StyleSheet.create({
test: {
color: "blue",
},
test2: {
color: "red",
},
});
const __tailwindStyles = StyleSheet.create({
"font-bold": {
fontWeight: "700",
},
});
const __tailwindMedia = {};

View File

@@ -1,3 +0,0 @@
module.exports = {
content: [`${__dirname}/*.{js,ts,jsx,tsx}`],
};

View File

@@ -1,4 +0,0 @@
{
"tailwindConfigPath": "./__tests__/native-inline-fixtures/ignores-invalid-classes/tailwind.config.js",
"platform": "native-inline"
}

View File

@@ -1,19 +0,0 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.Test = Test;
var _tailwindcssReactNative = require("tailwindcss-react-native");
var _reactNative = require("react-native");
var _src = require("../../../src");
var _jsxRuntime = require("react/jsx-runtime");
function Test() {
return (0, _jsxRuntime.jsx)(_src.TailwindProvider, {
children: (0, _jsxRuntime.jsx)(_reactNative.Text, {
style: (0, _tailwindcssReactNative.__useParseTailwind)(
"grid grid-cols-3",
{ styles: __tailwindStyles, media: __tailwindMedia }
),
children: "Hello world!",
}),
});
}
const __tailwindStyles = _reactNative.StyleSheet.create({});
const __tailwindMedia = {};

View File

@@ -1,3 +0,0 @@
module.exports = {
content: [`${__dirname}/*.{js,ts,jsx,tsx}`],
};

View File

@@ -1,13 +0,0 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.Test = Test;
var _reactNative = require("react-native");
var _src = require("../../../src");
var _jsxRuntime = require("react/jsx-runtime");
function Test() {
return (0, _jsxRuntime.jsx)(_src.TailwindProvider, {
children: (0, _jsxRuntime.jsx)(_reactNative.Text, {
style: { $$css: true, tailwindcssReactNative: "font-bold" },
children: "Hello world!",
}),
});
}

View File

@@ -1,10 +1,16 @@
import { Text } from "react-native";
import { TailwindProvider } from "../../../src";
export function Test() {
return (
<TailwindProvider>
<Text className="grid grid-cols-3">Hello world!</Text>
<Text
style={{
$$css: true,
tailwindcssReactNative: "font-bold",
}}
>
Hello world!
</Text>
</TailwindProvider>
);
}

View File

@@ -1,19 +1,17 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.Test = Test;
var _reactNative = require("react-native");
var _src = require("../../../src");
var _jsxRuntime = require("react/jsx-runtime");
function Test() {
return (0, _jsxRuntime.jsxs)(_src.TailwindProvider, {
children: [
(0, _jsxRuntime.jsx)(_reactNative.Text, {
style: { $$css: true, tailwindcssReactNative: "font-bold" },
children: "Hello world!",
}),
(0, _jsxRuntime.jsx)("div", {
className: "text-white",
children: "Should be untransformed",
}),
],
});
import { Text } from "react-native";
import { TailwindProvider } from "../../../src";
export function Test() {
return (
<TailwindProvider>
<Text
style={{
$$css: true,
tailwindcssReactNative: "font-bold",
}}
>
Hello world!
</Text>
<div className="text-white">Should be untransformed</div>
</TailwindProvider>
);
}

View File

@@ -1,16 +0,0 @@
module.exports = {
presets: [
[
"module:metro-react-native-babel-preset",
{ useTransformReactJSXExperimental: true },
],
],
plugins: [
[
"@babel/plugin-transform-react-jsx",
{
runtime: "automatic",
},
],
],
};

1
babel.js Normal file
View File

@@ -0,0 +1 @@
module.exports = require("./dist/babel").default;

View File

@@ -1,16 +0,0 @@
const web = require("./web");
const native = require("./native");
const platformPlugins = {
web,
native,
"native-context": native,
"native-inline": native,
};
const convertClassNameIntoTailwindStyles = (...args) => {
const [, { platform = "native" }] = args;
return platformPlugins[platform](...args);
};
module.exports = convertClassNameIntoTailwindStyles;

View File

@@ -1,100 +0,0 @@
const getTailwindConfig = require("./utils/get-tailwind-config");
const transformClassNames = require("./utils/transform-class-names");
const processStyles = require("./utils/process-styles");
const appendVariables = require("./utils/babel-variables");
const { appendImport, hasImport } = require("./utils/babel-imports");
const convertClassNameIntoTailwindStyles = (
babelConfig,
{ tailwindConfigPath },
cwd
) => {
const { types: t } = babelConfig;
const tailwindConfig = getTailwindConfig(cwd, tailwindConfigPath);
const { styles, media } = processStyles(babelConfig, tailwindConfig);
const hasClassNames = new Set();
const hasUseParseTailwind = new Set();
const hasStyleSheetImport = new Set();
const hasProvider = new Set();
return {
visitor: {
ImportDeclaration(path, state) {
const { filename } = state.file.opts;
if (hasImport(path, "__useParseTailwind", "tailwindcss-react-native")) {
hasUseParseTailwind.add(filename);
}
if (hasImport(path, "StyleSheet", "react-native")) {
hasStyleSheetImport.add(filename);
}
},
JSXOpeningElement(path, state) {
const { filename } = state.file.opts;
classNames = transformClassNames(babelConfig, path, {
inlineStyles: false,
});
if (classNames) {
hasClassNames.add(filename);
}
if (path.node.name.name === "TailwindProvider") {
hasProvider.add(filename);
path.node.attributes.push(
t.JSXAttribute(
t.JSXIdentifier("styles"),
t.JSXExpressionContainer(t.identifier("__tailwindStyles"))
)
);
path.node.attributes.push(
t.JSXAttribute(
t.JSXIdentifier("media"),
t.JSXExpressionContainer(t.identifier("__tailwindMedia"))
)
);
}
},
Program: {
exit(path, state) {
const {
node: { body },
} = path;
const { filename } = state.file.opts;
// If there are classNames, but there is no import - add the import
if (
hasClassNames.has(filename) &&
!hasUseParseTailwind.has(filename)
) {
appendImport(
t,
body,
"__useParseTailwind",
"tailwindcss-react-native"
);
}
// If there is no provider - then our work here is done
if (!hasProvider.has(filename)) {
return;
}
if (!hasStyleSheetImport.has(filename)) {
appendImport(t, body, "StyleSheet", "react-native");
}
appendVariables(body, styles, media);
},
},
},
};
};
module.exports = convertClassNameIntoTailwindStyles;

View File

@@ -1,11 +0,0 @@
const contextPlugin = require("./context");
const inlinePlugin = require("./inline");
const convertClassNameIntoTailwindStyles = (babelConfig, pluginConfig, cwd) => {
const { platform } = pluginConfig;
return platform === "native-context" || process.env.NODE_ENV === "production"
? contextPlugin(babelConfig, pluginConfig, cwd)
: inlinePlugin(babelConfig, pluginConfig, cwd);
};
module.exports = convertClassNameIntoTailwindStyles;

View File

@@ -1,101 +0,0 @@
const { relative } = require("path");
const transformClassNames = require("./utils/transform-class-names");
const processStyles = require("./utils/process-styles");
const getTailwindConfig = require("./utils/get-tailwind-config");
const appendVariables = require("./utils/babel-variables");
const { appendImport, hasImport } = require("./utils/babel-imports");
/**
* While in development mode, this plugin applies this transformation
*
* In: `<Component className="text-dark" styles={styles.myStyles}>`
* Out:
*
* ```
* import { __useParseTailwind } from 'tailwind-react-native'
* import { StyleSheet } from 'react-native'
* <Component styles={[__useParseTailwind("text-dark", { styles: __tailwindStyles, media: __tailwindMedia), styles.myStyles]}>
* const __tailwindStyles = StyleSheet.create(<Compiled styles>)
* const __tailwindMedia = <Compiled media rules>
* ```
*/
const convertClassNameIntoTailwindStyles = (
babelConfig,
{ tailwindConfigPath },
cwd
) => {
const { types: t } = babelConfig;
const tailwindConfig = getTailwindConfig(cwd, tailwindConfigPath);
const hasClassNames = new Set();
const hasUseParseTailwind = new Set();
const hasStyleSheetImport = new Set();
return {
visitor: {
ImportDeclaration(path, state) {
const { filename } = state.file.opts;
if (hasImport(path, "__useParseTailwind", "tailwindcss-react-native")) {
hasUseParseTailwind.add(filename);
}
if (hasImport(path, "StyleSheet", "react-native")) {
hasStyleSheetImport.add(filename);
}
},
JSXOpeningElement(path, state) {
const { filename } = state.file.opts;
const classNames = transformClassNames(babelConfig, path, {
inlineStyles: true,
});
if (classNames) {
hasClassNames.add(filename);
}
},
Program: {
exit(path, state) {
const {
node: { body },
} = path;
const { filename, root } = state.file.opts;
// There are no classNames so skip this file
if (!hasClassNames.has(filename)) {
return;
}
if (!hasStyleSheetImport.has(filename)) {
appendImport(t, body, "StyleSheet", "react-native");
}
if (!hasUseParseTailwind.has(filename)) {
appendImport(
t,
body,
"__useParseTailwind",
"tailwindcss-react-native"
);
}
/**
* Override tailwind to only process the classnames in this file
*/
const { styles, media } = processStyles(babelConfig, {
...tailwindConfig,
// Make sure its relative to the tailwind.config.js
content: [relative(root, filename)],
});
appendVariables(body, styles, media);
},
},
},
};
};
module.exports = convertClassNameIntoTailwindStyles;

View File

@@ -1,28 +0,0 @@
function appendImport(t, body, variable, source) {
body.unshift(
t.importDeclaration(
[t.importSpecifier(t.identifier(variable), t.identifier(variable))],
t.stringLiteral(source)
)
)
}
function hasImport(path, variable, source) {
let match = false
if (path.node.source.value === source) {
path.traverse({
Identifier(path) {
if (path.key === 'local' && path.node.name === variable) {
match = true
}
},
})
}
return match
}
module.exports = {
appendImport,
hasImport,
}

View File

@@ -1,17 +0,0 @@
const template = require('@babel/template').default
const variableAst = template(`
const __tailwindStyles = StyleSheet.create(STYLES);
const __tailwindMedia = MEDIA;
`)
function appendVariables(body, styles, media) {
variableAst({
STYLES: styles,
MEDIA: media,
}).forEach((ast) => {
body.push(ast)
})
}
module.exports = appendVariables

View File

@@ -1,89 +0,0 @@
const cssToReactNative = require("css-to-react-native").default;
const normaliseSelector = require("../../../dist/shared/selector");
const isValidStyle = require("./is-valid-style");
/** @typedef {import('react-native').ViewStyle | import('react-native').TextStyle | import('react-native').ImageStyle} Style */
/** @typedef {{ selector: string, media: string[], rules: Style, rulesAst: any }} CssRule */
/**
* Flattens StyleRules from the 'css' package
* - spreads selectors into multiple entries
* - flattens media rules so they are not nested
* - flattens styles to be react-native style objects
* @param {import('@types/css').StyleRules["rules"]} cssRules
* @param {{ important?: string }} options
* @param {Array<string> | undefined} media
* @returns {Array<CssRule>}
*/
function flattenRules(t, cssRules, options, media = []) {
return cssRules.flatMap((cssRule) => {
if (cssRule.type === "media") {
return flattenRules(t, cssRule.rules, options, [
...new Set([...media, cssRule.media]),
]);
} else if (cssRule.type === "rule") {
const declarationRuleTuples = [];
const invalidStyleProps = [];
for (const { type, property, value } of cssRule.declarations || []) {
if (type !== "declaration") {
continue;
}
declarationRuleTuples.push([property, value]);
}
if (declarationRuleTuples.length === 0) {
return [];
}
const rules = Object.fromEntries(
Object.entries(cssToReactNative(declarationRuleTuples)).filter(
([prop, value]) => {
if (isValidStyle(prop, value)) {
return true;
} else {
invalidStyleProps.push(prop);
return false;
}
}
)
);
if (
process.env.NODE_ENV !== "development" &&
process.env.NODE_ENV !== "test"
) {
if (invalidStyleProps.length > 0) {
console.warn(
`Selectors ${cssRule.selectors} use invalid styles ${invalidStyleProps}`
);
console.warn(
`Either remove these selectors or change to file to be platform specific (eg compoent.web.js)`
);
}
}
if (Object.keys(rules).length === 0) {
return [];
}
/** @type {Array<CssRule>} */
const selectors = (cssRule.selectors || []).map((selectorDirty) => {
const selector = normaliseSelector(selectorDirty, options);
return {
selector,
media,
rules,
};
});
return selectors;
} else {
return [];
}
});
}
module.exports = flattenRules;

View File

@@ -1,91 +0,0 @@
function transformClassNames({ types: t }, path, { inlineStyles }) {
try {
let classNames;
let existingStyles;
/**
* Find the className attribute
* - Save its value
* - Remove it as react-native components will ignore it
*/
for (const attribute of path.get("attributes")) {
if (attribute.node.name?.name === "className") {
if (t.isStringLiteral(attribute.node.value)) {
classNames = attribute.node.value;
} else {
classNames = attribute.node.value.expression;
}
attribute.remove();
break;
}
}
/**
* If classNames is empty or does not exist then exit
*/
if (!classNames) {
return false;
}
/**
* Find the styles attribute
* - Save its value
* - Remove it, as we're going to reconstruct it
*/
for (const attribute of path.get("attributes")) {
if (attribute.node.name?.name === "style") {
existingStyles = attribute.node.value.expression;
attribute.remove();
break;
}
}
/**
* If there are existing styles we need to merge them
*
* Classnames have lower specificity than inline styles
* so they should always be first
*/
const callExpressionArguments = inlineStyles
? [
classNames,
t.objectExpression([
t.ObjectProperty(
t.Identifier("styles"),
t.Identifier("__tailwindStyles")
),
t.ObjectProperty(
t.Identifier("media"),
t.Identifier("__tailwindMedia")
),
]),
]
: [classNames];
const callExpression = t.callExpression(
t.identifier("__useParseTailwind"),
callExpressionArguments
);
let newStyles;
if (t.isMemberExpression(existingStyles)) {
newStyles = t.JSXExpressionContainer(
t.arrayExpression([callExpression, existingStyles])
);
} else if (t.isArrayExpression(existingStyles)) {
existingStyles.elements.unshift(callExpression);
newStyles = t.JSXExpressionContainer(existingStyles);
} else {
newStyles = t.JSXExpressionContainer(callExpression);
}
path.node.attributes.push(
t.JSXAttribute(t.JSXIdentifier("style"), newStyles)
);
return true;
} catch (error) {
throw error;
}
}
module.exports = transformClassNames;

View File

@@ -1,97 +0,0 @@
const convertClassNameIntoTailwindStyles = ({ types: t }) => {
return {
visitor: {
JSXOpeningElement(path) {
try {
let classNames;
let existingStyles;
const name = path.node.name.name || path.node.name.object?.name;
const firstCharOfName = name[0];
// Ignore elements that start in lower case
if (
firstCharOfName &&
firstCharOfName === firstCharOfName.toLowerCase()
) {
return;
}
/**
* Find the className attribute
* - Save its value
* - Remove it as react-native components will ignore it
*/
for (const attribute of path.get("attributes")) {
if (attribute.node.name?.name === "className") {
if (t.isStringLiteral(attribute.node.value)) {
classNames = attribute.node.value;
} else {
classNames = attribute.node.value.expression;
}
attribute.remove();
break;
}
}
/**
* If classNames is empty or does not exist then exit
*/
if (!classNames) {
return;
}
/**
* Find the styles attribute
* - Save its value
* - Remove it, as we're going to reconstruct it
*/
for (const attribute of path.get("attributes")) {
if (attribute.node.name?.name === "style") {
existingStyles = attribute.node.value.expression;
attribute.remove();
break;
}
}
/**
* Convert the classNames into a compiled style object
*/
let newStyles = t.objectExpression([
t.ObjectProperty(t.Identifier("$$css"), t.BooleanLiteral(true)),
t.ObjectProperty(
t.Identifier("tailwindcssReactNative"),
classNames
),
]);
/**
* If there are existing styles we need to merge them
*
* Classnames have lower specificity than inline styles
* so they should always be first
*/
if (t.isObjectExpression(existingStyles)) {
newStyles = t.JSXExpressionContainer(
t.arrayExpression([newStyles, existingStyles])
);
} else if (t.isArrayExpression(existingStyles)) {
existingStyles.elements.unshift(newStyles);
newStyles = t.JSXExpressionContainer(existingStyles);
} else {
newStyles = t.JSXExpressionContainer(newStyles);
}
path.node.attributes.push(
t.JSXAttribute(t.JSXIdentifier("style"), newStyles)
);
} catch (error) {
throw error;
}
},
},
};
};
module.exports = convertClassNameIntoTailwindStyles;

View File

@@ -1,25 +1,25 @@
## Overview
Ultimately all these libraries achieve mostly the same result. The difference is either philosophical or implementation details.
Ultimately all these libraries achieve mostly the same result. The difference is either philosophical or implementation details.
These are my notes I made before I created `tailwindcss-react-native` but they also help explain some of the differences.
## tailwind-rn
## tailwind-rn
https://github.com/vadimdemedes/tailwind-rn
`tailwind-rn` requires you to manually run two extra processes while developing, `tailwind-cli` and `tailwind-rn`. These processes the styles and stores them via React context. This method has a couple of flaws
`tailwind-rn` requires you to manually run two extra processes while developing, `tailwind-cli` and `tailwind-rn`. These processes the styles and stores them via React context. This method has a couple of flaws
* The processes may run slower than your web application causing warnings/delays https://github.com/vadimdemedes/tailwind-rn/issues/154
* Requires custom setup of editor/IDE plugins
* Rerenders all components when a style has changed
* Does not support responsive SSR for web (cannot apply varients until hydration)
- The processes may run slower than your web application causing warnings/delays https://github.com/vadimdemedes/tailwind-rn/issues/154
- Requires custom setup of editor/IDE plugins
- Rerenders all components when a style has changed
- Does not support responsive SSR for web (cannot apply varients until hydration)
## react-native-tailwindcss
https://github.com/TVke/react-native-tailwindcss
* Same issues as `tailwind-rn`
- Same issues as `tailwind-rn`
## react-native-styled.macro
@@ -27,37 +27,37 @@ https://github.com/z0al/react-native-styled.macro
Uses babel macros to compile Tailwind selectors to RN Styles.
* Does not use Tailwind to compile styles
* Introduces a new API. Doesn't provide out of box support for varient values
* Uses a custom config file
- Does not use Tailwind to compile styles
- Introduces a new API. Doesn't provide out of box support for varient values
- Uses a custom config file
## react-native-tailwind
https://github.com/MythicalFish/react-native-tailwind
* Only works with it's exported components
- Only works with it's exported components
## tailwind-react-native
https://github.com/ajsmth/tailwind-react-native
* Same issues as `tailwind-rn`
* Introduces a new API. Doesn't provide out of box support for varient values
- Same issues as `tailwind-rn`
- Introduces a new API. Doesn't provide out of box support for varient values
## react-native-tailwind-classnames
https://github.com/leobauza/react-native-tailwind-classnames
* Only works with StyledComponents
- Only works with StyledComponents
## react-native-tailwind-style
https://github.com/etc-tiago/react-native-tailwind-style
* Same issues as `tailwind-rn`
- Same issues as `tailwind-rn`
## tailwind-react-native-classnames
https://github.com/jaredh159/tailwind-react-native-classnames
* Same issues as `tailwind-rn`
- Same issues as `tailwind-rn`

View File

@@ -8,12 +8,11 @@ The `native` platform switches platform based upon the `NODE_ENV` environment va
The platform best suited for production environments. It compiles the entire applications styles and produces the smallest possible output. It shares these styles to components via a React context.
Feature | Included
------ | -------------
Small output | :heavy_check_mark:
Hot module reload | :x:
Requires external tooling | :x:
| Feature | Included |
| ------------------------- | ------------------ |
| Small output | :heavy_check_mark: |
| Hot module reload | :x: |
| Requires external tooling | :x: |
```diff
- import { Text } from "react-native"
@@ -39,11 +38,11 @@ export function Test() {
The platform best suited for development environments. Produces larger output but works with hot-reload. Each file will generate it's own styles and provide them inline.
Feature | Included
------ | -------------
Small output | :x:
Hot module reload | :heavy_check_mark:
Requires external tooling | :x:
| Feature | Included |
| ------------------------- | ------------------ |
| Small output | :x: |
| Hot module reload | :heavy_check_mark: |
| Requires external tooling | :x: |
```diff
- import { Text } from "react-native"
@@ -69,11 +68,11 @@ The platform to use when using `react-native-web`. It leaves the className attri
Relies on external tooling for production minification.
Feature | Included
------ | -------------
Small output | :heavy_check_mark:
Hot module reload | :heavy_check_mark:
Requires external tooling | :heavy_check_mark:
| Feature | Included |
| ------------------------- | ------------------ |
| Small output | :heavy_check_mark: |
| Hot module reload | :heavy_check_mark: |
| Requires external tooling | :heavy_check_mark: |
```diff
import { Text } from "react-native"

View File

@@ -1,6 +1,7 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: "react-native",
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
preset: "ts-jest",
testEnvironment: "node",
testPathIgnorePatterns: [
"/node_modules/",
"/__tests__/native-context-fixtures/",

542
package-lock.json generated
View File

@@ -1,14 +1,13 @@
{
"name": "tailwindcss-react-native",
"version": "0.0.15",
"version": "0.0.17",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "tailwindcss-react-native",
"version": "0.0.15",
"version": "0.0.17",
"dependencies": {
"@babel/template": "^7.16.7",
"@react-native-community/hooks": "^2.8.1",
"babel-literal-to-ast": "^2.1.0",
"css": "^3.0.0",
@@ -20,7 +19,8 @@
},
"devDependencies": {
"@babel/core": "^7.17.8",
"@babel/preset-typescript": "^7.16.7",
"@babel/plugin-syntax-jsx": "^7.16.7",
"@babel/types": "^7.17.0",
"@types/css": "^0.0.33",
"@types/css-mediaquery": "^0.1.1",
"@types/css-to-react-native": "^3.0.0",
@@ -30,12 +30,11 @@
"@types/tailwindcss": "^3.0.10",
"babel-plugin-tester": "^10.1.0",
"jest": "^27.5.1",
"metro-react-native-babel-preset": "^0.70.0",
"postcss": "^8.4.12",
"prettier": "^2.6.1",
"react-native": "^0.68.0",
"react-native-web": "^0.17.7",
"tailwindcss": "^3.0.23",
"ts-jest": "^27.1.4",
"typescript": "^4.6.3"
},
"peerDependencies": {
@@ -527,23 +526,6 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/plugin-proposal-async-generator-functions": {
"version": "7.16.8",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.8.tgz",
"integrity": "sha512-71YHIvMuiuqWJQkebWJtdhQTfd4Q4mF76q2IX37uZPkG9+olBxsX+rH1vkhFto4UeJZ9dPY2s+mDvhDm1u2BGQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.16.7",
"@babel/helper-remap-async-to-generator": "^7.16.8",
"@babel/plugin-syntax-async-generators": "^7.8.4"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-proposal-class-properties": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.7.tgz",
@@ -3758,15 +3740,6 @@
"integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=",
"dev": true
},
"node_modules/array-find-index": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
"integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/array-map": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz",
@@ -4327,6 +4300,18 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/bs-logger": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
"integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==",
"dev": true,
"dependencies": {
"fast-json-stable-stringify": "2.x"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/bser": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
@@ -4921,25 +4906,6 @@
"node": ">=4"
}
},
"node_modules/create-react-class": {
"version": "15.7.0",
"resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.7.0.tgz",
"integrity": "sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==",
"dev": true,
"dependencies": {
"loose-envify": "^1.3.1",
"object-assign": "^4.1.1"
}
},
"node_modules/cross-fetch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
"integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
"dev": true,
"dependencies": {
"node-fetch": "2.6.7"
}
},
"node_modules/cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
@@ -4983,16 +4949,6 @@
"node": ">=4"
}
},
"node_modules/css-in-js-utils": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz",
"integrity": "sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA==",
"dev": true,
"dependencies": {
"hyphenate-style-name": "^1.0.2",
"isobject": "^3.0.1"
}
},
"node_modules/css-mediaquery": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz",
@@ -5797,36 +5753,6 @@
"bser": "2.1.1"
}
},
"node_modules/fbjs": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.4.tgz",
"integrity": "sha512-ucV0tDODnGV3JCnnkmoszb5lf4bNpzjv80K41wd4k798Etq+UYD0y0TIfalLjZoKgjive6/adkRnszwapiDgBQ==",
"dev": true,
"dependencies": {
"cross-fetch": "^3.1.5",
"fbjs-css-vars": "^1.0.0",
"loose-envify": "^1.0.0",
"object-assign": "^4.1.0",
"promise": "^7.1.1",
"setimmediate": "^1.0.5",
"ua-parser-js": "^0.7.30"
}
},
"node_modules/fbjs-css-vars": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz",
"integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==",
"dev": true
},
"node_modules/fbjs/node_modules/promise": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"dev": true,
"dependencies": {
"asap": "~2.0.3"
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -6366,12 +6292,6 @@
"node": ">=10.17.0"
}
},
"node_modules/hyphenate-style-name": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
"integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==",
"dev": true
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -6484,15 +6404,6 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/inline-style-prefixer": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-6.0.1.tgz",
"integrity": "sha512-AsqazZ8KcRzJ9YPN1wMH2aNM7lkWQ8tSPrW5uDk1ziYwiAPWSZnUsC7lfZq+BDqLqz0B4Pho5wscWcJzVvRzDQ==",
"dev": true,
"dependencies": {
"css-in-js-utils": "^2.0.0"
}
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@@ -10018,6 +9929,12 @@
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
"dev": true
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
"dev": true
},
"node_modules/lodash.mergewith": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
@@ -10102,6 +10019,12 @@
"semver": "bin/semver"
}
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"node_modules/makeerror": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
@@ -10314,55 +10237,6 @@
"uglify-es": "^3.1.9"
}
},
"node_modules/metro-react-native-babel-preset": {
"version": "0.70.0",
"resolved": "https://registry.npmjs.org/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.70.0.tgz",
"integrity": "sha512-MoOK5/rdDE2bABA+KpbXV6w0Q96sZeZiE9Ct89NYp9nPwXIaY7ylulLsjW3+rT6BdecKuNPUVwvtO0vYIQwvRw==",
"dev": true,
"dependencies": {
"@babel/core": "^7.14.0",
"@babel/plugin-proposal-async-generator-functions": "^7.0.0",
"@babel/plugin-proposal-class-properties": "^7.0.0",
"@babel/plugin-proposal-export-default-from": "^7.0.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0",
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
"@babel/plugin-proposal-optional-catch-binding": "^7.0.0",
"@babel/plugin-proposal-optional-chaining": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-syntax-export-default-from": "^7.0.0",
"@babel/plugin-syntax-flow": "^7.2.0",
"@babel/plugin-syntax-nullish-coalescing-operator": "^7.0.0",
"@babel/plugin-syntax-optional-chaining": "^7.0.0",
"@babel/plugin-transform-arrow-functions": "^7.0.0",
"@babel/plugin-transform-async-to-generator": "^7.0.0",
"@babel/plugin-transform-block-scoping": "^7.0.0",
"@babel/plugin-transform-classes": "^7.0.0",
"@babel/plugin-transform-computed-properties": "^7.0.0",
"@babel/plugin-transform-destructuring": "^7.0.0",
"@babel/plugin-transform-exponentiation-operator": "^7.0.0",
"@babel/plugin-transform-flow-strip-types": "^7.0.0",
"@babel/plugin-transform-function-name": "^7.0.0",
"@babel/plugin-transform-literals": "^7.0.0",
"@babel/plugin-transform-modules-commonjs": "^7.0.0",
"@babel/plugin-transform-parameters": "^7.0.0",
"@babel/plugin-transform-react-display-name": "^7.0.0",
"@babel/plugin-transform-react-jsx": "^7.0.0",
"@babel/plugin-transform-react-jsx-self": "^7.0.0",
"@babel/plugin-transform-react-jsx-source": "^7.0.0",
"@babel/plugin-transform-runtime": "^7.0.0",
"@babel/plugin-transform-shorthand-properties": "^7.0.0",
"@babel/plugin-transform-spread": "^7.0.0",
"@babel/plugin-transform-sticky-regex": "^7.0.0",
"@babel/plugin-transform-template-literals": "^7.0.0",
"@babel/plugin-transform-typescript": "^7.5.0",
"@babel/plugin-transform-unicode-regex": "^7.0.0",
"@babel/template": "^7.0.0",
"react-refresh": "^0.4.0"
},
"peerDependencies": {
"@babel/core": "*"
}
},
"node_modules/metro-react-native-babel-transformer": {
"version": "0.67.0",
"resolved": "https://registry.npmjs.org/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.67.0.tgz",
@@ -10958,12 +10832,6 @@
"url": "https://github.com/sponsors/antelle"
}
},
"node_modules/normalize-css-color": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/normalize-css-color/-/normalize-css-color-1.0.2.tgz",
"integrity": "sha1-Apkel8zOxmI/5XOvu/Deah8+n40=",
"dev": true
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -12103,25 +11971,6 @@
"react-native-codegen": "*"
}
},
"node_modules/react-native-web": {
"version": "0.17.7",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.17.7.tgz",
"integrity": "sha512-4OOU/QjyRySOXyHfTvljEMS4VXKn42Qs3y9uHDPMwaCUFjwg0oasR/j706OaVgan9kF4Ipa2vJ3F6Z/Xqy8KeQ==",
"dev": true,
"dependencies": {
"array-find-index": "^1.0.2",
"create-react-class": "^15.7.0",
"fbjs": "^3.0.0",
"hyphenate-style-name": "^1.0.4",
"inline-style-prefixer": "^6.0.0",
"normalize-css-color": "^1.0.2",
"prop-types": "^15.6.0"
},
"peerDependencies": {
"react": ">=17.0.1",
"react-dom": ">=17.0.1"
}
},
"node_modules/react-refresh": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz",
@@ -12632,12 +12481,6 @@
"node": ">=0.10.0"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
"dev": true
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -13699,6 +13542,82 @@
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
"dev": true
},
"node_modules/ts-jest": {
"version": "27.1.4",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.4.tgz",
"integrity": "sha512-qjkZlVPWVctAezwsOD1OPzbZ+k7zA5z3oxII4dGdZo5ggX/PL7kvwTM0pXTr10fAtbiVpJaL3bWd502zAhpgSQ==",
"dev": true,
"dependencies": {
"bs-logger": "0.x",
"fast-json-stable-stringify": "2.x",
"jest-util": "^27.0.0",
"json5": "2.x",
"lodash.memoize": "4.x",
"make-error": "1.x",
"semver": "7.x",
"yargs-parser": "20.x"
},
"bin": {
"ts-jest": "cli.js"
},
"engines": {
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
},
"peerDependencies": {
"@babel/core": ">=7.0.0-beta.0 <8",
"@types/jest": "^27.0.0",
"babel-jest": ">=27.0.0 <28",
"jest": "^27.0.0",
"typescript": ">=3.8 <5.0"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"@types/jest": {
"optional": true
},
"babel-jest": {
"optional": true
},
"esbuild": {
"optional": true
}
}
},
"node_modules/ts-jest/node_modules/lru-cache": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.7.3.tgz",
"integrity": "sha512-WY9wjJNQt9+PZilnLbuFKM+SwDull9+6IAguOrarOMoOHTcJ9GnXSO11+Gw6c7xtDkBkthR57OZMtZKYr+1CEw==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/ts-jest/node_modules/semver": {
"version": "7.3.6",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.6.tgz",
"integrity": "sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w==",
"dev": true,
"dependencies": {
"lru-cache": "^7.4.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": "^10.0.0 || ^12.0.0 || ^14.0.0 || >=16.0.0"
}
},
"node_modules/ts-jest/node_modules/yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
@@ -13757,25 +13676,6 @@
"node": ">=4.2.0"
}
},
"node_modules/ua-parser-js": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz",
"integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/ua-parser-js"
},
{
"type": "paypal",
"url": "https://paypal.me/faisalman"
}
],
"engines": {
"node": "*"
}
},
"node_modules/uglify-es": {
"version": "3.3.9",
"resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz",
@@ -14683,17 +14583,6 @@
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.8.tgz",
"integrity": "sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ=="
},
"@babel/plugin-proposal-async-generator-functions": {
"version": "7.16.8",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.8.tgz",
"integrity": "sha512-71YHIvMuiuqWJQkebWJtdhQTfd4Q4mF76q2IX37uZPkG9+olBxsX+rH1vkhFto4UeJZ9dPY2s+mDvhDm1u2BGQ==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.16.7",
"@babel/helper-remap-async-to-generator": "^7.16.8",
"@babel/plugin-syntax-async-generators": "^7.8.4"
}
},
"@babel/plugin-proposal-class-properties": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.7.tgz",
@@ -17121,12 +17010,6 @@
"integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=",
"dev": true
},
"array-find-index": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
"integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
"dev": true
},
"array-map": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz",
@@ -17557,6 +17440,15 @@
"picocolors": "^1.0.0"
}
},
"bs-logger": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
"integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==",
"dev": true,
"requires": {
"fast-json-stable-stringify": "2.x"
}
},
"bser": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
@@ -18018,25 +17910,6 @@
"parse-json": "^4.0.0"
}
},
"create-react-class": {
"version": "15.7.0",
"resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.7.0.tgz",
"integrity": "sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==",
"dev": true,
"requires": {
"loose-envify": "^1.3.1",
"object-assign": "^4.1.1"
}
},
"cross-fetch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
"integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
"dev": true,
"requires": {
"node-fetch": "2.6.7"
}
},
"cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
@@ -18073,16 +17946,6 @@
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
"integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU="
},
"css-in-js-utils": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz",
"integrity": "sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA==",
"dev": true,
"requires": {
"hyphenate-style-name": "^1.0.2",
"isobject": "^3.0.1"
}
},
"css-mediaquery": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz",
@@ -18726,38 +18589,6 @@
"bser": "2.1.1"
}
},
"fbjs": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.4.tgz",
"integrity": "sha512-ucV0tDODnGV3JCnnkmoszb5lf4bNpzjv80K41wd4k798Etq+UYD0y0TIfalLjZoKgjive6/adkRnszwapiDgBQ==",
"dev": true,
"requires": {
"cross-fetch": "^3.1.5",
"fbjs-css-vars": "^1.0.0",
"loose-envify": "^1.0.0",
"object-assign": "^4.1.0",
"promise": "^7.1.1",
"setimmediate": "^1.0.5",
"ua-parser-js": "^0.7.30"
},
"dependencies": {
"promise": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"dev": true,
"requires": {
"asap": "~2.0.3"
}
}
}
},
"fbjs-css-vars": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz",
"integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==",
"dev": true
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -19171,12 +19002,6 @@
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
"dev": true
},
"hyphenate-style-name": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
"integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==",
"dev": true
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -19250,15 +19075,6 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"inline-style-prefixer": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-6.0.1.tgz",
"integrity": "sha512-AsqazZ8KcRzJ9YPN1wMH2aNM7lkWQ8tSPrW5uDk1ziYwiAPWSZnUsC7lfZq+BDqLqz0B4Pho5wscWcJzVvRzDQ==",
"dev": true,
"requires": {
"css-in-js-utils": "^2.0.0"
}
},
"invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@@ -21894,6 +21710,12 @@
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
"dev": true
},
"lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
"dev": true
},
"lodash.mergewith": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
@@ -21962,6 +21784,12 @@
}
}
},
"make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"makeerror": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
@@ -22286,52 +22114,6 @@
"uglify-es": "^3.1.9"
}
},
"metro-react-native-babel-preset": {
"version": "0.70.0",
"resolved": "https://registry.npmjs.org/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.70.0.tgz",
"integrity": "sha512-MoOK5/rdDE2bABA+KpbXV6w0Q96sZeZiE9Ct89NYp9nPwXIaY7ylulLsjW3+rT6BdecKuNPUVwvtO0vYIQwvRw==",
"dev": true,
"requires": {
"@babel/core": "^7.14.0",
"@babel/plugin-proposal-async-generator-functions": "^7.0.0",
"@babel/plugin-proposal-class-properties": "^7.0.0",
"@babel/plugin-proposal-export-default-from": "^7.0.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0",
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
"@babel/plugin-proposal-optional-catch-binding": "^7.0.0",
"@babel/plugin-proposal-optional-chaining": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-syntax-export-default-from": "^7.0.0",
"@babel/plugin-syntax-flow": "^7.2.0",
"@babel/plugin-syntax-nullish-coalescing-operator": "^7.0.0",
"@babel/plugin-syntax-optional-chaining": "^7.0.0",
"@babel/plugin-transform-arrow-functions": "^7.0.0",
"@babel/plugin-transform-async-to-generator": "^7.0.0",
"@babel/plugin-transform-block-scoping": "^7.0.0",
"@babel/plugin-transform-classes": "^7.0.0",
"@babel/plugin-transform-computed-properties": "^7.0.0",
"@babel/plugin-transform-destructuring": "^7.0.0",
"@babel/plugin-transform-exponentiation-operator": "^7.0.0",
"@babel/plugin-transform-flow-strip-types": "^7.0.0",
"@babel/plugin-transform-function-name": "^7.0.0",
"@babel/plugin-transform-literals": "^7.0.0",
"@babel/plugin-transform-modules-commonjs": "^7.0.0",
"@babel/plugin-transform-parameters": "^7.0.0",
"@babel/plugin-transform-react-display-name": "^7.0.0",
"@babel/plugin-transform-react-jsx": "^7.0.0",
"@babel/plugin-transform-react-jsx-self": "^7.0.0",
"@babel/plugin-transform-react-jsx-source": "^7.0.0",
"@babel/plugin-transform-runtime": "^7.0.0",
"@babel/plugin-transform-shorthand-properties": "^7.0.0",
"@babel/plugin-transform-spread": "^7.0.0",
"@babel/plugin-transform-sticky-regex": "^7.0.0",
"@babel/plugin-transform-template-literals": "^7.0.0",
"@babel/plugin-transform-typescript": "^7.5.0",
"@babel/plugin-transform-unicode-regex": "^7.0.0",
"@babel/template": "^7.0.0",
"react-refresh": "^0.4.0"
}
},
"metro-react-native-babel-transformer": {
"version": "0.67.0",
"resolved": "https://registry.npmjs.org/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.67.0.tgz",
@@ -22666,12 +22448,6 @@
"integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==",
"dev": true
},
"normalize-css-color": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/normalize-css-color/-/normalize-css-color-1.0.2.tgz",
"integrity": "sha1-Apkel8zOxmI/5XOvu/Deah8+n40=",
"dev": true
},
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -23513,21 +23289,6 @@
"react-native-codegen": "*"
}
},
"react-native-web": {
"version": "0.17.7",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.17.7.tgz",
"integrity": "sha512-4OOU/QjyRySOXyHfTvljEMS4VXKn42Qs3y9uHDPMwaCUFjwg0oasR/j706OaVgan9kF4Ipa2vJ3F6Z/Xqy8KeQ==",
"dev": true,
"requires": {
"array-find-index": "^1.0.2",
"create-react-class": "^15.7.0",
"fbjs": "^3.0.0",
"hyphenate-style-name": "^1.0.4",
"inline-style-prefixer": "^6.0.0",
"normalize-css-color": "^1.0.2",
"prop-types": "^15.6.0"
}
},
"react-refresh": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz",
@@ -23928,12 +23689,6 @@
}
}
},
"setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
"dev": true
},
"setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -24767,6 +24522,45 @@
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
"dev": true
},
"ts-jest": {
"version": "27.1.4",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.4.tgz",
"integrity": "sha512-qjkZlVPWVctAezwsOD1OPzbZ+k7zA5z3oxII4dGdZo5ggX/PL7kvwTM0pXTr10fAtbiVpJaL3bWd502zAhpgSQ==",
"dev": true,
"requires": {
"bs-logger": "0.x",
"fast-json-stable-stringify": "2.x",
"jest-util": "^27.0.0",
"json5": "2.x",
"lodash.memoize": "4.x",
"make-error": "1.x",
"semver": "7.x",
"yargs-parser": "20.x"
},
"dependencies": {
"lru-cache": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.7.3.tgz",
"integrity": "sha512-WY9wjJNQt9+PZilnLbuFKM+SwDull9+6IAguOrarOMoOHTcJ9GnXSO11+Gw6c7xtDkBkthR57OZMtZKYr+1CEw==",
"dev": true
},
"semver": {
"version": "7.3.6",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.6.tgz",
"integrity": "sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w==",
"dev": true,
"requires": {
"lru-cache": "^7.4.0"
}
},
"yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"dev": true
}
}
},
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
@@ -24809,12 +24603,6 @@
"integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
"dev": true
},
"ua-parser-js": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz",
"integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==",
"dev": true
},
"uglify-es": {
"version": "3.3.9",
"resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz",

View File

@@ -5,7 +5,7 @@
"sideEffects": false,
"scripts": {
"test": "jest",
"build": "tsc"
"build": "rm -rf dist && tsc"
},
"files": [
"dist/",
@@ -13,7 +13,6 @@
"types.d.ts"
],
"dependencies": {
"@babel/template": "^7.16.7",
"@react-native-community/hooks": "^2.8.1",
"babel-literal-to-ast": "^2.1.0",
"css": "^3.0.0",
@@ -25,7 +24,8 @@
},
"devDependencies": {
"@babel/core": "^7.17.8",
"@babel/preset-typescript": "^7.16.7",
"@babel/plugin-syntax-jsx": "^7.16.7",
"@babel/types": "^7.17.0",
"@types/css": "^0.0.33",
"@types/css-mediaquery": "^0.1.1",
"@types/css-to-react-native": "^3.0.0",
@@ -35,12 +35,11 @@
"@types/tailwindcss": "^3.0.10",
"babel-plugin-tester": "^10.1.0",
"jest": "^27.5.1",
"metro-react-native-babel-preset": "^0.70.0",
"postcss": "^8.4.12",
"prettier": "^2.6.1",
"react-native": "^0.68.0",
"react-native-web": "^0.17.7",
"tailwindcss": "^3.0.23",
"ts-jest": "^27.1.4",
"typescript": "^4.6.3"
},
"peerDependencies": {

33
src/babel/index.ts Normal file
View File

@@ -0,0 +1,33 @@
import { Babel, TailwindReactNativeOptions } from "./types";
import web from "./web";
import nativeInline from "./native-inline";
import nativeContext from "./native-context";
const platformPlugins = {
web,
"native-context": nativeContext,
"native-inline": nativeInline,
};
export default function (
babel: Babel,
options: TailwindReactNativeOptions,
cwd: string
) {
let { platform = "native" } = options ?? {};
if (platform === "native" && process.env.NODE_ENV === "production") {
platform = "native-inline";
} else if (platform === "native") {
platform = "native-context";
} else if (!(platform in platformPlugins)) {
throw new Error(`Unknown platform ${platform}`);
}
return platformPlugins[platform as keyof typeof platformPlugins](
babel,
options,
cwd
);
}

112
src/babel/native-context.ts Normal file
View File

@@ -0,0 +1,112 @@
import { NodePath, Visitor } from "@babel/core";
import { Program } from "@babel/types";
import { Babel, State, TailwindReactNativeOptions } from "./types";
import { nativeVisitor, NativeVisitorState } from "./native-visitor";
import { getTailwindConfig } from "./utils/get-tailwind-config";
import { processStyles } from "./utils/process-styles";
import { appendVariables } from "./utils/native-variables";
import { appendImport } from "./utils/imports";
import { getJSXElementName } from "./utils/jsx";
export default function (
babel: Babel,
options: TailwindReactNativeOptions,
cwd: string
) {
const tailwindConfig = getTailwindConfig(cwd, options);
const { styles, media } = processStyles(babel, tailwindConfig);
return {
visitor: {
Program: {
enter(path: NodePath<Program>, state: State) {
/* Dirty check the file for
* - className attribute
* - TailwindProvider
*/
if (
!state.file.code.includes("className=") &&
!state.file.code.includes("TailwindProvider")
) {
return;
}
const nativeVisitorState: NativeVisitorState = {
...state,
babel,
tailwindConfig,
blackedListedComponents: new Set(),
hasUseParseTailwind: false,
hasStyleSheetImport: false,
hasClassNames: false,
hasProvider: false,
transformClassNameOptions: { inlineStyles: false },
visitor: nativeContextVisitor,
};
// Traverse the file
path.traverse(nativeVisitor, nativeVisitorState);
const {
hasClassNames,
hasStyleSheetImport,
hasUseParseTailwind,
hasProvider,
} = nativeVisitorState;
const bodyNode = path.node.body;
// Add the __useParseTailwind import if it is missing
if (hasClassNames && !hasUseParseTailwind) {
appendImport(
babel,
bodyNode,
"__useParseTailwind",
"tailwindcss-react-native"
);
}
// If there is no TailwindProvider in this file, then we can finish early
if (!hasProvider) {
return;
}
// Add the StyleSheet import if it is missing
if (!hasStyleSheetImport) {
appendImport(babel, bodyNode, "StyleSheet", "react-native");
}
appendVariables(babel, bodyNode, styles, media);
},
},
},
};
}
const nativeContextVisitor: Visitor<NativeVisitorState> = {
JSXOpeningElement(path, state) {
const { types: t } = state.babel;
const elementIsTailwindProvider =
getJSXElementName(path.node) === "TailwindProvider";
state.hasProvider ||= elementIsTailwindProvider;
if (elementIsTailwindProvider) {
path.node.attributes.push(
t.jSXAttribute(
t.jSXIdentifier("styles"),
t.jSXExpressionContainer(t.identifier("__tailwindStyles"))
)
);
path.node.attributes.push(
t.jSXAttribute(
t.jSXIdentifier("media"),
t.jSXExpressionContainer(t.identifier("__tailwindMedia"))
)
);
}
},
};

View File

@@ -0,0 +1,85 @@
import { relative } from "path";
import { Program } from "@babel/types";
import { NodePath } from "@babel/traverse";
import { processStyles } from "./utils/process-styles";
import { getTailwindConfig } from "./utils/get-tailwind-config";
import { appendVariables } from "./utils/native-variables";
import { appendImport } from "./utils/imports";
import { NativeVisitorState, nativeVisitor } from "./native-visitor";
import { TailwindReactNativeOptions, State, Babel } from "./types";
export default function (
babel: Babel,
options: TailwindReactNativeOptions,
cwd: string
) {
const tailwindConfig = getTailwindConfig(cwd, options);
return {
visitor: {
Program: {
enter(path: NodePath<Program>, state: State) {
// Dirty check the file for the className attribute
if (!state.file.code.includes("className=")) {
return;
}
const nativeVisitorState: NativeVisitorState = {
...state,
babel,
tailwindConfig,
blackedListedComponents: new Set(),
hasUseParseTailwind: false,
hasStyleSheetImport: false,
hasClassNames: false,
hasProvider: false,
transformClassNameOptions: { inlineStyles: true },
};
// Traverse the file
path.traverse(nativeVisitor, nativeVisitorState);
const {
filename,
hasClassNames,
hasStyleSheetImport,
hasUseParseTailwind,
} = nativeVisitorState;
// There are no classNames so skip this file
if (!hasClassNames) {
return;
}
const rootDir = state.file.opts.root ?? ".";
const bodyNode = path.node.body;
if (!hasUseParseTailwind) {
appendImport(
babel,
bodyNode,
"__useParseTailwind",
"tailwindcss-react-native"
);
}
if (!hasStyleSheetImport) {
appendImport(babel, bodyNode, "StyleSheet", "react-native");
}
/**
* Override tailwind to only process the classnames in this file
*/
const { styles, media } = processStyles(babel, {
...tailwindConfig,
// Make sure its relative to the tailwind.config.js
content: [relative(rootDir, filename)],
});
appendVariables(babel, bodyNode, styles, media);
},
},
},
};
}

View File

@@ -0,0 +1,80 @@
import type { Visitor } from "@babel/traverse";
import { TailwindConfig } from "tailwindcss/tailwind-config";
import { Babel, State } from "./types";
import { getJSXElementName } from "./utils/jsx";
import { getBlackedListedComponents, hasNamedImport } from "./utils/imports";
import {
transformClassName,
TransformClassNameOptions,
} from "./utils/transform-class-names";
export interface NativeVisitorState extends State {
babel: Babel;
tailwindConfig: TailwindConfig;
blackedListedComponents: Set<string>;
hasUseParseTailwind: boolean;
hasStyleSheetImport: boolean;
hasClassNames: boolean;
hasProvider: boolean;
visitor?: Visitor<NativeVisitorState>;
transformClassNameOptions: TransformClassNameOptions;
}
/**
* Visitor that preforms common operations betweent the native platforms
*
* @remarks
*
* If a platform needs to provide bespoke functionality, they can provide a Visitor object
* on the state. (note: This Visitor is not valided by Babel so use with caution)
*
* @privateRemarks
*
* This should only focus on common functionality, eg:
* - Detecting imports
* - Detecting if a JSXElement should be processed
*
* The only exception to this is the className -> style transform
* as that is also used to detect the className attribute should be processed
*
*/
export const nativeVisitor: Visitor<NativeVisitorState> = {
ImportDeclaration(path, state) {
state.blackedListedComponents = new Set(
getBlackedListedComponents(path, state)
);
// We only need to check named imports.
// THe code will still work if they are using a Namespace Specifier
state.hasStyleSheetImport = hasNamedImport(
path,
"StyleSheet",
"react-native"
);
state.hasUseParseTailwind = hasNamedImport(
path,
"__useParseTailwind",
"tailwindcss-react-native"
);
},
JSXOpeningElement(path, state) {
const name = getJSXElementName(path.node);
if (state.blackedListedComponents.has(name)) {
return;
}
const hasClassNames = transformClassName(
state.babel,
path,
state.transformClassNameOptions
);
state.hasClassNames ||= hasClassNames;
if (typeof state.visitor?.JSXOpeningElement === "function") {
state.visitor.JSXOpeningElement.bind(this)(path, state);
}
},
};

20
src/babel/types.d.ts vendored Normal file
View File

@@ -0,0 +1,20 @@
import { ViewStyle, TextStyle, ImageStyle } from "react-native";
import * as BabelCore from "@babel/core";
export type Style = ViewStyle | TextStyle | ImageStyle;
export type Babel = typeof BabelCore;
export interface TailwindReactNativeOptions {
tailwindConfigPath?: string;
platform?: "web" | "native" | "native-context" | "native-inline";
allowedImports?: "*" | Array<string | RegExp>;
deniedImports?: Array<string | RegExp>;
}
export type State = {
get: (name: string) => any;
set: (name: string, value: any) => any;
opts: TailwindReactNativeOptions;
file: BabelCore.BabelFile;
filename: string;
};

View File

@@ -0,0 +1,114 @@
import cssToReactNative, { StyleTuple } from "css-to-react-native";
import { TailwindConfig } from "tailwindcss/tailwind-config";
import { AtRule, Comment, Media, Rule, StyleRules } from "css";
import { normaliseSelector } from "../../shared/selector";
import { Babel, Style } from "../types";
import { isValidStyle } from "./is-valid-style";
interface CssRule {
selector: string;
media: string[];
rules: Style;
}
/**
* Flattens StyleRules from the 'css' package
* - spreads selectors into multiple entries
* - flattens media rules so they are not nested
* - flattens styles to be react-native style objects
*/
export function flattenRules(
babel: Babel,
cssRules: StyleRules["rules"],
tailwindConfig: TailwindConfig,
media: string[] = []
): CssRule[] {
return cssRules.flatMap((cssRule) => {
if (isMedia(cssRule)) {
return flattenRules(babel, cssRule.rules ?? [], tailwindConfig, [
...new Set(cssRule.media ? [...media, cssRule.media] : media),
]);
} else if (isRule(cssRule)) {
const declarationRuleTuples: StyleTuple[] = [];
const invalidStyleProps: string[] = [];
for (const declaration of cssRule.declarations || []) {
if (isComment(declaration)) {
continue;
}
const { property, value } = declaration;
if (property === undefined || value === undefined) {
continue;
}
declarationRuleTuples.push([property, value]);
}
if (declarationRuleTuples.length === 0) {
return [];
}
const rules = Object.fromEntries(
Object.entries(cssToReactNative(declarationRuleTuples)).filter(
([prop, value]) => {
if (isValidStyle(prop, value)) {
return true;
} else {
invalidStyleProps.push(prop);
return false;
}
}
)
);
if (
process.env.NODE_ENV !== "development" &&
process.env.NODE_ENV !== "test"
) {
if (invalidStyleProps.length > 0) {
console.warn(
`Selectors ${cssRule.selectors} use invalid styles ${invalidStyleProps}`
);
console.warn(
`Either remove these selectors or change to file to be platform specific (eg compoent.web.js)`
);
}
}
if (Object.keys(rules).length === 0) {
return [];
}
const selectors: CssRule[] = (cssRule.selectors || []).map(
(selectorDirty) => {
const selector = normaliseSelector(selectorDirty, tailwindConfig);
return {
selector,
media,
rules,
};
}
);
return selectors;
} else {
return [];
}
});
}
function isRule(rule: Rule | Comment | AtRule): rule is Rule {
return rule.type === "rule";
}
function isComment(rule: Rule | Comment | AtRule): rule is Comment {
return rule.type === "comment";
}
function isMedia(rule: Rule | Comment | AtRule): rule is Media {
return rule.type === "media";
}

View File

@@ -1,21 +1,23 @@
const { join } = require("path");
const { existsSync } = require("fs");
const plugin = require("tailwindcss/plugin");
const resolveTailwindConfig = require("tailwindcss/resolveConfig");
import { join } from "path";
import { existsSync } from "fs";
import plugin from "tailwindcss/plugin";
import resolveTailwindConfig from "tailwindcss/resolveConfig";
import { TailwindReactNativeOptions } from "../types";
/**
* Resolves the Tailwind config based upon the cwd
* @param {string} cwd
* @returns {import('tailwindcss/tailwind-config').TailwindConfig}
*/
function getTailwindConfig(cwd, configPath) {
const fullConfigPath = join(cwd, configPath || "./tailwind.config.js");
export function getTailwindConfig(
cwd: string,
{ tailwindConfigPath }: TailwindReactNativeOptions
): import("tailwindcss/tailwind-config").TailwindConfig {
const fullConfigPath = join(
cwd,
tailwindConfigPath || "./tailwind.config.js"
);
// Get the config. Throw an error if configPath was set but we were unable to find it
let projectTailwindConfig;
if (existsSync(fullConfigPath)) {
projectTailwindConfig = require(fullConfigPath);
} else if (configPath) {
} else if (tailwindConfigPath) {
throw new Error(`Unable to find config ${fullConfigPath}`);
} else {
projectTailwindConfig = {};
@@ -33,5 +35,3 @@ function getTailwindConfig(cwd, configPath) {
],
});
}
module.exports = getTailwindConfig;

View File

@@ -0,0 +1,87 @@
import { NodePath } from "@babel/core";
import * as t from "@babel/types";
import { Statement } from "@babel/types";
import { Babel, State } from "../types";
export function appendImport(
{ types: t }: Babel,
body: Statement[],
variable: string,
source: string
) {
body.unshift(
t.importDeclaration(
[t.importSpecifier(t.identifier(variable), t.identifier(variable))],
t.stringLiteral(source)
)
);
}
export function hasNamedImport(
path: NodePath<t.ImportDeclaration>,
variable: string,
source: string
) {
if (path.node.source.value === source) {
return path.node.specifiers.some((specifier) => {
if (!t.isImportSpecifier(specifier)) {
return;
}
if (t.isStringLiteral(specifier.imported)) {
return specifier.imported.value === variable;
} else {
return specifier.imported.name === variable;
}
});
}
return false;
}
export function getBlackedListedComponents(
path: NodePath<t.ImportDeclaration>,
state: State
) {
const { allowedImports = "*", deniedImports = [] } = state.opts;
if (allowedImports === "*" && deniedImports.length === 0) {
return [];
}
const module = path.node.source.value;
if (allowedImports !== "*") {
const isAllowed = allowedImports.every((allowed) => {
if (allowed instanceof RegExp) {
return allowed.test(module);
} else {
return module.startsWith(allowed);
}
});
if (isAllowed) {
return [];
}
}
if (deniedImports.length > 0) {
const isDenied = deniedImports.every((deny) => {
if (deny instanceof RegExp) {
return deny.test(module);
} else {
return module.startsWith(deny);
}
});
if (!isDenied) {
return [];
}
}
// If we have made it to here, either the module was
// not allowed or was denied.
return path.node.specifiers.map((specifier) => {
return specifier.local.name;
});
}

View File

@@ -1,15 +1,11 @@
function isValidStyle(prop, value) {
export function isValidStyle(prop: string, _value: unknown) {
if (!validProps.has(prop)) {
return false;
} else if (prop === "display" && !displayValues.has(value)) {
return false;
}
return true;
}
const displayValues = new Set(["none", "flex", undefined]);
const validProps = new Set([
"alignContent",
"alignItems",
@@ -120,5 +116,3 @@ const validProps = new Set([
"writingDirection",
"zIndex",
]);
module.exports = isValidStyle;

160
src/babel/utils/jsx.ts Normal file
View File

@@ -0,0 +1,160 @@
import { NodePath } from "@babel/core";
import {
Expression,
isJSXIdentifier,
isJSXNamespacedName,
JSXAttribute,
JSXEmptyExpression,
JSXExpressionContainer,
JSXIdentifier,
JSXMemberExpression,
JSXNamespacedName,
JSXOpeningElement,
JSXSpreadAttribute,
StringLiteral,
} from "@babel/types";
import { Babel } from "../types";
/**
* Get the name of a JXS Element
*
* @remarks
*
* Valid JSX:
*
* <MyComponent />
* <A.MyComponent />
* <B.A.MyComponent />
* <C.B.A.MyComponent />
* ..etc
*
* @privateRemarks
*
* This was abstracted as it needs to be recursive
*
*/
export function getJSXElementName(node: JSXOpeningElement) {
return getElementName(node.name);
}
/**
* Recursive helper function for getJSXElementName
*/
function getElementName(
node: JSXIdentifier | JSXNamespacedName | JSXMemberExpression
): string {
if (isJSXIdentifier(node)) {
return node.name;
} else if (isJSXNamespacedName(node)) {
return node.name.name;
} else {
return getElementName(node.object);
}
}
/**
* Get the expression and attribute of the 'className' & 'style'
*
* @privateRemarks
*
* This was abstracted into a function due to the number of code branches
*
*/
export function getStyleAttributesAndValues(
{ types: t }: Babel,
nodePath: NodePath<JSXOpeningElement>
) {
let style: JSXEmptyExpression | Expression | undefined;
let styleAttribute: NodePath<JSXAttribute | JSXSpreadAttribute> | undefined;
let className: StringLiteral | Expression | undefined;
let classNameAttribute:
| NodePath<JSXAttribute | JSXSpreadAttribute>
| undefined;
/**
* Loop over all the attributes
*
* While silly, this is valid JSX
*
* <View style={myStyles} style={actualStyles} />
*
* So we need to loop over all attributes and just take the last ones
*
* We could reverse the array and break early, but most elements have <10 attributes
*/
for (const attribute of nodePath.get("attributes")) {
// Ignore spreads, we cannot process them
if (t.isJSXSpreadAttribute(attribute.node)) {
continue;
}
const { name, value } = attribute.node;
if (t.isJSXIdentifier(name) && name.name === "className") {
classNameAttribute = attribute;
if (t.isStringLiteral(value)) {
// className="font-bold"
className = value;
} else if (t.isJSXExpressionContainer(value)) {
// className={myClassNames}
if (t.isJSXEmptyExpression(value.expression)) {
// Do nothing if the expression is empty
} else {
className = value.expression;
}
} else {
throw nodePath.buildCodeFrameError(
"Unable to parse className attribute"
);
}
} else if (t.isJSXIdentifier(name) && name.name === "style") {
styleAttribute = attribute;
/*
* Get the style.
**/
if (t.isJSXExpressionContainer(value)) {
// style={styles.myStyles}
style = value.expression;
}
}
}
return {
className,
classNameAttribute,
style,
styleAttribute,
};
}
/**
* Creates a JSXExpressionContainer for a style attribute containing __useParseTailwind().
* It will merge with existing styles if provided.
*
* @privateRemarks
*
* This was moved into a helper function to increase readbility
*/
export function createMergedStylesExpressionContainer(
{ types: t }: Babel,
newExpression: Expression,
existingExpression: JSXEmptyExpression | Expression | undefined
) {
let expressionContainer: JSXExpressionContainer;
if (t.isMemberExpression(existingExpression)) {
expressionContainer = t.jSXExpressionContainer(
t.arrayExpression([newExpression, existingExpression])
);
} else if (t.isArrayExpression(existingExpression)) {
existingExpression.elements.unshift(newExpression);
expressionContainer = t.jSXExpressionContainer(existingExpression);
} else {
expressionContainer = t.jSXExpressionContainer(newExpression);
}
return expressionContainer;
}

View File

@@ -0,0 +1,32 @@
import { Expression, Statement } from "@babel/types";
import { Babel } from "../types";
export function appendVariables(
babel: Babel,
body: Statement[],
styles: Expression,
media: Expression
) {
const { types: t } = babel;
body.push(
t.variableDeclaration("const", [
t.variableDeclarator(
t.identifier("__tailwindStyles"),
t.callExpression(
t.memberExpression(
t.identifier("StyleSheet"),
t.identifier("create")
),
[styles]
)
),
])
);
body.push(
t.variableDeclaration("const", [
t.variableDeclarator(t.identifier("__tailwindMedia"), media),
])
);
}

View File

@@ -1,19 +1,20 @@
const css = require("css");
const postcss = require("postcss");
const tailwind = require("tailwindcss");
const postcssCssvariables = require("postcss-css-variables");
const postcssColorRBG = require("postcss-color-rgb");
const postcssRemToPixel = require("postcss-rem-to-pixel");
const serialize = require("babel-literal-to-ast");
const flattenRules = require("./flatten-rules");
const normaliseSelector = require("../../../dist/shared/selector");
import { Babel, Style } from "../types";
/** @typedef {import('react-native').ViewStyle | import('react-native').TextStyle | import('react-native').ImageStyle} Style */
import css from "css";
import postcss from "postcss";
import tailwind from "tailwindcss";
import postcssCssvariables from "postcss-css-variables";
import postcssColorRBG from "postcss-color-rgb";
import postcssRemToPixel from "postcss-rem-to-pixel";
import serialize from "babel-literal-to-ast";
function processStyles({ types: t }, tailwindConfig) {
import { flattenRules } from "./flatten-rules";
import { normaliseSelector } from "../../shared/selector";
import { TailwindConfig } from "tailwindcss/tailwind-config";
export function processStyles(babel: Babel, tailwindConfig: TailwindConfig) {
const cssInput = "@tailwind utilities";
/** @type {string} */
const processedCss = postcss([
tailwind(tailwindConfig),
postcssCssvariables(),
@@ -33,19 +34,19 @@ function processStyles({ types: t }, tailwindConfig) {
}),
]).process(cssInput).css;
const cssRules = css.parse(processedCss).stylesheet.rules;
const cssRules = css.parse(processedCss).stylesheet?.rules ?? [];
const parsedRules = flattenRules(t, cssRules, tailwindConfig);
const parsedRules = flattenRules(babel, cssRules, tailwindConfig);
/** @type {Record<string, Style>} */
const styles = {};
/** @type {Record<string, Array<{ media: string, suffix: number>} */
const mediaRules = {};
const styles: Record<string, Style> = {};
const mediaRules: Record<
string,
Array<{ media: string[]; suffix: number }>
> = {};
for (const [suffix, parsedRule] of parsedRules.entries()) {
const { selector, media, rules } = parsedRule;
for (const [
suffix,
{ selector, media, rules } = {},
] of parsedRules.entries()) {
const normalisedSelector = normaliseSelector(selector, tailwindConfig);
if (media.length > 0) {
@@ -69,5 +70,3 @@ function processStyles({ types: t }, tailwindConfig) {
media: serialize(mediaRules),
};
}
module.exports = processStyles;

View File

@@ -0,0 +1,78 @@
import { NodePath } from "@babel/core";
import { Expression, JSXOpeningElement } from "@babel/types";
import { Babel } from "../types";
import {
createMergedStylesExpressionContainer,
getStyleAttributesAndValues,
} from "./jsx";
export interface TransformClassNameOptions {
inlineStyles: boolean;
}
export function transformClassName(
babel: Babel,
path: NodePath<JSXOpeningElement>,
{ inlineStyles }: TransformClassNameOptions
): boolean {
const {
className: existingClassName,
classNameAttribute: existingClassNameAttribute,
styleAttribute: existingStyleAttribute,
style: existingStyle,
} = getStyleAttributesAndValues(babel, path);
/**
* If we didn't find any classNames, return early
*/
if (!existingClassName) {
return false;
}
/**
* Remove the existing attributes, we are going to add a new ones
*/
existingClassNameAttribute?.remove();
existingStyleAttribute?.remove();
/**
* If there are existing styles we need to merge them
*
* Classnames have lower specificity than inline styles
* so they should always be first
*/
const { types: t } = babel;
const callExpressionArguments: Array<Expression> = inlineStyles
? [
existingClassName,
t.objectExpression([
t.objectProperty(
t.identifier("styles"),
t.identifier("__tailwindStyles")
),
t.objectProperty(
t.identifier("media"),
t.identifier("__tailwindMedia")
),
]),
]
: [existingClassName];
const hookExpression = t.callExpression(
t.identifier("__useParseTailwind"),
callExpressionArguments
);
const newStyleExpression = createMergedStylesExpressionContainer(
babel,
hookExpression,
existingStyle
);
path.node.attributes.push(
t.jSXAttribute(t.jSXIdentifier("style"), newStyleExpression)
);
return true;
}

73
src/babel/web-visitor.ts Normal file
View File

@@ -0,0 +1,73 @@
import type { Visitor } from "@babel/traverse";
import { Babel, State } from "./types";
import {
createMergedStylesExpressionContainer,
getJSXElementName,
getStyleAttributesAndValues,
} from "./utils/jsx";
export interface WebVisitorState extends State {
babel: Babel;
}
export const webVisitor: Visitor<WebVisitorState> = {
JSXOpeningElement(path, state) {
try {
const { types: t } = state.babel;
const name = getJSXElementName(path.node);
const firstCharOfName = name[0];
// Ignore elements that start in lower case
if (
firstCharOfName &&
firstCharOfName === firstCharOfName.toLowerCase()
) {
return;
}
const {
className: existingClassName,
classNameAttribute: existingClassNameAttribute,
styleAttribute: existingStyleAttribute,
style: existingStyle,
} = getStyleAttributesAndValues(state.babel, path);
/**
* If we didn't find any classNames, return early
*/
if (!existingClassName) {
return;
}
/**
* Remove the existing attributes, we are going to add a new ones
*/
existingClassNameAttribute?.remove();
existingStyleAttribute?.remove();
/**
* Convert the classNames into a compiled style object
*/
let newStyle = t.objectExpression([
t.objectProperty(t.identifier("$$css"), t.booleanLiteral(true)),
t.objectProperty(
t.identifier("tailwindcssReactNative"),
existingClassName
),
]);
const newStyleExpression = createMergedStylesExpressionContainer(
state.babel,
newStyle,
existingStyle
);
path.node.attributes.push(
t.jSXAttribute(t.jSXIdentifier("style"), newStyleExpression)
);
} catch (error) {
throw error;
}
},
};

27
src/babel/web.ts Normal file
View File

@@ -0,0 +1,27 @@
import { NodePath } from "@babel/core";
import { Program } from "@babel/types";
import { Babel, State } from "./types";
import { webVisitor, WebVisitorState } from "./web-visitor";
export default function (babel: Babel) {
return {
visitor: {
Program: {
enter(path: NodePath<Program>, state: State) {
// Dirty check the file for the className attribute
if (!state.file.code.includes("className=")) {
return;
}
const webVisitorState: WebVisitorState = {
...state,
babel,
};
// Traverse the file
path.traverse(webVisitor, webVisitorState);
},
},
},
};
}

View File

@@ -1,32 +1,32 @@
import { createContext } from 'react'
import { createContext } from "react";
import {
Appearance,
ColorSchemeName,
ImageStyle,
TextStyle,
ViewStyle,
} from 'react-native'
} from "react-native";
export interface TailwindStyleContext {
styles: StyleRecord
media: MediaRules
styles: StyleRecord;
media: MediaRules;
}
export type StyleRecord = Record<string, ViewStyle | TextStyle | ImageStyle>
export type StyleRecord = Record<string, ViewStyle | TextStyle | ImageStyle>;
export type MediaRules = Record<
string,
Array<{ media: string[]; suffix: number }>
>
>;
export const TailwindStyleContext = createContext<TailwindStyleContext>({
styles: {},
media: {},
})
});
export const TailwindColorSchemeContext = createContext<ColorSchemeName>(
Appearance.getColorScheme()
)
);
export const TailwindSetColorSchemeContext = createContext<
(colorScheme: ColorSchemeName) => void
>(() => {})
>(() => {});

View File

@@ -1,3 +1,3 @@
export * from './provider'
export * from './context'
export * from './use-parse-tailwind'
export * from "./provider";
export * from "./context";
export * from "./use-parse-tailwind";

View File

@@ -1,24 +0,0 @@
/** @typedef {{ important?: string }} NormaliseSelectorOptions */
/**
* Normalise a css selector/rule into a standard format
* @param {string} selector
* @param {NormaliseSelectorOptions} options
* @returns {string}
*/
function normaliseSelector(selector, options = {}) {
const { important } = options;
const s = important
? selector.replace(new RegExp(`^${important}`), "")
: selector;
// prettier-ignore
return s
.split(" ").join("")
.split("\\").join("")
.split(":").join("_")
.replace(/^\./, "");
}
module.exports = normaliseSelector;

18
src/shared/selector.ts Normal file
View File

@@ -0,0 +1,18 @@
import { TailwindConfig } from "tailwindcss/tailwind-config";
/**
* Normalise a selector so it can be used as an object key
*/
export function normaliseSelector(
selector: string,
{ important }: Partial<TailwindConfig> = {}
) {
const leadingDots = "^\\.";
const regex =
typeof important === "string"
? new RegExp(`^${important}|${leadingDots}`)
: new RegExp(leadingDots);
return selector.replace(regex, "").replace(/\s/g, "_");
}

15
src/types/node-modules.d.ts vendored Normal file
View File

@@ -0,0 +1,15 @@
declare module "postcss-css-variables" {
export default unknown;
}
declare module "postcss-color-rgb" {
export default unknown;
}
declare module "postcss-rem-to-pixel" {
export default unknown;
}
declare module "babel-literal-to-ast" {
export default unknown;
}

View File

@@ -1,3 +0,0 @@
declare module "react-native-web" {
export const Appearance = any;
}

View File

@@ -1,5 +1,5 @@
import { useContext } from "react";
import normaliseSelector from "./shared/selector";
import { normaliseSelector } from "./shared/selector";
import {
MediaRules,
StyleRecord,