inup 1.4.10 → 1.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -54,74 +54,116 @@ class InteractiveUI {
54
54
  console.log(this.renderer.renderPackagesTable(packages));
55
55
  }
56
56
  async selectPackagesToUpgrade(packages, previousSelections) {
57
- const outdatedPackages = packages.filter((p) => p.isOutdated);
58
- if (outdatedPackages.length === 0) {
57
+ const selectionStates = this.createSelectionStates(packages, previousSelections, false);
58
+ if (selectionStates.length === 0) {
59
59
  return [];
60
60
  }
61
- // Deduplicate packages by name and version specifier, but track all package.json paths
62
- const uniquePackages = new Map();
63
- for (const pkg of outdatedPackages) {
64
- const key = `${pkg.name}@${pkg.currentVersion}@${pkg.type}`;
65
- if (!uniquePackages.has(key)) {
66
- uniquePackages.set(key, {
67
- pkg,
68
- packageJsonPaths: new Set([pkg.packageJsonPath]),
69
- type: pkg.type,
70
- });
71
- }
72
- else {
73
- uniquePackages.get(key).packageJsonPaths.add(pkg.packageJsonPath);
74
- }
75
- }
76
- // Convert to array and sort alphabetically by name (@scoped packages first, then unscoped)
77
- const deduplicatedPackages = Array.from(uniquePackages.values()).map(({ pkg, packageJsonPaths, type }) => ({
78
- ...pkg,
79
- packageJsonPaths: Array.from(packageJsonPaths),
80
- type,
81
- }));
82
- deduplicatedPackages.sort((a, b) => {
83
- const aIsScoped = a.name.startsWith('@');
84
- const bIsScoped = b.name.startsWith('@');
85
- // If one is scoped and the other isn't, scoped comes first
86
- if (aIsScoped && !bIsScoped)
87
- return -1;
88
- if (!aIsScoped && bIsScoped)
89
- return 1;
90
- // Both scoped or both unscoped - sort alphabetically
91
- return a.name.localeCompare(b.name);
92
- });
93
- // Create selection states for each unique package
94
- const selectionStates = deduplicatedPackages.map((pkg) => {
61
+ const selectedStates = await this.interactiveTableSelector(selectionStates);
62
+ return this.createUpgradeChoices(selectedStates);
63
+ }
64
+ createSelectionStates(packages, previousSelections, includeUpToDate = true) {
65
+ const relevantPackages = includeUpToDate ? packages : packages.filter((p) => p.isOutdated);
66
+ const uniquePackages = this.deduplicatePackages(relevantPackages);
67
+ return Array.from(uniquePackages.values()).map(({ pkg, packageJsonPaths }) => {
95
68
  const currentClean = semver.coerce(pkg.currentVersion)?.version || pkg.currentVersion;
96
69
  const rangeClean = semver.coerce(pkg.rangeVersion)?.version || pkg.rangeVersion;
97
70
  const latestClean = semver.coerce(pkg.latestVersion)?.version || pkg.latestVersion;
98
- // Use previous selection if available, otherwise default to 'none'
99
71
  const key = `${pkg.name}@${pkg.currentVersion}@${pkg.type}`;
100
72
  const previousSelection = previousSelections?.get(key) || 'none';
101
73
  return {
102
74
  name: pkg.name,
103
- packageJsonPath: pkg.packageJsonPaths[0], // Use first path for display
104
- packageJsonPaths: pkg.packageJsonPaths, // Store all paths for upgrading
105
- currentVersionSpecifier: pkg.currentVersion, // Keep original with prefix
75
+ packageJsonPath: pkg.packageJsonPath,
76
+ packageJsonPaths: Array.from(packageJsonPaths),
77
+ currentVersionSpecifier: pkg.currentVersion,
106
78
  currentVersion: currentClean,
107
79
  rangeVersion: rangeClean,
108
80
  latestVersion: latestClean,
109
81
  selectedOption: previousSelection,
82
+ loadState: 'ready',
110
83
  hasRangeUpdate: pkg.hasRangeUpdate,
111
84
  hasMajorUpdate: pkg.hasMajorUpdate,
112
85
  type: pkg.type,
113
86
  };
114
87
  });
115
- // Use custom interactive table selector (simplified - no grouping)
116
- const selectedStates = await this.interactiveTableSelector(selectionStates);
117
- // Convert to PackageUpgradeChoice[] - create one choice per package.json path
88
+ }
89
+ createPendingSelectionStates(packages, previousSelections) {
90
+ const uniquePackages = this.deduplicatePackages(packages.map((pkg) => ({
91
+ ...pkg,
92
+ rangeVersion: pkg.currentVersion,
93
+ latestVersion: pkg.currentVersion,
94
+ isOutdated: false,
95
+ hasRangeUpdate: false,
96
+ hasMajorUpdate: false,
97
+ })));
98
+ return Array.from(uniquePackages.values()).map(({ pkg, packageJsonPaths }) => {
99
+ const currentClean = semver.coerce(pkg.currentVersion)?.version || pkg.currentVersion;
100
+ const key = `${pkg.name}@${pkg.currentVersion}@${pkg.type}`;
101
+ const previousSelection = previousSelections?.get(key) || 'none';
102
+ return {
103
+ name: pkg.name,
104
+ packageJsonPath: pkg.packageJsonPath,
105
+ packageJsonPaths: Array.from(packageJsonPaths),
106
+ currentVersionSpecifier: pkg.currentVersion,
107
+ currentVersion: currentClean,
108
+ rangeVersion: 'loading',
109
+ latestVersion: 'loading',
110
+ selectedOption: previousSelection,
111
+ loadState: 'pending',
112
+ hasRangeUpdate: false,
113
+ hasMajorUpdate: false,
114
+ type: pkg.type,
115
+ };
116
+ });
117
+ }
118
+ appendOutdatedBatchToSelectionStates(selectionStates, batch, previousSelections) {
119
+ const outdatedStates = this.createSelectionStates(batch.flatMap((batchItem) => batchItem.packageInfo).filter((pkg) => pkg.isOutdated), previousSelections, false);
120
+ if (outdatedStates.length === 0) {
121
+ return;
122
+ }
123
+ const seen = new Set(selectionStates.map((state) => `${state.name}@${state.currentVersionSpecifier}@${state.type}`));
124
+ outdatedStates.forEach((state) => {
125
+ const key = `${state.name}@${state.currentVersionSpecifier}@${state.type}`;
126
+ if (!seen.has(key)) {
127
+ selectionStates.push(state);
128
+ seen.add(key);
129
+ }
130
+ });
131
+ }
132
+ async selectPackagesToUpgradeProgressive(selectionStates, progress, attachRefresh) {
133
+ const selectedStates = await this.interactiveTableSelector(selectionStates, progress, attachRefresh);
134
+ return this.createUpgradeChoices(selectedStates);
135
+ }
136
+ deduplicatePackages(packages) {
137
+ const uniquePackages = new Map();
138
+ for (const pkg of packages) {
139
+ const key = `${pkg.name}@${pkg.currentVersion}@${pkg.type}`;
140
+ if (!uniquePackages.has(key)) {
141
+ uniquePackages.set(key, {
142
+ pkg,
143
+ packageJsonPaths: new Set([pkg.packageJsonPath]),
144
+ });
145
+ }
146
+ else {
147
+ uniquePackages.get(key).packageJsonPaths.add(pkg.packageJsonPath);
148
+ }
149
+ }
150
+ return new Map(Array.from(uniquePackages.entries()).sort(([, a], [, b]) => {
151
+ const aIsScoped = a.pkg.name.startsWith('@');
152
+ const bIsScoped = b.pkg.name.startsWith('@');
153
+ if (aIsScoped && !bIsScoped)
154
+ return -1;
155
+ if (!aIsScoped && bIsScoped)
156
+ return 1;
157
+ return a.pkg.name.localeCompare(b.pkg.name);
158
+ }));
159
+ }
160
+ createUpgradeChoices(selectedStates) {
118
161
  const choices = [];
119
162
  selectedStates
120
- .filter((state) => state.selectedOption !== 'none')
163
+ .filter((state) => state.loadState === 'ready' && state.selectedOption !== 'none')
121
164
  .forEach((state) => {
122
165
  const targetVersion = state.selectedOption === 'range' ? state.rangeVersion : state.latestVersion;
123
166
  const targetVersionWithPrefix = ui_1.VersionUtils.applyVersionPrefix(state.currentVersionSpecifier, targetVersion);
124
- // Create a choice for each package.json path where this package appears
125
167
  const pathsToUpdate = state.packageJsonPaths || [state.packageJsonPath];
126
168
  pathsToUpdate.forEach((packageJsonPath) => {
127
169
  choices.push({
@@ -143,10 +185,11 @@ class InteractiveUI {
143
185
  }
144
186
  return 24; // Fallback default
145
187
  }
146
- async interactiveTableSelector(selectionStates) {
188
+ async interactiveTableSelector(selectionStates, loadingProgress, attachRefresh) {
147
189
  return new Promise((resolve) => {
148
- const states = [...selectionStates];
190
+ const states = selectionStates;
149
191
  const stateManager = new ui_1.StateManager(0, this.getTerminalHeight());
192
+ let isResolved = false;
150
193
  // No grouping needed - packages are already filtered by type
151
194
  // This simplifies scrolling and avoids rendering issues
152
195
  stateManager.setRenderableItems([]);
@@ -199,21 +242,25 @@ class InteractiveUI {
199
242
  // Opening modal - load package info asynchronously
200
243
  stateManager.toggleInfoModal();
201
244
  const currentState = filteredStates[uiState.currentRow];
202
- stateManager.setModalLoading(true);
245
+ const canFetchMetadata = currentState?.loadState === 'ready';
246
+ stateManager.setModalLoading(canFetchMetadata);
203
247
  renderInterface();
204
- // Fetch metadata asynchronously
205
- services_1.changelogFetcher.fetchPackageMetadata(currentState.name).then((metadata) => {
206
- if (metadata) {
207
- currentState.description = metadata.description;
208
- currentState.homepage = metadata.homepage;
209
- currentState.repository = metadata.releaseNotes;
210
- currentState.weeklyDownloads = metadata.weeklyDownloads;
211
- currentState.author = metadata.author;
212
- currentState.license = metadata.license;
213
- }
214
- stateManager.setModalLoading(false);
215
- renderInterface();
216
- });
248
+ if (currentState && canFetchMetadata) {
249
+ services_1.changelogFetcher
250
+ .fetchPackageMetadata(currentState.name, currentState.latestVersion)
251
+ .then((metadata) => {
252
+ if (metadata) {
253
+ currentState.description = metadata.description;
254
+ currentState.homepage = metadata.homepage;
255
+ currentState.repository = metadata.releaseNotes;
256
+ currentState.weeklyDownloads = metadata.weeklyDownloads;
257
+ currentState.author = metadata.author;
258
+ currentState.license = metadata.license;
259
+ }
260
+ stateManager.setModalLoading(false);
261
+ renderInterface();
262
+ });
263
+ }
217
264
  }
218
265
  else {
219
266
  // Closing modal
@@ -277,6 +324,7 @@ class InteractiveUI {
277
324
  }
278
325
  };
279
326
  const handleConfirm = (selectedStates) => {
327
+ isResolved = true;
280
328
  // Reset terminal colors
281
329
  process.stdout.write((0, themes_colors_1.getTerminalResetCode)());
282
330
  ui_1.CursorUtils.show();
@@ -290,6 +338,7 @@ class InteractiveUI {
290
338
  resolve(selectedStates);
291
339
  };
292
340
  const handleCancel = () => {
341
+ isResolved = true;
293
342
  // Reset terminal colors
294
343
  process.stdout.write((0, themes_colors_1.getTerminalResetCode)());
295
344
  ui_1.CursorUtils.show();
@@ -370,7 +419,7 @@ class InteractiveUI {
370
419
  const lines = this.renderer.renderInterface(filteredStates, uiState.currentRow, uiState.scrollOffset, uiState.maxVisibleItems, uiState.forceFullRender, [], // No renderable items - use flat rendering
371
420
  activeFilterLabel, // Show current dependency type filter state
372
421
  this.packageManager, // Pass package manager info for header
373
- uiState.filterMode, uiState.filterQuery, states.length, terminalWidth);
422
+ uiState.filterMode, uiState.filterQuery, states.length, terminalWidth, loadingProgress);
374
423
  // Print all lines
375
424
  lines.forEach((line) => console.log(line));
376
425
  // Clear any remaining lines from previous render
@@ -389,6 +438,11 @@ class InteractiveUI {
389
438
  };
390
439
  // Setup keypress handling
391
440
  try {
441
+ attachRefresh?.(() => {
442
+ if (!isResolved) {
443
+ renderInterface();
444
+ }
445
+ });
392
446
  keypress(process.stdin);
393
447
  if (process.stdin.setRawMode) {
394
448
  process.stdin.setRawMode(true);
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.changelogFetcher = exports.ChangelogFetcher = void 0;
4
4
  const constants_1 = require("../config/constants");
5
+ const jsdelivr_registry_1 = require("./jsdelivr-registry");
5
6
  /**
6
7
  * Fetches package metadata from npm registry
7
8
  * Includes description, repository info, and basic metadata
@@ -10,38 +11,62 @@ class ChangelogFetcher {
10
11
  constructor() {
11
12
  this.cache = new Map();
12
13
  this.failureCache = new Set(); // Track packages that failed to fetch
14
+ this.inFlight = new Map();
15
+ }
16
+ getCacheKey(packageName, version) {
17
+ return `${packageName}@${version?.trim() || 'latest'}`;
13
18
  }
14
19
  /**
15
20
  * Fetch package metadata from npm registry
16
21
  * Uses a cached approach to avoid repeated requests
17
22
  */
18
- async fetchPackageMetadata(packageName) {
23
+ async fetchPackageMetadata(packageName, version) {
24
+ const cacheKey = this.getCacheKey(packageName, version);
19
25
  // Check if we already have this in cache
20
- if (this.cache.has(packageName)) {
21
- return this.cache.get(packageName);
26
+ if (this.cache.has(cacheKey)) {
27
+ return this.cache.get(cacheKey);
22
28
  }
23
29
  // Check if we already failed to fetch this
24
- if (this.failureCache.has(packageName)) {
30
+ if (this.failureCache.has(cacheKey)) {
25
31
  return null;
26
32
  }
33
+ const inFlight = this.inFlight.get(cacheKey);
34
+ if (inFlight) {
35
+ return await inFlight;
36
+ }
37
+ const lookupPromise = this.fetchAndCachePackageMetadata(packageName, version).finally(() => {
38
+ this.inFlight.delete(cacheKey);
39
+ });
40
+ this.inFlight.set(cacheKey, lookupPromise);
41
+ return await lookupPromise;
42
+ }
43
+ async fetchAndCachePackageMetadata(packageName, version) {
44
+ const cacheKey = this.getCacheKey(packageName, version);
27
45
  try {
28
- // Fetch from npm registry
29
- const response = await this.fetchFromRegistry(packageName);
46
+ const response = await this.fetchPackageManifest(packageName, version);
30
47
  if (!response) {
31
- this.failureCache.add(packageName);
48
+ this.failureCache.add(cacheKey);
32
49
  return null;
33
50
  }
34
- const repositoryUrl = this.extractRepositoryUrl(response.repository?.url || '');
51
+ const repository = response.repository;
52
+ const bugs = response.bugs;
53
+ const keywords = Array.isArray(response.keywords) ? response.keywords : [];
54
+ const author = typeof response.author === 'object' && response.author !== null
55
+ ? (response.author.name ?? response.author)
56
+ : response.author;
57
+ const repositoryUrl = this.extractRepositoryUrl(repository?.url || '');
35
58
  const npmUrl = `https://www.npmjs.com/package/${encodeURIComponent(packageName)}`;
36
59
  const issuesUrl = repositoryUrl ? `${repositoryUrl}/issues` : undefined;
37
60
  const metadata = {
38
- description: response.description || 'No description available',
39
- homepage: response.homepage,
40
- repository: response.repository,
41
- bugs: response.bugs,
42
- keywords: response.keywords || [],
43
- author: response.author?.name || response.author,
44
- license: response.license,
61
+ description: typeof response.description === 'string' && response.description
62
+ ? response.description
63
+ : 'No description available',
64
+ homepage: typeof response.homepage === 'string' ? response.homepage : undefined,
65
+ repository,
66
+ bugs,
67
+ keywords,
68
+ author: typeof author === 'string' ? author : undefined,
69
+ license: typeof response.license === 'string' ? response.license : undefined,
45
70
  repositoryUrl,
46
71
  npmUrl,
47
72
  issuesUrl,
@@ -60,23 +85,29 @@ class ChangelogFetcher {
60
85
  catch {
61
86
  // Ignore download stats errors - optional data
62
87
  }
63
- this.cache.set(packageName, metadata);
88
+ this.cache.set(cacheKey, metadata);
64
89
  return metadata;
65
90
  }
66
- catch (error) {
91
+ catch {
67
92
  // Cache the failure to avoid retrying
68
- this.failureCache.add(packageName);
93
+ this.failureCache.add(cacheKey);
69
94
  return null;
70
95
  }
71
96
  }
72
97
  /**
73
- * Fetch data from jsdelivr CDN
74
- * Returns the package data by fetching package.json directly from jsdelivr
98
+ * Fetch metadata from a lightweight manifest endpoint.
75
99
  */
76
- async fetchFromRegistry(packageName) {
100
+ async fetchPackageManifest(packageName, version) {
77
101
  try {
78
- // Fetch package.json directly from jsdelivr CDN (resolves to latest automatically)
79
- const response = await fetch(`${constants_1.JSDELIVR_CDN_URL}/${encodeURIComponent(packageName)}@latest/package.json`, {
102
+ const normalizedVersion = version?.trim();
103
+ if (normalizedVersion) {
104
+ const jsdelivrManifest = await (0, jsdelivr_registry_1.fetchExactPackageManifest)(packageName, normalizedVersion);
105
+ if (jsdelivrManifest) {
106
+ return jsdelivrManifest;
107
+ }
108
+ }
109
+ const npmPath = normalizedVersion ? normalizedVersion : 'latest';
110
+ const response = await fetch(`${constants_1.NPM_REGISTRY_URL}/${encodeURIComponent(packageName)}/${encodeURIComponent(npmPath)}`, {
80
111
  method: 'GET',
81
112
  headers: {
82
113
  accept: 'application/json',
@@ -85,16 +116,7 @@ class ChangelogFetcher {
85
116
  if (!response.ok) {
86
117
  return null;
87
118
  }
88
- const pkgData = (await response.json());
89
- return {
90
- description: pkgData.description,
91
- homepage: pkgData.homepage,
92
- repository: pkgData.repository,
93
- bugs: pkgData.bugs,
94
- keywords: (pkgData.keywords || []),
95
- author: pkgData.author,
96
- license: pkgData.license,
97
- };
119
+ return (await response.json());
98
120
  }
99
121
  catch {
100
122
  return null;
@@ -147,7 +169,9 @@ class ChangelogFetcher {
147
169
  * Get repository release URL for a package
148
170
  */
149
171
  getRepositoryReleaseUrl(packageName, version) {
150
- const metadata = this.cache.get(packageName);
172
+ const metadata = this.cache.get(this.getCacheKey(packageName, version)) ??
173
+ this.cache.get(this.getCacheKey(packageName)) ??
174
+ this.cache.get(packageName);
151
175
  if (!metadata || !metadata.releaseNotes) {
152
176
  return null;
153
177
  }
@@ -183,6 +207,7 @@ class ChangelogFetcher {
183
207
  clearCache() {
184
208
  this.cache.clear();
185
209
  this.failureCache.clear();
210
+ this.inFlight.clear();
186
211
  }
187
212
  }
188
213
  exports.ChangelogFetcher = ChangelogFetcher;
@@ -21,6 +21,4 @@ __exportStar(require("./npm-registry"), exports);
21
21
  __exportStar(require("./jsdelivr-registry"), exports);
22
22
  __exportStar(require("./changelog-fetcher"), exports);
23
23
  __exportStar(require("./version-checker"), exports);
24
- __exportStar(require("./persistent-cache"), exports);
25
- __exportStar(require("./cache-manager"), exports);
26
24
  //# sourceMappingURL=index.js.map