[READ-ONLY] a fast, modern browser for the npm registry

feat: add module-replacements support first class (#268)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: James Garbutt <43081j@users.noreply.github.com>
Co-authored-by: Daniel Roe <daniel@roe.dev>

authored by

Lino Le Van
autofix-ci[bot]
James Garbutt
Daniel Roe
and committed by
GitHub
cbfc01e1 88509beb

+120 -2
+71
app/components/PackageReplacement.vue
··· 1 + <script setup lang="ts"> 2 + import type { ModuleReplacement } from 'module-replacements' 3 + 4 + const props = defineProps<{ 5 + replacement: ModuleReplacement 6 + }>() 7 + 8 + const { t } = useI18n() 9 + 10 + const message = computed(() => { 11 + switch (props.replacement.type) { 12 + case 'native': 13 + return t('package.replacement.native', { 14 + replacement: props.replacement.replacement, 15 + nodeVersion: props.replacement.nodeVersion, 16 + }) 17 + case 'simple': 18 + return t('package.replacement.simple', { 19 + replacement: props.replacement.replacement, 20 + }) 21 + case 'documented': 22 + return t('package.replacement.documented') 23 + case 'none': 24 + return t('package.replacement.none') 25 + } 26 + }) 27 + 28 + const mdnUrl = computed(() => { 29 + if (props.replacement.type !== 'native' || !props.replacement.mdnPath) return null 30 + return `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/${props.replacement.mdnPath}` 31 + }) 32 + 33 + const docPath = computed(() => { 34 + if (props.replacement.type !== 'documented' || !props.replacement.docPath) return null 35 + return `https://github.com/es-tooling/module-replacements/blob/main/docs/modules/${props.replacement.docPath}.md` 36 + }) 37 + </script> 38 + 39 + <template> 40 + <div 41 + class="border border-amber-600/40 bg-amber-500/10 rounded-lg px-3 py-2 text-base text-amber-700 dark:text-amber-400" 42 + > 43 + <h2 class="font-medium mb-1 flex items-center gap-2"> 44 + <span class="i-carbon-idea w-4 h-4" aria-hidden="true" /> 45 + {{ $t('package.replacement.title') }} 46 + </h2> 47 + <p class="text-sm m-0"> 48 + {{ message }} 49 + <a 50 + v-if="mdnUrl" 51 + :href="mdnUrl" 52 + target="_blank" 53 + rel="noopener noreferrer" 54 + class="inline-flex items-center gap-1 ml-1 underline underline-offset-4 decoration-amber-600/60 dark:decoration-amber-400/50 hover:decoration-fg transition-colors" 55 + > 56 + {{ $t('package.replacement.mdn') }} 57 + <span class="i-carbon-launch w-3 h-3" aria-hidden="true" /> 58 + </a> 59 + <a 60 + v-if="docPath" 61 + :href="docPath" 62 + target="_blank" 63 + rel="noopener noreferrer" 64 + class="inline-flex items-center gap-1 ml-1 underline underline-offset-4 decoration-amber-600/60 dark:decoration-amber-400/50 hover:decoration-fg transition-colors" 65 + > 66 + {{ $t('package.replacement.learn_more') }} 67 + <span class="i-carbon-launch w-3 h-3" aria-hidden="true" /> 68 + </a> 69 + </p> 70 + </div> 71 + </template>
+6
app/composables/useModuleReplacement.ts
··· 1 + import type { ModuleReplacement } from 'module-replacements' 2 + 3 + /** @public */ 4 + export function useModuleReplacement(packageName: MaybeRefOrGetter<string>) { 5 + return useLazyFetch<ModuleReplacement | null>(() => `/api/replacements/${toValue(packageName)}`) 6 + }
+5 -2
app/pages/[...package].vue
··· 66 66 onMounted(() => fetchInstallSize()) 67 67 68 68 const { data: packageAnalysis } = usePackageAnalysis(packageName, requestedVersion) 69 + const { data: moduleReplacement } = useModuleReplacement(packageName) 69 70 70 71 const { data: pkg, status, error } = await usePackage(packageName, requestedVersion) 71 72 const resolvedVersion = computed(() => pkg.value?.resolvedVersion ?? null) ··· 1201 1202 </div> 1202 1203 </section> 1203 1204 1204 - <!-- Vulnerability scan - full width --> 1205 - <div class="area-vulns"> 1205 + <div class="area-vulns space-y-6"> 1206 + <!-- Bad package warning --> 1207 + <PackageReplacement v-if="moduleReplacement" :replacement="moduleReplacement" /> 1208 + <!-- Vulnerability scan --> 1206 1209 <ClientOnly> 1207 1210 <PackageVulnerabilityTree 1208 1211 v-if="displayVersion"
+9
i18n/locales/en.json
··· 116 116 "version": "This version has been deprecated.", 117 117 "no_reason": "No reason provided" 118 118 }, 119 + "replacement": { 120 + "title": "You might not need this dependency.", 121 + "native": "This can be replaced with {replacement}, available since Node {nodeVersion}.", 122 + "simple": "The community has flagged this package as redundant, with the advice: {replacement}.", 123 + "documented": "The community has flagged this package as having more performant alternatives.", 124 + "none": "This package has been flagged as no longer needed, and its functionality is likely available natively in all engines.", 125 + "learn_more": "Learn more", 126 + "mdn": "MDN" 127 + }, 119 128 "stats": { 120 129 "license": "License", 121 130 "deps": "Deps",
+9
lunaria/files/en-US.json
··· 116 116 "version": "This version has been deprecated.", 117 117 "no_reason": "No reason provided" 118 118 }, 119 + "replacement": { 120 + "title": "You might not need this dependency.", 121 + "native": "This can be replaced with {replacement}, available since Node {nodeVersion}.", 122 + "simple": "The community has flagged this package as redundant, with the advice: {replacement}.", 123 + "documented": "The community has flagged this package as having more performant alternatives.", 124 + "none": "This package has been flagged as no longer needed, and its functionality is likely available natively in all engines.", 125 + "learn_more": "Learn more", 126 + "mdn": "MDN" 127 + }, 119 128 "stats": { 120 129 "license": "License", 121 130 "deps": "Deps",
+1
package.json
··· 49 49 "@shikijs/themes": "^3.21.0", 50 50 "@vueuse/core": "^14.1.0", 51 51 "@vueuse/nuxt": "14.1.0", 52 + "module-replacements": "^2.11.0", 52 53 "nuxt": "^4.3.0", 53 54 "nuxt-og-image": "^5.1.13", 54 55 "ohash": "^2.0.11",
+8
pnpm-lock.yaml
··· 71 71 '@vueuse/nuxt': 72 72 specifier: 14.1.0 73 73 version: 14.1.0(magicast@0.5.1)(nuxt@4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.2))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.2(typescript@5.9.3))(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3)) 74 + module-replacements: 75 + specifier: ^2.11.0 76 + version: 2.11.0 74 77 nuxt: 75 78 specifier: ^4.3.0 76 79 version: 4.3.0(@parcel/watcher@2.5.6)(@types/node@24.10.9)(@vue/compiler-sfc@3.5.27)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.42.0(oxlint-tsgolint@0.11.2))(rolldown@1.0.0-rc.1)(rollup@4.57.0)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.2(typescript@5.9.3))(yaml@2.8.2) ··· 6896 6899 6897 6900 mocked-exports@0.1.1: 6898 6901 resolution: {integrity: sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==} 6902 + 6903 + module-replacements@2.11.0: 6904 + resolution: {integrity: sha512-j5sNQm3VCpQQ7nTqGeOZtoJtV3uKERgCBm9QRhmGRiXiqkf7iRFOkfxdJRZWLkqYY8PNf4cDQF/WfXUYLENrRA==} 6899 6905 6900 6906 motion-dom@12.29.2: 6901 6907 resolution: {integrity: sha512-/k+NuycVV8pykxyiTCoFzIVLA95Nb1BFIVvfSu9L50/6K6qNeAYtkxXILy/LRutt7AzaYDc2myj0wkCVVYAPPA==} ··· 16942 16948 ufo: 1.6.3 16943 16949 16944 16950 mocked-exports@0.1.1: {} 16951 + 16952 + module-replacements@2.11.0: {} 16945 16953 16946 16954 motion-dom@12.29.2: 16947 16955 dependencies:
+11
server/api/replacements/[...pkg].get.ts
··· 1 + import { all, type ModuleReplacement } from 'module-replacements' 2 + 3 + const replacementMap = new Map<string, ModuleReplacement>( 4 + all.moduleReplacements.map(r => [r.moduleName, r]), 5 + ) 6 + 7 + export default defineEventHandler((event): ModuleReplacement | null => { 8 + const pkg = getRouterParam(event, 'pkg') 9 + if (!pkg) return null 10 + return replacementMap.get(pkg) ?? null 11 + })