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