From ec8cb30ce95eb1d0762b9f8629feff57ae88d890 Mon Sep 17 00:00:00 2001 From: "Mentenaz-Server (Francois)" Date: Wed, 7 Oct 2026 14:42:04 +0200 Subject: [PATCH 1/3] fix(zed): use the bare version when locating the rcl-lsp release asset `zed::latest_github_release` returns the release tag as `version`. This repository tags releases per package, e.g. `react-component-lens(packages/vscode/package.json)@1.0.5`, so the extension asked for rcl-lsp-react-component-lens(packages/vscode/package.json)@1.0.5-x86_64-pc-windows-msvc.zip while the asset attached to that release is named rcl-lsp-1.0.5-x86_64-pc-windows-msvc.zip The language server therefore never downloaded, on any platform, and Zed reported "no release asset found matching ...". Take the part of the tag after the last `@` and use it for both the asset name and the install directory. The directory matters too: the full tag contains `/` and parentheses. Co-Authored-By: Claude Opus 5.5 --- packages/zed/src/lib.rs | 44 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/packages/zed/src/lib.rs b/packages/zed/src/lib.rs index ca2cca7..422eba9 100644 --- a/packages/zed/src/lib.rs +++ b/packages/zed/src/lib.rs @@ -15,6 +15,18 @@ use zed_extension_api::{self as zed, Result}; const BINARY_NAME: &str = "rcl-lsp"; const REPOSITORY: &str = "dev-five-git/react-component-lens"; +/// The version number inside a release tag. +/// +/// This repository's releases are tagged by package, e.g. +/// `react-component-lens(packages/vscode/package.json)@1.0.5`, while the +/// `rcl-lsp` archives attached to them are named with the bare version +/// (`rcl-lsp-1.0.5-x86_64-pc-windows-msvc.zip`). A tag without an `@` is +/// returned as it is, minus a leading `v`. +fn release_version(tag: &str) -> &str { + let version = tag.rsplit('@').next().unwrap_or(tag); + version.strip_prefix('v').unwrap_or(version) +} + struct ReactComponentLensExtension { cached_binary_path: Option, } @@ -69,10 +81,9 @@ impl ReactComponentLensExtension { zed::Os::Windows => "zip", }; - let asset_name = format!( - "{BINARY_NAME}-{version}-{arch_str}-{os_str}.{archive_ext}", - version = release.version, - ); + // `release.version` is the whole tag, not a version number. + let version = release_version(&release.version); + let asset_name = format!("{BINARY_NAME}-{version}-{arch_str}-{os_str}.{archive_ext}"); let asset = release .assets @@ -80,7 +91,7 @@ impl ReactComponentLensExtension { .find(|asset| asset.name == asset_name) .ok_or_else(|| format!("no release asset found matching {asset_name:?}"))?; - let install_dir = format!("{BINARY_NAME}-{}", release.version); + let install_dir = format!("{BINARY_NAME}-{version}"); let binary_suffix = if matches!(platform, zed::Os::Windows) { ".exe" } else { @@ -141,3 +152,26 @@ impl zed::Extension for ReactComponentLensExtension { } zed::register_extension!(ReactComponentLensExtension); + +#[cfg(test)] +mod tests { + use super::release_version; + + #[test] + fn version_is_taken_from_a_package_scoped_tag() { + assert_eq!( + release_version("react-component-lens(packages/vscode/package.json)@1.0.5"), + "1.0.5" + ); + assert_eq!( + release_version("rcl-zed(packages/zed/Cargo.toml)@1.0.5"), + "1.0.5" + ); + } + + #[test] + fn plain_tags_are_left_alone() { + assert_eq!(release_version("1.0.5"), "1.0.5"); + assert_eq!(release_version("v1.0.5"), "1.0.5"); + } +} From 075e29bee45622bcd5fbcb775033e4275353696a Mon Sep 17 00:00:00 2001 From: "Mentenaz-Server (Francois)" Date: Thu, 8 Oct 2026 09:50:27 +0200 Subject: [PATCH 2/3] fix(core): collapse nested if into match guard for clippy 1.95 Also sync Cargo.lock workspace versions to 1.0.5 and add scripts/ci-check.ps1, which runs the deploy.yml test job steps locally using the bun version CI pins. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 8 +- packages/core/src/analyzer.rs | 8 +- scripts/ci-check.ps1 | 197 ++++++++++++++++++++++++++++++++++ 3 files changed, 205 insertions(+), 8 deletions(-) create mode 100644 scripts/ci-check.ps1 diff --git a/Cargo.lock b/Cargo.lock index 9c4a420..44cf6db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1141,7 +1141,7 @@ dependencies = [ [[package]] name = "rcl-core" -version = "1.0.4" +version = "1.0.5" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1155,7 +1155,7 @@ dependencies = [ [[package]] name = "rcl-core-wasm" -version = "1.0.4" +version = "1.0.5" dependencies = [ "console_error_panic_hook", "js-sys", @@ -1169,7 +1169,7 @@ dependencies = [ [[package]] name = "rcl-lsp" -version = "1.0.4" +version = "1.0.5" dependencies = [ "dashmap 6.2.1", "rcl-core", @@ -1179,7 +1179,7 @@ dependencies = [ [[package]] name = "rcl-zed" -version = "1.0.4" +version = "1.0.5" dependencies = [ "zed_extension_api", ] diff --git a/packages/core/src/analyzer.rs b/packages/core/src/analyzer.rs index f9a57a6..c8b5b61 100644 --- a/packages/core/src/analyzer.rs +++ b/packages/core/src/analyzer.rs @@ -1562,10 +1562,10 @@ fn add_default_export_name( component_names.insert("default".to_string()); } } - ExportDefaultDeclarationKind::Identifier(identifier) => { - if local_component_names.contains(identifier.name.as_str()) { - component_names.insert("default".to_string()); - } + ExportDefaultDeclarationKind::Identifier(identifier) + if local_component_names.contains(identifier.name.as_str()) => + { + component_names.insert("default".to_string()); } ExportDefaultDeclarationKind::ArrowFunctionExpression(_) | ExportDefaultDeclarationKind::FunctionExpression(_) diff --git a/scripts/ci-check.ps1 b/scripts/ci-check.ps1 new file mode 100644 index 0000000..f16937b --- /dev/null +++ b/scripts/ci-check.ps1 @@ -0,0 +1,197 @@ +<# +.SYNOPSIS + Runs the same checks as the `test` job in .github/workflows/deploy.yml. + +.DESCRIPTION + Steps, in CI order: + 1. bun install --frozen-lockfile + 2. bun run lint (oxlint + cargo fmt --check + 3x cargo clippy) + 3. bun run typecheck + 4. bun run test (cargo test + wasm-pack test + zed wasm build + build + integration) + 5. bun test (TypeScript 100% coverage gate) + 6. Rust coverage gate (only with -Coverage; CI runs this on Linux only) + + Every step runs even if an earlier one fails, so one run shows everything + that is broken. Exit code is 0 only if all steps pass. + +.PARAMETER Coverage + Also run the cargo-tarpaulin 100% coverage gate. Temporarily rewrites the + Rust sources with a wide rustfmt config, then restores them. + +.PARAMETER SkipInstall + Skip `bun install --frozen-lockfile`. + +.PARAMETER FailFast + Stop at the first failing step. + +.PARAMETER LocalBun + Use the installed bun even if it differs from the version CI pins in + deploy.yml. By default a mismatch runs every step through the pinned one. + +.EXAMPLE + .\scripts\ci-check.ps1 +.EXAMPLE + .\scripts\ci-check.ps1 -SkipInstall -FailFast +#> +[CmdletBinding()] +param( + [switch]$Coverage, + [switch]$SkipInstall, + [switch]$FailFast, + [switch]$LocalBun +) + +$ErrorActionPreference = 'Stop' + +$repoRoot = Split-Path -Parent $PSScriptRoot +$results = New-Object System.Collections.Generic.List[object] +$stopped = $false + +function Invoke-Step { + param( + [string]$Name, + [scriptblock]$Action + ) + + if ($script:stopped) { + $script:results.Add([pscustomobject]@{ Step = $Name; Result = 'SKIPPED'; Seconds = 0 }) + return + } + + Write-Host '' + Write-Host "==> $Name" -ForegroundColor Cyan + $timer = [System.Diagnostics.Stopwatch]::StartNew() + $global:LASTEXITCODE = 0 + $ok = $true + try { + & $Action + if ($LASTEXITCODE -ne 0) { $ok = $false } + } catch { + Write-Host $_ -ForegroundColor Red + $ok = $false + } + $timer.Stop() + + $result = if ($ok) { 'PASS' } else { 'FAIL' } + $color = if ($ok) { 'Green' } else { 'Red' } + Write-Host "<== $Name : $result" -ForegroundColor $color + $script:results.Add([pscustomobject]@{ + Step = $Name + Result = $result + Seconds = [math]::Round($timer.Elapsed.TotalSeconds, 1) + }) + + if (-not $ok -and $FailFast) { $script:stopped = $true } +} + +# CI pins bun in deploy.yml; a different local bun can pass or fail differently +# (bun 1.4 rejects the `require()` in packages/vscode/test/wasmSetup.ts), so run +# every step through the pinned version when the local one does not match. +function Get-PinnedBunVersion { + $workflow = Join-Path $repoRoot '.github\workflows\deploy.yml' + $match = Select-String -Path $workflow -Pattern "bun-version:\s*'?([0-9.]+)'?" | Select-Object -First 1 + if ($match) { return $match.Matches[0].Groups[1].Value } + return $null +} + +function Invoke-Bun { + if ($script:pinnedBun) { + bun x "bun@$script:pinnedBun" @args + } else { + bun @args + } +} + +function Test-Prerequisites { + $missing = @() + foreach ($tool in 'cargo', 'rustup', 'bun') { + if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) { $missing += $tool } + } + # Only `bun run test` needs wasm-pack, so lint and typecheck still run without it. + if (-not (Get-Command 'wasm-pack' -ErrorAction SilentlyContinue)) { + Write-Host 'wasm-pack not found: "bun run test" will fail (cargo install wasm-pack)' -ForegroundColor Yellow + } + if ($Coverage -and -not (Get-Command 'cargo-tarpaulin' -ErrorAction SilentlyContinue)) { + $missing += 'cargo-tarpaulin (cargo install cargo-tarpaulin)' + } + if ($missing.Count -gt 0) { + Write-Host "Missing tools: $($missing -join ', ')" -ForegroundColor Red + return $false + } + + $installed = & rustup target list --installed + foreach ($target in 'wasm32-wasip1', 'wasm32-unknown-unknown') { + if ($installed -notcontains $target) { + Write-Host "Installing Rust target $target" -ForegroundColor Yellow + & rustup target add $target + if ($LASTEXITCODE -ne 0) { return $false } + } + } + return $true +} + +Push-Location $repoRoot +try { + if (-not (Test-Prerequisites)) { exit 1 } + + $pinnedBun = $null + if (-not $LocalBun) { + $ciBun = Get-PinnedBunVersion + $localBunVersion = (& bun --version).Trim() + if ($ciBun -and $ciBun -ne $localBunVersion) { + Write-Host "Local bun is $localBunVersion, CI uses ${ciBun}: running steps with bun@${ciBun}" -ForegroundColor Yellow + $pinnedBun = $ciBun + } + } + + if (-not $SkipInstall) { + Invoke-Step 'bun install --frozen-lockfile' { Invoke-Bun install --frozen-lockfile } + } + Invoke-Step 'bun run lint' { Invoke-Bun run lint } + Invoke-Step 'bun run typecheck' { Invoke-Bun run typecheck } + Invoke-Step 'bun run test' { Invoke-Bun run test } + Invoke-Step 'bun test (TS coverage gate)' { Invoke-Bun test } + + if ($Coverage) { + Invoke-Step 'Rust coverage (100% gate)' { + $rustfmtToml = Join-Path $repoRoot '.rustfmt.toml' + if (Test-Path $rustfmtToml) { + throw '.rustfmt.toml already exists; remove it before running -Coverage.' + } + try { + # Same wide config as CI: collapses multi-line expressions so + # tarpaulin can attribute coverage to them. + @( + 'max_width = 100000' + 'tab_spaces = 4' + 'newline_style = "Unix"' + 'fn_call_width = 100000' + 'fn_params_layout = "Compressed"' + 'chain_width = 100000' + 'merge_derives = true' + 'use_small_heuristics = "Default"' + ) | Set-Content -Path $rustfmtToml -Encoding ascii + cargo fmt + if ($LASTEXITCODE -eq 0) { Invoke-Bun run test:coverage } + $coverageExit = $LASTEXITCODE + } finally { + Remove-Item $rustfmtToml -Force -ErrorAction SilentlyContinue + cargo fmt + } + $global:LASTEXITCODE = $coverageExit + } + } +} finally { + Pop-Location +} + +Write-Host '' +Write-Host 'Summary' -ForegroundColor Cyan +$results | Format-Table -AutoSize | Out-Host + +if ($results | Where-Object { $_.Result -ne 'PASS' }) { + Write-Host 'CI checks FAILED' -ForegroundColor Red + exit 1 +} +Write-Host 'All CI checks passed' -ForegroundColor Green +exit 0 From 9845cbbe8931c836812cec12df0a93009c3c02b8 Mon Sep 17 00:00:00 2001 From: "Mentenaz-Server (Francois)" Date: Thu, 8 Oct 2026 10:00:24 +0200 Subject: [PATCH 3/3] fix(tests): replace assert!(is_empty()) for clippy 1.99 assert_is_empty CI runs the latest stable clippy, which rejects assert!(x.is_empty()) and assert!(!x.is_empty()). Compare lengths (or the empty string) instead so the failing value is shown. Co-Authored-By: Claude Opus 5.5 --- packages/core/src/analyzer.rs | 13 ++++++++----- packages/core/tests/conformance.rs | 10 ++++------ packages/lsp/src/main.rs | 8 ++++---- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/core/src/analyzer.rs b/packages/core/src/analyzer.rs index c8b5b61..2f5e88b 100644 --- a/packages/core/src/analyzer.rs +++ b/packages/core/src/analyzer.rs @@ -2478,8 +2478,8 @@ mod tests { &mut ranges, &mut locals, ); - assert!(ranges.is_empty()); - assert!(locals.is_empty()); + assert_eq!(ranges.len(), 0); + assert_eq!(locals.len(), 0); } #[rstest] @@ -2652,8 +2652,8 @@ mod tests { ); } } - assert!(local_component_names.is_empty()); - assert!(pattern_component_names.is_empty()); + assert_eq!(local_component_names.len(), 0); + assert_eq!(pattern_component_names.len(), 0); } #[test] @@ -2734,6 +2734,9 @@ mod tests { }; collector.track_function_declaration(function); - assert!(collector.per_component_funcs.expect("func map")["Component"].is_empty()); + assert_eq!( + collector.per_component_funcs.expect("func map")["Component"].len(), + 0 + ); } } diff --git a/packages/core/tests/conformance.rs b/packages/core/tests/conformance.rs index 6527723..a9a7f06 100644 --- a/packages/core/tests/conformance.rs +++ b/packages/core/tests/conformance.rs @@ -102,10 +102,7 @@ fn conformance_corpus_matches_oracle_goldens() { let mut goldens = Vec::new(); collect_goldens(&goldens_root, &mut goldens); - assert!( - !goldens.is_empty(), - "no goldens found under {goldens_root:?}" - ); + assert_ne!(goldens.len(), 0, "no goldens found under {goldens_root:?}"); let mut passed = 0usize; let mut failures: Vec = Vec::new(); @@ -142,8 +139,9 @@ fn conformance_corpus_matches_oracle_goldens() { let total = goldens.len(); eprintln!("conformance: {passed}/{total} passing"); - assert!( - failures.is_empty(), + assert_eq!( + failures.len(), + 0, "{}/{} fixtures failed:\n{}", failures.len(), total, diff --git a/packages/lsp/src/main.rs b/packages/lsp/src/main.rs index 1ad3199..cd80b1e 100644 --- a/packages/lsp/src/main.rs +++ b/packages/lsp/src/main.rs @@ -408,7 +408,7 @@ mod tests { .iter() .filter(|(line, character, _, _)| *line == 1 && *character >= 20) .collect::>(); - assert!(!client_jsx_tuples.is_empty()); + assert_ne!(client_jsx_tuples.len(), 0); assert!( client_jsx_tuples .iter() @@ -484,7 +484,7 @@ mod tests { assert_eq!(legend.token_types.len(), 2); assert_eq!(legend.token_types[0].as_str(), "rscClientComponent"); assert_eq!(legend.token_types[1].as_str(), "rscServerComponent"); - assert!(legend.token_modifiers.is_empty()); + assert_eq!(legend.token_modifiers.len(), 0); } #[test] @@ -502,7 +502,7 @@ mod tests { fn path_from_uri_returns_document_tsx_for_non_file_url_with_empty_path() { let url = Url::parse("untitled:").expect("parse empty untitled URL"); assert!(url.to_file_path().is_err()); - assert!(url.path().is_empty()); + assert_eq!(url.path(), ""); let result = path_from_uri(&url); @@ -624,7 +624,7 @@ mod tests { let tuples = semantic_token_tuples_for_source(&entry_path, &entry_source); // The imported Star is a client component, so its JSX usage in entry.tsx is colored as client (token_type 0). - assert!(!tuples.is_empty()); + assert_ne!(tuples.len(), 0); let star_jsx = tuples .iter() .find(|(line, character, _, _)| *line == 1 && *character >= 20);