Skip to content

[feat] SWC plugin#1623

Draft
nmn wants to merge 1 commit into
mainfrom
feat/rust-maybe
Draft

[feat] SWC plugin#1623
nmn wants to merge 1 commit into
mainfrom
feat/rust-maybe

Conversation

@nmn
Copy link
Copy Markdown
Collaborator

@nmn nmn commented Apr 19, 2026

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.

Copilot AI review requested due to automatic review settings April 19, 2026 23:06
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stylex Error Error Apr 19, 2026 11:08pm

Request Review

@nmn nmn marked this pull request as draft April 19, 2026 23:06
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Apr 19, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
continue;
return Some(format!(
"Only static values are allowed inside of a {method_name}() call."
));

Copilot uses AI. Check for mistakes.
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 `{}`.",
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"[@stylexjs/babel-plugin] Expected (options.enableFontSizePxToRem) to be a boolean, but got `{}`.",
"[@stylexjs/swc-plugin] Expected (options.enableFontSizePxToRem) to be a boolean, but got `{}`.",

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +44
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,
},
);
}
}
}
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +154 to +164
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);
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

let module_export = transform_source(
&format!(
"import * as stylex from '@stylexjs/stylex';\nconst styles = {};\nmodule.export = styles;",
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;.

Suggested change
"import * as stylex from '@stylexjs/stylex';\nconst styles = {};\nmodule.export = styles;",
"import * as stylex from '@stylexjs/stylex';\nconst styles = {};\nmodule.exports = styles;",

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
- run: yarn install --frozen-lockfile --ignore-scripts
- run: yarn install --frozen-lockfile --ignore-scripts
- run: yarn build

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +35
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-wasip1
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
- 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

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown

workflow: benchmarks/perf

Comparison of performance test results, measured in operations per second. Larger is better.
yarn workspace v1.22.22
yarn run v1.22.22
$ node ./compare.js /tmp/tmp.gIa32vw0Zu /tmp/tmp.UbsU1GkuSC

Results Base Patch Ratio
babel-plugin: stylex.create
· basic create 550 540 0.98 -
· complex create 64 67 1.05 +
babel-plugin: stylex.createTheme
· basic themes 427 438 1.03 +
· complex themes 33 34 1.03 +
Done in 0.09s.
Done in 0.33s.

@github-actions
Copy link
Copy Markdown

workflow: benchmarks/size

Comparison of minified (terser) and compressed (brotli) size results, measured in bytes. Smaller is better.
yarn workspace v1.22.22
yarn run v1.22.22
$ node ./compare.js /tmp/tmp.5WGjEJpvh1 /tmp/tmp.XIWkWrR0HC

Results Base Patch Ratio
@stylexjs/stylex/lib/cjs/stylex.js
· compressed 1,535 1,535 1.00
· minified 5,166 5,166 1.00
@stylexjs/stylex/lib/cjs/inject.js
· compressed 1,793 1,793 1.00
· minified 4,915 4,915 1.00
benchmarks/size/.build/bundle.js
· compressed 496,650 496,650 1.00
· minified 4,847,840 4,847,840 1.00
benchmarks/size/.build/stylex.css
· compressed 99,757 99,757 1.00
· minified 748,850 748,850 1.00
Done in 0.09s.
Done in 0.33s.

@skovhus
Copy link
Copy Markdown
Contributor

skovhus commented May 13, 2026

@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.

@matclayton
Copy link
Copy Markdown
Contributor

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!

@nmn
Copy link
Copy Markdown
Collaborator Author

nmn commented May 16, 2026

@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!

@skovhus
Copy link
Copy Markdown
Contributor

skovhus commented May 17, 2026

@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.

@Dwlad90
Copy link
Copy Markdown
Contributor

Dwlad90 commented Jun 2, 2026

@nmn, this is great news!

@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.

I'd love to share my experience and the results of implementing StyleX in Rust.

To give some background, I initially implemented StyleX in Rust using an SWC plugin, but that introduced a lot of headaches:

The SWC plugin is tightly coupled to the locally installed version of @swc/core - it must be strictly compatible with the swc_core crate version used inside the plugin.

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 stylex.env). This could seriously bottleneck the plugin's capabilities and make it difficult to maintain feature parity with the Babel plugin.

Because of this, and following @SukkaW's recommendation, I decided to rewrite the plugin using NAPI-RS combined with SWC for JS parsing and AST transformation.

This instantly solved a lot of our problems. The biggest win was that NAPI-RS allows you to generate native JS bindings, meaning you can use it simply by swapping out the Babel plugin function call for the NAPI-RS compiler.

I'd be more than happy to help out and dive deeper into my findings if it would be valuable for the community!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants