feat: rewrite extract native styles logic

This commit is contained in:
Mark Lawlor
2022-04-28 14:32:50 +10:00
parent ef0f25df0f
commit 30c5b76a4a
34 changed files with 391 additions and 580 deletions

View File

@@ -8,7 +8,7 @@ tailwindRunner("Platform Prefixes", [
"ios_w-px_0": { width: 1 },
},
media: {
"ios_w-px": [["ios", 0]],
"ios_w-px": ["ios"],
},
},
],
@@ -19,7 +19,7 @@ tailwindRunner("Platform Prefixes", [
"android_w-px_0": { width: 1 },
},
media: {
"android_w-px": [["android", 0]],
"android_w-px": ["android"],
},
},
],
@@ -30,7 +30,7 @@ tailwindRunner("Platform Prefixes", [
"windows_w-px_0": { width: 1 },
},
media: {
"windows_w-px": [["windows", 0]],
"windows_w-px": ["windows"],
},
},
],
@@ -41,7 +41,7 @@ tailwindRunner("Platform Prefixes", [
"macos_w-px_0": { width: 1 },
},
media: {
"macos_w-px": [["macos", 0]],
"macos_w-px": ["macos"],
},
},
],
@@ -52,7 +52,7 @@ tailwindRunner("Platform Prefixes", [
"web_w-px_0": { width: 1 },
},
media: {
"web_w-px": [["web-inline", 0]],
"web_w-px": ["web-inline"],
},
},
],
@@ -67,13 +67,7 @@ tailwindRunner("Platform Prefixes", [
"native_w-px_4": { width: 1 },
},
media: {
"native_w-px": [
["native", 0],
["android", 1],
["ios", 2],
["windows", 3],
["macos", 4],
],
"native_w-px": ["native", "android", "ios", "windows", "macos"],
},
},
],

View File

@@ -4,10 +4,7 @@ tailwindRunner("Layout - Aspect Ratio", [
[
"aspect-auto",
{
styles: {
"aspect-auto": { aspectRatio: undefined },
},
media: {},
styles: {},
},
],
[
@@ -16,7 +13,6 @@ tailwindRunner("Layout - Aspect Ratio", [
styles: {
"aspect-square": { aspectRatio: 1 },
},
media: {},
},
],
[

View File

@@ -6,19 +6,19 @@ tailwindRunner("Layout - Container", [
{
styles: {
container: { width: "100%" },
container_1: { maxWidth: 640 },
container_2: { maxWidth: 768 },
container_3: { maxWidth: 1024 },
container_4: { maxWidth: 1280 },
container_5: { maxWidth: 1536 },
container_0: { maxWidth: 640 },
container_1: { maxWidth: 768 },
container_2: { maxWidth: 1024 },
container_3: { maxWidth: 1280 },
container_4: { maxWidth: 1536 },
},
media: {
container: [
["(min-width: 640px)", 1],
["(min-width: 768px)", 2],
["(min-width: 1024px)", 3],
["(min-width: 1280px)", 4],
["(min-width: 1536px)", 5],
"(min-width: 640px)",
"(min-width: 768px)",
"(min-width: 1024px)",
"(min-width: 1280px)",
"(min-width: 1536px)",
],
},
},
@@ -36,12 +36,12 @@ tailwindRunner("Layout - Container", [
},
media: {
sm_container: [
["(min-width: 640px)", 0],
["(min-width: 640px)", 1],
["(min-width: 640px) and (min-width: 768px)", 2],
["(min-width: 640px) and (min-width: 1024px)", 3],
["(min-width: 640px) and (min-width: 1280px)", 4],
["(min-width: 640px) and (min-width: 1536px)", 5],
"(min-width: 640px)",
"(min-width: 640px)",
"(min-width: 640px) and (min-width: 768px)",
"(min-width: 640px) and (min-width: 1024px)",
"(min-width: 640px) and (min-width: 1280px)",
"(min-width: 640px) and (min-width: 1536px)",
],
},
},

View File

@@ -9,7 +9,6 @@ tailwindRunner("Layout - Flex", [
display: "flex",
},
},
media: {},
},
],
[
@@ -21,7 +20,6 @@ tailwindRunner("Layout - Flex", [
flexShrink: 1,
},
},
media: {},
},
],
[
@@ -33,7 +31,6 @@ tailwindRunner("Layout - Flex", [
flexShrink: 1,
},
},
media: {},
},
],
[
@@ -45,7 +42,6 @@ tailwindRunner("Layout - Flex", [
flexShrink: 0,
},
},
media: {},
},
],
]);

