Skip to content

fix(purl): handle literal @ in scoped package shorthand#50

Merged
oritwoen merged 1 commit intomainfrom
fix/purl-scoped-package-parsing
Mar 17, 2026
Merged

fix(purl): handle literal @ in scoped package shorthand#50
oritwoen merged 1 commit intomainfrom
fix/purl-scoped-package-parsing

Conversation

@oritwoen
Copy link
Owner

Version extraction in parsePURL used indexOf("@") on the full remainder, so pkg:npm/@vue/core hit the scope @ first and treated @vue/core as the version string. Every CLI command (info, versions, deps, maintainers) threw InvalidPURLError on scoped shorthand that wasn't percent-encoded.

Switched to lastIndexOf("@") with a positional guard - the @ only counts as a version separator if it sits after the last /. Scoped @ always comes before a /, so it's left alone now.

Existing tests still pass (percent-encoded scopes were fine before). Two new tests cover the literal @ path: pkg:npm/@vue/core without version and pkg:npm/@vue/core@3.4.0 with one.

Closes #25

Version extraction used indexOf("@") which grabbed the scope marker
in inputs like pkg:npm/@vue/core, treating @vue/core as the version.

Use lastIndexOf("@") guarded by position after the last "/" so the
scope @ is left alone and only a real version separator gets picked up.

Closes #25
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 17, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d718c768-bfae-49da-93c6-db58b20a285b

📥 Commits

Reviewing files that changed from the base of the PR and between 2850330 and da4d14f.

📒 Files selected for processing (2)
  • src/core/purl.ts
  • test/unit/purl.test.ts
📜 Recent review details
🧰 Additional context used
📓 Path-based instructions (7)
test/**/*.test.ts

📄 CodeRabbit inference engine (test/AGENTS.md)

test/**/*.test.ts: Test files should use *.test.ts naming convention
Vitest globals (describe, it, expect, vi) are enabled and should be used without imports in test files

test/**/*.test.ts: Use Vitest with globals enabled. Use describe, it, expect, vi without imports.
Use vi.hoisted() for module mocks and vi.fn() for spies in tests.

Files:

  • test/unit/purl.test.ts
test/unit/**/*.test.ts

📄 CodeRabbit inference engine (test/AGENTS.md)

Unit tests should rely on mocks/spies rather than external dependencies

Name unit tests as *.test.ts and structure as describe("module")it("should ...").

Files:

  • test/unit/purl.test.ts
test/unit/purl.test.ts

📄 CodeRabbit inference engine (test/AGENTS.md)

PURL contract tests should be located in test/unit/purl.test.ts for parse/build validation

Files:

  • test/unit/purl.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use .ts extensions on all source imports in TypeScript files, enforced by verbatimModuleSyntax.
Use relative paths only for imports, no import aliases.
Enable strict: true in tsconfig. Never use as any, @ts-ignore, or @ts-expect-error workarounds.
Use interface for contracts like Package, Version, Registry. Use type unions for closed sets like VersionStatus, Scope.
Enable noUnusedLocals: true in tsconfig. Remove dead code instead of commenting it out.
Use typed error hierarchy from src/core/errors.ts: throw PkioError, HTTPError, NotFoundError, RateLimitError, UnknownEcosystemError, or InvalidPURLError. Never throw plain Error.

Files:

  • test/unit/purl.test.ts
  • src/core/purl.ts
src/**/*.ts

📄 CodeRabbit inference engine (src/AGENTS.md)

Use .ts import suffixes consistently

Files:

  • src/core/purl.ts
src/**/!(client).ts

📄 CodeRabbit inference engine (src/AGENTS.md)

Do not implement fetch/retry behavior outside src/core/client.ts

Files:

  • src/core/purl.ts
src/core/**/*.ts

📄 CodeRabbit inference engine (src/core/AGENTS.md)

src/core/**/*.ts: Throw typed errors (InvalidPURLError, NotFoundError, RateLimitError) instead of plain Error in core flows
VersionStatus and Scope are closed unions; keep adapter outputs inside allowed values

