Skip to content

Commit ca31505

Browse files
committed
feat: metro integration
1 parent ecac705 commit ca31505

16 files changed

+1613
-91
lines changed

example/global.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import "tailwindcss";

example/metro.config.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1-
const path = require('path');
2-
const { getDefaultConfig } = require('@react-native/metro-config');
3-
const { withMetroConfig } = require('react-native-monorepo-config');
1+
const path = require("path");
2+
const { getDefaultConfig } = require("@react-native/metro-config");
3+
const { withMetroConfig } = require("react-native-monorepo-config");
4+
const { withReactNativeCSS } = require("../lib/commonjs/metro");
45

5-
const root = path.resolve(__dirname, '..');
6+
const root = path.resolve(__dirname, "..");
67

78
/**
89
* Metro configuration
910
* https://facebook.github.io/metro/docs/configuration
1011
*
1112
* @type {import('metro-config').MetroConfig}
1213
*/
13-
module.exports = withMetroConfig(getDefaultConfig(__dirname), {
14-
root,
15-
dirname: __dirname,
16-
});
14+
module.exports = withMetroConfig(
15+
withReactNativeCSS(getDefaultConfig(__dirname)),
16+
{
17+
root,
18+
dirname: __dirname,
19+
},
20+
);

example/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@
66
"android": "react-native run-android",
77
"ios": "react-native run-ios",
88
"start": "react-native start",
9+
"tailwind": "@tailwindcss/cli -i global.css -o global/output.css --watch",
910
"build:android": "react-native build-android --extra-params \"--no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a\"",
1011
"build:ios": "react-native build-ios --mode Debug"
1112
},
1213
"dependencies": {
14+
"@expo/metro-config": "^54.0.7",
1315
"@react-native/new-app-screen": "0.81.1",
16+
"@tailwindcss/postcss": "^4.1.16",
17+
"postcss": "^8.5.6",
1418
"react": "19.1.0",
1519
"react-native": "0.82.0",
20+
"react-native-css-nitro": "portal:..",
1621
"react-native-nitro-modules": "^0.29.1",
1722
"react-native-reanimated": "^4.1.3",
18-
"react-native-worklets": "^0.6.1"
23+
"react-native-worklets": "^0.6.1",
24+
"tailwindcss": "^4.1.16"
1925
},
2026
"devDependencies": {
2127
"@babel/core": "^7.25.2",

example/postcss.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default {
2+
plugins: {
3+
"@tailwindcss/postcss": {},
4+
},
5+
};

example/react-native-css-env.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/// <reference types="react-native-css/types" />
2+
3+
// NOTE: This file should not be edited and should be committed with your source code. It is generated by react-native-css. If you need to move or disable this file, please see the documentation.

example/src/App.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { StyleSheet, View } from "react-native";
33
import { multiply, specificity, StyleRegistry } from "react-native-css-nitro";
44
import { Text } from "react-native-css-nitro/components/Text";
55

6+
import "../global.css";
7+
68
StyleRegistry.addStyleSheet({
79
s: {
810
"text-red-500": [
@@ -19,7 +21,7 @@ StyleRegistry.addStyleSheet({
1921
export default function App() {
2022
return (
2123
<View style={styles.container}>
22-
<Text className="text-red-500" style={{ fontSize: 30 }}>
24+
<Text className="test" style={{ fontSize: 30 }}>
2325
Multiply28: {multiply(3, 7)}
2426
</Text>
2527
</View>

package.json

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,25 @@
22
"name": "react-native-css-nitro",
33
"version": "0.1.0",
44
"description": "A",
5-
"main": "./lib/module/index.js",
6-
"types": "./lib/typescript/src/index.d.ts",
5+
"main": "./lib/commonjs/index.js",
6+
"types": "./lib/typescript/module/src/index.d.ts",
77
"exports": {
88
".": {
99
"source": "./src/index.tsx",
10-
"types": "./lib/typescript/src/index.d.ts",
11-
"default": "./lib/module/index.js"
10+
"types": "./lib/typescript/module/src/index.d.ts",
11+
"default": "./lib/module/index.js",
12+
"import": "./lib/module/index.js"
1213
},
1314
"./components/*": {
1415
"source": "./src/components/*/index.ts",
15-
"types": "./lib/typescript/src/components/*/index.d.ts",
16+
"types": "./lib/typescript/module/src/components/*/index.d.ts",
1617
"default": "./lib/module/components/*/index.js"
1718
},
19+
"./metro": {
20+
"source": "./src/metro/index.ts",
21+
"types": "./lib/typescript/module/src/metro/index.d.ts",
22+
"default": "./lib/module/metro/index.js"
23+
},
1824
"./package.json": "./package.json"
1925
},
2026
"files": [
@@ -73,6 +79,7 @@
7379
"@eslint/eslintrc": "^3.3.1",
7480
"@eslint/js": "^9.35.0",
7581
"@evilmartians/lefthook": "^1.12.3",
82+
"@expo/metro-config": "^54.0.7",
7683
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
7784
"@react-native/babel-preset": "0.81.1",
7885
"@react-native/eslint-config": "^0.81.1",
@@ -88,7 +95,7 @@
8895
"eslint-config-prettier": "^10.1.8",
8996
"eslint-plugin-prettier": "^5.5.4",
9097
"jest": "^29.7.0",
91-
"lightningcss": "^1.30.2",
98+
"lightningcss": "1.30.1",
9299
"nitrogen": "^0.29.1",
93100
"prettier": "^3.6.2",
94101
"react": "19.1.0",
@@ -161,6 +168,12 @@
161168
"esm": true
162169
}
163170
],
171+
[
172+
"commonjs",
173+
{
174+
"esm": true
175+
}
176+
],
164177
[
165178
"typescript",
166179
{
@@ -176,6 +189,10 @@
176189
},
177190
"dependencies": {
178191
"colorjs.io": "^0.5.2",
192+
"comment-json": "^4.4.1",
179193
"debug": "^4.4.3"
194+
},
195+
"resolutions": {
196+
"lightningcss": "1.30.1"
180197
}
181198
}

src/metro/index.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/* eslint-disable */
2+
import { versions } from "node:process";
3+
4+
import type { MetroConfig } from "metro-config";
5+
6+
import { type CompilerOptions } from "../compiler";
7+
import { nativeResolver, webResolver } from "./resolver";
8+
import { setupTypeScript } from "./typescript";
9+
10+
export interface WithReactNativeCSSOptions extends CompilerOptions {
11+
/* Specify the path to the TypeScript environment file. Defaults types-env.d.ts */
12+
typescriptEnvPath?: string;
13+
/* Disable generation of the types-env.d.ts file. Defaults false */
14+
disableTypeScriptGeneration?: boolean;
15+
/** Add className to all React Native primitives. Defaults false */
16+
globalClassNamePolyfill?: boolean;
17+
hexColors?: boolean;
18+
}
19+
20+
const metroOverrideResolution = {
21+
type: "sourceFile",
22+
filePath: require.resolve("./override"),
23+
};
24+
25+
export function withReactNativeCSS<
26+
T extends MetroConfig | (() => Promise<MetroConfig>),
27+
>(config: T, options?: WithReactNativeCSSOptions): T {
28+
if (typeof config === "function") {
29+
return (async () => {
30+
return withReactNativeCSS(await config(), options);
31+
}) as T;
32+
}
33+
34+
if (Number(versions.node.split(".")[0]) < 20) {
35+
throw new Error("react-native-css only supports NodeJS >20");
36+
}
37+
38+
console.log("asdf", (global as any).__REACT_NATIVE_METRO_CONFIG_LOADED);
39+
40+
const {
41+
disableTypeScriptGeneration,
42+
typescriptEnvPath,
43+
globalClassNamePolyfill = false,
44+
} = options || {};
45+
46+
if (disableTypeScriptGeneration !== true) {
47+
setupTypeScript(typescriptEnvPath);
48+
}
49+
50+
return {
51+
...config,
52+
transformerPath: require.resolve("./metro-transformer"),
53+
transformer: {
54+
...config.transformer,
55+
reactNativeCSS: {
56+
...options,
57+
communityCLI: !!(global as any).__REACT_NATIVE_METRO_CONFIG_LOADED,
58+
},
59+
},
60+
resolver: {
61+
...config.resolver,
62+
sourceExts: [...(config?.resolver?.sourceExts || []), "css"],
63+
resolveRequest: (context, moduleName, platform) => {
64+
if (moduleName.includes("react-native-css-metro-override")) {
65+
return metroOverrideResolution;
66+
}
67+
68+
const parentResolver =
69+
config.resolver?.resolveRequest ?? context.resolveRequest;
70+
71+
// Don't hijack the resolution of react-native imports
72+
if (!globalClassNamePolyfill) {
73+
return parentResolver(context, moduleName, platform);
74+
}
75+
76+
const resolver = platform === "web" ? webResolver : nativeResolver;
77+
const resolved = resolver(
78+
parentResolver,
79+
context,
80+
moduleName,
81+
platform,
82+
);
83+
84+
return resolved;
85+
},
86+
},
87+
};
88+
}

src/metro/injection-code.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* This is a hack around Metro's handling of bundles.
3+
* When a component is inside a lazy() barrier, it is inside a different JS bundle.
4+
* So when it updates, it only updates its local bundle, not the global one which contains the CSS files.
5+
*
6+
* This means that the CSS file will not be re-evaluated when a component in a different bundle updates,
7+
* breaking tools like Tailwind CSS
8+
*
9+
* To fix this, we force our code to always import the CSS files, so now the CSS files are in every bundle.
10+
*/
11+
export function getWebInjectionCode(filePaths: string[]) {
12+
const importStatements = filePaths
13+
.map((filePath) => `import "${filePath}";`)
14+
.join("\n");
15+
16+
return Buffer.from(importStatements);
17+
}
18+
19+
export function getNativeInjectionCode(
20+
cssFilePaths: string[],
21+
values: unknown[],
22+
) {
23+
const importStatements = cssFilePaths
24+
.map((filePath) => `import "${filePath}";`)
25+
.join("\n");
26+
27+
const contents = values
28+
.map((value) => `StyleRegistry.addStyleSheet(${JSON.stringify(value)});`)
29+
.join("\n");
30+
31+
return Buffer.from(
32+
`import { StyleRegistry } from "react-native-css-nitro";\n${importStatements}\n${contents};export {};`,
33+
);
34+
}

src/metro/metro-transformer.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { unstable_transformerPath } from "@expo/metro-config";
2+
import type {
3+
JsTransformerConfig,
4+
JsTransformOptions,
5+
TransformResponse,
6+
} from "metro-transform-worker";
7+
8+
import { compile, type CompilerOptions } from "../compiler";
9+
import { getNativeInjectionCode } from "./injection-code";
10+
11+
const worker =
12+
// eslint-disable-next-line @typescript-eslint/no-require-imports
13+
require(unstable_transformerPath) as typeof import("metro-transform-worker");
14+
15+
export async function transform(
16+
config: JsTransformerConfig & {
17+
reactNativeCSS?: CompilerOptions | undefined;
18+
},
19+
projectRoot: string,
20+
filePath: string,
21+
data: Buffer,
22+
options: JsTransformOptions,
23+
): Promise<TransformResponse> {
24+
const isCss = options.type !== "asset" && /\.(s?css|sass)$/.test(filePath);
25+
26+
if (options.platform === "web" || !isCss) {
27+
return worker.transform(config, projectRoot, filePath, data, options);
28+
}
29+
30+
const cssFile = (await worker.transform(config, projectRoot, filePath, data, {
31+
...options,
32+
platform: "web",
33+
})) as TransformResponse & {
34+
output: [{ data: { css: { code: Buffer } } }];
35+
};
36+
37+
const css = cssFile.output[0].data.css.code.toString();
38+
39+
const productionJS = compile(css, {
40+
...config.reactNativeCSS,
41+
filename: filePath,
42+
projectRoot: projectRoot,
43+
}).stylesheet();
44+
45+
data = Buffer.from(getNativeInjectionCode([], [productionJS]));
46+
47+
const transform = await worker.transform(
48+
config,
49+
projectRoot,
50+
`${filePath}.js`,
51+
data,
52+
options,
53+
);
54+
55+
(transform as any).output[0].data.css = {
56+
skipCache: true,
57+
code: "",
58+
};
59+
60+
return transform;
61+
}

0 commit comments

Comments
 (0)