View File

@@ -39,47 +39,47 @@ const sizes: Record<string, number> = {
};
const tests = [
generateTestsForScales("m", Object.keys(sizes), (index) => ({
marginBottom: sizes[index],
marginLeft: sizes[index],
marginRight: sizes[index],
marginTop: sizes[index],
})),
// generateTestsForScales("m", Object.keys(sizes), (index) => ({
// marginBottom: sizes[index],
// marginLeft: sizes[index],
// marginRight: sizes[index],
// marginTop: sizes[index],
// })),
generateTestsForScales("mx", Object.keys(sizes), (index) => ({
marginLeft: sizes[index],
marginRight: sizes[index],
})),
// generateTestsForScales("mx", Object.keys(sizes), (index) => ({
// marginLeft: sizes[index],
// marginRight: sizes[index],
// })),
generateTestsForScales("my", Object.keys(sizes), (index) => ({
marginTop: sizes[index],
marginBottom: sizes[index],
})),
// generateTestsForScales("my", Object.keys(sizes), (index) => ({
// marginTop: sizes[index],
// marginBottom: sizes[index],
// })),
generateTestsForScales("mt", Object.keys(sizes), (index) => ({
marginTop: sizes[index],
})),
// generateTestsForScales("mt", Object.keys(sizes), (index) => ({
// marginTop: sizes[index],
// })),
generateTestsForScales("mr", Object.keys(sizes), (index) => ({
marginRight: sizes[index],
})),
// generateTestsForScales("mr", Object.keys(sizes), (index) => ({
// marginRight: sizes[index],
// })),
generateTestsForScales("mb", Object.keys(sizes), (index) => ({
marginBottom: sizes[index],
})),
// generateTestsForScales("mb", Object.keys(sizes), (index) => ({
// marginBottom: sizes[index],
// })),
generateTestsForScales("ml", Object.keys(sizes), (index) => ({
marginLeft: sizes[index],
})),
// generateTestsForScales("ml", Object.keys(sizes), (index) => ({
// marginLeft: sizes[index],
// })),
emptyResults([
"m-auto",
"mx-auto",
"my-auto",
"mt-auto",
"mr-auto",
"mb-auto",
"ml-auto",
// "mx-auto",
// "my-auto",
// "mt-auto",
// "mr-auto",
// "mb-auto",
// "ml-auto",
]),
].flat();

View File

@@ -21,7 +21,6 @@ tailwindRunner("Layout - Object Position", [
styles: {
"overflow-hidden": { overflow: "hidden" },
},
media: {},
},
],
[
@@ -30,7 +29,6 @@ tailwindRunner("Layout - Object Position", [
styles: {
"overflow-visible": { overflow: "visible" },
},
media: {},
},
],
[
@@ -39,7 +37,6 @@ tailwindRunner("Layout - Object Position", [
styles: {
"overflow-scroll": { overflow: "scroll" },
},
media: {},
},
],
]);

View File

