Skip to content

Commit f288456

Browse files
committed
feat: inline css variables
1 parent cef8ff6 commit f288456

File tree

10 files changed

+197
-19
lines changed

10 files changed

+197
-19
lines changed

src/__tests__/native/text-shadow.test.ios.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@ import { registerCSS, testID } from "react-native-css/jest";
55
describe("text-shadow", () => {
66
test("<offsetX> <offsetY>", () => {
77
registerCSS(
8-
`.my-class { --my-var: 10px 10px; text-shadow: var(--my-var); }`,
8+
`.my-class {
9+
--my-var: 10px 10px;
10+
text-shadow: var(--my-var);
11+
}`,
912
);
1013

1114
render(<Text testID={testID} className="my-class" />);
1215

1316
expect(screen.getByTestId(testID).props.style).toStrictEqual({
14-
textShadowColor: "black",
17+
textShadowColor: {
18+
semantic: ["label", "labelColor"],
19+
},
1520
textShadowOffset: {
1621
height: 10,
1722
width: 10,
@@ -28,7 +33,7 @@ describe("text-shadow", () => {
2833
render(<Text testID={testID} className="my-class" />);
2934

3035
expect(screen.getByTestId(testID).props.style).toStrictEqual({
31-
textShadowColor: "red",
36+
textShadowColor: "#f00",
3237
textShadowOffset: {
3338
height: 10,
3439
width: 10,
@@ -45,7 +50,7 @@ describe("text-shadow", () => {
4550
render(<Text testID={testID} className="my-class" />);
4651

4752
expect(screen.getByTestId(testID).props.style).toStrictEqual({
48-
textShadowColor: "red",
53+
textShadowColor: "#f00",
4954
textShadowOffset: {
5055
height: 10,
5156
width: 10,

src/__tests__/native/variables.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ test("inline variable", () => {
2020
});
2121
});
2222

23-
test("combined inline variable", () => {
23+
test("combined inline variables", () => {
2424
registerCSS(`
2525
.my-class-1 { width: var(--my-var); }
2626
.my-class-2 { --my-var: 10px; }

src/__tests__/vendor/tailwind/interactivity.test.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ describe("Interactivity - Caret Color", () => {
6060
});
6161
test("caret-current", async () => {
6262
expect(await renderCurrentTest()).toStrictEqual({
63-
props: { style: {} },
63+
props: {
64+
cursorColor: {
65+
semantic: ["label", "labelColor"],
66+
},
67+
},
6468
});
6569
});
6670
test("caret-white", async () => {

src/__tests__/vendor/tailwind/typography.test.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,11 @@ describe("Typography - Text Color", () => {
313313
test("text-current", async () => {
314314
expect(await renderCurrentTest()).toStrictEqual({
315315
props: {
316-
style: {},
316+
style: {
317+
color: {
318+
semantic: ["label", "labelColor"],
319+
},
320+
},
317321
},
318322
});
319323
});

src/compiler/compiler.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,14 @@ import {
1414
} from "lightningcss";
1515

1616
import { maybeMutateReactNativeOptions, parsePropAtRule } from "./atRules";
17-
import type { CompilerOptions, ContainerQuery } from "./compiler.types";
17+
import type {
18+
CompilerOptions,
19+
ContainerQuery,
20+
UniqueVarInfo,
21+
} from "./compiler.types";
1822
import { parseContainerCondition } from "./container-query";
1923
import { parseDeclaration, round } from "./declarations";
24+
import { inlineVariables } from "./inline-variables";
2025
import { extractKeyFrames } from "./keyframes";
2126
import { parseMediaQuery } from "./media-query";
2227
import { StylesheetBuilder } from "./stylesheet";
@@ -64,6 +69,9 @@ export function compile(code: Buffer | string, options: CompilerOptions = {}) {
6469
*
6570
* Due to the above issue, we run lightningcss twice
6671
*/
72+
73+
const vars = new Map<string, UniqueVarInfo>();
74+
6775
const { code: firstPass } = lightningcss({
6876
code: typeof code === "string" ? new TextEncoder().encode(code) : code,
6977
include: Features.DoublePositionGradients | Features.ColorFunction,
@@ -79,6 +87,27 @@ export function compile(code: Buffer | string, options: CompilerOptions = {}) {
7987
value: round(length.value * (options.inlineRem ?? 14)),
8088
};
8189
},
90+
Declaration(decl) {
91+
if (decl.property === "custom" && decl.value.name.startsWith("--")) {
92+
const entry = vars.get(decl.value.name) ?? {
93+
count: 0,
94+
value: [
95+
...decl.value.value,
96+
{ type: "token", value: { type: "white-space", value: " " } },
97+
],
98+
};
99+
entry.count++;
100+
vars.set(decl.value.name, entry);
101+
}
102+
},
103+
StyleSheetExit(sheet) {
104+
for (const [name, info] of vars) {
105+
if (info.count !== 1) {
106+
vars.delete(name);
107+
}
108+
}
109+
return inlineVariables(sheet, vars);
110+
},
82111
},
83112
filename: options.filename ?? "style.css",
84113
projectRoot: options.projectRoot ?? process.cwd(),

src/compiler/compiler.types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
/* eslint-disable */
22
import type { Debugger } from "debug";
3-
import type { MediaFeatureNameFor_MediaFeatureId } from "lightningcss";
3+
import type {
4+
MediaFeatureNameFor_MediaFeatureId,
5+
TokenOrValue,
6+
} from "lightningcss";
47

58
import { VAR_SYMBOL } from "../runtime/native/reactivity";
69

@@ -151,6 +154,8 @@ export type InlineVariable = {
151154
[key: string]: unknown | undefined;
152155
};
153156

157+
export type UniqueVarInfo = { count: number; value: TokenOrValue[] };
158+
154159
/****************************** Animations ******************************/
155160

156161
export type Animation = [string, AnimationKeyframes[]];

src/compiler/declarations.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import type {
3535
Translate,
3636
UnresolvedColor,
3737
} from "lightningcss";
38+
import { isStyleFunction } from "react-native-css/runtime/utils";
3839

3940
import type {
4041
StyleDescriptor,
@@ -966,7 +967,13 @@ export function parseUnparsedDeclaration(
966967
}
967968

968969
if (property === "color") {
969-
builder.addDescriptor("--__rn-css-current-color", value);
970+
if (
971+
!isStyleFunction(value) ||
972+
value[1] !== "var" ||
973+
value[2] !== "-css-color"
974+
) {
975+
builder.addDescriptor("--__rn-css-color", value);
976+
}
970977
}
971978
}
972979
}
@@ -1125,7 +1132,7 @@ export function parseUnparsed(
11251132
} else if (tokenOrValue === "false") {
11261133
return false;
11271134
} else if (tokenOrValue === "currentcolor") {
1128-
return [{}, "var", "__rn-css-current-color"] as const;
1135+
return [{}, "var", "__rn-css-color"] as const;
11291136
} else {
11301137
return tokenOrValue;
11311138
}
@@ -1255,7 +1262,7 @@ export function parseUnparsed(
12551262
builder.addWarning("value", value);
12561263
return;
12571264
} else if (value === "currentcolor") {
1258-
return [{}, "var", "__rn-css-current-color"] as const;
1265+
return [{}, "var", "__rn-css-color"] as const;
12591266
}
12601267

12611268
if (value === "true") {
@@ -1579,10 +1586,15 @@ export function parseFontColorDeclaration(
15791586
) {
15801587
parseColorDeclaration(declaration, builder);
15811588

1582-
builder.addDescriptor(
1583-
"--__rn-css-color",
1584-
parseColor(declaration.value, builder),
1585-
);
1589+
if (
1590+
typeof declaration.value !== "object" ||
1591+
declaration.value.type !== "currentcolor"
1592+
) {
1593+
builder.addDescriptor(
1594+
"--__rn-css-color",
1595+
parseColor(declaration.value, builder),
1596+
);
1597+
}
15861598
}
15871599

15881600
export function parseColorDeclaration(
@@ -1609,7 +1621,7 @@ export function parseColor(cssColor: CssColor, builder: StylesheetBuilder) {
16091621

16101622
switch (cssColor.type) {
16111623
case "currentcolor":
1612-
return [{}, "var", "__rn-css-current-color"] as const;
1624+
return [{}, "var", "__rn-css-color"] as const;
16131625
case "light-dark": {
16141626
const extraRule: StyleRule = {
16151627
s: [],
@@ -1982,11 +1994,11 @@ export function parseTextShadow(
19821994
parseColor(textShadow.color, builder),
19831995
);
19841996
builder.addDescriptor(
1985-
"textShadowOffset.width",
1997+
"*.textShadowOffset.width",
19861998
parseLength(textShadow.xOffset, builder),
19871999
);
19882000
builder.addDescriptor(
1989-
"textShadowOffset.height",
2001+
"*.textShadowOffset.height",
19902002
parseLength(textShadow.yOffset, builder),
19912003
);
19922004
builder.addDescriptor(

src/compiler/inline-variables.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import type { Declaration, DeclarationBlock, StyleSheet } from "lightningcss";
2+
3+
import type { UniqueVarInfo } from "./compiler.types";
4+
5+
export function inlineVariables(
6+
stylesheet: StyleSheet,
7+
vars: Map<string, UniqueVarInfo>,
8+
) {
9+
stylesheet.rules = stylesheet.rules.map(function checkRule(rule) {
10+
switch (rule.type) {
11+
case "custom":
12+
case "font-face":
13+
case "font-palette-values":
14+
case "font-feature-values":
15+
case "namespace":
16+
case "layer-statement":
17+
case "property":
18+
case "view-transition":
19+
case "ignored":
20+
case "unknown":
21+
case "import":
22+
case "page":
23+
case "counter-style":
24+
case "moz-document":
25+
case "nesting":
26+
case "viewport":
27+
case "custom-media":
28+
case "scope":
29+
case "starting-style":
30+
return rule;
31+
32+
case "media":
33+
rule.value.rules = rule.value.rules.map((rule) => checkRule(rule));
34+
return rule;
35+
case "keyframes":
36+
return rule;
37+
case "style":
38+
rule.value.declarations = replaceDeclarationBlock(
39+
rule.value.declarations,
40+
vars,
41+
);
42+
return rule;
43+
case "nested-declarations":
44+
case "supports":
45+
case "layer-block":
46+
case "container":
47+
return rule;
48+
}
49+
});
50+
51+
return stylesheet;
52+
}
53+
54+
function replaceDeclarationBlock(
55+
block: DeclarationBlock | undefined,
56+
vars: Map<string, UniqueVarInfo>,
57+
) {
58+
if (!block) return;
59+
60+
block.declarations = block.declarations
61+
?.map((decl) => {
62+
return replaceDeclaration(decl, vars);
63+
})
64+
.filter((d) => !!d);
65+
66+
block.importantDeclarations = block.importantDeclarations
67+
?.map((decl) => {
68+
return replaceDeclaration(decl, vars);
69+
})
70+
.filter((d) => !!d);
71+
72+
return block;
73+
}
74+
75+
function replaceDeclaration(
76+
declaration: Declaration,
77+
vars: Map<string, UniqueVarInfo>,
78+
) {
79+
if (
80+
declaration.property !== "unparsed" &&
81+
declaration.property !== "custom"
82+
) {
83+
return declaration;
84+
}
85+
86+
if (declaration.property === "custom" && vars.has(declaration.value.name)) {
87+
return;
88+
}
89+
90+
declaration.value.value = declaration.value.value.flatMap((part) => {
91+
if (part.type === "var") {
92+
const varInfo = vars.get(part.value.name.ident);
93+
94+
if (!varInfo) {
95+
return part;
96+
}
97+
98+
return varInfo.value;
99+
}
100+
return part;
101+
});
102+
103+
return declaration;
104+
}

src/compiler/stylesheet.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,10 @@ export class StylesheetBuilder {
391391
let propPath: string | string[] | undefined =
392392
this.mapping[rawProperty] ?? this.mapping[property] ?? this.mapping["*"];
393393

394+
if (typeof property === "string" && property.includes(".")) {
395+
propPath = property.split(".");
396+
}
397+
394398
if (Array.isArray(propPath)) {
395399
const [first, second] = propPath;
396400

src/style-collection/root.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Platform, PlatformColor } from "react-native";
2+
13
import type { StyleDescriptor, VariableValue } from "react-native-css/compiler";
24

35
import { testMediaQuery } from "../runtime/native/conditions/media-query";
@@ -35,3 +37,12 @@ export const rootVariables = rootVariableFamily();
3537
export const universalVariables = rootVariableFamily();
3638

3739
rootVariables("__rn-css-rem").set([[14]]);
40+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
41+
rootVariables("__rn-css-color").set([
42+
[
43+
Platform.OS === "ios"
44+
? PlatformColor("label", "labelColor")
45+
: PlatformColor("?attr/textColorPrimary", "SystemBaseHighColor"),
46+
],
47+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
48+
] as any);

0 commit comments

Comments
 (0)