Skip to content
Merged
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
11 changes: 7 additions & 4 deletions docs/demo/demo-real.tape
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,15 @@ Sleep 600ms
Type "express"
Sleep 800ms

# Navigate down to see the express package
# Press Enter to apply the search filter
Enter
Sleep 600ms

# Show the applied search UI - navigate and select versions
Down
Sleep 400ms

# Show version selection in search mode - press right to see patch version
# Show version selection - press right to select
Right
Sleep 600ms

Expand All @@ -69,8 +73,7 @@ Sleep 600ms
Left
Sleep 400ms


# Exit search mode with Escape
# Clear search filter with Escape to show all packages again
Ctrl+[
Sleep 600ms

Expand Down
Binary file modified docs/demo/interactive-upgrade.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { UpgradeRunner } from './index'
import { checkForUpdateAsync } from './services'
import { loadProjectConfig } from './config'
import { PackageManager } from './types'
import { enableDebugLogging } from './utils'

const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'))

Expand All @@ -21,6 +22,7 @@ program
.option('-e, --exclude <patterns>', 'exclude paths matching regex patterns (comma-separated)', '')
.option('-i, --ignore <packages>', 'ignore packages (comma-separated, supports glob patterns like @babel/*)')
.option('--package-manager <name>', 'manually specify package manager (npm, yarn, pnpm, bun)')
.option('--debug', 'write verbose debug log to /tmp/inup-debug-YYYY-MM-DD.log')
.action(async (options) => {
console.log(chalk.bold.blue(`🚀 `) + chalk.bold.red(`i`) + chalk.bold.yellow(`n`) + chalk.bold.blue(`u`) + chalk.bold.magenta(`p`) + `\n`)

Expand All @@ -29,6 +31,10 @@ program

const cwd = resolve(options.dir)

if (options.debug || process.env.INUP_DEBUG === '1') {
enableDebugLogging()
}

// Load project config from .inuprc
const projectConfig = loadProjectConfig(cwd)

Expand Down Expand Up @@ -67,6 +73,7 @@ program
excludePatterns,
ignorePackages,
packageManager,
debug: options.debug || process.env.INUP_DEBUG === '1',
})
await upgrader.run()

Expand Down
3 changes: 3 additions & 0 deletions src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ export const JSDELIVR_CDN_URL = 'https://cdn.jsdelivr.net/npm'
export const MAX_CONCURRENT_REQUESTS = 150
export const CACHE_TTL = 5 * 60 * 1000 // 5 minutes in milliseconds
export const REQUEST_TIMEOUT = 60000 // 60 seconds in milliseconds
export const JSDELIVR_RETRY_TIMEOUTS = [2000, 3500] // short retry budget to keep fallback fast
export const JSDELIVR_RETRY_DELAYS = [150] // tiny backoff between jsDelivr retries in ms
export const JSDELIVR_POOL_TIMEOUT = 60000 // keep-alive/connect lifecycle should be looser than per-request timeouts
export const DEFAULT_REGISTRY: 'jsdelivr' | 'npm' = 'jsdelivr'
60 changes: 59 additions & 1 deletion src/core/package-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { getAllPackageDataFromJsdelivr, getAllPackageData } from '../services'
import { DEFAULT_REGISTRY, isPackageIgnored } from '../config'
import { ConsoleUtils } from '../ui/utils'
import { debugLog } from '../utils'

export class PackageDetector {
private packageJsonPath: string | null = null
Expand Down Expand Up @@ -38,32 +39,51 @@ export class PackageDetector {
}

const packages: PackageInfo[] = []
const t0 = Date.now()
debugLog.info('PackageDetector', `Starting scan in ${this.cwd}`)

// Always check all package.json files recursively with timeout protection
this.showProgress('🔍 Scanning repository for package.json files...')
const tScan = Date.now()
const allPackageJsonFiles = this.findPackageJsonFilesWithTimeout(30000) // 30 second timeout
debugLog.perf('PackageDetector', `file scan (${allPackageJsonFiles.length} files)`, tScan, {
files: allPackageJsonFiles,
})
this.showProgress(
`🔍 Found ${allPackageJsonFiles.length} package.json file${allPackageJsonFiles.length === 1 ? '' : 's'}`
)

// Step 2: Collect all dependencies from package.json files (parallelized)
this.showProgress('🔍 Reading dependencies from package.json files...')
const tDeps = Date.now()
const allDepsRaw = await collectAllDependenciesAsync(allPackageJsonFiles, {
includePeerDeps: true,
includeOptionalDeps: true,
})
debugLog.perf('PackageDetector', `dependency collection (${allDepsRaw.length} raw deps)`, tDeps)

// Step 3: Get unique package names while filtering out workspace references and ignored packages
this.showProgress('🔍 Identifying unique packages...')
const uniquePackageNames = new Set<string>()
const allDeps: typeof allDepsRaw = []
let ignoredCount = 0
const seenWorkspaceRefs = new Set<string>()
const seenIgnored = new Set<string>()
for (const dep of allDepsRaw) {
if (this.isWorkspaceReference(dep.version)) {
const key = `${dep.name}@${dep.version}`
if (!seenWorkspaceRefs.has(key)) {
seenWorkspaceRefs.add(key)
debugLog.info('PackageDetector', `skipping workspace ref: ${key}`)
}
continue
}
if (this.ignorePackages.length > 0 && isPackageIgnored(dep.name, this.ignorePackages)) {
ignoredCount++
if (!seenIgnored.has(dep.name)) {
seenIgnored.add(dep.name)
debugLog.info('PackageDetector', `ignoring package: ${dep.name}`)
}
continue
}
allDeps.push(dep)
Expand All @@ -73,6 +93,10 @@ export class PackageDetector {
this.showProgress(`🔍 Skipped ${ignoredCount} ignored package(s)`)
}
const packageNames = Array.from(uniquePackageNames)
debugLog.info(
'PackageDetector',
`${packageNames.length} unique packages to check, ${ignoredCount} ignored`
)

// Step 4: Fetch all package data in one call per package
// Create a map of package names to their current versions for major version optimization
Expand All @@ -84,6 +108,8 @@ export class PackageDetector {
}
}

const tFetch = Date.now()
debugLog.info('PackageDetector', `fetching version data via ${DEFAULT_REGISTRY}`)
const allPackageData =
DEFAULT_REGISTRY === 'jsdelivr'
? await getAllPackageDataFromJsdelivr(
Expand All @@ -99,12 +125,25 @@ export class PackageDetector {
this.showProgress(`🌐 Checking versions... (${completed}/${total} packages)`)
}
)
debugLog.perf(
'PackageDetector',
`registry fetch (${allPackageData.size}/${packageNames.length} resolved)`,
tFetch
)

const loggedOutdated = new Set<string>()
const loggedNoData = new Set<string>()
try {
for (const dep of allDeps) {
try {
const packageData = allPackageData.get(dep.name)
if (!packageData) continue
if (!packageData) {
if (!loggedNoData.has(dep.name)) {
loggedNoData.add(dep.name)
debugLog.warn('PackageDetector', `no data returned for ${dep.name} — skipping`)
}
continue
}

const { latestVersion, allVersions } = packageData

Expand All @@ -122,6 +161,17 @@ export class PackageDetector {
const hasMajorUpdate = semver.major(latestClean) > semver.major(installedClean)
const isOutdated = hasRangeUpdate || hasMajorUpdate

if (isOutdated) {
const outdatedKey = `${dep.name}@${dep.version}`
if (!loggedOutdated.has(outdatedKey)) {
loggedOutdated.add(outdatedKey)
debugLog.info(
'PackageDetector',
`outdated: ${dep.name} ${dep.version} → range:${closestMinorVersion ?? '-'} latest:${latestVersion}`
)
}
}

packages.push({
name: dep.name,
currentVersion: dep.version, // Keep original version specifier with prefix
Expand All @@ -138,6 +188,7 @@ export class PackageDetector {
hasMajorUpdate,
})
} catch (error) {
debugLog.error('PackageDetector', `error processing ${dep.name}`, error)
// Skip packages that can't be checked (private packages, etc.)
packages.push({
name: dep.name,
Expand All @@ -157,9 +208,16 @@ export class PackageDetector {
}
}

const outdatedCount = packages.filter((p) => p.isOutdated).length
debugLog.perf(
'PackageDetector',
`total scan complete (${outdatedCount} outdated of ${packages.length} deps)`,
t0
)
return packages
} catch (error) {
this.showProgress('❌ Failed to check packages\n')
debugLog.error('PackageDetector', 'fatal error during package check', error)
throw error
}
}
Expand Down
Loading
Loading