Skip to content

Commit 59f5224

Browse files
authored
chore/batch-load (#12)
* perf(deps): implement progressive loading Implement progressive loading with immediate npm fallback for faster perceived performance. Instead of waiting for all jsDelivr requests to complete before falling back to npm, each package now immediately tries npm if jsDelivr fails. Added batch callbacks to allow UI updates every 5 packages or 500ms, providing users with real-time feedback as packages load. * chore(ui): clarify progress message for version checking * docs: improve README with new structure and privacy section Restructure documentation with emoji headers, rewrite features section to emphasize benefits, add privacy policy, and remove peer/optional CLI options as dependencies are now loaded by default. * style(jsdelivr): format batch buffer code * docs(demo): adjust demo timing * refactor(deps): use jsdelivr cdn for package metadata Replace npm registry with jsdelivr CDN for fetching package metadata. This simplifies the code by removing the need to parse dist-tags and versions, as jsdelivr automatically resolves to the latest version.
1 parent f2452e6 commit 59f5224

8 files changed

Lines changed: 129 additions & 85 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,8 @@ on:
88

99
jobs:
1010
test:
11-
name: Test (Node ${{ matrix.node-version }})
11+
name: Test
1212
runs-on: ubuntu-latest
13-
14-
strategy:
15-
matrix:
16-
node-version: [20, 22, 24]
1713

1814
steps:
1915
- name: Checkout repository
@@ -25,7 +21,7 @@ jobs:
2521
- name: Setup Node.js
2622
uses: actions/setup-node@v6
2723
with:
28-
node-version: ${{ matrix.node-version }}
24+
node-version: 24
2925
cache: 'pnpm'
3026

3127
- name: Install dependencies

README.md

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
# inup
1+
# 🚀 inup
22

33
[![npm version](https://img.shields.io/npm/v/inup?logo=npm&logoColor=%23CB3837&style=for-the-badge&color=crimson)](https://www.npmjs.com/package/inup)
44
[![Downloads](https://img.shields.io/npm/dm/inup?style=for-the-badge&color=646CFF&logoColor=white)](https://www.npmjs.com/package/inup)
55
[![Total downloads](https://img.shields.io/npm/dt/inup?style=for-the-badge&color=informational)](https://www.npmjs.com/package/inup)
66

7-
Interactive upgrade for your dependencies. Works with npm, yarn, pnpm, and bun.
7+
Upgrade your dependencies interactively. Works with npm, yarn, pnpm, and bun.
88

99
![Interactive Upgrade Demo](docs/demo/interactive-upgrade.gif)
1010

11-
## Install
11+
## 🚀 Usage
1212

1313
```bash
1414
npx inup
@@ -20,24 +20,17 @@ Or install globally:
2020
npm install -g inup
2121
```
2222

23-
## Usage
24-
25-
```bash
26-
npx inup
27-
```
28-
2923
That's it. The tool scans your project, finds outdated packages, and lets you pick what to upgrade.
3024

31-
## Features
25+
## 💡 Why inup?
3226

33-
- Auto-detects package manager (npm, yarn, pnpm, bun)
34-
- Works with monorepos and workspaces
35-
- Batch upgrades with keyboard shortcuts
36-
- Search packages with `/`
37-
- Multiple themes (press `t`)
38-
- Package info modal (press `i`)
27+
- **Inclusive by Default**: We load Dev, Peer, and Optional dependencies automatically. No more restarting the tool because you forgot a `--peer` flag.
28+
- **Live Toggles**: Toggle dependency types (`d`, `p`, `o`) on the fly without exiting.
29+
- **Zero Config**: Auto-detects your package manager.
30+
- **Monorepo Ready**: Seamlessly handles workspaces.
31+
- **Modern UX**: Search with `/`, view package details with `i`, and swap themes with `t`.
3932

40-
## Keyboard Shortcuts
33+
## ⌨️ Keyboard Shortcuts
4134

4235
- `↑/↓` - Navigate packages
4336
- `←/→` - Select version (current, patch, minor, major)
@@ -50,18 +43,22 @@ That's it. The tool scans your project, finds outdated packages, and lets you pi
5043
- `i` - View package info
5144
- `Enter` - Confirm and upgrade
5245

53-
## Options
46+
## ⚙️ Options
5447

5548
```bash
5649
inup [options]
5750

5851
-d, --dir <path> Run in specific directory
5952
-e, --exclude <patterns> Skip directories (comma-separated regex)
60-
-p, --peer Include peer dependencies
61-
-o, --optional Include optional dependencies
6253
--package-manager <name> Force package manager (npm, yarn, pnpm, bun)
6354
```
6455

65-
## License
56+
## 🔒 Privacy
57+
58+
We don't track anything. Ever.
59+
60+
The only network requests made are to the npm registry and jsDelivr CDN to fetch package version data. That's it.
61+
62+
## 📄 License
6663

6764
MIT

docs/demo/demo-real.tape

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ Sleep 400ms
100100

101101
# Press 'i' to show info modal for current package
102102
Type "i"
103-
Sleep 1.5s
103+
Sleep 2s
104104

105105
# Press 'i' again to toggle info modal
106106
Type "i"

docs/demo/interactive-upgrade.gif

-15.5 KB
Loading

src/core/package-detector.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -77,24 +77,14 @@ export class PackageDetector {
7777
? await getAllPackageDataFromJsdelivr(
7878
packageNames,
7979
currentVersions,
80-
(currentPackage: string, completed: number, total: number) => {
81-
const percentage = Math.round((completed / total) * 100)
82-
const truncatedPackage =
83-
currentPackage.length > 40
84-
? currentPackage.substring(0, 37) + '...'
85-
: currentPackage
86-
this.showProgress(`🌐 Fetching ${percentage}% (${truncatedPackage})`)
80+
(_currentPackage: string, completed: number, total: number) => {
81+
this.showProgress(`🌐 Checking versions... (${completed}/${total} packages)`)
8782
}
8883
)
8984
: await getAllPackageData(
9085
packageNames,
91-
(currentPackage: string, completed: number, total: number) => {
92-
const percentage = Math.round((completed / total) * 100)
93-
const truncatedPackage =
94-
currentPackage.length > 40
95-
? currentPackage.substring(0, 37) + '...'
96-
: currentPackage
97-
this.showProgress(`🌐 Fetching ${percentage}% (${truncatedPackage})`)
86+
(_currentPackage: string, completed: number, total: number) => {
87+
this.showProgress(`🌐 Checking versions... (${completed}/${total} packages)`)
9888
}
9989
)
10090

src/services/changelog-fetcher.ts

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import chalk from 'chalk'
2+
import { JSDELIVR_CDN_URL } from '../config/constants'
23

34
export interface PackageMetadata {
45
description: string
@@ -95,13 +96,14 @@ export class ChangelogFetcher {
9596
}
9697

9798
/**
98-
* Fetch data from npm registry
99-
* Returns the package data from the registry
99+
* Fetch data from jsdelivr CDN
100+
* Returns the package data by fetching package.json directly from jsdelivr
100101
*/
101102
private async fetchFromRegistry(packageName: string): Promise<any> {
102103
try {
104+
// Fetch package.json directly from jsdelivr CDN (resolves to latest automatically)
103105
const response = await fetch(
104-
`https://registry.npmjs.org/${encodeURIComponent(packageName)}`,
106+
`${JSDELIVR_CDN_URL}/${encodeURIComponent(packageName)}@latest/package.json`,
105107
{
106108
method: 'GET',
107109
headers: {
@@ -114,21 +116,16 @@ export class ChangelogFetcher {
114116
return null
115117
}
116118

117-
const data = (await response.json()) as Record<string, unknown>
118-
// Get the latest version data
119-
const distTags = data['dist-tags'] as Record<string, string> | undefined
120-
const latestVersion = distTags?.latest
121-
const versions = data.versions as Record<string, any> | undefined
122-
const latestPackageData = latestVersion ? versions?.[latestVersion] : undefined
119+
const pkgData = (await response.json()) as Record<string, unknown>
123120

124121
return {
125-
description: data.description,
126-
homepage: (data.homepage || latestPackageData?.homepage) as string | undefined,
127-
repository: (data.repository || latestPackageData?.repository) as any,
128-
bugs: (data.bugs || latestPackageData?.bugs) as any,
129-
keywords: (data.keywords || []) as string[],
130-
author: (data.author || latestPackageData?.author) as any,
131-
license: (data.license || latestPackageData?.license) as string | undefined,
122+
description: pkgData.description,
123+
homepage: pkgData.homepage as string | undefined,
124+
repository: pkgData.repository as any,
125+
bugs: pkgData.bugs as any,
126+
keywords: (pkgData.keywords || []) as string[],
127+
author: pkgData.author as any,
128+
license: pkgData.license as string | undefined,
132129
}
133130
} catch {
134131
return null

src/services/jsdelivr-registry.ts

Lines changed: 87 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Pool, request } from 'undici'
22
import * as semver from 'semver'
33
import { CACHE_TTL, JSDELIVR_CDN_URL, MAX_CONCURRENT_REQUESTS, REQUEST_TIMEOUT } from '../config'
44
import { getAllPackageData } from './npm-registry'
5+
import { OnBatchReadyCallback } from '../types'
56

67
// Create a persistent connection pool for jsDelivr CDN with optimal settings
78
// This enables connection reuse and HTTP/1.1 keep-alive for blazing fast requests
@@ -13,6 +14,10 @@ const jsdelivrPool = new Pool('https://cdn.jsdelivr.net', {
1314
connectTimeout: REQUEST_TIMEOUT, // 60 seconds connect timeout
1415
})
1516

17+
// Batch configuration for progressive loading
18+
const BATCH_SIZE = 5
19+
const BATCH_TIMEOUT_MS = 500
20+
1621
// In-memory cache for package data
1722
interface CacheEntry {
1823
data: { latestVersion: string; allVersions: string[] }
@@ -62,16 +67,19 @@ async function fetchPackageJsonFromJsdelivr(
6267
/**
6368
* Fetches package version data from jsdelivr CDN for multiple packages.
6469
* Uses undici connection pool for blazing fast performance with connection reuse.
65-
* Falls back to npm registry in batches if jsdelivr doesn't have packages.
70+
* Falls back to npm registry immediately when jsdelivr fails (interleaved, not sequential).
71+
* Supports batched callbacks for progressive UI updates.
6672
* @param packageNames - Array of package names to fetch
6773
* @param currentVersions - Optional map of package names to their current versions
6874
* @param onProgress - Optional progress callback
75+
* @param onBatchReady - Optional callback for batch updates (fires every BATCH_SIZE packages or BATCH_TIMEOUT_MS)
6976
* @returns Map of package names to their version data
7077
*/
7178
export async function getAllPackageDataFromJsdelivr(
7279
packageNames: string[],
7380
currentVersions?: Map<string, string>,
74-
onProgress?: (currentPackage: string, completed: number, total: number) => void
81+
onProgress?: (currentPackage: string, completed: number, total: number) => void,
82+
onBatchReady?: OnBatchReadyCallback
7583
): Promise<Map<string, { latestVersion: string; allVersions: string[] }>> {
7684
const packageData = new Map<string, { latestVersion: string; allVersions: string[] }>()
7785

@@ -82,11 +90,43 @@ export async function getAllPackageDataFromJsdelivr(
8290
const total = packageNames.length
8391
let completedCount = 0
8492

85-
// Track packages that need npm fallback (not found on jsDelivr)
86-
const failedPackages: string[] = []
93+
// Batch buffer for progressive updates
94+
let batchBuffer: Array<{ name: string; data: { latestVersion: string; allVersions: string[] } }> =
95+
[]
96+
let batchTimer: NodeJS.Timeout | null = null
97+
98+
// Helper to flush the current batch
99+
const flushBatch = () => {
100+
if (batchBuffer.length > 0 && onBatchReady) {
101+
onBatchReady([...batchBuffer])
102+
batchBuffer = []
103+
}
104+
if (batchTimer) {
105+
clearTimeout(batchTimer)
106+
batchTimer = null
107+
}
108+
}
109+
110+
// Helper to add package to batch and flush if needed
111+
const addToBatch = (
112+
packageName: string,
113+
data: { latestVersion: string; allVersions: string[] }
114+
) => {
115+
if (onBatchReady) {
116+
batchBuffer.push({ name: packageName, data })
87117

88-
// Fire all jsDelivr requests simultaneously - undici pool handles concurrency internally
89-
const allPromises = packageNames.map(async (packageName) => {
118+
// Flush if batch is full
119+
if (batchBuffer.length >= BATCH_SIZE) {
120+
flushBatch()
121+
} else if (!batchTimer) {
122+
// Set timer to flush batch after timeout
123+
batchTimer = setTimeout(flushBatch, BATCH_TIMEOUT_MS)
124+
}
125+
}
126+
}
127+
128+
// Process individual package fetch with immediate npm fallback on failure
129+
const fetchPackageWithFallback = async (packageName: string): Promise<void> => {
90130
const currentVersion = currentVersions?.get(packageName)
91131

92132
// Try to get from cache first
@@ -97,6 +137,7 @@ export async function getAllPackageDataFromJsdelivr(
97137
if (onProgress) {
98138
onProgress(packageName, completedCount, total)
99139
}
140+
addToBatch(packageName, cached.data)
100141
return
101142
}
102143

@@ -122,8 +163,23 @@ export async function getAllPackageDataFromJsdelivr(
122163
const majorResult = results[1]
123164

124165
if (!latestResult) {
125-
// Package not on jsDelivr, mark for npm fallback
126-
failedPackages.push(packageName)
166+
// Package not on jsDelivr, immediately try npm fallback
167+
const npmData = await getAllPackageData([packageName])
168+
const result = npmData.get(packageName)
169+
170+
if (result) {
171+
packageData.set(packageName, result)
172+
packageCache.set(packageName, {
173+
data: result,
174+
timestamp: Date.now(),
175+
})
176+
addToBatch(packageName, result)
177+
}
178+
179+
completedCount++
180+
if (onProgress) {
181+
onProgress(packageName, completedCount, total)
182+
}
127183
return
128184
}
129185

@@ -152,34 +208,38 @@ export async function getAllPackageDataFromJsdelivr(
152208
if (onProgress) {
153209
onProgress(packageName, completedCount, total)
154210
}
211+
addToBatch(packageName, result)
155212
} catch (error) {
156-
// On error, mark for npm fallback
157-
failedPackages.push(packageName)
158-
}
159-
})
213+
// On error, immediately try npm fallback
214+
try {
215+
const npmData = await getAllPackageData([packageName])
216+
const result = npmData.get(packageName)
160217

161-
// Wait for all jsDelivr requests to complete
162-
await Promise.all(allPromises)
218+
if (result) {
219+
packageData.set(packageName, result)
220+
packageCache.set(packageName, {
221+
data: result,
222+
timestamp: Date.now(),
223+
})
224+
addToBatch(packageName, result)
225+
}
226+
} catch (npmError) {
227+
// If both fail, just continue
228+
}
163229

164-
// Batch fetch all failed packages from npm registry in one call
165-
if (failedPackages.length > 0) {
166-
const npmData = await getAllPackageData(failedPackages, (pkg, completed, npmTotal) => {
167230
completedCount++
168231
if (onProgress) {
169-
onProgress(pkg, completedCount, total)
232+
onProgress(packageName, completedCount, total)
170233
}
171-
})
172-
173-
// Merge npm data into results and cache it
174-
for (const [packageName, data] of npmData.entries()) {
175-
packageData.set(packageName, data)
176-
packageCache.set(packageName, {
177-
data,
178-
timestamp: Date.now(),
179-
})
180234
}
181235
}
182236

237+
// Fire all requests simultaneously - they handle fallback internally and immediately
238+
await Promise.all(packageNames.map(fetchPackageWithFallback))
239+
240+
// Flush any remaining batch items
241+
flushBatch()
242+
183243
// Clear the progress line and show completion time if no custom progress handler
184244
if (!onProgress) {
185245
process.stdout.write('\r' + ' '.repeat(80) + '\r')

src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,7 @@ export interface PackageJson {
8181
workspaces?: string[] | { packages: string[] }
8282
[key: string]: any
8383
}
84+
85+
export type OnBatchReadyCallback = (
86+
batch: Array<{ name: string; data: { latestVersion: string; allVersions: string[] } }>
87+
) => void

0 commit comments

Comments
 (0)