Skip to content

fix(compiler): accept any Uint8Array as compile() input and decode it once - #460

Open
YevheniiKotyrlo wants to merge 3 commits into
nativewind:mainfrom
YevheniiKotyrlo:fix/compile-input-type
Open

YevheniiKotyrlo wants to merge 3 commits into
nativewind:mainfrom
YevheniiKotyrlo:fix/compile-input-type

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Problem

compile types its input as Node's Buffer, so a Uint8Array that is not a Buffer passes the type and stringifies to its byte values joined with commas — and that declaration reaches the public types, where a program without @types/node fails inside node_modules.

// src/compiler/compiler.ts
export function compile(code: Buffer | string, options: CompilerOptions = {}) {
  …
  defaultLogger(code.toString());
  …
  const css = typeof code === "string" ? code : code.toString();

Two things follow.

The declaration reaches every consumer of the native types. native/conditions/index.d.ts imports StyleRule from react-native-css/compiler, whose index re-exports compile, so a program without @types/node fails with TS2591: Cannot find name 'Buffer' and no first-party line named.

And toString() on a plain Uint8Array is Array.prototype.toString — what TextEncoder, fetch and Bun.file().bytes() hand out. The rem probe then reads 58,114,111,111,116 instead of CSS, so :root { font-size } matches nothing, effectiveRem falls back to 14, and every rem in the sheet is scaled wrong. lightningcss still receives the bytes it wanted, so nothing downstream reports it.

Solution

Uint8Array | string, decoded once through TextDecoder. A Buffer IS a Uint8Array, so every existing caller still type-checks and the decode is the same UTF-8.

export function compile(code: Uint8Array | string, options: CompilerOptions = {}) {
  const source = typeof code === "string" ? code : new TextDecoder().decode(code);

Tests

One case in compiler.test.tsx compiles the same sheet as text, as TextEncoder output and as Buffer.from, asserting the three stylesheets toStrictEqual. The sheet carries :root { font-size: 16px } and a 1rem padding, so the byte input is not merely accepted — its rem is read. Restoring code.toString() at the rem probe fails it with padding 16 expected, 14 received.

public-surface-node-types.test.ts covers the type half, which nothing asserted before: two cases type-check the compiler entry with types: [] — no diagnostic naming a missing Node global, plus a control that reports one when a global is genuinely absent, so the first cannot pass over a program that resolved nothing. It reads src/ rather than dist/, so it needs no build. Restoring Buffer | string fails it with the exact TS2591 above.

Every caller in the suite and in the Metro transformer hands compile a string or a real Buffer, and every program that type-checks the package has Node's types in scope — which is why neither half surfaces today.

Mutation-proved: reverting the src/ diff and re-running these files alone turns 3 of 14 red.

Verification

yarn test src/__tests__/compiler 50 passed, 6 skipped · yarn typecheck 0 · yarn lint 0

Known limits

No existing issue tracks this — searched the tracker for Uint8Array, Buffer compile and TextDecoder, zero relevant hits.

Base

Branched off f70c402. main has since taken #451 (a5002c5). 2 of the 3 files this changes also moved there (src/__tests__/compiler/compiler.test.tsx, src/compiler/compiler.ts), and it still merges cleanly onto current main. Every measurement above was taken on f70c402. Say the word and I will re-apply it onto current main.

… 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.
@YevheniiKotyrlo

YevheniiKotyrlo commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor Author

Evidence

The wrong answer is plausible rather than absent: Array.prototype.toString on a plain Uint8Array produces 58,114,111,111,116 — a string the :root { font-size } probe then fails to match in silence, while lightningcss still receives the bytes it wanted.

One case in compiler.test.tsx compiles the same sheet as text, as TextEncoder output and as Buffer.from, asserting the three stylesheets are toStrictEqual. The sheet carries :root { font-size: 16px } and a 1rem padding, so the byte input is not merely accepted: its rem is read.

Mutation proof: restoring code.toString() at the rem probe fails it with padding 16 expected, 14 received, every other assertion green.

yarn jest src/__tests__/compiler/compiler.test.tsx 11 passed (main: 10) · yarn typecheck 0 · yarn lint 0

No device frame: the type half is a compile failure in the consumer's own program, and the byte path is reachable only from outside the bundler.

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.
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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant