feat(pacquet): port JSR specifier parser and wire resolve_jsr#11772
Conversation
…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).
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📜 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)
🧰 Additional context used📓 Path-based instructions (1)pacquet/**/*.rs📄 CodeRabbit inference engine (pacquet/AGENTS.md)
Files:
🔇 Additional comments (3)
📝 WalkthroughWalkthroughA new JSR specifier parser crate is created to parse ChangesJSR Specifier Resolution
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Micro-Benchmark ResultsLinux |
Codecov Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
Integrated-Benchmark Report (Linux)Scenario: Frozen Lockfile
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)
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
]
}
]
} |
Review Summary by QodoPort JSR specifier parser and wire resolve_jsr through npm resolver
WalkthroughsDescription• 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 Diagramflowchart 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
File Changes1. pacquet/crates/resolving-jsr-specifier-parser/src/lib.rs
|
Code Review by Qodo
Context used✅ Compliance rules (platform):
50 rules 1. Invalid JSR selector falls through
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
pacquet/crates/resolving-npm-resolver/src/npm_resolver/tests.rs (1)
295-297: ⚡ Quick winAssert 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
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (10)
Cargo.tomlpacquet/crates/resolving-jsr-specifier-parser/Cargo.tomlpacquet/crates/resolving-jsr-specifier-parser/src/lib.rspacquet/crates/resolving-jsr-specifier-parser/src/tests.rspacquet/crates/resolving-npm-resolver/Cargo.tomlpacquet/crates/resolving-npm-resolver/src/lib.rspacquet/crates/resolving-npm-resolver/src/npm_resolver.rspacquet/crates/resolving-npm-resolver/src/npm_resolver/tests.rspacquet/crates/resolving-npm-resolver/src/parse_bare_specifier.rspacquet/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 firespnpm:<channel>events throughglobalLogger,logger.debug(), orstreamParser.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 plainStringor&str.
If upstream always validates before construction, validate in pacquet's wrapper too. The wrapper must construct only viaTryFrom<String>and/orFromStr. Do not provide an infallible public constructor.
If upstream never validates, just brand for type-safety. Expose an infallibleFrom<String>(andFrom<&str>when convenient).
If upstream occasionally constructs without validation, exposefrom_str_uncheckedas 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 manualimplonly when conversion needs custom logic.
String-literal unions should becomeenums, 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 (&Pathover&PathBuf,&strover&String) when it doesn't force extra copies.
PreferArc::clone(&x)/Rc::clone(&x)overx.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. Writeuse super::{Foo, bar}instead ofuse super::*;. Two forms stay allowed: external-crate preludes likeuse rayon::prelude::*;and root-of-module re-...
Files:
pacquet/crates/resolving-jsr-specifier-parser/src/tests.rspacquet/crates/resolving-npm-resolver/src/lib.rspacquet/crates/resolving-jsr-specifier-parser/src/lib.rspacquet/crates/resolving-npm-resolver/src/parse_bare_specifier.rspacquet/crates/resolving-npm-resolver/src/parse_bare_specifier/tests.rspacquet/crates/resolving-npm-resolver/src/npm_resolver/tests.rspacquet/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
…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).
|
Pushed 49eed65 addressing the review feedback:
Written by an agent (Claude Code, claude-opus-4-7). |
…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().
…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().
Summary
pacquet-resolving-jsr-specifier-parsercrate that ports@pnpm/resolving.jsr-specifier-parser(parseJsrSpecifier+JsrSpec), mirroring upstream'sERR_PNPM_MISSING_JSR_PACKAGE_SCOPE/ERR_PNPM_INVALID_JSR_PACKAGE_NAME/ERR_PNPM_INVALID_JSR_SPECIFIERcodes 1:1.parse_jsr_specifier_to_registry_package_specalongsideparse_bare_specifier, matching upstream's same-file shape inresolving/npm-resolver/src/parseBareSpecifier.ts.jsr:specifiers through a newNpmResolver::resolve_jsr_implthat picks againstregistries['@jsr'](with thehttps://npm.jsr.io/DEFAULT_REGISTRIESfallback) and stampsresolved_via = "jsr-registry"plusalias = spec.jsr_pkg_name, mirroring upstream'sresolveJsr.pick_from_registryhelper so the npm and JSR paths share the picker invocation;build_resolve_resultnow takes aresolved_viaparameter.Out of scope (matches existing pacquet gaps)
calcSpecifier/calcPrefixedSpecifier— no pacquet caller yet, mirrors the npm path's behavior.createResolveLatestclosure — pacquet'sresolve_latest_implis unified and just forwards throughresolve_impl, sojsr: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 ofresolving/jsr-specifier-parser/test/parse.test.ts), 5 new adapter tests inparse_bare_specifier/tests.rs, 3 new integration tests innpm_resolver/tests.rscovering the JSR happy path, default-tag fallback, and parser-error propagation through the resolver.just lintclean (--deny warnings).just checkclean.just fmtclean.just readyend-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