Skip to content
Open
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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,28 @@ This API only allows for setting CSS variables as primitive values. For more com
> [!IMPORTANT]
> By using `VariableContext` you may need to disable the `inlineVariable` optimization

## Compiler warnings

Not every CSS declaration has a React Native equivalent. When the compiler cannot translate one it drops that declaration, and Metro prints a summary for the stylesheet it came from:

```
react-native-css: src/global.css - 3 declarations dropped, no React Native equivalent
properties: columns, float
values: z-index: auto
```

A block is printed the first time a file is compiled, and after that only when the file's set of warnings changes — an incremental rebuild that changes nothing about them stays quiet. Warnings are advisory and never fail a build.

Use the `warnings` option to change how much is printed:

```tsx
export default withReactNativeCSS(defaultConfig, {
warnings: "verbose", // "summary" (the default) | "verbose" | "none"
});
```

`summary` lists the first ten entries of each channel, `verbose` lists every entry, and `none` prints nothing. A web bundle never runs the compiler, so this only affects native builds.

## Optimizations

CSS is a dynamic styling language that use highly optimized engines that are not available in React Native. Instead, we optimize the styles to improve performance
Expand Down
65 changes: 65 additions & 0 deletions src/__tests__/compiler/warnings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { compile } from "react-native-css/compiler";

function ruleNames(css: string) {
return compile(css)
.stylesheet()
.s?.map(([name]) => name);
}

describe("the syntax channel", () => {
test("an unknown at-rule drops the rules inside it, and says so", () => {
const css = `@unknown-thing { .b { color: blue } }\n.c { color: green }`;

expect(ruleNames(css)).toStrictEqual(["c"]);
expect(compile(css).warnings()).toStrictEqual({
syntax: ["Unknown at rule: @unknown-thing"],
});
});

test("an unrecognised pseudo-element drops its rule, and says so", () => {
const css = `.a::wat { color: red }\n.c { color: green }`;

expect(ruleNames(css)).toStrictEqual(["c"]);
expect(compile(css).warnings()).toStrictEqual({
syntax: [
"'wat' is not recognized as a valid pseudo-element. Did you mean ':wat' (pseudo-class) or is this a typo?",
],
});
});

test("the same shape spelled correctly keeps both rules and warns nothing", () => {
const css = `@media (min-width: 1px) { .b { color: blue } }\n.c { color: green }`;

expect(ruleNames(css)).toStrictEqual(["b", "c"]);
expect(compile(css).warnings()).toStrictEqual({});
});

test.each([
"@nativeMapping {}",
".a { @nativeMapping { color: textColor; } color: red }",
])("this package's own at-rule is not reported: %s", (css) => {
expect(compile(css).warnings()).toStrictEqual({});
});

test("a repeated mistake is one entry, and two different ones are two", () => {
expect(
compile(`@unknown-a { .x { color: red } }
@unknown-a { .y { color: blue } }`).warnings().syntax,
).toStrictEqual(["Unknown at rule: @unknown-a"]);

expect(
compile(`@unknown-a { .x { color: red } }
@unknown-b { .y { color: blue } }`).warnings().syntax,
).toStrictEqual([
"Unknown at rule: @unknown-a",
"Unknown at rule: @unknown-b",
]);
});
});

// float rather than a property React Native may yet gain: Yoga lays out flexbox and has no floats
test("a property with no React Native equivalent is reported on its own channel", () => {
expect(compile(`.a { float: left }`).warnings()).toStrictEqual({
properties: ["float"],
});
});
16 changes: 16 additions & 0 deletions src/__tests__/metro/_transform-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { JsTransformOptions } from "metro-transform-worker";

// Stands in for Expo's transform worker: on web a stylesheet comes back as CSS, and anything else as code
export function transform(
_config: unknown,
_projectRoot: string,
filename: string,
data: Buffer,
options: JsTransformOptions,
) {
return Promise.resolve(
options.platform === "web" && filename.endsWith(".css")
? { output: [{ data: { css: { code: data } } }] }
: { output: [{ data: { code: data.toString() } }] },
);
}
143 changes: 143 additions & 0 deletions src/__tests__/metro/metro-transformer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { join } from "path";

