[feat] SWC plugin#1623
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Implements a Rust-based StyleX SWC plugin and ports/extends the existing Babel golden fixture test suite to validate parity, plus adds CI/package scaffolding to build/test the WASM artifact.
Changes:
- Added a new Rust SWC plugin implementation (import collection, transform pipeline, runtime injection, validation, nested config helpers).
- Ported and expanded test coverage in Rust to match Babel golden fixtures and unit tests.
- Updated workspace scripts and CI to build/test Rust/WASM and to run installs without lifecycle scripts.
Reviewed changes
Copilot reviewed 68 out of 95 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/@stylexjs/swc-plugin/tests/transform_imports.rs | Adds Rust tests for import collection behavior. |
| packages/@stylexjs/swc-plugin/tests/transform_import_export.rs | Adds Rust tests covering import/export shape interactions and metadata parity. |
| packages/@stylexjs/swc-plugin/tests/test_utils/mod.rs | Introduces Rust golden-fixture loader + snapshot/assert helpers. |
| packages/@stylexjs/swc-plugin/tests/stylex_nested_utils.rs | Adds Rust unit tests for nested config flatten/unflatten helpers. |
| packages/@stylexjs/swc-plugin/tests/shared_nested_transforms.rs | Adds Rust tests for nested defineVars/defineConsts/createTheme compilation. |
| packages/@stylexjs/swc-plugin/tests/process_stylex_rules_unit.rs | Adds Rust unit tests mirroring Babel processStylexRules behavior. |
| packages/@stylexjs/swc-plugin/tests/legacy_transform_pre_plugin.rs | Adds Rust tests for pre-plugin rewrite compatibility. |
| packages/@stylexjs/swc-plugin/tests/legacy_transform_logical_values.rs | Adds Rust tests for logical value transformations parity. |
| packages/@stylexjs/swc-plugin/tests/legacy_transform_logical_properties.rs | Adds Rust tests for logical property transforms + legacy expand behavior. |
| packages/@stylexjs/swc-plugin/tests/golden_fixtures.rs | Adds Rust harness tests for selected golden fixtures. |
| packages/@stylexjs/swc-plugin/tests/golden_css.rs | Adds parity test: Babel fixture CSS equals Rust CSS processor output. |
| packages/@stylexjs/swc-plugin/tests/fixtures.rs | Adds fixture discovery test for Rust harness. |
| packages/@stylexjs/swc-plugin/tests/evaluation_import.rs | Adds Rust tests for .stylex import evaluation and hashing behavior. |
| packages/@stylexjs/swc-plugin/src/visitors/mod.rs | Exposes visitor entrypoints (import collector). |
| packages/@stylexjs/swc-plugin/src/visitors/imports.rs | Implements StyleX + theme import collection and theme import path canonicalization. |
| packages/@stylexjs/swc-plugin/src/utils/transform/validation.rs | Adds validation for create/define-vars/define-consts/createTheme usage constraints. |
| packages/@stylexjs/swc-plugin/src/utils/transform/state.rs | Introduces transform state tracking for create vars, runtime injections, metadata, etc. |
| packages/@stylexjs/swc-plugin/src/utils/transform/runtime.rs | Adds runtime injection insertion + pruning/placeholder logic for exported style vars. |
| packages/@stylexjs/swc-plugin/src/utils/transform/render.rs | Adds code rendering utilities + runtime rule rendering + error formatting. |
| packages/@stylexjs/swc-plugin/src/utils/transform/options.rs | Adds options validation + theme import rewrite + CreateOptions mapping. |
| packages/@stylexjs/swc-plugin/src/utils/transform/mod.rs | Wires parsing, validation, transforms, runtime injection, pruning, and output assembly. |
| packages/@stylexjs/swc-plugin/src/utils/parser.rs | Adds parser helpers and JS normalization via SWC parse/emit. |
| packages/@stylexjs/swc-plugin/src/utils/mod.rs | Re-exports parser/transform APIs. |
| packages/@stylexjs/swc-plugin/src/shared/types.rs | Defines shared option types + collected import structures + transform output. |
| packages/@stylexjs/swc-plugin/src/shared/nested_utils.rs | Implements flatten/unflatten helpers for nested configs. |
| packages/@stylexjs/swc-plugin/src/shared/mod.rs | Exposes shared helpers/types. |
| packages/@stylexjs/swc-plugin/src/lib.rs | Adds Rust crate exports and wasm32 plugin entrypoint wiring. |
| packages/@stylexjs/swc-plugin/rust-toolchain.toml | Pins Rust toolchain and WASM target for building the plugin. |
| packages/@stylexjs/swc-plugin/package.json | Adds npm package scripts to build/test and ship the WASM artifact. |
| packages/@stylexjs/swc-plugin/README.md | Documents how to use the SWC plugin and build the wasm artifact. |
| packages/@stylexjs/swc-plugin/Cargo.toml | Adds crate manifest and dependencies for SWC plugin + tests. |
| packages/@stylexjs/babel-plugin/test-utils/golden-fixtures.js | Adds/updates Babel golden fixture rendering harness utilities. |
| packages/@stylexjs/babel-plugin/src/utils/tests/state-manager-path-utils-test.js | Adds Babel unit tests around theme path resolution helpers. |
| packages/@stylexjs/babel-plugin/src/shared/utils/tests/file-based-identifier-test.js | Adds Babel unit tests for file/export identifier generation. |
| packages/@stylexjs/babel-plugin/tests/process-stylex-rules-unit-test.js | Adds/updates Babel unit tests for processStylexRules parity. |
| packages/@stylexjs/babel-plugin/tests/golden-fixtures-test.js | Adds Babel test runner asserting generated output equals fixtures. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/validation-error/manifest.json | Adds fixture manifest for validation-error golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/validation-error/input.js | Adds fixture input for validation-error golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/validation-error/expected.json | Adds fixture expected output for validation-error golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/runtime-injection/manifest.json | Adds fixture manifest for runtime-injection golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/runtime-injection/input.js | Adds fixture input for runtime-injection golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/runtime-injection/expected.json | Adds fixture expected output for runtime-injection golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/rewrite-theme-extension/tokens.stylex.js | Adds fixture token source for rewrite-theme-extension golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/rewrite-theme-extension/manifest.json | Adds fixture manifest for rewrite-theme-extension golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/rewrite-theme-extension/input.js | Adds fixture input for rewrite-theme-extension golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/rewrite-theme-extension/expected.json | Adds fixture expected output for rewrite-theme-extension golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/props-and-merge/manifest.json | Adds fixture manifest for props-and-merge golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/props-and-merge/input.jsx | Adds fixture input for props-and-merge golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/props-and-merge/expected.json | Adds fixture expected output for props-and-merge golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/options-warning/manifest.json | Adds fixture manifest for options-warning golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/options-warning/input.js | Adds fixture input for options-warning golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/options-warning/expected.json | Adds fixture expected output for options-warning golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/keyframes-and-when/manifest.json | Adds fixture manifest for keyframes-and-when golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/keyframes-and-when/input.js | Adds fixture input for keyframes-and-when golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/keyframes-and-when/expected.json | Adds fixture expected output for keyframes-and-when golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/define-vars-theme/manifest.json | Adds fixture manifest for define-vars-theme golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/define-vars-theme/input.js | Adds fixture input for define-vars-theme golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/define-vars-theme/expected.json | Adds fixture expected output for define-vars-theme golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/create-basic/manifest.json | Adds fixture manifest for create-basic golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/create-basic/input.js | Adds fixture input for create-basic golden case. |
| packages/@stylexjs/babel-plugin/tests/fixtures/golden/create-basic/expected.json | Adds fixture expected output for create-basic golden case. |
| package.json | Adds root scripts to build/test Rust workspace package. |
| .github/workflows/tests.yml | Adjusts CI install behavior and adds Rust/WASM setup steps. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| )); | ||
| }; | ||
| let swc_ecma_ast::Prop::KeyValue(key_value) = &**property else { | ||
| continue; |
There was a problem hiding this comment.
The defineVars() argument validation skips non-KeyValue object properties (e.g. methods/getters/setters/shorthand), which means some invalid shapes can bypass validation entirely. This should return the same “only static values…” error for any non-KeyValue property (or explicitly validate Shorthand if it’s intended to be supported), rather than continue.
| continue; | |
| return Some(format!( | |
| "Only static values are allowed inside of a {method_name}() call." | |
| )); |
| if let Some(value) = options.additional_options.get("enableFontSizePxToRem") { | ||
| if !value.is_boolean() { | ||
| state.errors.push(format!( | ||
| "[@stylexjs/babel-plugin] Expected (options.enableFontSizePxToRem) to be a boolean, but got `{}`.", |
There was a problem hiding this comment.
The error prefix references @stylexjs/babel-plugin even though this is emitted by the SWC plugin. Updating the prefix to @stylexjs/swc-plugin (or a shared neutral prefix) will avoid confusing users when they hit option validation errors.
| "[@stylexjs/babel-plugin] Expected (options.enableFontSizePxToRem) to be a boolean, but got `{}`.", | |
| "[@stylexjs/swc-plugin] Expected (options.enableFontSizePxToRem) to be a boolean, but got `{}`.", |
| if is_theme_import_source(&source) { | ||
| for specifier in &import.specifiers { | ||
| if let ImportSpecifier::Named(named) = specifier { | ||
| let local_name = named.local.sym.to_string(); | ||
| let imported_name = named | ||
| .imported | ||
| .as_ref() | ||
| .map(import_name_to_string) | ||
| .unwrap_or_else(|| local_name.clone()); | ||
| let canonical_import_path = | ||
| canonicalize_theme_import_path(&source, self.filename, self.options); | ||
| self.state.theme_imports.insert( | ||
| local_name, | ||
| ThemeImport { | ||
| import_path: canonical_import_path, | ||
| imported_name, | ||
| }, | ||
| ); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
canonicalize_theme_import_path(...) can do filesystem work (path normalization, existence checks, package.json traversal), but it’s recomputed once per import specifier. Compute it once per ImportDecl (outside the specifier loop) and reuse the result to avoid repeated disk reads on import {a,b,c} from "./tokens.stylex" patterns.
| let (import_items, call_items) = insert_runtime_items(filename, &all_rules, import_path)?; | ||
| let has_runtime_import = module_has_runtime_inject_import(module, import_path); | ||
| if !has_runtime_import { | ||
| module.body.splice(0..0, import_items); | ||
| } | ||
| let insert_index = if has_runtime_import { | ||
| anchor_index | ||
| } else { | ||
| anchor_index + 2 | ||
| }; | ||
| module.body.splice(insert_index..insert_index, call_items); |
There was a problem hiding this comment.
The anchor_index + 2 offset assumes import_items.len() == 2 (import + _inject2 var). This is brittle and will silently break insertion ordering if the import prelude changes. Prefer using anchor_index + import_items.len() (and keep import_items.len() available even after the splice) to make the logic robust.
|
|
||
| let module_export = transform_source( | ||
| &format!( | ||
| "import * as stylex from '@stylexjs/stylex';\nconst styles = {};\nmodule.export = styles;", |
There was a problem hiding this comment.
This test uses module.export = styles;, but CommonJS uses module.exports. As written, the fixture doesn’t represent a real module export, which can reduce confidence in the “export forms” parity being validated. Update the test input to module.exports = styles;.
| "import * as stylex from '@stylexjs/stylex';\nconst styles = {};\nmodule.export = styles;", | |
| "import * as stylex from '@stylexjs/stylex';\nconst styles = {};\nmodule.exports = styles;", |
| cache-dependency-path: yarn.lock | ||
| - run: corepack prepare yarn@1.22.22 --activate | ||
| - run: yarn install --frozen-lockfile | ||
| - run: yarn install --frozen-lockfile --ignore-scripts |
There was a problem hiding this comment.
Switching to --ignore-scripts disables the root postinstall hook that runs npm run build (per package.json). Since the repo’s test script explicitly runs build before flow, this CI job may now be running Flow on an unbuilt workspace. Consider adding an explicit yarn build step before yarn flow (while keeping --ignore-scripts for safety).
| - run: yarn install --frozen-lockfile --ignore-scripts | |
| - run: yarn install --frozen-lockfile --ignore-scripts | |
| - run: yarn build |
| - uses: dtolnay/rust-toolchain@stable | ||
| with: | ||
| targets: wasm32-wasip1 |
There was a problem hiding this comment.
The workflow installs the “stable” toolchain, but the repo also adds packages/@stylexjs/swc-plugin/rust-toolchain.toml pinning a specific Rust version. This can lead to implicit toolchain downloads during the build (or version mismatches) depending on runner state. To make CI deterministic, either configure the action to install the pinned version explicitly, or remove the pin and rely on stable.
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: wasm32-wasip1 | |
| - name: Install pinned Rust toolchain for swc-plugin | |
| run: | | |
| cd packages/@stylexjs/swc-plugin | |
| rustup show active-toolchain | |
| TOOLCHAIN="$(rustup show active-toolchain | cut -d' ' -f1)" | |
| rustup target add --toolchain "$TOOLCHAIN" wasm32-wasip1 |
workflow: benchmarks/perfComparison of performance test results, measured in operations per second. Larger is better.
|
workflow: benchmarks/sizeComparison of minified (terser) and compressed (brotli) size results, measured in bytes. Smaller is better.
|
|
@nmn happy to see this! Let me know if you would like some help testing this out when you have something that passes the tests. |
|
Likewise, stylex is now about 46% of our build time on vite, so anything we can do to speed this up is really welcome! We're deep in a conversion from Styled-Components to Stylex and the build performance is an ever growing concern! |
|
@matclayton we spent a lot of time making sure that StyleX transforms are strictly cache-able on a per-file basis. So if it’s a bottleneck during builds for you, try to improve caching. @skovhus would love help testing this! |
|
@nmn great, please ping me when this is ready for testing. We are currently using https://github.com/Dwlad90/stylex-swc-plugin for some of our projects. |
|
@nmn, this is great news!
I'd love to share my experience and the results of implementing To give some background, I initially implemented The SWC plugin is tightly coupled to the locally installed version of Since the SWC plugin is a WebAssembly module, it runs in a sandbox and lacks raw access to the file system. This is a massive limitation when working with paths. You only get access to the directory from which the plugin was executed (PWD), and even then, it comes with various restrictions and inconsistencies depending on the bundler and OS. While I haven't hit this directly yet, I'm almost 100% certain that SWC plugins don't support the serialization of functions passed through plugin options (like Because of this, and following @SukkaW's recommendation, I decided to rewrite the plugin using This instantly solved a lot of our problems. The biggest win was that I'd be more than happy to help out and dive deeper into my findings if it would be valuable for the community! |
What changed / motivation ?
Using the tests as the contracts this PR implements an entire SWC plugin (using about 3 days of Codex) in Rust. Some new tests were added to Babel, all the Babel plugin tests were ported to Rust and they all pass now.
Additional Context
I'm looking to see if the unplugin package works when using this new plugin correctly next to build more confidence.