Skip to content

Commit 225466e

Browse files
fix(compiler): accept any Uint8Array as compile() input and decode it once
A Uint8Array that is not a Buffer stringifies to its byte values, so the rem probe read `58,114,…` instead of CSS and fell back to 14, and the `Buffer` in the public signature failed every consumer without @types/node. The input is `Uint8Array | string`, decoded once with TextDecoder.
1 parent a5002c5 commit 225466e

3 files changed

Lines changed: 92 additions & 4 deletions

File tree

‎src/__tests__/compiler/compiler.test.tsx‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { debug } from "debug";
12
import { compile } from "react-native-css/compiler";
23

34
test("hello world", () => {
@@ -26,6 +27,39 @@ test("hello world", () => {
2627
});
2728
});
2829

30+
test("compiles bytes exactly as it compiles their text", () => {
31+
const css = `:root { font-size: 16px; } .my-class { padding: 1rem; }`;
32+
const fromText = compile(css).stylesheet();
33+
34+
expect(compile(new TextEncoder().encode(css)).stylesheet()).toStrictEqual(
35+
fromText,
36+
);
37+
expect(compile(Buffer.from(css)).stylesheet()).toStrictEqual(fromText);
38+
});
39+
40+
test("the debug log is handed the decoded text, not the byte values", () => {
41+
const css = `:root { font-size: 16px; }`;
42+
const written: string[] = [];
43+
const emit = debug.log;
44+
45+
// `enable` flips the instance the compiler module already built, so no reload is needed.
46+
debug.enable("react-native-css:compiler");
47+
debug.log = (...args: unknown[]): void => {
48+
written.push(args.map(String).join(" "));
49+
};
50+
51+
try {
52+
compile(new TextEncoder().encode(css));
53+
} finally {
54+
debug.log = emit;
55+
debug.disable();
56+
}
57+
58+
const joined = written.join("\n");
59+
expect(joined).toContain("font-size: 16px");
60+
expect(joined).not.toContain("58,114");
61+
});
62+
2963
test("reads global CSS variables", () => {
3064
const compiled = compile(
3165
`@layer theme {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import path from "node:path";
2+
3+
import ts from "typescript";
4+
5+
// `native/conditions/index.d.ts` imports `StyleRule` from `react-native-css/compiler`, so a Node
6+
// global in that surface is a TS2591 inside `node_modules` for any consumer without `@types/node`.
7+
const COMPILER_ENTRY = path.join(__dirname, "../../compiler/index.ts");
8+
9+
const OPTIONS: ts.CompilerOptions = {
10+
lib: ["lib.es2024.d.ts"],
11+
module: ts.ModuleKind.ESNext,
12+
moduleResolution: ts.ModuleResolutionKind.Bundler,
13+
noEmit: true,
14+
skipLibCheck: true,
15+
strict: true,
16+
target: ts.ScriptTarget.ES2022,
17+
types: [],
18+
};
19+
20+
function checkCompilerSurface(
21+
options: ts.CompilerOptions,
22+
): readonly ts.Diagnostic[] {
23+
return ts.getPreEmitDiagnostics(ts.createProgram([COMPILER_ENTRY], options));
24+
}
25+
26+
function namesMissing(
27+
diagnostics: readonly ts.Diagnostic[],
28+
name: string,
29+
): readonly string[] {
30+
return diagnostics
31+
.map((diagnostic) =>
32+
ts.flattenDiagnosticMessageText(diagnostic.messageText, " "),
33+
)
34+
.filter((message) => message.includes(`Cannot find name '${name}'`));
35+
}
36+
37+
test("no Node global reaches the compiler's public surface", () => {
38+
expect(namesMissing(checkCompilerSurface(OPTIONS), "Buffer")).toStrictEqual(
39+
[],
40+
);
41+
});
42+
43+
test("the probe reports a missing global when one is genuinely absent", () => {
44+
// Without this, the assertion above is satisfied by a program that resolved nothing.
45+
const onES5 = checkCompilerSurface({ ...OPTIONS, lib: ["lib.es5.d.ts"] });
46+
47+
expect(onES5.length).toBeGreaterThan(0);
48+
});

‎src/compiler/compiler.ts‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ const defaultLogger = debug("react-native-css:compiler");
4949
* @param options - Compiler options
5050
* @returns A `ReactNativeCssStyleSheet` that can be passed to `StyleSheet.register` or used with a custom runtime
5151
*/
52-
export function compile(code: Buffer | string, options: CompilerOptions = {}) {
52+
export function compile(
53+
code: Uint8Array | string,
54+
options: CompilerOptions = {},
55+
) {
5356
const { logger = defaultLogger } = options;
5457

5558
const isLoggerEnabled =
@@ -63,9 +66,13 @@ export function compile(code: Buffer | string, options: CompilerOptions = {}) {
6366

6467
logger(`Features ${JSON.stringify(features)}`);
6568

69+
// Decoded once: a `Uint8Array` that is not a `Buffer` stringifies to its bytes, not its text.
70+
const source =
71+
typeof code === "string" ? code : new TextDecoder().decode(code);
72+
6673
if (process.env.NODE_ENV !== "production") {
6774
if (defaultLogger.enabled) {
68-
defaultLogger(code.toString());
75+
defaultLogger(source);
6976
}
7077
}
7178

@@ -92,8 +99,7 @@ export function compile(code: Buffer | string, options: CompilerOptions = {}) {
9299
// :root { font-size: Npx } in the CSS to allow CSS-based configuration.
93100
let effectiveRem: number | false = options.inlineRem ?? undefined!;
94101
if (effectiveRem === undefined) {
95-
const css = typeof code === "string" ? code : code.toString();
96-
const match = css.match(/:root\s*\{[^}]*font-size:\s*([\d.]+)px/);
102+
const match = source.match(/:root\s*\{[^}]*font-size:\s*([\d.]+)px/);
97103
effectiveRem = match?.[1] ? parseFloat(match[1]) : 14;
98104
options.inlineRem = effectiveRem;
99105
}

0 commit comments

Comments
 (0)