Skip to content

compile() fails with "failed to deserialize; expected an object-like struct named Specifier, found ()" on lightningcss >= 1.31 (any unresolved var()); the 1.30.2 guard doesn't cover it #470

Description

@EmanuelBacalhau

Summary

Any stylesheet that still contains a var() after variable inlining makes compile() throw when the installed lightningcss is 1.31.x, 1.32.0 or 1.33.0. This is the same lightningcss regression that the loader already blocks for 1.30.2, but the guard only matches that exact version (and its message says "or try upgrading", which doesn't help). The peer range is lightningcss >=1.27.0, so npm/bun install 1.33.0 by default and nothing catches it.

In practice, Tailwind 4 emits .border{border-style:var(--tw-border-style)}, so any NativeWind 5 app that uses border crashes the iOS/Android bundle on a fresh install.

Versions

  • react-native-css 3.1.0-rc.0 (also 3.0.7)
  • lightningcss 1.31.1, 1.32.0, 1.33.0 (also 1.30.2 and 1.31.0)
  • Node 24.18, macOS arm64, npm 11

Minimal reproduction

mkdir repro && cd repro && npm init -y
npm i --legacy-peer-deps react-native-css@3.1.0-rc.0 lightningcss@1.33.0
// repro.cjs
const { compile } = require("react-native-css/compiler");
compile(".a{color:var(--x)}", {});
$ node repro.cjs
Error: failed to deserialize; expected an object-like struct named Specifier, found ()

compile(".a{color:var(--x)}", { inlineVariables: false }) fails the same way.

Expected: the same output as with lightningcss 1.30.1.
Actual: throws.

Results

react-native-css lightningcss .a{color:var(--x)} .border{border-style:var(--x)} + .border-dashed{border-style:dashed} .a{border-style:dashed}
3.1.0-rc.0 1.30.1 OK OK OK
3.1.0-rc.0 1.31.1 FAIL FAIL OK
3.1.0-rc.0 1.32.0 FAIL FAIL OK
3.1.0-rc.0 1.33.0 FAIL FAIL OK
3.0.7 1.30.1 — OK —
3.0.7 1.33.0 — FAIL —

When --x is defined once (so it gets inlined away), it passes on every version.

Cause

This comes from upstream lightningcss: parcel-bundler/lightningcss#1065 and #1081, both still open on 1.33.0. Since 1.30.2 (commit 9879b91, "Use serde-content instead of private serde types"), an explicit null in an Option<T> field of a node that a JS visitor returns no longer deserializes as None. The var() token is serialized to JS as {"name":{"ident":"--x","from":null},"fallback":null}, so handing it back unchanged throws. If the field is omitted, it works.

You can reproduce it with lightningcss alone:

require("lightningcss").transform({
  filename: "a.css",
  code: Buffer.from(".a{color:var(--x)}"),
  visitor: { Declaration: (d) => d },  // identity -> throws on >= 1.30.2
});

In src/compiler/compiler.ts, three visitors return the node they received:

  • second pass: Rule(rule) { ...; return rule; } and StyleSheetExit(sheet) { ...; return sheet; }. Neither changes the AST, and this pass's output is discarded anyway (only builder is used). This is why inlineVariables: false still fails.
  • first pass: StyleSheetExit(sheet) { return inlineVariables(sheet, vars); }. It legitimately modifies the AST, but any remaining var() still has from: null / fallback: null.

Suggested fix

I tested this locally by patching dist/ against lightningcss 1.31.1 and 1.33.0. The output is byte-identical to lightningcss 1.30.1 on a stylesheet with defined, undefined, nested-fallback and media-conditional variables:

  1. In the second pass, return nothing from Rule and StyleSheetExit (the visitor only reads).

  2. In the first pass, strip nulls before returning the inlined sheet, e.g.

    const stripNulls = <T>(v: T): T =>
      JSON.parse(JSON.stringify(v, (_k, x) => (x === null ? undefined : x)));
    firstPassVisitor.StyleSheetExit = (sheet) =>
      stripNulls(inlineVariables(sheet, vars));

    A targeted recursive delete would be cheaper than a JSON round-trip. The same workaround is described in lightningcss#1081 and #1065.

If you'd rather not touch the compiler, a smaller step is to make the guard in lightningcss-loader.ts reject >=1.30.2 until upstream ships a fix (or narrow the peer to >=1.27.0 <1.30.2), and remove "or try upgrading" from the message.

Workaround for users

{ "overrides": { "lightningcss": "1.30.1" } }

(resolutions for yarn, pnpm.overrides for pnpm.)

Related

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions