Skip to content

buildCSS fails with "Missing closing } at @theme" on Linux CI (Cloudflare Workers Builds) — same code+lockfile, passes on Windows #548

@deadro22

Description

@deadro22

These tests were generated using Claude Code to further understand the issue.

Versions

  • uniwind: 1.2.7
  • tailwindcss: 4.1.18 (root) + 4.1.17 (bundled with @tailwindcss/node@4.1.17 that uniwind pins)
  • expo: 54.0.33, metro per Expo defaults
  • Node: 22.16.0
  • pnpm: 10.28.2 with --config.node-linker=hoisted

What happens

Running pnpm expo export -p web on Cloudflare Workers Builds (Ubuntu CI) consistently fails at ~93% of bundling
with:

SyntaxError: node_modules/uniwind/dist/common/components/web/metro-injected.js: Missing closing } at @theme
Error: Missing closing } at @theme
at Ee (.../@tailwindcss/node/node_modules/tailwindcss/dist/lib.js:1:3353)
...
at Object.Jr (.../@tailwindcss/node/dist/index.js:10:3473)
at findVariantsRec (.../uniwind/dist/shared/uniwind.Hbe7II-i.cjs:225:5)
at generateCSSForThemes (.../uniwind/dist/shared/uniwind.Hbe7II-i.cjs:238:3)
at Object.buildCSS (.../uniwind/dist/shared/uniwind.Hbe7II-i.cjs:300:21)
at injectThemes (.../uniwind/dist/metro/metro-transformer.cjs:1515:3)
at Object.transform (.../uniwind/dist/metro/metro-transformer.cjs:1534:33)

The same code, same lockfile, same --config.node-linker=hoisted install layout succeeds on Windows (I verified
by running pnpm install --config.node-linker=hoisted && pnpm expo export -p web locally on Windows — full export
completes, 124 HTML files generated).

What I ruled out

Hypothesis Test Result
Cache poisoning rm -rf node_modules/uniwind/uniwind.css before each build Still fails
tailwindcss version drift Pinned root to 4.1.17 to match @tailwindcss/node@4.1.17 Still fails
Metro worker race config.maxWorkers = 1 in metro.config.js Still fails
pnpm hoisted layout Reproduced exact hoisted layout on Windows Succeeds
Concurrent buildCSS calls 6 parallel await buildCSS(['light','dark'], cssPath) on Windows All succeed
Tailwind parsing the regenerated uniwind.css Manually ran @tailwindcss/node compile against the 421-line
regenerated file Parses cleanly

The only difference between succeeding and failing is the host OS.

CSS input

global.css uses @variant dark { ... } blocks nested inside @layer theme { :root { ... } }:

@import 'tailwindcss';
@import 'uniwind';

@layer theme {
  :root {
    @variant dark {
      --color-primary: rgb(211, 6, 65);
      /* ... 15 more --color-* vars */
    }
    @variant light {
      /* ... same 16 vars */
    }
    --color-error: rgb(239, 68, 68);
    /* ... shared status colors */
  }
}

@theme {
  --color-primary: var(--color-primary);
  /* ... aliases */
  --font-sans: 'Quicksand-Regular';
}

lightningcss correctly extracts 16 light + 16 dark dashed-idents (confirmed by manually running uniwind's visitor on
Windows).

Suspected location

The error path is findVariantsRec → node.compile('@import "tailwindcss";\n@import "uniwind";') reading the regenerated
node_modules/uniwind/uniwind.css. Since the JS parser is identical across platforms but only fails on Linux, the
corruption likely comes from one of the native pieces upstream (@tailwindcss/oxide or lightningcss's Linux binary)
producing a malformed chunk that the JS parser then chokes on.

Asks

  1. Could buildCSS write uniwind.css atomically (write-tmp + rename) and read its own freshly-written content from
    memory rather than re-reading from disk via Tailwind's @import?
  2. Could injectThemes be guarded by an in-process mutex to prevent overlap when both the web bundle and the SSR bundle
    hit metro-injected.js?
  3. Any known interactions with @tailwindcss/oxide on Linux x64 (gnu) at version 4.1.17?

Thank you in advance.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions