forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1import type { ModuleReplacement } from 'module-replacements'
2
3export interface ReplacementSuggestion {
4 forPackage: string
5 replacement: ModuleReplacement
6}
7
8/**
9 * Replacement types that suggest "no dependency" (can be replaced with native code or inline).
10 */
11const NO_DEP_REPLACEMENT_TYPES = ['native', 'simple'] as const
12
13/**
14 * Replacement types that are informational only.
15 * These suggest alternative packages exist but don't fit the "no dependency" pattern.
16 */
17const INFO_REPLACEMENT_TYPES = ['documented'] as const
18
19/**
20 * Composable for fetching module replacement suggestions for packages in comparison.
21 * Returns replacements split into "no dep" (actionable) and informational categories.
22 */
23export function useCompareReplacements(packageNames: MaybeRefOrGetter<string[]>) {
24 const packages = computed(() => toValue(packageNames))
25
26 // Cache replacement data by package name
27 const replacements = shallowRef(new Map<string, ModuleReplacement | null>())
28 const loading = shallowRef(false)
29
30 // Fetch replacements for all packages
31 async function fetchReplacements(names: string[]) {
32 if (names.length === 0) return
33
34 // Filter out packages we've already checked
35 const namesToCheck = names.filter(name => !replacements.value.has(name))
36 if (namesToCheck.length === 0) return
37
38 loading.value = true
39
40 try {
41 const results = await Promise.all(
42 namesToCheck.map(async name => {
43 try {
44 const replacement = await $fetch<ModuleReplacement | null>(`/api/replacements/${name}`)
45 return { name, replacement }
46 } catch {
47 return { name, replacement: null }
48 }
49 }),
50 )
51
52 const newReplacements = new Map(replacements.value)
53 for (const { name, replacement } of results) {
54 newReplacements.set(name, replacement)
55 }
56 replacements.value = newReplacements
57 } finally {
58 loading.value = false
59 }
60 }
61
62 // Watch for package changes and fetch replacements
63 if (import.meta.client) {
64 watch(
65 packages,
66 newPackages => {
67 fetchReplacements(newPackages)
68 },
69 { immediate: true },
70 )
71 }
72
73 // Build suggestions from replacements
74 const allSuggestions = computed(() => {
75 const result: ReplacementSuggestion[] = []
76
77 for (const pkg of packages.value) {
78 const replacement = replacements.value.get(pkg)
79 if (!replacement) continue
80
81 result.push({ forPackage: pkg, replacement })
82 }
83
84 return result
85 })
86
87 // Suggestions that prompt adding the "no dep" column (native, simple)
88 const noDepSuggestions = computed(() =>
89 allSuggestions.value.filter(s =>
90 (NO_DEP_REPLACEMENT_TYPES as readonly string[]).includes(s.replacement.type),
91 ),
92 )
93
94 // Informational suggestions that don't prompt "no dep" (documented)
95 const infoSuggestions = computed(() =>
96 allSuggestions.value.filter(s =>
97 (INFO_REPLACEMENT_TYPES as readonly string[]).includes(s.replacement.type),
98 ),
99 )
100
101 return {
102 replacements: readonly(replacements),
103 noDepSuggestions: readonly(noDepSuggestions),
104 infoSuggestions: readonly(infoSuggestions),
105 loading: readonly(loading),
106 }
107}