Skip to content

feat(pacquet): port JSR specifier parser and wire resolve_jsr#11772

Merged
zkochan merged 2 commits into
mainfrom
feat/pacquet-jsr-resolver
May 20, 2026
Merged

feat(pacquet): port JSR specifier parser and wire resolve_jsr#11772
zkochan merged 2 commits into
mainfrom
feat/pacquet-jsr-resolver

Conversation

@zkochan

@zkochan zkochan commented May 20, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds a new pacquet-resolving-jsr-specifier-parser crate that ports @pnpm/resolving.jsr-specifier-parser (parseJsrSpecifier + JsrSpec), mirroring upstream's ERR_PNPM_MISSING_JSR_PACKAGE_SCOPE / ERR_PNPM_INVALID_JSR_PACKAGE_NAME / ERR_PNPM_INVALID_JSR_SPECIFIER codes 1:1.
  • Adds parse_jsr_specifier_to_registry_package_spec alongside parse_bare_specifier, matching upstream's same-file shape in resolving/npm-resolver/src/parseBareSpecifier.ts.
  • Routes jsr: specifiers through a new NpmResolver::resolve_jsr_impl that picks against registries['@jsr'] (with the https://npm.jsr.io/ DEFAULT_REGISTRIES fallback) and stamps resolved_via = "jsr-registry" plus alias = spec.jsr_pkg_name, mirroring upstream's resolveJsr.
  • Extracts a shared pick_from_registry helper so the npm and JSR paths share the picker invocation; build_resolve_result now takes a resolved_via parameter.

Out of scope (matches existing pacquet gaps)

  • calcSpecifier / calcPrefixedSpecifier — no pacquet caller yet, mirrors the npm path's behavior.
  • JSR-specific createResolveLatest closure — pacquet's resolve_latest_impl is unified and just forwards through resolve_impl, so jsr: queries already route correctly without a separate JSR-only path.

Test plan

  • cargo nextest run — 1599 tests pass workspace-wide; 7 new parser tests (1:1 port of resolving/jsr-specifier-parser/test/parse.test.ts), 5 new adapter tests in parse_bare_specifier/tests.rs, 3 new integration tests in npm_resolver/tests.rs covering the JSR happy path, default-tag fallback, and parser-error propagation through the resolver.
  • just lint clean (--deny warnings).
  • just check clean.
  • just fmt clean.
  • just ready end-to-end (typos step trips on pre-existing CHANGELOG.md typos in the broader tree — none in the files touched here).

Written by an agent (Claude Code, claude-opus-4-7).

Summary by CodeRabbit

  • New Features
    • Adds support for JSR package specifiers: parsing, alias handling, and routing through the JSR registry with version-selector and default-tag behavior; clearer errors for invalid JSR inputs.
  • Tests
    • Adds comprehensive unit and integration tests covering JSR specifier parsing, resolution flows, default-tag fallback, alias behavior, and error cases.

Review Change Stack

…ire resolve_jsr

Adds a new `pacquet-resolving-jsr-specifier-parser` crate that ports
`@pnpm/resolving.jsr-specifier-parser` (`parseJsrSpecifier` + `JsrSpec`),
mirroring upstream's `ERR_PNPM_MISSING_JSR_PACKAGE_SCOPE` /
`ERR_PNPM_INVALID_JSR_PACKAGE_NAME` / `ERR_PNPM_INVALID_JSR_SPECIFIER`
error codes 1:1.

Wires the parser into the npm resolver:

- `parse_jsr_specifier_to_registry_package_spec` adapter alongside
  `parse_bare_specifier`, matching upstream's same-file shape in
  `resolving/npm-resolver/src/parseBareSpecifier.ts`.
- `NpmResolver::resolve_impl` detects `jsr:` early and routes to a new
  `resolve_jsr_impl` that picks against `registries['@jsr']` (with the
  `https://npm.jsr.io/` `DEFAULT_REGISTRIES` fallback) and stamps
  `resolved_via = "jsr-registry"` plus `alias = spec.jsr_pkg_name`.
- Extracts a shared `pick_from_registry` helper so npm and JSR paths
  share the picker invocation; `build_resolve_result` now takes a
  `resolved_via` parameter.

Ports the 6 upstream parser test cases and adds adapter + resolver
integration tests covering the JSR happy path, default-tag fallback,
and parser-error propagation.

Ports https://github.com/pnpm/pnpm/blob/05dd45ea82/resolving/jsr-specifier-parser/src/index.ts
and https://github.com/pnpm/pnpm/blob/1627943d2a/resolving/npm-resolver/src/index.ts.

---
Written by an agent (Claude Code, claude-opus-4-7).
@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 635a232a-6fd0-4f2f-a7f5-c1afdfded09c

📥 Commits

Reviewing files that changed from the base of the PR and between 839fba2 and 49eed65.

📒 Files selected for processing (3)
  • pacquet/crates/resolving-jsr-specifier-parser/src/lib.rs
  • pacquet/crates/resolving-jsr-specifier-parser/src/tests.rs
  • pacquet/crates/resolving-npm-resolver/src/npm_resolver/tests.rs
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Code Coverage
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Dylint
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: When porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(), or streamParser.write(), mirror the call site, payload, and ordering so the reporter parses pacquet's NDJSON the same way it parses pnpm's.
Declare a newtype wrapper for branded string types. Do not collapse the brand into a plain String or &str.
If upstream always validates before construction, validate in pacquet's wrapper too. The wrapper must construct only via TryFrom<String> and/or FromStr. Do not provide an infallible public constructor.
If upstream never validates, just brand for type-safety. Expose an infallible From<String> (and From<&str> when convenient).
If upstream occasionally constructs without validation, expose from_str_unchecked as an escape hatch alongside the validating constructor.
Match upstream serde behavior for branded types that cross JSON, YAML, or INI boundaries. Use #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization.
Use #[derive(derive_more::From)] and #[derive(derive_more::Into)] for mechanical conversion impls. Fall back to manual impl only when conversion needs custom logic.
String-literal unions should become enums, not newtype wrappers. Model closed sets of valid string values as enums.
Template literal types should be treated as branded strings with validation discipline from rules 2-5.
Choose owned vs. borrowed parameters to minimize copies. Widen to the most encompassing type (&Path over &PathBuf, &str over &String) when it doesn't force extra copies.
Prefer Arc::clone(&x) / Rc::clone(&x) over x.clone() for reference-counted types, so the cost is visible at the call site.
Follow Rust API Guidelines for naming conventions.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*;. Two forms stay allowed: external-crate preludes like use rayon::prelude::*; and root-of-module re-...

Files:

  • pacquet/crates/resolving-npm-resolver/src/npm_resolver/tests.rs
  • pacquet/crates/resolving-jsr-specifier-parser/src/lib.rs
  • pacquet/crates/resolving-jsr-specifier-parser/src/tests.rs
🔇 Additional comments (3)
pacquet/crates/resolving-jsr-specifier-parser/src/lib.rs (1)

120-125: LGTM!

pacquet/crates/resolving-jsr-specifier-parser/src/tests.rs (1)

99-109: LGTM!

pacquet/crates/resolving-npm-resolver/src/npm_resolver/tests.rs (1)

297-301: LGTM!


📝 Walkthrough

Walkthrough

A new JSR specifier parser crate is created to parse jsr: package specifiers, then integrated into the npm resolver to handle JSR package resolution. The resolver now detects jsr: inputs and routes them through the JSR registry (@jsr) while maintaining unified registry-picking logic and consistent provenance tracking for both npm and JSR resolutions.

Changes

JSR Specifier Resolution

Layer / File(s) Summary
JSR Specifier Parser Crate
Cargo.toml, pacquet/crates/resolving-jsr-specifier-parser/Cargo.toml, pacquet/crates/resolving-jsr-specifier-parser/src/lib.rs, pacquet/crates/resolving-jsr-specifier-parser/src/tests.rs
New standalone crate exports JsrSpec, ParseJsrSpecifierError, and parse_jsr_specifier(). Parses jsr:@scope/name``[@version] and selector-only `jsr:` (with alias) into structured data, folding JSR scopes to npm format (`@jsr/scope__name`). Returns `Ok(None)` for non-`jsr:` inputs. Includes unit tests covering success and error cases.
NPM Resolver JSR Integration
pacquet/crates/resolving-npm-resolver/Cargo.toml, pacquet/crates/resolving-npm-resolver/src/lib.rs, pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs
Adds workspace dependency on the parser crate. resolve_impl dispatches jsr: bare specifiers to resolve_jsr_impl, routes through the @jsr registry, refactors registry picking into pick_from_registry, and parameterizes build_resolve_result with resolved_via. Public API re-exports JSR types.
Bare Specifier Parser JSR Wrapper
pacquet/crates/resolving-npm-resolver/src/parse_bare_specifier.rs, pacquet/crates/resolving-npm-resolver/src/parse_bare_specifier/tests.rs
Adds JsrRegistryPackageSpec and parse_jsr_specifier_to_registry_package_spec() to convert jsr: inputs into npm-shaped RegistryPackageSpec plus JSR package name, classify selector types, and fallback to default tag when needed. Tests cover scope+range parsing, alias borrowing, non-JSR rejection, and MissingScope errors.
Resolver Integration Tests
pacquet/crates/resolving-npm-resolver/src/npm_resolver/tests.rs
Refactors build_resolver to support per-registry maps, adds JSR_PACKAGE_BODY packument, and adds three resolver tests verifying JSR routing, default dist-tag fallback, and parser error propagation.

Sequence Diagram(s)

sequenceDiagram
  participant resolve_impl
  participant resolve_jsr_impl
  participant pick_from_registry
  participant PickPackage as PickPackage<br/>Upstream
  participant BuildResult as build_resolve_result

  resolve_impl->>resolve_impl: Check bare_specifier prefix
  alt jsr: specifier
    resolve_impl->>resolve_jsr_impl: JSR dispatch
    resolve_jsr_impl->>pick_from_registry: `@jsr` registry + parsed spec
    pick_from_registry->>PickPackage: Select version from registry
    PickPackage-->>pick_from_registry: meta + version
    pick_from_registry-->>resolve_jsr_impl: {meta, version}
    resolve_jsr_impl->>BuildResult: resolved_via: "jsr-registry"
  else npm or aliased
    resolve_impl->>pick_from_registry: default/aliased registry
    pick_from_registry->>PickPackage: Select version from registry
    PickPackage-->>pick_from_registry: meta + version
    pick_from_registry-->>resolve_impl: {meta, version}
    resolve_impl->>BuildResult: resolved_via: "npm-registry"
  end
  BuildResult-->>resolve_impl: ResolveResult
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

area: resolution

Poem

🐰 I hopped through crates and parsed a string,
jsr: scoped names now wear a new ring.
@jsr/scope__pack is the npm disguise,
Resolvers follow, provenance wise,
Hooray—tests pass with joyful springs!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(pacquet): port JSR specifier parser and wire resolve_jsr' clearly and accurately summarizes the main changes—introducing JSR specifier parsing and resolver integration.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pacquet-jsr-resolver

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00      7.5±0.10ms   581.9 KB/sec    1.02      7.6±0.18ms   571.9 KB/sec

@codecov-commenter

codecov-commenter commented May 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.26230% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.01%. Comparing base (c068720) to head (49eed65).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
.../crates/resolving-npm-resolver/src/npm_resolver.rs 91.66% 5 Missing ⚠️
...t/crates/resolving-jsr-specifier-parser/src/lib.rs 97.61% 1 Missing ⚠️
...resolving-npm-resolver/src/parse_bare_specifier.rs 95.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #11772      +/-   ##
==========================================
+ Coverage   89.98%   90.01%   +0.03%     
==========================================
  Files         155      156       +1     
  Lines       18114    18223     +109     
==========================================
+ Hits        16299    16404     +105     
- Misses       1815     1819       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Scenario: Frozen Lockfile

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.505 ± 0.131 2.344 2.686 1.03 ± 0.06
pacquet@main 2.442 ± 0.054 2.363 2.537 1.00
pnpm 5.012 ± 0.050 4.919 5.108 2.05 ± 0.05
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.5052487106399997,
      "stddev": 0.13094613315704826,
      "median": 2.49260250904,
      "user": 2.81279084,
      "system": 3.841703,
      "min": 2.34355359754,
      "max": 2.68583978954,
      "times": [
        2.66328750254,
        2.43602323654,
        2.68583978954,
        2.64226634154,
        2.35428934154,
        2.52500136954,
        2.56967451854,
        2.46020364854,
        2.37234776054,
        2.34355359754
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.44243180104,
      "stddev": 0.05423783294632783,
      "median": 2.43591918304,
      "user": 2.8242438400000003,
      "system": 3.7930289999999998,
      "min": 2.36251659854,
      "max": 2.53736684954,
      "times": [
        2.36251659854,
        2.41730856954,
        2.47072086254,
        2.4411887555400003,
        2.53736684954,
        2.43064961054,
        2.37670617554,
        2.5063452435399998,
        2.46414971554,
        2.41736562954
      ]
    },
    {
      "command": "pnpm",
      "mean": 5.01169380964,
      "stddev": 0.049562069341600616,
      "median": 5.00918023754,
      "user": 8.479205140000001,
      "system": 4.321282699999999,
      "min": 4.91936072554,
      "max": 5.10819298754,
      "times": [
        4.91936072554,
        5.0511707675399995,
        4.9926108425399995,
        4.9722818125399995,
        5.00423428354,
        5.01412619154,
        4.99852827054,
        5.10819298754,
        5.030798573539999,
        5.02563364154
      ]
    }
  ]
}

Scenario: Frozen Lockfile (Hot Cache)

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 692.1 ± 18.2 675.0 740.9 1.00
pacquet@main 738.2 ± 50.7 693.2 825.2 1.07 ± 0.08
pnpm 2638.9 ± 90.2 2504.5 2789.7 3.81 ± 0.16
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.69208044952,
      "stddev": 0.018192966467113603,
      "median": 0.68688656392,
      "user": 0.3810061599999999,
      "system": 1.5821909599999997,
      "min": 0.67502577692,
      "max": 0.74089314592,
      "times": [
        0.74089314592,
        0.69037335492,
        0.6806269579199999,
        0.68573482492,
        0.68457764492,
        0.68578022892,
        0.69598627692,
        0.68799289892,
        0.69381338492,
        0.67502577692
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.73815571762,
      "stddev": 0.05066840332882879,
      "median": 0.70846825742,
      "user": 0.38216276000000005,
      "system": 1.61342146,
      "min": 0.69317123892,
      "max": 0.82522567192,
      "times": [
        0.77174178492,
        0.70174964692,
        0.82522567192,
        0.70832134592,
        0.70092721192,
        0.70861516892,
        0.81859135692,
        0.75071121792,
        0.69317123892,
        0.70250253192
      ]
    },
    {
      "command": "pnpm",
      "mean": 2.63888655602,
      "stddev": 0.09020841026566674,
      "median": 2.61753522392,
      "user": 3.3152680599999997,
      "system": 2.29263366,
      "min": 2.50446907792,
      "max": 2.78966835692,
      "times": [
        2.7418080049199998,
        2.5879388529200003,
        2.61080605692,
        2.78966835692,
        2.62426439092,
        2.54269996792,
        2.50446907792,
        2.59249389492,
        2.6983383389199997,
        2.69637861792
      ]
    }
  ]
}

@zkochan zkochan marked this pull request as ready for review May 20, 2026 14:58
@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

Port JSR specifier parser and wire resolve_jsr through npm resolver

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Port JSR specifier parser from pnpm with error handling
• Wire JSR resolver through @jsr registry with npm-shaped names
• Extract shared picker helper for npm and JSR resolution paths
• Add comprehensive parser and resolver integration tests
Diagram
flowchart LR
  A["jsr: specifier"] -->|parse_jsr_specifier| B["JsrSpec"]
  B -->|parse_jsr_specifier_to_registry_package_spec| C["JsrRegistryPackageSpec"]
  C -->|resolve_jsr_impl| D["@jsr registry"]
  D -->|pick_from_registry| E["PickedFromRegistry"]
  E -->|build_resolve_result| F["ResolveResult<br/>resolved_via=jsr-registry"]
  G["npm: specifier"] -->|parse_bare_specifier| H["RegistryPackageSpec"]
  H -->|pick_from_registry| E

Loading

File Changes

1. pacquet/crates/resolving-jsr-specifier-parser/src/lib.rs ✨ Enhancement +147/-0

JSR specifier parser with error handling

pacquet/crates/resolving-jsr-specifier-parser/src/lib.rs


2. pacquet/crates/resolving-jsr-specifier-parser/src/tests.rs 🧪 Tests +97/-0

