Skip to content

Commit d23edad

Browse files
feat(metro): surface compiler warnings during a build
The compiler records every declaration it could not translate, but nothing read compile().warnings() during a build, so a dropped float or z-index: auto was silent. The Metro transformer now prints one block per CSS file: capped in summary mode, listed in full in verbose mode, and printed again only when the file's warnings change. The warnings option sets the level, and none silences it. lightningcss also warns about input it cannot parse, such as an unknown at-rule or pseudo-element. Those warnings reach a new syntax channel, kept apart from what React Native cannot express, and the block counts them as syntax warnings; this package's own @nativeMapping is not reported. Every recorded value is text, so CompilerWarnings.values is Record<string, string[]>; the one number, a percentage line-height, is recorded as the ratio it resolved to.
1 parent a5002c5 commit d23edad

12 files changed

Lines changed: 906 additions & 16 deletions

‎README.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,28 @@ This API only allows for setting CSS variables as primitive values. For more com
235235
> [!IMPORTANT]
236236
> By using `VariableContext` you may need to disable the `inlineVariable` optimization
237237
238+
## Compiler warnings
239+
240+
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:
241+
242+
```
243+
react-native-css: src/global.css - 3 declarations dropped, no React Native equivalent
244+
properties: columns, float
245+
values: z-index: auto
246+
```
247+
248+
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.
249+
250+
Use the `warnings` option to change how much is printed:
251+
252+
```tsx
253+
export default withReactNativeCSS(defaultConfig, {
254+
warnings: "verbose", // "summary" (the default) | "verbose" | "none"
255+
});
256+
```
257+
258+
`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.
259+
238260
## Optimizations
239261

240262
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
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { compile } from "react-native-css/compiler";
2+
3+
function ruleNames(css: string) {
4+
return compile(css)
5+
.stylesheet()
6+
.s?.map(([name]) => name);
7+
}
8+
9+
describe("the syntax channel", () => {
10+
test("an unknown at-rule drops the rules inside it, and says so", () => {
11+
const css = `@unknown-thing { .b { color: blue } }\n.c { color: green }`;
12+
13+
expect(ruleNames(css)).toStrictEqual(["c"]);
14+
expect(compile(css).warnings()).toStrictEqual({
15+
syntax: ["Unknown at rule: @unknown-thing"],
16+
});
17+
});
18+
19+
test("an unrecognised pseudo-element drops its rule, and says so", () => {
20+
const css = `.a::wat { color: red }\n.c { color: green }`;
21+
22+
expect(ruleNames(css)).toStrictEqual(["c"]);
23+
expect(compile(css).warnings()).toStrictEqual({
24+
syntax: [
25+
"'wat' is not recognized as a valid pseudo-element. Did you mean ':wat' (pseudo-class) or is this a typo?",
26+
],
27+
});
28+
});
29+
30+
test("the same shape spelled correctly keeps both rules and warns nothing", () => {
31+
const css = `@media (min-width: 1px) { .b { color: blue } }\n.c { color: green }`;
32+
33+
expect(ruleNames(css)).toStrictEqual(["b", "c"]);
34+
expect(compile(css).warnings()).toStrictEqual({});
35+
});
36+
37+
test.each([
38+
"@nativeMapping {}",
39+
".a { @nativeMapping { color: textColor; } color: red }",
40+
])("this package's own at-rule is not reported: %s", (css) => {
41+
expect(compile(css).warnings()).toStrictEqual({});
42+
});
43+
44+
test("a misspelling of this package's own at-rule is reported like any unknown one", () => {
45+
expect(compile(`@nativeMapping-typo {}`).warnings()).toStrictEqual({
46+
syntax: ["Unknown at rule: @nativeMapping-typo"],
47+
});
48+
});
49+
50+
test("a repeated mistake is one entry, and two different ones are two", () => {
51+
expect(
52+
compile(`@unknown-a { .x { color: red } }
53+
@unknown-a { .y { color: blue } }`).warnings().syntax,
54+
).toStrictEqual(["Unknown at rule: @unknown-a"]);
55+
56+
expect(
57+
compile(`@unknown-a { .x { color: red } }
58+
@unknown-b { .y { color: blue } }`).warnings().syntax,
59+
).toStrictEqual([
60+
"Unknown at rule: @unknown-a",
61+
"Unknown at rule: @unknown-b",
62+
]);
63+
});
64+
});
65+
66+
// float rather than a property React Native may yet gain: Yoga lays out flexbox and has no floats
67+
test("a property with no React Native equivalent is reported on its own channel", () => {
68+
expect(compile(`.a { float: left }`).warnings()).toStrictEqual({
69+
properties: ["float"],
70+
});
71+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { JsTransformOptions } from "metro-transform-worker";
2+
3+
// Stands in for Expo's transform worker: on web a stylesheet comes back as CSS, and anything else as code
4+
export function transform(
5+
_config: unknown,
6+
_projectRoot: string,
7+
filename: string,
8+
data: Buffer,
9+
options: JsTransformOptions,
10+
) {
11+
return Promise.resolve(
12+
options.platform === "web" && filename.endsWith(".css")
13+
? { output: [{ data: { css: { code: data } } }] }
14+
: { output: [{ data: { code: data.toString() } }] },
15+
);
16+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { join } from "path";
2+
3+
import type { JsTransformOptions } from "metro-transform-worker";
4+
5+
import { transform } from "../../metro/metro-transformer";
6+
import type { WarningLevel } from "../../metro/warnings";
7+
8+
type TransformerConfig = Parameters<typeof transform>[0];
9+
10+
jest.mock("@expo/metro-config", () => ({
11+
unstable_transformerPath: require.resolve("./_transform-worker"),
12+
}));
13+
14+
const PROJECT_ROOT = process.cwd();
15+
16+
const written: string[] = [];
17+
18+
beforeEach(() => {
19+
written.length = 0;
20+
jest.spyOn(console, "warn").mockImplementation((...args: unknown[]) => {
21+
written.push(args.map((arg) => String(arg)).join(" "));
22+
});
23+
});
24+
25+
afterEach(() => {
26+
jest.restoreAllMocks();
27+
});
28+
29+
function reported(): string[] {
30+
return written.filter((message) => message.startsWith("react-native-css:"));
31+
}
32+
33+
function runTransform(options: {
34+
css: string;
35+
name: string;
36+
platform?: string;
37+
warnings?: WarningLevel;
38+
}) {
39+
// The stand-in worker reads neither, so each carries only what the transformer reads
40+
const config: Pick<TransformerConfig, "reactNativeCSS"> = {
41+
reactNativeCSS:
42+
options.warnings === undefined
43+
? undefined
44+
: { warnings: options.warnings },
45+
};
46+
const transformOptions: Pick<JsTransformOptions, "type" | "platform"> = {
47+
type: "module",
48+
platform: options.platform ?? "ios",
49+
};
50+
51+
return transform(
52+
config as TransformerConfig,
53+
PROJECT_ROOT,
54+
join(PROJECT_ROOT, "src", options.name),
55+
Buffer.from(options.css),
56+
transformOptions as JsTransformOptions,
57+
);
58+
}
59+
60+
test("a dropped declaration reaches the terminal running the bundler", async () => {
61+
const output = await runTransform({
62+
css: `.a { float: left; z-index: auto; color: red; }`,
63+
name: "surfaced.css",
64+
});
65+
66+
expect(reported()).toStrictEqual([
67+
[
68+
`react-native-css: ${join("src", "surfaced.css")} - 2 declarations dropped, no React Native equivalent`,
69+
" properties: float",
70+
" values: z-index: auto",
71+
].join("\n"),
72+
]);
73+
expect(JSON.stringify(output.output[0])).toContain("color");
74+
});
75+
76+
test("a stylesheet the compiler fully understood says nothing", async () => {
77+
await runTransform({ css: `.a { color: red; }`, name: "clean.css" });
78+
79+
expect(reported()).toStrictEqual([]);
80+
});
81+
82+
test('"none" silences a stylesheet that would otherwise warn', async () => {
83+
await runTransform({
84+
css: `.a { float: left; }`,
85+
name: "silenced.css",
86+
warnings: "none",
87+
});
88+
89+
expect(reported()).toStrictEqual([]);
90+
});
91+
92+
test('"verbose" lifts the cap', async () => {
93+
await runTransform({
94+
css: `.a { border-style: hidden; }
95+
.b { border-style: double; }
96+
.c { border-style: groove; }
97+
.d { border-style: ridge; }
98+
.e { border-style: inset; }
99+
.f { border-style: outset; }
100+
.g { border-style: none; }`,
101+
name: "verbose.css",
102+
warnings: "verbose",
103+
});
104+
105+
expect(reported()[0]).toContain(
106+
"values: border-style: double, groove, hidden, inset, none, outset, ridge",
107+
);
108+
});
109+
110+
test("a rebuilt file reports again only when its warnings change", async () => {
111+
const css = `.a { float: left; }`;
112+
113+
await runTransform({ css, name: "rebuilt.css" });
114+
await runTransform({ css, name: "rebuilt.css" });
115+
await runTransform({ css: `${css} .b { color: red; }`, name: "rebuilt.css" });
116+
await runTransform({
117+
css: `${css} .b { z-index: auto; }`,
118+
name: "rebuilt.css",
119+
});
120+
121+
const calls = reported();
122+
expect(calls).toHaveLength(2);
123+
expect(calls[1]).toContain("values: z-index: auto");
124+
});
125+
126+
test("web reports nothing, because web never reaches the compiler", async () => {
127+
await runTransform({
128+
css: `.a { float: left; z-index: auto; }`,
129+
name: "web.css",
130+
platform: "web",
131+
});
132+
133+
expect(reported()).toStrictEqual([]);
134+
});
135+
136+
test("a file that is not CSS is passed through untouched", async () => {
137+
await runTransform({
138+
css: `export const value = 1;`,
139+
name: "component.tsx",
140+
});
141+
142+
expect(reported()).toStrictEqual([]);
143+
});

0 commit comments

Comments
 (0)