Files:

  • src/core/purl.ts
🧠 Learnings (12)
📓 Common learnings
Learnt from: CR
Repo: oritwoen/regxa PR: 0
File: src/commands/AGENTS.md:0-0
Timestamp: 2026-03-10T07:36:29.354Z
Learning: Applies to src/commands/**/*.ts : PURL input resolution and optional `pkg:` prefix normalization should be implemented in `src/commands/shared.ts`
Learnt from: CR
Repo: oritwoen/regxa PR: 0
File: src/commands/AGENTS.md:0-0
Timestamp: 2026-03-10T07:36:29.354Z
Learning: Applies to src/commands/**/*.ts : Do not reimplement PURL parsing in individual command files; use the shared implementation from `src/commands/shared.ts`
Learnt from: CR
Repo: oritwoen/regxa PR: 0
File: test/AGENTS.md:0-0
Timestamp: 2026-03-10T07:36:54.862Z
Learning: Applies to test/unit/purl.test.ts : PURL contract tests should be located in `test/unit/purl.test.ts` for parse/build validation
Learnt from: CR
Repo: oritwoen/regxa PR: 0
File: src/core/AGENTS.md:0-0
Timestamp: 2026-03-10T07:36:38.679Z
Learning: No duplicate PURL parsing utilities outside purl.ts
Learnt from: CR
Repo: oritwoen/regxa PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-13T19:49:05.270Z
Learning: Never parse PURLs outside `src/core/purl.ts`. Always use `createFromPURL()` or `parsePURL()`.
Learnt from: CR
Repo: oritwoen/regxa PR: 0
File: src/AGENTS.md:0-0
Timestamp: 2026-03-10T07:36:12.605Z
Learning: Applies to src/{commands,helpers}/**/*.ts : Route all parsing logic through `createFromPURL` instead of duplicating in commands or helpers
Learnt from: CR
Repo: oritwoen/regxa PR: 0
File: src/AGENTS.md:0-0
Timestamp: 2026-03-10T07:36:12.605Z
Learning: Applies to src/helpers.ts : Wrap `createFromPURL` when adding convenience API in `src/helpers.ts` and preserve normalization path
📚 Learning: 2026-03-10T07:36:54.862Z
Learnt from: CR
Repo: oritwoen/regxa PR: 0
File: test/AGENTS.md:0-0
Timestamp: 2026-03-10T07:36:54.862Z
Learning: Applies to test/unit/purl.test.ts : PURL contract tests should be located in `test/unit/purl.test.ts` for parse/build validation

Applied to files:

  • test/unit/purl.test.ts
  • src/core/purl.ts
📚 Learning: 2026-03-10T07:36:38.679Z
Learnt from: CR
Repo: oritwoen/regxa PR: 0
File: src/core/AGENTS.md:0-0
Timestamp: 2026-03-10T07:36:38.679Z
Learning: No duplicate PURL parsing utilities outside purl.ts

Applied to files:

  • test/unit/purl.test.ts
  • src/core/purl.ts
📚 Learning: 2026-03-10T07:36:29.354Z
Learnt from: CR
Repo: oritwoen/regxa PR: 0
File: src/commands/AGENTS.md:0-0
Timestamp: 2026-03-10T07:36:29.354Z
Learning: Applies to src/commands/**/*.ts : PURL input resolution and optional `pkg:` prefix normalization should be implemented in `src/commands/shared.ts`

Applied to files:

  • test/unit/purl.test.ts
  • src/core/purl.ts
📚 Learning: 2026-03-10T07:36:29.354Z
Learnt from: CR
Repo: oritwoen/regxa PR: 0
File: src/commands/AGENTS.md:0-0
Timestamp: 2026-03-10T07:36:29.354Z
Learning: Applies to src/commands/**/*.ts : Do not reimplement PURL parsing in individual command files; use the shared implementation from `src/commands/shared.ts`