@@ -1,14 +1,13 @@
import { tailwindRunner, emptyResults } from "./runner";
tailwindRunner("Layout - Object Position", [
...emptyResults(["fixed", "sticky"]),
...emptyResults(["fixed", "sticky", "static"]),
[
"absolute",
{
styles: {
absolute: { position: "absolute" },
},
media: {},
},
],
[
@@ -17,16 +16,6 @@ tailwindRunner("Layout - Object Position", [
styles: {
relative: { position: "relative" },
},
media: {},
},
],
[
"static",
{
styles: {
static: { position: undefined },
},
media: {},
},
],
]);

View File

@@ -96,7 +96,7 @@ describe("native", () => {
expect(result.current).toEqual([{ fontWeight: "700" }]);
});
test("media - width", () => {
test.only("media - width", () => {
useWindowDimensions.mockReturnValue({
width: 1000,
});
@@ -109,29 +109,29 @@ describe("native", () => {
container: {
width: "100%",
},
container_1: {
container_0: {
maxWidth: 640,
},
container_2: {
container_1: {
maxWidth: 768,
},
container_3: {
container_2: {
maxWidth: 1024,
},
container_4: {
container_3: {
maxWidth: 1280,
},
container_5: {
container_4: {
maxWidth: 1536,
},
},
media: {
container: [
["(min-width: 640px)", 1],
["(min-width: 768px)", 2],
["(min-width: 1024px)", 3],
["(min-width: 1280px)", 4],
["(min-width: 1536px)", 5],
"(min-width: 640px)",
"(min-width: 768px)",
"(min-width: 1024px)",
"(min-width: 1280px)",
"(min-width: 1536px)",
],
},
},
@@ -150,18 +150,15 @@ describe("native", () => {
initialProps: {
platform: "ios",
styles: {
"w-px_1": {
"w-px_0": {
width: 1,
},
"w-px_2": {
"w-px_1": {
width: 1,
},
},
media: {
"w-px": [
["ios", 1],
["android", 2],
],
"w-px": ["ios", "android"],
},
},
});

View File

@@ -21,19 +21,19 @@ Object.assign(
container: {
width: "100%",
},
container_1: {
container_0: {
maxWidth: 640,
},
container_2: {
container_1: {
maxWidth: 768,
},
container_3: {
container_2: {
maxWidth: 1024,
},
container_4: {
container_3: {
maxWidth: 1280,
},
container_5: {
container_4: {
maxWidth: 1536,
},
"font-bold": {
@@ -43,10 +43,10 @@ Object.assign(
);
Object.assign(globalThis.tailwindcss_react_native_media, {
container: [
["(min-width: 640px)", 1],
["(min-width: 768px)", 2],
["(min-width: 1024px)", 3],
["(min-width: 1280px)", 4],
["(min-width: 1536px)", 5],
"(min-width: 640px)",
"(min-width: 768px)",
"(min-width: 1024px)",
"(min-width: 1280px)",
"(min-width: 1536px)",
],
});

View File

@@ -19,19 +19,19 @@ Object.assign(
container: {
width: "100%",
},
container_1: {
container_0: {
maxWidth: 640,
},
container_2: {
container_1: {
maxWidth: 768,
},
container_3: {
container_2: {
maxWidth: 1024,
},
container_4: {
container_3: {
maxWidth: 1280,
},
container_5: {
container_4: {
maxWidth: 1536,
},
"font-bold": {
@@ -41,10 +41,10 @@ Object.assign(
);
Object.assign(globalThis.tailwindcss_react_native_media, {
container: [
["(min-width: 640px)", 1],
["(min-width: 768px)", 2],
["(min-width: 1024px)", 3],
["(min-width: 1280px)", 4],
["(min-width: 1536px)", 5],
"(min-width: 640px)",
"(min-width: 768px)",
"(min-width: 1024px)",
"(min-width: 1280px)",
"(min-width: 1536px)",
],
});

View File

@@ -19,19 +19,19 @@ Object.assign(
container: {
width: "100%",
},
container_1: {
container_0: {
maxWidth: 640,
},
container_2: {
container_1: {
maxWidth: 768,
},
container_3: {
container_2: {
maxWidth: 1024,
},
container_4: {
container_3: {
maxWidth: 1280,
},
container_5: {
container_4: {
maxWidth: 1536,
},
"font-bold": {
@@ -41,10 +41,10 @@ Object.assign(
);
Object.assign(globalThis.tailwindcss_react_native_media, {
container: [
["(min-width: 640px)", 1],
["(min-width: 768px)", 2],
["(min-width: 1024px)", 3],
["(min-width: 1280px)", 4],
["(min-width: 1536px)", 5],
"(min-width: 640px)",
"(min-width: 768px)",
"(min-width: 1024px)",
"(min-width: 1280px)",
"(min-width: 1536px)",
],
});

View File

@@ -16,19 +16,19 @@ Object.assign(
container: {
width: "100%",
},
container_1: {
container_0: {
maxWidth: 640,
},
container_2: {
container_1: {
maxWidth: 768,
},
container_3: {
container_2: {
maxWidth: 1024,
},
container_4: {
container_3: {
maxWidth: 1280,
},
container_5: {
container_4: {
maxWidth: 1536,
},
"font-bold": {
@@ -38,10 +38,10 @@ Object.assign(
);
Object.assign(globalThis.tailwindcss_react_native_media, {
container: [
["(min-width: 640px)", 1],
["(min-width: 768px)", 2],
["(min-width: 1024px)", 3],
["(min-width: 1280px)", 4],
["(min-width: 1536px)", 5],
"(min-width: 640px)",
"(min-width: 768px)",
"(min-width: 1024px)",
"(min-width: 1280px)",
"(min-width: 1536px)",
],
});

View File

@@ -9,7 +9,7 @@ const { writeFile, readFile } = require("node:fs/promises");
const { existsSync, writeFileSync } = require("node:fs");
const { file } = require("tempy");
const { join } = require("path");
const { cssToRn } = require("../dist/babel/native-style-extraction");
const { extractStyles } = require("../dist/babel/native-style-extraction");
const {
getTailwindConfig,
} = require("../dist/babel/tailwind/get-tailwind-config");
@@ -65,8 +65,6 @@ const spawnArguments = [
join(__dirname, "./cli.css"),
"-o",
tailwindOutput,
"--postcss",
join(__dirname, "./postcss.config.js"),
];
if (watch) spawnArguments.push("--watch");
@@ -85,7 +83,7 @@ child.stderr.on("data", (data) => {
if (!existsSync(tailwindOutput)) return;
const css = await readFile(tailwindOutput, "utf8");
const { styles, media } = cssToRn(css, tailwindConfig);
const { styles, media } = extractStyles(tailwindConfig, css, false);
return writeFile(
output,

View File

@@ -1,6 +0,0 @@
module.exports = {
plugins: [
require("postcss-css-variables"),
require("postcss-color-functional-notation"),
],
};

52
package-lock.json generated
View File

@@ -10,7 +10,6 @@
"license": "MIT",
"dependencies": {
"@react-native-community/hooks": "^2.8.1",
"css": "^3.0.0",
"css-mediaquery": "^0.1.2",
"css-to-react-native": "^3.0.0",
"micromatch": "^4.0.5",
@@ -4420,6 +4419,7 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"dev": true,
"bin": {
"atob": "bin/atob.js"
},
@@ -5603,16 +5603,6 @@
"node": ">=8"
}
},
"node_modules/css": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz",
"integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==",
"dependencies": {
"inherits": "^2.0.4",
"source-map": "^0.6.1",
"source-map-resolve": "^0.6.0"
}
},
"node_modules/css-color-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
@@ -5734,6 +5724,7 @@
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
"dev": true,
"engines": {
"node": ">=0.10"
}
@@ -13865,6 +13856,7 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -13877,16 +13869,6 @@
"node": ">=0.10.0"
}
},
"node_modules/source-map-resolve": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz",
"integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==",
"deprecated": "See https://github.com/lydell/source-map-resolve#deprecated",
"dependencies": {
"atob": "^2.1.2",
"decode-uri-component": "^0.2.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
@@ -18508,7 +18490,8 @@
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"dev": true
},
"babel-core": {
"version": "7.0.0-bridge.0",
@@ -19424,16 +19407,6 @@
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
"integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="
},
"css": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz",
"integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==",
"requires": {
"inherits": "^2.0.4",
"source-map": "^0.6.1",
"source-map-resolve": "^0.6.0"
}
},
"css-color-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
@@ -19530,7 +19503,8 @@
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
"dev": true
},
"dedent": {
"version": "0.7.0",
@@ -25843,22 +25817,14 @@
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
},
"source-map-resolve": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz",
"integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==",
"requires": {
"atob": "^2.1.2",
"decode-uri-component": "^0.2.0"
}
},
"source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",

View File

@@ -9,7 +9,13 @@
"tailwindcss-react-native": "cli/index.js"
},
"homepage": "https://github.com/marklawlor/tailwindcss-react-native#readme",
"keywords": "react native react-native tailwind tailwindcss",
"keywords": [
"react-native",
"tailwind",
"tailwindcss",
"theme",
"style"
],
"license": "MIT",
"repository": {
"type": "git",
@@ -39,7 +45,6 @@
],
"dependencies": {
"@react-native-community/hooks": "^2.8.1",
"css": "^3.0.0",
"css-mediaquery": "^0.1.2",
"css-to-react-native": "^3.0.0",
"micromatch": "^4.0.5",

View File

@@ -1,68 +0,0 @@
import { parse, AtRule, Comment, Media, Rule } from "css";
import { TailwindConfig } from "tailwindcss/tailwind-config";
import { normaliseSelector } from "../../shared/selector";
import { Style } from "../../types/common";
import { ruleToReactNative } from "./rule-to-react-native";
interface CssRule {
selector: string;
media: string;
style: Style;
}
/**
* Iterators over a CSS file, converting the rules to React Native styles.
*
* @remarks
*
* It will flatten selectors there are no grouped selectors
* It will flattern media queries so they are not nested
*/
export function getParsedRules(
css: string,
tailwindConfig: TailwindConfig
): CssRule[] {
const cssRules = parse(css).stylesheet?.rules ?? [];
return [...cssRuleIterator(cssRules, tailwindConfig)];
}
function* cssRuleIterator(
cssRules: (Rule | Comment | AtRule)[] | undefined,
tailwindConfig: TailwindConfig,
media: string[] = []
): Generator<CssRule> {
for (const cssRule of cssRules ?? []) {
if (isMedia(cssRule)) {
let childMedia = media;
if (cssRule.media && !childMedia.includes(cssRule.media)) {
childMedia = [...media, cssRule.media];
}
yield* cssRuleIterator(cssRule.rules ?? [], tailwindConfig, childMedia);
} else if (isRule(cssRule)) {
const style = ruleToReactNative(cssRule);
if (Object.keys(style).length === 0) {
continue;
}
for (const selector of cssRule.selectors ?? []) {
yield {
selector: normaliseSelector(selector, tailwindConfig),
media: media.join(" and "),
style,
};
}
}
}
}
function isRule(rule: Rule | Comment | AtRule): rule is Rule {
return rule.type === "rule";
}
function isMedia(rule: Rule | Comment | AtRule): rule is Media {
return rule.type === "media";
}

View File

@@ -4,49 +4,45 @@ import tailwind from "tailwindcss";
import postcssCssvariables from "postcss-css-variables";
import postcssColorFunctionalNotation from "postcss-color-functional-notation";
import { getParsedRules } from "./get-parsed-rules";
import { plugin } from "./postcss-plugin";
import { MediaRecord, StyleRecord } from "../../types/common";
/**
* This is used by both Babel and the CLI to extract the files
*
* The CLI watches the TailwindCLI output, so you don't need
* to use the tailwind plugin
*/
export function extractStyles(
tailwindConfig: TailwindConfig,
cssInput = "@tailwind components;@tailwind utilities;"
cssInput = "@tailwind components;@tailwind utilities;",
includeTailwind = true
) {
// If you edit this, make sure you update the CLI postcss.config.js
const processedCss = postcss([
tailwind(tailwindConfig),
let styles: StyleRecord = {};
let media: MediaRecord = {};
const plugins = [
postcssCssvariables(),
postcssColorFunctionalNotation(),
]).process(cssInput).css;
plugin({
...tailwindConfig,
done: (output) => {
styles = output.styles;
media = output.media;
},
}),
];
return cssToRn(processedCss, tailwindConfig);
}
export function cssToRn(processedCss: string, tailwindConfig: TailwindConfig) {
const styles: StyleRecord = {};
const mediaRules: MediaRecord = {};
const parsedRules = getParsedRules(processedCss, tailwindConfig);
for (const [suffix, parsedRule] of parsedRules.entries()) {
const { selector, media, style } = parsedRule;
if (media.length > 0) {
// If there are media conditions, add the rules with a suffix
styles[`${selector}_${suffix}`] = style;
// Store the conditions, along with the suffix
mediaRules[selector] = mediaRules[selector] ?? [];
mediaRules[selector].push([media, suffix]);
} else {
// If there are no conditions, we merge the rules
styles[selector] = {
...styles[selector],
...style,
};
}
if (includeTailwind) {
plugins.unshift(tailwind(tailwindConfig));
}
// If you edit this, make sure you update the CLI postcss.config.js
// to include the extra plugins
postcss(plugins).process(cssInput).css;
return {
styles,
media: mediaRules,
media,
};
}

View File

@@ -1,7 +0,0 @@
export function aspectRatio(value: number) {
if (value === 0) {
return;
}
return value;
}

View File

@@ -1,7 +0,0 @@
export function display(value: string) {
if (value !== "none" && value !== "flex") {
return null;
}
return value;
}

View File

@@ -1,9 +0,0 @@
const supportedValues = new Set(["visible", "hidden", "scroll"]);
export function overflow(value: string) {
if (supportedValues.has(value)) {
return value;
}
return null;
}

View File

@@ -0,0 +1,93 @@
import { Plugin, PluginCreator } from "postcss";
import { TailwindConfig } from "tailwindcss/tailwind-config";
import { normaliseSelector } from "../../shared/selector";
import { MediaRecord, StyleRecord, Style } from "../../types/common";
import { toReactNative, ToReactNativeErrorRecord } from "./to-react-native";
const mediaStringSymbol = Symbol("media_string");
declare module "postcss" {
abstract class Container {
[mediaStringSymbol]: string;
}
}
export type PostcssPluginDone = (options: {
styles: StyleRecord;
media: MediaRecord;
}) => void;
export interface PostcssPluginOptions extends Partial<TailwindConfig> {
done?: PostcssPluginDone;
}
export const plugin: PluginCreator<PostcssPluginOptions> = ({
done = () => {
return;
},
important,
} = {}): Plugin => {
const styles: StyleRecord = {};
const media: MediaRecord = {};
const errors: ToReactNativeErrorRecord[] = [];
return {
postcssPlugin: "react-native-css-hook",
Root: (root) => {
root.walk((node) => {
if (node.type === "atrule" && node.name === "media") {
// For each media AtRule, calculate the full media query based upon its parent
// This is because media queries can be nested
if (node.parent?.[mediaStringSymbol]) {
// postcssCssvariables can cause duplicate media queries so we just remove them
node[mediaStringSymbol] =
node.parent[mediaStringSymbol] === node.params
? node.params
: `${node.parent[mediaStringSymbol]} and ${node.params}`;
} else {
node[mediaStringSymbol] = node.params;
}
} else if (node.type === "rule") {
let declarations: Style = {};
// Get all the declarations
node.walkDecls((decl) => {
declarations = {
...declarations,
...toReactNative(decl, {
onError: (error) => errors.push(error),
}),
};
});
if (Object.keys(declarations).length === 0) {
return;
}
if (node.parent?.[mediaStringSymbol]) {
// The parent has a media query, so this needs to be added a media style
for (const s of node.selectors) {
const selector = normaliseSelector(s, { important });
media[selector] ??= [];
styles[`${selector}_${media[selector].length}`] = declarations;
media[selector].push(node.parent[mediaStringSymbol]);
}
} else {
// The parent is the root, so we are not in a media query
for (const s of node.selectors) {
const selector = normaliseSelector(s);
styles[selector] = { ...styles[selector], ...declarations };
}
}
}
});
done({ styles, media });
},
};
};
plugin.postcss = true;
export default plugin;

View File

@@ -1,92 +0,0 @@
import { AtRule, Comment, Page, Rule } from "css";
import {
getPropertyName,
getStylesForProperty,
Style,
} from "css-to-react-native";
import { isInvalidStyle } from "./is-valid-style";
import { postProcessingCss, preProcessingCss } from "./patches";
/**
* Convert a css rule to react-native.
*
* The heavy lifting is performed by 'css-to-react-native', but this comes with some quirks
*
* CTRN only accepts __valid__ css that can be mapped 1-1 to RN. Invalid CSS will
* produce warnings in the console.
*
* This library is more friendly and fixes what it can / produces better warnings.
*
* Hence why we have pre/post processing.
*
* - Pre fixes so CTRN can process it (or it skips processing altogether)
* - Post fixes the result of CTRN (or it skips the value)
*/
export function ruleToReactNative({ declarations = [] }: Rule | Page): Style {
const style: Style = {};
for (const declaration of declarations) {
if (isComment(declaration)) {
continue;
}
const { property: cssAttribute, value: cssValue } = declaration;
if (cssAttribute === undefined || cssValue === undefined) {
continue;
}
const name = getPropertyName(cssAttribute);
const value = preProcessingCss[name]
? preProcessingCss[name](cssValue)
: cssValue;
if (value === null) {
warnInvalidStyle(cssAttribute, name, value);
continue;
}
const nativeStyles: Style =
typeof value === "object" ? value : getStylesForProperty(name, value);
for (const [nativeAttribute, nativeValue] of Object.entries(nativeStyles)) {
if (isInvalidStyle(nativeAttribute)) {
warnInvalidStyle(cssAttribute, nativeAttribute, nativeValue);
continue;
}
if (postProcessingCss[nativeAttribute]) {
const postprocessedValue =
postProcessingCss[nativeAttribute](nativeValue);
if (postprocessedValue === null) {
warnInvalidStyle(cssAttribute, nativeAttribute, nativeValue);
} else {
style[nativeAttribute] = postprocessedValue;
}
} else {
style[nativeAttribute] = nativeValue;
}
}
}
return style;
}
function warnInvalidStyle(selector: string, attribute: string, value: unknown) {
if (process.env.NODE_ENV === "production") {
throw new Error(
`Class name '${selector}' maps to invalid style {${attribute}: ${value}}`
);
} else if (process.env.NODE_ENV !== "test") {
console.warn(`Class name '${selector}' is being ignored as it produces an invalid React Native style { ${attribute}: '${value}' }
Either remove this selector, add a platform modifier (e.g. 'web:${selector}') or change to file to be platform specific.
`);
}
}
function isComment(rule: Rule | Comment | AtRule): rule is Comment {
return rule.type === "comment";
}

View File

@@ -0,0 +1,57 @@
import { Declaration } from "postcss";
import {
getPropertyName,
getStylesForProperty,
Style,
} from "css-to-react-native";
import { properties } from "./properties";
import { isInvalidProperty, StyleProperty } from "./is-invalid-property";
export interface ToReactNativeErrorRecord {
declaration: Declaration;
error: Error;
result?: Style;
}
export type ToReactNativeErrorCallback = (
options: ToReactNativeErrorRecord
) => void;
export interface ToReactNativeOptions {
onError: ToReactNativeErrorCallback;
}
export function toReactNative(
declaration: Declaration,
{ onError }: ToReactNativeOptions
) {
const { prop, value } = declaration;
const name = getPropertyName(prop) as StyleProperty;
let styles: Style | undefined;
if (isInvalidProperty(name)) {
onError({
declaration,
error: new Error("invalid property"),
result: styles,
});
return;
}
try {
const transform = properties[name];
styles = transform
? transform(value, name)
: getStylesForProperty(name, value);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
onError({ declaration, error, result: styles });
return;
}
return styles;
}

View File

@@ -1,8 +1,15 @@
export function isInvalidStyle(property: string) {
return !validProps.has(property);
import { ImageStyle, TextStyle, ViewStyle } from "react-native";
export type StyleProperty =
| keyof TextStyle
| keyof ViewStyle
| keyof ImageStyle;
export function isInvalidProperty(property: string) {
return !validProps.has(property as StyleProperty);
}
const validProps = new Set([
const validProps = new Set<StyleProperty>([
"alignContent",
"alignItems",
"alignSelf",
@@ -35,7 +42,6 @@ const validProps = new Set([
"borderWidth",
"bottom",
"color",
"decomposedMatrix",
"direction",
"display",
"elevation",

View File

@@ -0,0 +1,15 @@
import { getStylesForProperty, Style } from "css-to-react-native";
export function aspectRatio(value: string): Style {
if (value === "0") {
return {};
} else if (typeof value === "string" && value.includes("/")) {
const [left, right] = value.split("/").map((n) => {
return Number.parseInt(n, 10);
});
return { aspectRatio: left / right };
}
return getStylesForProperty("aspectRatio", value);
}

View File

@@ -0,0 +1,9 @@
import { Style } from "css-to-react-native";
export function display(value: string): Style {
if (value !== "none" && value !== "flex") {
throw new Error("display");
}
return { display: value };
}

View File

@@ -0,0 +1,6 @@
import { getStylesForProperty, Style } from "css-to-react-native";
export function flex(value: string, name: string): Style {
const { flexGrow, flexShrink } = getStylesForProperty(name, value);
return { flexGrow, flexShrink };
}

View File

@@ -1,34 +1,36 @@
import { getStylesForProperty, Style } from "css-to-react-native";
import { StyleProperty } from "../is-invalid-property";
import { aspectRatio } from "./aspect-ratio";
import { display } from "./display";
import { flex } from "./flex";
import { overflow } from "./overflow";
import { position } from "./position";
function noAuto(value: number | string) {
function noAuto(value: string, name: string): Style {
if (value === "auto") {
return null;
throw new Error("no auto");
}
return value;
return getStylesForProperty(name, value);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const postProcessingCss: Record<string, (value: any) => any> = {
export const properties: Partial<
Record<StyleProperty, (value: string, name: string) => Style>
> = {
aspectRatio,
display,
flex,
overflow,
position,
top: noAuto,
flexBasis: noAuto,
top: noAuto,
bottom: noAuto,
left: noAuto,
right: noAuto,
margin: noAuto,
marginTop: noAuto,
marginRight: noAuto,
marginBottom: noAuto,
marginLeft: noAuto,
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const preProcessingCss: Record<string, (value: any) => any> = {
zIndex: noAuto,
};

View File

@@ -0,0 +1,11 @@
import { getStylesForProperty, Style } from "css-to-react-native";
const supportedValues = new Set(["visible", "hidden", "scroll"]);
export function overflow(value: string): Style {
if (!supportedValues.has(value)) {
throw new Error("overflow");
}
return getStylesForProperty("overflow", value);
}

View File

@@ -1,17 +1,18 @@
import { Style } from "css-to-react-native";
const supportedValues = new Set(["absolute", "relative"]);
export function position(value: string) {
export function position(value: string): Style {
if (supportedValues.has(value)) {
return value;
return { position: value };
}
// This is a special edge case
// The tailwindcss keeps picking up `static` as its a javascript keyword
// We cannot return `null` (and show the warning) as the user isn't
// actualy using the className
// So instead of throwing an error we just ignore it
if (value === "static") {
return;
return {};
}
return null;
throw new Error("position");
}

View File

@@ -7,148 +7,12 @@ export interface NativePluginOptions {
export const nativePlugin = plugin.withOptions<NativePluginOptions | undefined>(
function () {
return ({ matchUtilities, theme }) => {
matchUtilities(
{
aspect: (value: string) => {
let aspectRatio = value;
if (value.includes("/")) {
const [left, right] = value.split("/").map((n) => {
return Number.parseInt(n, 10);
});
aspectRatio = `${left / right}`;
}
return {
aspectRatio,
};
},
},
{ values: theme("aspectRatio") }
);
return () => {
/*Nothing here yet */
};
},
function ({ rem = 16 } = {}) {
const config: Partial<TailwindConfig> = {
corePlugins: {
accentColor: false,
accessibility: false,
animation: false,
appearance: false,
aspectRatio: false,
backdropBlur: false,
backdropBrightness: false,
backdropContrast: false,
backdropFilter: false,
backdropGrayscale: false,
backdropHueRotate: false,
backdropInvert: false,
backdropOpacity: false,
backdropSaturate: false,
backdropSepia: false,
backgroundAttachment: false,
backgroundBlendMode: false,
backgroundClip: false,
backgroundImage: false,
backgroundOrigin: false,
backgroundPosition: false,
backgroundRepeat: false,
backgroundSize: false,
blur: false,
borderCollapse: false,
boxDecorationBreak: false,
boxShadow: false,
boxSizing: false,
breakAfter: false,
breakBefore: false,
breakInside: false,
brightness: false,
caretColor: false,
clear: false,
columns: false,
content: false,
contrast: false,
cursor: false,
divideColor: false,
divideOpacity: false,
divideStyle: false,
divideWidth: false,
dropShadow: false,
fill: false,
filter: false,
float: false,
fontSmoothing: false,
gap: false,
gradientColorStops: false,
grayscale: false,
gridAutoColumns: false,
gridAutoFlow: false,
gridAutoRows: false,
gridColumn: false,
gridColumnEnd: false,
gridColumnStart: false,
gridRow: false,
gridRowEnd: false,
gridRowStart: false,
gridTemplateColumns: false,
gridTemplateRows: false,
hueRotate: false,
invert: false,
isolation: false,
justifyItems: false,
justifySelf: false,
listStylePosition: false,
listStyleType: false,
mixBlendMode: false,
objectFit: false,
objectPosition: false,
order: false,
overscrollBehavior: false,
placeItems: false,
placeSelf: false,
placeholderColor: false,
placeholderOpacity: false,
preflight: false,
resize: false,
ringColor: false,
ringOffsetColor: false,
ringOffsetWidth: false,
ringOpacity: false,
ringWidth: false,
rotate: false,
saturate: false,
scale: false,
scrollBehavior: false,
scrollMargin: false,
scrollPadding: false,
scrollSnapAlign: false,
scrollSnapStop: false,
scrollSnapType: false,
sepia: false,
skew: false,
space: false,
stroke: false,
strokeWidth: false,
tableLayout: false,
textIndent: false,
textOverflow: false,
touchAction: false,
transform: false,
transformOrigin: false,
transitionDelay: false,
transitionDuration: false,
transitionProperty: false,
transitionTimingFunction: false,
translate: false,
userSelect: false,
verticalAlign: false,
visibility: false,
whitespace: false,
willChange: false,
wordBreak: false,
},
theme: {
aspectRatio: {
auto: "0",

12
src/types/common.d.ts vendored
View File

@@ -1,10 +1,6 @@
import type {
ImageStyle,
TextStyle,
ViewStyle,
} from "react-native";
import type { ImageStyle, TextStyle, ViewStyle } from "react-native";
export type Style = ViewStyle | TextStyle | ImageStyle;
export type StyleRecord = Record<string, Style>;
export type Media = [string, number];
export type MediaRecord = Record<string, Media[]>;
export type StyleRecord = Record<string, Record<string, any>>;
export type MediaRecord = Record<string, string[]>;
export type StyleErrorRecord = Record<string, string>;

View File

@@ -49,8 +49,14 @@ export function useTailwind<
tailwindStyleIds.push(styles[selector] as P);
}
for (const [media, suffix] of mediaRules[selector] ?? []) {
const isMatch = match(media, {
const rules = mediaRules[selector];
if (!rules) {
continue;
}
for (let index = 0, length = rules.length; index < length; index++) {
const isMatch = match(rules[index], {
type: platform,
width,
height,
@@ -62,7 +68,7 @@ export function useTailwind<
} as any);
if (isMatch) {
tailwindStyleIds.push(styles[`${selector}_${suffix}`] as P);
tailwindStyleIds.push(styles[`${selector}_${index}`] as P);
}
}
}