Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
5ee3927
Update module-replacements dependency to 3.0.0-beta.0
gameroman Mar 13, 2026
49dd770
Update [...pkg].get.ts
gameroman Mar 13, 2026
f276037
wip
gameroman Mar 13, 2026
e528c90
use a simpler approach
gameroman Mar 13, 2026
ad27978
revert
gameroman Mar 13, 2026
59ac18c
wip
gameroman Mar 13, 2026
6422b11
wip
gameroman Mar 13, 2026
3ecebf0
Update use-replacement-dependencies.spec.ts
gameroman Mar 13, 2026
7d4198b
Update use-replacement-dependencies.spec.ts
gameroman Mar 13, 2026
1e94e2d
Update use-replacement-dependencies.spec.ts
gameroman Mar 13, 2026
7ae1a63
Update use-replacement-dependencies.spec.ts
gameroman Mar 13, 2026
8e8882b
Update Replacement.vue
gameroman Mar 13, 2026
1c72e05
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 13, 2026
a36b4ce
Update Replacement.vue
gameroman Mar 13, 2026
2464a08
Update Replacement.vue
gameroman Mar 13, 2026
d6f4b0b
update tests
gameroman Mar 13, 2026
83102e3
Update use-compare-replacements.spec.ts
gameroman Mar 13, 2026
363aa2a
Update use-compare-replacements.spec.ts
gameroman Mar 13, 2026
b30c0fd
Update use-compare-replacements.spec.ts
gameroman Mar 13, 2026
c1f52f2
Update use-compare-replacements.spec.ts
gameroman Mar 13, 2026
e346d83
Update a11y.spec.ts
gameroman Mar 13, 2026
f89546e
wip
gameroman Mar 14, 2026
74eee66
Revert "wip"
gameroman Mar 14, 2026
a95b7a0
Update ReplacementSuggestion.vue
gameroman Mar 14, 2026
b60b234
Update ReplacementSuggestion.vue
gameroman Mar 14, 2026
7207e50
Update en.json
gameroman Mar 14, 2026
9e742ab
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 14, 2026
e365d7e
Update schema.json
gameroman Mar 14, 2026
63417bf
create resolveReplacementUrl util
gameroman Mar 14, 2026
93babd2
wip
gameroman Mar 14, 2026
03584bd
don't render 'unknown'
gameroman Mar 14, 2026
375bd0a
fix
gameroman Mar 14, 2026
7ad1a05
Update Replacement.vue
gameroman Mar 14, 2026
bfd7ce1
Update schema.json
gameroman Mar 14, 2026
2c69f4e
Update useCompareReplacements.ts
gameroman Mar 14, 2026
e16308c
Update useCompareReplacements.ts
gameroman Mar 14, 2026
01d39ff
Merge branch 'main' into module-replacements-v3
gameroman Mar 16, 2026
53aeaf6
fix: use url from mapping when provided
gameroman Mar 16, 2026
3eeb9cf
fix: add missing imports
gameroman Mar 16, 2026
0eda816
fix: update tests
gameroman Mar 16, 2026
07bd1ba
fix: correctly format files
gameroman Mar 16, 2026
25f99e6
fix
gameroman Mar 16, 2026
55abc67
tests: add package replacement tests
gameroman Mar 16, 2026
fcd16fb
Merge branch 'main' into module-replacements-v3
gameroman Mar 18, 2026
c4ef9dd
fix: resolve git confilcts
gameroman Mar 18, 2026
f6161b8
Merge branch 'main' into module-replacements-v3
gameroman Mar 18, 2026
6133408
Merge branch 'main' into module-replacements-v3
gameroman Mar 24, 2026
0df272e
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 24, 2026
227f7eb
Merge branch 'main' into module-replacements-v3
gameroman Mar 25, 2026
b86fbff
Merge branch 'main' into module-replacements-v3
gameroman Mar 25, 2026
4041dbd
update module-replacements
gameroman Mar 25, 2026
65d1a87
Use resolveDocUrl
gameroman Mar 25, 2026
bcf3ed9
3.0.0-beta.4
gameroman Mar 25, 2026
d1ca961
Merge branch 'main' into module-replacements-v3
gameroman Mar 25, 2026
2f6de9e
revert incorrect i18n changes
gameroman Mar 26, 2026
503f2ed
fix
gameroman Mar 26, 2026
ac522de
fix
gameroman Mar 26, 2026
96accb3
Merge branch 'main' into module-replacements-v3
gameroman Mar 26, 2026
46af065
use markdown
gameroman Mar 26, 2026
12e1b56
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 26, 2026
fbed36b
revert markdown
gameroman Mar 26, 2026
2782dc7
fix the bug and add e2e test
gameroman Mar 26, 2026
1e5d9e3
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 26, 2026
f6838c5
remove double period
gameroman Mar 26, 2026
f44bec6
do test differently
gameroman Mar 26, 2026
3f1fb7e
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 26, 2026
b9ddce5
wip
gameroman Mar 26, 2026
2257ca3
update test
gameroman Mar 26, 2026
d80c709
use markdown
gameroman Mar 26, 2026
b0ef6d9
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 26, 2026
e4bcf9e
add data-testid
gameroman Mar 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions app/components/Compare/ReplacementSuggestion.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { ModuleReplacement } from 'module-replacements'
import { resolveDocUrl } from 'module-replacements'

