From f07ba3e6d760e0a4a276eff6eccc16fff818fd71 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Tue, 22 Sep 2026 14:11:19 +0300 Subject: [PATCH 1/3] fix(compiler): accept any Uint8Array as compile() input and decode it once `compile` typed its input as Node's `Buffer` and read it back with `toString()`. A `Uint8Array` that is not a `Buffer` stringifies to its bytes rather than its text, and the type kept every consumer typing against the compiler in a program without Node types from compiling at all. Widen the input to `Uint8Array | string`, decode through `TextDecoder` once, and read the rem hint and the debug log from that one string. --- src/__tests__/compiler/compiler.test.tsx | 10 ++++++++++ src/compiler/compiler.ts | 14 ++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/__tests__/compiler/compiler.test.tsx b/src/__tests__/compiler/compiler.test.tsx index 7a9fbea8..b1fd3747 100644 --- a/src/__tests__/compiler/compiler.test.tsx +++ b/src/__tests__/compiler/compiler.test.tsx @@ -26,6 +26,16 @@ test("hello world", () => { }); }); +test("compiles bytes exactly as it compiles their text", () => { + const css = `:root { font-size: 16px; } .my-class { padding: 1rem; }`; + const fromText = compile(css).stylesheet(); + + expect(compile(new TextEncoder().encode(css)).stylesheet()).toStrictEqual( + fromText, + ); + expect(compile(Buffer.from(css)).stylesheet()).toStrictEqual(fromText); +}); + test("reads global CSS variables", () => { const compiled = compile( `@layer theme { diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 214cd615..e8581fbb 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -46,7 +46,10 @@ const defaultLogger = debug("react-native-css:compiler"); * @param options - Compiler options * @returns A `ReactNativeCssStyleSheet` that can be passed to `StyleSheet.register` or used with a custom runtime */ -export function compile(code: Buffer | string, options: CompilerOptions = {}) { +export function compile( + code: Uint8Array | string, + options: CompilerOptions = {}, +) { const { logger = defaultLogger } = options; const isLoggerEnabled = @@ -60,9 +63,13 @@ export function compile(code: Buffer | string, options: CompilerOptions = {}) { logger(`Features ${JSON.stringify(features)}`); + // Decoded once: a `Uint8Array` that is not a `Buffer` stringifies to its bytes, not its text. + const source = + typeof code === "string" ? code : new TextDecoder().decode(code); + if (process.env.NODE_ENV !== "production") { if (defaultLogger.enabled) { - defaultLogger(code.toString()); + defaultLogger(source); } } @@ -89,8 +96,7 @@ export function compile(code: Buffer | string, options: CompilerOptions = {}) { // :root { font-size: Npx } in the CSS to allow CSS-based configuration. let effectiveRem: number | false = options.inlineRem ?? undefined!; if (effectiveRem === undefined) { - const css = typeof code === "string" ? code : code.toString(); - const match = css.match(/:root\s*\{[^}]*font-size:\s*([\d.]+)px/); + const match = source.match(/:root\s*\{[^}]*font-size:\s*([\d.]+)px/); effectiveRem = match?.[1] ? parseFloat(match[1]) : 14; options.inlineRem = effectiveRem; } From e398bb1147dde10fd2ef551752bd859d85151dc8 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Tue, 22 Sep 2026 18:44:14 +0300 Subject: [PATCH 2/3] test(compiler): pin that no Node global reaches the public surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The type half of this change had no test. `Buffer` in `compile`'s declaration is a TS2591 raised inside `node_modules` for any consumer whose tsconfig does not pull in `@types/node` — and `native/conditions/index.d.ts` imports `StyleRule` from `react-native-css/compiler`, so the surface reaches them all. Two cases over the compiler entry, type-checked with `types: []`: no diagnostic naming a missing Node global, and a control that reports one when a global is genuinely absent, so the first cannot pass over a program that resolved nothing. Checked against `src/` rather than `dist/`, so it needs no build: the published declarations are generated from these files and the `source` condition resolves a consumer here directly. Restoring `Buffer | string` fails it with the exact diagnostic above. --- .../public-surface-node-types.test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/__tests__/compiler/public-surface-node-types.test.ts diff --git a/src/__tests__/compiler/public-surface-node-types.test.ts b/src/__tests__/compiler/public-surface-node-types.test.ts new file mode 100644 index 00000000..2b2d0e78 --- /dev/null +++ b/src/__tests__/compiler/public-surface-node-types.test.ts @@ -0,0 +1,48 @@ +import path from "node:path"; + +import ts from "typescript"; + +// `native/conditions/index.d.ts` imports `StyleRule` from `react-native-css/compiler`, so a Node +// global in that surface is a TS2591 inside `node_modules` for any consumer without `@types/node`. +const COMPILER_ENTRY = path.join(__dirname, "../../compiler/index.ts"); + +const OPTIONS: ts.CompilerOptions = { + lib: ["lib.es2024.d.ts"], + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.Bundler, + noEmit: true, + skipLibCheck: true, + strict: true, + target: ts.ScriptTarget.ES2022, + types: [], +}; + +function checkCompilerSurface( + options: ts.CompilerOptions, +): readonly ts.Diagnostic[] { + return ts.getPreEmitDiagnostics(ts.createProgram([COMPILER_ENTRY], options)); +} + +function namesMissing( + diagnostics: readonly ts.Diagnostic[], + name: string, +): readonly string[] { + return diagnostics + .map((diagnostic) => + ts.flattenDiagnosticMessageText(diagnostic.messageText, " "), + ) + .filter((message) => message.includes(`Cannot find name '${name}'`)); +} + +test("no Node global reaches the compiler's public surface", () => { + expect(namesMissing(checkCompilerSurface(OPTIONS), "Buffer")).toStrictEqual( + [], + ); +}); + +test("the probe reports a missing global when one is genuinely absent", () => { + // Without this, the assertion above is satisfied by a program that resolved nothing. + const onES5 = checkCompilerSurface({ ...OPTIONS, lib: ["lib.es5.d.ts"] }); + + expect(onES5.length).toBeGreaterThan(0); +}); From 0f2f1fd1d42cf278fe37391edc96696f6fbcad10 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Tue, 22 Sep 2026 19:20:43 +0300 Subject: [PATCH 3/3] test(compiler): pin the second read of the input, the debug log A coverage sweep found `defaultLogger(source)` never executed: the `debug` namespace is off under jest, so the only read this suite drove was the rem probe. That is one of the two sites this change touches, and the one whose wrong answer is a debug trail of byte values nobody can read. `debug.enable` flips the instance the compiler module already built, so the test needs no module reload; the sink is restored and the namespace disabled in a `finally`. Restoring `code.toString()` there fails it. --- src/__tests__/compiler/compiler.test.tsx | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/__tests__/compiler/compiler.test.tsx b/src/__tests__/compiler/compiler.test.tsx index b1fd3747..df14aa16 100644 --- a/src/__tests__/compiler/compiler.test.tsx +++ b/src/__tests__/compiler/compiler.test.tsx @@ -1,3 +1,4 @@ +import { debug } from "debug"; import { compile } from "react-native-css/compiler"; test("hello world", () => { @@ -36,6 +37,29 @@ test("compiles bytes exactly as it compiles their text", () => { expect(compile(Buffer.from(css)).stylesheet()).toStrictEqual(fromText); }); +test("the debug log is handed the decoded text, not the byte values", () => { + const css = `:root { font-size: 16px; }`; + const written: string[] = []; + const emit = debug.log; + + // `enable` flips the instance the compiler module already built, so no reload is needed. + debug.enable("react-native-css:compiler"); + debug.log = (...args: unknown[]): void => { + written.push(args.map(String).join(" ")); + }; + + try { + compile(new TextEncoder().encode(css)); + } finally { + debug.log = emit; + debug.disable(); + } + + const joined = written.join("\n"); + expect(joined).toContain("font-size: 16px"); + expect(joined).not.toContain("58,114"); +}); + test("reads global CSS variables", () => { const compiled = compile( `@layer theme {