Skip to content

Commit

Permalink
feat: enable add-dependency version selection (#1357)
Browse files Browse the repository at this point in the history
  • Loading branch information
MaxKless authored Sep 21, 2022
1 parent 92ecb61 commit c87da06
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 10 deletions.
158 changes: 158 additions & 0 deletions libs/vscode/add-dependency/src/lib/dependency-versioning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { xhr } from 'request-light';
import { gte, rcompare } from 'semver';
import { QuickPickItem, QuickPickItemKind, window } from 'vscode';

type VersionMap = Record<string, { latest: string; all: string[] }>;

export async function resolveDependencyVersioning(
depInput: string
): Promise<{ dep: string; version: string | undefined } | undefined> {
const match = depInput.match(/^(.+)@(.+)/);
if (match) {
const [_, dep, version] = match;
return { dep, version };
}
let packageInfo: PackageInformationResponse;
try {
packageInfo = await getPackageInfo(depInput);
} catch (e) {
window.showErrorMessage(
`Package ${depInput} couldn't be found. Are you sure it exists?`
);
return { dep: depInput, version: undefined };
}

const versionMap = createVersionMap(packageInfo);
const versionQuickPickOptions = createVersionQuickPickItems(versionMap);

const version = await promptForVersion(versionQuickPickOptions, versionMap);

return { dep: depInput, version };
}

async function promptForVersion(
versionQuickPickItems: QuickPickItem[],
versionMap: VersionMap
): Promise<string | undefined> {
const selection = await new Promise<string | undefined>((resolve) => {
const quickPick = window.createQuickPick();
quickPick.canSelectMany = false;

quickPick.items = versionQuickPickItems;

quickPick.onDidChangeValue(() => {
quickPick.items = [
...versionQuickPickItems,
{
label: quickPick.value,
description: 'install specific version',
},
];
});

quickPick.onDidAccept(() => {
resolve(quickPick.selectedItems[0]?.label);
quickPick.hide();
});

quickPick.show();
});
const match = selection?.match(/^(\d+).x/);
if (match) {
const majorToSelect = match[1];
const version = await window.showQuickPick(versionMap[majorToSelect].all, {
canPickMany: false,
});
if (!version) {
return promptForVersion(versionQuickPickItems, versionMap);
}
return version;
}
return selection;
}

/**
* Create a map that tracks the latest version and an array of all versions per major version
*/
function createVersionMap(packageInfo: PackageInformationResponse): VersionMap {
const versionMap: VersionMap = {};
Object.entries(packageInfo.versions).forEach(([versionNum, versionInfo]) => {
if (versionInfo.deprecated) {
return;
}
const major = versionNum.split('.')[0];
if (!versionMap[major]) {
versionMap[major] = { latest: versionNum, all: [] };
}
versionMap[major].all.push(versionNum);
if (gte(versionNum, versionMap[major].latest)) {
versionMap[major].latest = versionNum;
}
});
return versionMap;
}

/**
* For each major version, add the following options to the quickpick:
* - the latest version of that major
* - the patch before the latest version of that major
* - the minor before the latest version of that major
* - the option to select a specific version of that major
*/
function createVersionQuickPickItems(versionMap: VersionMap): QuickPickItem[] {
return Object.entries(versionMap)
.sort(
(
a: [keyof VersionMap, VersionMap[keyof VersionMap]],
b: [keyof VersionMap, VersionMap[keyof VersionMap]]
) => (parseInt(a[0]) < parseInt(b[0]) ? 1 : -1)
)
.flatMap(([major, { latest, all }], index) => {
const allSorted = all.sort(rcompare);
const quickPickOptions = [];
quickPickOptions.push({
label: `Version ${major}.x`,
kind: QuickPickItemKind.Separator,
});
quickPickOptions.push({
label: latest,
description: index === 0 ? 'latest' : '',
});
if (allSorted.length > 1) {
quickPickOptions.push({ label: allSorted[1] });
}
const minorBefore = allSorted.find(
(v) =>
v.split('.')[1] === (parseInt(latest.split('.')[1]) - 1).toString()
);
if (minorBefore && minorBefore !== allSorted[1]) {
quickPickOptions.push({ label: minorBefore });
}
if (allSorted.length > 2) {
quickPickOptions.push({
label: `${major}.x`,
description: 'select specific version',
});
}
return quickPickOptions;
});
}

type PackageInformationResponse = {
versions: Record<string, { deprecated: string }>;
};
function getPackageInfo(dep: string): Promise<PackageInformationResponse> {
const headers = {
'Accept-Encoding': 'gzip, deflate',
Accept: 'application/vnd.npm.install-v1+json',
};

// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
return xhr({
url: `https://registry.npmjs.org/${dep}`,
headers,
}).then(
(res) => JSON.parse(res.responseText),
(error) => Promise.reject(error)
);
}
35 changes: 25 additions & 10 deletions libs/vscode/add-dependency/src/lib/vscode-add-dependency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@ import {
commands,
ExtensionContext,
QuickInput,
QuickPickItem,
QuickPickItemKind,
ShellExecution,
Task,
tasks,
TaskScope,
window,
} from 'vscode';
import { gte, major, rcompare } from 'semver';
import { resolveDependencyVersioning } from './dependency-versioning';

export const ADD_DEPENDENCY_COMMAND = 'nxConsole.addDependency';
export const ADD_DEV_DEPENDENCY_COMMAND = 'nxConsole.addDevDependency';
Expand All @@ -48,12 +52,23 @@ function vscodeAddDependencyCommand(installAsDevDependency: boolean) {
);
pkgManager = detectPackageManager(workspacePath);

const dep = await promptForDependencyName();
const depInput = await promptForDependencyInput();

if (!depInput) {
return;
}
const depVersioningInfo = await resolveDependencyVersioning(depInput);

if (!depVersioningInfo?.version) {
return;
}

const { dep, version } = depVersioningInfo;

if (dep) {
const quickInput = showLoadingQuickInput(dep);
getTelemetry().featureUsed('add-dependency');
addDependency(dep, installAsDevDependency);
addDependency(dep, version, installAsDevDependency);
const disposable = tasks.onDidEndTaskProcess((taskEndEvent) => {
if (
taskEndEvent.execution.task.definition.type === 'nxconsole-add-dep'
Expand All @@ -67,13 +82,14 @@ function vscodeAddDependencyCommand(installAsDevDependency: boolean) {
};
}

async function promptForDependencyName(): Promise<string | undefined> {
async function promptForDependencyInput(): Promise<string | undefined> {
const packageSuggestions = (await getDependencySuggestions()).map((pkg) => ({
label: pkg.name,
description: pkg.description,
}));
const dep = await new Promise<string | undefined>((resolve) => {
const quickPick = window.createQuickPick();
quickPick.title = 'Select Dependency';
quickPick.items = packageSuggestions;
quickPick.placeholder =
'The name of the dependency you want to add. Can be anything on the npm registry.';
Expand Down Expand Up @@ -112,11 +128,15 @@ function showLoadingQuickInput(dependency: string): QuickInput {
return quickInput;
}

function addDependency(dependency: string, installAsDevDependency: boolean) {
function addDependency(
dependency: string,
version: string,
installAsDevDependency: boolean
) {
const pkgManagerCommands = getPackageManagerCommand(pkgManager);
const command = `${
installAsDevDependency ? pkgManagerCommands.addDev : pkgManagerCommands.add
} ${dependency}`;
} ${dependency}@${version}`;
const task = new Task(
{
type: 'nxconsole-add-dep',
Expand All @@ -138,11 +158,6 @@ async function executeInitGenerator(
includeNgAdd: true,
});

// get the dependency's name if it came with a version
if (dependency.lastIndexOf('@') > 0) {
dependency = dependency.substring(0, dependency.lastIndexOf('@'));
}

let initGeneratorName = `${dependency}:init`;
let initGenerator = generators.find((g) => g.name === initGeneratorName);
if (!initGenerator) {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"jsonc-parser": "^3.0.0",
"request-light": "^0.5.8",
"rxjs": "7.5.6",
"semver": "^7.3.7",
"tslib": "^2.0.0",
"vscode-json-languageservice": "^5.1.0",
"vscode-languageclient": "^8.0.2",
Expand Down Expand Up @@ -67,6 +68,7 @@
"@types/find-cache-dir": "^3.2.1",
"@types/jest": "27.4.1",
"@types/node": "18.7.1",
"@types/semver": "^7.3.12",
"@types/universal-analytics": "0.4.5",
"@types/uuid": "^8.3.4",
"@types/vscode": "1.71.0",
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7199,6 +7199,11 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==

"@types/semver@^7.3.12":
version "7.3.12"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.12.tgz#920447fdd78d76b19de0438b7f60df3c4a80bf1c"
integrity sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==

"@types/serve-index@^1.9.1":
version "1.9.1"
resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278"
Expand Down

0 comments on commit c87da06

Please sign in to comment.