Skip to content

Commit d630f93

Browse files
committed
feat: add ability to select package version to install
1 parent 5a61c30 commit d630f93

File tree

3 files changed

+103
-9
lines changed

3 files changed

+103
-9
lines changed

src/extension.ts

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { exec } from 'node:child_process';
22
import { commands, env, type ExtensionContext, QuickPickItemKind, Uri, window, workspace } from 'vscode';
33

44
import { detectPackageManager } from './detectPackageManager';
5-
import { DirectoryEntry } from './types';
5+
import { DirectoryEntry, NpmRegistryData } from './types';
66
import {
77
deduplicateSearchTokens,
88
ENTRY_OPTION,
@@ -11,12 +11,14 @@ import {
1111
getCompatibilityList,
1212
getEntryTypeLabel,
1313
getPlatformsList,
14+
invertObject,
1415
KEYWORD_REGEX,
1516
numberFormatter,
1617
openListWithSearch,
1718
STRINGS,
1819
VALID_KEYWORDS_MAP,
19-
ValidKeyword
20+
ValidKeyword,
21+
VERSIONS_OPTION
2022
} from './utils';
2123

2224
export async function activate(context: ExtensionContext) {
@@ -90,6 +92,10 @@ export async function activate(context: ExtensionContext) {
9092
label: ENTRY_OPTION.INSTALL,
9193
description: `with ${preferredManager}${selectedEntry.dev ? ' as devDependency' : ''}`
9294
},
95+
workspacePath && {
96+
label: ENTRY_OPTION.INSTALL_SPECIFIC_VERSION,
97+
description: `with ${preferredManager}${selectedEntry.dev ? ' as devDependency' : ''}`
98+
},
9399
{ label: `open URLs`, kind: QuickPickItemKind.Separator },
94100
{
95101
label: ENTRY_OPTION.VISIT_REPO,
@@ -144,11 +150,15 @@ export async function activate(context: ExtensionContext) {
144150
{ label: ENTRY_OPTION.GO_BACK }
145151
].filter((option) => !!option && typeof option === 'object');
146152

153+
function setupAndShowEntryPicker() {
154+
optionPick.title = `Actions for "${selectedEntry.label}" ${getEntryTypeLabel(selectedEntry)}`;
155+
optionPick.placeholder = 'Select an action';
156+
optionPick.items = possibleActions;
157+
optionPick.show();
158+
}
159+
147160
const optionPick = window.createQuickPick();
148-
optionPick.title = `Actions for "${selectedEntry.label}" ${getEntryTypeLabel(selectedEntry)}`;
149-
optionPick.placeholder = 'Select an action';
150-
optionPick.items = possibleActions;
151-
optionPick.show();
161+
setupAndShowEntryPicker();
152162

153163
optionPick.onDidAccept(async () => {
154164
const selectedAction = optionPick.selectedItems[0];
@@ -169,6 +179,71 @@ export async function activate(context: ExtensionContext) {
169179
});
170180
break;
171181
}
182+
case ENTRY_OPTION.INSTALL_SPECIFIC_VERSION: {
183+
const versionPick = window.createQuickPick();
184+
versionPick.title = `Select "${selectedEntry.label}" package version to install`;
185+
versionPick.placeholder = 'Loading versions...';
186+
versionPick.show();
187+
188+
const apiUrl = new URL(`https://registry.npmjs.org/${selectedEntry.npmPkg}`);
189+
const response = await fetch(apiUrl.href);
190+
191+
if (!response.ok) {
192+
window.showErrorMessage(`Cannot fetch package versions from npm registry`);
193+
}
194+
const data = (await response.json()) as NpmRegistryData;
195+
const tags = invertObject(data['dist-tags']);
196+
197+
if ('versions' in data) {
198+
const versions = Object.values(data.versions).map((item: NpmRegistryData['versions'][number]) => ({
199+
label: item.version,
200+
description: item.version in tags ? tags[item.version] : '',
201+
alwaysShow: true
202+
}));
203+
204+
versionPick.placeholder = 'Select a version';
205+
versionPick.items = [
206+
...versions.reverse(),
207+
{ label: '', kind: QuickPickItemKind.Separator },
208+
{ label: VERSIONS_OPTION.CANCEL }
209+
];
210+
211+
versionPick.onDidAccept(async () => {
212+
const selectedVersion = versionPick.selectedItems[0];
213+
214+
if (selectedVersion.label === VERSIONS_OPTION.CANCEL) {
215+
versionPick.hide();
216+
setupAndShowEntryPicker();
217+
return;
218+
}
219+
220+
exec(
221+
getCommandToRun(selectedEntry, preferredManager, selectedVersion.label),
222+
{ cwd: workspacePath },
223+
(error, stout) => {
224+
if (error) {
225+
window.showErrorMessage(
226+
`An error occurred while trying to install the \`${selectedEntry.npmPkg}@${selectedVersion.label}\` package: ${error.message}`
227+
);
228+
versionPick.hide();
229+
setupAndShowEntryPicker();
230+
return;
231+
}
232+
window.showInformationMessage(
233+
`\`${selectedEntry.npmPkg}@${selectedVersion.label}\` package has been installed${selectedEntry.dev ? ' as `devDependency`' : ''} in current workspace using \`${preferredManager}\`: ${stout}`
234+
);
235+
versionPick.hide();
236+
}
237+
);
238+
});
239+
} else {
240+
window.showErrorMessage(`Incompatible response from npm registry`);
241+
versionPick.hide();
242+
setupAndShowEntryPicker();
243+
}
244+
245+
break;
246+
}
172247
case ENTRY_OPTION.VISIT_HOMEPAGE: {
173248
if (selectedEntry.github.urls.homepage) {
174249
env.openExternal(Uri.parse(selectedEntry.github.urls.homepage));

src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,13 @@ export type PackageData = {
8989
popularity?: number;
9090
matchScore?: number;
9191
};
92+
93+
export type NpmRegistryData = {
94+
'dist-tags': Record<string, string>;
95+
versions: Record<
96+
string,
97+
{
98+
version: string;
99+
} & Record<string, unknown>
100+
>;
101+
};

src/utils.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const numberFormatter = new Intl.NumberFormat('en-EN', { notation: 'compa
99

1010
export enum ENTRY_OPTION {
1111
INSTALL = 'Install package in the current workspace',
12+
INSTALL_SPECIFIC_VERSION = 'Install specific package version in the current workspace',
1213
VISIT_HOMEPAGE = 'Visit homepage',
1314
VISIT_REPO = 'Visit GitHub repository',
1415
VISIT_NPM = 'Visit npm registry entry',
@@ -25,6 +26,10 @@ export enum ENTRY_OPTION {
2526
DIRECTORY_SCORE = 'Directory score'
2627
}
2728

29+
export enum VERSIONS_OPTION {
30+
CANCEL = '$(newline) Cancel'
31+
}
32+
2833
export enum STRINGS {
2934
DEFAULT_TITLE = 'Search in React Native Directory',
3035
PLACEHOLDER_BUSY = 'Loading directory data...',
@@ -78,14 +83,14 @@ function getDetailLabel(item: PackageData) {
7883
.join(' ');
7984
}
8085

81-
export function getCommandToRun({ dev, npmPkg }: DirectoryEntry, preferredManager: string): string {
86+
export function getCommandToRun({ dev, npmPkg }: DirectoryEntry, preferredManager: string, version?: string): string {
8287
switch (preferredManager) {
8388
case 'bun':
8489
case 'pnpm':
8590
case 'yarn':
86-
return `${preferredManager} add${dev ? ' -D' : ''} ${npmPkg}`;
91+
return `${preferredManager} add${dev ? ' -D' : ''} ${npmPkg}${version ? `@${version}` : ''}`;
8792
default:
88-
return `${preferredManager} install${dev ? ' -D' : ''} ${npmPkg}`;
93+
return `${preferredManager} install${dev ? ' -D' : ''} ${npmPkg}${version ? `@${version}` : ''}`;
8994
}
9095
}
9196

@@ -183,3 +188,7 @@ export function getEntryTypeLabel(entry: DirectoryEntry): string {
183188
}
184189
return 'library';
185190
}
191+
192+
export function invertObject(obj: Record<string, string>): Record<string, string> {
193+
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [v, k]));
194+
}

0 commit comments

Comments
 (0)