Skip to content

Commit dd1026c

Browse files
chore: example of rslib in React Spectrum (#9728)
* example of rslib issues in React Spectrum * Commit a working rslib --------- Co-authored-by: Rob Snow <rsnow@adobe.com>
1 parent b12a29e commit dd1026c

14 files changed

Lines changed: 1434 additions & 0 deletions

examples/s2-rslib/.gitignore

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Local
2+
.DS_Store
3+
*.local
4+
*.log*
5+
6+
# Dist
7+
node_modules
8+
dist/
9+
storybook-static
10+
doc_build/
11+
12+
# IDE
13+
.vscode/*
14+
!.vscode/extensions.json
15+
.idea
69.7 KB
Binary file not shown.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
diff --git a/dist/index.js b/dist/index.js
2+
index 6a30f19344431fcbd495c8951695bdd2e5917583..00f310c9123972aa013a924bcc9c8d5b7cb20205 100644
3+
--- a/dist/index.js
4+
+++ b/dist/index.js
5+
@@ -1646,19 +1646,38 @@ function decodeVirtualModuleId(encoded, _plugin) {
6+
function isVirtualModuleId(encoded, plugin) {
7+
return (0, import_path8.dirname)(encoded) === plugin.__virtualModulePrefix;
8+
}
9+
-var FakeVirtualModulesPlugin = class {
10+
+var FakeVirtualModulesPlugin = class FakeVirtualModulesPlugin {
11+
+ // PATCH (ported from unplugin>=2.x): share the virtual dir per-process and
12+
+ // reference-count it so parallel compilers in one process (e.g. rslib's js+dts)
13+
+ // don't delete the dir out from under each other on shutdown.
14+
+ static counters = /* @__PURE__ */ new Map();
15+
+ static initCleanup = false;
16+
+ name = "FakeVirtualModulesPlugin";
17+
constructor(plugin) {
18+
this.plugin = plugin;
19+
+ if (!FakeVirtualModulesPlugin.initCleanup) {
20+
+ FakeVirtualModulesPlugin.initCleanup = true;
21+
+ process.once("exit", () => {
22+
+ FakeVirtualModulesPlugin.counters.forEach((_, dir) => {
23+
+ import_fs3.default.rmSync(dir, { recursive: true, force: true });
24+
+ });
25+
+ });
26+
+ }
27+
}
28+
- name = "FakeVirtualModulesPlugin";
29+
apply(compiler) {
30+
const dir = this.plugin.__virtualModulePrefix;
31+
if (!import_fs3.default.existsSync(dir)) {
32+
import_fs3.default.mkdirSync(dir, { recursive: true });
33+
}
34+
+ const counter = FakeVirtualModulesPlugin.counters.get(dir) ?? 0;
35+
+ FakeVirtualModulesPlugin.counters.set(dir, counter + 1);
36+
compiler.hooks.shutdown.tap(this.name, () => {
37+
- if (import_fs3.default.existsSync(dir)) {
38+
- import_fs3.default.rmdirSync(dir, { recursive: true });
39+
+ const remaining = (FakeVirtualModulesPlugin.counters.get(dir) ?? 1) - 1;
40+
+ if (remaining === 0) {
41+
+ FakeVirtualModulesPlugin.counters.delete(dir);
42+
+ import_fs3.default.rmSync(dir, { recursive: true, force: true });
43+
+ } else {
44+
+ FakeVirtualModulesPlugin.counters.set(dir, remaining);
45+
}
46+
});
47+
}
48+
@@ -1682,7 +1701,7 @@ function getRspackPlugin(factory) {
49+
return (userOptions) => {
50+
return {
51+
apply(compiler) {
52+
- const VIRTUAL_MODULE_PREFIX = (0, import_path9.resolve)(compiler.options.context ?? process.cwd(), "node_modules/.virtual");
53+
+ const VIRTUAL_MODULE_PREFIX = (0, import_path9.resolve)(compiler.options.context ?? process.cwd(), "node_modules/.virtual", compiler.rspack.experiments.VirtualModulesPlugin ? "" : process.pid.toString());
54+
const injected = compiler.$unpluginContext || {};
55+
compiler.$unpluginContext = injected;
56+
const meta = {
57+
@@ -1710,7 +1729,7 @@ function getRspackPlugin(factory) {
58+
});
59+
const externalModules = /* @__PURE__ */ new Set();
60+
if (plugin.resolveId) {
61+
- const vfs = new FakeVirtualModulesPlugin(plugin);
62+
+ const vfs = compiler.rspack.experiments.VirtualModulesPlugin ? new compiler.rspack.experiments.VirtualModulesPlugin() : new FakeVirtualModulesPlugin(plugin);
63+
vfs.apply(compiler);
64+
plugin.__vfsModules = /* @__PURE__ */ new Set();
65+
compiler.hooks.compilation.tap(plugin.name, (compilation, { normalModuleFactory }) => {
66+
@@ -1744,11 +1763,16 @@ function getRspackPlugin(factory) {
67+
if (isExternal)
68+
externalModules.add(resolved);
69+
if (!import_fs4.default.existsSync(resolved)) {
70+
+ const encodedVirtualPath = encodeVirtualModuleId(resolved, plugin);
71+
if (!plugin.__vfsModules.has(resolved)) {
72+
plugin.__vfsModules.add(resolved);
73+
- await vfs.writeModule(resolved);
74+
+ if (compiler.rspack.experiments.VirtualModulesPlugin) {
75+
+ await vfs.writeModule(encodedVirtualPath, "");
76+
+ } else {
77+
+ await vfs.writeModule(resolved);
78+
+ }
79+
}
80+
- resolved = encodeVirtualModuleId(resolved, plugin);
81+
+ resolved = encodedVirtualPath;
82+
}
83+
resolveData.request = resolved;
84+
});

examples/s2-rslib/README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# s2-rslib
2+
3+
With the patch, it should work. Maintaining the rest of this README for historical context.
4+
5+
Patch is based on these PRs with assistance from Claude:
6+
https://github.com/unjs/unplugin/pull/510
7+
https://github.com/unjs/unplugin/pull/549
8+
https://github.com/unjs/unplugin/pull/538
9+
Because the newer version of unplugin works. We're currently unable to upgrade to the newer version though due to some old build tools we still support.
10+
11+
This is an example of Rslib with unplugin-parcel-macros, that reliably does not work and fails to build if you turn off the unplugin patch.
12+
13+
14+
## Reproduction steps
15+
16+
1. turn off the patch (delete from resolutions)
17+
2. `yarn install`
18+
3. `yarn build`
19+
4. Observe the error `Module not found: Can't resolve 'macro-xyz.css'`
20+
21+
## Debugging notes from AI
22+
23+
*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.*
24+
25+
---
26+
27+
:mag: **The Problem**
28+
`pnpm nx build @pandora/react-card-container` fails ~50% of the time with:
29+
```
30+
Module not found: Can't resolve 'macro-<hash>.css'
31+
```
32+
Errors come from `EmptyStateStyles.ts` and `ErrorStateStyles.ts`.
33+
34+
---
35+
36+
:detective: **Root Cause**
37+
The S2 `style()` macro generates CSS at build time. That CSS lives in:
38+
• A module-level `assets` Map inside `unplugin-parcel-macros` (shared across plugin instances)
39+
• Empty placeholder files in `node_modules/.virtual/` written by unplugin's `FakeVirtualModulesPlugin`
40+
41+
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.
42+
43+
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.
44+
45+
---
46+
47+
:test_tube: **What I Tested**
48+
49+
| Approach | Result |
50+
|---|---|
51+
| Normal build (ESM + CJS parallel) | ~40% pass |
52+
| Patch: remove `assets.delete()` cleanup | Still fails |
53+
| Patch: prevent `.virtual/` dir deletion on shutdown | Still fails |
54+
| Both patches combined | Still fails |
55+
| Patch: per-instance `assets` Map | Worse |
56+
| Sequential builds (separate processes) | ESM passes, CJS still flaky |
57+
| Single lib entry only | Still flaky (3/10) |
58+
| Disable DTS | Still fails |
59+
| Clean `.virtual/` before every build | Still fails |
60+
| `cssModules: { namedExport: false }` | Still fails |
61+
62+
---
63+
64+
:bulb: **Key Takeaways**
65+
• Not caused by parallel ESM+CJS — fails with a single lib entry too
66+
• Not caused by stale `.virtual/` files — cleaning doesn't help
67+
• Not caused by the asset cleanup code — removing it doesn't fix it
68+
• The working repo (`pandora-tooling-demo`) has fewer macro files and simpler inline `style()` calls — smaller race window
69+
• Importing style files `with { type: 'macro' }` doesn't work — S2 throws an error since the exports are already-expanded values, not macro functions

examples/s2-rslib/package.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "s2-rslib",
3+
"version": "0.0.0",
4+
"private": true,
5+
"files": [
6+
"dist"
7+
],
8+
"type": "module",
9+
"types": "./dist/index.d.ts",
10+
"exports": {
11+
".": {
12+
"types": "./dist/index.d.ts",
13+
"import": "./dist/index.js"
14+
}
15+
},
16+
"scripts": {
17+
"build": "rslib build",
18+
"dev": "rslib build --watch"
19+
},
20+
"dependencies": {
21+
"@react-spectrum/s2": "latest",
22+
"react": "^19.2.0",
23+
"react-dom": "^19.2.0"
24+
},
25+
"devDependencies": {
26+
"@rsbuild/plugin-react": "^1.4.5",
27+
"@rslib/core": "^0.19.6",
28+
"@types/react": "^19.2.14",
29+
"react": "^19.2.4",
30+
"typescript": "^5.9.3",
31+
"unplugin-parcel-macros": "^0.2.0"
32+
},
33+
"resolutions": {
34+
"unplugin@npm:^1.9.0": "patch:unplugin@npm%3A1.16.1#~/.yarn/patches/unplugin-npm-1.16.1-77bc28083a.patch"
35+
}
36+
}

examples/s2-rslib/rslib.config.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {pluginReact} from '@rsbuild/plugin-react';
2+
import {defineConfig} from '@rslib/core';
3+
import {rspack as macros} from 'unplugin-parcel-macros';
4+
5+
export default defineConfig({
6+
source: {
7+
entry: {
8+
index: ['./src/index.tsx']
9+
}
10+
},
11+
lib: [
12+
{
13+
bundle: true,
14+
dts: true,
15+
format: 'esm'
16+
}
17+
],
18+
output: {
19+
target: 'web'
20+
},
21+
plugins: [pluginReact()],
22+
tools: {
23+
/**
24+
* Add the macros plugin to enable support for React Spectrum S2 styles.
25+
* This is a webpack plugin, so it is added to the rspack config. We should
26+
* use the appendPlugins rather than appending it directly to the config, as
27+
* this is the recommended way to add plugins.
28+
*/
29+
rspack: (config, {appendPlugins}) => {
30+
appendPlugins(macros());
31+
return config;
32+
}
33+
}
34+
});

examples/s2-rslib/src/App.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
2+
import Button from './Button';
3+
import Card from './Card';
4+
import Icon from './Icon';
5+
6+
const Header = () => {
7+
return (
8+
<header className={style({fontSize: 'ui-xl', fontWeight: 'bold', gridArea: 'header'})}>
9+
<h1>Header</h1>
10+
</header>
11+
);
12+
};
13+
14+
const Nav = () => {
15+
return (
16+
<nav className={style({backgroundColor: 'cyan-400', color: 'magenta-400', gridArea: 'nav'})}>
17+
<h1>Nav</h1>
18+
</nav>
19+
);
20+
};
21+
22+
const Main = () => {
23+
return (
24+
<main className={style({backgroundColor: 'cyan-400', color: 'magenta-400', gridArea: 'main'})}>
25+
<h1>Main</h1>
26+
<Button label="Click me" />
27+
<Card />
28+
<Icon />
29+
</main>
30+
);
31+
};
32+
33+
const Footer = () => {
34+
return (
35+
<footer
36+
className={style({backgroundColor: 'cyan-400', color: 'magenta-400', gridArea: 'footer'})}>
37+
<h1>Footer</h1>
38+
</footer>
39+
);
40+
};
41+
42+
const appStyles = style({
43+
display: 'flex',
44+
flexDirection: 'column',
45+
height: '100vh',
46+
gridTemplateAreas: {
47+
default: ['header', 'nav main', 'footer']
48+
},
49+
gridTemplateRows: ['auto', '1fr', 'auto'],
50+
gridTemplateColumns: ['1fr', '1fr'],
51+
gridGap: 8,
52+
gridAutoFlow: 'row',
53+
gridAutoColumns: '1fr',
54+
gridAutoRows: 'auto'
55+
});
56+
57+
const App = () => {
58+
return (
59+
<div className={appStyles}>
60+
<Header />
61+
<Nav />
62+
<Main />
63+
<Footer />
64+
</div>
65+
);
66+
};
67+
68+
export default App;

examples/s2-rslib/src/Button.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import './button.css';
2+
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
3+
4+
export interface ButtonProps {
5+
/**
6+
* Whether the button is primary.
7+
*
8+
* @default false
9+
*/
10+
primary?: boolean;
11+
/**
12+
* Background color of the button.
13+
*/
14+
backgroundColor?: string;
15+
/**
16+
* Size of Button.
17+
*
18+
* @default 'medium'
19+
*/
20+
size?: 'small' | 'medium' | 'large';
21+
/**
22+
* Label of the button.
23+
*/
24+
label: string;
25+
/**
26+
* Optional click handler.
27+
*/
28+
onClick?: () => void;
29+
}
30+
31+
const Button = ({
32+
primary = false,
33+
size = 'medium',
34+
backgroundColor,
35+
label,
36+
...props
37+
}: ButtonProps) => {
38+
const mode = primary ? 'demo-button--primary' : 'demo-button--secondary';
39+
return (
40+
<div className={style({backgroundColor: 'cyan-400', color: 'magenta-400'})}>
41+
<button
42+
type="button"
43+
className={['demo-button', `demo-button--${size}`, mode].join(' ')}
44+
{...props}>
45+
{label}
46+
</button>
47+
</div>
48+
);
49+
};
50+
51+
export default Button;

examples/s2-rslib/src/Card.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
2+
3+
const Card = () => {
4+
return (
5+
<div className={style({backgroundColor: 'cyan-400', color: 'magenta-400'})}>
6+
<h1>Card</h1>
7+
</div>
8+
);
9+
};
10+
11+
export default Card;

examples/s2-rslib/src/Icon.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import AlertTriangle from '@react-spectrum/s2/icons/AlertTriangle';
2+
import {iconStyle} from '@react-spectrum/s2/style' with {type: 'macro'};
3+
4+
const Icon = () => {
5+
return <AlertTriangle styles={iconStyle({size: 'M', color: 'negative'})} />;
6+
};
7+
8+
export default Icon;

0 commit comments

Comments
 (0)