From c58ad127cfd9f7d371809ccd9580a372b16025f6 Mon Sep 17 00:00:00 2001 From: Mark Lawlor Date: Wed, 10 Sep 2025 14:51:53 +1000 Subject: [PATCH] fix: remove lightningcss as a peerDep. Use the version provided by expo --- package.json | 1 - src/__tests__/compiler/@prop.test.tsx | 40 ++++++++++++++++++- src/__tests__/compiler/compiler.test.tsx | 51 +++++++++++++++++++----- src/compiler/compiler.ts | 39 +++++++++++------- src/compiler/lightningcss-loader.ts | 40 +++++++++++++++++++ src/compiler/selector-builder.ts | 8 +++- yarn.lock | 45 ++++++++++----------- 7 files changed, 174 insertions(+), 50 deletions(-) create mode 100644 src/compiler/lightningcss-loader.ts diff --git a/package.json b/package.json index 08cdd09..ed92e4d 100644 --- a/package.json +++ b/package.json @@ -193,7 +193,6 @@ }, "peerDependencies": { "@expo/metro-config": ">=0.21.8", - "lightningcss": ">=1.27.0", "react": "19.1.0", "react-native": "0.81.1" }, diff --git a/src/__tests__/compiler/@prop.test.tsx b/src/__tests__/compiler/@prop.test.tsx index 63f8793..db02f56 100644 --- a/src/__tests__/compiler/@prop.test.tsx +++ b/src/__tests__/compiler/@prop.test.tsx @@ -25,7 +25,45 @@ test("@prop target (nested @media)", () => { { d: [["#00f", ["test"]]], v: [["__rn-css-color", "#00f"]], - s: [2, 1], + s: [3, 1], + }, + ], + ], + ], + }); + + render(); + 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], }, ], ], diff --git a/src/__tests__/compiler/compiler.test.tsx b/src/__tests__/compiler/compiler.test.tsx index 5e5775b..2ea8439 100644 --- a/src/__tests__/compiler/compiler.test.tsx +++ b/src/__tests__/compiler/compiler.test.tsx @@ -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, }, @@ -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 { @@ -326,7 +359,7 @@ test("media query nested in rules", () => { }, ], m: [[">=", "width", 600]], - s: [2, 1], + s: [3, 1], v: [["__rn-css-color", "#00f"]], }, { @@ -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], }, ], ], diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index f403ea6..c493ef7 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -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, @@ -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"; @@ -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`); /** @@ -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": { @@ -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": { @@ -227,13 +233,13 @@ 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); @@ -241,7 +247,7 @@ function extractRule(rule: Rule, builder: StylesheetBuilder) { builder.applyRuleToSelectors(); } - if (declarationBlock.importantDeclarations) { + if (declarationBlock.importantDeclarations?.length) { builder.newRule(mapping, { important: true }); for (const declaration of declarationBlock.importantDeclarations) { parseDeclaration(declaration, builder); @@ -252,7 +258,7 @@ function extractRule(rule: Rule, builder: StylesheetBuilder) { if (value.rules) { for (const nestedRule of value.rules) { - extractRule(nestedRule, builder); + extractRule(nestedRule, builder, mapping); } } @@ -260,13 +266,13 @@ function extractRule(rule: Rule, builder: StylesheetBuilder) { } 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; @@ -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 @@ -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); } } @@ -348,6 +358,7 @@ function extractMedia(mediaRule: MediaRule, builder: StylesheetBuilder) { function extractContainer( containerRule: ContainerRule, builder: StylesheetBuilder, + mapping: StyleRuleMapping, ) { builder = builder.fork("container"); @@ -363,6 +374,6 @@ function extractContainer( builder.addContainerQuery(query); for (const rule of containerRule.rules) { - extractRule(rule, builder); + extractRule(rule, builder, mapping); } } diff --git a/src/compiler/lightningcss-loader.ts b/src/compiler/lightningcss-loader.ts new file mode 100644 index 0000000..84fb147 --- /dev/null +++ b/src/compiler/lightningcss-loader.ts @@ -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, + }; +} diff --git a/src/compiler/selector-builder.ts b/src/compiler/selector-builder.ts index 0aca7ec..88561b7 100644 --- a/src/compiler/selector-builder.ts +++ b/src/compiler/selector-builder.ts @@ -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" ); } diff --git a/yarn.lock b/yarn.lock index 11cd55b..71db3a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6930,9 +6930,9 @@ __metadata: linkType: hard "get-east-asian-width@npm:^1.0.0": - version: 1.3.1 - resolution: "get-east-asian-width@npm:1.3.1" - checksum: 10c0/cfe2eba0ae066d9a8b9f2e524922c6ec00ed91427758d701850839315febbbc56b26b06b43c8a9c1373ae769cc188c04c6a6fcaf3c9273e712a1cc8cd438a1f8 + version: 1.4.0 + resolution: "get-east-asian-width@npm:1.4.0" + checksum: 10c0/4e481d418e5a32061c36fbb90d1b225a254cc5b2df5f0b25da215dcd335a3c111f0c2023ffda43140727a9cafb62dac41d022da82c08f31083ee89f714ee3b83 languageName: node linkType: hard @@ -11007,7 +11007,6 @@ __metadata: typescript-eslint: "npm:^8.40.0" peerDependencies: "@expo/metro-config": ">=0.21.8" - lightningcss: ">=1.27.0" react: 19.1.0 react-native: 0.81.1 languageName: unknown @@ -11078,8 +11077,8 @@ __metadata: linkType: hard "react-native-worklets@npm:~0.5.0": - version: 0.5.0 - resolution: "react-native-worklets@npm:0.5.0" + version: 0.5.1 + resolution: "react-native-worklets@npm:0.5.1" dependencies: "@babel/plugin-transform-arrow-functions": "npm:^7.0.0-0" "@babel/plugin-transform-class-properties": "npm:^7.0.0-0" @@ -11096,7 +11095,7 @@ __metadata: "@babel/core": ^7.0.0-0 react: "*" react-native: "*" - checksum: 10c0/2e9b2b3b3c1549c5bc32d758aff538384bb851d80ed2a13c0c011dbe5857f62c2fcbd02dd2a7b2f38f8c357a6b1f2939f756ae24ea34a277e62a2a6c087e5ff8 + checksum: 10c0/9eb9e6dea9abaf889400a6618355ef59af3075f5004a4bec9e4cba6dcfd13d8b63de0d4b29d75c00a3dcf5ad422e1bdb71636c75b1a2ad1c43d8b512f198bdab languageName: node linkType: hard @@ -11215,12 +11214,12 @@ __metadata: languageName: node linkType: hard -"regenerate-unicode-properties@npm:^10.2.0": - version: 10.2.0 - resolution: "regenerate-unicode-properties@npm:10.2.0" +"regenerate-unicode-properties@npm:^10.2.2": + version: 10.2.2 + resolution: "regenerate-unicode-properties@npm:10.2.2" dependencies: regenerate: "npm:^1.4.2" - checksum: 10c0/5510785eeaf56bbfdf4e663d6753f125c08d2a372d4107bc1b756b7bf142e2ed80c2733a8b54e68fb309ba37690e66a0362699b0e21d5c1f0255dea1b00e6460 + checksum: 10c0/66a1d6a1dbacdfc49afd88f20b2319a4c33cee56d245163e4d8f5f283e0f45d1085a78f7f7406dd19ea3a5dd7a7799cd020cd817c97464a7507f9d10fbdce87c languageName: node linkType: hard @@ -11239,16 +11238,16 @@ __metadata: linkType: hard "regexpu-core@npm:^6.2.0": - version: 6.2.0 - resolution: "regexpu-core@npm:6.2.0" + version: 6.3.0 + resolution: "regexpu-core@npm:6.3.0" dependencies: regenerate: "npm:^1.4.2" - regenerate-unicode-properties: "npm:^10.2.0" + regenerate-unicode-properties: "npm:^10.2.2" regjsgen: "npm:^0.8.0" regjsparser: "npm:^0.12.0" unicode-match-property-ecmascript: "npm:^2.0.0" - unicode-match-property-value-ecmascript: "npm:^2.1.0" - checksum: 10c0/bbcb83a854bf96ce4005ee4e4618b71c889cda72674ce6092432f0039b47890c2d0dfeb9057d08d440999d9ea03879ebbb7f26ca005ccf94390e55c348859b98 + unicode-match-property-value-ecmascript: "npm:^2.2.1" + checksum: 10c0/7b35d4682b44cd4d0a75ed358e32ea2f9c43954254a60bdd93b94e09f9c1db4b757eb621d98c9c1f4bc4b9a048a3a8f4cdfdd2c4ddb7644a141522809d4ede2b languageName: node linkType: hard @@ -11504,9 +11503,9 @@ __metadata: linkType: hard "run-applescript@npm:^7.0.0": - version: 7.0.0 - resolution: "run-applescript@npm:7.0.0" - checksum: 10c0/bd821bbf154b8e6c8ecffeaf0c33cebbb78eb2987476c3f6b420d67ab4c5301faa905dec99ded76ebb3a7042b4e440189ae6d85bbbd3fc6e8d493347ecda8bfe + version: 7.1.0 + resolution: "run-applescript@npm:7.1.0" + checksum: 10c0/ab826c57c20f244b2ee807704b1ef4ba7f566aa766481ae5922aac785e2570809e297c69afcccc3593095b538a8a77d26f2b2e9a1d9dffee24e0e039502d1a03 languageName: node linkType: hard @@ -12509,10 +12508,10 @@ __metadata: languageName: node linkType: hard -"unicode-match-property-value-ecmascript@npm:^2.1.0": - version: 2.2.0 - resolution: "unicode-match-property-value-ecmascript@npm:2.2.0" - checksum: 10c0/1d0a2deefd97974ddff5b7cb84f9884177f4489928dfcebb4b2b091d6124f2739df51fc6ea15958e1b5637ac2a24cff9bf21ea81e45335086ac52c0b4c717d6d +"unicode-match-property-value-ecmascript@npm:^2.2.1": + version: 2.2.1 + resolution: "unicode-match-property-value-ecmascript@npm:2.2.1" + checksum: 10c0/93acd1ad9496b600e5379d1aaca154cf551c5d6d4a0aefaf0984fc2e6288e99220adbeb82c935cde461457fb6af0264a1774b8dfd4d9a9e31548df3352a4194d languageName: node linkType: hard