Parser unit tests ported from pnpm

pacquet/crates/resolving-jsr-specifier-parser/src/tests.rs


3. pacquet/crates/resolving-jsr-specifier-parser/Cargo.toml ⚙️ Configuration changes +18/-0

New JSR parser crate manifest

pacquet/crates/resolving-jsr-specifier-parser/Cargo.toml


View more (7)
4. pacquet/crates/resolving-npm-resolver/src/lib.rs ✨ Enhancement +3/-1

Export JSR adapter and registry package spec

pacquet/crates/resolving-npm-resolver/src/lib.rs


5. pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs ✨ Enhancement +114/-15

Add JSR resolution path and shared picker helper

pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs


6. pacquet/crates/resolving-npm-resolver/src/npm_resolver/tests.rs 🧪 Tests +113/-1

Add JSR resolver integration tests

pacquet/crates/resolving-npm-resolver/src/npm_resolver/tests.rs


7. pacquet/crates/resolving-npm-resolver/src/parse_bare_specifier.rs ✨ Enhancement +57/-1

Add JSR specifier adapter and registry package spec

pacquet/crates/resolving-npm-resolver/src/parse_bare_specifier.rs


8. pacquet/crates/resolving-npm-resolver/src/parse_bare_specifier/tests.rs 🧪 Tests +59/-1

