···11+import type { ModuleReplacement } from 'module-replacements'
22+33+export interface ReplacementSuggestion {
44+ forPackage: string
55+ replacement: ModuleReplacement
66+}
77+88+/**
99+ * Replacement types that suggest "no dependency" (can be replaced with native code or inline).
1010+ */
1111+const NO_DEP_REPLACEMENT_TYPES = ['native', 'simple'] as const
1212+1313+/**
1414+ * Replacement types that are informational only.
1515+ * These suggest alternative packages exist but don't fit the "no dependency" pattern.
1616+ */
1717+const INFO_REPLACEMENT_TYPES = ['documented'] as const
1818+1919+/**
2020+ * Composable for fetching module replacement suggestions for packages in comparison.
2121+ * Returns replacements split into "no dep" (actionable) and informational categories.
2222+ */
2323+export function useCompareReplacements(packageNames: MaybeRefOrGetter<string[]>) {
2424+ const packages = computed(() => toValue(packageNames))
2525+2626+ // Cache replacement data by package name
2727+ const replacements = shallowRef(new Map<string, ModuleReplacement | null>())
2828+ const loading = shallowRef(false)
2929+3030+ // Fetch replacements for all packages
3131+ async function fetchReplacements(names: string[]) {
3232+ if (names.length === 0) return
3333+3434+ // Filter out packages we've already checked
3535+ const namesToCheck = names.filter(name => !replacements.value.has(name))
3636+ if (namesToCheck.length === 0) return
3737+3838+ loading.value = true
3939+4040+ try {
4141+ const results = await Promise.all(
4242+ namesToCheck.map(async name => {
4343+ try {
4444+ const replacement = await $fetch<ModuleReplacement | null>(`/api/replacements/${name}`)
4545+ return { name, replacement }
4646+ } catch {
4747+ return { name, replacement: null }
4848+ }
4949+ }),
5050+ )
5151+5252+ const newReplacements = new Map(replacements.value)
5353+ for (const { name, replacement } of results) {
5454+ newReplacements.set(name, replacement)
5555+ }
5656+ replacements.value = newReplacements
5757+ } finally {
5858+ loading.value = false
5959+ }
6060+ }
6161+6262+ // Watch for package changes and fetch replacements
6363+ if (import.meta.client) {
6464+ watch(
6565+ packages,
6666+ newPackages => {
6767+ fetchReplacements(newPackages)
6868+ },
6969+ { immediate: true },
7070+ )
7171+ }
7272+7373+ // Build suggestions from replacements
7474+ const allSuggestions = computed(() => {
7575+ const result: ReplacementSuggestion[] = []
7676+7777+ for (const pkg of packages.value) {
7878+ const replacement = replacements.value.get(pkg)
7979+ if (!replacement) continue
8080+8181+ result.push({ forPackage: pkg, replacement })
8282+ }
8383+8484+ return result
8585+ })
8686+8787+ // Suggestions that prompt adding the "no dep" column (native, simple)
8888+ const noDepSuggestions = computed(() =>
8989+ allSuggestions.value.filter(s =>
9090+ (NO_DEP_REPLACEMENT_TYPES as readonly string[]).includes(s.replacement.type),
9191+ ),
9292+ )
9393+9494+ // Informational suggestions that don't prompt "no dep" (documented)
9595+ const infoSuggestions = computed(() =>
9696+ allSuggestions.value.filter(s =>
9797+ (INFO_REPLACEMENT_TYPES as readonly string[]).includes(s.replacement.type),
9898+ ),
9999+ )
100100+101101+ return {
102102+ replacements: readonly(replacements),
103103+ noDepSuggestions: readonly(noDepSuggestions),
104104+ infoSuggestions: readonly(infoSuggestions),
105105+ loading: readonly(loading),
106106+ }
107107+}
+146-20
app/composables/usePackageComparison.ts
···1111import { formatBytes } from '~/utils/formatters'
1212import { getDependencyCount } from '~/utils/npm/dependency-count'
13131414+/** Special identifier for the "What Would James Do?" comparison column */
1515+export const NO_DEPENDENCY_ID = '__no_dependency__'
1616+1717+/**
1818+ * Special display values for the "no dependency" column.
1919+ * These are explicit markers that get special rendering treatment.
2020+ */
2121+export const NoDependencyDisplay = {
2222+ /** Display as "–" (en-dash) */
2323+ DASH: '__display_dash__',
2424+ /** Display as "Up to you!" with good status */
2525+ UP_TO_YOU: '__display_up_to_you__',
2626+} as const
2727+1428export interface PackageComparisonData {
1529 package: ComparisonPackage
1630 downloads?: number
···4458 }
4559 /** Whether this is a binary-only package (CLI without library entry points) */
4660 isBinaryOnly?: boolean
6161+ /** Marks this as the "no dependency" column for special display */
6262+ isNoDependency?: boolean
4763}
48644965/**
···7692 return
7793 }
78947979- // Only fetch packages not already cached
8080- const namesToFetch = names.filter(name => !cache.value.has(name))
9595+ // Handle "no dependency" column - add to cache immediately
9696+ if (names.includes(NO_DEPENDENCY_ID) && !cache.value.has(NO_DEPENDENCY_ID)) {
9797+ const newCache = new Map(cache.value)
9898+ newCache.set(NO_DEPENDENCY_ID, createNoDependencyData())
9999+ cache.value = newCache
100100+ }
101101+102102+ // Only fetch packages not already cached (excluding "no dep" which has no remote data)
103103+ const namesToFetch = names.filter(name => name !== NO_DEPENDENCY_ID && !cache.value.has(name))
8110482105 if (namesToFetch.length === 0) {
83106 status.value = 'success'
···108131 $fetch<{ downloads: number }>(
109132 `https://api.npmjs.org/downloads/point/last-week/${encodePackageName(name)}`,
110133 ).catch(() => null),
111111- $fetch<PackageAnalysisResponse>(`/api/registry/analysis/${name}`).catch(() => null),
112112- $fetch<VulnerabilityTreeResult>(`/api/registry/vulnerabilities/${name}`).catch(
113113- () => null,
114114- ),
134134+ $fetch<PackageAnalysisResponse>(
135135+ `/api/registry/analysis/${encodePackageName(name)}`,
136136+ ).catch(() => null),
137137+ $fetch<VulnerabilityTreeResult>(
138138+ `/api/registry/vulnerabilities/${encodePackageName(name)}`,
139139+ ).catch(() => null),
115140 ])
116141117142 const versionData = pkgData.versions[latestVersion]
···191216 selfSize: number
192217 totalSize: number
193218 dependencyCount: number
194194- }>(`/api/registry/install-size/${name}`)
219219+ }>(`/api/registry/install-size/${encodePackageName(name)}`)
195220196221 // Update cache with install size
197222 const existing = cache.value.get(name)
···258283 }
259284}
260285286286+/**
287287+ * Creates mock data for the "What Would James Do?" comparison column.
288288+ * This represents the baseline of having no dependency at all.
289289+ *
290290+ * Uses explicit display markers (NoDependencyDisplay) instead of undefined
291291+ * to clearly indicate intentional special values vs missing data.
292292+ */
293293+function createNoDependencyData(): PackageComparisonData {
294294+ return {
295295+ package: {
296296+ name: NO_DEPENDENCY_ID,
297297+ version: '',
298298+ description: undefined,
299299+ },
300300+ isNoDependency: true,
301301+ downloads: undefined,
302302+ packageSize: 0,
303303+ directDeps: 0,
304304+ installSize: {
305305+ selfSize: 0,
306306+ totalSize: 0,
307307+ dependencyCount: 0,
308308+ },
309309+ analysis: undefined,
310310+ vulnerabilities: undefined,
311311+ metadata: {
312312+ license: NoDependencyDisplay.DASH,
313313+ lastUpdated: NoDependencyDisplay.UP_TO_YOU,
314314+ engines: undefined,
315315+ deprecated: undefined,
316316+ },
317317+ }
318318+}
319319+320320+/**
321321+ * Converts a special display marker to its FacetValue representation.
322322+ */
323323+function resolveNoDependencyDisplay(
324324+ marker: string,
325325+ t: (key: string) => string,
326326+): { display: string; status: FacetValue['status'] } | null {
327327+ switch (marker) {
328328+ case NoDependencyDisplay.DASH:
329329+ return { display: '–', status: 'neutral' }
330330+ case NoDependencyDisplay.UP_TO_YOU:
331331+ return { display: t('compare.facets.values.up_to_you'), status: 'good' }
332332+ default:
333333+ return null
334334+ }
335335+}
336336+261337function computeFacetValue(
262338 facet: ComparisonFacet,
263339 data: PackageComparisonData,
264340 t: (key: string, params?: Record<string, unknown>) => string,
265341): FacetValue | null {
342342+ const { isNoDependency } = data
343343+266344 switch (facet) {
267345 case 'downloads': {
268268- if (data.downloads === undefined) return null
346346+ if (data.downloads === undefined) {
347347+ if (isNoDependency) return { raw: 0, display: '–', status: 'neutral' }
348348+ return null
349349+ }
269350 return {
270351 raw: data.downloads,
271352 display: formatCompactNumber(data.downloads),
···273354 }
274355 }
275356 case 'packageSize': {
276276- if (!data.packageSize) return null
357357+ // A size of zero is valid
358358+ if (data.packageSize == null) return null
277359 return {
278360 raw: data.packageSize,
279361 display: formatBytes(data.packageSize),
···281363 }
282364 }
283365 case 'installSize': {
284284- if (!data.installSize) return null
366366+ // A size of zero is valid
367367+ if (data.installSize == null) return null
285368 return {
286369 raw: data.installSize.totalSize,
287370 display: formatBytes(data.installSize.totalSize),
···289372 }
290373 }
291374 case 'moduleFormat': {
292292- if (!data.analysis) return null
375375+ if (!data.analysis) {
376376+ if (isNoDependency)
377377+ return {
378378+ raw: 'up-to-you',
379379+ display: t('compare.facets.values.up_to_you'),
380380+ status: 'good',
381381+ }
382382+ return null
383383+ }
293384 const format = data.analysis.moduleFormat
294385 return {
295386 raw: format,
···306397 tooltip: t('compare.facets.binary_only_tooltip'),
307398 }
308399 }
309309- if (!data.analysis) return null
400400+ if (!data.analysis) {
401401+ if (isNoDependency)
402402+ return {
403403+ raw: 'up-to-you',
404404+ display: t('compare.facets.values.up_to_you'),
405405+ status: 'good',
406406+ }
407407+ return null
408408+ }
310409 const types = data.analysis.types
311410 return {
312411 raw: types.kind,
···322421 case 'engines': {
323422 const engines = data.metadata?.engines
324423 if (!engines?.node) {
325325- return { raw: null, display: t('compare.facets.values.any'), status: 'neutral' }
424424+ if (isNoDependency)
425425+ return {
426426+ raw: 'up-to-you',
427427+ display: t('compare.facets.values.up_to_you'),
428428+ status: 'good',
429429+ }
430430+ return {
431431+ raw: null,
432432+ display: t('compare.facets.values.any'),
433433+ status: 'neutral',
434434+ }
326435 }
327436 return {
328437 raw: engines.node,
···331440 }
332441 }
333442 case 'vulnerabilities': {
334334- if (!data.vulnerabilities) return null
443443+ if (!data.vulnerabilities) {
444444+ if (isNoDependency)
445445+ return {
446446+ raw: 'up-to-you',
447447+ display: t('compare.facets.values.up_to_you'),
448448+ status: 'good',
449449+ }
450450+ return null
451451+ }
335452 const count = data.vulnerabilities.count
336453 const sev = data.vulnerabilities.severity
337454 return {
···348465 }
349466 }
350467 case 'lastUpdated': {
351351- if (!data.metadata?.lastUpdated) return null
352352- const date = new Date(data.metadata.lastUpdated)
468468+ const lastUpdated = data.metadata?.lastUpdated
469469+ const resolved = lastUpdated ? resolveNoDependencyDisplay(lastUpdated, t) : null
470470+ if (resolved) return { raw: 0, ...resolved }
471471+ if (!lastUpdated) return null
472472+ const date = new Date(lastUpdated)
353473 return {
354474 raw: date.getTime(),
355355- display: data.metadata.lastUpdated,
475475+ display: lastUpdated,
356476 status: isStale(date) ? 'warning' : 'neutral',
357477 type: 'date',
358478 }
359479 }
360480 case 'license': {
361481 const license = data.metadata?.license
482482+ const resolved = license ? resolveNoDependencyDisplay(license, t) : null
483483+ if (resolved) return { raw: null, ...resolved }
362484 if (!license) {
363363- return { raw: null, display: t('compare.facets.values.unknown'), status: 'warning' }
485485+ if (isNoDependency) return { raw: null, display: '–', status: 'neutral' }
486486+ return {
487487+ raw: null,
488488+ display: t('compare.facets.values.unknown'),
489489+ status: 'warning',
490490+ }
364491 }
365492 return {
366493 raw: license,
···370497 }
371498 case 'dependencies': {
372499 const depCount = data.directDeps
373373- if (depCount === null) return null
500500+ if (depCount == null) return null
374501 return {
375502 raw: depCount,
376503 display: String(depCount),
···387514 status: isDeprecated ? 'bad' : 'good',
388515 }
389516 }
390390- // Coming soon facets
391517 case 'totalDependencies': {
392518 if (!data.installSize) return null
393519 const totalDepCount = data.installSize.dependencyCount
+66-9
app/pages/compare.vue
···11<script setup lang="ts">
22+import { NO_DEPENDENCY_ID } from '~/composables/usePackageComparison'
23import { useRouteQuery } from '@vueuse/router'
3445definePageMeta({
···3233// Fetch comparison data
3334const { packagesData, status, getFacetValues, isFacetLoading, isColumnLoading } =
3435 usePackageComparison(packages)
3636+3737+// Fetch module replacement suggestions
3838+const { noDepSuggestions, infoSuggestions, replacements } = useCompareReplacements(packages)
3939+4040+// Whether the "no dependency" baseline column is active
4141+const showNoDependency = computed(() => packages.value.includes(NO_DEPENDENCY_ID))
4242+4343+// Build column definitions for real packages only (no-dep is handled separately by the grid)
4444+const gridColumns = computed(() =>
4545+ packages.value
4646+ .map((pkg, i) => ({ pkg, originalIndex: i }))
4747+ .filter(({ pkg }) => pkg !== NO_DEPENDENCY_ID)
4848+ .map(({ pkg, originalIndex }) => {
4949+ const data = packagesData.value?.[originalIndex]
5050+ const header = data
5151+ ? data.package.version
5252+ ? `${data.package.name}@${data.package.version}`
5353+ : data.package.name
5454+ : pkg
5555+ return {
5656+ header,
5757+ replacement: replacements.value.get(pkg) ?? null,
5858+ }
5959+ }),
6060+)
6161+6262+// Whether we can add the no-dep column (not already added and have room)
6363+const canAddNoDep = computed(
6464+ () => packages.value.length < 4 && !packages.value.includes(NO_DEPENDENCY_ID),
6565+)
6666+6767+// Add "no dependency" column to comparison
6868+function addNoDep() {
6969+ if (packages.value.length >= 4) return
7070+ if (packages.value.includes(NO_DEPENDENCY_ID)) return
7171+ packages.value = [...packages.value, NO_DEPENDENCY_ID]
7272+}
35733674// Get loading state for each column
3775const columnLoading = computed(() => packages.value.map((_, i) => isColumnLoading(i)))
···3977// Check if we have enough packages to compare
4078const canCompare = computed(() => packages.value.length >= 2)
41794242-// Get headers for the grid
4343-const gridHeaders = computed(() => {
4444- if (!packagesData.value) return packages.value
4545- return packagesData.value.map((p, i) =>
4646- p ? `${p.package.name}@${p.package.version}` : (packages.value[i] ?? ''),
4747- )
4848-})
8080+// Extract headers from columns for facet rows
8181+const gridHeaders = computed(() => gridColumns.value.map(col => col.header))
49825083useSeoMeta({
5184 title: () =>
···103136 {{ $t('compare.packages.section_packages') }}
104137 </h2>
105138 <ComparePackageSelector v-model="packages" :max="4" />
139139+140140+ <!-- "No dep" replacement suggestions (native, simple) -->
141141+ <div v-if="noDepSuggestions.length > 0" class="mt-3 space-y-2">
142142+ <CompareReplacementSuggestion
143143+ v-for="suggestion in noDepSuggestions"
144144+ :key="suggestion.forPackage"
145145+ :package-name="suggestion.forPackage"
146146+ :replacement="suggestion.replacement"
147147+ variant="nodep"
148148+ :show-action="canAddNoDep"
149149+ @add-no-dep="addNoDep"
150150+ />
151151+ </div>
152152+153153+ <!-- Informational replacement suggestions (documented) -->
154154+ <div v-if="infoSuggestions.length > 0" class="mt-3 space-y-2">
155155+ <CompareReplacementSuggestion
156156+ v-for="suggestion in infoSuggestions"
157157+ :key="suggestion.forPackage"
158158+ :package-name="suggestion.forPackage"
159159+ :replacement="suggestion.replacement"
160160+ variant="info"
161161+ />
162162+ </div>
106163 </section>
107164108165 <!-- Facet selector -->
···152209 <div v-else-if="packagesData && packagesData.some(p => p !== null)">
153210 <!-- Desktop: Grid layout -->
154211 <div class="hidden md:block overflow-x-auto">
155155- <CompareComparisonGrid :columns="packages.length" :headers="gridHeaders">
212212+ <CompareComparisonGrid :columns="gridColumns" :show-no-dependency="showNoDependency">
156213 <CompareFacetRow
157214 v-for="facet in selectedFacets"
158215 :key="facet.id"
···189246 {{ $t('package.downloads.title') }}
190247 </h2>
191248192192- <CompareLineChart :packages />
249249+ <CompareLineChart :packages="packages.filter(p => p !== NO_DEPENDENCY_ID)" />
193250 </div>
194251195252 <div v-else class="text-center py-12" role="alert">
+14-2
i18n/locales/en.json
···139139 "documented": "The {community} has flagged this package as having more performant alternatives.",
140140 "none": "This package has been flagged as no longer needed, and its functionality is likely available natively in all engines.",
141141 "learn_more": "Learn more",
142142+ "learn_more_above": "Learn more above.",
142143 "mdn": "MDN",
143143- "community": "community"
144144+ "community": "community",
145145+ "consider_no_dep": "+ Consider no dep?"
144146 },
145147 "stats": {
146148 "license": "License",
···882884 "loading_versions": "Loading versions...",
883885 "select_version": "Select version"
884886 },
887887+ "no_dependency": {
888888+ "label": "(No dependency)",
889889+ "typeahead_title": "What Would James Do?",
890890+ "typeahead_description": "Compare against not using a dependency! e18e approved.",
891891+ "tooltip_title": "You might not need a dependency",
892892+ "tooltip_description": "Compare against not using a dependency! The {link} maintains a list of packages that can be replaced with native APIs or simpler alternatives.",
893893+ "e18e_community": "e18e community",
894894+ "add_column": "Add no dependency column to comparison"
895895+ },
885896 "facets": {
886897 "group_label": "Comparison facets",
887898 "all": "all",
···956967 "not_deprecated": "No",
957968 "types_included": "Included",
958969 "types_none": "None",
959959- "vulnerabilities_summary": "{count} ({critical}C/{high}H)"
970970+ "vulnerabilities_summary": "{count} ({critical}C/{high}H)",
971971+ "up_to_you": "Up to you!"
960972 }
961973 }
962974 }
+14-2
i18n/locales/fr-FR.json
···135135 "documented": "La {community} a signalé que ce paquet a des alternatives plus performantes.",
136136 "none": "Ce paquet a été signalé comme n'étant plus nécessaire, et sa fonctionnalité est probablement disponible nativement dans tous les moteurs.",
137137 "learn_more": "En savoir plus",
138138+ "learn_more_above": "En savoir plus ci-dessus.",
138139 "mdn": "MDN",
139139- "community": "communauté"
140140+ "community": "communauté",
141141+ "consider_no_dep": "+ Envisager sans dépendance ?"
140142 },
141143 "stats": {
142144 "license": "Licence",
···854856 "loading_versions": "Chargement des versions...",
855857 "select_version": "Sélectionner une version"
856858 },
859859+ "no_dependency": {
860860+ "label": "(Sans dépendance)",
861861+ "typeahead_title": "Et sans dépendance ?",
862862+ "typeahead_description": "Comparer avec l'absence de dépendance ! Approuvé par e18e.",
863863+ "tooltip_title": "Vous n'avez peut-être pas besoin d'une dépendance",
864864+ "tooltip_description": "Comparer avec l'absence de dépendance ! La {link} maintient une liste de paquets pouvant être remplacés par des API natives ou des alternatives plus simples.",
865865+ "e18e_community": "communauté e18e",
866866+ "add_column": "Ajouter la colonne sans dépendance à la comparaison"
867867+ },
857868 "facets": {
858869 "group_label": "Facettes de comparaison",
859870 "all": "tout",
···928939 "not_deprecated": "Non",
929940 "types_included": "Inclus",
930941 "types_none": "Aucun",
931931- "vulnerabilities_summary": "{count} ({critical}C/{high}H)"
942942+ "vulnerabilities_summary": "{count} ({critical}C/{high}H)",
943943+ "up_to_you": "À vous de décider !"
932944 }
933945 }
934946 }
+14-2
lunaria/files/en-GB.json
···139139 "documented": "The {community} has flagged this package as having more performant alternatives.",
140140 "none": "This package has been flagged as no longer needed, and its functionality is likely available natively in all engines.",
141141 "learn_more": "Learn more",
142142+ "learn_more_above": "Learn more above.",
142143 "mdn": "MDN",
143143- "community": "community"
144144+ "community": "community",
145145+ "consider_no_dep": "+ Consider no dep?"
144146 },
145147 "stats": {
146148 "license": "License",
···882884 "loading_versions": "Loading versions...",
883885 "select_version": "Select version"
884886 },
887887+ "no_dependency": {
888888+ "label": "(No dependency)",
889889+ "typeahead_title": "What Would James Do?",
890890+ "typeahead_description": "Compare against not using a dependency! e18e approved.",
891891+ "tooltip_title": "You might not need a dependency",
892892+ "tooltip_description": "Compare against not using a dependency! The {link} maintains a list of packages that can be replaced with native APIs or simpler alternatives.",
893893+ "e18e_community": "e18e community",
894894+ "add_column": "Add no dependency column to comparison"
895895+ },
885896 "facets": {
886897 "group_label": "Comparison facets",
887898 "all": "all",
···956967 "not_deprecated": "No",
957968 "types_included": "Included",
958969 "types_none": "None",
959959- "vulnerabilities_summary": "{count} ({critical}C/{high}H)"
970970+ "vulnerabilities_summary": "{count} ({critical}C/{high}H)",
971971+ "up_to_you": "Up to you!"
960972 }
961973 }
962974 }
+14-2
lunaria/files/en-US.json
···139139 "documented": "The {community} has flagged this package as having more performant alternatives.",
140140 "none": "This package has been flagged as no longer needed, and its functionality is likely available natively in all engines.",
141141 "learn_more": "Learn more",
142142+ "learn_more_above": "Learn more above.",
142143 "mdn": "MDN",
143143- "community": "community"
144144+ "community": "community",
145145+ "consider_no_dep": "+ Consider no dep?"
144146 },
145147 "stats": {
146148 "license": "License",
···882884 "loading_versions": "Loading versions...",
883885 "select_version": "Select version"
884886 },
887887+ "no_dependency": {
888888+ "label": "(No dependency)",
889889+ "typeahead_title": "What Would James Do?",
890890+ "typeahead_description": "Compare against not using a dependency! e18e approved.",
891891+ "tooltip_title": "You might not need a dependency",
892892+ "tooltip_description": "Compare against not using a dependency! The {link} maintains a list of packages that can be replaced with native APIs or simpler alternatives.",
893893+ "e18e_community": "e18e community",
894894+ "add_column": "Add no dependency column to comparison"
895895+ },
885896 "facets": {
886897 "group_label": "Comparison facets",
887898 "all": "all",
···956967 "not_deprecated": "No",
957968 "types_included": "Included",
958969 "types_none": "None",
959959- "vulnerabilities_summary": "{count} ({critical}C/{high}H)"
970970+ "vulnerabilities_summary": "{count} ({critical}C/{high}H)",
971971+ "up_to_you": "Up to you!"
960972 }
961973 }
962974 }
+14-2
lunaria/files/fr-FR.json
···135135 "documented": "La {community} a signalé que ce paquet a des alternatives plus performantes.",
136136 "none": "Ce paquet a été signalé comme n'étant plus nécessaire, et sa fonctionnalité est probablement disponible nativement dans tous les moteurs.",
137137 "learn_more": "En savoir plus",
138138+ "learn_more_above": "En savoir plus ci-dessus.",
138139 "mdn": "MDN",
139139- "community": "communauté"
140140+ "community": "communauté",
141141+ "consider_no_dep": "+ Envisager sans dépendance ?"
140142 },
141143 "stats": {
142144 "license": "Licence",
···854856 "loading_versions": "Chargement des versions...",
855857 "select_version": "Sélectionner une version"
856858 },
859859+ "no_dependency": {
860860+ "label": "(Sans dépendance)",
861861+ "typeahead_title": "Et sans dépendance ?",
862862+ "typeahead_description": "Comparer avec l'absence de dépendance ! Approuvé par e18e.",
863863+ "tooltip_title": "Vous n'avez peut-être pas besoin d'une dépendance",
864864+ "tooltip_description": "Comparer avec l'absence de dépendance ! La {link} maintient une liste de paquets pouvant être remplacés par des API natives ou des alternatives plus simples.",
865865+ "e18e_community": "communauté e18e",
866866+ "add_column": "Ajouter la colonne sans dépendance à la comparaison"
867867+ },
857868 "facets": {
858869 "group_label": "Facettes de comparaison",
859870 "all": "tout",
···928939 "not_deprecated": "Non",
929940 "types_included": "Inclus",
930941 "types_none": "Aucun",
931931- "vulnerabilities_summary": "{count} ({critical}C/{high}H)"
942942+ "vulnerabilities_summary": "{count} ({critical}C/{high}H)",
943943+ "up_to_you": "À vous de décider !"
932944 }
933945 }
934946 }
+49
test/e2e/interactions.spec.ts
···11import { expect, test } from './test-utils'
2233+test.describe('Compare Page', () => {
44+ test('no-dep column renders separately from package columns', async ({ page, goto }) => {
55+ await goto('/compare?packages=vue,__no_dependency__', { waitUntil: 'hydration' })
66+77+ const grid = page.locator('.comparison-grid')
88+ await expect(grid).toBeVisible({ timeout: 15000 })
99+1010+ // Should have the no-dep column with special styling
1111+ const noDepColumn = grid.locator('.comparison-cell-nodep')
1212+ await expect(noDepColumn).toBeVisible()
1313+1414+ // The no-dep column should not contain a link
1515+ await expect(noDepColumn.locator('a')).toHaveCount(0)
1616+ })
1717+1818+ test('no-dep column is always last even when packages are added after', async ({
1919+ page,
2020+ goto,
2121+ }) => {
2222+ // Start with vue and no-dep
2323+ await goto('/compare?packages=vue,__no_dependency__', { waitUntil: 'hydration' })
2424+2525+ const grid = page.locator('.comparison-grid')
2626+ await expect(grid).toBeVisible({ timeout: 15000 })
2727+2828+ // Add another package via the input
2929+ const input = page.locator('#package-search')
3030+ await input.fill('nuxt')
3131+3232+ // Wait for search results and click on nuxt
3333+ const nuxtOption = page.locator('button:has-text("nuxt")').first()
3434+ await expect(nuxtOption).toBeVisible({ timeout: 10000 })
3535+ await nuxtOption.click()
3636+3737+ // URL should have no-dep at the end, not in the middle
3838+ await expect(page).toHaveURL(/packages=vue,nuxt,__no_dependency__/)
3939+4040+ // Verify column order in the grid: vue, nuxt, then no-dep
4141+ const headerLinks = grid.locator('.comparison-cell-header a.truncate')
4242+ await expect(headerLinks).toHaveCount(2)
4343+ await expect(headerLinks.nth(0)).toContainText('vue')
4444+ await expect(headerLinks.nth(1)).toContainText('nuxt')
4545+4646+ // No-dep should still be visible as the last column
4747+ const noDepColumn = grid.locator('.comparison-cell-nodep')
4848+ await expect(noDepColumn).toBeVisible()
4949+ })
5050+})
5151+352test.describe('Search Pages', () => {
453 test('/search?q=vue → keyboard navigation (arrow keys + enter)', async ({ page, goto }) => {
554 await goto('/search?q=vue', { waitUntil: 'hydration' })