[Bug/Feature Request] Vite dev mode: imported constants as computed property keys produce var(--xxx) instead of resolved values
- Dominant language
- JavaScript
- Stars
- 10.3k
- Forks
- 481
- Avg merge
- 3d 8h
- Merged PRs (30d)
- 13
Description
### Summary
When using `@stylexjs/unplugin` with Vite in development mode, **imported constants used as computed property keys** in `stylex.create()` are not resolved during the first CSS collection pass. This causes `var(--xxx)` CSS variable references to appear where the actual values should be, leading to cryptic LightningCSS errors like "Invalid empty selector".
### Reproduction
**Minimal reproduction:**
```javascript
// breakpoints.js
export const breakpoints = {
tablet: "@media (max-width: 768px)",
};
// Or using stylex.defineConsts (same issue):
// export const breakpoints = stylex.defineConsts({
// tablet: "@media (max-width: 768px)",
// });
```
```javascript
// styles.stylex.js
import * as stylex from '@stylexjs/stylex';
import { breakpoints } from './breakpoints';
const styles = stylex.create({
container: {
padding: {
default: '2rem',
[breakpoints.tablet]: '1rem', // <-- FAILS in Vite dev mode
},
},
});
```
**Expected CSS:**
```css
@media (max-width: 768px) {
.x1abc123 { padding: 1rem; }
}
```
**Actual CSS produced:**
```css
@media var(--xgageza) {
.x1abc123 { padding: 1rem; }
}
```
This invalid CSS causes LightningCSS to throw "Invalid empty selector" with an unhelpful stack trace.
### Root Cause Analysis
Based on our debugging, here's what happens:
1. **Vite dev server starts** and begins hot module processing
2. **First CSS collection pass**: StyleX collects rules from all modules
3. **Race condition**: During this first pass, the imported constants module (`breakpoints.js`) may not be fully processed yet
4. **Unresolved references**: `breakpoints.tablet` is not resolved to `"@media (max-width: 768px)"` — instead, StyleX generates a CSS variable reference `var(--xgageza)`
5. **Invalid CSS**: The resulting `@media var(--xgageza) { ... }` is invalid CSS syntax
6. **LightningCSS crash**: LightningCSS throws "Invalid empty selector" when parsing this invalid CSS
**Key observation**: This only happens in Vite **dev mode**. Production builds (where all files are processed together) resolve correctly.
### Our Debugging Approach
To diagnose this issue, we patched `node_modules/@stylexjs/unplugin/lib/es/index.mjs`:
```javascript
function processCollectedRulesToCSS(rules, options) {
if (!rules || rules.length === 0) return '';
const collectedCSS = stylexBabelPlugin.processStylexRules(rules, {
useLayers: !!options.useCSSLayers,
enableLTRRTLComments: options?.enableLTRRTLComments
});
// DEBUG: Write CSS to file for inspection
const fs = require('fs');
const lines = collectedCSS.split('\n');
console.log('[StyleX DEBUG] Total CSS lines:', lines.length);
fs.writeFileSync(`stylex-debug-${lines.length}.css`, collectedCSS);
let code;
try {
const result = lightningTransform({
filename: 'styles.css',
code: Buffer.from(collectedCSS),
// ...
});
code = result.code;
} catch (error) {
// Capture failing CSS for inspection
fs.writeFileSync('stylex-debug-FAILED.css', collectedCSS);
console.log('[StyleX DEBUG] FAILING CSS written to stylex-debug-FAILED.css');
throw error;
}
return code.toString();
}
```
This revealed the `var(--xgageza)` pattern appearing where media queries should be.
### Current Workaround
**Use inline string literals instead of imported constants as computed property keys:**
```javascript
// ❌ BROKEN - computed property key with imported constant
import { breakpoints } from './breakpoints';
const styles = stylex.create({
container: {
padding: {
default: '2rem',
[breakpoints.tablet]: '1rem', // Produces var(--xxx)!
},
},
});
// ✅ WORKS - inline string literal
const styles = stylex.create({
container: {
padding: {
default: '2rem',
"@media (max-width: 768px)": '1rem', // Always works
},
},
});
```
**Note**: This workaround requires copy-pasting media query strings across all style files, which is error-prone and defeats the purpose of having centralized breakpoint constants.
---
## Feature Request: Debug Options for CSS Generation
### Problem
When CSS generation errors occur, users have no visibility into what CSS StyleX is generating. The error messages point to line numbers in generated CSS that cannot be accessed or inspected.
**Example error:**
```
Invalid empty selector
unknown file:528:1
at processCollectedRulesToCSS (node_modules/@stylexjs/unplugin/lib/es/index.mjs:34:7)
```
### Proposed Solution
Add optional debug configuration to `@stylexjs/unplugin`:
```javascript
// vite.config.js
styleXUnplugin.vite({
dev: true,
// New debug options
debug: {
// Write generated CSS to file on each collection
writeCSS: true,
// Path for debug output (default: './stylex-debug/')
outputPath: './stylex-debug/',
// Log collection stats to console
logStats: true,
// On error, write the failing CSS for inspection
writeOnError: true,
// Show context lines around error location
errorContext: 5,
}
})
```
**Proposed behaviors:**
1. **`debug.writeCSS`**: Write each CSS collection to `{outputPath}/stylex-{timestamp}.css` with metadata header
2. **`debug.writeOnError`**: On LightningCSS errors, write failing CSS to `{outputPath}/stylex-FAILED.css` and show context:
```
[StyleX ERROR] Invalid CSS at line 528:
526 | }
527 |
> 528 | @media var(--xgageza) {
529 | .x1abc123 {
530 | padding: 1rem;
Failing CSS written to: ./stylex-debug/stylex-FAILED.css
```
3. **`debug.logStats`**: Show collection progress:
```
[StyleX] CSS collection: 553 rules from 47 modules
[StyleX] CSS collection: 1098 rules from 89 modules (full)
```
4. **Source mapping**: When an error occurs, attempt to map the failing CSS rule back to the source file/line that generated it
### Alternative: Fix the Race Condition
Ideally, the root cause should be fixed so that computed property keys with imported constants work reliably in Vite dev mode. Potential approaches:
1. **Ensure import resolution completes** before CSS collection
2. **Defer CSS generation** until all module values are resolved
3. **Detect unresolved references** and throw a helpful error early (e.g., "breakpoints.tablet was not resolved during CSS collection. Ensure the module is loaded before styles that reference it.")
### Environment
- `@stylexjs/unplugin`: 0.17.4
- `@stylexjs/stylex`: 0.17.4
- `@stylexjs/babel-plugin`: 0.17.4
- Vite: 7.3.0
- Node: v20.x
- OS: Windows 11
### Benefits of Debug Options
1. **Faster debugging** - Users can immediately inspect generated CSS without patching node_modules
2. **Better error messages** - Show context around failing CSS lines
3. **Issue reporting** - Users can attach debug output to GitHub issues
4. **Development visibility** - Understand what CSS is being generated and when
5. **Source tracing** - Map CSS errors back to source files
---
## Additional Context
### Files Affected in Our Codebase
We had to replace 54 occurrences of `[breakpoints.xxx]` patterns across 11 files to work around this issue:
- Typography/semantic token files
- Component style files
- Layout and spacing definitions
### Documentation Created
- Internal troubleshooting guide with debugging approach
- Long-term solutions proposal (ESLint rule, Babel macro, pre-build code generation)
- Blog post explaining the debugging journey
### Related Issues
*Search for and link to any related StyleX issues about Vite dev mode, CSS variable resolution, or `defineConsts` timing*
---
*This issue was created after spending significant time debugging an "Invalid empty selector" error. We hope the debugging approach and workaround help others facing similar issues, and that the debug options feature request improves the developer experience for all StyleX users.*
**Full debugging writeup:** https://dev.to/sal_lancaster/debugging-stylex-vite-the-mystery-of-invalid-empty-selector-158k
Contributor guide
Assessment
This issue has not been assessed yet.