diff --git a/package.json b/package.json index eee8f789..b3020f72 100644 --- a/package.json +++ b/package.json @@ -246,7 +246,7 @@ "@react-native/jest-preset": "0.86.3", "@react-native/metro-config": "0.86.3", "@release-it/conventional-changelog": "10.0.1", - "@tailwindcss/postcss": "^4.1.12", + "@tailwindcss/node": "^4.1.12", "@testing-library/react-native": "^13.3.3", "@tsconfig/react-native": "^3.0.6", "@types/babel__core": "^7", @@ -266,7 +266,6 @@ "lefthook": "^2.1.5", "lightningcss": "^1.30.1", "metro-runtime": "^0.84.2", - "postcss": "^8.5.6", "prettier": "^3.6.2", "react": "19.2.3", "react-native": "0.86.3", diff --git a/src/__tests__/vendor/tailwind/_tailwind.tsx b/src/__tests__/vendor/tailwind/_tailwind.tsx index 3ba5db5e..9f5a6e56 100644 --- a/src/__tests__/vendor/tailwind/_tailwind.tsx +++ b/src/__tests__/vendor/tailwind/_tailwind.tsx @@ -1,12 +1,11 @@ import type { PropsWithChildren, ReactElement } from "react"; -import tailwind from "@tailwindcss/postcss"; +import { compile as compileTailwindCss } from "@tailwindcss/node"; import { screen, render as tlRender, type RenderOptions, } from "@testing-library/react-native"; -import postcss from "postcss"; import type { compile } from "react-native-css/compiler"; import { View } from "react-native-css/components"; import { registerCSS } from "react-native-css/jest"; @@ -74,13 +73,7 @@ export async function render( console.log(`Input CSS:\n---\n${css}\n---\n`); } - // Process the TailwindCSS - const { css: output } = await postcss([ - /* Tailwind seems to internally cache things, so we need a random value to cache bust */ - tailwind({ base: Date.now().toString() }), - ]).process(css, { - from: __dirname, - }); + const output = await compileTailwind(css); if (debug) { console.log(`Output CSS:\n---\n${output}\n---\n`); @@ -97,6 +90,28 @@ export async function render( ); } +// @tailwindcss/postcss keeps one compiler per from and base and reuses it for other CSS until a file it read changes, and only logs a failed compile, so each render compiles with the compiler it wraps +// https://github.com/tailwindlabs/tailwindcss/blob/v4.1.13/packages/@tailwindcss-postcss/src/index.ts#L32-L33 +// https://github.com/tailwindlabs/tailwindcss/blob/v4.1.13/packages/@tailwindcss-postcss/src/index.ts#L182-L196 +// https://github.com/tailwindlabs/tailwindcss/blob/v4.1.13/packages/@tailwindcss-postcss/src/index.ts#L334-L352 +async function compileTailwind(css: string): Promise { + const compiler = await compileTailwindCss(css, { + base: __dirname, + onDependency: () => undefined, + }); + + if ( + compiler.sources.some((source) => !source.negated) || + (compiler.root !== null && compiler.root !== "none") + ) { + throw new Error( + `The harness builds only the classes of @source inline(), and this CSS names a file source: ${JSON.stringify({ sources: compiler.sources, root: compiler.root })}`, + ); + } + + return compiler.build([]); +} + render.debug = ( component: ReactElement, options: RenderOptions = {}, diff --git a/src/__tests__/vendor/tailwind/harness.test.tsx b/src/__tests__/vendor/tailwind/harness.test.tsx new file mode 100644 index 00000000..8f6cc40b --- /dev/null +++ b/src/__tests__/vendor/tailwind/harness.test.tsx @@ -0,0 +1,70 @@ +import { View } from "react-native-css/components"; + +import { render } from "./_tailwind"; + +test("renders that read the same millisecond compile and apply their own classes", async () => { + const now = jest.spyOn(Date, "now").mockReturnValue(0); + + try { + for (const [className, style] of [ + ["left-0", { left: 0 }], + ["left-1", { left: 3.5 }], + ] as const) { + const { stylesheet, getByTestId } = await render( + , + ); + + expect(stylesheet().s?.map(([name]) => name)).toStrictEqual([className]); + expect(getByTestId(className).props.style).toStrictEqual(style); + } + } finally { + now.mockRestore(); + } +}); + +test("a render whose CSS fails to compile rejects", async () => { + await expect( + render(, { css: '@import "./missing.css";' }), + ).rejects.toThrow("Can't resolve './missing.css'"); +}); + +test("a render after one whose CSS uses no Tailwind feature compiles its own classes", async () => { + // A millisecond no other case reads, so only this case's renders could share a compiler + const now = jest.spyOn(Date, "now").mockReturnValue(1); + + try { + await render(, { + css: "@theme { --color-brand: red; }", + }); + const { stylesheet } = await render(); + + expect(stylesheet().s?.map(([name]) => name)).toStrictEqual(["left-1"]); + } finally { + now.mockRestore(); + } +}); + +test.each([ + ["an @source path", { extraCss: '@source "./";' }], + [ + "a source() root", + { + css: '@import "tailwindcss/utilities.css" layer(utilities) source("./");', + }, + ], +])( + "a render whose CSS names a file source through %s rejects", + async (_sourceKind, options) => { + await expect(render(, options)).rejects.toThrow( + "names a file source", + ); + }, +); + +test("a render whose CSS only excludes a path compiles its classes", async () => { + const { stylesheet } = await render(, { + extraCss: '@source not "./";', + }); + + expect(stylesheet().s?.map(([name]) => name)).toStrictEqual(["left-0"]); +}); diff --git a/yarn.lock b/yarn.lock index ced6f69e..c4641044 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4501,7 +4501,7 @@ __metadata: languageName: node linkType: hard -"@tailwindcss/node@npm:4.1.13": +"@tailwindcss/node@npm:4.1.13, @tailwindcss/node@npm:^4.1.12": version: 4.1.13 resolution: "@tailwindcss/node@npm:4.1.13" dependencies: @@ -4654,7 +4654,7 @@ __metadata: languageName: node linkType: hard -"@tailwindcss/postcss@npm:^4.1.11, @tailwindcss/postcss@npm:^4.1.12": +"@tailwindcss/postcss@npm:^4.1.11": version: 4.1.13 resolution: "@tailwindcss/postcss@npm:4.1.13" dependencies: @@ -11824,7 +11824,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.4.41, postcss@npm:^8.5.6": +"postcss@npm:^8.4.41": version: 8.5.19 resolution: "postcss@npm:8.5.19" dependencies: @@ -12171,7 +12171,7 @@ __metadata: "@react-native/jest-preset": "npm:0.86.3" "@react-native/metro-config": "npm:0.86.3" "@release-it/conventional-changelog": "npm:10.0.1" - "@tailwindcss/postcss": "npm:^4.1.12" + "@tailwindcss/node": "npm:^4.1.12" "@testing-library/react-native": "npm:^13.3.3" "@tsconfig/react-native": "npm:^3.0.6" "@types/babel__core": "npm:^7" @@ -12196,7 +12196,6 @@ __metadata: lefthook: "npm:^2.1.5" lightningcss: "npm:^1.30.1" metro-runtime: "npm:^0.84.2" - postcss: "npm:^8.5.6" prettier: "npm:^3.6.2" react: "npm:19.2.3" react-native: "npm:0.86.3"