From eb4100a68f6b19e4502b537687cee93da4bfb223 Mon Sep 17 00:00:00 2001 From: Mark Lawlor Date: Wed, 6 Apr 2022 18:56:41 +1000 Subject: [PATCH] feat: options to control what is transformed refactor: convert everything to typescript --- README.md | 42 +- .../{fixtures.spec.js => fixtures.spec.ts} | 12 +- .../basic/{code.js => code.ts} | 0 .../native-context-fixtures/basic/output.js | 20 - .../native-context-fixtures/basic/output.ts | 19 + .../basic/{code.js => code.ts} | 0 .../native-inline-fixtures/basic/options.json | 1 - .../native-inline-fixtures/basic/output.js | 21 - .../native-inline-fixtures/basic/output.ts | 26 + .../basic/tailwind.config.js | 3 - .../conditionals/{code.js => code.ts} | 0 .../conditionals/options.json | 1 - .../conditionals/output.js | 27 - .../conditionals/output.ts | 32 ++ .../conditionals/tailwind.config.js | 3 - .../empty-className/{code.js => code.ts} | 0 .../empty-className/options.json | 1 - .../empty-className/output.js | 19 - .../empty-className/output.ts | 22 + .../empty-className/tailwind.config.js | 3 - .../existing-style/{code.js => code.ts} | 0 .../existing-style/options.json | 1 - .../existing-style/output.js | 25 - .../existing-style/output.ts | 34 ++ .../existing-style/tailwind.config.js | 3 - .../existing-styles/{code.js => code.ts} | 2 +- .../existing-styles/options.json | 1 - .../existing-styles/output.js | 29 - .../existing-styles/output.ts | 38 ++ .../existing-styles/tailwind.config.js | 3 - .../ignores-invalid-classes/options.json | 4 - .../ignores-invalid-classes/output.js | 19 - .../tailwind.config.js | 3 - .../web-fixtures/basic/{code.js => code.ts} | 0 __tests__/web-fixtures/basic/output.js | 13 - .../code.js => web-fixtures/basic/output.ts} | 10 +- .../ignores-native-element/output.js | 34 +- babel.config.js | 16 - babel.js | 1 + babel/index.js | 16 - babel/native/context.js | 100 ---- babel/native/index.js | 11 - babel/native/inline.js | 101 ---- babel/native/utils/babel-imports.js | 28 - babel/native/utils/babel-variables.js | 17 - babel/native/utils/flatten-rules.js | 89 --- babel/native/utils/transform-class-names.js | 91 --- babel/web.js | 97 ---- docs/library-comparision.md | 34 +- docs/platforms.md | 31 +- jest.config.js | 5 +- package-lock.json | 542 ++++++------------ package.json | 9 +- src/babel/index.ts | 33 ++ src/babel/native-context.ts | 112 ++++ src/babel/native-inline.ts | 85 +++ src/babel/native-visitor.ts | 80 +++ src/babel/types.d.ts | 20 + src/babel/utils/flatten-rules.ts | 114 ++++ .../babel/utils/get-tailwind-config.ts | 28 +- src/babel/utils/imports.ts | 87 +++ .../babel/utils/is-valid-style.ts | 8 +- src/babel/utils/jsx.ts | 160 ++++++ src/babel/utils/native-variables.ts | 32 ++ .../babel/utils/process-styles.ts | 47 +- src/babel/utils/transform-class-names.ts | 78 +++ src/babel/web-visitor.ts | 73 +++ src/babel/web.ts | 27 + src/context.ts | 18 +- src/index.ts | 6 +- src/shared/selector.js | 24 - src/shared/selector.ts | 18 + src/types/node-modules.d.ts | 15 + src/types/react-native-web.d.ts | 3 - src/use-parse-tailwind.ts | 2 +- 75 files changed, 1411 insertions(+), 1318 deletions(-) rename __tests__/{fixtures.spec.js => fixtures.spec.ts} (72%) rename __tests__/native-context-fixtures/basic/{code.js => code.ts} (100%) delete mode 100644 __tests__/native-context-fixtures/basic/output.js create mode 100644 __tests__/native-context-fixtures/basic/output.ts rename __tests__/native-inline-fixtures/basic/{code.js => code.ts} (100%) delete mode 100644 __tests__/native-inline-fixtures/basic/output.js create mode 100644 __tests__/native-inline-fixtures/basic/output.ts delete mode 100644 __tests__/native-inline-fixtures/basic/tailwind.config.js rename __tests__/native-inline-fixtures/conditionals/{code.js => code.ts} (100%) delete mode 100644 __tests__/native-inline-fixtures/conditionals/output.js create mode 100644 __tests__/native-inline-fixtures/conditionals/output.ts delete mode 100644 __tests__/native-inline-fixtures/conditionals/tailwind.config.js rename __tests__/native-inline-fixtures/empty-className/{code.js => code.ts} (100%) delete mode 100644 __tests__/native-inline-fixtures/empty-className/output.js create mode 100644 __tests__/native-inline-fixtures/empty-className/output.ts delete mode 100644 __tests__/native-inline-fixtures/empty-className/tailwind.config.js rename __tests__/native-inline-fixtures/existing-style/{code.js => code.ts} (100%) delete mode 100644 __tests__/native-inline-fixtures/existing-style/output.js create mode 100644 __tests__/native-inline-fixtures/existing-style/output.ts delete mode 100644 __tests__/native-inline-fixtures/existing-style/tailwind.config.js rename __tests__/native-inline-fixtures/existing-styles/{code.js => code.ts} (92%) delete mode 100644 __tests__/native-inline-fixtures/existing-styles/output.js create mode 100644 __tests__/native-inline-fixtures/existing-styles/output.ts delete mode 100644 __tests__/native-inline-fixtures/existing-styles/tailwind.config.js delete mode 100644 __tests__/native-inline-fixtures/ignores-invalid-classes/options.json delete mode 100644 __tests__/native-inline-fixtures/ignores-invalid-classes/output.js delete mode 100644 __tests__/native-inline-fixtures/ignores-invalid-classes/tailwind.config.js rename __tests__/web-fixtures/basic/{code.js => code.ts} (100%) delete mode 100644 __tests__/web-fixtures/basic/output.js rename __tests__/{native-inline-fixtures/ignores-invalid-classes/code.js => web-fixtures/basic/output.ts} (53%) delete mode 100644 babel.config.js create mode 100644 babel.js delete mode 100644 babel/index.js delete mode 100644 babel/native/context.js delete mode 100644 babel/native/index.js delete mode 100644 babel/native/inline.js delete mode 100644 babel/native/utils/babel-imports.js delete mode 100644 babel/native/utils/babel-variables.js delete mode 100644 babel/native/utils/flatten-rules.js delete mode 100644 babel/native/utils/transform-class-names.js delete mode 100644 babel/web.js create mode 100644 src/babel/index.ts create mode 100644 src/babel/native-context.ts create mode 100644 src/babel/native-inline.ts create mode 100644 src/babel/native-visitor.ts create mode 100644 src/babel/types.d.ts create mode 100644 src/babel/utils/flatten-rules.ts rename babel/native/utils/get-tailwind-config.js => src/babel/utils/get-tailwind-config.ts (54%) create mode 100644 src/babel/utils/imports.ts rename babel/native/utils/is-valid-style.js => src/babel/utils/is-valid-style.ts (90%) create mode 100644 src/babel/utils/jsx.ts create mode 100644 src/babel/utils/native-variables.ts rename babel/native/utils/process-styles.js => src/babel/utils/process-styles.ts (52%) create mode 100644 src/babel/utils/transform-class-names.ts create mode 100644 src/babel/web-visitor.ts create mode 100644 src/babel/web.ts delete mode 100644 src/shared/selector.js create mode 100644 src/shared/selector.ts create mode 100644 src/types/node-modules.d.ts delete mode 100644 src/types/react-native-web.d.ts diff --git a/README.md b/README.md index 61aa405..ef50f2c 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/__tests__/fixtures.spec.js b/__tests__/fixtures.spec.ts similarity index 72% rename from __tests__/fixtures.spec.js rename to __tests__/fixtures.spec.ts index 3d3881e..764afee 100644 --- a/__tests__/fixtures.spec.js +++ b/__tests__/fixtures.spec.ts @@ -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, }); diff --git a/__tests__/native-context-fixtures/basic/code.js b/__tests__/native-context-fixtures/basic/code.ts similarity index 100% rename from __tests__/native-context-fixtures/basic/code.js rename to __tests__/native-context-fixtures/basic/code.ts diff --git a/__tests__/native-context-fixtures/basic/output.js b/__tests__/native-context-fixtures/basic/output.js deleted file mode 100644 index 9a0440f..0000000 --- a/__tests__/native-context-fixtures/basic/output.js +++ /dev/null @@ -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 = {}; diff --git a/__tests__/native-context-fixtures/basic/output.ts b/__tests__/native-context-fixtures/basic/output.ts new file mode 100644 index 0000000..e877aeb --- /dev/null +++ b/__tests__/native-context-fixtures/basic/output.ts @@ -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 ( + + Hello world! + + ); +} + +const __tailwindStyles = StyleSheet.create({ + "font-bold": { + fontWeight: "700", + }, +}); + +const __tailwindMedia = {}; diff --git a/__tests__/native-inline-fixtures/basic/code.js b/__tests__/native-inline-fixtures/basic/code.ts similarity index 100% rename from __tests__/native-inline-fixtures/basic/code.js rename to __tests__/native-inline-fixtures/basic/code.ts diff --git a/__tests__/native-inline-fixtures/basic/options.json b/__tests__/native-inline-fixtures/basic/options.json index 308d14a..05821a5 100644 --- a/__tests__/native-inline-fixtures/basic/options.json +++ b/__tests__/native-inline-fixtures/basic/options.json @@ -1,4 +1,3 @@ { - "tailwindConfigPath": "./__tests__/native-inline-fixtures/basic/tailwind.config.js", "platform": "native-inline" } diff --git a/__tests__/native-inline-fixtures/basic/output.js b/__tests__/native-inline-fixtures/basic/output.js deleted file mode 100644 index 30c88c8..0000000 --- a/__tests__/native-inline-fixtures/basic/output.js +++ /dev/null @@ -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 = {}; diff --git a/__tests__/native-inline-fixtures/basic/output.ts b/__tests__/native-inline-fixtures/basic/output.ts new file mode 100644 index 0000000..8cfbf25 --- /dev/null +++ b/__tests__/native-inline-fixtures/basic/output.ts @@ -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 ( + + + Hello world! + + + ); +} + +const __tailwindStyles = StyleSheet.create({ + "font-bold": { + fontWeight: "700", + }, +}); + +const __tailwindMedia = {}; diff --git a/__tests__/native-inline-fixtures/basic/tailwind.config.js b/__tests__/native-inline-fixtures/basic/tailwind.config.js deleted file mode 100644 index 4b4d7a3..0000000 --- a/__tests__/native-inline-fixtures/basic/tailwind.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - content: [`${__dirname}/*.{js,ts,jsx,tsx}`], -}; diff --git a/__tests__/native-inline-fixtures/conditionals/code.js b/__tests__/native-inline-fixtures/conditionals/code.ts similarity index 100% rename from __tests__/native-inline-fixtures/conditionals/code.js rename to __tests__/native-inline-fixtures/conditionals/code.ts diff --git a/__tests__/native-inline-fixtures/conditionals/options.json b/__tests__/native-inline-fixtures/conditionals/options.json index 67a5eaf..05821a5 100644 --- a/__tests__/native-inline-fixtures/conditionals/options.json +++ b/__tests__/native-inline-fixtures/conditionals/options.json @@ -1,4 +1,3 @@ { - "tailwindConfigPath": "./__tests__/native-inline-fixtures/conditionals/tailwind.config.js", "platform": "native-inline" } diff --git a/__tests__/native-inline-fixtures/conditionals/output.js b/__tests__/native-inline-fixtures/conditionals/output.js deleted file mode 100644 index c1f6640..0000000 --- a/__tests__/native-inline-fixtures/conditionals/output.js +++ /dev/null @@ -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 = {}; diff --git a/__tests__/native-inline-fixtures/conditionals/output.ts b/__tests__/native-inline-fixtures/conditionals/output.ts new file mode 100644 index 0000000..067f59a --- /dev/null +++ b/__tests__/native-inline-fixtures/conditionals/output.ts @@ -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 ( + + + Hello world! + + + ); +} + +const __tailwindStyles = StyleSheet.create({ + "font-bold": { + fontWeight: "700", + }, + underline: { + textDecorationLine: "underline", + }, +}); + +const __tailwindMedia = {}; diff --git a/__tests__/native-inline-fixtures/conditionals/tailwind.config.js b/__tests__/native-inline-fixtures/conditionals/tailwind.config.js deleted file mode 100644 index 4b4d7a3..0000000 --- a/__tests__/native-inline-fixtures/conditionals/tailwind.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - content: [`${__dirname}/*.{js,ts,jsx,tsx}`], -}; diff --git a/__tests__/native-inline-fixtures/empty-className/code.js b/__tests__/native-inline-fixtures/empty-className/code.ts similarity index 100% rename from __tests__/native-inline-fixtures/empty-className/code.js rename to __tests__/native-inline-fixtures/empty-className/code.ts diff --git a/__tests__/native-inline-fixtures/empty-className/options.json b/__tests__/native-inline-fixtures/empty-className/options.json index a4d3376..05821a5 100644 --- a/__tests__/native-inline-fixtures/empty-className/options.json +++ b/__tests__/native-inline-fixtures/empty-className/options.json @@ -1,4 +1,3 @@ { - "tailwindConfigPath": "./__tests__/native-inline-fixtures/empty-className/tailwind.config.js", "platform": "native-inline" } diff --git a/__tests__/native-inline-fixtures/empty-className/output.js b/__tests__/native-inline-fixtures/empty-className/output.js deleted file mode 100644 index 7963a65..0000000 --- a/__tests__/native-inline-fixtures/empty-className/output.js +++ /dev/null @@ -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 = {}; diff --git a/__tests__/native-inline-fixtures/empty-className/output.ts b/__tests__/native-inline-fixtures/empty-className/output.ts new file mode 100644 index 0000000..5cf11d8 --- /dev/null +++ b/__tests__/native-inline-fixtures/empty-className/output.ts @@ -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 ( + + + Hello world! + + + ); +} + +const __tailwindStyles = StyleSheet.create({}); + +const __tailwindMedia = {}; diff --git a/__tests__/native-inline-fixtures/empty-className/tailwind.config.js b/__tests__/native-inline-fixtures/empty-className/tailwind.config.js deleted file mode 100644 index 4b4d7a3..0000000 --- a/__tests__/native-inline-fixtures/empty-className/tailwind.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - content: [`${__dirname}/*.{js,ts,jsx,tsx}`], -}; diff --git a/__tests__/native-inline-fixtures/existing-style/code.js b/__tests__/native-inline-fixtures/existing-style/code.ts similarity index 100% rename from __tests__/native-inline-fixtures/existing-style/code.js rename to __tests__/native-inline-fixtures/existing-style/code.ts diff --git a/__tests__/native-inline-fixtures/existing-style/options.json b/__tests__/native-inline-fixtures/existing-style/options.json index 8ba6e0e..05821a5 100644 --- a/__tests__/native-inline-fixtures/existing-style/options.json +++ b/__tests__/native-inline-fixtures/existing-style/options.json @@ -1,4 +1,3 @@ { - "tailwindConfigPath": "./__tests__/native-inline-fixtures/existing-style/tailwind.config.js", "platform": "native-inline" } diff --git a/__tests__/native-inline-fixtures/existing-style/output.js b/__tests__/native-inline-fixtures/existing-style/output.js deleted file mode 100644 index 4406873..0000000 --- a/__tests__/native-inline-fixtures/existing-style/output.js +++ /dev/null @@ -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 = {}; diff --git a/__tests__/native-inline-fixtures/existing-style/output.ts b/__tests__/native-inline-fixtures/existing-style/output.ts new file mode 100644 index 0000000..64abb82 --- /dev/null +++ b/__tests__/native-inline-fixtures/existing-style/output.ts @@ -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 ( + + + Hello world! + + + ); +} +const styles = StyleSheet.create({ + test: { + color: "blue", + }, +}); + +const __tailwindStyles = StyleSheet.create({ + "font-bold": { + fontWeight: "700", + }, +}); + +const __tailwindMedia = {}; diff --git a/__tests__/native-inline-fixtures/existing-style/tailwind.config.js b/__tests__/native-inline-fixtures/existing-style/tailwind.config.js deleted file mode 100644 index 4b4d7a3..0000000 --- a/__tests__/native-inline-fixtures/existing-style/tailwind.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - content: [`${__dirname}/*.{js,ts,jsx,tsx}`], -}; diff --git a/__tests__/native-inline-fixtures/existing-styles/code.js b/__tests__/native-inline-fixtures/existing-styles/code.ts similarity index 92% rename from __tests__/native-inline-fixtures/existing-styles/code.js rename to __tests__/native-inline-fixtures/existing-styles/code.ts index e0edad3..8c96383 100644 --- a/__tests__/native-inline-fixtures/existing-styles/code.js +++ b/__tests__/native-inline-fixtures/existing-styles/code.ts @@ -13,5 +13,5 @@ export function Test() { const styles = StyleSheet.create({ test: { color: "blue" }, - test2: { color: "blue" }, + test2: { color: "red" }, }); diff --git a/__tests__/native-inline-fixtures/existing-styles/options.json b/__tests__/native-inline-fixtures/existing-styles/options.json index 255a233..05821a5 100644 --- a/__tests__/native-inline-fixtures/existing-styles/options.json +++ b/__tests__/native-inline-fixtures/existing-styles/options.json @@ -1,4 +1,3 @@ { - "tailwindConfigPath": "./__tests__/native-inline-fixtures/existing-styles/tailwind.config.js", "platform": "native-inline" } diff --git a/__tests__/native-inline-fixtures/existing-styles/output.js b/__tests__/native-inline-fixtures/existing-styles/output.js deleted file mode 100644 index 4d915ee..0000000 --- a/__tests__/native-inline-fixtures/existing-styles/output.js +++ /dev/null @@ -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 = {}; diff --git a/__tests__/native-inline-fixtures/existing-styles/output.ts b/__tests__/native-inline-fixtures/existing-styles/output.ts new file mode 100644 index 0000000..9961958 --- /dev/null +++ b/__tests__/native-inline-fixtures/existing-styles/output.ts @@ -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 ( + + + Hello world! + + + ); +} +const styles = StyleSheet.create({ + test: { + color: "blue", + }, + test2: { + color: "red", + }, +}); + +const __tailwindStyles = StyleSheet.create({ + "font-bold": { + fontWeight: "700", + }, +}); + +const __tailwindMedia = {}; diff --git a/__tests__/native-inline-fixtures/existing-styles/tailwind.config.js b/__tests__/native-inline-fixtures/existing-styles/tailwind.config.js deleted file mode 100644 index 4b4d7a3..0000000 --- a/__tests__/native-inline-fixtures/existing-styles/tailwind.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - content: [`${__dirname}/*.{js,ts,jsx,tsx}`], -}; diff --git a/__tests__/native-inline-fixtures/ignores-invalid-classes/options.json b/__tests__/native-inline-fixtures/ignores-invalid-classes/options.json deleted file mode 100644 index 48a27ac..0000000 --- a/__tests__/native-inline-fixtures/ignores-invalid-classes/options.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "tailwindConfigPath": "./__tests__/native-inline-fixtures/ignores-invalid-classes/tailwind.config.js", - "platform": "native-inline" -} diff --git a/__tests__/native-inline-fixtures/ignores-invalid-classes/output.js b/__tests__/native-inline-fixtures/ignores-invalid-classes/output.js deleted file mode 100644 index d20abc5..0000000 --- a/__tests__/native-inline-fixtures/ignores-invalid-classes/output.js +++ /dev/null @@ -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 = {}; diff --git a/__tests__/native-inline-fixtures/ignores-invalid-classes/tailwind.config.js b/__tests__/native-inline-fixtures/ignores-invalid-classes/tailwind.config.js deleted file mode 100644 index 4b4d7a3..0000000 --- a/__tests__/native-inline-fixtures/ignores-invalid-classes/tailwind.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - content: [`${__dirname}/*.{js,ts,jsx,tsx}`], -}; diff --git a/__tests__/web-fixtures/basic/code.js b/__tests__/web-fixtures/basic/code.ts similarity index 100% rename from __tests__/web-fixtures/basic/code.js rename to __tests__/web-fixtures/basic/code.ts diff --git a/__tests__/web-fixtures/basic/output.js b/__tests__/web-fixtures/basic/output.js deleted file mode 100644 index 9eeafa9..0000000 --- a/__tests__/web-fixtures/basic/output.js +++ /dev/null @@ -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!", - }), - }); -} diff --git a/__tests__/native-inline-fixtures/ignores-invalid-classes/code.js b/__tests__/web-fixtures/basic/output.ts similarity index 53% rename from __tests__/native-inline-fixtures/ignores-invalid-classes/code.js rename to __tests__/web-fixtures/basic/output.ts index 01238c5..c5ecb61 100644 --- a/__tests__/native-inline-fixtures/ignores-invalid-classes/code.js +++ b/__tests__/web-fixtures/basic/output.ts @@ -1,10 +1,16 @@ import { Text } from "react-native"; import { TailwindProvider } from "../../../src"; - export function Test() { return ( - Hello world! + + Hello world! + ); } diff --git a/__tests__/web-fixtures/ignores-native-element/output.js b/__tests__/web-fixtures/ignores-native-element/output.js index 79a35a8..5277647 100644 --- a/__tests__/web-fixtures/ignores-native-element/output.js +++ b/__tests__/web-fixtures/ignores-native-element/output.js @@ -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 ( + + + Hello world! + +
Should be untransformed
+
+ ); } diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index 66327f5..0000000 --- a/babel.config.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = { - presets: [ - [ - "module:metro-react-native-babel-preset", - { useTransformReactJSXExperimental: true }, - ], - ], - plugins: [ - [ - "@babel/plugin-transform-react-jsx", - { - runtime: "automatic", - }, - ], - ], -}; diff --git a/babel.js b/babel.js new file mode 100644 index 0000000..e42d8d2 --- /dev/null +++ b/babel.js @@ -0,0 +1 @@ +module.exports = require("./dist/babel").default; diff --git a/babel/index.js b/babel/index.js deleted file mode 100644 index cdc8a7f..0000000 --- a/babel/index.js +++ /dev/null @@ -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; diff --git a/babel/native/context.js b/babel/native/context.js deleted file mode 100644 index f2fbea3..0000000 --- a/babel/native/context.js +++ /dev/null @@ -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; diff --git a/babel/native/index.js b/babel/native/index.js deleted file mode 100644 index be6d034..0000000 --- a/babel/native/index.js +++ /dev/null @@ -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; diff --git a/babel/native/inline.js b/babel/native/inline.js deleted file mode 100644 index 2bbc424..0000000 --- a/babel/native/inline.js +++ /dev/null @@ -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: `` - * Out: - * - * ``` - * import { __useParseTailwind } from 'tailwind-react-native' - * import { StyleSheet } from 'react-native' - * - * const __tailwindStyles = StyleSheet.create() - * const __tailwindMedia = - * ``` - */ -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; diff --git a/babel/native/utils/babel-imports.js b/babel/native/utils/babel-imports.js deleted file mode 100644 index 9d7f780..0000000 --- a/babel/native/utils/babel-imports.js +++ /dev/null @@ -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, -} diff --git a/babel/native/utils/babel-variables.js b/babel/native/utils/babel-variables.js deleted file mode 100644 index 00de43d..0000000 --- a/babel/native/utils/babel-variables.js +++ /dev/null @@ -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 diff --git a/babel/native/utils/flatten-rules.js b/babel/native/utils/flatten-rules.js deleted file mode 100644 index 117a932..0000000 --- a/babel/native/utils/flatten-rules.js +++ /dev/null @@ -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 | undefined} media - * @returns {Array} - */ -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} */ - const selectors = (cssRule.selectors || []).map((selectorDirty) => { - const selector = normaliseSelector(selectorDirty, options); - - return { - selector, - media, - rules, - }; - }); - - return selectors; - } else { - return []; - } - }); -} - -module.exports = flattenRules; diff --git a/babel/native/utils/transform-class-names.js b/babel/native/utils/transform-class-names.js deleted file mode 100644 index 940bb7f..0000000 --- a/babel/native/utils/transform-class-names.js +++ /dev/null @@ -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; diff --git a/babel/web.js b/babel/web.js deleted file mode 100644 index 587aa84..0000000 --- a/babel/web.js +++ /dev/null @@ -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; diff --git a/docs/library-comparision.md b/docs/library-comparision.md index 00450b8..6e41f95 100644 --- a/docs/library-comparision.md +++ b/docs/library-comparision.md @@ -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` diff --git a/docs/platforms.md b/docs/platforms.md index c384829..6d969a7 100644 --- a/docs/platforms.md +++ b/docs/platforms.md @@ -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" diff --git a/jest.config.js b/jest.config.js index 10db84f..cbd76b8 100644 --- a/jest.config.js +++ b/jest.config.js @@ -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/", diff --git a/package-lock.json b/package-lock.json index cbcd03b..aa0a7c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index c5138bb..8166ba7 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/babel/index.ts b/src/babel/index.ts new file mode 100644 index 0000000..859fa68 --- /dev/null +++ b/src/babel/index.ts @@ -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 + ); +} diff --git a/src/babel/native-context.ts b/src/babel/native-context.ts new file mode 100644 index 0000000..b12148e --- /dev/null +++ b/src/babel/native-context.ts @@ -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, 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 = { + 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")) + ) + ); + } + }, +}; diff --git a/src/babel/native-inline.ts b/src/babel/native-inline.ts new file mode 100644 index 0000000..de3b916 --- /dev/null +++ b/src/babel/native-inline.ts @@ -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, 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); + }, + }, + }, + }; +} diff --git a/src/babel/native-visitor.ts b/src/babel/native-visitor.ts new file mode 100644 index 0000000..d5a4e77 --- /dev/null +++ b/src/babel/native-visitor.ts @@ -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; + hasUseParseTailwind: boolean; + hasStyleSheetImport: boolean; + hasClassNames: boolean; + hasProvider: boolean; + visitor?: Visitor; + 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 = { + 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); + } + }, +}; diff --git a/src/babel/types.d.ts b/src/babel/types.d.ts new file mode 100644 index 0000000..ee26988 --- /dev/null +++ b/src/babel/types.d.ts @@ -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; + deniedImports?: Array; +} + +export type State = { + get: (name: string) => any; + set: (name: string, value: any) => any; + opts: TailwindReactNativeOptions; + file: BabelCore.BabelFile; + filename: string; +}; diff --git a/src/babel/utils/flatten-rules.ts b/src/babel/utils/flatten-rules.ts new file mode 100644 index 0000000..3da09c1 --- /dev/null +++ b/src/babel/utils/flatten-rules.ts @@ -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"; +} diff --git a/babel/native/utils/get-tailwind-config.js b/src/babel/utils/get-tailwind-config.ts similarity index 54% rename from babel/native/utils/get-tailwind-config.js rename to src/babel/utils/get-tailwind-config.ts index af563d5..d0650bf 100644 --- a/babel/native/utils/get-tailwind-config.js +++ b/src/babel/utils/get-tailwind-config.ts @@ -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; diff --git a/src/babel/utils/imports.ts b/src/babel/utils/imports.ts new file mode 100644 index 0000000..39cc931 --- /dev/null +++ b/src/babel/utils/imports.ts @@ -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, + 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, + 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; + }); +} diff --git a/babel/native/utils/is-valid-style.js b/src/babel/utils/is-valid-style.ts similarity index 90% rename from babel/native/utils/is-valid-style.js rename to src/babel/utils/is-valid-style.ts index a6f4508..d3f4a73 100644 --- a/babel/native/utils/is-valid-style.js +++ b/src/babel/utils/is-valid-style.ts @@ -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; diff --git a/src/babel/utils/jsx.ts b/src/babel/utils/jsx.ts new file mode 100644 index 0000000..4efc844 --- /dev/null +++ b/src/babel/utils/jsx.ts @@ -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: + * + * + * + * + * + * ..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 +) { + let style: JSXEmptyExpression | Expression | undefined; + let styleAttribute: NodePath | undefined; + let className: StringLiteral | Expression | undefined; + let classNameAttribute: + | NodePath + | undefined; + + /** + * Loop over all the attributes + * + * While silly, this is valid JSX + * + * + * + * 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; +} diff --git a/src/babel/utils/native-variables.ts b/src/babel/utils/native-variables.ts new file mode 100644 index 0000000..7f32257 --- /dev/null +++ b/src/babel/utils/native-variables.ts @@ -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), + ]) + ); +} diff --git a/babel/native/utils/process-styles.js b/src/babel/utils/process-styles.ts similarity index 52% rename from babel/native/utils/process-styles.js rename to src/babel/utils/process-styles.ts index 3e9f2de..606cd27 100644 --- a/babel/native/utils/process-styles.js +++ b/src/babel/utils/process-styles.ts @@ -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} */ - const styles = {}; - /** @type {Record} */ - const mediaRules = {}; + const styles: Record = {}; + 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; diff --git a/src/babel/utils/transform-class-names.ts b/src/babel/utils/transform-class-names.ts new file mode 100644 index 0000000..d3bfc69 --- /dev/null +++ b/src/babel/utils/transform-class-names.ts @@ -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, + { 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 = 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; +} diff --git a/src/babel/web-visitor.ts b/src/babel/web-visitor.ts new file mode 100644 index 0000000..ea39130 --- /dev/null +++ b/src/babel/web-visitor.ts @@ -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 = { + 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; + } + }, +}; diff --git a/src/babel/web.ts b/src/babel/web.ts new file mode 100644 index 0000000..b1e73ec --- /dev/null +++ b/src/babel/web.ts @@ -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, 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); + }, + }, + }, + }; +} diff --git a/src/context.ts b/src/context.ts index 798fae6..676d994 100644 --- a/src/context.ts +++ b/src/context.ts @@ -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 +export type StyleRecord = Record; export type MediaRules = Record< string, Array<{ media: string[]; suffix: number }> -> +>; export const TailwindStyleContext = createContext({ styles: {}, media: {}, -}) +}); export const TailwindColorSchemeContext = createContext( Appearance.getColorScheme() -) +); export const TailwindSetColorSchemeContext = createContext< (colorScheme: ColorSchemeName) => void ->(() => {}) +>(() => {}); diff --git a/src/index.ts b/src/index.ts index 67dc56c..048880b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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"; diff --git a/src/shared/selector.js b/src/shared/selector.js deleted file mode 100644 index 55477b3..0000000 --- a/src/shared/selector.js +++ /dev/null @@ -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; diff --git a/src/shared/selector.ts b/src/shared/selector.ts new file mode 100644 index 0000000..9b2b6a4 --- /dev/null +++ b/src/shared/selector.ts @@ -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 = {} +) { + const leadingDots = "^\\."; + + const regex = + typeof important === "string" + ? new RegExp(`^${important}|${leadingDots}`) + : new RegExp(leadingDots); + + return selector.replace(regex, "").replace(/\s/g, "_"); +} diff --git a/src/types/node-modules.d.ts b/src/types/node-modules.d.ts new file mode 100644 index 0000000..09a98af --- /dev/null +++ b/src/types/node-modules.d.ts @@ -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; +} diff --git a/src/types/react-native-web.d.ts b/src/types/react-native-web.d.ts deleted file mode 100644 index 969de0d..0000000 --- a/src/types/react-native-web.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare module "react-native-web" { - export const Appearance = any; -} diff --git a/src/use-parse-tailwind.ts b/src/use-parse-tailwind.ts index ea5e285..035ddbe 100644 --- a/src/use-parse-tailwind.ts +++ b/src/use-parse-tailwind.ts @@ -1,5 +1,5 @@ import { useContext } from "react"; -import normaliseSelector from "./shared/selector"; +import { normaliseSelector } from "./shared/selector"; import { MediaRules, StyleRecord,