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
- 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?
- 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?
- Any known interactions with @tailwindcss/oxide on Linux x64 (gnu) at version 4.1.17?
Thank you in advance.
These tests were generated using Claude Code to further understand the issue.
Versions
uniwind: 1.2.7tailwindcss: 4.1.18 (root) + 4.1.17 (bundled with@tailwindcss/node@4.1.17that uniwind pins)expo: 54.0.33,metroper Expo defaults--config.node-linker=hoistedWhat happens
Running
pnpm expo export -p webon Cloudflare Workers Builds (Ubuntu CI) consistently fails at ~93% of bundlingwith:
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=hoistedinstall layout succeeds on Windows (I verifiedby running
pnpm install --config.node-linker=hoisted && pnpm expo export -p weblocally on Windows — full exportcompletes, 124 HTML files generated).
What I ruled out
rm -rf node_modules/uniwind/uniwind.cssbefore each buildtailwindcssversion drift4.1.17to match@tailwindcss/node@4.1.17config.maxWorkers = 1in metro.config.jsbuildCSScallsawait buildCSS(['light','dark'], cssPath)on Windowsuniwind.css@tailwindcss/nodecompile against the 421-lineThe only difference between succeeding and failing is the host OS.
CSS input
global.cssuses@variant dark { ... }blocks nested inside@layer theme { :root { ... } }: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
memory rather than re-reading from disk via Tailwind's @import?
hit metro-injected.js?
Thank you in advance.