fix(npm-stats): use official npm registry search and fix search UX#845
fix(npm-stats): use official npm registry search and fix search UX#845tannerlinsley merged 1 commit intomainfrom
Conversation
- Swap deprecated api.npms.io for registry.npmjs.org so results reflect current package versions - Always mount Command.List and stabilize the synthetic "Use X" item to stop the input losing focus while typing - Replace custom overlay for the combine-packages modal with @radix-ui/react-dialog to match the rest of the site's modals
✅ Deploy Preview for tanstack ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughWalkthroughThe NPM search component was updated to use a different API endpoint (npmjs.org registry instead of npms.io), simplified query gating logic, removed label-based rendering, and introduced create/select functionality via a sentinel value. The combining package dialog was refactored to use Radix UI Dialog primitives instead of inline markup. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~35 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/npm-stats/PackageSearch.tsx`:
- Around line 53-77: The UI is using debouncedInputValue for create/empty-state
logic which can show stale info; keep the debounce only for the network request
(use debouncedInputValue for useQuery and its enabled flag) but derive UI state
from the live inputValue: replace trimmedInput = debouncedInputValue.trim() with
trimmedInput = inputValue.trim(), compute showCreateItem using trimmedInput and
searchResults (which still come from searchQuery), and compute the
empty-list/“no packages found” condition from inputValue.trim() as well so the
create/empty UI reflects the immediate user input while queries remain
debounced.
- Around line 57-67: The queryFn currently assumes a successful npm registry
response and maps response.json() directly, so HTTP 4xx/5xx responses are
treated as empty results; modify the queryFn used in PackageSearch (the async
fetch block that builds the registry URL) to check response.ok and, if false,
throw an Error including response.status and response.statusText (or
response.text()) so the react-query hook sets searchQuery.isError; then update
the component render logic to detect searchQuery.isError and render an explicit
error state/message instead of falling back to the empty-results path; apply the
same change to the other queryFn instance referenced around lines 129–137.
In `@src/routes/stats/npm/index.tsx`:
- Around line 618-620: The close control using DialogPrimitive.Close that
renders only the X icon is missing an accessible name; update the
DialogPrimitive.Close element to include an accessible label (e.g., add
aria-label="Close dialog") or include visually hidden text next to the X icon so
screen readers get a reliable name—modify the DialogPrimitive.Close usage that
wraps the X component to include the aria-label or hidden label text.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: c2715b81-418c-4aba-a41d-53ed27851f67
📒 Files selected for processing (2)
src/components/npm-stats/PackageSearch.tsxsrc/routes/stats/npm/index.tsx
| const hasUsableQuery = debouncedInputValue.length > 2 | ||
|
|
||
| const searchQuery = useQuery({ | ||
| queryKey: ['npm-search', debouncedInputValue], | ||
| queryFn: async () => { | ||
| if (!debouncedInputValue || debouncedInputValue.length <= 2) | ||
| return [] as Array<NpmSearchResult> | ||
|
|
||
| const response = await fetch( | ||
| `https://api.npms.io/v2/search?q=${encodeURIComponent( | ||
| `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent( | ||
| debouncedInputValue, | ||
| )}&size=10`, | ||
| ) | ||
| const data = (await response.json()) as { | ||
| results: Array<{ package: NpmSearchResult }> | ||
| objects: Array<{ package: NpmSearchResult }> | ||
| } | ||
| return data.results.map((r) => r.package) | ||
| return data.objects.map((r) => r.package) | ||
| }, | ||
| enabled: debouncedInputValue.length > 2, | ||
| enabled: hasUsableQuery, | ||
| placeholderData: keepPreviousData, | ||
| }) | ||
|
|
||
| const results = React.useMemo(() => { | ||
| const hasInputValue = searchQuery.data?.find( | ||
| (d) => d.name === debouncedInputValue, | ||
| ) | ||
|
|
||
| return [ | ||
| ...(hasInputValue | ||
| ? [] | ||
| : [ | ||
| { | ||
| name: debouncedInputValue, | ||
| label: `Use "${debouncedInputValue}"`, | ||
| }, | ||
| ]), | ||
| ...(searchQuery.data ?? []), | ||
| ] | ||
| }, [searchQuery.data, debouncedInputValue]) | ||
|
|
||
| const handleInputChange = (value: string) => { | ||
| setInputValue(value) | ||
| } | ||
| const searchResults = hasUsableQuery ? (searchQuery.data ?? []) : [] | ||
| const trimmedInput = debouncedInputValue.trim() | ||
| const showCreateItem = | ||
| hasUsableQuery && | ||
| trimmedInput.length > 0 && | ||
| !searchResults.some((d) => d.name === trimmedInput) |
There was a problem hiding this comment.
Use the live input for the create item and list state.
trimmedInput comes from the debounced value, so Line 80 can submit the previous term if the user presses Enter before the debounce settles. The same split state also makes Line 133 briefly show “No packages found” right after the third keystroke. Keep the debounce on the request only; derive create/empty-state UI from the current inputValue.
💡 Suggested fix
- const hasUsableQuery = debouncedInputValue.length > 2
+ const trimmedInput = inputValue.trim()
+ const debouncedTrimmedInput = debouncedInputValue.trim()
+ const hasUsableQuery = debouncedTrimmedInput.length > 2
const searchQuery = useQuery({
- queryKey: ['npm-search', debouncedInputValue],
+ queryKey: ['npm-search', debouncedTrimmedInput],
queryFn: async () => {
const response = await fetch(
`https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(
- debouncedInputValue,
+ debouncedTrimmedInput,
)}&size=10`,
)
@@
- const trimmedInput = debouncedInputValue.trim()
+ const isDebouncing = trimmedInput !== debouncedTrimmedInput
const showCreateItem =
- hasUsableQuery &&
+ trimmedInput.length > 2 &&
trimmedInput.length > 0 &&
!searchResults.some((d) => d.name === trimmedInput)
@@
- {inputValue.length < 3 ? (
+ {trimmedInput.length < 3 ? (
<div className="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
Keep typing to search...
</div>
- ) : searchQuery.isLoading ? (
+ ) : isDebouncing || searchQuery.isLoading ? (
<div className="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
Searching...
</div>Also applies to: 80-82, 125-145
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/npm-stats/PackageSearch.tsx` around lines 53 - 77, The UI is
using debouncedInputValue for create/empty-state logic which can show stale
info; keep the debounce only for the network request (use debouncedInputValue
for useQuery and its enabled flag) but derive UI state from the live inputValue:
replace trimmedInput = debouncedInputValue.trim() with trimmedInput =
inputValue.trim(), compute showCreateItem using trimmedInput and searchResults
(which still come from searchQuery), and compute the empty-list/“no packages
found” condition from inputValue.trim() as well so the create/empty UI reflects
the immediate user input while queries remain debounced.
| queryFn: async () => { | ||
| if (!debouncedInputValue || debouncedInputValue.length <= 2) | ||
| return [] as Array<NpmSearchResult> | ||
|
|
||
| const response = await fetch( | ||
| `https://api.npms.io/v2/search?q=${encodeURIComponent( | ||
| `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent( | ||
| debouncedInputValue, | ||
| )}&size=10`, | ||
| ) | ||
| const data = (await response.json()) as { | ||
| results: Array<{ package: NpmSearchResult }> | ||
| objects: Array<{ package: NpmSearchResult }> | ||
| } | ||
| return data.results.map((r) => r.package) | ||
| return data.objects.map((r) => r.package) | ||
| }, |
There was a problem hiding this comment.
Handle registry failures explicitly.
Line 66 assumes a successful npm payload, but 4xx/5xx responses will put the query into an error state. The list then falls through to the empty-results path, which turns outages or rate limits into “No packages found”. Check response.ok and render an error state when searchQuery.isError is true.
🛡️ Suggested fix
const searchQuery = useQuery({
queryKey: ['npm-search', debouncedInputValue],
queryFn: async () => {
const response = await fetch(
`https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(
debouncedInputValue,
)}&size=10`,
)
+ if (!response.ok) {
+ throw new Error(`npm search failed: ${response.status}`)
+ }
const data = (await response.json()) as {
objects: Array<{ package: NpmSearchResult }>
}
- return data.objects.map((r) => r.package)
+ return data.objects?.map((r) => r.package) ?? []
},
@@
- ) : !searchResults.length && !showCreateItem ? (
+ ) : searchQuery.isError ? (
+ <div className="px-3 py-2 text-sm text-red-600 dark:text-red-400">
+ Search failed. Try again.
+ </div>
+ ) : !searchResults.length && !showCreateItem ? (
<div className="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
No packages found
</div>Also applies to: 129-137
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/npm-stats/PackageSearch.tsx` around lines 57 - 67, The queryFn
currently assumes a successful npm registry response and maps response.json()
directly, so HTTP 4xx/5xx responses are treated as empty results; modify the
queryFn used in PackageSearch (the async fetch block that builds the registry
URL) to check response.ok and, if false, throw an Error including
response.status and response.statusText (or response.text()) so the react-query
hook sets searchQuery.isError; then update the component render logic to detect
searchQuery.isError and render an explicit error state/message instead of
falling back to the empty-results path; apply the same change to the other
queryFn instance referenced around lines 129–137.
| <DialogPrimitive.Close className="rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500"> | ||
| <X className="w-4 h-4 sm:w-5 sm:h-5" /> | ||
| </button> | ||
| </DialogPrimitive.Close> |
There was a problem hiding this comment.
Add an accessible name to the close button.
This is an icon-only control right now, so assistive tech does not get a reliable label. Add aria-label="Close dialog" or visually hidden text.
♿ Suggested fix
- <DialogPrimitive.Close className="rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500">
+ <DialogPrimitive.Close
+ aria-label="Close dialog"
+ className="rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500"
+ >
<X className="w-4 h-4 sm:w-5 sm:h-5" />
</DialogPrimitive.Close>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <DialogPrimitive.Close className="rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500"> | |
| <X className="w-4 h-4 sm:w-5 sm:h-5" /> | |
| </button> | |
| </DialogPrimitive.Close> | |
| <DialogPrimitive.Close | |
| aria-label="Close dialog" | |
| className="rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500" | |
| > | |
| <X className="w-4 h-4 sm:w-5 sm:h-5" /> | |
| </DialogPrimitive.Close> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/routes/stats/npm/index.tsx` around lines 618 - 620, The close control
using DialogPrimitive.Close that renders only the X icon is missing an
accessible name; update the DialogPrimitive.Close element to include an
accessible label (e.g., add aria-label="Close dialog") or include visually
hidden text next to the X icon so screen readers get a reliable name—modify the
DialogPrimitive.Close usage that wraps the X component to include the aria-label
or hidden label text.

Summary
api.npms.iowithregistry.npmjs.org/-/v1/searchso the package picker returns current versionsCommand.Listand giving the synthetic "Use X" item a stable key, per cmdk's recommended pattern@radix-ui/react-dialogso it matches the look and a11y of the other modals on the siteTest plan
/stats/npm— type a package name; focus stays in the input throughout typingSummary by CodeRabbit
New Features
Improvements