import type { JsTransformOptions } from "metro-transform-worker";

import { transform } from "../../metro/metro-transformer";
import type { WarningLevel } from "../../metro/warnings";

type TransformerConfig = Parameters<typeof transform>[0];

jest.mock("@expo/metro-config", () => ({
unstable_transformerPath: require.resolve("./_transform-worker"),
}));

const PROJECT_ROOT = process.cwd();

const written: string[] = [];

beforeEach(() => {
written.length = 0;
jest.spyOn(console, "warn").mockImplementation((...args: unknown[]) => {
written.push(args.map((arg) => String(arg)).join(" "));
});
});

afterEach(() => {
jest.restoreAllMocks();
});

function reported(): string[] {
return written.filter((message) => message.startsWith("react-native-css:"));
}

function runTransform(options: {
css: string;
name: string;
platform?: string;
warnings?: WarningLevel;
}) {
// The stand-in worker reads neither, so each carries only what the transformer reads
const config: Pick<TransformerConfig, "reactNativeCSS"> = {
reactNativeCSS:
options.warnings === undefined
? undefined
: { warnings: options.warnings },
};
const transformOptions: Pick<JsTransformOptions, "type" | "platform"> = {
type: "module",
platform: options.platform ?? "ios",
};

return transform(
config as TransformerConfig,
PROJECT_ROOT,
join(PROJECT_ROOT, "src", options.name),
Buffer.from(options.css),
transformOptions as JsTransformOptions,
);
}

test("a dropped declaration reaches the terminal running the bundler", async () => {
const output = await runTransform({
css: `.a { float: left; z-index: auto; color: red; }`,
name: "surfaced.css",
});

expect(reported()).toStrictEqual([
[
`react-native-css: ${join("src", "surfaced.css")} - 2 declarations dropped, no React Native equivalent`,
" properties: float",
" values: z-index: auto",
].join("\n"),
]);
expect(JSON.stringify(output.output[0])).toContain("color");
});

test("a stylesheet the compiler fully understood says nothing", async () => {
await runTransform({ css: `.a { color: red; }`, name: "clean.css" });

expect(reported()).toStrictEqual([]);
});

test('"none" silences a stylesheet that would otherwise warn', async () => {
await runTransform({
css: `.a { float: left; }`,
name: "silenced.css",
warnings: "none",
});

expect(reported()).toStrictEqual([]);
});

test('"verbose" lifts the cap', async () => {
await runTransform({
css: `.a { border-style: hidden; }
.b { border-style: double; }
.c { border-style: groove; }
.d { border-style: ridge; }
.e { border-style: inset; }
.f { border-style: outset; }
.g { border-style: none; }`,
name: "verbose.css",
warnings: "verbose",
});

expect(reported()[0]).toContain(
"values: border-style: double, groove, hidden, inset, none, outset, ridge",
);
});

test("a rebuilt file reports again only when its warnings change", async () => {
const css = `.a { float: left; }`;

await runTransform({ css, name: "rebuilt.css" });
await runTransform({ css, name: "rebuilt.css" });
await runTransform({ css: `${css} .b { color: red; }`, name: "rebuilt.css" });
await runTransform({
css: `${css} .b { z-index: auto; }`,
name: "rebuilt.css",
});

const calls = reported();
expect(calls).toHaveLength(2);
expect(calls[1]).toContain("values: z-index: auto");
});

test("web reports nothing, because web never reaches the compiler", async () => {
await runTransform({
css: `.a { float: left; z-index: auto; }`,
name: "web.css",
platform: "web",
});

expect(reported()).toStrictEqual([]);
});

test("a file that is not CSS is passed through untouched", async () => {
await runTransform({
css: `export const value = 1;`,
name: "component.tsx",
});

expect(reported()).toStrictEqual([]);
});
Loading