Applied to files:

  • test/unit/purl.test.ts
  • src/core/purl.ts
📚 Learning: 2026-03-10T07:36:12.605Z
Learnt from: CR
Repo: oritwoen/regxa PR: 0
File: src/AGENTS.md:0-0
Timestamp: 2026-03-10T07:36:12.605Z
Learning: Applies to src/{commands,helpers}/**/*.ts : Route all parsing logic through `createFromPURL` instead of duplicating in commands or helpers

Applied to files:

  • test/unit/purl.test.ts
  • src/core/purl.ts
📚 Learning: 2026-03-10T07:36:54.862Z
Learnt from: CR
Repo: oritwoen/regxa PR: 0
File: test/AGENTS.md:0-0
Timestamp: 2026-03-10T07:36:54.862Z
Learning: Applies to test/unit/{license,repository}.test.ts : Normalization tests should be located in `test/unit/license.test.ts` and `test/unit/repository.test.ts` for canonical output normalization

Applied to files:

  • test/unit/purl.test.ts
📚 Learning: 2026-03-13T19:49:05.270Z
Learnt from: CR
Repo: oritwoen/regxa PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-13T19:49:05.270Z
Learning: Never parse PURLs outside `src/core/purl.ts`. Always use `createFromPURL()` or `parsePURL()`.

Applied to files:

  • test/unit/purl.test.ts
  • src/core/purl.ts
📚 Learning: 2026-03-10T07:36:12.605Z
Learnt from: CR
Repo: oritwoen/regxa PR: 0
File: src/AGENTS.md:0-0
Timestamp: 2026-03-10T07:36:12.605Z
Learning: Applies to src/helpers.ts : Wrap `createFromPURL` when adding convenience API in `src/helpers.ts` and preserve normalization path

Applied to files:

  • test/unit/purl.test.ts
  • src/core/purl.ts
📚 Learning: 2026-03-10T07:36:54.862Z
Learnt from: CR
Repo: oritwoen/regxa PR: 0
File: test/AGENTS.md:0-0
Timestamp: 2026-03-10T07:36:54.862Z
Learning: Applies to test/unit/{lockfile,cached-registry}.test.ts : Cache behavior tests should be located in `test/unit/lockfile.test.ts` and `test/unit/cached-registry.test.ts` for freshness, TTL, integrity, and wrapper behavior validation

Applied to files:

  • test/unit/purl.test.ts
📚 Learning: 2026-03-13T19:49:05.270Z
Learnt from: CR
Repo: oritwoen/regxa PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-13T19:49:05.270Z
Learning: Applies to src/index.ts : Export public API additions from `src/index.ts` barrel export.

Applied to files:

  • test/unit/purl.test.ts
📚 Learning: 2026-03-17T10:54:06.865Z
Learnt from: oritwoen
Repo: oritwoen/regxa PR: 43
File: test/unit/license.test.ts:147-147
Timestamp: 2026-03-17T10:54:06.865Z
Learning: In test/unit/**/*.test.ts in the oritwoen/regxa repository, reviewer guidance should NOT flag missing a 'should' prefix for it(...) descriptions. The codebase uses imperative/descriptive phrasing (e.g., 'returns empty string for empty array', 'combines two licenses with OR', 'filters out null/undefined values'). Apply this pattern broadly to test files in test/unit, and document this as a project convention to avoid false positives during reviews.

Applied to files:

  • test/unit/purl.test.ts
🧬 Code graph analysis (1)
test/unit/purl.test.ts (1)
src/core/purl.ts (1)
  • parsePURL (22-111)
🔇 Additional comments (3)
src/core/purl.ts (1)

58-66: Version delimiter handling is fixed for scoped shorthand.

This closes the exact failure path where a scope marker was misread as a version separator and broke parsing for inputs like pkg:npm/@vue/core.

test/unit/purl.test.ts (2)

30-40: Regression test covers literal @ scoped path without version.

