Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,6 @@
},
"peerDependencies": {
"@expo/metro-config": ">=0.21.8",
"lightningcss": ">=1.27.0",
"react": "19.1.0",
"react-native": "0.81.1"
},
Expand Down
40 changes: 39 additions & 1 deletion src/__tests__/compiler/@prop.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,45 @@ test("@prop target (nested @media)", () => {
{
d: [["#00f", ["test"]]],
v: [["__rn-css-color", "#00f"]],
s: [2, 1],
s: [3, 1],
},
],
],
],
});

render(<View testID={testID} className="my-class" />);
const component = screen.getByTestId(testID);

expect(component.props).toStrictEqual({
testID,
children: undefined,
test: "#00f",
style: {},
});
});

test("@prop target (nested @media and nested declarations)", () => {
const compiled = registerCSS(`
.my-class {
@prop test;
@media all {
& {
color: #00f;
}
}
}
`);

expect(compiled.stylesheet()).toStrictEqual({
s: [
[
"my-class",
[
{
d: [["#00f", ["test"]]],
v: [["__rn-css-color", "#00f"]],
s: [3, 1],
},
],
],
Expand Down
51 changes: 42 additions & 9 deletions src/__tests__/compiler/compiler.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,11 @@ test("hello world", () => {

test("reads global CSS variables", () => {
const compiled = compile(
`
@layer theme {
:root, :host {
--color-red-500: oklch(63.7% 0.237 25.331);
}
}`,
`@layer theme {
:root, :host {
--color-red-500: oklch(63.7% 0.237 25.331);
}
}`,
{
inlineVariables: false,
},
Expand All @@ -44,6 +43,40 @@ test("reads global CSS variables", () => {
});
});

test(":root CSS variables with media queries", () => {
const compiled = compile(
`:root {
@media ios {
& {
--my-var: System;
}
}

@media android {
& {
--my-var: SystemAndroid;
}
}
}
`,
{
inlineVariables: false,
},
);

expect(compiled.stylesheet()).toStrictEqual({
vr: [
[
"my-var",
[
["SystemAndroid", [["=", "platform", "android"]]],
["System", [["=", "platform", "ios"]]],
],
],
],
});
});

test.skip("removes unused CSS variables", () => {
const compiled = compile(`
.test {
Expand Down Expand Up @@ -326,7 +359,7 @@ test("media query nested in rules", () => {
},
],
m: [[">=", "width", 600]],
s: [2, 1],
s: [3, 1],
v: [["__rn-css-color", "#00f"]],
},
{
Expand All @@ -335,12 +368,12 @@ test("media query nested in rules", () => {
[">=", "width", 600],
[">=", "width", 400],
],
s: [3, 1],
s: [5, 1],
},
{
d: [{ backgroundColor: "#ff0" }],
m: [[">=", "width", 100]],
s: [4, 1],
s: [7, 1],
},
],
],
Expand Down
39 changes: 25 additions & 14 deletions src/compiler/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import { inspect } from "node:util";

import { debug } from "debug";
import {
Features,
transform as lightningcss,
type ContainerRule,
type MediaQuery as CSSMediaQuery,
type CustomAtRules,
Expand All @@ -17,12 +15,14 @@ import { maybeMutateReactNativeOptions, parsePropAtRule } from "./atRules";
import type {
CompilerOptions,
ContainerQuery,
StyleRuleMapping,
UniqueVarInfo,
} from "./compiler.types";
import { parseContainerCondition } from "./container-query";
import { parseDeclaration, round } from "./declarations";
import { inlineVariables } from "./inline-variables";
import { extractKeyFrames } from "./keyframes";
import { lightningcssLoader } from "./lightningcss-loader";
import { parseMediaQuery } from "./media-query";
import { StylesheetBuilder } from "./stylesheet";
import { supportsConditionValid } from "./supports";
Expand Down Expand Up @@ -58,6 +58,8 @@ export function compile(code: Buffer | string, options: CompilerOptions = {}) {

const builder = new StylesheetBuilder(options);

const { lightningcss, Features } = lightningcssLoader();

logger(`Lightningcss first pass`);

/**
Expand Down Expand Up @@ -182,7 +184,11 @@ export function compile(code: Buffer | string, options: CompilerOptions = {}) {
/**
* Extracts style declarations and animations from a given CSS rule, based on its type.
*/
function extractRule(rule: Rule, builder: StylesheetBuilder) {
function extractRule(
rule: Rule,
builder: StylesheetBuilder,
mapping: StyleRuleMapping = {},
) {
// Check the rule's type to determine which extraction function to call
switch (rule.type) {
case "keyframes": {
Expand All @@ -192,12 +198,12 @@ function extractRule(rule: Rule, builder: StylesheetBuilder) {
}
case "container": {
// If the rule is a container, extract it with the `extractedContainer` function
extractContainer(rule.value, builder);
extractContainer(rule.value, builder, mapping);
break;
}
case "media": {
// If the rule is a media query, extract it with the `extractMedia` function
extractMedia(rule.value, builder);
extractMedia(rule.value, builder, mapping);
break;
}
case "nested-declarations": {
Expand Down Expand Up @@ -227,21 +233,21 @@ function extractRule(rule: Rule, builder: StylesheetBuilder) {
const value = rule.value;

const declarationBlock = value.declarations;
const mapping = parsePropAtRule(value.rules);
mapping = { ...mapping, ...parsePropAtRule(value.rules) };

// If the rule is a style declaration, extract it with the `getExtractedStyle` function and store it in the `declarations` map
builder = builder.fork("style", value.selectors);

if (declarationBlock) {
if (declarationBlock.declarations) {
if (declarationBlock.declarations?.length) {
builder.newRule(mapping);
for (const declaration of declarationBlock.declarations) {
parseDeclaration(declaration, builder);
}
builder.applyRuleToSelectors();
}

if (declarationBlock.importantDeclarations) {
if (declarationBlock.importantDeclarations?.length) {
builder.newRule(mapping, { important: true });
for (const declaration of declarationBlock.importantDeclarations) {
parseDeclaration(declaration, builder);
Expand All @@ -252,21 +258,21 @@ function extractRule(rule: Rule, builder: StylesheetBuilder) {

if (value.rules) {
for (const nestedRule of value.rules) {
extractRule(nestedRule, builder);
extractRule(nestedRule, builder, mapping);
}
}

break;
}
case "layer-block":
for (const layerRule of rule.value.rules) {
extractRule(layerRule, builder);
extractRule(layerRule, builder, mapping);
}
break;
case "supports":
if (supportsConditionValid(rule.value.condition)) {
for (const layerRule of rule.value.rules) {
extractRule(layerRule, builder);
extractRule(layerRule, builder, mapping);
}
}
break;
Expand Down Expand Up @@ -303,7 +309,11 @@ function extractRule(rule: Rule, builder: StylesheetBuilder) {
*
* @returns undefined if no screen media queries are found in the mediaRule, else it returns the extracted styles.
*/
function extractMedia(mediaRule: MediaRule, builder: StylesheetBuilder) {
function extractMedia(
mediaRule: MediaRule,
builder: StylesheetBuilder,
mapping: StyleRuleMapping,
) {
builder = builder.fork("media");

// Initialize an empty array to store screen media queries
Expand Down Expand Up @@ -336,7 +346,7 @@ function extractMedia(mediaRule: MediaRule, builder: StylesheetBuilder) {

// Iterate over all rules in the mediaRule and extract their styles using the updated CompilerCollection
for (const rule of mediaRule.rules) {
extractRule(rule, builder);
extractRule(rule, builder, mapping);
}
}

Expand All @@ -348,6 +358,7 @@ function extractMedia(mediaRule: MediaRule, builder: StylesheetBuilder) {
function extractContainer(
containerRule: ContainerRule,
builder: StylesheetBuilder,
mapping: StyleRuleMapping,
) {
builder = builder.fork("container");

Expand All @@ -363,6 +374,6 @@ function extractContainer(
builder.addContainerQuery(query);

for (const rule of containerRule.rules) {
extractRule(rule, builder);
extractRule(rule, builder, mapping);
}
}
40 changes: 40 additions & 0 deletions src/compiler/lightningcss-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export function lightningcssLoader() {
let lightningcssPath: string | undefined;

// Try to resolve the path to lightningcss from the @expo/metro-config package
// lightningcss is a direct dependency of @expo/metro-config
try {
lightningcssPath = require.resolve("lightningcss", {
paths: [
require
.resolve("@expo/metro-config/package.json")
.replace("/package.json", ""),
],
});
} catch {
// Intentionally left empty
}

// If @expo/metro-config is not being used (non-metro bundler?), try and resolve it directly
try {
lightningcssPath ??= require.resolve("lightningcss");
} catch {
// Intentionally left empty
}

if (!lightningcssPath) {
throw new Error(
"react-native-css was unable to determine the path to lightningcss",
);
}

// eslint-disable-next-line @typescript-eslint/no-require-imports
const { transform: lightningcss, Features } = require(
lightningcssPath,
) as typeof import("lightningcss");

return {
lightningcss,
Features,
};
}
8 changes: 6 additions & 2 deletions src/compiler/selector-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,9 +557,13 @@ function getMediaQuery(
return mediaQuery;
}

function isRootVariableSelector([first, second]: Selector) {
function isRootVariableSelector([first, ...rest]: Selector) {
rest = rest.filter((item) => item.type !== "nesting");
return (
first && !second && first.type === "pseudo-class" && first.kind === "root"
first &&
rest.length === 0 &&
first.type === "pseudo-class" &&
first.kind === "root"
);
}

Expand Down
Loading