const props = defineProps<{
packageName: string
Expand All @@ -14,9 +15,11 @@ const emit = defineEmits<{
addNoDep: []
}>()

const docUrl = computed(() => {
if (props.replacement.type !== 'documented' || !props.replacement.docPath) return null
return `https://e18e.dev/docs/replacements/${props.replacement.docPath}.html`
const docUrl = computed(() => resolveDocUrl(props.replacement.url))

const nodeVersion = computed(() => {
const nodeEngine = props.replacement.engines?.find(e => e.engine === 'nodejs')
return nodeEngine?.minVersion || null
})
</script>

Expand All @@ -28,6 +31,7 @@ const docUrl = computed(() => {
? 'bg-amber-500/10 border border-amber-600/30 text-amber-800 dark:text-amber-400'
: 'bg-blue-500/10 border border-blue-600/30 text-blue-700 dark:text-blue-400'
"
data-testid="replacement-suggestion-card"
>
<span
class="w-4 h-4 flex-shrink-0 mt-0.5"
Expand All @@ -38,31 +42,38 @@ const docUrl = computed(() => {
<p class="text-xs mt-0.5 opacity-80">
<template v-if="replacement.type === 'native'">
{{
$t('package.replacement.native', {
replacement: replacement.replacement,
nodeVersion: replacement.nodeVersion,
})
nodeVersion
? $t('package.replacement.native', {
replacement: replacement.description || replacement.id,
nodeVersion,
})
: $t('package.replacement.native_no_version', {
replacement: replacement.description || replacement.id,
})
}}
</template>
<template v-else-if="replacement.type === 'simple'">
{{
$t('package.replacement.simple', {
replacement: replacement.replacement,
replacement: replacement.description,
community: $t('package.replacement.community'),
})
}}
</template>
<template v-else-if="replacement.type === 'documented'">
{{
$t('package.replacement.documented', {
replacement: replacement.replacementModule,
community: $t('package.replacement.community'),
})
}}
</template>
<template v-else-if="replacement.type === 'removal'">
{{ replacement.description }}
</template>
Comment on lines 56 to +73
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should run these through a markdown render since es-tooling/module-replacements#477 means that code parts of the description can be rendered nicely

</p>
</div>

<!-- No dependency action button -->
<ButtonBase
v-if="variant === 'nodep' && showAction !== false"
size="sm"
Expand Down
163 changes: 82 additions & 81 deletions app/components/Package/Replacement.vue
Original file line number Diff line number Diff line change
@@ -1,102 +1,103 @@
<script setup lang="ts">
import type { ModuleReplacement } from 'module-replacements'
import type { ModuleReplacement, ModuleReplacementMapping } from 'module-replacements'
import { resolveDocUrl } from 'module-replacements'

const props = defineProps<{
mapping: ModuleReplacementMapping
replacement: ModuleReplacement
}>()

const mdnUrl = computed(() => {
if (props.replacement.type !== 'native' || !props.replacement.mdnPath) return null
return `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/${props.replacement.mdnPath}`
})
const externalUrl = computed(() => resolveDocUrl(props.mapping.url ?? props.replacement.url))

const docPath = computed(() => {
if (props.replacement.type !== 'documented' || !props.replacement.docPath) return null
return `https://e18e.dev/docs/replacements/${props.replacement.docPath}.html`
const nodeVersion = computed(() => {
const nodeEngine = props.replacement.engines?.find(e => e.engine === 'nodejs')
return nodeEngine?.minVersion || null
})

const replacementDescription =
props.replacement.type === 'documented'
? null
: useMarkdown(() => ({
text: props.replacement.description ?? '',

Check failure on line 21 in app/components/Package/Replacement.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

Property 'description' does not exist on type 'ModuleReplacement'.
}))
</script>

<template>
<div
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"
data-testid="replacement-card"
>
<h2 class="font-medium mb-1 flex items-center gap-2">
<span class="i-lucide:lightbulb w-4 h-4" aria-hidden="true" />
{{ $t('package.replacement.title') }}
</h2>
<p class="text-sm m-0">
<i18n-t
v-if="replacement.type === 'native'"
keypath="package.replacement.native"
scope="global"
>
<template #replacement>
{{ replacement.replacement }}
</template>
<template #nodeVersion>
{{ replacement.nodeVersion }}
</template>
</i18n-t>
<i18n-t
v-else-if="replacement.type === 'simple'"
keypath="package.replacement.simple"
scope="global"
>
<template #community>
<a
href="https://e18e.dev/docs/replacements/"
target="_blank"
rel="noopener noreferrer"
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"
>
{{ $t('package.replacement.community') }}
<span class="i-lucide:external-link w-3 h-3" aria-hidden="true" />
</a>
</template>
<template #replacement>
{{ replacement.replacement }}
</template>
</i18n-t>
<i18n-t
v-else-if="replacement.type === 'documented'"
keypath="package.replacement.documented"
scope="global"
>
<template #community>
<a
href="https://e18e.dev/docs/replacements/"
target="_blank"
rel="noopener noreferrer"
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"
>
{{ $t('package.replacement.community') }}
<span class="i-lucide:external-link w-3 h-3" aria-hidden="true" />
</a>
</template>
</i18n-t>
<template v-else>
{{ $t('package.replacement.none') }}
<i18n-t
v-if="nodeVersion && replacement.type === 'native'"
keypath="package.replacement.native"
scope="global"
>
<template #replacement>
<span v-if="replacementDescription" v-html="replacementDescription" />
<span v-else>{{ replacement.id }}</span>
</template>
<template #nodeVersion>
{{ nodeVersion }}
</template>
</i18n-t>
<i18n-t
v-else-if="replacement.type === 'native'"
keypath="package.replacement.native_no_version"
scope="global"
>
<template #replacement>
<span v-if="replacementDescription" v-html="replacementDescription" />
<span v-else>{{ replacement.id }}</span>
</template>
</i18n-t>
<div v-else-if="replacement.type === 'simple'" class="block">
<div class="mb-2" v-html="replacementDescription" />
<div v-if="replacement.example">
<strong class="block mb-1.5">{{ $t('package.replacement.example') }}</strong>
<pre
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"
><code>{{ replacement.example }}</code></pre>
</div>
</div>
<i18n-t
v-else-if="replacement.type === 'documented'"
keypath="package.replacement.documented"
scope="global"
>
<template #replacement>
<code>{{ replacement.replacementModule }}</code>
</template>
<template #community>
<a
href="https://e18e.dev/docs/replacements/"
target="_blank"
rel="noopener noreferrer"
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"
>
{{ $t('package.replacement.community') }}
<span class="i-lucide:external-link w-3 h-3" aria-hidden="true" />
</a>
</template>
<a
v-if="mdnUrl"
:href="mdnUrl"
target="_blank"
rel="noopener noreferrer"
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"
>
{{ $t('package.replacement.mdn') }}
<span class="i-lucide:external-link w-3 h-3" aria-hidden="true" />
</a>
<a
v-if="docPath"
:href="docPath"
target="_blank"
rel="noopener noreferrer"
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"
>
{{ $t('package.replacement.learn_more') }}
<span class="i-lucide:external-link w-3 h-3" aria-hidden="true" />
</a>
</p>
</i18n-t>
<template v-else-if="replacement.type === 'removal'">
<span v-html="replacementDescription" />
</template>
<template v-else>
{{ $t('package.replacement.none') }}
Comment on lines +39 to +90
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai How do I make replacement.description render with markdown and what e2e test should I add?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for markdown rendering libraries in package.json
cat package.json | grep -i "mark\|prose\|remark\|mdx\|shiki\|highlight"

echo "---"
# Check for existing markdown rendering in Vue components
rg -n "v-html\|marked\|markdown\|prose\|remark\|mdx" --type=vue -l

echo "---"
# Look at how markdown is rendered in any existing component
rg -n "v-html\|marked\|markdown" --type=vue -A2 -B2 | head -80

echo "---"
# Check e2e test setup
fd -e spec.ts -e test.ts | head -30

echo "---"
# Check for playwright or cypress config
fd "playwright\|cypress\|e2e" --type f | head -20

echo "---"
# Check nuxt test utils or playwright usage
rg -n "test\|describe\|it\(" test/nuxt/a11y.spec.ts | head -30

echo "---"
# Check the a11y spec for replacement-related tests
grep -n "replacement\|Replacement" test/nuxt/a11y.spec.ts | head -30

Length of output: 3025


🏁 Script executed:

#!/bin/bash
# Find how marked is used in the project
rg -n "marked\|useMarkdown\|renderMarkdown" --type=ts -l
echo "---"
rg -n "marked\|useMarkdown\|renderMarkdown" -g "*.vue" -l
echo "---"
# Look at existing markdown usage
rg -n "marked\|v-html" -g "*.ts" -A3 -B1 | head -60
echo "---"
# Check composables for markdown
fd -e ts composables | xargs grep -l "marked\|markdown" 2>/dev/null | head -10
echo "---"
# Look at existing e2e package-replacements spec
cat test/e2e/package-replacements.spec.ts
echo "---"
# Look at a11y spec for PackageReplacement (around line 2518)
sed -n '2518,2620p' test/nuxt/a11y.spec.ts

Length of output: 4012


🏁 Script executed:

#!/bin/bash
# Search for marked usage across all files
rg -n "from 'marked'\|import marked\|useMarked" -A2 | head -50
echo "---"
# List utils files
fd . app/utils --type f | head -30
echo "---"
# Check if there's a markdown composable
fd . app/composables --type f | xargs grep -l "marked\|markdown\|html" 2>/dev/null | head -10
echo "---"
# Check the resolve-replacement-url util
cat app/utils/resolve-replacement-url.ts
echo "---"
# Check how v-html is used in the project (broader search)
rg -n "v-html" -g "*.vue" | head -30
echo "---"
# Check how the Readme component renders HTML (it uses html prop in a11y test)
fd "Readme" app/components --type f | head -5

Length of output: 3240


🏁 Script executed:

#!/bin/bash
# Examine the useMarkdown composable
cat app/composables/useMarkdown.ts
echo "---"
# Check how Card.vue uses pkgDescription (likely markdown)
sed -n '1,100p' app/components/Package/Card.vue
echo "---"
# Check if there's DOMPurify or sanitization
rg -n "DOMPurify\|sanitize\|xss" -g "*.ts" | head -20
rg -n "DOMPurify\|sanitize\|xss" -g "*.vue" | head -20

Length of output: 7060


@gameroman, here's how to approach both questions.


1. Rendering replacement.description as Markdown

The project already has a useMarkdown composable (app/composables/useMarkdown.ts) that handles inline markdown (bold, italic, inline code, links, strikethrough) with built-in XSS sanitization via stripAndEscapeHtml. This is the right tool to use.

Script changes

Add one computed ref in the <script setup>:

import { resolveReplacementUrl } from '~/utils/resolve-replacement-url'
// ^ already present

const descriptionHtml = useMarkdown(() => ({
  text: props.replacement.description ?? '',
}))

Template changes

simple type — replace the plain <div> with v-html:

- <div class="mb-2">{{ replacement.description }}</div>
+ <!-- eslint-disable-next-line vue/no-v-html -->
+ <div class="mb-2" v-html="descriptionHtml" />

removal type — same pattern:

 <template v-else-if="replacement.type === 'removal'">
-  {{ replacement.description }}
+  <!-- eslint-disable-next-line vue/no-v-html -->
+  <span v-html="descriptionHtml" />
 </template>

native type — the description there is used as a code identifier label inside <code>, so markdown rendering is not meaningful. Leave it as plain text interpolation as-is.

Note: The useMarkdown composable strips all raw HTML tags first, then applies its own safe inline-markdown transformations, so using v-html on its output is safe here — consistent with the pattern in Card.vue (pkgDescription).


2. E2e Tests to Add

test/e2e/package-replacements.spec.ts

Add a test for a simple replacement (description rendered as HTML) and a native replacement (node version shown):

test('/package/is-even shows a simple replacement with a description', async ({
  page,
  goto,
}) => {
  await goto('/package/is-even', { waitUntil: 'hydration' })
  await expect(page.locator('h1')).toContainText('is-even', { timeout: 15_000 })

  // The replacement section should render the plain-text/markdown description
  const replacementSection = page.locator('[data-testid="replacement"]') // or adjust selector to match your component
  await expect(replacementSection).toBeVisible({ timeout: 15_000 })
  await expect(replacementSection).toContainText('modulo')
})

test('/package/array-every shows a native replacement with node version', async ({
  page,
  goto,
}) => {
  await goto('/package/array-every', { waitUntil: 'hydration' })
  await expect(page.locator('h1')).toContainText('array-every', { timeout: 15_000 })

  // Native replacement with nodeVersion should render the i18n "native" message
  const replacementSection = page.locator('.bg-amber-500\\/10')
  await expect(replacementSection).toBeVisible({ timeout: 15_000 })
  await expect(replacementSection).toContainText('0.10.0')
})

Adjust the selectors to match your actual DOM — you may want to add a data-testid="replacement-card" attribute to the root <div> in Replacement.vue to make selection robust.

test/nuxt/a11y.spec.ts

Add a removal type case to the existing PackageReplacement describe block (after the existing simple test):

it('should have no accessibility violations for removal replacement', async () => {
  const component = await mountSuspended(PackageReplacement, {
    props: {
      mapping: {
        type: 'module',
        moduleName: 'some-deprecated-pkg',
        replacements: ['snippet::removal-example'],
      },
      replacement: {
        id: 'snippet::removal-example',
        type: 'removal',
        description: 'This package is no longer needed. Use **native** APIs instead.',
      },
    },
  })
  const results = await runAxe(component)
  expect(results.violations).toEqual([])
})

This also doubles as a smoke test that markdown in description (the **native**) renders without breaking accessibility.


🧠 Learnings used
Learnt from: alexdln
Repo: npmx-dev/npmx.dev PR: 838
File: app/pages/package/[...package].vue:445-449
Timestamp: 2026-02-03T13:59:33.392Z
Learning: The copy button pattern in app/pages/package/[...package].vue may be made into a reusable component or pattern in the future, but currently it's acceptable to keep it inline with the CSS-only approach for smooth animations.

Learnt from: jellydeck
Repo: npmx-dev/npmx.dev PR: 904
File: app/components/Package/AccessControls.vue:253-253
Timestamp: 2026-02-04T05:34:20.527Z
Learning: In the npmx.dev project, ensure that focus-visible styling for button and select elements is implemented globally in app/assets/main.css with the rule: button:focus-visible, select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }. Do not apply per-element inline utility classes like focus-visible:outline-accent/70 on these elements in Vue templates or components (e.g., AccessControls.vue). Rely on the global rule for consistency and maintainability; only use inline focus-visible utilities when styling non-button/select elements or in exceptional cases outside the global scope.

Learnt from: jellydeck
Repo: npmx-dev/npmx.dev PR: 904
File: app/components/Package/Versions.vue:332-332
Timestamp: 2026-02-04T05:34:54.335Z
Learning: In the npmx.dev project, focus-visible styling for buttons and selects is applied globally via main.css with: button:focus-visible, select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }. Therefore, individual buttons or selects in Vue components should not rely on inline focus-visible utility classes like focus-visible:outline-accent/70. Ensure components do not add per-element focus-visible utilities; rely on the global rule. If a component seems to require a different focus state, adjust only through global CSS or maintain accessibility with native focus styles, rather than inserting inline utility classes.

Learnt from: userquin
Repo: npmx-dev/npmx.dev PR: 1170
File: app/components/Package/MetricsBadges.vue:66-66
Timestamp: 2026-02-08T15:02:02.232Z
Learning: In Vue components that use UnoCSS with the preset-icons collection, prefer colon-syntax for icons (e.g., i-carbon:checkmark) over the dash-separated form (i-carbon-checkmark). This aids UnoCSS in resolving the correct collection directly, which improves performance for long icon names. Apply this pattern to all Vue components (e.g., app/components/**/*.vue) where UnoCSS icons are used; ensure UnoCSS is configured with the preset-icons collection.

Learnt from: userquin
Repo: npmx-dev/npmx.dev PR: 1335
File: app/components/Compare/FacetSelector.vue:72-78
Timestamp: 2026-02-10T15:47:33.467Z
Learning: In the npmx.dev project, ButtonBase (used via app/components/ButtonBase.vue or similar) provides default classes: border border-border. When styling ButtonBase instances in Vue components (e.g., app/components/Compare/FacetSelector.vue and other files under app/components), avoid duplicating the border class to prevent the HTML validator error no-dup-class and CI failures. If styling overrides are needed, ensure only unique classes are applied (remove redundant border classes or adjust via props) so the default border remains intact without duplication.

Learnt from: abbeyperini
Repo: npmx-dev/npmx.dev PR: 1049
File: app/components/Settings/Toggle.client.vue:22-29
Timestamp: 2026-02-11T00:01:33.121Z
Learning: In Vue 3.4 and later, you can use same-name shorthand for attribute bindings: use :attributeName instead of :attributeName="attributeName" when binding to a variable with the same name in scope. For example, :id is equivalent to :id="id" when an id variable exists. Apply this shorthand in .vue components (notably in Settings/Toggle.client.vue) to simplify templates. Ensure the bound variable exists and that you are using a Vue version that supports this shorthand.

Learnt from: alexdln
Repo: npmx-dev/npmx.dev PR: 1845
File: app/components/InstantSearch.vue:6-11
Timestamp: 2026-03-03T09:42:52.533Z
Learning: Maintain the established prehydration pattern across the project: use JSON.parse(localStorage.getItem('npmx-settings') || '{}') without per-call try-catch blocks. Do not introduce try-catch error handling for this pattern unless a coordinated, project-wide refactor of all onPrehydrate readers is planned and executed.

Learnt from: graphieros
Repo: npmx-dev/npmx.dev PR: 1974
File: app/components/Compare/FacetBarChart.vue:174-178
Timestamp: 2026-03-07T12:00:18.120Z
Learning: When reviewing Compare/FacetBarChart.vue (or other Vue components in app/components), treat config.datapoint as a runtime object that can include extra properties from the dataset item (e.g., formattedValue, custom metadata). The TypeScript typings for config.datapoint may be incomplete, but that should not be flagged as a runtime error. Base reviews on actual runtime behavior and data presence, not on missing type checks; focus on correct usage and data-driven effects.

Learnt from: mihaizaurus
Repo: npmx-dev/npmx.dev PR: 2072
File: app/components/Compare/ComparisonGrid.vue:38-40
Timestamp: 2026-03-14T08:57:22.084Z
Learning: In Vue components, when using v-for, provide a stable unique key (e.g., :key="col.name"). This learning notes that in app/components/Compare/ComparisonGrid.vue, using :key="col.name" is safe because duplicates are prevented upstream: app/components/Compare/PackageSelector.vue enforces two guards (filteredResults excludes already-selected names; addPackage early-returns if the name already exists). Do not flag :key="col.name" as a collision risk in this context. If applying this guideline elsewhere, ensure a unique key is guaranteed by logic or consider fallbacks (e.g., index or composite keys) only if duplicates could be introduced.

Learnt from: ShroXd
Repo: npmx-dev/npmx.dev PR: 2025
File: app/components/Package/Versions.vue:533-540
Timestamp: 2026-03-15T03:59:32.483Z
Learning: In Vue 3, templates that use v-if/v-else render exactly one root element at runtime. Attributes (data-testid, title, etc.) will fall through to the active root without needing explicit prop forwarding. Do not flag attribute forwarding as a bug for this pattern; reserve review for cases where multiple roots render simultaneously or where explicit forwarding is required by design. This applies broadly to Vue single-file components using v-if/v-else.

</template>
<a
v-if="externalUrl"
:href="externalUrl"
target="_blank"
rel="noopener noreferrer"
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"
>
{{ $t('package.replacement.learn_more') }}
<span class="i-lucide:external-link w-3 h-3" aria-hidden="true" />
</a>
</div>
</template>
8 changes: 6 additions & 2 deletions app/composables/npm/useReplacementDependencies.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ModuleReplacement } from 'module-replacements'
import type { ModuleReplacement, ModuleReplacementMapping } from 'module-replacements'

async function fetchReplacements(
deps: Record<string, string>,
Expand All @@ -8,7 +8,11 @@ async function fetchReplacements(
const results = await Promise.all(
names.map(async name => {
try {
const replacement = await $fetch<ModuleReplacement | null>(`/api/replacements/${name}`)
const response = await $fetch<{
mapping: ModuleReplacementMapping
replacement: ModuleReplacement
} | null>(`/api/replacements/${name}`)
const replacement = response?.replacement ?? null
return { name, replacement }
} catch {
return { name, replacement: null }
Expand Down
15 changes: 9 additions & 6 deletions app/composables/useCompareReplacements.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ModuleReplacement } from 'module-replacements'
import type { ModuleReplacement, ModuleReplacementMapping } from 'module-replacements'

export interface ReplacementSuggestion {
forPackage: string
Expand Down Expand Up @@ -41,8 +41,11 @@ export function useCompareReplacements(packageNames: MaybeRefOrGetter<string[]>)
const results = await Promise.all(
namesToCheck.map(async name => {
try {
const replacement = await $fetch<ModuleReplacement | null>(`/api/replacements/${name}`)
return { name, replacement }
const result = await $fetch<{
mapping: ModuleReplacementMapping
replacement: ModuleReplacement
} | null>(`/api/replacements/${name}`)
return { name, replacement: result?.replacement ?? null }
} catch {
return { name, replacement: null }
}
Expand Down Expand Up @@ -99,9 +102,9 @@ export function useCompareReplacements(packageNames: MaybeRefOrGetter<string[]>)
)

return {
replacements: readonly(replacements),
noDepSuggestions: readonly(noDepSuggestions),
infoSuggestions: readonly(infoSuggestions),
replacements,
noDepSuggestions,
infoSuggestions,
loading: readonly(loading),
}
}
6 changes: 4 additions & 2 deletions app/composables/useModuleReplacement.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { ModuleReplacement } from 'module-replacements'
import type { ModuleReplacement, ModuleReplacementMapping } from 'module-replacements'

export function useModuleReplacement(packageName: MaybeRefOrGetter<string>) {
return useLazyFetch<ModuleReplacement | null>(() => `/api/replacements/${toValue(packageName)}`)
return useLazyFetch<{ mapping: ModuleReplacementMapping; replacement: ModuleReplacement } | null>(
() => `/api/replacements/${toValue(packageName)}`,
)
}
6 changes: 5 additions & 1 deletion app/pages/package/[[org]]/[name].vue
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,11 @@ const showSkeleton = shallowRef(false)

<div class="space-y-6" :class="$style.areaVulns">
<!-- Bad package warning -->
<PackageReplacement v-if="moduleReplacement" :replacement="moduleReplacement" />
<PackageReplacement
v-if="moduleReplacement"
:mapping="moduleReplacement.mapping"
:replacement="moduleReplacement.replacement"
/>
Comment on lines +887 to +891
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should just call useModuleReplacement inside this component

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense, yes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually I'm not sure, since it's rendered conditionally based on if moduleReplacement is defined or not

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should

<!-- Size / dependency increase notice -->
<PackageSizeIncrease v-if="sizeDiff" :diff="sizeDiff" />
<!-- Vulnerability scan -->
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,6 @@
"none": "تم وضع علامة على هذه الحزمة بأنها لم تعد مطلوبة، ومن المرجح أن وظيفتها متاحة أصلًا في جميع المحركات.",
"learn_more": "اعرف المزيد",
"learn_more_above": "اعرف المزيد أعلاه.",
"mdn": "MDN",
"community": "المجتمع",
"consider_no_dep": "+ هل تريد خيار «بدون تبعية»؟"
},
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/az-AZ.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,6 @@
"none": "Bu paket artıq lazımsız olaraq qeyd edilib və funksionallığı bütün mühərriklərdə yerli olaraq mövcuddur.",
"learn_more": "Ətraflı öyrən",
"learn_more_above": "Yuxarıda ətraflı öyrənin.",
"mdn": "MDN",
"community": "icma",
"consider_no_dep": "+ Asılılıqsız düşünün?"
},
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/bg-BG.json
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,6 @@
"none": "Този пакет е отбелязан като вече ненужен и функционалността му вероятно е налична нативно във всички двигатели.",
"learn_more": "Научете повече",
"learn_more_above": "Научете повече по-горе.",
"mdn": "MDN",
"community": "общност",
"consider_no_dep": "+ Обмислете без зависимости?"
},
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/bn-IN.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,6 @@
"documented": "{community} এই প্যাকেজটিকে আরো ভাল পারফরম্যান্স অপশন হিসাবে চিহ্নিত করেছে।",
"none": "এই প্যাকেজটি এখন অনাবশ্যক হিসাবে চিহ্নিত, এবং এর কার্যকরিতা সমস্ত ইঞ্জিনে নেটিভভাবে উপলব্ধ হওয়ার সম্ভাবনা আছে।",
"learn_more": "আরো জানুন",
"mdn": "MDN",
"community": "কমিউনিটি"
},
"stats": {
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/cs-CZ.json
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,6 @@
"none": "Tento balíček byl označen jako již nepotřebný a jeho funkčnost je pravděpodobně dostupná nativně ve všech enginech.",
"learn_more": "Zjistit více",
"learn_more_above": "Zjistit více výše.",
"mdn": "MDN",
"community": "komunita",
"consider_no_dep": "+ Zvážit žádnou závislost?"
},
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,6 @@
"none": "Dieses Paket wurde als nicht mehr nötig markiert, und seine Funktionalität ist wahrscheinlich in allen Engines nativ verfügbar.",
"learn_more": "Mehr erfahren",
"learn_more_above": "Erfahre oben mehr.",
"mdn": "MDN",
"community": "Community",
"consider_no_dep": "+ Ohne Abhängigkeit lösen?"
},
Expand Down
5 changes: 3 additions & 2 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -267,13 +267,14 @@
},
"replacement": {
"title": "You might not need this dependency.",
"example": "Example:",
"native": "This can be replaced with {replacement}, available since Node {nodeVersion}.",
"simple": "The {community} has flagged this package as redundant, with the advice: {replacement}.",
"native_no_version": "This can be replaced with {replacement}.",
"simple": "The {community} has flagged this package as redundant, with the advice: {replacement}",
"documented": "The {community} has flagged this package as having more performant alternatives.",
"none": "This package has been flagged as no longer needed, and its functionality is likely available natively in all engines.",
"learn_more": "Learn more",
"learn_more_above": "Learn more above.",
"mdn": "MDN",
"community": "community",
"consider_no_dep": "+ Consider no dep?"
},
Expand Down
Loading
Loading