Add JSR adapter unit tests

pacquet/crates/resolving-npm-resolver/src/parse_bare_specifier/tests.rs


9. pacquet/crates/resolving-npm-resolver/Cargo.toml Dependencies +6/-5

Add JSR parser dependency

pacquet/crates/resolving-npm-resolver/Cargo.toml


10. Cargo.toml Dependencies +1/-0

Register new JSR parser crate

Cargo.toml


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented May 20, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0)

Context used
✅ Compliance rules (platform): 50 rules

Grey Divider


Action required

1. Invalid JSR selector falls through 🐞 Bug ≡ Correctness
Description
parse_jsr_specifier_to_registry_package_spec returns Ok(None) when the version selector is not a
valid version/range/tag, and resolve_jsr_impl propagates that Ok(None) even though jsr: was
already claimed. This makes the resolver chain treat jsr: inputs as “unsupported by any resolver”
instead of surfacing a JSR parse error.
Code

pacquet/crates/resolving-npm-resolver/src/parse_bare_specifier.rs[R135-138]

Evidence
The adapter explicitly returns Ok(None) on selector classification failure, and the JSR resolver
path returns Ok(None) when that happens. Resolver-base and the default resolver treat Ok(None)
as “not handled”, which leads to SpecNotSupportedByAnyResolverError when the chain is
exhausted—misreporting invalid jsr: specifiers as unsupported.

pacquet/crates/resolving-npm-resolver/src/parse_bare_specifier.rs[126-138]
pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs[135-144]
pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs[206-214]
pacquet/crates/resolving-resolver-base/src/resolve.rs[249-276]
pacquet/crates/resolving-default-resolver/src/lib.rs[45-60]
pacquet/crates/resolving-default-resolver/src/lib.rs[81-83]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
For `jsr:` specifiers, selector classification failure is currently mapped to `Ok(None)`, which semantically means “this resolver doesn’t handle the dependency”. Because `NpmResolver::resolve_impl` eagerly dispatches `jsr:` to `resolve_jsr_impl`, this `Ok(None)` can bubble up to the default resolver and become `SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER`, masking the real cause.

### Issue Context
- `Ok(None)` in the resolver contract means “not handled; try next resolver”.
- `jsr:` is already claimed by `NpmResolver`, so invalid selectors should be a hard error (likely `ERR_PNPM_INVALID_JSR_SPECIFIER`).
- Also consider the empty-selector case (`jsr:@foo/bar@`), which currently can slip through classification as an empty tag.

### Fix Focus Areas
- pacquet/crates/resolving-npm-resolver/src/parse_bare_specifier.rs[126-178]
- pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs[135-214]
- pacquet/crates/resolving-jsr-specifier-parser/src/lib.rs[38-69]

