rollup-plugin-react-scoped-css 1.2.7 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,135 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ - Demonstrating empathy and kindness toward other people
21
+ - Being respectful of differing opinions, viewpoints, and experiences
22
+ - Giving and gracefully accepting constructive feedback
23
+ - Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ - Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ - The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ - Trolling, insulting or derogatory comments, and personal or political attacks
33
+ - Public or private harassment
34
+ - Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ - Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official e-mail address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported privately to the project maintainer on GitHub at
63
+ [@cmseguin](https://github.com/cmseguin). If you would rather not contact the
64
+ maintainer directly, you can use GitHub's
65
+ [report abuse](https://github.com/contact/report-abuse) form.
66
+
67
+ All complaints will be reviewed and investigated promptly and fairly.
68
+
69
+ All community leaders are obligated to respect the privacy and security of the
70
+ reporter of any incident.
71
+
72
+ ## Enforcement Guidelines
73
+
74
+ Community leaders will follow these Community Impact Guidelines in determining
75
+ the consequences for any action they deem in violation of this Code of Conduct:
76
+
77
+ ### 1. Correction
78
+
79
+ **Community Impact**: Use of inappropriate language or other behavior deemed
80
+ unprofessional or unwelcome in the community.
81
+
82
+ **Consequence**: A private, written warning from community leaders, providing
83
+ clarity around the nature of the violation and an explanation of why the
84
+ behavior was inappropriate. A public apology may be requested.
85
+
86
+ ### 2. Warning
87
+
88
+ **Community Impact**: A violation through a single incident or series of
89
+ actions.
90
+
91
+ **Consequence**: A warning with consequences for continued behavior. No
92
+ interaction with the people involved, including unsolicited interaction with
93
+ those enforcing the Code of Conduct, for a specified period of time. This
94
+ includes avoiding interactions in community spaces as well as external channels
95
+ like social media. Violating these terms may lead to a temporary or permanent
96
+ ban.
97
+
98
+ ### 3. Temporary Ban
99
+
100
+ **Community Impact**: A serious violation of community standards, including
101
+ sustained inappropriate behavior.
102
+
103
+ **Consequence**: A temporary ban from any sort of interaction or public
104
+ communication with the community for a specified period of time. No public or
105
+ private interaction with the people involved, including unsolicited interaction
106
+ with those enforcing the Code of Conduct, is allowed during this period.
107
+ Violating these terms may lead to a permanent ban.
108
+
109
+ ### 4. Permanent Ban
110
+
111
+ **Community Impact**: Demonstrating a pattern of violation of community
112
+ standards, including sustained inappropriate behavior, harassment of an
113
+ individual, or aggression toward or disparagement of classes of individuals.
114
+
115
+ **Consequence**: A permanent ban from any sort of public interaction within the
116
+ community.
117
+
118
+ ## Attribution
119
+
120
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
121
+ version 2.1, available at
122
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
123
+
124
+ Community Impact Guidelines were inspired by
125
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
126
+
127
+ For answers to common questions about this code of conduct, see the FAQ at
128
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
129
+ [https://www.contributor-covenant.org/translations][translations].
130
+
131
+ [homepage]: https://www.contributor-covenant.org
132
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
133
+ [Mozilla CoC]: https://github.com/mozilla/diversity
134
+ [FAQ]: https://www.contributor-covenant.org/faq
135
+ [translations]: https://www.contributor-covenant.org/translations
@@ -0,0 +1,182 @@
1
+ # Contributing to rollup-plugin-react-scoped-css
2
+
3
+ Thanks for your interest in improving the plugin! Contributions of every size
4
+ are welcome — bug reports, documentation fixes, new test cases, and code.
5
+
6
+ ## Code of Conduct
7
+
8
+ This project follows the [Contributor Covenant](CODE_OF_CONDUCT.md). By
9
+ participating, you are expected to uphold it. Disagreement about technical
10
+ decisions is fine; personal attacks, harassment, and abusive behavior are not.
11
+ Report unacceptable behavior through the channels listed in the policy.
12
+
13
+ Anyone is free to open a PR and contribute to this project... just be civilized!
14
+
15
+ ## Ways to contribute
16
+
17
+ ### Reporting bugs
18
+
19
+ Before opening an issue, please search
20
+ [existing issues](https://github.com/cmseguin/rollup-plugin-react-scoped-css/issues)
21
+ to avoid duplicates. A good bug report includes:
22
+
23
+ - the plugin version (from `package.json` or `npm ls rollup-plugin-react-scoped-css`),
24
+ - your bundler and version (Vite, Rollup, or Rolldown), plus your Node version,
25
+ - a minimal reproduction — the smallest component and stylesheet that show the
26
+ problem, ideally as a link to a repository or a code sandbox,
27
+ - the actual output and the output you expected,
28
+ - any error output in full.
29
+
30
+ ### Suggesting features
31
+
32
+ Open an issue describing the problem you are trying to solve before writing
33
+ code. Explain the use case and, if possible, how other libraries handle it.
34
+ Features that can be implemented by an existing CSS or bundler plugin are
35
+ usually out of scope — this plugin deliberately does not transpile JSX,
36
+ preprocess SCSS/LESS, or bundle CSS.
37
+
38
+ ### Improving documentation
39
+
40
+ The README is the primary documentation. Fixes for typos, unclear examples, or
41
+ missing caveats are always appreciated and can go straight to a pull request.
42
+
43
+ ## Development setup
44
+
45
+ **Requirements:** Node >= 18 (CI runs Node 24) and npm.
46
+
47
+ ```sh
48
+ git clone https://github.com/cmseguin/rollup-plugin-react-scoped-css.git
49
+ cd rollup-plugin-react-scoped-css
50
+ npm ci
51
+ npm run build
52
+ ```
53
+
54
+ Useful scripts:
55
+
56
+ | Script | What it does |
57
+ | --- | --- |
58
+ | `npm run build` | Type-checks and compiles `src/` to `dist/` with `tsc` |
59
+ | `npm run build:watch` | Rebuilds on change |
60
+ | `npm test` | Runs the Jest unit test suite |
61
+ | `npm run lint` | Runs ESLint (including Prettier formatting checks) |
62
+ | `npm run test:e2e` | Runs the browser compatibility harness (see below) |
63
+
64
+ ## Project structure
65
+
66
+ ```text
67
+ src/
68
+ ├── index.ts # Plugin entry point: the Vite plugin and the
69
+ │ # pre/post halves used by Rollup and Rolldown
70
+ ├── ast-program.ts # Walks the compiled React output
71
+ ├── ast-transformer.ts # Adds the data-<hash> attributes to JSX elements
72
+ ├── ast-iterator.ts # Generic AST traversal helpers
73
+ ├── parsers/ # Babel output parsers (classic and automatic JSX)
74
+ ├── css/
75
+ │ ├── scope-css.ts # The CSS scoper and selector rewriting
76
+ │ └── css-features.spec.ts # CSS support matrix tests
77
+ └── utils/ # Shared helpers
78
+ e2e/ # Playwright harness for Vite, Rollup, and Rolldown
79
+ ```
80
+
81
+ Unit tests live next to the code they cover as `*.spec.ts`. The e2e harness
82
+ installs the packed plugin into isolated throwaway projects under
83
+ `e2e/.cache/`, so it is slow but exercises the real published artifact.
84
+
85
+ ## Running tests
86
+
87
+ Unit tests are fast and should cover any new scoping logic:
88
+
89
+ ```sh
90
+ npm test
91
+ ```
92
+
93
+ The end-to-end harness drives a real browser against Vite 2–8, Rollup, and
94
+ Rolldown. It needs Node >= 20.19 and a Playwright browser:
95
+
96
+ ```sh
97
+ npm run test:e2e:install # once; installs Chromium
98
+ npm run test:e2e # all bundlers
99
+ ```
100
+
101
+ See the [Testing](README.md#testing) section of the README for the full option
102
+ list. If your change touches CSS scoping, please add cases to
103
+ `src/css/scope-css.spec.ts` or `src/css/css-features.spec.ts` rather than
104
+ relying on the e2e harness alone.
105
+
106
+ ## Coding standards
107
+
108
+ - **TypeScript** with `strict` mode enabled. Add types; avoid `any`.
109
+ - **Formatting** is enforced by Prettier through ESLint. Run `npm run lint`
110
+ before pushing (or `npx eslint --fix` on the files you changed).
111
+ - Match the style of the surrounding code: small, focused modules with
112
+ descriptive names.
113
+ - Keep the plugin's scope tight. It transforms CSS and JSX; it should not start
114
+ transpiling, bundling, or resolving dependencies.
115
+ - New behavior needs tests. New options need a JSDoc comment in the plugin
116
+ options type and a README entry.
117
+
118
+ ## Commit messages
119
+
120
+ This project uses [Conventional Commits](https://www.conventionalcommits.org/)
121
+ and releases are automated with
122
+ [semantic-release](https://github.com/semantic-release/semantic-release). The
123
+ commit message controls the next version number, so it matters:
124
+
125
+ | Prefix | Use for | Effect |
126
+ | --- | --- | --- |
127
+ | `feat:` | A new feature | Minor release |
128
+ | `fix:` | A bug fix | Patch release |
129
+ | `docs:` | Documentation only | No release |
130
+ | `test:` | Tests only | No release |
131
+ | `chore:` | Tooling, dependencies, CI | No release |
132
+ | `refactor:` | Code change with no behavior change | No release |
133
+ | `perf:` | Performance improvement | Patch release |
134
+
135
+ A scope is optional but encouraged, e.g. `fix(css): scope selectors inside
136
+ container queries`. A `!` after the type or scope marks a breaking change:
137
+
138
+ ```text
139
+ feat(css)!: rename the deep selector to ::deep
140
+ ```
141
+
142
+ Pull requests are squash-merged, so the PR title becomes the commit message —
143
+ please make it conventional too.
144
+
145
+ ## Pull request process
146
+
147
+ 1. Fork the repository and create a topic branch from `main`
148
+ (`git checkout -b fix/scoped-at-rules`).
149
+ 2. Make your change, add tests, and update the README if you changed behavior
150
+ or options.
151
+ 3. Make sure the full suite passes locally:
152
+
153
+ ```sh
154
+ npm run build && npm test && npm run lint
155
+ ```
156
+
157
+ 4. Open the pull request against `main`. Describe the problem, the approach,
158
+ and any trade-offs. Link related issues with `Closes #123`.
159
+ 5. CI runs the build, unit tests, lint, and the e2e harness. All jobs must be
160
+ green before a review.
161
+ 6. Address review feedback by pushing additional commits; the branch is
162
+ squash-merged when approved.
163
+
164
+ Draft pull requests are welcome if you want early feedback on an approach.
165
+
166
+ ## Release process
167
+
168
+ Releases are cut automatically from `main` by semantic-release on every merge.
169
+ It determines the version from the commit history, publishes to npm, creates the
170
+ GitHub release with generated notes, and commits the new version back to the
171
+ repository with `[skip ci]`. Maintainers do not publish manually — merging a
172
+ `feat:` or `fix:` pull request is the release.
173
+
174
+ Prereleases are published under the `alpha` npm tag from `releases/<version>-alpha`
175
+ branches, independently of `main`. You do not need to do anything special for
176
+ those; contributors work against `main`.
177
+
178
+ ## Questions
179
+
180
+ If anything here is unclear, open a
181
+ [discussion or issue](https://github.com/cmseguin/rollup-plugin-react-scoped-css/issues).
182
+ We would rather answer a question than have you guess.
package/README.md CHANGED
@@ -3,13 +3,14 @@
3
3
  [![npm version](https://img.shields.io/npm/v/rollup-plugin-react-scoped-css.svg?style=flat-square)](https://www.npmjs.com/package/rollup-plugin-react-scoped-css)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/rollup-plugin-react-scoped-css.svg?style=flat-square)](https://www.npmjs.com/package/rollup-plugin-react-scoped-css)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
6
+ [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg?style=flat-square)](CODE_OF_CONDUCT.md)
6
7
 
7
8
  ## Motivations
8
9
  While using react in a professional context, I realized that it was lacking the scoped css feature that I learned to love from Vue and Angular. After some research I came across good plugins, but sadly were not compatible with vite and/or rollup. Thus, I decided to create this plugin.
9
10
 
10
11
  ## Requirements
11
12
  + **node** >= 18
12
- + **vite** >= 5
13
+ + **vite** >= 2, or **rollup** >= 4, or **rolldown** >= 1
13
14
 
14
15
  ## How to install
15
16
 
@@ -32,19 +33,22 @@ export default defineConfig({
32
33
  })
33
34
  ```
34
35
 
35
- in rollup:
36
+ without vite (rollup or rolldown):
36
37
  ```js
37
- // rollup.config.js
38
+ // rollup.config.js / rolldown.config.js
38
39
  import { reactScopedCssPlugin } from 'rollup-plugin-react-scoped-css';
39
40
 
41
+ // The plugin returns two halves that must run before and after your JSX and
42
+ // CSS plugins.
43
+ const [scopedCssPre, scopedCssPost] = reactScopedCssPlugin();
44
+
40
45
  export default {
41
- input: 'src/input.js',
42
- output: { file: 'ouput.js' },
43
- plugins: [ reactScopedCssPlugin() ]
46
+ input: 'src/main.tsx',
47
+ plugins: [scopedCssPre, /* JSX + CSS plugins go here */, scopedCssPost],
44
48
  };
45
49
  ```
46
50
 
47
- ***Notes:** Because this plugin has been built with vite as its primary usecase, it doesn't transpile LESS, SCSS or other preprocessors. For that reason, you will likely need to add your transpilation plugins before reactScopedCssPlugin if you plan on using this without vite.*
51
+ ***Notes:** Because this plugin has been built with vite as its primary usecase, it doesn't transpile JSX/TypeScript, LESS, SCSS or other preprocessors, and it doesn't process CSS imports. When you use Rollup or Rolldown directly, you need to plug in those pieces between the two halves of the plugin. See [Using without Vite](#using-without-vite-rollup-and-rolldown) for complete, working configs.*
48
52
 
49
53
  ### Customizing the plugin
50
54
  There are a few options available to customize how the plugin works
@@ -102,17 +106,213 @@ There are a few options available to customize how the plugin works
102
106
  }
103
107
  ```
104
108
 
105
- ### Advanced Rollup usecases
106
- Since this plugin works in two parts, you might need to expose the first part, then add any other plugin, and then expose the second part of the plugin. This part is automatically handled with vite thanks to the enforce attribute.
109
+ ### Using without Vite (Rollup and Rolldown)
110
+ The plugin works in two halves:
111
+
112
+ 1. `scopedCssPre` rewrites scoped style imports such as
113
+ `./Component.scoped.css` to `./Component.scoped.css?scope=<hash>`.
114
+ 2. `scopedCssPost` scopes the CSS and adds the matching `data-<hash>` attributes
115
+ to the compiled React output.
116
+
117
+ Vite orders these automatically through the `enforce` option. With Rollup or
118
+ Rolldown, `enforce` is ignored, so you must place them in the `plugins` array
119
+ yourself and put the following between them:
120
+
121
+ - **JSX/TypeScript transpilation first.** The second half inspects compiled
122
+ `React.createElement()` or `_jsx()` calls, not raw JSX. Babel (shown below) or
123
+ any equivalent transpiler works.
124
+ - **A CSS loader/emitter.** Rollup and Rolldown do not process `.css` imports
125
+ on their own. The bridge below reads the file, lets `scopedCssPost` transform
126
+ it, then emits the transformed CSS and replaces the import with an empty
127
+ module.
128
+
129
+ #### When is the bridge needed?
130
+
131
+ The bridge is needed when nothing else in your build turns `.css` imports into
132
+ JavaScript. Neither Rollup nor Rolldown does that on its own, so a plain config
133
+ fails as soon as it reaches the first style import.
134
+
135
+ Use the bridge when:
136
+
137
+ - you are adding the plugin to a plain Rollup or Rolldown config and want
138
+ minimal CSS handling — all styles collected into one stylesheet;
139
+ - you don't already have a CSS plugin that can load the scoped imports.
140
+
141
+ You can skip it when:
142
+
143
+ - Vite is handling CSS and plugin ordering for you;
144
+ - another CSS plugin already handles `.css` imports. Put that plugin between
145
+ `scopedCssPre` and `scopedCssPost`, and make sure it accepts the
146
+ `?scope=<hash>` query the plugin adds to scoped imports.
147
+
148
+ The bridge only does extraction. If you need style injection, CSS modules,
149
+ preprocessor support, or per-chunk styles, use a dedicated CSS plugin instead.
150
+
151
+ #### CSS bridge
152
+ Save this next to your config:
107
153
 
108
154
  ```js
109
- const reactScopedPlugins = reactScopedCssPlugin()
155
+ // scoped-css-bridge.js
156
+ import { readFile } from 'node:fs/promises'
157
+
158
+ const stripQuery = (id) => id.split('?')[0]
159
+ const isCss = (id) => stripQuery(id).endsWith('.css')
160
+
161
+ export function scopedCssBridge() {
162
+ const styles = new Map()
163
+
164
+ return [
165
+ {
166
+ name: 'scoped-css:load',
167
+ async load(id) {
168
+ if (isCss(id)) {
169
+ return readFile(stripQuery(id), 'utf8')
170
+ }
171
+ },
172
+ },
173
+ {
174
+ name: 'scoped-css:emit',
175
+ transform(code, id) {
176
+ if (!isCss(id)) return
177
+ styles.set(stripQuery(id), code)
178
+ return { code: 'export default undefined', map: null }
179
+ },
180
+ generateBundle() {
181
+ this.emitFile({
182
+ type: 'asset',
183
+ fileName: 'styles.css',
184
+ source: [...styles.values()].join('\n'),
185
+ })
186
+ },
187
+ },
188
+ ]
189
+ }
190
+ ```
191
+
192
+ #### Rollup
193
+
194
+ ```sh
195
+ npm i -D rollup @babel/core @babel/preset-react @babel/preset-typescript \
196
+ @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-node-resolve \
197
+ @rollup/plugin-replace
198
+ ```
199
+
200
+ ```js
201
+ // rollup.config.js
202
+ import babel from '@rollup/plugin-babel'
203
+ import commonjs from '@rollup/plugin-commonjs'
204
+ import { nodeResolve } from '@rollup/plugin-node-resolve'
205
+ import replace from '@rollup/plugin-replace'
206
+ import { reactScopedCssPlugin } from 'rollup-plugin-react-scoped-css'
207
+ import { scopedCssBridge } from './scoped-css-bridge.js'
208
+
209
+ const [scopedCssPre, scopedCssPost] = reactScopedCssPlugin()
210
+ const [cssLoader, cssEmitter] = scopedCssBridge()
211
+
110
212
  export default {
111
- //...
112
- plugins: [ reactScopedPlugins[0], {...stylingPlugins}, reactScopedPlugins[1] ]
113
- };
213
+ input: 'src/main.tsx',
214
+ output: {
215
+ dir: 'dist',
216
+ format: 'esm',
217
+ entryFileNames: 'main.js',
218
+ sourcemap: true,
219
+ },
220
+ plugins: [
221
+ scopedCssPre,
222
+ babel({
223
+ babelHelpers: 'bundled',
224
+ extensions: ['.js', '.jsx', '.ts', '.tsx'],
225
+ exclude: '**/node_modules/**',
226
+ presets: [
227
+ ['@babel/preset-react', { runtime: 'automatic' }],
228
+ '@babel/preset-typescript',
229
+ ],
230
+ }),
231
+ cssLoader,
232
+ scopedCssPost,
233
+ cssEmitter,
234
+ replace({
235
+ preventAssignment: true,
236
+ values: { 'process.env.NODE_ENV': JSON.stringify('production') },
237
+ }),
238
+ nodeResolve({ extensions: ['.js', '.jsx', '.ts', '.tsx'] }),
239
+ commonjs(),
240
+ ],
241
+ }
242
+ ```
243
+
244
+ `nodeResolve` and `commonjs` are only needed if React is bundled rather than
245
+ loaded externally. `replace` is needed because React's CommonJS build checks
246
+ `process.env.NODE_ENV` at runtime.
247
+
248
+ #### Rolldown
249
+
250
+ Rolldown resolves node_modules and bundles CommonJS itself, but Babel still has
251
+ to run before `scopedCssPost`, and CSS has to be treated as JavaScript for the
252
+ bridge to take over.
253
+
254
+ ```sh
255
+ npm i -D rolldown @babel/core @babel/preset-react @babel/preset-typescript \
256
+ @rollup/plugin-babel
114
257
  ```
115
258
 
259
+ ```js
260
+ // rolldown.config.js
261
+ import { defineConfig } from 'rolldown'
262
+ import babel from '@rollup/plugin-babel'
263
+ import { reactScopedCssPlugin } from 'rollup-plugin-react-scoped-css'
264
+ import { scopedCssBridge } from './scoped-css-bridge.js'
265
+
266
+ const [scopedCssPre, scopedCssPost] = reactScopedCssPlugin()
267
+ const [cssLoader, cssEmitter] = scopedCssBridge()
268
+
269
+ export default defineConfig({
270
+ input: 'src/main.tsx',
271
+ platform: 'browser',
272
+ // Rolldown no longer bundles CSS itself. The bridge emits it instead.
273
+ moduleTypes: { '.css': 'js' },
274
+ transform: {
275
+ define: { 'process.env.NODE_ENV': JSON.stringify('production') },
276
+ },
277
+ plugins: [
278
+ scopedCssPre,
279
+ babel({
280
+ babelHelpers: 'bundled',
281
+ extensions: ['.js', '.jsx', '.ts', '.tsx'],
282
+ exclude: '**/node_modules/**',
283
+ presets: [
284
+ ['@babel/preset-react', { runtime: 'automatic' }],
285
+ '@babel/preset-typescript',
286
+ ],
287
+ }),
288
+ cssLoader,
289
+ scopedCssPost,
290
+ cssEmitter,
291
+ ],
292
+ output: {
293
+ dir: 'dist',
294
+ format: 'esm',
295
+ entryFileNames: 'main.js',
296
+ sourcemap: true,
297
+ },
298
+ })
299
+ ```
300
+
301
+ #### Notes
302
+ - Neither Rollup nor Rolldown generates an HTML entry. Load the emitted files
303
+ yourself, for example
304
+ `<link rel="stylesheet" href="./styles.css">` and
305
+ `<script type="module" src="./main.js"></script>`.
306
+ - If you use a CSS plugin such as `rollup-plugin-postcss` instead of the bridge,
307
+ make sure it accepts the `?scope=<hash>` query that `scopedCssPre` adds and
308
+ runs between the two halves of the plugin.
309
+ - Preprocessors (SCSS, LESS, etc.) still need to run before `scopedCssPost`.
310
+ Compile them to CSS inside the bridge (or add a preprocessor plugin between
311
+ `cssLoader` and `scopedCssPost`) so the scoper receives plain CSS.
312
+ - `scopedCssPost` must stay after your JSX transpiler and after the CSS loader,
313
+ and the CSS emitter must stay after `scopedCssPost`.
314
+
315
+
116
316
  ## Usage
117
317
 
118
318
  ```jsx
@@ -170,24 +370,34 @@ Be careful with descendant selectors in recursive components! For a CSS rule wit
170
370
  npm test
171
371
  ```
172
372
 
173
- ### End-to-end Vite compatibility harness
373
+ ### End-to-end compatibility harness
374
+
375
+ The e2e harness packs the plugin with `npm pack`, installs it into isolated
376
+ projects, then drives a real browser against each setup:
174
377
 
175
- The e2e harness packs the plugin with `npm pack`, installs it into an isolated
176
- project for each Vite major, then drives Chromium against both the dev server
177
- and a production build served with `vite preview`.
378
+ - **Vite 2, 3, 4, 5, 6, 7, and 8** — the dev server and a production build
379
+ served with `vite preview`.
380
+ - **Rollup** — a production build using `@rollup/plugin-babel`, the CSS bridge
381
+ documented above, and the bundled React runtime, served by a static Node
382
+ server.
383
+ - **Rolldown** — the same production build and CSS bridge with Rolldown's
384
+ built-in resolver, served the same way.
178
385
 
179
386
  Prerequisites: Node >= 20.19 and a Playwright browser.
180
387
 
181
388
  ```sh
182
389
  npm run test:e2e:install # once; installs Chromium
183
- npm run test:e2e # Vite 5, 6, 7, and 8; dev + build/preview
390
+ npm run test:e2e # all bundlers (Vite matrix + Rollup + Rolldown)
391
+ npm run test:e2e:vite # Vite only
392
+ npm run test:e2e:bundlers # Rollup and Rolldown only
184
393
  ```
185
394
 
186
395
  Options (pass-through after `--`):
187
396
 
188
397
  ```sh
189
- npm run test:e2e -- --vite 7 # one major (repeatable/comma-separated)
190
- npm run test:e2e -- --mode dev # dev, build, or both (default)
398
+ npm run test:e2e -- --bundler rollup # vite, rollup, rolldown, or all (default)
399
+ npm run test:e2e -- --bundler vite --vite 7 # one Vite major
400
+ npm run test:e2e -- --mode dev # dev, build, or both (Vite only)
191
401
  npm run test:e2e -- --browser firefox # chromium (default), firefox, webkit
192
402
  npm run test:e2e -- --headed # watch the browser
193
403
  npm run test:e2e -- --clean --force # wipe and reinstall cached installs
@@ -195,25 +405,35 @@ npm run test:e2e -- --keep-temp # keep production build output
195
405
  npm run test:e2e -- --allow-broken-sourcemap
196
406
  ```
197
407
 
408
+ `--vite` and `--mode` only affect the Vite matrix. The Rollup and Rolldown
409
+ setups run production builds only, so `--mode dev` fails when either is
410
+ selected.
411
+
198
412
  The harness asserts that:
199
413
 
200
414
  - scoped JSX receives `data-<hash>` attributes and every element from a file
201
415
  shares that file's hash;
202
416
  - scoped selectors are applied, global styles stay global, and styles from one
203
417
  component do not leak into another;
204
- - computed styles match in both dev and production builds;
418
+ - computed styles match in Vite dev, Vite production, and the standalone
419
+ bundles;
205
420
  - emitted CSS contains the scoped selectors and JS contains the matching hash;
206
421
  - JS sourcemaps reference the original `.tsx` files and include their content;
207
- - there are no page/console/network errors and no `SOURCEMAP_BROKEN` warnings.
422
+ - there are no page/console/network errors, and Vite runs emit no
423
+ `SOURCEMAP_BROKEN` warnings.
208
424
 
209
- Vite installs are cached in `e2e/.cache/`; the cache key includes the packed
210
- plugin, so plugin changes trigger a reinstall. Vite 8 uses Rolldown rather than
211
- Rollup; `rollup` is installed in the harness only to satisfy this package's peer
212
- dependency. No CI workflow runs the e2e harness yet — it is a local script.
425
+ All installs are cached in `e2e/.cache/`; the cache key includes the packed
426
+ plugin, so plugin changes trigger a reinstall. Each Vite major uses the
427
+ `@vitejs/plugin-react` release that still declares it as a peer. Vite 8 uses
428
+ Rolldown rather than Rollup; `rollup` is installed in the Vite projects only to
429
+ satisfy this package's peer dependency. The `e2e` CI job runs
430
+ `npm run test:e2e`, covering all three bundlers.
213
431
 
214
432
  ## Contributing
215
433
  Anyone is free to open a PR and contribute to this project... just be civilized!
216
434
 
435
+ Read [CONTRIBUTING.md](CONTRIBUTING.md) for the development setup, test commands, commit conventions, and pull request process.
436
+
217
437
 
218
438
  ## Credits
219
439
  Inspired by [react-scoped-css](https://github.com/gaoxiaoliangz/react-scoped-css).
@@ -21,6 +21,13 @@ const isDeepFunctionSelector = (node) => node.type === "PseudoClassSelector" &&
21
21
  */
22
22
  const isRootLikePseudoSelector = (node) => node.type === "PseudoClassSelector" &&
23
23
  (node.name.toLowerCase() === "root" || node.name.toLowerCase() === "scope");
24
+ /**
25
+ * The `::view-transition-*` tree is rooted at the document root, which never
26
+ * receives the scope attribute. A bare occurrence can therefore never match
27
+ * once scoped, so it is left global just like `:root`.
28
+ */
29
+ const isViewTransitionPseudoElement = (node) => node.type === "PseudoElementSelector" &&
30
+ node.name.toLowerCase().startsWith("view-transition");
24
31
  const isCombinator = (node) => node.type === "Combinator";
25
32
  const isNestingSelector = (node) => node.type === "NestingSelector";
26
33
  /**
@@ -159,6 +166,11 @@ export function scopeCss(css, filename, hash, source = filename) {
159
166
  if (items.some((item) => isRootLikePseudoSelector(item.data))) {
160
167
  return;
161
168
  }
169
+ // Bare view-transition pseudo-elements are also rooted at the document
170
+ // root, so scoping them would only produce an unmatchable selector.
171
+ if (items.some((item) => isViewTransitionPseudoElement(item.data))) {
172
+ return;
173
+ }
162
174
  if (items.some((item) => item.data.type === "PseudoElementSelector")) {
163
175
  // An attribute cannot follow a pseudo-element, so it has to be
164
176
  // prepended: `::before` -> `[hash]::before`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rollup-plugin-react-scoped-css",
3
- "version": "1.2.7",
4
- "description": "Bring scoped CSS to React with Vite & Rollup. Lightweight component-level style isolation similar to Vue and Angular.",
3
+ "version": "1.3.1",
4
+ "description": "Bring scoped CSS to React with Vite, Rollup & Rolldown. Lightweight component-level style isolation similar to Vue and Angular.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -11,6 +11,8 @@
11
11
  "lint": "eslint '*/**/*.{js,jsx,ts,tsx}'",
12
12
  "test": "jest",
13
13
  "test:e2e": "node e2e/run.mjs",
14
+ "test:e2e:vite": "node e2e/run.mjs --bundler vite",
15
+ "test:e2e:bundlers": "node e2e/run.mjs --bundler rollup,rolldown",
14
16
  "test:e2e:install": "playwright install chromium"
15
17
  },
16
18
  "repository": {
@@ -25,6 +27,7 @@
25
27
  ],
26
28
  "keywords": [
27
29
  "rollup",
30
+ "rolldown",
28
31
  "react",
29
32
  "css",
30
33
  "styles",
@@ -39,6 +42,15 @@
39
42
  "isolation"
40
43
  ],
41
44
  "license": "MIT",
45
+ "jest": {
46
+ "modulePathIgnorePatterns": [
47
+ "<rootDir>/e2e/.cache/"
48
+ ],
49
+ "testPathIgnorePatterns": [
50
+ "/node_modules/",
51
+ "<rootDir>/e2e/"
52
+ ]
53
+ },
42
54
  "bugs": {
43
55
  "url": "https://github.com/cmseguin/rollup-plugin-react-scoped-css/issues"
44
56
  },