Skip to content

Comments

feat: package diffing#356

Open
taskyliz wants to merge 37 commits intonpmx-dev:mainfrom
taskyliz:feat/versions-diff-view
Open

feat: package diffing#356
taskyliz wants to merge 37 commits intonpmx-dev:mainfrom
taskyliz:feat/versions-diff-view

Conversation

@taskyliz
Copy link
Contributor

@taskyliz taskyliz commented Jan 29, 2026

Closes #49

@vercel
Copy link

vercel bot commented Jan 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Feb 7, 2026 8:10pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Feb 7, 2026 8:10pm
npmx-lunaria Ignored Ignored Feb 7, 2026 8:10pm

Request Review

@github-actions
Copy link

github-actions bot commented Jan 29, 2026

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
lunaria/files/en-GB.json Localization changed, will be marked as complete. 🔄️
lunaria/files/en-US.json Source changed, localizations will be marked as outdated.
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

@taskyliz taskyliz force-pushed the feat/versions-diff-view branch from 28c17c5 to a787b16 Compare January 29, 2026 21:33
@taskyliz taskyliz force-pushed the feat/versions-diff-view branch from a787b16 to 3149f5d Compare January 29, 2026 21:36
@taskyliz taskyliz force-pushed the feat/versions-diff-view branch from 3149f5d to 376868d Compare January 29, 2026 21:39
@taskyliz taskyliz marked this pull request as ready for review January 30, 2026 08:17
Copy link
Member

@danielroe danielroe left a comment

Choose a reason for hiding this comment

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

I'll make some tweaks later - but this looks amazing!

a few notes to myself (or you if you get a chance!):

  • we should have consistent font size for code trees between diff viewer + code viewer
  • we should have a similar kind of mobile panel that slides in between the two of them
  • we should use the toggles from settings.vue rather than checkboxes
  • I think there are some hydration issues but not sure if that originates in this PR
  • the sliders can also be more aligned with the existing visual styles - some nice work from @serhalp in the search filters might be a helpful guide

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (3)
package.json (1)

56-56: Duplicate dependency @intlify/core-base in both dependencies and devDependencies.

This package appears at line 56 in dependencies and again at line 111 in devDependencies with the same version 11.2.8. This is redundant—packages in dependencies are already available during development. Consider removing the devDependencies entry unless there's a specific reason for the duplication.

app/utils/language-detection.ts (1)

64-72: filenameMap is case-sensitive—lowercase variants will fall through to extension lookup.

Files named dockerfile or makefile (lowercase, common in many projects) won't match the exact keys Dockerfile and Makefile, and will fall back to extension-based lookup (returning 'text' since they have no extension).

Consider a case-insensitive lookup:

🔧 Proposed fix
-  if (filenameMap[fileName]) {
-    return filenameMap[fileName]
+  const lowerFileName = fileName.toLowerCase()
+  const filenameMatch = Object.entries(filenameMap).find(
+    ([key]) => key.toLowerCase() === lowerFileName
+  )
+  if (filenameMatch) {
+    return filenameMatch[1]
   }

Alternatively, store lowercase keys in filenameMap and compare against fileName.toLowerCase().

app/components/diff/Line.vue (1)

112-118: Consider if deep: true is necessary for the watch.

The watch uses deep: true which recursively watches props.line. Since DiffLine objects are typically replaced rather than mutated, a shallow watch might suffice and be more performant. However, this is a minor optimisation consideration.

})
const borderClasses = computed(() => {
const classes = ['border-transparent', 'w-1', 'border-l-3']
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use RTL-compatible utility class for border.

The pipeline flagged border-l-3 as incompatible with RTL layouts. Replace with border-is-3 for proper bidirectional support.

🔧 Proposed fix
-  const classes = ['border-transparent', 'w-1', 'border-l-3']
+  const classes = ['border-transparent', 'w-1', 'border-is-3']
🧰 Tools
🪛 GitHub Actions: autofix.ci

[error] 52-52: [RTL] avoid using 'border-l-3', use 'border-is-3' instead.

const contentClasses = computed(() => {
const shouldWrap = diffContext?.wordWrap?.value ?? false
return ['pr-6', shouldWrap ? 'whitespace-pre-wrap break-words' : 'text-nowrap']
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use RTL-compatible utility class for padding.

The pipeline flagged pr-6 as incompatible with RTL layouts. Replace with pe-6 for proper bidirectional support.

🔧 Proposed fix
-  return ['pr-6', shouldWrap ? 'whitespace-pre-wrap break-words' : 'text-nowrap']
+  return ['pe-6', shouldWrap ? 'whitespace-pre-wrap break-words' : 'text-nowrap']
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return ['pr-6', shouldWrap ? 'whitespace-pre-wrap break-words' : 'text-nowrap']
return ['pe-6', shouldWrap ? 'whitespace-pre-wrap break-words' : 'text-nowrap']
🧰 Tools
🪛 GitHub Actions: autofix.ci

[error] 66-66: [RTL] avoid using 'pr-6', use 'pe-6' instead.

{{ section.label }} ({{ section.changes.length }})
</summary>

<div class="space-y-1 ml-5 max-h-40 overflow-y-auto">
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix RTL compliance: replace ml-5 with ms-5.

The pipeline reports an RTL linting error. Use logical margin property ms-5 (margin-inline-start) instead of ml-5 for proper right-to-left language support.

🔧 Proposed fix
-          <div class="space-y-1 ml-5 max-h-40 overflow-y-auto">
+          <div class="space-y-1 ms-5 max-h-40 overflow-y-auto">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="space-y-1 ml-5 max-h-40 overflow-y-auto">
<div class="space-y-1 ms-5 max-h-40 overflow-y-auto">
🧰 Tools
🪛 GitHub Actions: autofix.ci

[error] 117-117: [RTL] avoid using 'ml-5', use 'ms-5' instead.

</NuxtLink>

<div
class="flex items-center gap-1.5 text-fg-muted font-mono text-[10px] ml-auto shrink-0"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix RTL compliance: replace ml-auto with ms-auto.

The pipeline reports an RTL linting error. Use logical margin property ms-auto instead of ml-auto for proper right-to-left language support.

🔧 Proposed fix
-              <div
-                class="flex items-center gap-1.5 text-fg-muted font-mono text-[10px] ml-auto shrink-0"
+              <div
+                class="flex items-center gap-1.5 text-fg-muted font-mono text-[10px] ms-auto shrink-0"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class="flex items-center gap-1.5 text-fg-muted font-mono text-[10px] ml-auto shrink-0"
class="flex items-center gap-1.5 text-fg-muted font-mono text-[10px] ms-auto shrink-0"
🧰 Tools
🪛 GitHub Actions: autofix.ci

[error] 142-142: [RTL] avoid using 'ml-auto', use 'ms-auto' instead.

<span class="i-lucide-chevrons-up-down w-4 h-4" />
</td>
<td>
<span class="px-0 sticky left-2 italic opacity-50">
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use RTL-compatible utility class for positioning.

The pipeline flagged left-2 as incompatible with RTL layouts. Replace with inset-is-2 for proper bidirectional support.

🔧 Proposed fix
       <span class="px-0 sticky left-2 italic opacity-50">
+      <span class="px-0 sticky inset-is-2 italic opacity-50">
🧰 Tools
🪛 GitHub Actions: autofix.ci

[error] 19-19: [RTL] avoid using 'left-2', use 'inset-is-2' instead.

<!-- Dropdown menu -->
<motion.div
v-if="showOptions"
class="absolute right-0 top-full mt-2 z-20 p-4 bg-bg-elevated border border-border shadow-2xl overflow-auto"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix RTL compliance: replace right-0 with inset-ie-0.

The pipeline reports an RTL linting error. Use logical inset property inset-ie-0 (inset-inline-end) instead of right-0 for proper right-to-left language support.

🔧 Proposed fix
           <motion.div
             v-if="showOptions"
-            class="absolute right-0 top-full mt-2 z-20 p-4 bg-bg-elevated border border-border shadow-2xl overflow-auto"
+            class="absolute inset-ie-0 top-full mt-2 z-20 p-4 bg-bg-elevated border border-border shadow-2xl overflow-auto"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class="absolute right-0 top-full mt-2 z-20 p-4 bg-bg-elevated border border-border shadow-2xl overflow-auto"
<motion.div
v-if="showOptions"
class="absolute inset-ie-0 top-full mt-2 z-20 p-4 bg-bg-elevated border border-border shadow-2xl overflow-auto"
🧰 Tools
🪛 GitHub Actions: autofix.ci

[error] 278-278: [RTL] avoid using 'right-0', use 'inset-ie-0' instead.

<div v-else-if="compare" class="flex-1 flex flex-col md:flex-row min-h-0 overflow-hidden">
<!-- Desktop sidebar -->
<aside
class="hidden md:flex w-80 border-r border-border bg-bg-subtle flex-col shrink-0 min-h-0"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix RTL compliance: replace border-r with border-ie.

The pipeline reports an RTL linting error. Use logical border property border-ie (border-inline-end) instead of border-r for proper right-to-left language support.

🔧 Proposed fix
       <aside
-        class="hidden md:flex w-80 border-r border-border bg-bg-subtle flex-col shrink-0 min-h-0"
+        class="hidden md:flex w-80 border-ie border-border bg-bg-subtle flex-col shrink-0 min-h-0"
       >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class="hidden md:flex w-80 border-r border-border bg-bg-subtle flex-col shrink-0 min-h-0"
<aside
class="hidden md:flex w-80 border-ie border-border bg-bg-subtle flex-col shrink-0 min-h-0"
>
🧰 Tools
🪛 GitHub Actions: autofix.ci

[error] 204-204: [RTL] avoid using 'border-r', use 'border-ie' instead.

"view_file": "View file",
"view_in_browser": "View in code browser",
"close_diff": "Close diff",
"lines_hidden": "{count} lines hidden",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add pluralisation for lines_hidden.

The file already uses plural syntax for lines at line 608. This key should follow the same pattern to avoid rendering "1 lines hidden".

Proposed fix
-    "lines_hidden": "{count} lines hidden",
+    "lines_hidden": "{count} line hidden | {count} lines hidden",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"lines_hidden": "{count} lines hidden",
"lines_hidden": "{count} line hidden | {count} lines hidden",

fix: pass reactive getter to usePackage for version reactivity on client-side navigation
@vercel
Copy link

vercel bot commented Feb 7, 2026

@taskylizard is attempting to deploy a commit to the serhalp's projects Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Comment on lines +14 to +36
const parsedRoute = computed(() => {
const segments = route.params.path || []

// Find the /v/ separator
const vIndex = segments.indexOf('v')
if (vIndex === -1 || vIndex >= segments.length - 1) {
return { packageName: segments.join('/'), range: null }
}

const packageName = segments.slice(0, vIndex).join('/')
const rangeStr = segments[vIndex + 1] ?? ''

// Parse version range (from...to)
const parts = rangeStr.split('...')
if (parts.length !== 2) {
return { packageName, range: null }
}

return {
packageName,
range: { from: parts[0]!, to: parts[1]! },
}
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the file exists and read the relevant lines
cat -n app/pages/diff/[...path].vue | head -50

Repository: npmx-dev/npmx.dev

Length of output: 1869


🏁 Script executed:

# Also check the project structure and Vue Router setup to understand the typing
fd -e "config\|router\|nuxt" -t f | head -20

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Check if there's a tsconfig or type definitions
cat -n tsconfig.json 2>/dev/null | head -40 || echo "tsconfig.json not found"

Repository: npmx-dev/npmx.dev

Length of output: 506


🏁 Script executed:

# Find nuxt.config
fd -e "nuxt.config" -t f

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Check package.json to see Vue Router version
cat -n package.json | grep -A 2 -B 2 "vue-router\|nuxt" | head -30

Repository: npmx-dev/npmx.dev

Length of output: 1713


🏁 Script executed:

# Look for route type definitions or RouteLocationNormalized
rg "diff.*\[\.\.\.path\]|path.*catch" -t vue -t ts --max-count 5

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

# Get full dependencies section from package.json
cat package.json | jq '.dependencies, .devDependencies' | grep -A 5 -B 5 "nuxt\|vue"

Repository: npmx-dev/npmx.dev

Length of output: 2778


🏁 Script executed:

# Search for route definitions or definePageMeta with path param
rg "definePageMeta|useRoute" -A 3 -B 1 --max-count 10

Repository: npmx-dev/npmx.dev

Length of output: 11934


🏁 Script executed:

# Check if there's a types file that defines route params
fd "types\|route" -t f -e ts -e d.ts | head -20

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Check the package-code and package-docs implementations
cat -n app/pages/package-code/[...path].vue | head -40

Repository: npmx-dev/npmx.dev

Length of output: 1666


🏁 Script executed:

# Check the package-docs file more carefully
cat -n app/pages/package-docs/[...path].vue | head -40

Repository: npmx-dev/npmx.dev

Length of output: 1465


🏁 Script executed:

# Search for any TypeScript definitions or route type augmentation
rg "route\.params" -A 2 -B 2 --max-count 15 app/pages/

Repository: npmx-dev/npmx.dev

Length of output: 1909


🏁 Script executed:

# Check nuxt.config to see the router configuration
cat -n nuxt.config.ts | head -60

Repository: npmx-dev/npmx.dev

Length of output: 2148


🏁 Script executed:

# Look for unplugin-vue-router configuration or type generation
rg "unplugin-vue-router|router\.ts" -t ts -t js --max-count 10

Repository: npmx-dev/npmx.dev

Length of output: 99


🏁 Script executed:

# Search for any .d.ts files or type augmentation related to routes
fd -e "d.ts" | xargs grep -l "params\|route" 2>/dev/null | head -10

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Check if there's any route type definition or generated types
fd -e "routes.d.ts" -o -e "router.d.ts" -t f

Repository: npmx-dev/npmx.dev

Length of output: 177


🏁 Script executed:

# Search in .nuxt generated files for route type definitions
ls -la .nuxt/types/ 2>/dev/null | head -20

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Check for any imports or type annotations related to route.params in similar files
rg "route\.params\.path" -B 5 -A 5 --max-count 20

Repository: npmx-dev/npmx.dev

Length of output: 2451


🏁 Script executed:

# Check Vue Router type definitions or documentation in node_modules
find node_modules -name "*router*" -type d | grep -E "vue-router|@vue" | head -5

Repository: npmx-dev/npmx.dev

Length of output: 108


🏁 Script executed:

# Search for any Vue Router version info
rg "vue-router" package.json

Repository: npmx-dev/npmx.dev

Length of output: 97


🏁 Script executed:

# Check if there's any JSDoc or TypeScript comment explaining route.params type
rg "route\.params" -B 10 -A 1 app/pages/diff/ | head -40

Repository: npmx-dev/npmx.dev

Length of output: 674


🏁 Script executed:

# Check the actual TypeScript configuration and type checking setup
cat -n tsconfig.json

Repository: npmx-dev/npmx.dev

Length of output: 506


🏁 Script executed:

# Look for any type errors or issues in tests related to route.params
rg "route\.params\|catch.*all\|\.\.\.path" test/ --max-count 20

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Verify the actual behavior by checking if there are any runtime guards or type assertions
rg "Array\.isArray.*path|typeof.*path.*string" --max-count 10

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Search for any documentation or comments about how route.params.path is typed
rg "catch-all|\.\.\.path|\[\.\.\.path\]" -B 3 -A 3 --max-count 15

Repository: npmx-dev/npmx.dev

Length of output: 606


🏁 Script executed:

# Check if there's any validation or type checking happening elsewhere for route params
rg "route\.params\|RouteLocationNormalized" app/ --max-count 20 -t ts -t vue

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

# Let's verify the actual behavior by checking if the code works as-is in the codebase
rg "segments\." app/pages/diff/\[\.\.\.path\]\.vue

Repository: npmx-dev/npmx.dev

Length of output: 398


🏁 Script executed:

# Let me search for web documentation or check if there's any evidence of route.params.path being a string
# First, check if there are any error reports or TODOs related to this
rg "TODO|FIXME|BUG" app/pages/diff/\[\.\.\.path\] 2>/dev/null || echo "No specific TODOs found"

Repository: npmx-dev/npmx.dev

Length of output: 84


🏁 Script executed:

# Check the actual line 24 usage to see the indexing pattern
cat -n app/pages/diff/[...path].vue | sed -n '14,36p'

Repository: npmx-dev/npmx.dev

Length of output: 825


🏁 Script executed:

# Search for how route.params is documented or used in error handling
rg "route\.params\[|route\.query\[" app/ -A 1 -B 1 --max-count 10

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Check if there are any type issues reported in the codebase with route.params
rg "as.*string\[\]|as.*unknown|@ts-ignore" app/pages/ | head -20

Repository: npmx-dev/npmx.dev

Length of output: 313


🏁 Script executed:

# Search for any documentation or comments in the project about route param typing
cat -n app/pages/package-docs/[...path].vue | sed -n '1,30p'

Repository: npmx-dev/npmx.dev

Length of output: 1054


🏁 Script executed:

# Let me check what the review comment specifically says about the issue and the coding guidelines
# Check for any actual type errors by looking at similar patterns
rg "segments\[.*\].*\?" app/pages/ -B 2 -A 2

Repository: npmx-dev/npmx.dev

Length of output: 348


Normalise route.params.path to an array before calling array methods.

In Nuxt with catch-all routes, route.params.path should be string[], but the codebase's defensive patterns suggest potential edge cases. To ensure strict type safety per the coding guidelines, explicitly guard against the value being a string or undefined before calling .indexOf() or other array methods.

🔧 Suggested fix
-  const segments = route.params.path || []
+  const rawPath = route.params.path
+  const segments = Array.isArray(rawPath) ? rawPath : rawPath ? [rawPath] : []
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const parsedRoute = computed(() => {
const segments = route.params.path || []
// Find the /v/ separator
const vIndex = segments.indexOf('v')
if (vIndex === -1 || vIndex >= segments.length - 1) {
return { packageName: segments.join('/'), range: null }
}
const packageName = segments.slice(0, vIndex).join('/')
const rangeStr = segments[vIndex + 1] ?? ''
// Parse version range (from...to)
const parts = rangeStr.split('...')
if (parts.length !== 2) {
return { packageName, range: null }
}
return {
packageName,
range: { from: parts[0]!, to: parts[1]! },
}
})
const parsedRoute = computed(() => {
const rawPath = route.params.path
const segments = Array.isArray(rawPath) ? rawPath : rawPath ? [rawPath] : []
// Find the /v/ separator
const vIndex = segments.indexOf('v')
if (vIndex === -1 || vIndex >= segments.length - 1) {
return { packageName: segments.join('/'), range: null }
}
const packageName = segments.slice(0, vIndex).join('/')
const rangeStr = segments[vIndex + 1] ?? ''
// Parse version range (from...to)
const parts = rangeStr.split('...')
if (parts.length !== 2) {
return { packageName, range: null }
}
return {
packageName,
range: { from: parts[0]!, to: parts[1]! },
}
})

Comment on lines +67 to +71
const selectedFromQuery = computed(() => {
const filePath = route.query.file
if (!filePath || !compare.value) return null
return allChanges.value.find(f => f.path === filePath) ?? null
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n app/pages/diff/[...path].vue | head -100

Repository: npmx-dev/npmx.dev

Length of output: 3664


🏁 Script executed:

wc -l app/pages/diff/[...path].vue

Repository: npmx-dev/npmx.dev

Length of output: 93


🏁 Script executed:

rg "route\.query\." app/pages/diff/[...path].vue -A 2 -B 2

Repository: npmx-dev/npmx.dev

Length of output: 252


🏁 Script executed:

rg "route\.query\." --type ts --type tsx --type vue -c | head -20

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

# Search for patterns of handling query parameters in the codebase
rg "route\.query" app/ -A 1 | head -40

Repository: npmx-dev/npmx.dev

Length of output: 2092


🏁 Script executed:

# Find the normalizeSearchParam function to understand how query params are handled elsewhere
rg "normalizeSearchParam" -A 5 --type vue | head -40

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

# Find the definition of normalizeSearchParam
rg "function normalizeSearchParam|const normalizeSearchParam|export.*normalizeSearchParam" --type ts

Repository: npmx-dev/npmx.dev

Length of output: 178


🏁 Script executed:

cat -n shared/utils/url.ts | grep -A 10 "normalizeSearchParam"

Repository: npmx-dev/npmx.dev

Length of output: 321


🏁 Script executed:

# Check Vue Router/Nuxt types to understand LocationQueryValue
rg "LocationQueryValue" --type ts -B 2 -A 2 | head -30

Repository: npmx-dev/npmx.dev

Length of output: 463


Handle array-valued ?file= query parameter safely.

Vue Router's LocationQueryValue can be string | string[] | null, but the current code accesses route.query.file directly without guarding for arrays. If a user passes multiple file parameters, this will fail the comparison check. Extract the first element from arrays before use.

🔧 Suggested fix
-  const filePath = route.query.file
-  if (!filePath || !compare.value) return null
-  return allChanges.value.find(f => f.path === filePath) ?? null
+  const fileParam = route.query.file
+  const filePath = Array.isArray(fileParam) && fileParam.length > 0 ? fileParam[0] : fileParam
+  if (!filePath || !compare.value) return null
+  return allChanges.value.find(f => f.path === filePath) ?? null

Aligns with coding guidelines: "Ensure you write strictly type-safe code, for example by ensuring you always check when accessing an array value by index".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const selectedFromQuery = computed(() => {
const filePath = route.query.file
if (!filePath || !compare.value) return null
return allChanges.value.find(f => f.path === filePath) ?? null
})
const selectedFromQuery = computed(() => {
const fileParam = route.query.file
const filePath = Array.isArray(fileParam) && fileParam.length > 0 ? fileParam[0] : fileParam
if (!filePath || !compare.value) return null
return allChanges.value.find(f => f.path === filePath) ?? null
})

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/components/diff/FileTree.vue (1)

47-50: Avoid non‑null assertions when indexing parts.

Use entries() to keep index access type‑safe and remove the non‑null assertion.

♻️ Suggested refactor
-    for (let i = 0; i < parts.length; i++) {
-      const part = parts[i]!
+    for (const [i, part] of parts.entries()) {
       const isFile = i === parts.length - 1
       const path = parts.slice(0, i + 1).join('/')
As per coding guidelines: Ensure you write strictly type-safe code, for example by ensuring you always check when accessing an array value by index.

Comment on lines +26 to +36
// Sort: directories first, then alphabetically
function sortTree(nodes: DiffTreeNode[]): DiffTreeNode[] {
return nodes
.map(n => ({
...n,
children: n.children ? sortTree(n.children) : undefined,
}))
.sort((a, b) => {
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1
return a.name.localeCompare(b.name)
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the file
fd "FileTree.vue" --type f

Repository: npmx-dev/npmx.dev

Length of output: 126


🏁 Script executed:

# Read the file to examine lines 26-36 and surrounding context
cat -n app/components/diff/FileTree.vue | head -80

Repository: npmx-dev/npmx.dev

Length of output: 2706


🏁 Script executed:

# Check if there's SSR/Nuxt context and how other sorting is done
rg -A 3 -B 3 "Intl\.Collator|localeCompare" --type ts --type vue

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

# Check what framework this is (Nuxt, Next, etc.)
head -20 package.json

Repository: npmx-dev/npmx.dev

Length of output: 664


🏁 Script executed:

# Search for localeCompare and Intl.Collator patterns correctly
rg "localeCompare|Intl\.Collator" --type-list | grep -i vue

Repository: npmx-dev/npmx.dev

Length of output: 450


🏁 Script executed:

# Better approach - search without specific type
rg "localeCompare|Intl\.Collator" -l

Repository: npmx-dev/npmx.dev

Length of output: 693


🏁 Script executed:

# Check if this is an SSR-heavy Nuxt app (check nuxt.config)
cat nuxt.config.ts 2>/dev/null || cat nuxt.config.js 2>/dev/null || echo "Config not found in standard locations"

Repository: npmx-dev/npmx.dev

Length of output: 8056


🏁 Script executed:

# Search for other localeCompare usage
rg "localeCompare" -B 2 -A 2

Repository: npmx-dev/npmx.dev

Length of output: 12193


🏁 Script executed:

# Check if there are similar type-safety issues with array access assertions
rg "parts\[|children\!" -B 1 -A 1 app/components/diff/FileTree.vue

Repository: npmx-dev/npmx.dev

Length of output: 161


🏁 Script executed:

# Look at server-side implementation for reference
cat -n server/utils/file-tree.ts | head -100

Repository: npmx-dev/npmx.dev

Length of output: 2932


🏁 Script executed:

# Verify the Intl.Collator approach is standard in the codebase (check if already used)
rg "Intl\.Collator" app/ server/

Repository: npmx-dev/npmx.dev

Length of output: 43


Make tree sorting deterministic across SSR and client locales.

localeCompare without an explicit locale can sort differently between Node.js (server-side rendering) and the browser, which risks hydration mismatches and inconsistent ordering. Use a fixed Intl.Collator to ensure the tree order is stable across both environments.

💡 Suggested fix
+const treeNameCollator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' })
+
 function sortTree(nodes: DiffTreeNode[]): DiffTreeNode[] {
   return nodes
     .map(n => ({
       ...n,
       children: n.children ? sortTree(n.children) : undefined,
     }))
     .sort((a, b) => {
       if (a.type !== b.type) return a.type === 'directory' ? -1 : 1
-      return a.name.localeCompare(b.name)
+      return treeNameCollator.compare(a.name, b.name)
     })
 }

Comment on lines +39 to +67
// Build tree structure from flat file list (only at root level)
function buildTree(files: FileChange[]): DiffTreeNode[] {
const root: DiffTreeNode[] = []
for (const file of files) {
const parts = file.path.split('/')
let current = root
for (let i = 0; i < parts.length; i++) {
const part = parts[i]!
const isFile = i === parts.length - 1
const path = parts.slice(0, i + 1).join('/')
let node = current.find(n => n.name === part)
if (!node) {
node = {
name: part,
path,
type: isFile ? 'file' : 'directory',
changeType: isFile ? file.type : undefined,
children: isFile ? undefined : [],
}
current.push(node)
}
if (!isFile) {
current = node.children!
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd app/components/diff && cat -n FileTree.vue | head -100

Repository: npmx-dev/npmx.dev

Length of output: 3354


🏁 Script executed:

fd -t f "types" --extension ts --extension tsx | head -20

Repository: npmx-dev/npmx.dev

Length of output: 104


🏁 Script executed:

rg "type FileChange" --type ts --type tsx -A 5

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

rg "type FileChange" -A 5

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

find . -name "types*" -type f | grep -i shared

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

rg "FileChange" --type ts -B 2 -A 3 | head -50

Repository: npmx-dev/npmx.dev

Length of output: 1836


🏁 Script executed:

cat -n shared/types/compare.ts | head -40

Repository: npmx-dev/npmx.dev

Length of output: 1576


🏁 Script executed:

cat -n server/utils/compare.ts | head -120

Repository: npmx-dev/npmx.dev

Length of output: 4247


🏁 Script executed:

rg "buildTree" -B 3 -A 3

Repository: npmx-dev/npmx.dev

Length of output: 955


🏁 Script executed:

# Let me trace through a potential collision scenario
# Check if test files exist for FileTree
find . -name "*FileTree*" -type f

Repository: npmx-dev/npmx.dev

Length of output: 168


🏁 Script executed:

# Let me examine the data flow more carefully
# Check where buildTree is called and what data is passed to it
grep -r "buildTree" app/components/diff/FileTree.vue -A 2 -B 2

Repository: npmx-dev/npmx.dev

Length of output: 406


🏁 Script executed:

# Let me create a test scenario to understand the data flow
# Trace through compareFileTrees to see if it can produce conflicting paths
cat -n server/utils/compare.ts | sed -n '49,95p'

Repository: npmx-dev/npmx.dev

Length of output: 1468


🏁 Script executed:

# Let me verify the collision scenario with a concrete example
# If the files array contains both 'foo' and 'foo/bar', the buildTree function will fail
python3 << 'EOF'
# Simulate the buildTree function with a collision scenario
files = [
    {'path': 'foo', 'type': 'added'},
    {'path': 'foo/bar', 'type': 'added'}
]

root = []

for file in files:
    parts = file['path'].split('/')
    current = root
    
    for i, part in enumerate(parts):
        is_file = i == len(parts) - 1
        path = '/'.join(parts[:i+1])
        
        # Find existing node
        node = None
        for n in current:
            if n['name'] == part:
                node = n
                break
        
        if not node:
            node = {
                'name': part,
                'path': path,
                'type': 'file' if is_file else 'directory',
                'children': None if is_file else []
            }
            current.append(node)
        
        if not is_file:
            # This is where the error would occur in TypeScript
            if node['children'] is None:
                print(f"ERROR: Tried to access .children on file node '{node['name']}' at path '{node['path']}'")
                print(f"This happens when processing file path '{file['path']}'")
                print(f"Node structure: {node}")
                break
            current = node['children']
        
        print(f"Processed: {file['path']} -> part='{part}', isFile={is_file}, nodeType={node['type']}")

print("\nConclusion: Collision DOES occur with paths 'foo' and 'foo/bar'")
EOF

Repository: npmx-dev/npmx.dev

Length of output: 378


🏁 Script executed:

# Check if such conflicting paths can realistically occur in npm packages
# by examining real package structures or test data
find . -path "*/test*" -name "*.ts" -type f | xargs grep -l "compareFileTrees\|FileChange" | head -5

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Check the PackageFileTree type to understand the data structure better
rg "interface PackageFileTree|type PackageFileTree" -A 10

Repository: npmx-dev/npmx.dev

Length of output: 1211


🏁 Script executed:

# Now let me verify if this scenario can actually occur in real npm packages
# by checking how compareFileTrees would generate such conflicting FileChange entries
cat -n server/utils/compare.ts | sed -n '58,85p'

Repository: npmx-dev/npmx.dev

Length of output: 946


Handle potential file–directory name collisions in the tree builder.

The buildTree function can fail if the input contains both a file and a directory with the same name segment (e.g., foo as a file and foo/bar as a file). This occurs because when foo is created as a file node with children: undefined, the subsequent attempt to access node.children! on line 65 will throw an error.

This scenario is possible in real package comparisons when a directory is replaced by a file or vice versa. Add a type guard to verify that the node is a directory before accessing its children property, or skip/merge conflicting paths explicitly.

The diff viewer was not showing green background highlighting for added
lines in modified files that only had additions (no deletions), like
`src/constants.js` in svelte 5.16.2→5.50.0.

Root cause:
1. `parseUnifiedDiff` used a heuristic that reclassified files based on
   diff content: if a file had only insertions, it was marked as 'add'
   (new file). This was wrong for modifications with only additions.
2. `computeDiff` in ViewerPanel used `parsed.type` ('add') instead of
   the correctly computed `type` ('modify') based on file existence.
3. DiffLine intentionally skips green background when fileStatus='add'
   (all lines in a new file are additions, so highlighting is redundant).
   This caused addition-only modifications to lose visual highlighting.

Fix:
- `parseUnifiedDiff`: Replace the content-based heuristic with proper
  /dev/null path detection from --- / +++ headers, which is the standard
  way unified diffs indicate new/deleted files.
- `ViewerPanel.vue`: Use the file-existence-based type instead of the
  parsed diff type.
@taskyliz
Copy link
Contributor Author

taskyliz commented Feb 7, 2026

Checklist for this PR:

  • Fix RTL issues
  • Fix a11y issues
  • Rename from compare to diff in i18n keys
  • Identify and fix hydration issues
  • Design tweaks
  • Update URLs for packages /{package}/... to /package/{package}/...
  • Nicer mobile design
  • Whatever coderabbit is complaining about, lol
  • Broken layout on hard-reload for some reason feat: package diffing #356 (comment)

- SidebarPanel.vue: Replace bare `/${dep.name}` dependency links with
  `packageRoute(dep.name)` which resolves to /package/:org?/:name
- ViewerPanel.vue: Update code browser URLs from /code/ alias to
  canonical /package-code/ path
- diff/[...path].vue: Remove duplicated local packageRoute() function
  and use the shared utility from ~/utils/router instead
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
app/pages/diff/[...path].vue (2)

28-36: Guard against empty array elements after split.

The parts array could contain empty strings if the version range is malformed (e.g., ...5.0.0 or 4.0.0...). The non-null assertions on lines 35 are safe from an array-bounds perspective due to the length check, but the values could be empty strings, which may cause downstream issues.

🛡️ Proposed defensive check
   const parts = rangeStr.split('...')
-  if (parts.length !== 2) {
+  if (parts.length !== 2 || !parts[0] || !parts[1]) {
     return { packageName, range: null }
   }
 
   return {
     packageName,
     range: { from: parts[0]!, to: parts[1]! },
   }

201-208: Redundant event emission alongside v-model binding.

The DiffSidebarPanel already has v-model:selected-file="selectedFile" for two-way binding. The additional @file-select="selectedFile = $event" handler is redundant and may cause double-updates.

♻️ Proposed fix
         <DiffSidebarPanel
           :compare="compare"
           :grouped-deps="groupedDeps"
           :all-changes="allChanges"
           v-model:selected-file="selectedFile"
           v-model:file-filter="fileFilter"
-          `@file-select`="selectedFile = $event"
         />
app/components/diff/SidebarPanel.vue (1)

21-22: Consider using auto-imported $t() instead of destructuring from useI18n().

Based on learnings, this Nuxt 4 project with nuxtjs/i18n v10 exposes $t() globally in <script setup>. The pattern across other pages is to rely on the auto-imported $t() rather than destructuring t from useI18n(). However, both approaches work correctly.

♻️ Proposed change for consistency
 const sectionOrder = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
-const { t } = useI18n()
 const sectionMeta = computed<Record<string, { label: string; icon: string }>>(() => ({
-  dependencies: { label: t('compare.dependencies'), icon: 'i-carbon-cube' },
-  devDependencies: { label: t('compare.dev_dependencies'), icon: 'i-carbon-tools' },
-  peerDependencies: { label: t('compare.peer_dependencies'), icon: 'i-carbon-user-multiple' },
-  optionalDependencies: { label: t('compare.optional_dependencies'), icon: 'i-carbon-help' },
+  dependencies: { label: $t('compare.dependencies'), icon: 'i-carbon-cube' },
+  devDependencies: { label: $t('compare.dev_dependencies'), icon: 'i-carbon-tools' },
+  peerDependencies: { label: $t('compare.peer_dependencies'), icon: 'i-carbon-user-multiple' },
+  optionalDependencies: { label: $t('compare.optional_dependencies'), icon: 'i-carbon-help' },
 }))

Based on learnings: "In this Nuxt 4 project with nuxtjs/i18n v10, $t() and other globals like $n, $d are exposed in <script setup>... Do not destructure t from useI18n(); rely on the global provided by Nuxt i18n in script setup."

Comment on lines +199 to +208
<div class="relative">
<span
class="absolute left-2 top-1/2 -translate-y-1/2 i-carbon-search w-3 h-3 text-fg-subtle pointer-events-none"
/>
<input
v-model="fileSearch"
type="search"
:placeholder="$t('compare.search_files_placeholder')"
:aria-label="$t('compare.search_files_placeholder')"
class="w-full text-[11px] pl-6.5 pr-2 py-1 bg-bg-subtle border border-border rounded font-mono placeholder:text-fg-subtle transition-colors hover:border-border-hover focus:border-accent focus:outline-none"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix RTL compliance: replace physical positioning/padding with logical properties.

The pipeline reports multiple RTL linting errors on these lines. Use logical properties for proper right-to-left language support:

  • left-2inset-is-2 (inset-inline-start)
  • pl-6.5ps-6.5 (padding-inline-start)
  • pr-2pe-2 (padding-inline-end)
🔧 Proposed fix
         <div class="relative">
           <span
-            class="absolute left-2 top-1/2 -translate-y-1/2 i-carbon-search w-3 h-3 text-fg-subtle pointer-events-none"
+            class="absolute inset-is-2 top-1/2 -translate-y-1/2 i-carbon-search w-3 h-3 text-fg-subtle pointer-events-none"
           />
           <input
             v-model="fileSearch"
             type="search"
             :placeholder="$t('compare.search_files_placeholder')"
             :aria-label="$t('compare.search_files_placeholder')"
-            class="w-full text-[11px] pl-6.5 pr-2 py-1 bg-bg-subtle border border-border rounded font-mono placeholder:text-fg-subtle transition-colors hover:border-border-hover focus:border-accent focus:outline-none"
+            class="w-full text-[11px] ps-6.5 pe-2 py-1 bg-bg-subtle border border-border rounded font-mono placeholder:text-fg-subtle transition-colors hover:border-border-hover focus:border-accent focus:outline-none"
           />
         </div>
🧰 Tools
🪛 GitHub Actions: autofix.ci

[error] 201-201: RTL: avoid using 'left-2', use 'inset-is-2' instead.


[error] 208-208: RTL: avoid using 'pl-6.5', use 'ps-6.5' instead.


[error] 208-208: RTL: avoid using 'pr-2', use 'pe-2' instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

add version diff/comparison view between releases

4 participants