### Suggested change
- Distinguish "non-jsr" from "jsr but invalid selector":
 - Keep `Ok(None)` only for non-`jsr:` inputs.
 - If `parse_jsr_specifier(...)` returned `Some(_)` but `get_version_selector_type(...)` returns `None`, return an `Err(...)`.
- Implement the error as either:
 - a new `ParseJsrSpecifierError::InvalidSpecifier { specifier: String }` with diagnostic code `ERR_PNPM_INVALID_JSR_SPECIFIER`, or
 - a resolver-local error type that is boxed into `ResolveError`.
- Ensure empty selector is rejected (e.g. require `!selector_input.is_empty()` before accepting it as a tag).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Invalid JSR names folded 🐞 Bug ≡ Correctness
Description
jsr_to_npm_package_name accepts structurally invalid scoped names (empty scope/name or extra /
segments) and still produces an npm package name (e.g. @jsr/foo__ or names containing /). This
violates the npm package-name grammar and pushes bad input into network resolution where it fails
with confusing registry errors instead of a deterministic InvalidPackageName parser error.
Code

pacquet/crates/resolving-jsr-specifier-parser/src/lib.rs[R132-143]

Evidence
The folding helper uses split_once('/') and then directly formats @jsr/{scope}__{name} without
checking empty segments or additional /, which can produce npm names that contradict the repo’s
documented npm package-name grammar; the picker’s package-name validation also won’t reject extra
slashes for scoped names.

pacquet/crates/resolving-jsr-specifier-parser/src/lib.rs[132-144]
pacquet/crates/resolving-npm-resolver/src/registry_url.rs[13-19]
pacquet/crates/resolving-npm-resolver/src/pick_package.rs[582-589]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`jsr_to_npm_package_name` only checks for `@` and the presence of a `/`, but does not validate that the scope and name segments are non-empty and that the name segment contains no additional `/`. This can generate invalid npm package names (e.g. empty name) and defer failure to later network/picker code.

### Issue Context
- JSR specifiers are expected to be `jsr:@<scope>/<name>(@<selector>)`.
- Repo docs already describe npm package-name grammar as allowing only the leading `@scope/` slash.

### Fix Focus Areas
- pacquet/crates/resolving-jsr-specifier-parser/src/lib.rs[132-144]

### Suggested change
- After `split_once('/')`, explicitly reject:
 - `scope.is_empty()`
 - `name.is_empty()`
 - `name.contains('/')`
- Return `ParseJsrSpecifierError::InvalidPackageName { pkg_name: jsr_pkg_name.to_string() }` for these cases.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread pacquet/crates/resolving-npm-resolver/src/parse_bare_specifier.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
pacquet/crates/resolving-npm-resolver/src/npm_resolver/tests.rs (1)

295-297: ⚡ Quick win

Assert the pnpm error code instead of a generic "JSR" substring.

Line 297 is too broad and can pass on unrelated errors. Assert the specific code (e.g. ERR_PNPM_MISSING_JSR_PACKAGE_SCOPE) to lock the public contract.

Proposed test assertion tightening
     let err = resolver.resolve(&wanted, &ResolveOptions::default()).await.unwrap_err();
     let msg = err.to_string();
-    assert!(msg.contains("JSR"), "unexpected error message: {msg}");
+    assert!(
+        msg.contains("ERR_PNPM_MISSING_JSR_PACKAGE_SCOPE"),
+        "unexpected error message: {msg}"
+    );

As per coding guidelines, "Match pnpm's error codes and messages where pnpm defines them. Error codes are part of the public contract, not implementation detail."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pacquet/crates/resolving-npm-resolver/src/npm_resolver/tests.rs` around lines
295 - 297, The test currently checks that the error string contains "JSR" which
is too broad; update the assertion to verify the pnpm-specific error code (for
example ERR_PNPM_MISSING_JSR_PACKAGE_SCOPE) is present in the error, by calling
resolver.resolve(&wanted, &ResolveOptions::default()).await.unwrap_err() into
err and then asserting err.to_string() (or err.code()/err.kind() if available)
contains the exact pnpm error code constant instead of the generic "JSR"
substring; adjust the assertion in the test in tests.rs accordingly so the
public contract is locked to the specific error code.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pacquet/crates/resolving-jsr-specifier-parser/src/lib.rs`:
- Around line 120-122: The code treats alias = Some("") as a valid alias,
causing the wrong error to be returned; update the check around the alias
variable in lib.rs (the let Some(alias) = alias else { ... } block inside the
JSR spec parser) to treat empty strings as missing package names by treating
Some("") like None and returning ParseJsrSpecifierError::MissingPackageName {
specifier: rest.to_string() } for version-only inputs; ensure downstream logic
that returns MissingScope still only runs for truly present non-empty aliases.

