Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions examples/s2-rslib-notworking/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Local
.DS_Store
*.local
*.log*

# Dist
node_modules
dist/
storybook-static
doc_build/

# IDE
.vscode/*
!.vscode/extensions.json
.idea
Binary file not shown.
61 changes: 61 additions & 0 deletions examples/s2-rslib-notworking/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# s2-rslib-notworking

This is an example of Rslib with unplugin-parcel-macros, that reliably does not work and fails to build.

## Reproduction steps

1. `yarn install`
2. `yarn build`
3. Observe the error `Module not found: Can't resolve 'macro-xyz.css'`

Note that ../s2-rslib-semiworking _does_ build. This is because it uses less complex, and fewer S2 macros, and therefore does not run into this race condition.

## Debugging notes from AI

*TL;DR: It's a race condition in `unplugin-parcel-macros` + `unplugin`'s virtual module system within rspack. No config-level fix found yet β€” likely needs an upstream fix or inlining styles.*

---

:mag: **The Problem**
`pnpm nx build @pandora/react-card-container` fails ~50% of the time with:
```
Module not found: Can't resolve 'macro-<hash>.css'
```
Errors come from `EmptyStateStyles.ts` and `ErrorStateStyles.ts`.

---

:detective: **Root Cause**
The S2 `style()` macro generates CSS at build time. That CSS lives in:
β€’ A module-level `assets` Map inside `unplugin-parcel-macros` (shared across plugin instances)
β€’ Empty placeholder files in `node_modules/.virtual/` written by unplugin's `FakeVirtualModulesPlugin`

The `transform` hook generates CSS β†’ adds to `assets` Map β†’ appends `import "macro-<hash>.css"` to the source. Then `resolveId` maps it to a `.virtual/` file, and the `load` hook serves the real CSS from the Map.

The race happens within rspack's internal concurrent module processing. Even with a *single* lib entry and a *single* rspack compiler, the async `transform β†’ resolveId β†’ load` pipeline has a timing window where virtual CSS module resolution fails.

---

:test_tube: **What I Tested**

| Approach | Result |
|---|---|
| Normal build (ESM + CJS parallel) | ~40% pass |
| Patch: remove `assets.delete()` cleanup | Still fails |
| Patch: prevent `.virtual/` dir deletion on shutdown | Still fails |
| Both patches combined | Still fails |
| Patch: per-instance `assets` Map | Worse |
| Sequential builds (separate processes) | ESM passes, CJS still flaky |
| Single lib entry only | Still flaky (3/10) |
| Disable DTS | Still fails |
| Clean `.virtual/` before every build | Still fails |
| `cssModules: { namedExport: false }` | Still fails |

---

:bulb: **Key Takeaways**
β€’ Not caused by parallel ESM+CJS β€” fails with a single lib entry too
β€’ Not caused by stale `.virtual/` files β€” cleaning doesn't help
β€’ Not caused by the asset cleanup code β€” removing it doesn't fix it
β€’ The working repo (`pandora-tooling-demo`) has fewer macro files and simpler inline `style()` calls β€” smaller race window
β€’ Importing style files `with { type: 'macro' }` doesn't work β€” S2 throws an error since the exports are already-expanded values, not macro functions
32 changes: 32 additions & 0 deletions examples/s2-rslib-notworking/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "s2-rslib-notworking",
"version": "0.0.0",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "rslib build",
"dev": "rslib build --watch"
},
"devDependencies": {
"@rsbuild/plugin-react": "^1.4.5",
"@rslib/core": "^0.19.6",
"@types/react": "^19.2.14",
"react": "^19.2.4",
"typescript": "^5.9.3",
"unplugin-parcel-macros": "^0.2.0"
},
"dependencies": {
"@react-spectrum/s2": "latest",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
34 changes: 34 additions & 0 deletions examples/s2-rslib-notworking/rslib.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { pluginReact } from '@rsbuild/plugin-react';
import { defineConfig } from '@rslib/core';
import { rspack as macros } from 'unplugin-parcel-macros';

export default defineConfig({
source: {
entry: {
index: ['./src/index.tsx'],
},
},
lib: [
{
bundle: true,
dts: true,
format: 'esm',
},
],
output: {
target: 'web',
},
plugins: [pluginReact()],
tools: {
/**
* Add the macros plugin to enable support for React Spectrum S2 styles.
* This is a webpack plugin, so it is added to the rspack config. We should
* use the appendPlugins rather than appending it directly to the config, as
* this is the recommended way to add plugins.
*/
rspack: (config, { appendPlugins }) => {
appendPlugins(macros());
return config;
},
},
});
67 changes: 67 additions & 0 deletions examples/s2-rslib-notworking/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { style } from '@react-spectrum/s2/style' with {type: 'macro'};
import Button from './Button';
import Card from './Card';
import Icon from './Icon';