Good guard for the old bug where this input could end up with an invalid parsed name/version split.

Based on learnings: PURL contract tests should be located in test/unit/purl.test.ts for parse/build validation.


42-52: Versioned scoped shorthand case is covered.

This validates that version parsing still works when a literal scope @ exists earlier in the path.


📝 Walkthrough

Walkthrough

Fixed PURL parsing for npm scoped packages by refining version extraction to find the last @ occurring after the last / in the remainder, preventing misdetection of version delimiters in scope names like @vue. Covers fix with new test cases for scoped packages with and without versions.

Changes

Cohort / File(s) Summary
Version Extraction Logic
src/core/purl.ts
Refines condition for version extraction: now checks if the last @ appears after the last / before treating it as a version delimiter. Prevents @ in scoped package names (e.g., @vue/core) from being misidentified as version boundaries.
Scoped Package Test Coverage
test/unit/purl.test.ts
Added two test cases: parsing pkg:npm/@vue/core (namespace "@VUE", no version) and pkg:npm/@vue/core@3.4.0 (namespace "@VUE", version "3.4.0"). Validates fix handles scoped names with literal @ characters correctly.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested labels

bug

Suggested reviewers

  • aeitwoen
  • cubic-dev-ai

Poem

A scoped package walks into a parser,
The @ gets mistaken for something sharper,
Last after slash—that's the real divider,
@vue/core flows smoother now, glider! 📦✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the core fix: handling literal @ symbols in scoped package names, which is exactly what the changeset addresses.
Description check ✅ Passed The description explains the root cause (indexOf hitting scope @ first), the fix (lastIndexOf with positional guard), and test coverage, all directly relevant to the changeset.
Linked Issues check ✅ Passed The PR fully addresses issue #25 by switching from indexOf to lastIndexOf with a positional guard so scoped @ (before /) is ignored and only @ after / counts as version separator.
Out of Scope Changes check ✅ Passed Changes are confined to the purl parser logic and corresponding test coverage for scoped packages, both directly required by issue #25.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/purl-scoped-package-parsing
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/purl-scoped-package-parsing
📝 Coding Plan
  • Generate coding plan for human review comments

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

@coderabbitai coderabbitai bot added the bug Something isn't working label Mar 17, 2026
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 2 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: This PR modifies core parsing logic in a core utility. Changes to business logic or string parsing in core paths require human review to prevent regressions.

Architecture diagram
sequenceDiagram
    participant Client as CLI (info/deps/versions)
    participant Parser as parsePURL()
    participant Decoder as decodePURLComponent()

    Note over Client,Parser: User provides PURL (e.g., "pkg:npm/@vue/core@3.4.0")

    Client->>Parser: parsePURL(purlStr)

    Note over Parser: Step 1: Strip 'pkg:' prefix

    rect fontcolor:#ffffff,fill:#172554
        Note over Parser: NEW: Version Extraction Logic
        Parser->>Parser: Find last '/' position
        Parser->>Parser: Find last '@' position
        
        alt last '@' exists AND last '@' > last '/'
            Parser->>Decoder: decodePURLComponent(versionPart)
            Decoder-->>Parser: decoded version
        else last '@' is before '/' or missing
            Note over Parser: Treat '@' as part of scope/name
            Parser->>Parser: version = ""
        end
    end

    Parser->>Parser: Extract package type (e.g., 'npm')
    Parser->>Parser: Extract namespace (e.g., '@vue') and name ('core')

    alt Parsing Successful
        Parser-->>Client: Return ParsedPURL object
    else Parsing Failed (Invalid format)
        Parser-->>Client: Throw InvalidPURLError
    end

    Note over Client: Command proceeds with parsed metadata
Loading

@oritwoen oritwoen merged commit 05ac697 into main Mar 17, 2026
3 checks passed
@oritwoen oritwoen deleted the fix/purl-scoped-package-parsing branch March 17, 2026 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix PURL parsing for npm scoped packages

1 participant