diff --git a/src/__tests__/compiler/logical-borders.test.ts b/src/__tests__/compiler/logical-borders.test.ts new file mode 100644 index 0000000..87db4f7 --- /dev/null +++ b/src/__tests__/compiler/logical-borders.test.ts @@ -0,0 +1,118 @@ +import { compile } from "react-native-css/compiler"; + +const getRule = (css: string) => { + const compiled = compile(`.my-class { ${css} }`); + return { + rule: compiled.stylesheet().s?.find((rule) => rule[0] === "my-class")?.[1], + warnings: compiled.warnings(), + }; +}; + +describe("logical border colors", () => { + test("border-inline-start-color", () => { + expect(getRule("border-inline-start-color: red;").rule).toStrictEqual([ + { s: [1, 1], d: [{ borderStartColor: "#f00" }] }, + ]); + }); + + test("border-inline-end-color", () => { + expect(getRule("border-inline-end-color: red;").rule).toStrictEqual([ + { s: [1, 1], d: [{ borderEndColor: "#f00" }] }, + ]); + }); + + test("border-inline-color", () => { + expect(getRule("border-inline-color: red;").rule).toStrictEqual([ + { s: [1, 1], d: [{ borderStartColor: "#f00", borderEndColor: "#f00" }] }, + ]); + }); + + // nativewind/nativewind#1737: colors that resolve at runtime take the + // unparsed path, which previously skipped the border-inline-* renames + test("border-inline-start-color with var()", () => { + expect( + getRule("border-inline-start-color: hsl(var(--primary));").rule, + ).toStrictEqual([ + { + s: [1, 1], + d: [[[{}, "hsl", [{}, "var", "primary", 1]], "borderStartColor", 1]], + dv: 1, + }, + ]); + }); +}); + +describe("logical border widths", () => { + test("border-inline-start-width", () => { + expect(getRule("border-inline-start-width: 2px;").rule).toStrictEqual([ + { s: [1, 1], d: [{ borderStartWidth: 2 }] }, + ]); + }); + + test("border-inline-end-width", () => { + expect(getRule("border-inline-end-width: 2px;").rule).toStrictEqual([ + { s: [1, 1], d: [{ borderEndWidth: 2 }] }, + ]); + }); + + test("border-inline-width", () => { + expect(getRule("border-inline-width: 2px;").rule).toStrictEqual([ + { s: [1, 1], d: [{ borderStartWidth: 2, borderEndWidth: 2 }] }, + ]); + }); +}); + +describe("logical border shorthands", () => { + test("border-inline-start", () => { + expect(getRule("border-inline-start: 2px solid red;").rule).toStrictEqual([ + { s: [1, 1], d: [{ borderStartColor: "#f00", borderStartWidth: 2 }] }, + ]); + }); + + test("border-inline-end", () => { + expect(getRule("border-inline-end: 2px solid red;").rule).toStrictEqual([ + { s: [1, 1], d: [{ borderEndColor: "#f00", borderEndWidth: 2 }] }, + ]); + }); + + test("border-inline", () => { + expect(getRule("border-inline: 2px solid red;").rule).toStrictEqual([ + { + s: [1, 1], + d: [ + { + borderStartColor: "#f00", + borderEndColor: "#f00", + borderStartWidth: 2, + borderEndWidth: 2, + }, + ], + }, + ]); + }); +}); + +describe("logical border styles", () => { + // React Native only has a uniform borderStyle. solid matches the native + // default and is dropped silently, anything else drops with a warning + test("border-inline-start-style: solid is dropped without warning", () => { + const { rule, warnings } = getRule("border-inline-start-style: solid;"); + expect(rule).toBeUndefined(); + expect(warnings).toStrictEqual({}); + }); + + test("border-inline-start-style: dashed is dropped with a warning", () => { + const { rule, warnings } = getRule("border-inline-start-style: dashed;"); + expect(rule).toBeUndefined(); + expect(warnings).toStrictEqual({ + values: { "border-inline-start-style": ["dashed"] }, + }); + }); + + test("border-inline shorthand with a dashed style warns", () => { + const { warnings } = getRule("border-inline: 2px dashed red;"); + expect(warnings).toStrictEqual({ + values: { "border-inline-style": ["dashed"] }, + }); + }); +}); diff --git a/src/__tests__/vendor/tailwind/borders.test.tsx b/src/__tests__/vendor/tailwind/borders.test.tsx index 48f065c..03a37a0 100644 --- a/src/__tests__/vendor/tailwind/borders.test.tsx +++ b/src/__tests__/vendor/tailwind/borders.test.tsx @@ -35,8 +35,8 @@ describe("Border - Border Width", () => { expect(await renderCurrentTest()).toStrictEqual({ props: { style: { - borderInlineWidth: 1, - borderInlineStyle: "solid", + borderStartWidth: 1, + borderEndWidth: 1, }, }, }); @@ -54,14 +54,14 @@ describe("Border - Border Width", () => { test("border-s-1", async () => { expect(await renderCurrentTest()).toStrictEqual({ props: { - style: { borderInlineStartWidth: 1, borderInlineStartStyle: "solid" }, + style: { borderStartWidth: 1 }, }, }); }); test("border-e-1", async () => { expect(await renderCurrentTest()).toStrictEqual({ props: { - style: { borderInlineEndWidth: 1, borderInlineEndStyle: "solid" }, + style: { borderEndWidth: 1 }, }, }); }); @@ -98,8 +98,8 @@ describe("Border - Border Width", () => { expect(await renderCurrentTest()).toStrictEqual({ props: { style: { - borderInlineWidth: 2, - borderInlineStyle: "solid", + borderStartWidth: 2, + borderEndWidth: 2, }, }, }); @@ -117,14 +117,14 @@ describe("Border - Border Width", () => { test("border-s-2", async () => { expect(await renderCurrentTest()).toStrictEqual({ props: { - style: { borderInlineStartWidth: 2, borderInlineStartStyle: "solid" }, + style: { borderStartWidth: 2 }, }, }); }); test("border-e-2", async () => { expect(await renderCurrentTest()).toStrictEqual({ props: { - style: { borderInlineEndWidth: 2, borderInlineEndStyle: "solid" }, + style: { borderEndWidth: 2 }, }, }); }); @@ -160,7 +160,8 @@ describe("Border - Border Color", () => { expect(await renderCurrentTest()).toStrictEqual({ props: { style: { - borderInlineColor: "#fff", + borderStartColor: "#fff", + borderEndColor: "#fff", }, }, }); @@ -219,8 +220,8 @@ describe("Border - Border Color", () => { ).toStrictEqual({ props: { style: { - borderLeftColor: "#fb2c36", - borderRightColor: "#fb2c36", + borderStartColor: "#fb2c36", + borderEndColor: "#fb2c36", color: "#fb2c36", }, }, diff --git a/src/compiler/declarations.ts b/src/compiler/declarations.ts index 1301364..17beba0 100644 --- a/src/compiler/declarations.ts +++ b/src/compiler/declarations.ts @@ -62,9 +62,24 @@ type Parser = ( const propertyRename: Record = { "background-image": "experimental_backgroundImage", + // React Native has no border-inline-* props, but ships the equivalent + // RTL-aware border-start-* / border-end-* props + "border-inline-end-color": "border-end-color", + "border-inline-end-width": "border-end-width", + "border-inline-start-color": "border-start-color", + "border-inline-start-width": "border-start-width", "font-variant-caps": "font-variant", }; +// React Native only supports a uniform borderStyle, so per-side border +// styles have no native equivalent and are dropped. "solid" is dropped +// silently as it matches React Native's default rendering. +const unsupportedInlineStyles = new Set([ + "border-inline-style", + "border-inline-start-style", + "border-inline-end-style", +]); + const unparsedRuntimeParsing = new Set([ "animation", "border", @@ -382,14 +397,14 @@ function parseBorderColor( const start = parseColor(declaration.value.start, builder); const end = parseColor(declaration.value.end, builder); - if (start === end) { + if (declaration.property === "border-inline-color") { + builder.addDescriptor("border-start-color", start); + builder.addDescriptor("border-end-color", end); + } else if (start === end) { builder.addDescriptor(declaration.property, start); - } else if (declaration.property === "border-block-color") { + } else { builder.addDescriptor("border-top-color", start); builder.addDescriptor("border-bottom-color", end); - } else { - builder.addDescriptor("border-left-color", start); - builder.addDescriptor("border-right-color", end); } } } @@ -480,17 +495,17 @@ function parseBorderInline( { value }: DeclarationType<"border-inline">, builder: StylesheetBuilder, ) { - builder.addDescriptor( - "border-inline-color", - parseColor(value.color, builder), - ); - builder.addDescriptor( - "border-inline-width", - parseBorderSideWidth(value.width, builder), - ); - builder.addDescriptor( - "border-inline-style", + const color = parseColor(value.color, builder); + const width = parseBorderSideWidth(value.width, builder); + + builder.addDescriptor("border-start-color", color); + builder.addDescriptor("border-end-color", color); + builder.addDescriptor("border-start-width", width); + builder.addDescriptor("border-end-width", width); + dropUnsupportedInlineStyle( parseBorderStyle(value.style, builder), + builder, + "border-inline-style", ); } @@ -498,17 +513,15 @@ function parseBorderInlineStart( { value }: DeclarationType<"border-inline-start">, builder: StylesheetBuilder, ) { + builder.addDescriptor("border-start-color", parseColor(value.color, builder)); builder.addDescriptor( - "border-inline-start-color", - parseColor(value.color, builder), - ); - builder.addDescriptor( - "border-inline-start-width", + "border-start-width", parseBorderSideWidth(value.width, builder), ); - builder.addDescriptor( - "border-inline-start-style", + dropUnsupportedInlineStyle( parseBorderStyle(value.style, builder), + builder, + "border-inline-start-style", ); } @@ -516,17 +529,15 @@ function parseBorderInlineEnd( { value }: DeclarationType<"border-inline-end">, builder: StylesheetBuilder, ) { + builder.addDescriptor("border-end-color", parseColor(value.color, builder)); builder.addDescriptor( - "border-inline-end-color", - parseColor(value.color, builder), - ); - builder.addDescriptor( - "border-inline-end-width", + "border-end-width", parseBorderSideWidth(value.width, builder), ); - builder.addDescriptor( - "border-inline-end-style", + dropUnsupportedInlineStyle( parseBorderStyle(value.style, builder), + builder, + "border-inline-end-style", ); } @@ -535,9 +546,13 @@ export function parseBorderInlineWidth( builder: StylesheetBuilder, ) { builder.addDescriptor( - "border-inline-width", + "border-start-width", parseBorderSideWidth(declaration.value.start, builder), ); + builder.addDescriptor( + "border-end-width", + parseBorderSideWidth(declaration.value.end, builder), + ); } export function parseBorderInlineStyle( @@ -549,27 +564,35 @@ export function parseBorderInlineStyle( builder: StylesheetBuilder, ) { if (typeof declaration.value === "string") { - builder.addDescriptor( - declaration.property, + dropUnsupportedInlineStyle( parseBorderStyle(declaration.value, builder), - ); - } else if (declaration.value.start === declaration.value.end) { - builder.addDescriptor( + builder, declaration.property, - parseBorderStyle(declaration.value.start, builder), ); } else { - builder.addDescriptor( - "border-inline-start-style", + dropUnsupportedInlineStyle( parseBorderStyle(declaration.value.start, builder), + builder, + "border-inline-start-style", ); - builder.addDescriptor( - "border-inline-end-style", + dropUnsupportedInlineStyle( parseBorderStyle(declaration.value.end, builder), + builder, + "border-inline-end-style", ); } } +function dropUnsupportedInlineStyle( + style: string | undefined, + builder: StylesheetBuilder, + property: string, +) { + if (style !== undefined && style !== "solid") { + builder.addWarning("style", property, style); + } +} + function parseFlexFlow( { value }: DeclarationType<"flex-flow">, builder: StylesheetBuilder, @@ -917,6 +940,11 @@ export function parseUnparsedDeclaration( return; } + if (unsupportedInlineStyles.has(property)) { + builder.addWarning("property", property); + return; + } + builder.setWarningProperty(property); /** @@ -1607,7 +1635,7 @@ export function parseColorDeclaration( builder: StylesheetBuilder, ) { builder.addDescriptor( - declaration.property, + propertyRename[declaration.property] ?? declaration.property, parseColor(declaration.value, builder), ); } @@ -2191,7 +2219,7 @@ export function parseBorderSideWidthDeclaration( builder: StylesheetBuilder, ) { builder.addDescriptor( - declaration.property, + propertyRename[declaration.property] ?? declaration.property, parseBorderSideWidth(declaration.value, builder), ); } diff --git a/src/metro/metro-transformer.ts b/src/metro/metro-transformer.ts index e56e1ca..073aed8 100644 --- a/src/metro/metro-transformer.ts +++ b/src/metro/metro-transformer.ts @@ -7,6 +7,7 @@ import type { import { compile, type CompilerOptions } from "../compiler"; import { getNativeInjectionCode } from "./injection-code"; +import { bold, dim, yellow } from "./picocolors"; const worker = // eslint-disable-next-line @typescript-eslint/no-require-imports @@ -36,11 +37,17 @@ export async function transform( const css = cssFile.output[0].data.css.code.toString(); - const productionJS = compile(css, { + const compiled = compile(css, { ...config.reactNativeCSS, filename: filePath, projectRoot: projectRoot, - }).stylesheet(); + }); + + const productionJS = compiled.stylesheet(); + + if (options.dev) { + logWarnings(filePath, compiled.warnings()); + } data = Buffer.from(getNativeInjectionCode([], [productionJS])); @@ -60,3 +67,38 @@ export async function transform( return transform; } + +function logWarnings( + filePath: string, + warnings: ReturnType["warnings"]>, +) { + const lines: string[] = []; + + if (warnings.properties?.length) { + lines.push( + `properties with no React Native equivalent: ${[...new Set(warnings.properties)].join(", ")}`, + ); + } + + if (warnings.values) { + for (const [property, values] of Object.entries(warnings.values)) { + lines.push( + `unsupported values for ${property}: ${[...new Set(values.map(String))].join(", ")}`, + ); + } + } + + if (warnings.functions?.length) { + lines.push( + `unsupported functions: ${[...new Set(warnings.functions)].join(", ")}`, + ); + } + + if (!lines.length) { + return; + } + + console.warn( + `${yellow(bold("react-native-css"))} skipped styles in ${filePath} that cannot be represented on native:\n${lines.map((line) => dim(` - ${line}`)).join("\n")}`, + ); +}