allow to select bootVersion and custom service url (#31)

* allow to select bootVersion and custom service url

* use documented API to get dependencies

* can set default groupId and artifactId in settings

* fetch dependencies via documented API

* remove unused imports

* filter available dependencies by version
This commit is contained in:
Yan Zhang 2018-01-29 11:24:51 +08:00 коммит произвёл GitHub
Родитель 905278d37e
Коммит b3ed88dee5
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
12 изменённых файлов: 223 добавлений и 40 удалений

Просмотреть файл

@ -18,23 +18,46 @@
"Other"
],
"activationEvents": [
"onCommand:spring.initializr.maven",
"onCommand:spring.initializr.gradle"
"onCommand:spring.initializr.maven-project",
"onCommand:spring.initializr.gradle-project"
],
"main": "./out/extension",
"contributes": {
"commands": [
{
"command": "spring.initializr.maven",
"command": "spring.initializr.maven-project",
"title": "Generate a Maven Project",
"category": "Spring Initializr"
},
{
"command": "spring.initializr.gradle",
"command": "spring.initializr.gradle-project",
"title": "Generate a Gradle Project",
"category": "Spring Initializr"
}
]
],
"configuration": {
"title": "Spring Initializr",
"properties": {
"spring.initializr.serviceUrl": {
"default": "https://start.spring.io/",
"type": "string",
"scope": "window",
"description": "Spring Initializr Service URL."
},
"spring.initializr.defaultGroupId": {
"default": "com.example",
"type": "string",
"scope": "window",
"description": "Default value for Group Id."
},
"spring.initializr.defaultArtifactId": {
"default": "demo",
"type": "string",
"scope": "window",
"description": "Default value for Artifact Id."
}
}
}
},
"scripts": {
"vscode:prepublish": "npm run compile",

Просмотреть файл

@ -1,9 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
export interface IDependency {
id: string;
name?: string;
group?: string;
description?: string;
}

Просмотреть файл

@ -2,7 +2,8 @@
// Licensed under the MIT license.
import { QuickPickItem } from "vscode";
import { IDependency } from "./Dependency";
import { Metadata } from "./Metadata";
import { IDependency } from "./Model";
import { Utils } from "./Utils";
const PLACEHOLDER: string = "";
@ -29,10 +30,8 @@ export class DependencyManager {
DependencyManager.lastselected = this.genLastSelectedItem(v.id);
}
public async initialize(): Promise<void> {
const DEPENDENCY_URL: string = "https://start.spring.io/ui/dependencies.json?version=1.5.9.RELEASE";
const depsJSON: { dependencies: IDependency[] } = JSON.parse(await Utils.downloadFile(DEPENDENCY_URL, true));
this.dependencies = depsJSON.dependencies;
public async initialize(dependencies: IDependency[]): Promise<void> {
this.dependencies = dependencies;
for (const dep of this.dependencies) {
this.dict[dep.id] = dep;
}
@ -40,9 +39,9 @@ export class DependencyManager {
DependencyManager.lastselected = this.genLastSelectedItem(idList);
}
public async getQuickPickItems(): Promise<IDependencyQuickPickItem[]> {
public async getQuickPickItems(metadata: Metadata, bootVersion: string): Promise<IDependencyQuickPickItem[]> {
if (this.dependencies.length === 0) {
await this.initialize();
await this.initialize(await metadata.getAvailableDependencies(bootVersion));
}
const ret: IDependencyQuickPickItem[] = [];
if (this.selectedIds.length === 0) {
@ -89,14 +88,15 @@ export class DependencyManager {
}
private genLastSelectedItem(idList: string): IDependencyQuickPickItem {
const nameList: string[] = idList && idList.split(",").map((id: string) => this.dict[id].name).filter(Boolean);
if (nameList && nameList.length) {
const availIdList: string[] = idList.split(",").filter((id: string) => this.dict[id]);
const availNameList: string[] = idList && availIdList.map((id: string) => this.dict[id].name).filter(Boolean);
if (availNameList && availNameList.length) {
return {
itemType: "lastUsed",
id: idList,
id: availIdList.join(","),
label: "$(clock) Last used",
description: "",
detail: nameList.join(", ")
detail: availNameList.join(", ")
};
} else {
return null;

63
src/Metadata.ts Normal file
Просмотреть файл

@ -0,0 +1,63 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
import { IDependency, ITopLevelAttribute } from "./Model";
import { Utils } from "./Utils";
import { Versions } from "./Versions";
export class Metadata {
public serviceUrl: string;
private content: {
dependencies: ITopLevelAttribute,
// tslint:disable-next-line:no-reserved-keywords
type: ITopLevelAttribute,
packaging: ITopLevelAttribute,
javaVersion: ITopLevelAttribute,
language: ITopLevelAttribute,
bootVersion: ITopLevelAttribute
};
constructor(serviceUrl: string) {
this.serviceUrl = serviceUrl;
}
public async getBootVersion(): Promise<any[]> {
if (!this.content) {
await this.update();
}
if (!this.content.bootVersion) {
return [];
} else {
return this.content.bootVersion.values;
}
}
public async getAvailableDependencies(bootVersion: string): Promise<IDependency[]> {
if (!this.content) {
await this.update();
}
if (!this.content.dependencies) {
return [];
} else {
const ret: IDependency[] = [];
for (const grp of this.content.dependencies.values) {
const group: string = grp.name;
ret.push(...grp.values.filter(dep => this.isCompatible(dep, bootVersion)).map(dep => Object.assign({ group }, dep)));
}
return ret;
}
}
private isCompatible(dep: IDependency, bootVersion: string): boolean {
if (bootVersion && dep && dep.versionRange) {
return Versions.matchRange(bootVersion, dep.versionRange);
} else {
return true;
}
}
private async update(): Promise<void> {
const rawJSONString: string = await Utils.downloadFile(this.serviceUrl, true, { Accept: "application/vnd.initializr.v2.1+json" });
this.content = JSON.parse(rawJSONString);
}
}

29
src/Model.ts Normal file
Просмотреть файл

@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
export interface IDependency extends IValue {
group?: string;
versionRange?: string;
}
export interface ITopLevelAttribute {
// tslint:disable-next-line:no-reserved-keywords
type: AttributeType;
// tslint:disable-next-line:no-reserved-keywords
default?: any;
values?: IValue[];
}
export interface IValue {
id?: string;
name?: string;
description?: string;
values?: IValue[];
}
export enum AttributeType {
text = "text", // defines a simple text value with no option.
single = "single-select", // defines a simple value to be chosen amongst the specified options.
multi = "hierarchical-multi-select", // defines a hierarchical set of values (values in values) with the ability to select multiple values.
action = "action" // a special type that defines the attribute defining the action to use.
}

Просмотреть файл

@ -36,7 +36,7 @@ export namespace Utils {
return path.join(os.tmpdir(), getExtensionId());
}
export async function downloadFile(targetUrl: string, readContent?: boolean): Promise<string> {
export async function downloadFile(targetUrl: string, readContent?: boolean, customHeaders?: {}): Promise<string> {
const tempFilePath: string = path.join(getTempFolder(), md5(targetUrl));
await fse.ensureDir(getTempFolder());
if (await fse.pathExists(tempFilePath)) {
@ -45,7 +45,7 @@ export namespace Utils {
return await new Promise((resolve: (res: string) => void, reject: (e: Error) => void): void => {
const urlObj: url.Url = url.parse(targetUrl);
const options: Object = Object.assign({ headers: { 'User-Agent': `vscode/${getVersion()}` } }, urlObj);
const options: Object = Object.assign({ headers: Object.assign({}, customHeaders, { 'User-Agent': `vscode/${getVersion()}` }) }, urlObj);
https.get(options, (res: http.IncomingMessage) => {
let rawData: string;
let ws: fse.WriteStream;

Просмотреть файл

@ -96,10 +96,9 @@ export namespace VSCodeUI {
labelfunc: (item: T) => string, descfunc: (item: T) => string,
detailfunc: (item: T) => string, options?: QuickPickOptions
): Promise<T> {
const items: T[] = await itemsSource;
const itemWrappersPromise: Promise<IQuickPickItemEx<T>[]> = new Promise<IQuickPickItemEx<T>[]>(
(resolve: (value: IQuickPickItemEx<T>[]) => void, _reject: (e: Error) => void): void => {
const ret: IQuickPickItemEx<T>[] = items.map((item: T) => Object.assign({}, {
async (resolve: (value: IQuickPickItemEx<T>[]) => void, _reject: (e: Error) => void): Promise<void> => {
const ret: IQuickPickItemEx<T>[] = (await itemsSource).map((item: T) => Object.assign({}, {
description: (detailfunc && descfunc(item)),
detail: (detailfunc && detailfunc(item)),
label: (labelfunc && labelfunc(item)),

54
src/Versions.ts Normal file
Просмотреть файл

@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
export module Versions {
const strictRange: RegExp = /\[(.*),(.*)\]/;
const halfopenRightRange: RegExp = /\[(.*),(.*)\)/;
const halfopenLeftRange: RegExp = /\((.*),(.*)\]/;
const qualifiers: string[] = ['M', 'RC', 'BUILD-SNAPSHOT', 'RELEASE'];
export function matchRange(version: string, range: string): boolean {
const strictMatchGrp: RegExpMatchArray = range.match(strictRange);
if (strictMatchGrp) {
return compareVersions(strictMatchGrp[1], version) <= 0
&& compareVersions(strictMatchGrp[2], version) >= 0;
}
const horMatchGrp: RegExpMatchArray = range.match(halfopenRightRange);
if (horMatchGrp) {
return compareVersions(horMatchGrp[1], version) <= 0
&& compareVersions(horMatchGrp[2], version) > 0;
}
const holMatchGrp: RegExpMatchArray = range.match(halfopenLeftRange);
if (holMatchGrp) {
return compareVersions(holMatchGrp[1], version) < 0
&& compareVersions(holMatchGrp[2], version) >= 0;
}
return compareVersions(range, version) <= 0;
}
function compareVersions(a: string, b: string): number {
let result: number;
const versionA: string[] = a.split(".");
const versionB: string[] = b.split(".");
for (let i: number = 0; i < 3; i += 1) {
result = parseInt(versionA[i], 10) - parseInt(versionB[i], 10);
if (result !== 0) {
return result;
}
}
const aqual: string = parseQualifier(versionA[3]);
const bqual: string = parseQualifier(versionB[3]);
result = qualifiers.indexOf(aqual) - qualifiers.indexOf(bqual);
if (result !== 0) {
return result;
}
return versionA[3].localeCompare(versionB[3]);
}
function parseQualifier(version: string): string {
const qual: string = version.replace(/\d+/g, "");
return qualifiers.indexOf(qual) !== -1 ? qual : "RELEASE";
}
}

Просмотреть файл

@ -7,6 +7,8 @@ import * as unzip from "unzip";
import * as vscode from "vscode";
import { Session, TelemetryWrapper } from "vscode-extension-telemetry-wrapper";
import { DependencyManager, IDependencyQuickPickItem } from "./DependencyManager";
import { Metadata } from "./Metadata";
import { IValue } from "./Model";
import { Utils } from "./Utils";
import { VSCodeUI } from "./VSCodeUI";
@ -18,7 +20,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
await Utils.loadPackageInfo(context);
await TelemetryWrapper.initilizeFromJsonFile(context.asAbsolutePath("package.json"));
["maven", "gradle"].forEach((projectType: string) => {
["maven-project", "gradle-project"].forEach((projectType: string) => {
context.subscriptions.push(
TelemetryWrapper.registerCommand(`spring.initializr.${projectType}`, (t: Session) => {
return async () => await generateProjectRoutine(projectType, t);
@ -29,22 +31,45 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
async function generateProjectRoutine(projectType: string, session?: Session): Promise<void> {
session.extraProperties.finishedSteps = [];
const metadata: Metadata = new Metadata(vscode.workspace.getConfiguration("spring.initializr").get<string>("serviceUrl"));
// Step: Group Id
const groupId: string = await VSCodeUI.getFromInputBox({ prompt: STEP1_MESSAGE, placeHolder: "e.g. com.example", validateInput: groupIdValidation });
const defaultGroupId: string = vscode.workspace.getConfiguration("spring.initializr").get<string>("defaultGroupId");
const groupId: string = await VSCodeUI.getFromInputBox({
prompt: STEP1_MESSAGE,
placeHolder: "e.g. com.example",
value: defaultGroupId,
validateInput: groupIdValidation
});
if (groupId === undefined) { return; }
session.extraProperties.finishedSteps.push("GroupId");
session.info("GroupId inputed.");
// Step: Artifact Id
const artifactId: string = await VSCodeUI.getFromInputBox({ prompt: STEP2_MESSAGE, placeHolder: "e.g. demo", validateInput: artifactIdValidation });
const defaultArtifactId: string = vscode.workspace.getConfiguration("spring.initializr").get<string>("defaultArtifactId");
const artifactId: string = await VSCodeUI.getFromInputBox({
prompt: STEP2_MESSAGE,
placeHolder: "e.g. demo",
value: defaultArtifactId,
validateInput: artifactIdValidation
});
if (artifactId === undefined) { return; }
session.extraProperties.finishedSteps.push("ArtifactId");
session.info("ArtifactId inputed.");
// Step: bootVersion
const bootVersion: IValue = await VSCodeUI.getQuickPick<IValue>(
metadata.getBootVersion(),
version => version.name,
version => version.description,
null
);
session.extraProperties.finishedSteps.push("BootVersion");
session.info("BootVersion selected.");
// Step: Dependencies
let current: IDependencyQuickPickItem = null;
const manager: DependencyManager = new DependencyManager();
do {
current = await vscode.window.showQuickPick(
manager.getQuickPickItems(), { ignoreFocusOut: true, placeHolder: STEP3_MESSAGE, matchOnDetail: true, matchOnDescription: true }
manager.getQuickPickItems(metadata, bootVersion.id), { ignoreFocusOut: true, placeHolder: STEP3_MESSAGE, matchOnDetail: true, matchOnDescription: true }
);
if (current && current.itemType === "dependency") {
manager.toggleDependency(current.id);
@ -65,7 +90,7 @@ async function generateProjectRoutine(projectType: string, session?: Session): P
await vscode.window.withProgress({ location: vscode.ProgressLocation.Window }, (p: vscode.Progress<{ message?: string }>) => new Promise<void>(
async (resolve: () => void, _reject: (e: Error) => void): Promise<void> => {
p.report({ message: "Downloading zip package..." });
let targetUrl: string = `https://start.spring.io/starter.zip?type=${projectType}-project&style=${current.id}`;
let targetUrl: string = `${metadata.serviceUrl}/starter.zip?type=${projectType}&dependencies=${current.id}`;
if (groupId) {
targetUrl += `&groupId=${groupId}`;
}
@ -99,9 +124,9 @@ export function deactivate(): void {
}
function groupIdValidation(value: string): string {
return (value === "" || /^[a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)*$/.test(value)) ? null : "Invalid Group Id";
return (/^[a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)*$/.test(value)) ? null : "Invalid Group Id";
}
function artifactIdValidation(value: string): string {
return (value === "" || /^[a-z_][a-z0-9_]*(-[a-z_][a-z0-9_]*)*$/.test(value)) ? null : "Invalid Artifact Id";
return (/^[a-z_][a-z0-9_]*(-[a-z_][a-z0-9_]*)*$/.test(value)) ? null : "Invalid Artifact Id";
}

Просмотреть файл

@ -9,6 +9,7 @@ are grateful to these developers for their contribution to open source.
2. unzip (https://github.com/EvanOxfeld/node-unzip)
3. fs-extra (https://github.com/jprichardson/node-fs-extra)
4. vscode-extension-telemetry-wrapper (https://github.com/Eskibear/vscode-extension-telemetry-wrapper)
5. semver (https://github.com/npm/node-semver)
md5 NOTICES BEGIN HERE
=============================

Просмотреть файл

@ -2,7 +2,6 @@
"compilerOptions": {
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"module": "commonjs",
"target": "es6",

Просмотреть файл

@ -36,7 +36,7 @@
"match-default-export-name": true,
"mocha-avoid-only": true,
"mocha-no-side-effect-code": true,
"no-any": true,
"no-any": false,
"no-arg": true,
"no-backbone-get-set-outside-model": false,
"no-bitwise": true,
@ -188,7 +188,6 @@
true,
"call-signature",
"parameter",
"arrow-parameter",
"property-declaration",
"variable-declaration",
"member-variable-declaration"