---

Nitpick comments:
In `@pacquet/crates/resolving-npm-resolver/src/npm_resolver/tests.rs`:
- Around line 295-297: The test currently checks that the error string contains
"JSR" which is too broad; update the assertion to verify the pnpm-specific error
code (for example ERR_PNPM_MISSING_JSR_PACKAGE_SCOPE) is present in the error,
by calling resolver.resolve(&wanted,
&ResolveOptions::default()).await.unwrap_err() into err and then asserting
err.to_string() (or err.code()/err.kind() if available) contains the exact pnpm
error code constant instead of the generic "JSR" substring; adjust the assertion
in the test in tests.rs accordingly so the public contract is locked to the
specific error code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6868a4ad-d55a-42bd-8fa0-7f902a3873da

📥 Commits

Reviewing files that changed from the base of the PR and between 2061c55 and 839fba2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • Cargo.toml
  • pacquet/crates/resolving-jsr-specifier-parser/Cargo.toml
  • pacquet/crates/resolving-jsr-specifier-parser/src/lib.rs
  • pacquet/crates/resolving-jsr-specifier-parser/src/tests.rs
  • pacquet/crates/resolving-npm-resolver/Cargo.toml
  • pacquet/crates/resolving-npm-resolver/src/lib.rs
  • pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs
  • pacquet/crates/resolving-npm-resolver/src/npm_resolver/tests.rs
  • pacquet/crates/resolving-npm-resolver/src/parse_bare_specifier.rs
  • pacquet/crates/resolving-npm-resolver/src/parse_bare_specifier/tests.rs
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: When porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(), or streamParser.write(), mirror the call site, payload, and ordering so the reporter parses pacquet's NDJSON the same way it parses pnpm's.
Declare a newtype wrapper for branded string types. Do not collapse the brand into a plain String or &str.
If upstream always validates before construction, validate in pacquet's wrapper too. The wrapper must construct only via TryFrom<String> and/or FromStr. Do not provide an infallible public constructor.
If upstream never validates, just brand for type-safety. Expose an infallible From<String> (and From<&str> when convenient).
If upstream occasionally constructs without validation, expose from_str_unchecked as an escape hatch alongside the validating constructor.
Match upstream serde behavior for branded types that cross JSON, YAML, or INI boundaries. Use #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization.
Use #[derive(derive_more::From)] and #[derive(derive_more::Into)] for mechanical conversion impls. Fall back to manual impl only when conversion needs custom logic.
String-literal unions should become enums, not newtype wrappers. Model closed sets of valid string values as enums.
Template literal types should be treated as branded strings with validation discipline from rules 2-5.
Choose owned vs. borrowed parameters to minimize copies. Widen to the most encompassing type (&Path over &PathBuf, &str over &String) when it doesn't force extra copies.
Prefer Arc::clone(&x) / Rc::clone(&x) over x.clone() for reference-counted types, so the cost is visible at the call site.
Follow Rust API Guidelines for naming conventions.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*;. Two forms stay allowed: external-crate preludes like use rayon::prelude::*; and root-of-module re-...

Files:

  • pacquet/crates/resolving-jsr-specifier-parser/src/tests.rs
  • pacquet/crates/resolving-npm-resolver/src/lib.rs
  • pacquet/crates/resolving-jsr-specifier-parser/src/lib.rs
  • pacquet/crates/resolving-npm-resolver/src/parse_bare_specifier.rs
  • pacquet/crates/resolving-npm-resolver/src/parse_bare_specifier/tests.rs
  • pacquet/crates/resolving-npm-resolver/src/npm_resolver/tests.rs
  • pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs
