Skip to content

Commit 0dfb783

Browse files
committed
feat: backgroundImage
1 parent d213bd1 commit 0dfb783

File tree

11 files changed

+225
-82
lines changed

11 files changed

+225
-82
lines changed

CONTRIBUTING.md

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,6 @@ To run the example app on iOS:
4444
yarn example ios
4545
```
4646

47-
To confirm that the app is running with the new architecture, you can check the Metro logs for a message like this:
48-
49-
```sh
50-
Running "CssExample" with {"fabric":true,"initialProps":{"concurrentRoot":true},"rootTag":1}
51-
```
52-
53-
Note the `"fabric":true` and `"concurrentRoot":true` properties.
54-
5547
To run the example app on Web:
5648

5749
```sh
@@ -71,12 +63,34 @@ To fix formatting errors, run the following:
7163
yarn lint --fix
7264
```
7365

66+
### Testing
67+
7468
Remember to add tests for your change if possible. Run the unit tests by:
7569

7670
```sh
7771
yarn test
7872
```
7973

74+
### Debugging
75+
76+
Run the example via the command line with the `debug` script to enable debugging:
77+
78+
```sh
79+
yarn example debug
80+
```
81+
82+
This will print parsed CSS and style objects to the console, which can help you understand how the library processes CSS files.
83+
84+
### Commands
85+
86+
The `yarn example` command is a shortcut for running commands in the example app. You can run any command that is available in the example app's `package.json` by prefixing it with `yarn example`.
87+
88+
You can also run Expo commands directly from the root directory by using the `yarn example expo` command. For example, to run the Expo prebuild, you can use:
89+
90+
```sh
91+
yarn example expo prebuild
92+
```
93+
8094
### Commit message convention
8195

8296
We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages:

example/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"version": "1.0.0",
44
"main": "index.js",
55
"scripts": {
6-
"start": "expo start --clear",
6+
"start": "expo start",
7+
"debug": "DEBUG='react-native-css:*' expo start --clear",
78
"android": "expo run:android",
89
"ios": "expo run:ios",
910
"web": "expo start --web"

example/src/App.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,18 @@ import "../global.css";
44

55
export default function App() {
66
return (
7-
<View className="justify-center items-center flex-1">
8-
<Text className="text-red-500">Test Component</Text>
9-
</View>
7+
<>
8+
<View className="justify-center items-center flex-1 bg-linear-to-r from-cyan-500 to-blue-500">
9+
<Text className="">Test Component</Text>
10+
</View>
11+
<View
12+
className="justify-center items-center flex-1"
13+
style={{
14+
experimental_backgroundImage: "linear-gradient(to right, #f00, #0f0)",
15+
}}
16+
>
17+
<Text className="">Test Component2</Text>
18+
</View>
19+
</>
1020
);
1121
}

src/compiler/compiler.ts

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import { parseMediaQuery } from "./media-query";
2424
import { getSelectors } from "./selectors";
2525
import { StylesheetBuilder } from "./stylesheet";
2626

27+
const defaultLogger = debug("react-native-css:compiler");
28+
2729
/**
2830
* Converts a CSS file to a collection of style declarations that can be used with the StyleSheet API
2931
*
@@ -35,7 +37,7 @@ export function compile(
3537
code: Buffer | string,
3638
options: CompilerOptions = {},
3739
): ReactNativeCssStyleSheet {
38-
const { logger = debug("react-native-css") } = options;
40+
const { logger = defaultLogger } = options;
3941
const features = Object.assign({}, options.features);
4042

4143
if (options.selectorPrefix && options.selectorPrefix.startsWith(".")) {
@@ -44,22 +46,16 @@ export function compile(
4446

4547
logger(`Features ${JSON.stringify(features)}`);
4648

49+
if (process.env.NODE_ENV !== "production") {
50+
if (defaultLogger.enabled) {
51+
defaultLogger(code.toString());
52+
}
53+
}
54+
4755
const builder = new StylesheetBuilder(options);
4856

4957
logger(`Start lightningcss`);
5058

51-
const onVarUsage = (token: TokenOrValue) => {
52-
if (token.type === "function") {
53-
token.value.arguments.forEach((token) => onVarUsage(token));
54-
} else if (token.type === "var") {
55-
builder.varUsage.add(token.value.name.ident);
56-
if (token.value.fallback) {
57-
const fallbackValues = token.value.fallback;
58-
fallbackValues.forEach((varObj) => onVarUsage(varObj));
59-
}
60-
}
61-
};
62-
6359
const customAtRules: CustomAtRules = {
6460
"react-native": {
6561
body: "declaration-list",
@@ -84,19 +80,21 @@ export function compile(
8480
};
8581

8682
if (options.stripUnusedVariables) {
83+
const onVarUsage = (token: TokenOrValue) => {
84+
if (token.type === "function") {
85+
token.value.arguments.forEach((token) => onVarUsage(token));
86+
} else if (token.type === "var") {
87+
builder.varUsage.add(token.value.name.ident);
88+
if (token.value.fallback) {
89+
const fallbackValues = token.value.fallback;
90+
fallbackValues.forEach((varObj) => onVarUsage(varObj));
91+
}
92+
}
93+
};
94+
8795
visitor.Declaration = (decl) => {
8896
if (decl.property === "unparsed" || decl.property === "custom") {
89-
decl.value.value.forEach((token) => {
90-
if (token.type === "function") {
91-
token.value.arguments.forEach((token) => onVarUsage(token));
92-
} else if (token.type === "var") {
93-
builder.varUsage.add(token.value.name.ident);
94-
if (token.value.fallback) {
95-
const fallbackValues = token.value.fallback;
96-
fallbackValues.forEach((varObj) => onVarUsage(varObj));
97-
}
98-
}
99-
});
97+
decl.value.value.forEach((token) => onVarUsage(token));
10098
}
10199
return decl;
102100
};

src/compiler/declarations.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ import type {
1515
FontStyle,
1616
FontVariantCaps,
1717
FontWeight,
18+
Gradient,
19+
GradientItemFor_DimensionPercentageFor_LengthValue,
1820
Length,
1921
LengthPercentageOrAuto,
2022
LengthValue,
23+
LineDirection,
2124
LineHeight,
2225
LineStyle,
2326
MaxSize,
@@ -51,7 +54,9 @@ type Parser<T extends Declaration["property"]> = (
5154
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
5255
) => StyleDescriptor | void;
5356

54-
const propertyRename: Record<string, string> = {};
57+
const propertyRename: Record<string, string> = {
58+
"background-image": "experimental_backgroundImage",
59+
};
5560

5661
const needsRuntimeParsing = new Set([
5762
"animation",
@@ -78,6 +83,7 @@ const parsers: {
7883
"animation-timing-function": addAnimationValue,
7984
"aspect-ratio": parseAspectRatio,
8085
"background-color": parseColorDeclaration,
86+
"background-image": parseBackgroundImage,
8187
"block-size": parseSizeDeclaration,
8288
"border": parseBorder,
8389
"border-block": parseBorderBlock,
@@ -966,6 +972,7 @@ export function parseUnparsed(
966972
case "rgba":
967973
case "hsl":
968974
case "hsla":
975+
case "linear-gradient":
969976
return unparsedFunction(tokenOrValue, builder);
970977
case "hairlineWidth":
971978
return [{}, tokenOrValue.value.name, []];
@@ -2598,6 +2605,86 @@ export function kebabCase(str: string) {
25982605
);
25992606
}
26002607

2608+
function parseBackgroundImage(
2609+
declaration: DeclarationType<"background-image">,
2610+
builder: StylesheetBuilder,
2611+
) {
2612+
builder.addDescriptor(
2613+
"experimental_backgroundImage",
2614+
declaration.value.flatMap((image): StyleDescriptor[] => {
2615+
switch (image.type) {
2616+
case "gradient": {
2617+
const gradient = parseGradient(image.value, builder);
2618+
return gradient ? [gradient] : [];
2619+
}
2620+
case "none":
2621+
return ["none"];
2622+
}
2623+
2624+
return [];
2625+
}),
2626+
);
2627+
return;
2628+
}
2629+
2630+
function parseGradient(
2631+
gradient: Gradient,
2632+
builder: StylesheetBuilder,
2633+
): StyleDescriptor {
2634+
switch (gradient.type) {
2635+
case "linear": {
2636+
return [
2637+
{},
2638+
"@linear-gradient",
2639+
[
2640+
parseLineDirection(gradient.direction, builder),
2641+
...gradient.items.map((item) => parseGradientItem(item, builder)),
2642+
],
2643+
];
2644+
}
2645+
}
2646+
2647+
return;
2648+
}
2649+
2650+
function parseLineDirection(
2651+
lineDirection: LineDirection,
2652+
builder: StylesheetBuilder,
2653+
): StyleDescriptor {
2654+
switch (lineDirection.type) {
2655+
case "corner":
2656+
return `to ${lineDirection.horizontal} ${lineDirection.vertical}`;
2657+
case "horizontal":
2658+
case "vertical":
2659+
return `to ${lineDirection.value}`;
2660+
case "angle":
2661+
return parseAngle(lineDirection.value, builder);
2662+
default: {
2663+
lineDirection satisfies never;
2664+
}
2665+
}
2666+
2667+
return;
2668+
}
2669+
2670+
function parseGradientItem(
2671+
item: GradientItemFor_DimensionPercentageFor_LengthValue,
2672+
builder: StylesheetBuilder,
2673+
): StyleDescriptor {
2674+
switch (item.type) {
2675+
case "color-stop": {
2676+
const args: StyleDescriptor[] = [parseColor(item.color, builder)];
2677+
if (item.position) {
2678+
args.push(parseLength(item.position, builder));
2679+
}
2680+
2681+
return [{}, "@colorStop", args, 1];
2682+
}
2683+
case "hint":
2684+
return parseLength(item.value, builder);
2685+
}
2686+
}
2687+
26012688
const namedColors = new Set([
26022689
"aliceblue",
26032690
"antiquewhite",

src/metro/index.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
/* eslint-disable */
2-
import { versions } from "node:process";
32
import { dirname, relative, sep } from "node:path";
3+
import { versions } from "node:process";
44

55
import connect from "connect";
6+
import debug from "debug";
67
import type { MetroConfig } from "metro-config";
78

89
import { compile } from "../compiler/compiler";
9-
import { setupTypeScript } from "./typescript";
1010
import { getNativeInjectionCode, getWebInjectionCode } from "./injection-code";
1111
import { nativeResolver, webResolver } from "./resolver";
12+
import { setupTypeScript } from "./typescript";
1213

1314
export interface WithReactNativeCSSOptions {
1415
/* Specify the path to the TypeScript environment file. Defaults types-env.d.ts */
@@ -19,6 +20,8 @@ export interface WithReactNativeCSSOptions {
1920
globalClassNamePolyfill?: boolean;
2021
}
2122

23+
const defaultLogger = debug("react-native-css:metro");
24+
2225
export function withReactNativeCSS<
2326
T extends MetroConfig | (() => Promise<MetroConfig>),
2427
>(config: T, options?: WithReactNativeCSSOptions): T {
@@ -200,6 +203,11 @@ export function withReactNativeCSS<
200203
),
201204
Array.from(nativeCSSFiles.values()).map(([, value]) => value),
202205
);
206+
207+
if (defaultLogger.enabled && fileBuffer) {
208+
defaultLogger(`Transformed ${filePath}`);
209+
defaultLogger(fileBuffer?.toString());
210+
}
203211
}
204212

205213
return transformFile(filePath, transformOptions, fileBuffer);

src/runtime/native/__tests__/background-image.test.tsx

Whitespace-only changes.

src/runtime/native/injection.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ StyleCollection.keyframes = family<string, Observable<Animation_V2[1]>>(() => {
2727
StyleCollection.inject = function (options: ReactNativeCssStyleSheet) {
2828
observableBatch.current = new Set();
2929

30-
// console.log(JSON.stringify(options, null, 2));
31-
3230
if (options.s) {
3331
for (const style of options.s) {
3432
StyleCollection.styles(style[0]).set(style[1]);

src/runtime/native/styles/resolve.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,26 @@ export function resolveValue(
129129
// @translate, @rotate, @scale, etc.
130130
return { [name.slice(1)]: simpleResolve(value[2], castToArray)[0] };
131131
} else {
132-
const args = simpleResolve(value[2], castToArray);
132+
if (name === "linear-gradient") {
133+
debugger;
134+
}
135+
let args = simpleResolve(value[2], castToArray);
136+
133137
if (args === undefined) {
134138
return;
135139
} else if (Array.isArray(args)) {
136-
value = `${name}(${args.join(", ")})`;
140+
if (args.length === 1) {
141+
args = args[0];
142+
}
143+
144+
const joinedArgs = args.map((arg: unknown) => {
145+
if (Array.isArray(arg)) {
146+
return arg.flat().join(" ");
147+
}
148+
return arg;
149+
});
150+
151+
value = `${name}(${joinedArgs.join(", ")})`;
137152
} else {
138153
value = `${name}(${args})`;
139154
}

0 commit comments

Comments
 (0)