From 21c89c9fa087b23897999a74a378851e3c50b25c Mon Sep 17 00:00:00 2001
From: chirokas <157580465+chirokas@users.noreply.github.com>
Date: Tue, 1 Sep 2026 23:47:29 +0800
Subject: [PATCH 1/2] [DOM] Clean up Fragment listeners on signal abort
(#37457)
Listeners registered with `options.signal` are supposed to be removed
when the signal is aborted.
Since `FragmentInstance` does not clean up its tracked listeners on
abort, previously removed listeners cannot be re-attached.
---
.../src/client/ReactFiberConfigDOM.js | 41 ++++++-
.../__tests__/ReactDOMFragmentRefs-test.js | 116 ++++++++++++++++++
2 files changed, 152 insertions(+), 5 deletions(-)
diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
index 4d23b0ea1e8..9fe632e5ee0 100644
--- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
@@ -2991,6 +2991,7 @@ type StoredEventListener = {
// When once:true, a wrapper that removes the fragment listener after the
// first fire. Otherwise the same as listener.
attachedListener: EventListener,
+ cleanup: null | (() => void),
};
export type FragmentInstanceType = {
@@ -3034,6 +3035,15 @@ FragmentInstance.prototype.addEventListener = function (
listener: EventListener,
optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
): void {
+ let signal: null | AbortSignal = null;
+ let cleanup: null | (() => void) = null;
+ if (optionsOrUseCapture != null && typeof optionsOrUseCapture !== 'boolean') {
+ signal = optionsOrUseCapture.signal || null;
+ if (signal !== null && signal.aborted) {
+ return;
+ }
+ }
+
if (this._eventListeners === null) {
this._eventListeners = [];
}
@@ -3063,12 +3073,24 @@ FragmentInstance.prototype.addEventListener = function (
}
};
}
+ if (signal !== null) {
+ const onAbort = fragmentInstance.removeEventListener.bind(
+ fragmentInstance,
+ type,
+ listener,
+ optionsOrUseCapture,
+ );
+ signal.addEventListener('abort', onAbort, {once: true});
+ // $FlowFixMe[method-unbinding]
+ cleanup = signal.removeEventListener.bind(signal, 'abort', onAbort);
+ }
const attachOptions = getAttachOptions(optionsOrUseCapture);
listeners.push({
type,
listener,
optionsOrUseCapture,
attachedListener,
+ cleanup,
});
traverseFragmentInstancesAndTextInstances(
this._fragmentFiber,
@@ -3110,8 +3132,11 @@ FragmentInstance.prototype.removeEventListener = function (
if (index === -1) {
return;
}
- const {attachedListener, optionsOrUseCapture: storedOptions} =
- listeners[index];
+ const {
+ attachedListener,
+ optionsOrUseCapture: storedOptions,
+ cleanup,
+ } = listeners[index];
const attachOptions = getAttachOptions(storedOptions);
traverseFragmentInstancesAndTextInstances(
this._fragmentFiber,
@@ -3121,6 +3146,9 @@ FragmentInstance.prototype.removeEventListener = function (
attachOptions,
);
listeners.splice(index, 1);
+ if (cleanup !== null) {
+ cleanup();
+ }
};
function removeEventListenerFromChild(
child: Fiber,
@@ -3138,14 +3166,17 @@ function isOnceOption(opts: ?EventListenerOptionsOrUseCapture): boolean {
function getAttachOptions(
opts: void | EventListenerOptionsOrUseCapture,
): void | EventListenerOptionsOrUseCapture {
- // Strip once when attaching to host children; Fragment owns once semantics.
- if (opts == null || typeof opts === 'boolean' || opts.once !== true) {
+ // Strip once and signal when attaching to host children; Fragment owns once and signal semantics.
+ if (
+ opts == null ||
+ typeof opts === 'boolean' ||
+ (opts.once !== true && !(opts.signal instanceof AbortSignal))
+ ) {
return opts;
}
return {
capture: opts.capture,
passive: opts.passive,
- signal: opts.signal,
};
}
function normalizeListenerOptions(
diff --git a/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js b/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js
index 1ea1974f9bc..ead31622eee 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js
@@ -1098,6 +1098,122 @@ describe('FragmentRefs', () => {
expect(logs).toEqual([]);
});
+ // @gate enableFragmentRefs
+ it('should remove the listener when the signal is aborted before registration', async () => {
+ const fragmentRef = React.createRef();
+ const childRef = React.createRef();
+ const root = ReactDOMClient.createRoot(container);
+ let logs = [];
+
+ function registeredListener() {
+ logs.push('registered');
+ }
+
+ await act(() => {
+ root.render(
+
+ child
+ ,
+ );
+ });
+
+ const signal = AbortSignal.abort();
+ fragmentRef.current.addEventListener('click', registeredListener, {
+ signal,
+ });
+ childRef.current.click();
+ expect(logs).toEqual([]);
+
+ // The event listener can be re-added
+ fragmentRef.current.addEventListener('click', registeredListener);
+ logs = [];
+ childRef.current.click();
+ expect(logs).toEqual(['registered']);
+ });
+
+ // @gate enableFragmentRefs
+ it('should remove the listener when the signal is aborted after registration', async () => {
+ const fragmentRef = React.createRef();
+ const childRef = React.createRef();
+ const root = ReactDOMClient.createRoot(container);
+ let logs = [];
+
+ function registeredListener() {
+ logs.push('registered');
+ }
+
+ await act(() => {
+ root.render(
+
+ child
+ ,
+ );
+ });
+
+ const controller = new AbortController();
+ fragmentRef.current.addEventListener('click', registeredListener, {
+ signal: controller.signal,
+ });
+ childRef.current.click();
+ expect(logs).toEqual(['registered']);
+
+ logs = [];
+ controller.abort();
+ childRef.current.click();
+ expect(logs).toEqual([]);
+
+ // The event listener can be re-added
+ fragmentRef.current.addEventListener('click', registeredListener);
+ logs = [];
+ childRef.current.click();
+ expect(logs).toEqual(['registered']);
+ });
+
+ // @gate enableFragmentRefs
+ it('should NOT remove the new subscription when the signal for the old subscription is aborted', async () => {
+ const fragmentRef = React.createRef();
+ const childRef = React.createRef();
+ const root = ReactDOMClient.createRoot(container);
+ let logs = [];
+
+ function registeredListener() {
+ logs.push('registered');
+ }
+
+ await act(() => {
+ root.render(
+
+ child
+ ,
+ );
+ });
+
+ const controller = new AbortController();
+ fragmentRef.current.addEventListener('click', registeredListener, {
+ signal: controller.signal,
+ });
+ childRef.current.click();
+ expect(logs).toEqual(['registered']);
+
+ fragmentRef.current.removeEventListener('click', registeredListener);
+ logs = [];
+ childRef.current.click();
+ expect(logs).toEqual([]);
+
+ // Added without a signal
+ fragmentRef.current.addEventListener('click', registeredListener);
+ logs = [];
+ childRef.current.click();
+ // Listener is called
+ expect(logs).toEqual(['registered']);
+
+ logs = [];
+ controller.abort();
+ childRef.current.click();
+ // Listener is called
+ expect(logs).toEqual(['registered']);
+ });
+
// @gate enableFragmentRefs && enableFragmentRefsTextNodes
it('adds an event listener to a newly added text child', async () => {
const fragmentRef = React.createRef();
From fba23062c47c205839c1cff054ae8a5c380eec43 Mon Sep 17 00:00:00 2001
From: Will Binns-Smith
Date: Tue, 1 Sep 2026 10:04:34 -0700
Subject: [PATCH 2/2] Add Rust compiler crate publishing workflows (#37465)
## Summary
- configure the React Compiler Rust crates as a Cargo workspace with
shared package metadata and versioned internal dependencies
- add workflows to open a signed version-bump pull request and publish
the workspace through crates.io trusted publishing
- add release scripts and contributor documentation for versioning,
validation, and publishing
## Test plan
- Run `cargo check --locked --workspace` from `compiler/`.
- Run **(Compiler) Publish Rust Crates** from the Actions tab with **Dry
run** enabled; confirm every workspace crate packages successfully
without publishing.
- Run **(Compiler) Update Rust Crate Version** with a test version;
confirm it verifies one shared version and opens a signed version-bump
pull request containing the updated workspace manifest and lockfile.
---
.github/workflows/compiler_rust_publish.yml | 48 ++++++++++++++
.github/workflows/compiler_rust_version.yml | 50 +++++++++++++++
compiler/Cargo.lock | 24 +++----
compiler/Cargo.toml | 20 ++++++
compiler/README.md | 4 +-
compiler/crates/react_compiler/Cargo.toml | 28 +++++----
compiler/crates/react_compiler_ast/Cargo.toml | 10 ++-
.../react_compiler_diagnostics/Cargo.toml | 8 ++-
compiler/crates/react_compiler_hir/Cargo.toml | 10 ++-
.../react_compiler_inference/Cargo.toml | 20 +++---
.../crates/react_compiler_lowering/Cargo.toml | 14 +++--
.../react_compiler_optimization/Cargo.toml | 16 +++--
.../react_compiler_reactive_scopes/Cargo.toml | 14 +++--
compiler/crates/react_compiler_ssa/Cargo.toml | 12 ++--
.../react_compiler_typeinference/Cargo.toml | 14 +++--
.../crates/react_compiler_utils/Cargo.toml | 8 ++-
.../react_compiler_validation/Cargo.toml | 12 ++--
compiler/docs/RUST_CRATE_RELEASES.md | 19 ++++++
.../native/Cargo.toml | 1 +
compiler/scripts/open-rust-version-pr.sh | 63 +++++++++++++++++++
compiler/scripts/publish-rust-crates.sh | 60 ++++++++++++++++++
compiler/scripts/update-rust-crate-version.js | 49 +++++++++++++++
22 files changed, 432 insertions(+), 72 deletions(-)
create mode 100644 .github/workflows/compiler_rust_publish.yml
create mode 100644 .github/workflows/compiler_rust_version.yml
create mode 100644 compiler/docs/RUST_CRATE_RELEASES.md
create mode 100755 compiler/scripts/open-rust-version-pr.sh
create mode 100755 compiler/scripts/publish-rust-crates.sh
create mode 100755 compiler/scripts/update-rust-crate-version.js
diff --git a/.github/workflows/compiler_rust_publish.yml b/.github/workflows/compiler_rust_publish.yml
new file mode 100644
index 00000000000..057f425e717
--- /dev/null
+++ b/.github/workflows/compiler_rust_publish.yml
@@ -0,0 +1,48 @@
+name: (Compiler) Publish Rust Crates
+
+on:
+ workflow_dispatch:
+ inputs:
+ dry_run:
+ description: Dry run (do not actually publish crates)
+ type: boolean
+ required: true
+ default: false
+
+permissions: {}
+
+concurrency:
+ group: compiler-rust-publish
+ cancel-in-progress: false
+
+defaults:
+ run:
+ working-directory: compiler
+
+jobs:
+ publish:
+ name: Publish crates
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ id-token: write
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.sha }}
+ - uses: dtolnay/rust-toolchain@stable
+ - name: Check workspace
+ run: cargo check --locked --workspace
+ - name: Authenticate with crates.io
+ if: ${{ !inputs.dry_run }}
+ id: crates_io_auth
+ uses: rust-lang/crates-io-auth-action@v1
+ - name: Publish crates
+ env:
+ CARGO_REGISTRY_TOKEN: ${{ steps.crates_io_auth.outputs.token }}
+ run: |
+ if [ "${{ inputs.dry_run }}" = "true" ]; then
+ scripts/publish-rust-crates.sh --dry-run
+ else
+ scripts/publish-rust-crates.sh
+ fi
diff --git a/.github/workflows/compiler_rust_version.yml b/.github/workflows/compiler_rust_version.yml
new file mode 100644
index 00000000000..b28f2424848
--- /dev/null
+++ b/.github/workflows/compiler_rust_version.yml
@@ -0,0 +1,50 @@
+name: (Compiler) Update Rust Crate Version
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: New version for all Rust compiler crates
+ required: true
+ type: string
+
+permissions: {}
+
+concurrency:
+ group: compiler-rust-version
+ cancel-in-progress: false
+
+jobs:
+ update_version:
+ name: Open version bump PR
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ VERSION: ${{ inputs.version }}
+ BASE_SHA: ${{ github.sha }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.sha }}
+ - uses: dtolnay/rust-toolchain@stable
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ - name: Update crate versions
+ working-directory: compiler
+ run: |
+ scripts/update-rust-crate-version.js "$VERSION"
+ cargo check --workspace
+ - name: Verify crate versions
+ working-directory: compiler
+ run: |
+ versions=$(cargo metadata --no-deps --format-version 1 | jq -r '[.packages[] | select(.name | startswith("react_compiler")) | select(.name != "react_compiler_napi") | .version] | unique | .[]')
+ if [ "$versions" != "$VERSION" ]; then
+ echo "Expected all publishable crates to use $VERSION; found: $versions" >&2
+ exit 1
+ fi
+ - name: Open pull request
+ run: compiler/scripts/open-rust-version-pr.sh
\ No newline at end of file
diff --git a/compiler/Cargo.lock b/compiler/Cargo.lock
index 383e5916f8c..fa258c4a666 100644
--- a/compiler/Cargo.lock
+++ b/compiler/Cargo.lock
@@ -274,7 +274,7 @@ dependencies = [
[[package]]
name = "react_compiler"
-version = "0.1.0"
+version = "0.0.1-oidc.0"
dependencies = [
"indexmap",
"react_compiler_ast",
@@ -294,7 +294,7 @@ dependencies = [
[[package]]
name = "react_compiler_ast"
-version = "0.1.0"
+version = "0.0.1-oidc.0"
dependencies = [
"indexmap",
"react_compiler_diagnostics",
@@ -308,7 +308,7 @@ dependencies = [
[[package]]
name = "react_compiler_diagnostics"
-version = "0.1.0"
+version = "0.0.1-oidc.0"
dependencies = [
"rustc-hash",
"serde",
@@ -316,7 +316,7 @@ dependencies = [
[[package]]
name = "react_compiler_hir"
-version = "0.1.0"
+version = "0.0.1-oidc.0"
dependencies = [
"indexmap",
"react_compiler_diagnostics",
@@ -327,7 +327,7 @@ dependencies = [
[[package]]
name = "react_compiler_inference"
-version = "0.1.0"
+version = "0.0.1-oidc.0"
dependencies = [
"indexmap",
"react_compiler_diagnostics",
@@ -341,7 +341,7 @@ dependencies = [
[[package]]
name = "react_compiler_lowering"
-version = "0.1.0"
+version = "0.0.1-oidc.0"
dependencies = [
"indexmap",
"react_compiler_ast",
@@ -366,7 +366,7 @@ dependencies = [
[[package]]
name = "react_compiler_optimization"
-version = "0.1.0"
+version = "0.0.1-oidc.0"
dependencies = [
"indexmap",
"react_compiler_diagnostics",
@@ -378,7 +378,7 @@ dependencies = [
[[package]]
name = "react_compiler_reactive_scopes"
-version = "0.1.0"
+version = "0.0.1-oidc.0"
dependencies = [
"hmac-sha256",
"indexmap",
@@ -391,7 +391,7 @@ dependencies = [
[[package]]
name = "react_compiler_ssa"
-version = "0.1.0"
+version = "0.0.1-oidc.0"
dependencies = [
"indexmap",
"react_compiler_diagnostics",
@@ -401,7 +401,7 @@ dependencies = [
[[package]]
name = "react_compiler_typeinference"
-version = "0.1.0"
+version = "0.0.1-oidc.0"
dependencies = [
"react_compiler_diagnostics",
"react_compiler_hir",
@@ -411,7 +411,7 @@ dependencies = [
[[package]]
name = "react_compiler_utils"
-version = "0.1.0"
+version = "0.0.1-oidc.0"
dependencies = [
"indexmap",
"rustc-hash",
@@ -419,7 +419,7 @@ dependencies = [
[[package]]
name = "react_compiler_validation"
-version = "0.1.0"
+version = "0.0.1-oidc.0"
dependencies = [
"indexmap",
"react_compiler_diagnostics",
diff --git a/compiler/Cargo.toml b/compiler/Cargo.toml
index 9bd5cf8837c..a207b9f046d 100644
--- a/compiler/Cargo.toml
+++ b/compiler/Cargo.toml
@@ -5,6 +5,26 @@ members = [
]
resolver = "3"
+[workspace.package]
+version = "0.0.1-oidc.0"
+edition = "2024"
+rust-version = "1.85"
+license = "MIT"
+repository = "https://github.com/facebook/react"
+
+[workspace.dependencies]
+react_compiler_ast = { version = "0.0.1-oidc.0", path = "crates/react_compiler_ast" }
+react_compiler_diagnostics = { version = "0.0.1-oidc.0", path = "crates/react_compiler_diagnostics" }
+react_compiler_hir = { version = "0.0.1-oidc.0", path = "crates/react_compiler_hir" }
+react_compiler_inference = { version = "0.0.1-oidc.0", path = "crates/react_compiler_inference" }
+react_compiler_lowering = { version = "0.0.1-oidc.0", path = "crates/react_compiler_lowering" }
+react_compiler_optimization = { version = "0.0.1-oidc.0", path = "crates/react_compiler_optimization" }
+react_compiler_reactive_scopes = { version = "0.0.1-oidc.0", path = "crates/react_compiler_reactive_scopes" }
+react_compiler_ssa = { version = "0.0.1-oidc.0", path = "crates/react_compiler_ssa" }
+react_compiler_typeinference = { version = "0.0.1-oidc.0", path = "crates/react_compiler_typeinference" }
+react_compiler_utils = { version = "0.0.1-oidc.0", path = "crates/react_compiler_utils" }
+react_compiler_validation = { version = "0.0.1-oidc.0", path = "crates/react_compiler_validation" }
+
# Sizes the shipped napi binary (index.node). Measured on arm64 macOS:
# default release is 11.2MB; fat LTO + one codegen unit + stripping symbols
# lands at 7.2MB with no runtime cost. Release builds get slower; debug
diff --git a/compiler/README.md b/compiler/README.md
index 53c0c22a0dc..3641daf4349 100644
--- a/compiler/README.md
+++ b/compiler/README.md
@@ -4,4 +4,6 @@ React Compiler is a compiler that optimizes React applications, ensuring that on
More information about the design and architecture of the compiler are covered in the [Design Goals](./docs/DESIGN_GOALS.md).
-More information about developing the compiler itself is covered in the [Development Guide](./docs/DEVELOPMENT_GUIDE.md).
\ No newline at end of file
+More information about developing the compiler itself is covered in the [Development Guide](./docs/DEVELOPMENT_GUIDE.md).
+
+Instructions for publishing the Rust compiler crates are covered in [Publishing Rust Compiler Crates](./docs/RUST_CRATE_RELEASES.md).
diff --git a/compiler/crates/react_compiler/Cargo.toml b/compiler/crates/react_compiler/Cargo.toml
index b1cec9db841..cbe2570696f 100644
--- a/compiler/crates/react_compiler/Cargo.toml
+++ b/compiler/crates/react_compiler/Cargo.toml
@@ -1,19 +1,23 @@
[package]
name = "react_compiler"
-version = "0.1.0"
-edition = "2024"
+description = "Rust implementation of the React Compiler"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
[dependencies]
-react_compiler_ast = { path = "../react_compiler_ast" }
-react_compiler_diagnostics = { path = "../react_compiler_diagnostics" }
-react_compiler_hir = { path = "../react_compiler_hir" }
-react_compiler_inference = { path = "../react_compiler_inference" }
-react_compiler_lowering = { path = "../react_compiler_lowering" }
-react_compiler_optimization = { path = "../react_compiler_optimization" }
-react_compiler_reactive_scopes = { path = "../react_compiler_reactive_scopes" }
-react_compiler_ssa = { path = "../react_compiler_ssa" }
-react_compiler_typeinference = { path = "../react_compiler_typeinference" }
-react_compiler_validation = { path = "../react_compiler_validation" }
+react_compiler_ast.workspace = true
+react_compiler_diagnostics.workspace = true
+react_compiler_hir.workspace = true
+react_compiler_inference.workspace = true
+react_compiler_lowering.workspace = true
+react_compiler_optimization.workspace = true
+react_compiler_reactive_scopes.workspace = true
+react_compiler_ssa.workspace = true
+react_compiler_typeinference.workspace = true
+react_compiler_validation.workspace = true
indexmap = "2"
rustc-hash = "2"
serde = { version = "1", features = ["derive"] }
diff --git a/compiler/crates/react_compiler_ast/Cargo.toml b/compiler/crates/react_compiler_ast/Cargo.toml
index 4a3e387f06e..096028a4cb2 100644
--- a/compiler/crates/react_compiler_ast/Cargo.toml
+++ b/compiler/crates/react_compiler_ast/Cargo.toml
@@ -1,10 +1,14 @@
[package]
name = "react_compiler_ast"
-version = "0.1.0"
-edition = "2024"
+description = "JavaScript AST types and transforms for the React Compiler"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
[dependencies]
-react_compiler_diagnostics = { path = "../react_compiler_diagnostics" }
+react_compiler_diagnostics.workspace = true
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["raw_value", "unbounded_depth"] }
serde-transcode = "1"
diff --git a/compiler/crates/react_compiler_diagnostics/Cargo.toml b/compiler/crates/react_compiler_diagnostics/Cargo.toml
index bc9a84721fc..d5dcc3639de 100644
--- a/compiler/crates/react_compiler_diagnostics/Cargo.toml
+++ b/compiler/crates/react_compiler_diagnostics/Cargo.toml
@@ -1,7 +1,11 @@
[package]
name = "react_compiler_diagnostics"
-version = "0.1.0"
-edition = "2024"
+description = "Diagnostics infrastructure for the React Compiler"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
[dependencies]
rustc-hash = "2"
diff --git a/compiler/crates/react_compiler_hir/Cargo.toml b/compiler/crates/react_compiler_hir/Cargo.toml
index 8273eaa778b..9cab196f9b8 100644
--- a/compiler/crates/react_compiler_hir/Cargo.toml
+++ b/compiler/crates/react_compiler_hir/Cargo.toml
@@ -1,10 +1,14 @@
[package]
name = "react_compiler_hir"
-version = "0.1.0"
-edition = "2024"
+description = "High-level intermediate representation for the React Compiler"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
[dependencies]
-react_compiler_diagnostics = { path = "../react_compiler_diagnostics" }
+react_compiler_diagnostics.workspace = true
indexmap = { version = "2", features = ["serde"] }
rustc-hash = "2"
serde = { version = "1", features = ["derive"] }
diff --git a/compiler/crates/react_compiler_inference/Cargo.toml b/compiler/crates/react_compiler_inference/Cargo.toml
index 4fb7504e674..7d833394916 100644
--- a/compiler/crates/react_compiler_inference/Cargo.toml
+++ b/compiler/crates/react_compiler_inference/Cargo.toml
@@ -1,14 +1,18 @@
[package]
name = "react_compiler_inference"
-version = "0.1.0"
-edition = "2024"
+description = "Inference passes for the React Compiler"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
[dependencies]
-react_compiler_hir = { path = "../react_compiler_hir" }
-react_compiler_diagnostics = { path = "../react_compiler_diagnostics" }
-react_compiler_lowering = { path = "../react_compiler_lowering" }
-react_compiler_optimization = { path = "../react_compiler_optimization" }
-react_compiler_ssa = { path = "../react_compiler_ssa" }
-react_compiler_utils = { path = "../react_compiler_utils" }
+react_compiler_hir.workspace = true
+react_compiler_diagnostics.workspace = true
+react_compiler_lowering.workspace = true
+react_compiler_optimization.workspace = true
+react_compiler_ssa.workspace = true
+react_compiler_utils.workspace = true
indexmap = "2"
rustc-hash = "2"
diff --git a/compiler/crates/react_compiler_lowering/Cargo.toml b/compiler/crates/react_compiler_lowering/Cargo.toml
index ac84073e4f9..ae3d0b0e859 100644
--- a/compiler/crates/react_compiler_lowering/Cargo.toml
+++ b/compiler/crates/react_compiler_lowering/Cargo.toml
@@ -1,12 +1,16 @@
[package]
name = "react_compiler_lowering"
-version = "0.1.0"
-edition = "2024"
+description = "AST to HIR lowering for the React Compiler"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
[dependencies]
-react_compiler_ast = { path = "../react_compiler_ast" }
-react_compiler_hir = { path = "../react_compiler_hir" }
-react_compiler_diagnostics = { path = "../react_compiler_diagnostics" }
+react_compiler_ast.workspace = true
+react_compiler_hir.workspace = true
+react_compiler_diagnostics.workspace = true
indexmap = "2"
rustc-hash = "2"
serde_json = "1"
diff --git a/compiler/crates/react_compiler_optimization/Cargo.toml b/compiler/crates/react_compiler_optimization/Cargo.toml
index f7801fc1522..fd7510b9734 100644
--- a/compiler/crates/react_compiler_optimization/Cargo.toml
+++ b/compiler/crates/react_compiler_optimization/Cargo.toml
@@ -1,12 +1,16 @@
[package]
name = "react_compiler_optimization"
-version = "0.1.0"
-edition = "2024"
+description = "Optimization passes for the React Compiler"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
[dependencies]
-react_compiler_diagnostics = { path = "../react_compiler_diagnostics" }
-react_compiler_hir = { path = "../react_compiler_hir" }
-react_compiler_lowering = { path = "../react_compiler_lowering" }
-react_compiler_ssa = { path = "../react_compiler_ssa" }
+react_compiler_diagnostics.workspace = true
+react_compiler_hir.workspace = true
+react_compiler_lowering.workspace = true
+react_compiler_ssa.workspace = true
indexmap = "2"
rustc-hash = "2"
diff --git a/compiler/crates/react_compiler_reactive_scopes/Cargo.toml b/compiler/crates/react_compiler_reactive_scopes/Cargo.toml
index 4718cabb076..9cf89401898 100644
--- a/compiler/crates/react_compiler_reactive_scopes/Cargo.toml
+++ b/compiler/crates/react_compiler_reactive_scopes/Cargo.toml
@@ -1,12 +1,16 @@
[package]
name = "react_compiler_reactive_scopes"
-version = "0.1.0"
-edition = "2024"
+description = "Reactive scope inference for the React Compiler"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
[dependencies]
-react_compiler_ast = { path = "../react_compiler_ast" }
-react_compiler_diagnostics = { path = "../react_compiler_diagnostics" }
-react_compiler_hir = { path = "../react_compiler_hir" }
+react_compiler_ast.workspace = true
+react_compiler_diagnostics.workspace = true
+react_compiler_hir.workspace = true
indexmap = "2"
rustc-hash = "2"
serde_json = "1"
diff --git a/compiler/crates/react_compiler_ssa/Cargo.toml b/compiler/crates/react_compiler_ssa/Cargo.toml
index 9effe781181..d6f1bdd5dda 100644
--- a/compiler/crates/react_compiler_ssa/Cargo.toml
+++ b/compiler/crates/react_compiler_ssa/Cargo.toml
@@ -1,10 +1,14 @@
[package]
name = "react_compiler_ssa"
-version = "0.1.0"
-edition = "2024"
+description = "SSA transformations for the React Compiler"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
[dependencies]
-react_compiler_diagnostics = { path = "../react_compiler_diagnostics" }
-react_compiler_hir = { path = "../react_compiler_hir" }
+react_compiler_diagnostics.workspace = true
+react_compiler_hir.workspace = true
indexmap = "2"
rustc-hash = "2"
diff --git a/compiler/crates/react_compiler_typeinference/Cargo.toml b/compiler/crates/react_compiler_typeinference/Cargo.toml
index 93e905fb722..6cc1c13ad6a 100644
--- a/compiler/crates/react_compiler_typeinference/Cargo.toml
+++ b/compiler/crates/react_compiler_typeinference/Cargo.toml
@@ -1,10 +1,14 @@
[package]
name = "react_compiler_typeinference"
-version = "0.1.0"
-edition = "2024"
+description = "Type inference passes for the React Compiler"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
[dependencies]
rustc-hash = "2"
-react_compiler_diagnostics = { path = "../react_compiler_diagnostics" }
-react_compiler_hir = { path = "../react_compiler_hir" }
-react_compiler_ssa = { path = "../react_compiler_ssa" }
+react_compiler_diagnostics.workspace = true
+react_compiler_hir.workspace = true
+react_compiler_ssa.workspace = true
diff --git a/compiler/crates/react_compiler_utils/Cargo.toml b/compiler/crates/react_compiler_utils/Cargo.toml
index ee1eba46110..6d7b975985b 100644
--- a/compiler/crates/react_compiler_utils/Cargo.toml
+++ b/compiler/crates/react_compiler_utils/Cargo.toml
@@ -1,7 +1,11 @@
[package]
name = "react_compiler_utils"
-version = "0.1.0"
-edition = "2024"
+description = "Shared utilities for the React Compiler"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
[dependencies]
indexmap = "2"
diff --git a/compiler/crates/react_compiler_validation/Cargo.toml b/compiler/crates/react_compiler_validation/Cargo.toml
index 5274f4cf703..e165859136b 100644
--- a/compiler/crates/react_compiler_validation/Cargo.toml
+++ b/compiler/crates/react_compiler_validation/Cargo.toml
@@ -1,10 +1,14 @@
[package]
name = "react_compiler_validation"
-version = "0.1.0"
-edition = "2024"
+description = "Validation passes for the React Compiler"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
[dependencies]
indexmap = "2"
rustc-hash = "2"
-react_compiler_diagnostics = { path = "../react_compiler_diagnostics" }
-react_compiler_hir = { path = "../react_compiler_hir" }
+react_compiler_diagnostics.workspace = true
+react_compiler_hir.workspace = true
diff --git a/compiler/docs/RUST_CRATE_RELEASES.md b/compiler/docs/RUST_CRATE_RELEASES.md
new file mode 100644
index 00000000000..1ca11351755
--- /dev/null
+++ b/compiler/docs/RUST_CRATE_RELEASES.md
@@ -0,0 +1,19 @@
+# Publishing Rust Compiler Crates
+
+## Bump the version
+
+1. Open the repository's **Actions** tab.
+2. Select **(Compiler) Update Rust Crate Version**.
+3. Click **Run workflow**, enter the new version, and run it from `main`.
+4. Review and merge the pull request created by the workflow.
+
+All Rust compiler crates use the same version and are published together.
+
+## Publish
+
+1. After the version pull request is merged, open the **Actions** tab.
+2. Select **(Compiler) Publish Rust Crates**.
+3. Click **Run workflow**, select `main`, and leave **Validate packages without publishing** unchecked.
+4. Confirm that the workflow succeeds and that the new versions appear on [crates.io](https://crates.io/crates/react_compiler/versions).
+
+Use the validation checkbox to check packaging without publishing. Published crate versions cannot be deleted.
diff --git a/compiler/packages/babel-plugin-react-compiler-rust/native/Cargo.toml b/compiler/packages/babel-plugin-react-compiler-rust/native/Cargo.toml
index 568787480ad..70bf85f6aa4 100644
--- a/compiler/packages/babel-plugin-react-compiler-rust/native/Cargo.toml
+++ b/compiler/packages/babel-plugin-react-compiler-rust/native/Cargo.toml
@@ -2,6 +2,7 @@
name = "react_compiler_napi"
version = "0.1.0"
edition = "2024"
+publish = false
[lib]
crate-type = ["cdylib"]
diff --git a/compiler/scripts/open-rust-version-pr.sh b/compiler/scripts/open-rust-version-pr.sh
new file mode 100755
index 00000000000..58f3dc7cfcf
--- /dev/null
+++ b/compiler/scripts/open-rust-version-pr.sh
@@ -0,0 +1,63 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+: "${GH_TOKEN:?GH_TOKEN is required}"
+: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
+: "${BASE_SHA:?BASE_SHA is required}"
+: "${VERSION:?VERSION is required}"
+
+cd "$(dirname "$0")/../.."
+
+branch="automated/rust-crates-$VERSION"
+gh api "repos/$GITHUB_REPOSITORY/git/refs" \
+ --method POST \
+ --field ref="refs/heads/$branch" \
+ --field sha="$BASE_SHA"
+
+additions=$(jq -n \
+ --arg manifest "$(base64 < compiler/Cargo.toml | tr -d '\n')" \
+ --arg lockfile "$(base64 < compiler/Cargo.lock | tr -d '\n')" \
+ '[
+ {path: "compiler/Cargo.toml", contents: $manifest},
+ {path: "compiler/Cargo.lock", contents: $lockfile}
+ ]')
+graphql_input=$(jq -n \
+ --arg query 'mutation($input: CreateCommitOnBranchInput!) {
+ createCommitOnBranch(input: $input) {
+ commit { oid }
+ }
+ }' \
+ --arg repository "$GITHUB_REPOSITORY" \
+ --arg branch "$branch" \
+ --arg headline "Update Rust compiler crates to $VERSION" \
+ --arg expectedHeadOid "$BASE_SHA" \
+ --argjson additions "$additions" \
+ '{
+ query: $query,
+ variables: {
+ input: {
+ branch: {
+ repositoryNameWithOwner: $repository,
+ branchName: $branch
+ },
+ message: {headline: $headline},
+ fileChanges: {additions: $additions},
+ expectedHeadOid: $expectedHeadOid
+ }
+ }
+ }')
+commit=$(printf '%s' "$graphql_input" | gh api graphql \
+ --input - \
+ --jq '.data.createCommitOnBranch.commit.oid')
+
+if [ "$(gh api "repos/$GITHUB_REPOSITORY/commits/$commit" --jq '.commit.verification.verified')" != "true" ]; then
+ echo "GitHub did not sign commit $commit" >&2
+ exit 1
+fi
+
+gh pr create \
+ --base main \
+ --head "$branch" \
+ --title "Update Rust compiler crates to $VERSION" \
+ --body "Update all Rust compiler crates and their internal dependency requirements to \`$VERSION\`."
\ No newline at end of file
diff --git a/compiler/scripts/publish-rust-crates.sh b/compiler/scripts/publish-rust-crates.sh
new file mode 100755
index 00000000000..ff7e16435cb
--- /dev/null
+++ b/compiler/scripts/publish-rust-crates.sh
@@ -0,0 +1,60 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+cd "$(dirname "$0")/.."
+
+dry_run=false
+if [ "${1:-}" = "--dry-run" ]; then
+ dry_run=true
+elif [ "$#" -ne 0 ]; then
+ echo "Usage: $0 [--dry-run]" >&2
+ exit 2
+fi
+
+publish_with_retry() {
+ local output
+ local published_crate
+
+ while true; do
+ output=$(mktemp)
+ if [ "${#excluded_crates[@]}" -eq 0 ]; then
+ publish_command=(cargo publish --locked --workspace)
+ else
+ publish_command=(cargo publish --locked --workspace "${excluded_crates[@]}")
+ fi
+ if "${publish_command[@]}" 2> >(tee "$output" >&2); then
+ rm -f "$output"
+ return
+ fi
+
+ if grep -q "status 429 Too Many Requests" "$output"; then
+ rm -f "$output"
+ echo "Rate limited; retrying workspace publish in 10 minutes"
+ sleep 600
+ continue
+ fi
+
+ published_crate=$(sed -n 's/.*error: crate \([^@]*\)@.* already exists on crates.io index/\1/p' "$output" | tail -1)
+ if [ -n "$published_crate" ]; then
+ rm -f "$output"
+ excluded_crates+=("--exclude" "$published_crate")
+ continue
+ fi
+
+ rm -f "$output"
+ return 1
+ done
+}
+
+cargo check --locked --workspace
+
+excluded_crates=()
+
+if [ "$dry_run" = "true" ]; then
+ cargo publish --locked --workspace --dry-run
+else
+ publish_with_retry
+fi
+
+echo "Published all React Compiler crates. Revoke the bootstrap token after configuring trusted publishers."
diff --git a/compiler/scripts/update-rust-crate-version.js b/compiler/scripts/update-rust-crate-version.js
new file mode 100755
index 00000000000..e712e8c8eaf
--- /dev/null
+++ b/compiler/scripts/update-rust-crate-version.js
@@ -0,0 +1,49 @@
+#!/usr/bin/env node
+
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+
+const version = process.argv[2];
+if (
+ version == null ||
+ !/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(
+ version
+ )
+) {
+ console.error('Usage: update-rust-crate-version.js ');
+ process.exit(1);
+}
+
+const manifestPath = path.join(__dirname, '..', 'Cargo.toml');
+const manifest = fs.readFileSync(manifestPath, 'utf8');
+const packageVersionMatch = manifest.match(
+ /\[workspace\.package\]\nversion = "([^"]+)"/
+);
+
+if (packageVersionMatch == null) {
+ throw new Error('Could not find the workspace package version');
+}
+
+const currentVersion = packageVersionMatch[1];
+const internalDependencyPattern = new RegExp(
+ `(react_compiler(?:_[a-z_]+)? = \\{ version = ")${currentVersion.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}("[^\\n]+\\})`,
+ 'g'
+);
+const internalDependencies = manifest.match(internalDependencyPattern) ?? [];
+
+if (internalDependencies.length !== 11) {
+ throw new Error(
+ `Expected 11 versioned internal dependencies, found ${internalDependencies.length}`
+ );
+}
+
+const updatedManifest = manifest
+ .replace(
+ `[workspace.package]\nversion = "${currentVersion}"`,
+ `[workspace.package]\nversion = "${version}"`
+ )
+ .replace(internalDependencyPattern, `$1${version}$2`);
+
+fs.writeFileSync(manifestPath, updatedManifest);