Skip to content

Commit 9505597

Browse files
feat: add toggle to hide platform-specific packages in search (#285)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent b37b3b3 commit 9505597

7 files changed

Lines changed: 289 additions & 5 deletions

File tree

app/composables/useSettings.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@ export interface AppSettings {
1414
includeTypesInInstall: boolean
1515
/** Accent color theme */
1616
accentColorId: AccentColorId | null
17+
/** Hide platform-specific packages (e.g., @scope/pkg-linux-x64) from search results */
18+
hidePlatformPackages: boolean
1719
}
1820

1921
const DEFAULT_SETTINGS: AppSettings = {
2022
relativeDates: false,
2123
includeTypesInInstall: true,
2224
accentColorId: null,
25+
hidePlatformPackages: true,
2326
}
2427

2528
const STORAGE_KEY = 'npmx-settings'

app/pages/search.vue

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { formatNumber } from '#imports'
33
import { debounce } from 'perfect-debounce'
44
import { isValidNewPackageName, checkPackageExists } from '~/utils/package-name'
5+
import { isPlatformSpecificPackage } from '~/utils/platform-packages'
56
67
const route = useRoute()
78
const router = useRouter()
@@ -119,22 +120,37 @@ const rawVisibleResults = computed(() => {
119120
return results.value
120121
})
121122
123+
// Settings for platform package filtering
124+
const { settings } = useSettings()
125+
122126
/**
123-
* Reorder results to put exact package name match at the top
127+
* Reorder results to put exact package name match at the top,
128+
* and optionally filter out platform-specific packages.
124129
*/
125130
const visibleResults = computed(() => {
126131
const raw = rawVisibleResults.value
127132
if (!raw) return raw
128133
134+
let objects = raw.objects
135+
136+
// Filter out platform-specific packages if setting is enabled
137+
if (settings.value.hidePlatformPackages) {
138+
objects = objects.filter(r => !isPlatformSpecificPackage(r.package.name))
139+
}
140+
129141
const q = query.value.trim().toLowerCase()
130-
if (!q) return raw
142+
if (!q) {
143+
return objects === raw.objects ? raw : { ...raw, objects }
144+
}
131145
132146
// Find exact match index
133-
const exactIdx = raw.objects.findIndex(r => r.package.name.toLowerCase() === q)
134-
if (exactIdx <= 0) return raw // Already at top or not found
147+
const exactIdx = objects.findIndex(r => r.package.name.toLowerCase() === q)
148+
if (exactIdx <= 0) {
149+
return objects === raw.objects ? raw : { ...raw, objects }
150+
}
135151
136152
// Move exact match to top
137-
const reordered = [...raw.objects]
153+
const reordered = [...objects]
138154
const [exactMatch] = reordered.splice(exactIdx, 1)
139155
if (exactMatch) {
140156
reordered.unshift(exactMatch)

app/pages/settings.vue

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,43 @@ defineOgImageComponent('Default', {
163163
{{ $t('settings.include_types_description') }}
164164
</p>
165165
</div>
166+
167+
<!-- Divider -->
168+
<div class="border-t border-border" />
169+
170+
<!-- Hide platform-specific packages toggle -->
171+
<div class="space-y-2">
172+
<button
173+
type="button"
174+
class="w-full flex items-center justify-between gap-4 group"
175+
role="switch"
176+
:aria-checked="settings.hidePlatformPackages"
177+
@click="settings.hidePlatformPackages = !settings.hidePlatformPackages"
178+
>
179+
<span class="text-sm text-fg font-medium text-left">
180+
{{ $t('settings.hide_platform_packages') }}
181+
</span>
182+
<span
183+
class="relative inline-flex h-6 w-11 shrink-0 items-center rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out motion-reduce:transition-none shadow-sm cursor-pointer"
184+
:class="
185+
settings.hidePlatformPackages ? 'bg-accent' : 'bg-bg border border-border'
186+
"
187+
aria-hidden="true"
188+
>
189+
<span
190+
class="pointer-events-none inline-block h-5 w-5 rounded-full shadow-sm ring-0 transition-transform duration-200 ease-in-out motion-reduce:transition-none"
191+
:class="
192+
settings.hidePlatformPackages
193+
? 'translate-x-5 bg-bg'
194+
: 'translate-x-0 bg-fg-muted'
195+
"
196+
/>
197+
</span>
198+
</button>
199+
<p class="text-sm text-fg-muted">
200+
{{ $t('settings.hide_platform_packages_description') }}
201+
</p>
202+
</div>
166203
</div>
167204
</section>
168205

app/utils/platform-packages.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Detects if a package name is a platform-specific native binary package.
3+
* These are typically optional dependencies that contain native binaries
4+
* for specific OS/architecture combinations (e.g., @oxlint/win32-x64, esbuild-darwin-arm64).
5+
* Sourced from searches for esbuild, and the napi-rs build triplets support matrix.
6+
*/
7+
8+
const PLATFORMS = new Set([
9+
'win32',
10+
'darwin',
11+
'linux',
12+
'android',
13+
'freebsd',
14+
'openbsd',
15+
'netbsd',
16+
'sunos',
17+
'aix',
18+
])
19+
20+
const ARCHITECTURES = new Set([
21+
'x64',
22+
'arm64',
23+
'arm',
24+
'ia32',
25+
'ppc64',
26+
'ppc64le',
27+
's390x',
28+
'riscv64',
29+
'mips64el',
30+
'loong64',
31+
])
32+
33+
const ABI_SUFFIXES = new Set(['gnu', 'musl', 'msvc', 'gnueabihf'])
34+
35+
/**
36+
* Checks if a package name is a platform-specific native binary package.
37+
* Matches patterns like:
38+
* - @scope/pkg-win32-x64
39+
* - @scope/pkg-linux-arm64-gnu
40+
* - pkg-darwin-arm64
41+
* - @rollup/rollup-linux-x64-musl
42+
*
43+
* @param name - The full package name (including scope if present)
44+
* @returns true if the package appears to be a platform-specific binary
45+
*/
46+
export function isPlatformSpecificPackage(name: string): boolean {
47+
const unscopedName = name.startsWith('@') ? (name.split('/')[1] ?? '') : name
48+
if (!unscopedName) return false
49+
50+
const parts = unscopedName.split('-')
51+
if (parts.length < 2) return false
52+
53+
// Look for OS-arch pattern anywhere in the name as suffix parts
54+
// e.g., "pkg-linux-x64-gnu" -> ["pkg", "linux", "x64", "gnu"]
55+
for (let i = 0; i < parts.length - 1; i++) {
56+
const os = parts[i]
57+
const arch = parts[i + 1]
58+
59+
if (os && arch && PLATFORMS.has(os) && ARCHITECTURES.has(arch)) {
60+
// Optional ABI suffix check (next part if exists)
61+
const abiSuffix = parts[i + 2]
62+
if (abiSuffix && !ABI_SUFFIXES.has(abiSuffix)) {
63+
// NOTE: Has an extra part after arch but it's not a known ABI - might be a false positive??
64+
// but still consider it a match if OS+arch pattern is found at the end
65+
if (i + 2 === parts.length - 1) {
66+
// Extra unknown suffix at the end - be conservative
67+
continue
68+
}
69+
}
70+
return true
71+
}
72+
}
73+
74+
return false
75+
}

i18n/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@
5959
"relative_dates_description": "Show \"3 days ago\" instead of full dates",
6060
"include_types": "Include {'@'}types in install",
6161
"include_types_description": "Add {'@'}types package to install commands for untyped packages",
62+
"hide_platform_packages": "Hide platform-specific packages in search",
63+
"hide_platform_packages_description": "Hide native binary packages like {'@'}esbuild/linux-x64 from results",
6264
"theme": "Theme",
6365
"theme_light": "Light",
6466
"theme_dark": "Dark",

lunaria/files/en-US.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@
5959
"relative_dates_description": "Show \"3 days ago\" instead of full dates",
6060
"include_types": "Include {'@'}types in install",
6161
"include_types_description": "Add {'@'}types package to install commands for untyped packages",
62+
"hide_platform_packages": "Hide platform-specific packages in search",
63+
"hide_platform_packages_description": "Hide native binary packages like {'@'}esbuild/linux-x64 from results",
6264
"theme": "Theme",
6365
"theme_light": "Light",
6466
"theme_dark": "Dark",
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { isPlatformSpecificPackage } from '../../app/utils/platform-packages'
3+
4+
describe('isPlatformSpecificPackage', () => {
5+
describe('standard platform packages', () => {
6+
it.each([
7+
'esbuild-linux-x64',
8+
'esbuild-darwin-arm64',
9+
'esbuild-win32-x64',
10+
'esbuild-win32-ia32',
11+
'esbuild-freebsd-x64',
12+
'esbuild-android-arm64',
13+
])('detects "%s" as platform-specific', name => {
14+
expect(isPlatformSpecificPackage(name)).toBe(true)
15+
})
16+
})
17+
18+
describe('scoped platform packages', () => {
19+
it.each([
20+
'@oxlint/win32-x64',
21+
'@oxlint/linux-arm64',
22+
'@oxlint/darwin-arm64',
23+
'@swc/core-win32-x64-msvc',
24+
'@swc/core-linux-x64-gnu',
25+
'@swc/core-linux-arm64-musl',
26+
'@rollup/rollup-linux-x64-gnu',
27+
'@rollup/rollup-darwin-arm64',
28+
'@rollup/rollup-win32-x64-msvc',
29+
'@esbuild/linux-x64',
30+
'@esbuild/darwin-arm64',
31+
'@esbuild/win32-ia32',
32+
])('detects "%s" as platform-specific', name => {
33+
expect(isPlatformSpecificPackage(name)).toBe(true)
34+
})
35+
})
36+
37+
describe('packages with ABI suffixes', () => {
38+
it.each([
39+
'pkg-linux-x64-gnu',
40+
'pkg-linux-x64-musl',
41+
'pkg-win32-x64-msvc',
42+
'pkg-win32-arm64-msvc',
43+
'pkg-linux-arm-gnueabihf',
44+
])('detects "%s" with ABI suffix as platform-specific', name => {
45+
expect(isPlatformSpecificPackage(name)).toBe(true)
46+
})
47+
})
48+
49+
describe('all platform combinations', () => {
50+
it.each([
51+
// Windows variants
52+
'pkg-win32-x64',
53+
'pkg-win32-arm64',
54+
'pkg-win32-ia32',
55+
// macOS variants
56+
'pkg-darwin-x64',
57+
'pkg-darwin-arm64',
58+
// Linux variants
59+
'pkg-linux-x64',
60+
'pkg-linux-arm64',
61+
'pkg-linux-arm',
62+
'pkg-linux-ia32',
63+
'pkg-linux-ppc64',
64+
'pkg-linux-ppc64le',
65+
'pkg-linux-s390x',
66+
'pkg-linux-riscv64',
67+
'pkg-linux-mips64el',
68+
'pkg-linux-loong64',
69+
// Android
70+
'pkg-android-arm64',
71+
'pkg-android-arm',
72+
'pkg-android-x64',
73+
// BSD variants
74+
'pkg-freebsd-x64',
75+
'pkg-freebsd-arm64',
76+
'pkg-openbsd-x64',
77+
'pkg-netbsd-x64',
78+
// Others
79+
'pkg-sunos-x64',
80+
'pkg-aix-ppc64',
81+
])('detects "%s" as platform-specific', name => {
82+
expect(isPlatformSpecificPackage(name)).toBe(true)
83+
})
84+
})
85+
86+
describe('false positives - should NOT match', () => {
87+
it.each([
88+
'linux-tips',
89+
'node-linux',
90+
'darwin-utils',
91+
'win32-api',
92+
'android-sdk',
93+
'express',
94+
'react',
95+
'vue',
96+
'@types/node',
97+
'@babel/core',
98+
'lodash',
99+
'typescript',
100+
'eslint',
101+
'prettier',
102+
'platform-tools',
103+
'arch-decision-records',
104+
'arm-controller',
105+
'x64-utils',
106+
])('does NOT detect "%s" as platform-specific', name => {
107+
expect(isPlatformSpecificPackage(name)).toBe(false)
108+
})
109+
})
110+
111+
describe('edge cases', () => {
112+
it('returns false for empty string', () => {
113+
expect(isPlatformSpecificPackage('')).toBe(false)
114+
})
115+
116+
it('returns false for scoped package with empty name', () => {
117+
expect(isPlatformSpecificPackage('@scope/')).toBe(false)
118+
})
119+
120+
it('returns false for single-part names', () => {
121+
expect(isPlatformSpecificPackage('linux')).toBe(false)
122+
expect(isPlatformSpecificPackage('x64')).toBe(false)
123+
})
124+
125+
it('returns false for package with only OS, no arch', () => {
126+
expect(isPlatformSpecificPackage('pkg-linux')).toBe(false)
127+
expect(isPlatformSpecificPackage('pkg-darwin')).toBe(false)
128+
expect(isPlatformSpecificPackage('pkg-win32')).toBe(false)
129+
})
130+
131+
it('returns false for package with only arch, no OS', () => {
132+
expect(isPlatformSpecificPackage('pkg-x64')).toBe(false)
133+
expect(isPlatformSpecificPackage('pkg-arm64')).toBe(false)
134+
})
135+
136+
it('is conservative with OS-arch in middle of name followed by unknown suffix', () => {
137+
// These have unknown suffixes after the arch, so we're conservative
138+
expect(isPlatformSpecificPackage('my-linux-x64-bindings')).toBe(false)
139+
expect(isPlatformSpecificPackage('@scope/my-darwin-arm64-lib')).toBe(false)
140+
})
141+
142+
it('is conservative with unknown suffixes at the end', () => {
143+
// Unknown suffix after arch at the very end - should be conservative
144+
expect(isPlatformSpecificPackage('pkg-linux-x64-unknown')).toBe(false)
145+
// But if there are more parts after, still matches
146+
expect(isPlatformSpecificPackage('pkg-linux-x64-foo-bar')).toBe(true)
147+
})
148+
})
149+
})

0 commit comments

Comments
 (0)