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:
Родитель
905278d37e
Коммит
b3ed88dee5
33
package.json
33
package.json
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)),
|
||||
|
|
|
@ -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"
|
||||
|
|
Загрузка…
Ссылка в новой задаче