🔇 Additional comments (10)
Cargo.toml (1)

42-42: LGTM!

pacquet/crates/resolving-jsr-specifier-parser/Cargo.toml (1)

1-19: LGTM!

pacquet/crates/resolving-jsr-specifier-parser/src/lib.rs (1)

1-119: LGTM!

Also applies to: 123-148

pacquet/crates/resolving-jsr-specifier-parser/src/tests.rs (1)

1-98: LGTM!

pacquet/crates/resolving-npm-resolver/Cargo.toml (1)

14-19: LGTM!

pacquet/crates/resolving-npm-resolver/src/lib.rs (1)

50-52: LGTM!

pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs (1)

40-40: LGTM!

Also applies to: 46-63, 135-235, 237-277, 329-333, 340-340, 377-377

pacquet/crates/resolving-npm-resolver/src/parse_bare_specifier.rs (1)

2-2: LGTM!

Also applies to: 9-13, 16-16, 101-149

pacquet/crates/resolving-npm-resolver/src/parse_bare_specifier/tests.rs (1)

1-5: LGTM!

Also applies to: 156-209

pacquet/crates/resolving-npm-resolver/src/npm_resolver/tests.rs (1)

50-56: LGTM!

Also applies to: 71-103, 227-282

Comment thread pacquet/crates/resolving-jsr-specifier-parser/src/lib.rs Outdated
…sing in version-only specs

Upstream's `parseJsrSpecifier` guards the version-only branch with the
truthy check `if (!alias)`, so an empty string falls into the
`INVALID_JSR_SPECIFIER` arm. Pacquet's `let Some(alias) = alias else`
form only triggers on `None`, so `Some("")` was instead carried into
`jsr_to_npm_package_name` and surfaced `MISSING_JSR_PACKAGE_SCOPE`.
Filter empties out before the destructure so both sides agree on the
error code.

Also tighten the resolver-level integration test for the unscoped-name
case to assert the upstream-defined error message instead of a generic
"JSR" substring.

Reported by CodeRabbit on #11772.

---
Written by an agent (Claude Code, claude-opus-4-7).
@zkochan

zkochan commented May 20, 2026

Copy link
Copy Markdown
Member Author

Pushed 49eed65 addressing the review feedback:

  1. CodeRabbit — empty alias falls through as a real alias (resolving-jsr-specifier-parser/src/lib.rs:120-122): Fixed. Added alias.filter(|alias| !alias.is_empty()) so Some("") now lands on MissingPackageName, matching upstream's if (!alias) truthiness check. Regression test in tests.rs.

  2. CodeRabbit — test assertion too broad (npm_resolver/tests.rs:295-297): Tightened. The resolver seam returns errors as boxed dyn std::error::Error, so downcasting to the variant isn't available — instead asserts equality against the upstream-defined message "Package names from JSR must have a scope", which ties the test to the same string the TS PnpmError message carries.

  3. Qodo — invalid selector falls through as Ok(None) (parse_bare_specifier.rs:138): Not changing. TS parseJsrSpecifierToRegistryPackageSpec also returns null here, so the chain reaches SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER in both stacks. The empty-selector concern (jsr:@foo/bar@) is real but lives in get_version_selector_type / is_valid_dist_tag, which already accepts empty strings for the npm parser too — that's a parser-wide pre-existing behavior that should be fixed in a separate PR alongside the equivalent TS change.


Written by an agent (Claude Code, claude-opus-4-7).

@zkochan zkochan merged commit f807f6d into main May 20, 2026
28 checks passed
@zkochan zkochan deleted the feat/pacquet-jsr-resolver branch May 20, 2026 19:16
zkochan added a commit that referenced this pull request May 20, 2026
…solutionId

The jsr resolver tests added in #11772 still read .id.name / .id.suffix
from a PkgNameVer-shaped id; switch them to the new opaque
PkgResolutionId via .id.to_string().
zkochan added a commit that referenced this pull request May 20, 2026
…solutionId

The jsr resolver tests added in #11772 still read .id.name / .id.suffix
from a PkgNameVer-shaped id; switch them to the new opaque
PkgResolutionId via .id.to_string().
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants