Skip to content

Commit cf115c3

Browse files
authored
fix: tailwind's handling of transform styles (#102)
1 parent 46bc0ab commit cf115c3

File tree

14 files changed

+924
-160
lines changed

14 files changed

+924
-160
lines changed

.config/jest.config.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ process.env.REACT_NATIVE_CSS_TEST_DEBUG = true;
77

88
module.exports = {
99
...jestExpo,
10-
testPathIgnorePatterns: ["dist/"],
10+
testPathIgnorePatterns: ["dist/", ".*/_[a-zA-Z]"],
1111
setupFilesAfterEnv: ["./.config/jest.setup.js"],
1212
};

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@
157157
"@commitlint/config-conventional": "^19.8.1",
158158
"@eslint/js": "^9.30.1",
159159
"@ianvs/prettier-plugin-sort-imports": "^4.4.2",
160+
"@tailwindcss/postcss": "^4.1.12",
160161
"@testing-library/react-native": "^13.2.0",
161162
"@tsconfig/react-native": "^3.0.6",
162163
"@types/babel__core": "^7",
@@ -179,6 +180,7 @@
179180
"lefthook": "^1.12.2",
180181
"lightningcss": "^1.30.1",
181182
"metro-runtime": "^0.83.0",
183+
"postcss": "^8.5.6",
182184
"prettier": "^3.6.2",
183185
"react": "19.1.0",
184186
"react-native": "0.80.1",
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import type { PropsWithChildren, ReactElement } from "react";
2+
3+
import tailwind from "@tailwindcss/postcss";
4+
import {
5+
screen,
6+
render as tlRender,
7+
type RenderOptions,
8+
} from "@testing-library/react-native";
9+
import postcss from "postcss";
10+
import { registerCSS } from "react-native-css/jest";
11+
12+
import type { compile } from "../../../compiler";
13+
import { View } from "../../../components";
14+
15+
const testID = "tailwind";
16+
17+
export type NativewindRenderOptions = RenderOptions & {
18+
/** Replace the generated CSS*/
19+
css?: string;
20+
/** Appended after the generated CSS */
21+
extraCss?: string;
22+
/** Specify the className to use for the component @default sourceInline */
23+
className?: string;
24+
/** Add `@source inline('<className>')` to the CSS. @default Values are extracted from the component's className */
25+
sourceInline?: string[];
26+
/** Whether to include the theme in the generated CSS @default true */
27+
theme?: boolean;
28+
/** Whether to include the preflight in the generated CSS @default false */
29+
preflight?: boolean;
30+
/** Whether to include the plugin in the generated CSS. @default true */
31+
plugin?: boolean;
32+
/** Enable debug logging. @default false - Set process.env.NATIVEWIND_TEST_AUTO_DEBUG and run tests with the node inspector */
33+
debug?: boolean;
34+
};
35+
36+
const debugDefault = Boolean(process.env.NODE_OPTIONS?.includes("--inspect"));
37+
38+
export async function render(
39+
component: ReactElement<PropsWithChildren>,
40+
{
41+
css,
42+
sourceInline = Array.from(getClassNames(component)),
43+
debug = debugDefault,
44+
theme = true,
45+
preflight = false,
46+
extraCss,
47+
...options
48+
}: NativewindRenderOptions = {},
49+
): Promise<ReturnType<typeof tlRender> & ReturnType<typeof compile>> {
50+
if (!css) {
51+
css = ``;
52+
53+
if (theme) {
54+
css += `@import "tailwindcss/theme.css" layer(theme);\n`;
55+
}
56+
57+
if (preflight) {
58+
css += `@import "tailwindcss/preflight.css" layer(base);\n`;
59+
}
60+
61+
css += `@import "tailwindcss/utilities.css" layer(utilities) source(none);\n`;
62+
}
63+
64+
css += sourceInline
65+
.map((source) => `@source inline("${source}");`)
66+
.join("\n");
67+
68+
if (extraCss) {
69+
css += `\n${extraCss}`;
70+
}
71+
72+
if (debug) {
73+
console.log(`Input CSS:\n---\n${css}\n---\n`);
74+
}
75+
76+
// Process the TailwindCSS
77+
const { css: output } = await postcss([
78+
/* Tailwind seems to internally cache things, so we need a random value to cache bust */
79+
tailwind({ base: Date.now().toString() }),
80+
]).process(css, {
81+
from: __dirname,
82+
});
83+
84+
if (debug) {
85+
console.log(`Output CSS:\n---\n${output}\n---\n`);
86+
}
87+
88+
const compiled = registerCSS(output, { debug });
89+
90+
return Object.assign(
91+
{},
92+
tlRender(component, {
93+
...options,
94+
}),
95+
compiled,
96+
);
97+
}
98+
99+
render.debug = (
100+
component: ReactElement<PropsWithChildren>,
101+
options: RenderOptions = {},
102+
) => {
103+
return render(component, { ...options, debug: true });
104+
};
105+
106+
function getClassNames(
107+
component: ReactElement<PropsWithChildren>,
108+
classNames = new Set<string>(),
109+
) {
110+
if (
111+
typeof component.props === "object" &&
112+
"className" in component.props &&
113+
typeof component.props.className === "string"
114+
) {
115+
classNames.add(component.props.className);
116+
}
117+
118+
if (component.props.children) {
119+
const children: ReactElement[] = Array.isArray(component.props.children)
120+
? component.props.children
121+
: [component.props.children];
122+
123+
for (const child of children) {
124+
getClassNames(child as ReactElement<PropsWithChildren>, classNames);
125+
}
126+
}
127+
128+
return classNames;
129+
}
130+
131+
export async function renderSimple({
132+
className,
133+
...options
134+
}: NativewindRenderOptions & { className: string }) {
135+
const { warnings: warningFn } = await render(
136+
<View testID={testID} className={className} />,
137+
options,
138+
);
139+
const component = screen.getByTestId(testID, { hidden: true });
140+
141+
// Strip the testID and the children
142+
const { testID: _testID, children, ...props } = component.props;
143+
144+
const compilerWarnings = warningFn();
145+
146+
let warnings: Record<string, unknown> | undefined;
147+
148+
if (compilerWarnings.properties) {
149+
warnings ??= {};
150+
warnings.properties = compilerWarnings.properties;
151+
}
152+
153+
const warningValues = compilerWarnings.values;
154+
155+
if (warningValues) {
156+
warnings ??= {};
157+
warnings.values = Object.fromEntries(
158+
Object.entries(warningValues).map(([key, value]) => [
159+
key,
160+
value.length > 1 ? value : value[0],
161+
]),
162+
);
163+
}
164+
165+
return warnings ? { props, warnings } : { props };
166+
}
167+
168+
renderSimple.debug = (
169+
options: NativewindRenderOptions & { className: string },
170+
) => {
171+
return renderSimple({ ...options, debug: true });
172+
};
173+
174+
/**
175+
* Helper method that uses the current test name to render the component
176+
* Doesn't not support multiple components or changing the component type
177+
*/
178+
export async function renderCurrentTest({
179+
sourceInline = [expect.getState().currentTestName?.split(/\s+/).at(-1) ?? ""],
180+
className = sourceInline.join(" "),
181+
...options
182+
}: NativewindRenderOptions = {}) {
183+
return renderSimple({
184+
...options,
185+
sourceInline,
186+
className,
187+
});
188+
}
189+
190+
renderCurrentTest.debug = (options: NativewindRenderOptions = {}) => {
191+
return renderCurrentTest({ ...options, debug: true });
192+
};

0 commit comments

Comments
 (0)