const Header = () => {
return (
<header className={style({ fontSize: 'ui-xl', fontWeight: 'bold', gridArea: 'header' })}>
<h1>Header</h1>
</header>
);
};

const Nav = () => {
return (
<nav className={style({backgroundColor: 'cyan-400', color: 'magenta-400', gridArea: 'nav'})}>
<h1>Nav</h1>
</nav>
);
};

const Main = () => {
return (
<main className={style({backgroundColor: 'cyan-400', color: 'magenta-400', gridArea: 'main'})}>
<h1>Main</h1>
<Button label="Click me" />
<Card />
<Icon />
</main>
);
};

const Footer = () => {
return (
<footer className={style({backgroundColor: 'cyan-400', color: 'magenta-400', gridArea: 'footer'})}>
<h1>Footer</h1>
</footer>
);
};

const appStyles = style({
display: 'flex',
flexDirection: 'column',
height: '100vh',
gridTemplateAreas: {
default: ['header', 'nav main', 'footer'],
},
gridTemplateRows: ['auto', '1fr', 'auto'],
gridTemplateColumns: ['1fr', '1fr'],
gridGap: 8,
gridAutoFlow: 'row',
gridAutoColumns: '1fr',
gridAutoRows: 'auto',
});

const App = () => {
return (
<div className={appStyles}>
<Header />
<Nav />
<Main />
<Footer />
</div>
);
};

export default App;
50 changes: 50 additions & 0 deletions examples/s2-rslib-notworking/src/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import './button.css';
import { style } from '@react-spectrum/s2/style' with {type: 'macro'};

export interface ButtonProps {
/**
* Whether the button is primary
* @default false
*/
primary?: boolean;
/**
* Background color of the button
*/
backgroundColor?: string;
/**
* Size of Button
* @default 'medium'
*/
size?: 'small' | 'medium' | 'large';
/**
* Label of the button
*/
label: string;
/**
* Optional click handler
*/
onClick?: () => void;
}

const Button = ({
primary = false,
size = 'medium',
backgroundColor,
label,
...props
}: ButtonProps) => {
const mode = primary ? 'demo-button--primary' : 'demo-button--secondary';
return (
<div className={style({backgroundColor: 'cyan-400', color: 'magenta-400'})}>
<button
type="button"
className={['demo-button', `demo-button--${size}`, mode].join(' ')}
{...props}
>
{label}
</button>
</div>
);
};

export default Button;
11 changes: 11 additions & 0 deletions examples/s2-rslib-notworking/src/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { style } from '@react-spectrum/s2/style' with {type: 'macro'};

const Card = () => {
return (
<div className={style({backgroundColor: 'cyan-400', color: 'magenta-400'})}>
<h1>Card</h1>
</div>
);
};

export default Card;
12 changes: 12 additions & 0 deletions examples/s2-rslib-notworking/src/Icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import AlertTriangle from '@react-spectrum/s2/icons/AlertTriangle';
import { iconStyle } from '@react-spectrum/s2/style' with {type: 'macro'};

const Icon = () => {
return (
<AlertTriangle
styles={iconStyle({ size: 'M', color: 'negative' })}
/>
);
};

export default Icon;
34 changes: 34 additions & 0 deletions examples/s2-rslib-notworking/src/button.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.demo-button {
font-weight: 700;
border: 0;
border-radius: 3em;
cursor: pointer;
display: inline-block;
line-height: 1;
}

.demo-button--primary {
color: white;
background-color: #1ea7fd;
}

.demo-button--secondary {
color: #333;
background-color: transparent;
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
}

.demo-button--small {
font-size: 12px;
padding: 10px 16px;
}

.demo-button--medium {
font-size: 14px;
padding: 11px 20px;
}

.demo-button--large {
font-size: 16px;
padding: 12px 24px;
}
1 change: 1 addition & 0 deletions examples/s2-rslib-notworking/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as App } from './App';
14 changes: 14 additions & 0 deletions examples/s2-rslib-notworking/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"lib": ["DOM", "ES2022"],
"module": "ESNext",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"isolatedModules": true,
"resolveJsonModule": true,
"moduleResolution": "bundler",
"useDefineForClassFields": true
},
"include": ["src"]
}
Loading
Loading