Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions src/core/package-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface PreparedDependencies {
allDependencies: DependencyEntry[]
uniquePackages: string[]
currentVersions: Map<string, string>
ignoredDependencies: DependencyEntry[]
}

export class PackageDetector {
Expand Down Expand Up @@ -87,6 +88,7 @@ export class PackageDetector {
allDependencies: prepared.allDependencies,
uniquePackages: prepared.uniquePackages,
currentVersions: prepared.currentVersions,
ignoredDependencies: prepared.ignoredDependencies,
progress: this.createProgressSnapshot(prepared.uniquePackages.length, 0, 0, true),
}

Expand Down Expand Up @@ -223,6 +225,7 @@ export class PackageDetector {
const tFilter = Date.now()
const uniquePackageNames = new Set<string>()
const allDependencies: DependencyEntry[] = []
const ignoredDependencies: DependencyEntry[] = []
let ignoredCount = 0
const seenWorkspaceRefs = new Set<string>()
const seenIgnored = new Set<string>()
Expand All @@ -239,6 +242,12 @@ export class PackageDetector {

if (this.ignorePackages.length > 0 && isPackageIgnored(dep.name, this.ignorePackages)) {
ignoredCount++
ignoredDependencies.push({
name: dep.name,
version: dep.version,
type: dep.type as DependencyEntry['type'],
packageJsonPath: dep.packageJsonPath,
})
if (!seenIgnored.has(dep.name)) {
seenIgnored.add(dep.name)
debugLog.info('PackageDetector', `ignoring package: ${dep.name}`)
Expand Down Expand Up @@ -289,6 +298,7 @@ export class PackageDetector {
allDependencies,
uniquePackages,
currentVersions,
ignoredDependencies,
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/core/upgrade-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ export class UpgradeRunner {
progress.failed = event.payload.progress.failed
progress.isLoading = event.payload.progress.isLoading

selectionStates = []
// Seed with display-only ignored rows (grayed out, never fetched
// or selected) so they appear inline from the first render.
selectionStates = this.ui.createIgnoredSelectionStates(
event.payload.ignoredDependencies
)

this.ui
.selectPackagesToUpgradeProgressive(selectionStates, progress, (refresh) => {
Expand Down
16 changes: 16 additions & 0 deletions src/interactive-ui.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Key } from 'node:readline'
import {
DependencyEntry,
PackageLoadProgress,
PackageInfo,
PackageUpgradeChoice,
Expand All @@ -18,7 +19,9 @@ import { PackageInfoModalController, VulnerabilityAuditController } from './ui/c
import {
createSelectionStates,
createPendingSelectionStates,
createIgnoredSelectionStates,
createUpgradeChoices,
comparePackageNames,
runInteractiveSession,
} from './ui/session'

Expand Down Expand Up @@ -111,6 +114,10 @@ export class InteractiveUI {
)
}

public createIgnoredSelectionStates(ignoredDeps: DependencyEntry[]): PackageSelectionState[] {
return createIgnoredSelectionStates(ignoredDeps)
}

public appendOutdatedBatchToSelectionStates(
selectionStates: PackageSelectionState[],
batch: StreamOutdatedPackagesBatchItem[],
Expand All @@ -130,14 +137,23 @@ export class InteractiveUI {
selectionStates.map((state) => `${state.name}@${state.currentVersionSpecifier}@${state.type}`)
)

let added = false
outdatedStates.forEach((state) => {
const key = `${state.name}@${state.currentVersionSpecifier}@${state.type}`
if (!seen.has(key)) {
selectionStates.push(state)
seen.add(key)
added = true
}
})

// Streamed batches arrive in fetch order; re-sort so they interleave
// alphabetically with the ignored rows seeded up front (Array.sort is
// stable, so equal-name duplicates keep their relative order).
if (added) {
selectionStates.sort((a, b) => comparePackageNames(a.name, b.name))
}

this.enqueueSecurityAudit(selectionStates)
}

Expand Down
2 changes: 1 addition & 1 deletion src/types/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export interface DependencyEntry {
packageJsonPath: string
}

export type PackageLoadState = 'pending' | 'ready' | 'failed'
export type PackageLoadState = 'pending' | 'ready' | 'failed' | 'ignored'

export interface PackageUpgradeChoice {
name: string
Expand Down
1 change: 1 addition & 0 deletions src/types/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface StreamOutdatedPackagesInitialPayload {
allDependencies: DependencyEntry[]
uniquePackages: string[]
currentVersions: Map<string, string>
ignoredDependencies: DependencyEntry[]
progress: PackageLoadProgress
}

Expand Down
10 changes: 6 additions & 4 deletions src/ui/controllers/vulnerability-audit-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ export class VulnerabilityAuditController {
}

enqueueStates(selectionStates: PackageSelectionState[], onUpdate?: () => void): void {
const packages = selectionStates.map((state) => ({
name: state.name,
version: state.currentVersionSpecifier,
}))
const packages = selectionStates
.filter((state) => state.loadState !== 'ignored')
.map((state) => ({
name: state.name,
version: state.currentVersionSpecifier,
}))

const added = this.tracker.enqueue(packages)
if (added > 0) {
Expand Down
5 changes: 5 additions & 0 deletions src/ui/renderer/package-list/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,17 +145,22 @@ export function renderInterface(
statusLine =
matchCount + ' ' + chalk.bold.white('Esc ') + chalk.gray('Clear filter')
} else {
const ignoredCount = states.filter((state) => state.loadState === 'ignored').length
const ignoredSuffix =
ignoredCount > 0 ? chalk.gray(` (${chalk.white(ignoredCount)} ignored)`) : ''
if (totalVisualItems > maxVisibleItems) {
statusLine =
chalk.gray(
`Showing ${chalk.white(startItem)}-${chalk.white(endItem)} of ${chalk.white(totalPackages)} packages`
) +
ignoredSuffix +
' ' +
chalk.bold.white('Enter ') +
chalk.gray('Confirm')
} else {
statusLine =
chalk.gray(`Showing all ${chalk.white(totalPackages)} packages`) +
ignoredSuffix +
' ' +
chalk.bold.white('Enter ') +
chalk.gray('Confirm')
Expand Down
36 changes: 26 additions & 10 deletions src/ui/renderer/package-list/rows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,15 @@ export function renderPackageLine(
terminalWidth: number = 80,
options: PackageListRenderOptions = {}
): string {
const prefix = isCurrentRow ? getThemeColor('success')('❯ ') : ' '
const isIgnored = state.loadState === 'ignored'
// Ignored rows are never the cursor target (navigation skips them), but guard
// the caret defensively so it never appears on a disabled row.
const prefix = isCurrentRow && !isIgnored ? getThemeColor('success')('❯ ') : ' '

let packageName
if (state.name.startsWith('@')) {
if (isIgnored) {
packageName = chalk.gray(state.name)
} else if (state.name.startsWith('@')) {
const parts = state.name.split('/')
if (parts.length >= 2) {
const author = parts[0]
Expand Down Expand Up @@ -67,12 +72,21 @@ export function renderPackageLine(
const isPending = state.loadState === 'pending'
const isFailed = state.loadState === 'failed'

const currentDot = isCurrentSelected ? getThemeColor('dot')('●') : getThemeColor('dotEmpty')('○')
const currentVersion = chalk.white(state.currentVersionSpecifier)
const currentDot = isIgnored
? getThemeColor('dotEmpty')('○')
: isCurrentSelected
? getThemeColor('dot')('●')
: getThemeColor('dotEmpty')('○')
const currentVersion = isIgnored
? chalk.gray(state.currentVersionSpecifier)
: chalk.white(state.currentVersionSpecifier)

let rangeDot = ''
let rangeVersionText = ''
if (isPending) {
if (isIgnored) {
rangeDot = ''
rangeVersionText = chalk.gray('(ignored)')
} else if (isPending) {
rangeDot = getThemeColor('dotEmpty')('◌')
rangeVersionText = chalk.gray('loading')
} else if (isFailed) {
Expand Down Expand Up @@ -136,11 +150,13 @@ export function renderPackageLine(
const displayName = truncatedName !== state.name ? truncatedName : packageName

const typeBadge = getTypeBadge(state.type)
const shouldShowVulnerability = shouldDisplayVulnerabilityForDependency(state.type, options)
// Ignored rows carry no vuln/health data and are display-only; suppress badges.
const shouldShowVulnerability =
!isIgnored && shouldDisplayVulnerabilityForDependency(state.type, options)
const vulnBadge = shouldShowVulnerability ? getVulnerabilityBadge(state.vulnerability) : ''
const vulnBadgeWidth = vulnBadge ? VersionUtils.getVisualLength(vulnBadge) + 1 : 0
// Deprecation / engines-incompatibility marker (independent of dep type).
const healthBadge = getHealthBadge(state)
const healthBadge = isIgnored ? '' : getHealthBadge(state)
const healthBadgeWidth = healthBadge ? VersionUtils.getVisualLength(healthBadge) + 1 : 0
const nameLength = VersionUtils.getVisualLength(truncatedName)
const namePadding = Math.max(
Expand All @@ -157,7 +173,7 @@ export function renderPackageLine(
? `${displayName} ${nameDashes}${vulnSuffix}${healthSuffix}${typeBadge}`
: `${displayName} ${nameDashes}${vulnSuffix}${healthSuffix}`

const currentSection = `${currentDot} ${currentVersion}`
const currentSection = isIgnored ? ` ${currentVersion}` : `${currentDot} ${currentVersion}`
const currentSectionLength = VersionUtils.getVisualLength(currentSection) + 1
const currentPadding = Math.max(0, currentColumnWidth - currentSectionLength)
const currentPaddingText = shouldShowDashes(currentPadding)
Expand All @@ -166,8 +182,8 @@ export function renderPackageLine(
const currentWithPadding = currentSection + ' ' + currentPaddingText

let rangeSection = ''
if (isPending || isFailed || state.hasRangeUpdate) {
rangeSection = `${rangeDot} ${rangeVersionText}`
if (isIgnored || isPending || isFailed || state.hasRangeUpdate) {
rangeSection = rangeDot ? `${rangeDot} ${rangeVersionText}` : rangeVersionText
const rangeSectionLength = VersionUtils.getVisualLength(rangeSection) + 1
const rangePadding = Math.max(0, rangeColumnWidth - rangeSectionLength)
const rangePaddingText = shouldShowDashes(rangePadding)
Expand Down
8 changes: 4 additions & 4 deletions src/ui/session/action-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,16 @@ export function dispatchAction(action: InputAction, ctx: DispatchContext): void

switch (action.type) {
case 'navigate_up':
stateManager.navigateUp(filteredStates.length)
stateManager.navigateUp(filteredStates)
break
case 'navigate_down':
stateManager.navigateDown(filteredStates.length)
stateManager.navigateDown(filteredStates)
break
case 'navigate_top':
stateManager.navigateTop(filteredStates.length)
stateManager.navigateTop(filteredStates)
break
case 'navigate_bottom':
stateManager.navigateBottom(filteredStates.length)
stateManager.navigateBottom(filteredStates)
break
case 'select_left':
stateManager.updateSelection(filteredStates, 'left')
Expand Down
2 changes: 2 additions & 0 deletions src/ui/session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ export { runInteractiveSession } from './interactive-session'
export {
createSelectionStates,
createPendingSelectionStates,
createIgnoredSelectionStates,
createUpgradeChoices,
deduplicatePackages,
comparePackageNames,
} from './selection-state-builder'
export { dispatchAction } from './action-dispatcher'
export type { DispatchContext } from './action-dispatcher'
6 changes: 5 additions & 1 deletion src/ui/session/interactive-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,12 @@ export async function runInteractiveSession(
}

const renderInterface = () => {
const uiState = stateManager.getUIState()
const filteredStates = stateManager.getFilteredStates(states, vulnerabilityDisplayOptions)
// Keep the cursor off display-only ignored rows. Self-heals on the first
// render and after streamed batches re-sort the list around seeded rows.
// Must run before getUIState() so the render uses the corrected row.
stateManager.ensureCursorOnNavigable(filteredStates)
const uiState = stateManager.getUIState()
const auditProgress = vulnerabilityAuditController.getProgress()

const bgCode = getTerminalBgColorCode()
Expand Down
67 changes: 60 additions & 7 deletions src/ui/session/selection-state-builder.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import * as semver from 'semver'
import {
DependencyEntry,
PackageInfo,
PackageSelectionState,
PackageUpgradeChoice,
VulnerabilitySummary,
} from '../../types'
import { applyVersionPrefix } from '../utils'

/**
* Shared ordering for package lists: scoped packages (@…) first, then
* alphabetical. Used by deduplicatePackages and by the append-sort that keeps
* streamed-in outdated rows interleaved with seeded ignored rows.
*/
export function comparePackageNames(a: string, b: string): number {
const aIsScoped = a.startsWith('@')
const bIsScoped = b.startsWith('@')
if (aIsScoped && !bIsScoped) return -1
if (!aIsScoped && bIsScoped) return 1
return a.localeCompare(b)
}

type CachedSummaryFn = (
name: string,
version: string,
Expand All @@ -31,13 +45,9 @@ export function deduplicatePackages(
}

return new Map(
Array.from(uniquePackages.entries()).sort(([, a], [, b]) => {
const aIsScoped = a.pkg.name.startsWith('@')
const bIsScoped = b.pkg.name.startsWith('@')
if (aIsScoped && !bIsScoped) return -1
if (!aIsScoped && bIsScoped) return 1
return a.pkg.name.localeCompare(b.pkg.name)
})
Array.from(uniquePackages.entries()).sort(([, a], [, b]) =>
comparePackageNames(a.pkg.name, b.pkg.name)
)
)
}

Expand Down Expand Up @@ -117,6 +127,49 @@ export function createPendingSelectionStates(
})
}

/**
* Build display-only states for packages matched by the `.inuprc` ignore list.
* These are rendered grayed-out and are never fetched, selected, or upgraded —
* the `loadState: 'ignored'` value gates them out of every selection guard and
* out of createUpgradeChoices (which requires loadState === 'ready').
*/
export function createIgnoredSelectionStates(
ignoredDeps: DependencyEntry[]
): PackageSelectionState[] {
const uniquePackages = deduplicatePackages(
ignoredDeps.map((dep) => ({
name: dep.name,
currentVersion: dep.version,
rangeVersion: dep.version,
latestVersion: dep.version,
type: dep.type,
packageJsonPath: dep.packageJsonPath,
isOutdated: false,
hasRangeUpdate: false,
hasMajorUpdate: false,
}))
)

return Array.from(uniquePackages.values()).map(({ pkg, packageJsonPaths }) => {
const currentClean = semver.coerce(pkg.currentVersion)?.version || pkg.currentVersion

return {
name: pkg.name,
packageJsonPath: pkg.packageJsonPath,
packageJsonPaths: Array.from(packageJsonPaths),
currentVersionSpecifier: pkg.currentVersion,
currentVersion: currentClean,
rangeVersion: currentClean,
latestVersion: currentClean,
selectedOption: 'none',
loadState: 'ignored',
hasRangeUpdate: false,
hasMajorUpdate: false,
type: pkg.type,
}
})
}

export function createUpgradeChoices(
selectedStates: PackageSelectionState[],
saveExact: boolean = false
Expand Down
Loading
Loading