Skip to content

Commit c8fcd11

Browse files
gameromanautofix-ci[bot]ghostdevv43081j
authored
feat: module replacements v3 (#2068)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Willow (GHOST) <git@willow.sh> Co-authored-by: James Garbutt <43081j@users.noreply.github.com>
1 parent 4565f23 commit c8fcd11

47 files changed

Lines changed: 2508 additions & 226 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/components/Compare/ReplacementSuggestion.vue

Lines changed: 63 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<script setup lang="ts">
22
import type { ModuleReplacement } from 'module-replacements'
3+
import { resolveDocUrl } from 'module-replacements'
4+
import { getReplacementDescription, getReplacementNodeVersion } from '~/utils/module-replacements'
35
46
const props = defineProps<{
57
packageName: string
@@ -14,10 +16,13 @@ const emit = defineEmits<{
1416
addNoDep: []
1517
}>()
1618
17-
const docUrl = computed(() => {
18-
if (props.replacement.type !== 'documented' || !props.replacement.docPath) return null
19-
return `https://e18e.dev/docs/replacements/${props.replacement.docPath}.html`
20-
})
19+
const docUrl = computed(() => resolveDocUrl(props.replacement.url))
20+
21+
const nodeVersion = computed(() => getReplacementNodeVersion(props.replacement))
22+
23+
const replacementDescription = useMarkdown(() => ({
24+
text: getReplacementDescription(props.replacement),
25+
}))
2126
</script>
2227

2328
<template>
@@ -28,6 +33,7 @@ const docUrl = computed(() => {
2833
? 'bg-amber-500/10 border border-amber-600/30 text-amber-800 dark:text-amber-400'
2934
: 'bg-blue-500/10 border border-blue-600/30 text-blue-700 dark:text-blue-400'
3035
"
36+
data-testid="replacement-suggestion-card"
3137
>
3238
<span
3339
class="w-4 h-4 flex-shrink-0 mt-0.5"
@@ -36,33 +42,65 @@ const docUrl = computed(() => {
3642
<div class="min-w-0 flex-1">
3743
<p class="font-medium">{{ packageName }}: {{ $t('package.replacement.title') }}</p>
3844
<p class="text-xs mt-0.5 opacity-80">
39-
<template v-if="replacement.type === 'native'">
40-
{{
41-
$t('package.replacement.native', {
42-
replacement: replacement.replacement,
43-
nodeVersion: replacement.nodeVersion,
44-
})
45-
}}
46-
</template>
45+
<i18n-t
46+
v-if="nodeVersion && replacement.type === 'native'"
47+
keypath="package.replacement.native"
48+
scope="global"
49+
>
50+
<template #replacement>
51+
<span v-if="replacementDescription" v-html="replacementDescription" />
52+
<span v-else
53+
><code>{{ replacement.id }}</code></span
54+
>
55+
</template>
56+
<template #nodeVersion>
57+
{{ nodeVersion }}
58+
</template>
59+
</i18n-t>
60+
<i18n-t
61+
v-else-if="replacement.type === 'native'"
62+
keypath="package.replacement.native_no_version"
63+
scope="global"
64+
>
65+
<template #replacement>
66+
<span v-if="replacementDescription" v-html="replacementDescription" />
67+
<span v-else
68+
><code>{{ replacement.id }}</code></span
69+
>
70+
</template>
71+
</i18n-t>
4772
<template v-else-if="replacement.type === 'simple'">
48-
{{
49-
$t('package.replacement.simple', {
50-
replacement: replacement.replacement,
51-
community: $t('package.replacement.community'),
52-
})
53-
}}
73+
<i18n-t keypath="package.replacement.simple">
74+
<template #replacement><span v-html="replacementDescription" /></template>
75+
<template #community>{{ $t('package.replacement.community') }}</template>
76+
</i18n-t>
5477
</template>
55-
<template v-else-if="replacement.type === 'documented'">
56-
{{
57-
$t('package.replacement.documented', {
58-
community: $t('package.replacement.community'),
59-
})
60-
}}
78+
<i18n-t
79+
v-else-if="replacement.type === 'documented'"
80+
keypath="package.replacement.documented"
81+
scope="global"
82+
>
83+
<template #replacement>
84+
<code>{{ replacement.replacementModule }}</code>
85+
</template>
86+
<template #community>
87+
<a
88+
href="https://e18e.dev/docs/replacements/"
89+
target="_blank"
90+
rel="noopener noreferrer"
91+
class="inline-flex items-center gap-1 ms-1 underline underline-offset-4 decoration-amber-600/60 dark:decoration-amber-400/50 hover:decoration-fg transition-colors"
92+
>
93+
{{ $t('package.replacement.community') }}
94+
<span class="i-lucide:external-link w-3 h-3" aria-hidden="true" />
95+
</a>
96+
</template>
97+
</i18n-t>
98+
<template v-else-if="replacement.type === 'removal'">
99+
<span v-html="replacementDescription" />
61100
</template>
62101
</p>
63102
</div>
64103

65-
<!-- No dependency action button -->
66104
<ButtonBase
67105
v-if="variant === 'nodep' && showAction !== false"
68106
size="sm"
Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,67 @@
11
<script setup lang="ts">
2-
import type { ModuleReplacement } from 'module-replacements'
2+
import type { ModuleReplacement, ModuleReplacementMapping } from 'module-replacements'
3+
import { resolveDocUrl } from 'module-replacements'
4+
import { getReplacementDescription, getReplacementNodeVersion } from '~/utils/module-replacements'
35
46
const props = defineProps<{
7+
mapping: ModuleReplacementMapping
58
replacement: ModuleReplacement
69
}>()
710
8-
const mdnUrl = computed(() => {
9-
if (props.replacement.type !== 'native' || !props.replacement.mdnPath) return null
10-
return `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/${props.replacement.mdnPath}`
11-
})
11+
const externalUrl = computed(() => resolveDocUrl(props.mapping.url ?? props.replacement.url))
1212
13-
const docPath = computed(() => {
14-
if (props.replacement.type !== 'documented' || !props.replacement.docPath) return null
15-
return `https://e18e.dev/docs/replacements/${props.replacement.docPath}.html`
16-
})
13+
const nodeVersion = computed(() => getReplacementNodeVersion(props.replacement))
14+
15+
const replacementDescription = useMarkdown(() => ({
16+
text: getReplacementDescription(props.replacement),
17+
}))
1718
</script>
1819

1920
<template>
2021
<div
2122
class="border border-amber-600/40 bg-amber-500/10 rounded-lg px-3 py-2 text-base text-amber-800 dark:text-amber-400"
23+
data-testid="replacement-card"
2224
>
2325
<h2 class="font-medium mb-1 flex items-center gap-2">
2426
<span class="i-lucide:lightbulb w-4 h-4" aria-hidden="true" />
2527
{{ $t('package.replacement.title') }}
2628
</h2>
27-
<p class="text-sm m-0">
29+
<p>
2830
<i18n-t
29-
v-if="replacement.type === 'native'"
31+
v-if="nodeVersion && replacement.type === 'native'"
3032
keypath="package.replacement.native"
3133
scope="global"
3234
>
3335
<template #replacement>
34-
{{ replacement.replacement }}
36+
<span v-if="replacementDescription" v-html="replacementDescription" />
37+
<span v-else
38+
><code>{{ replacement.id }}</code></span
39+
>
3540
</template>
3641
<template #nodeVersion>
37-
{{ replacement.nodeVersion }}
42+
{{ nodeVersion }}
3843
</template>
3944
</i18n-t>
4045
<i18n-t
41-
v-else-if="replacement.type === 'simple'"
42-
keypath="package.replacement.simple"
46+
v-else-if="replacement.type === 'native'"
47+
keypath="package.replacement.native_no_version"
4348
scope="global"
4449
>
45-
<template #community>
46-
<a
47-
href="https://e18e.dev/docs/replacements/"
48-
target="_blank"
49-
rel="noopener noreferrer"
50-
class="inline-flex items-center gap-1 ms-1 underline underline-offset-4 decoration-amber-600/60 dark:decoration-amber-400/50 hover:decoration-fg transition-colors"
51-
>
52-
{{ $t('package.replacement.community') }}
53-
<span class="i-lucide:external-link w-3 h-3" aria-hidden="true" />
54-
</a>
55-
</template>
5650
<template #replacement>
57-
{{ replacement.replacement }}
51+
<span v-if="replacementDescription" v-html="replacementDescription" />
52+
<span v-else
53+
><code>{{ replacement.id }}</code></span
54+
>
5855
</template>
5956
</i18n-t>
6057
<i18n-t
6158
v-else-if="replacement.type === 'documented'"
6259
keypath="package.replacement.documented"
6360
scope="global"
6461
>
62+
<template #replacement>
63+
<code>{{ replacement.replacementModule }}</code>
64+
</template>
6565
<template #community>
6666
<a
6767
href="https://e18e.dev/docs/replacements/"
@@ -74,22 +74,18 @@ const docPath = computed(() => {
7474
</a>
7575
</template>
7676
</i18n-t>
77+
<template v-else-if="replacement.type === 'removal'">
78+
<span v-html="replacementDescription" />
79+
</template>
80+
<template v-else-if="replacement.type === 'simple'">
81+
<span v-html="replacementDescription" />
82+
</template>
7783
<template v-else>
7884
{{ $t('package.replacement.none') }}
7985
</template>
8086
<a
81-
v-if="mdnUrl"
82-
:href="mdnUrl"
83-
target="_blank"
84-
rel="noopener noreferrer"
85-
class="inline-flex items-center gap-1 ms-1 underline underline-offset-4 decoration-amber-600/60 dark:decoration-amber-400/50 hover:decoration-fg transition-colors"
86-
>
87-
{{ $t('package.replacement.mdn') }}
88-
<span class="i-lucide:external-link w-3 h-3" aria-hidden="true" />
89-
</a>
90-
<a
91-
v-if="docPath"
92-
:href="docPath"
87+
v-if="externalUrl"
88+
:href="externalUrl"
9389
target="_blank"
9490
rel="noopener noreferrer"
9591
class="inline-flex items-center gap-1 ms-1 underline underline-offset-4 decoration-amber-600/60 dark:decoration-amber-400/50 hover:decoration-fg transition-colors"
@@ -98,5 +94,11 @@ const docPath = computed(() => {
9894
<span class="i-lucide:external-link w-3 h-3" aria-hidden="true" />
9995
</a>
10096
</p>
97+
<div v-if="replacement.type === 'simple' && replacement.example">
98+
<strong class="block mb-1.5">{{ $t('package.replacement.example') }}</strong>
99+
<pre
100+
class="bg-amber-800/10 dark:bg-amber-950/30 p-2 rounded border border-amber-700/20 overflow-x-auto text-xs font-mono leading-relaxed"
101+
><code>{{ replacement.example }}</code></pre>
102+
</div>
101103
</div>
102104
</template>

app/composables/npm/useReplacementDependencies.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ModuleReplacement } from 'module-replacements'
1+
import type { ModuleReplacement, ModuleReplacementMapping } from 'module-replacements'
22

33
async function fetchReplacements(
44
deps: Record<string, string>,
@@ -8,7 +8,11 @@ async function fetchReplacements(
88
const results = await Promise.all(
99
names.map(async name => {
1010
try {
11-
const replacement = await $fetch<ModuleReplacement | null>(`/api/replacements/${name}`)
11+
const response = await $fetch<{
12+
mapping: ModuleReplacementMapping
13+
replacement: ModuleReplacement
14+
} | null>(`/api/replacements/${name}`)
15+
const replacement = response?.replacement ?? null
1216
return { name, replacement }
1317
} catch {
1418
return { name, replacement: null }

app/composables/useCompareReplacements.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ModuleReplacement } from 'module-replacements'
1+
import type { ModuleReplacement, ModuleReplacementMapping } from 'module-replacements'
22

33
export interface ReplacementSuggestion {
44
forPackage: string
@@ -8,13 +8,17 @@ export interface ReplacementSuggestion {
88
/**
99
* Replacement types that suggest "no dependency" (can be replaced with native code or inline).
1010
*/
11-
const NO_DEP_REPLACEMENT_TYPES = ['native', 'simple'] as const
11+
const NO_DEP_REPLACEMENT_TYPES = [
12+
'native',
13+
'simple',
14+
'removal',
15+
] as const satisfies ModuleReplacement['type'][]
1216

1317
/**
1418
* Replacement types that are informational only.
1519
* These suggest alternative packages exist but don't fit the "no dependency" pattern.
1620
*/
17-
const INFO_REPLACEMENT_TYPES = ['documented'] as const
21+
const INFO_REPLACEMENT_TYPES = ['documented'] as const satisfies ModuleReplacement['type'][]
1822

1923
/**
2024
* Composable for fetching module replacement suggestions for packages in comparison.
@@ -41,8 +45,11 @@ export function useCompareReplacements(packageNames: MaybeRefOrGetter<string[]>)
4145
const results = await Promise.all(
4246
namesToCheck.map(async name => {
4347
try {
44-
const replacement = await $fetch<ModuleReplacement | null>(`/api/replacements/${name}`)
45-
return { name, replacement, failed: false as const }
48+
const result = await $fetch<{
49+
mapping: ModuleReplacementMapping
50+
replacement: ModuleReplacement
51+
} | null>(`/api/replacements/${name}`)
52+
return { name, replacement: result?.replacement ?? null, failed: false as const }
4653
} catch {
4754
return { name, failed: true as const }
4855
}
@@ -100,9 +107,9 @@ export function useCompareReplacements(packageNames: MaybeRefOrGetter<string[]>)
100107
)
101108

102109
return {
103-
replacements: readonly(replacements),
104-
noDepSuggestions: readonly(noDepSuggestions),
105-
infoSuggestions: readonly(infoSuggestions),
110+
replacements,
111+
noDepSuggestions,
112+
infoSuggestions,
106113
loading: readonly(loading),
107114
}
108115
}
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import type { ModuleReplacement } from 'module-replacements'
1+
import type { ModuleReplacement, ModuleReplacementMapping } from 'module-replacements'
22

33
export function useModuleReplacement(packageName: MaybeRefOrGetter<string>) {
4-
return useLazyFetch<ModuleReplacement | null>(() => `/api/replacements/${toValue(packageName)}`)
4+
return useLazyFetch<{ mapping: ModuleReplacementMapping; replacement: ModuleReplacement } | null>(
5+
() => `/api/replacements/${toValue(packageName)}`,
6+
)
57
}

app/pages/package/[[org]]/[name].vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -913,7 +913,11 @@ const showSkeleton = shallowRef(false)
913913

914914
<div class="space-y-6" :class="$style.areaVulns">
915915
<!-- Bad package warning -->
916-
<PackageReplacement v-if="moduleReplacement" :replacement="moduleReplacement" />
916+
<PackageReplacement
917+
v-if="moduleReplacement"
918+
:mapping="moduleReplacement.mapping"
919+
:replacement="moduleReplacement.replacement"
920+
/>
917921
<!-- Size / dependency increase notice -->
918922
<PackageSizeIncrease v-if="sizeDiff" :diff="sizeDiff" />
919923
<!-- Vulnerability scan -->

app/utils/module-replacements.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { ModuleReplacement } from 'module-replacements'
2+
3+
export function getReplacementDescription(replacement: ModuleReplacement): string {
4+
if (replacement.type === 'documented') return ''
5+
return replacement.description ?? ''
6+
}
7+
8+
export function getReplacementNodeVersion(replacement: ModuleReplacement): string | null {
9+
const nodeEngine = replacement.engines?.find(e => e.engine === 'nodejs')
10+
return nodeEngine?.minVersion || null
11+
}

i18n/locales/ar.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,6 @@
178178
"none": "تم وضع علامة على هذه الحزمة بأنها لم تعد مطلوبة، ومن المرجح أن وظيفتها متاحة أصلًا في جميع المحركات.",
179179
"learn_more": "اعرف المزيد",
180180
"learn_more_above": "اعرف المزيد أعلاه.",
181-
"mdn": "MDN",
182181
"community": "المجتمع",
183182
"consider_no_dep": "+ هل تريد خيار «بدون تبعية»؟"
184183
},

i18n/locales/az-AZ.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,6 @@
243243
"none": "Bu paket artıq lazımsız olaraq qeyd edilib və funksionallığı bütün mühərriklərdə yerli olaraq mövcuddur.",
244244
"learn_more": "Ətraflı öyrən",
245245
"learn_more_above": "Yuxarıda ətraflı öyrənin.",
246-
"mdn": "MDN",
247246
"community": "icma",
248247
"consider_no_dep": "+ Asılılıqsız düşünün?"
249248
},

i18n/locales/bg-BG.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,6 @@
206206
"none": "Този пакет е отбелязан като вече ненужен и функционалността му вероятно е налична нативно във всички двигатели.",
207207
"learn_more": "Научете повече",
208208
"learn_more_above": "Научете повече по-горе.",
209-
"mdn": "MDN",
210209
"community": "общност",
211210
"consider_no_dep": "+ Обмислете без зависимости?"
212211
},

0 commit comments

Comments
 (0)