Add support for C# Class library projects (#161)
This supports project/function creation and local debugging. It does not include deploy logic. A brief summary: 1. We default C# projects to beta runtime and class library (instead of C# script) 1. We use dotnet templates for project/function creation. We will automatically install the templates for the user if they are not on their machine (I don't prompt at all - let me know if you think we should prompt). 1. We use the parameter information from the functions portal (Aka C# Script templates) since it's easier to parse than the dotnet cli and it gives us more information (like validation). This requires us to assume that parameters for C# Scripts are the same as the parameters for the C# Class libraries. Since that might not always be the case, I mitigated this with unit tests and hard-coding the version of the dotnet templates. 1. Unlike JavaScript debugging, we have to attach to a specific process instead of attaching with a port. I implemented a 'pickProcess' command to search for the functions host process. 1. This only works on Windows. There's a few issues on a Mac I still need to iron out.
This commit is contained in:
Родитель
a6f0af8e04
Коммит
7fe28b5a79
15
.travis.yml
15
.travis.yml
|
@ -3,6 +3,16 @@ language: node_js
|
|||
node_js:
|
||||
- 'stable'
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- gettext
|
||||
- libcurl4-openssl-dev
|
||||
- libicu-dev
|
||||
- libssl-dev
|
||||
- libunwind8
|
||||
- zlib1g
|
||||
|
||||
before_install:
|
||||
- if [ $TRAVIS_OS_NAME == "linux" ]; then
|
||||
export CXX="g++-4.9" CC="gcc-4.9" DISPLAY=:99.0;
|
||||
|
@ -10,6 +20,11 @@ before_install:
|
|||
sleep 3;
|
||||
fi
|
||||
|
||||
# Install dotnet cli and add to path
|
||||
- export DOTNET_INSTALL_DIR="$HOME/.dotnet"
|
||||
- curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --version 2.0.0 --install-dir "$DOTNET_INSTALL_DIR"
|
||||
- export PATH="$DOTNET_INSTALL_DIR:$PATH"
|
||||
|
||||
install:
|
||||
- npm install
|
||||
|
||||
|
|
18
README.md
18
README.md
|
@ -4,18 +4,20 @@
|
|||
* For local debugging:
|
||||
* [.NET Core 2.0](https://www.microsoft.com/net/download/core)
|
||||
* Install the [Azure Core Function Tools 2.0](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local) with the following command
|
||||
|
||||
```bash
|
||||
npm install -g azure-functions-core-tools@core
|
||||
```
|
||||
> Note: when installing on Ubuntu, you may need to use `sudo`. On MacOS and LInux, you may need to include the `unsafe-perm` flag, as follows:
|
||||
```bash
|
||||
sudo npm install -g azure-functions-core-tools@core --unsafe-perm true
|
||||
```
|
||||
```bash
|
||||
npm install -g azure-functions-core-tools@core
|
||||
```
|
||||
> Note: when installing on Ubuntu, you may need to use `sudo`. On MacOS and LInux, you may need to include the `unsafe-perm` flag, as follows:
|
||||
```bash
|
||||
sudo npm install -g azure-functions-core-tools@core --unsafe-perm true
|
||||
```
|
||||
|
||||
* For JavaScript based Functions:
|
||||
* [Node 8.0+](https://nodejs.org/)
|
||||
|
||||
* For C# based Functions:
|
||||
* [VS Code Debugger for C#](https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp)
|
||||
|
||||
* For Java based Functions:
|
||||
* [VS Code Debugger for Java](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-debug)
|
||||
* [JDK 1.8+](http://www.oracle.com/technetwork/java/javase/downloads/index.html)
|
||||
|
|
12
package.json
12
package.json
|
@ -47,6 +47,7 @@
|
|||
"onCommand:azureFunctions.appSettings.edit",
|
||||
"onCommand:azureFunctions.appSettings.rename",
|
||||
"onCommand:azureFunctions.appSettings.delete",
|
||||
"onCommand:azureFunctions.pickProcess",
|
||||
"onView:azureFunctionsExplorer"
|
||||
],
|
||||
"main": "./out/src/extension",
|
||||
|
@ -157,6 +158,11 @@
|
|||
"command": "azureFunctions.appSettings.delete",
|
||||
"title": "%azFunc.appSettings.delete%",
|
||||
"category": "Azure Functions"
|
||||
},
|
||||
{
|
||||
"command": "azureFunctions.pickProcess",
|
||||
"title": "%azFunc.pickProcess%",
|
||||
"category": "Azure Functions"
|
||||
}
|
||||
],
|
||||
"views": {
|
||||
|
@ -293,6 +299,10 @@
|
|||
{
|
||||
"command": "azureFunctions.loadMore",
|
||||
"when": "never"
|
||||
},
|
||||
{
|
||||
"command": "azureFunctions.pickProcess",
|
||||
"when": "never"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -352,6 +362,7 @@
|
|||
"enum": [
|
||||
"Bash",
|
||||
"Batch",
|
||||
"C#",
|
||||
"F#",
|
||||
"Java",
|
||||
"JavaScript",
|
||||
|
@ -398,6 +409,7 @@
|
|||
"fs-extra": "^4.0.2",
|
||||
"ms-rest": "^2.2.2",
|
||||
"ms-rest-azure": "^2.3.1",
|
||||
"ps-node": "^0.1.6",
|
||||
"request-promise": "^4.2.2",
|
||||
"vscode-azureappservice": "~0.8.2",
|
||||
"vscode-azureextensionui": "~0.5.1",
|
||||
|
|
|
@ -21,5 +21,6 @@
|
|||
"azFunc.appSettings.add": "Add new setting...",
|
||||
"azFunc.appSettings.edit": "Edit setting...",
|
||||
"azFunc.appSettings.rename": "Rename setting...",
|
||||
"azFunc.appSettings.delete": "Delete setting"
|
||||
"azFunc.appSettings.delete": "Delete setting",
|
||||
"azFunc.pickProcess": "Pick Process"
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ const previewDescription: string = localize('previewDescription', '(Preview)');
|
|||
export enum ProjectLanguage {
|
||||
Bash = 'Bash',
|
||||
Batch = 'Batch',
|
||||
CSharp = 'C#',
|
||||
FSharp = 'F#',
|
||||
Java = 'Java',
|
||||
JavaScript = 'JavaScript',
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as fse from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { OutputChannel } from "vscode";
|
||||
import { IUserInterface } from "../../IUserInterface";
|
||||
import { localize } from "../../localize";
|
||||
import { Template } from "../../templates/Template";
|
||||
import { removeLanguageFromId } from "../../templates/TemplateData";
|
||||
import { cpUtils } from "../../utils/cpUtils";
|
||||
import { dotnetUtils } from '../../utils/dotnetUtils';
|
||||
import * as fsUtil from '../../utils/fs';
|
||||
import { FunctionCreatorBase } from './FunctionCreatorBase';
|
||||
|
||||
export class CSharpFunctionCreator extends FunctionCreatorBase {
|
||||
private _outputChannel: OutputChannel;
|
||||
private _functionName: string;
|
||||
|
||||
constructor(functionAppPath: string, template: Template, outputChannel: OutputChannel) {
|
||||
super(functionAppPath, template);
|
||||
this._outputChannel = outputChannel;
|
||||
}
|
||||
|
||||
public async promptForSettings(ui: IUserInterface, functionName: string | undefined): Promise<void> {
|
||||
if (!functionName) {
|
||||
const defaultFunctionName: string | undefined = await fsUtil.getUniqueFsPath(this._functionAppPath, removeLanguageFromId(this._template.id), '.cs');
|
||||
const placeHolder: string = localize('azFunc.funcNamePlaceholder', 'Function name');
|
||||
const prompt: string = localize('azFunc.funcNamePrompt', 'Provide a function name');
|
||||
this._functionName = await ui.showInputBox(placeHolder, prompt, false, (s: string) => this.validateTemplateName(s), defaultFunctionName || this._template.defaultFunctionName);
|
||||
} else {
|
||||
this._functionName = functionName;
|
||||
}
|
||||
}
|
||||
|
||||
public async createFunction(userSettings: { [propertyName: string]: string }): Promise<string | undefined> {
|
||||
await dotnetUtils.validateTemplatesInstalled(this._outputChannel, this._functionAppPath);
|
||||
|
||||
const args: string[] = [];
|
||||
for (const key of Object.keys(userSettings)) {
|
||||
let parameter: string = key.charAt(0).toUpperCase() + key.slice(1);
|
||||
// the parameters for dotnet templates are not consistent. Hence, we have to special-case a few of them:
|
||||
if (parameter === 'AuthLevel') {
|
||||
parameter = 'AccessRights';
|
||||
} else if (parameter === 'QueueName') {
|
||||
parameter = 'Path';
|
||||
}
|
||||
|
||||
args.push(`--${parameter}="${userSettings[key]}"`);
|
||||
}
|
||||
|
||||
await cpUtils.executeCommand(
|
||||
this._outputChannel,
|
||||
this._functionAppPath,
|
||||
'dotnet',
|
||||
'new',
|
||||
removeLanguageFromId(this._template.id),
|
||||
`--name="${this._functionName}"`,
|
||||
...args
|
||||
);
|
||||
|
||||
return path.join(this._functionAppPath, `${this._functionName}.cs`);
|
||||
}
|
||||
|
||||
private validateTemplateName(name: string | undefined): string | undefined {
|
||||
if (!name) {
|
||||
return localize('azFunc.emptyTemplateNameError', 'The template name cannot be empty.');
|
||||
} else if (fse.existsSync(path.join(this._functionAppPath, `${name}.cs`))) {
|
||||
return localize('azFunc.existingCSFile', 'A CSharp file with the name \'{0}\' already exists.', name);
|
||||
} else if (!this._functionNameRegex.test(name)) {
|
||||
return localize('azFunc.functionNameInvalidError', 'Function name must start with a letter and can contain letters, digits, \'_\' and \'-\'');
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ import { OutputChannel } from "vscode";
|
|||
import { IUserInterface } from "../../IUserInterface";
|
||||
import { localize } from "../../localize";
|
||||
import { Template } from "../../templates/Template";
|
||||
import { convertTemplateIdToJava } from "../../templates/TemplateData";
|
||||
import { removeLanguageFromId } from "../../templates/TemplateData";
|
||||
import { cpUtils } from "../../utils/cpUtils";
|
||||
import * as fsUtil from '../../utils/fs';
|
||||
import { getFullClassName, parseJavaClassName, validatePackageName } from "../../utils/javaNameUtils";
|
||||
|
@ -36,7 +36,7 @@ export class JavaFunctionCreator extends FunctionCreatorBase {
|
|||
this._packageName = await ui.showInputBox(packagePlaceHolder, packagePrompt, false, validatePackageName, 'com.function');
|
||||
|
||||
if (!functionName) {
|
||||
const defaultFunctionName: string | undefined = await fsUtil.getUniqueJavaFsPath(this._functionAppPath, this._packageName, `${convertTemplateIdToJava(this._template.id)}Java`);
|
||||
const defaultFunctionName: string | undefined = await fsUtil.getUniqueJavaFsPath(this._functionAppPath, this._packageName, `${removeLanguageFromId(this._template.id)}Java`);
|
||||
const placeHolder: string = localize('azFunc.funcNamePlaceholder', 'Function name');
|
||||
const prompt: string = localize('azFunc.funcNamePrompt', 'Provide a function name');
|
||||
this._functionName = await ui.showInputBox(placeHolder, prompt, false, (s: string) => this.validateTemplateName(s), defaultFunctionName || this._template.defaultFunctionName);
|
||||
|
@ -61,7 +61,7 @@ export class JavaFunctionCreator extends FunctionCreatorBase {
|
|||
'-B',
|
||||
`"-Dfunctions.package=${this._packageName}"`,
|
||||
`"-Dfunctions.name=${this._functionName}"`,
|
||||
`"-Dfunctions.template=${convertTemplateIdToJava(this._template.id)}"`,
|
||||
`"-Dfunctions.template=${removeLanguageFromId(this._template.id)}"`,
|
||||
...javaFuntionProperties
|
||||
);
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import { TemplateData } from '../../templates/TemplateData';
|
|||
import * as workspaceUtil from '../../utils/workspace';
|
||||
import { VSCodeUI } from '../../VSCodeUI';
|
||||
import { createNewProject } from '../createNewProject/createNewProject';
|
||||
import { CSharpFunctionCreator } from './CSharpFunctionCreator';
|
||||
import { FunctionCreatorBase } from './FunctionCreatorBase';
|
||||
import { JavaFunctionCreator } from './JavaFunctionCreator';
|
||||
import { ScriptFunctionCreator } from './ScriptFunctionCreator';
|
||||
|
@ -86,7 +87,7 @@ export async function createFunction(
|
|||
functionAppPath?: string,
|
||||
templateId?: string,
|
||||
functionName?: string,
|
||||
...functionSettings: (string)[]): Promise<void> {
|
||||
...functionSettings: (string) []): Promise<void> {
|
||||
|
||||
if (functionAppPath === undefined) {
|
||||
const folderPlaceholder: string = localize('azFunc.selectFunctionAppFolderExisting', 'Select the folder containing your function app');
|
||||
|
@ -131,6 +132,9 @@ export async function createFunction(
|
|||
case ProjectLanguage.Java:
|
||||
functionCreator = new JavaFunctionCreator(functionAppPath, template, outputChannel);
|
||||
break;
|
||||
case ProjectLanguage.CSharp:
|
||||
functionCreator = new CSharpFunctionCreator(functionAppPath, template, outputChannel);
|
||||
break;
|
||||
default:
|
||||
functionCreator = new ScriptFunctionCreator(functionAppPath, template, language);
|
||||
break;
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OutputChannel } from 'vscode';
|
||||
import { localize } from "../../localize";
|
||||
import { ProjectRuntime } from '../../ProjectSettings';
|
||||
import { cpUtils } from '../../utils/cpUtils';
|
||||
import { dotnetUtils } from '../../utils/dotnetUtils';
|
||||
import { funcHostProblemMatcher, funcHostTaskId, IProjectCreator } from './IProjectCreator';
|
||||
|
||||
export class CSharpProjectCreator implements IProjectCreator {
|
||||
private _outputChannel: OutputChannel;
|
||||
|
||||
constructor(outputChannel: OutputChannel) {
|
||||
this._outputChannel = outputChannel;
|
||||
}
|
||||
|
||||
public async addNonVSCodeFiles(functionAppPath: string): Promise<void> {
|
||||
await dotnetUtils.validateTemplatesInstalled(this._outputChannel, functionAppPath);
|
||||
await cpUtils.executeCommand(
|
||||
this._outputChannel,
|
||||
functionAppPath,
|
||||
'dotnet',
|
||||
'new',
|
||||
dotnetUtils.funcProjectId
|
||||
);
|
||||
}
|
||||
|
||||
public getRuntime(): ProjectRuntime {
|
||||
return ProjectRuntime.beta;
|
||||
}
|
||||
|
||||
public getTasksJson(): {} {
|
||||
return {
|
||||
version: '2.0.0',
|
||||
tasks: [
|
||||
{
|
||||
label: 'build',
|
||||
command: 'dotnet build',
|
||||
type: 'shell',
|
||||
group: {
|
||||
kind: 'build',
|
||||
isDefault: true
|
||||
},
|
||||
presentation: {
|
||||
reveal: 'always'
|
||||
},
|
||||
problemMatcher: '$msCompile'
|
||||
},
|
||||
{
|
||||
label: localize('azFunc.runFuncHost', 'Run Functions Host'),
|
||||
identifier: funcHostTaskId,
|
||||
type: 'shell',
|
||||
dependsOn: 'build',
|
||||
options: {
|
||||
cwd: '\${workspaceFolder}/bin/Debug/netstandard2.0'
|
||||
},
|
||||
command: 'func host start',
|
||||
isBackground: true,
|
||||
presentation: {
|
||||
reveal: 'always'
|
||||
},
|
||||
problemMatcher: [
|
||||
funcHostProblemMatcher
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
public getLaunchJson(): {} {
|
||||
return {
|
||||
version: '0.2.0',
|
||||
configurations: [
|
||||
{
|
||||
name: localize('azFunc.attachToNetCoreFunc', "Attach to .NET Core Functions"),
|
||||
type: 'coreclr',
|
||||
request: 'attach',
|
||||
processId: '\${command:azureFunctions.pickProcess}',
|
||||
preLaunchTask: 'build'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
public getRecommendedExtensions(): string[] {
|
||||
return ['ms-vscode.csharp'];
|
||||
}
|
||||
}
|
|
@ -3,14 +3,35 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ProjectRuntime } from "../../ProjectSettings";
|
||||
import { localize } from "../../localize";
|
||||
import { extensionPrefix, ProjectRuntime } from "../../ProjectSettings";
|
||||
|
||||
export interface IProjectCreator {
|
||||
/**
|
||||
* Add all project files not included in the '.vscode' folder
|
||||
*/
|
||||
addNonVSCodeFiles(functionAppPath: string): Promise<void>;
|
||||
getTasksJson(launchTaskId: string, funcProblemMatcher: {}): {};
|
||||
getLaunchJson(launchTaskId: string): {};
|
||||
getTasksJson(): {};
|
||||
getLaunchJson(): {};
|
||||
getRecommendedExtensions?(): string[];
|
||||
getRuntime(): ProjectRuntime;
|
||||
}
|
||||
|
||||
export const funcHostTaskId: string = 'runFunctionsHost';
|
||||
export const funcHostTaskLabel: string = localize('azFunc.runFuncHost', 'Run Functions Host');
|
||||
export const funcHostProblemMatcher: {} = {
|
||||
owner: extensionPrefix,
|
||||
pattern: [
|
||||
{
|
||||
regexp: '\\b\\B',
|
||||
file: 1,
|
||||
location: 2,
|
||||
message: 3
|
||||
}
|
||||
],
|
||||
background: {
|
||||
activeOnStart: true,
|
||||
beginsPattern: '^.*Stopping host.*',
|
||||
endsPattern: '^.*Job host started.*'
|
||||
}
|
||||
};
|
||||
|
|
|
@ -14,7 +14,7 @@ import { cpUtils } from '../../utils/cpUtils';
|
|||
import * as fsUtil from '../../utils/fs';
|
||||
import { validateMavenIdentifier, validatePackageName } from '../../utils/javaNameUtils';
|
||||
import { mavenUtils } from '../../utils/mavenUtils';
|
||||
import { IProjectCreator } from './IProjectCreator';
|
||||
import { funcHostProblemMatcher, funcHostTaskId, funcHostTaskLabel, IProjectCreator } from './IProjectCreator';
|
||||
|
||||
export class JavaProjectCreator implements IProjectCreator {
|
||||
private _javaTargetPath: string;
|
||||
|
@ -78,13 +78,13 @@ export class JavaProjectCreator implements IProjectCreator {
|
|||
return ProjectRuntime.beta;
|
||||
}
|
||||
|
||||
public getTasksJson(launchTaskId: string, funcProblemMatcher: {}): {} {
|
||||
public getTasksJson(): {} {
|
||||
let tasksJson: {} = {
|
||||
version: '2.0.0',
|
||||
tasks: [
|
||||
{
|
||||
label: localize('azFunc.launchFuncApp', 'Launch Function App'),
|
||||
identifier: launchTaskId,
|
||||
label: funcHostTaskLabel,
|
||||
identifier: funcHostTaskId,
|
||||
linux: {
|
||||
command: 'sh -c "mvn clean package -B && func host start --script-root \\\"%path%\\\""'
|
||||
},
|
||||
|
@ -100,7 +100,7 @@ export class JavaProjectCreator implements IProjectCreator {
|
|||
reveal: 'always'
|
||||
},
|
||||
problemMatcher: [
|
||||
funcProblemMatcher
|
||||
funcHostProblemMatcher
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -114,7 +114,7 @@ export class JavaProjectCreator implements IProjectCreator {
|
|||
return tasksJson;
|
||||
}
|
||||
|
||||
public getLaunchJson(launchTaskId: string): {} {
|
||||
public getLaunchJson(): {} {
|
||||
return {
|
||||
version: '0.2.0',
|
||||
configurations: [
|
||||
|
@ -124,9 +124,13 @@ export class JavaProjectCreator implements IProjectCreator {
|
|||
request: 'attach',
|
||||
hostName: 'localhost',
|
||||
port: 5005,
|
||||
preLaunchTask: launchTaskId
|
||||
preLaunchTask: funcHostTaskId
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
public getRecommendedExtensions(): string[] {
|
||||
return ['vscjava.vscode-java-debug'];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { localize } from "../../localize";
|
||||
import { ProjectRuntime } from "../../ProjectSettings";
|
||||
import { funcHostProblemMatcher, funcHostTaskId, funcHostTaskLabel } from "./IProjectCreator";
|
||||
import { ScriptProjectCreatorBase } from './ScriptProjectCreatorBase';
|
||||
|
||||
export class JavaScriptProjectCreator extends ScriptProjectCreatorBase {
|
||||
|
@ -12,13 +13,13 @@ export class JavaScriptProjectCreator extends ScriptProjectCreatorBase {
|
|||
return ProjectRuntime.one;
|
||||
}
|
||||
|
||||
public getTasksJson(launchTaskId: string, funcProblemMatcher: {}): {} {
|
||||
public getTasksJson(): {} {
|
||||
return {
|
||||
version: '2.0.0',
|
||||
tasks: [
|
||||
{
|
||||
label: localize('azFunc.launchFuncApp', 'Launch Function App'),
|
||||
identifier: launchTaskId,
|
||||
label: funcHostTaskLabel,
|
||||
identifier: funcHostTaskId,
|
||||
type: 'shell',
|
||||
command: 'func host start',
|
||||
isBackground: true,
|
||||
|
@ -26,14 +27,14 @@ export class JavaScriptProjectCreator extends ScriptProjectCreatorBase {
|
|||
reveal: 'always'
|
||||
},
|
||||
problemMatcher: [
|
||||
funcProblemMatcher
|
||||
funcHostProblemMatcher
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
public getLaunchJson(launchTaskId: string): {} {
|
||||
public getLaunchJson(): {} {
|
||||
return {
|
||||
version: '0.2.0',
|
||||
configurations: [
|
||||
|
@ -43,7 +44,7 @@ export class JavaScriptProjectCreator extends ScriptProjectCreatorBase {
|
|||
request: 'attach',
|
||||
port: 5858,
|
||||
protocol: 'inspector',
|
||||
preLaunchTask: launchTaskId
|
||||
preLaunchTask: funcHostTaskId
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
|
@ -41,8 +41,8 @@ local.settings.json
|
|||
* Base class for all projects based on a simple script (i.e. JavaScript, C# Script, Bash, etc.) that don't require compilation
|
||||
*/
|
||||
export abstract class ScriptProjectCreatorBase implements IProjectCreator {
|
||||
public abstract getTasksJson(launchTaskId: string, funcProblemMatcher: {}): {};
|
||||
public abstract getLaunchJson(launchTaskId: string): {};
|
||||
public abstract getTasksJson(): {};
|
||||
public abstract getLaunchJson(): {};
|
||||
public abstract getRuntime(): ProjectRuntime;
|
||||
|
||||
public async addNonVSCodeFiles(functionAppPath: string): Promise<void> {
|
||||
|
|
|
@ -16,29 +16,11 @@ import { confirmOverwriteFile } from '../../utils/fs';
|
|||
import { gitUtils } from '../../utils/gitUtils';
|
||||
import * as workspaceUtil from '../../utils/workspace';
|
||||
import { VSCodeUI } from '../../VSCodeUI';
|
||||
import { CSharpProjectCreator } from './CSharpProjectCreator';
|
||||
import { IProjectCreator } from './IProjectCreator';
|
||||
import { JavaProjectCreator } from './JavaProjectCreator';
|
||||
import { JavaScriptProjectCreator } from './JavaScriptProjectCreator';
|
||||
|
||||
const launchTaskId: string = 'launchFunctionApp';
|
||||
|
||||
const funcProblemMatcher: {} = {
|
||||
owner: extensionPrefix,
|
||||
pattern: [
|
||||
{
|
||||
regexp: '\\b\\B',
|
||||
file: 1,
|
||||
location: 2,
|
||||
message: 3
|
||||
}
|
||||
],
|
||||
background: {
|
||||
activeOnStart: true,
|
||||
beginsPattern: '^.*Stopping host.*',
|
||||
endsPattern: '^.*Job host started.*'
|
||||
}
|
||||
};
|
||||
|
||||
export async function createNewProject(telemetryProperties: TelemetryProperties, outputChannel: OutputChannel, functionAppPath?: string, language?: string, openFolder: boolean = true, ui: IUserInterface = new VSCodeUI()): Promise<void> {
|
||||
if (functionAppPath === undefined) {
|
||||
functionAppPath = await workspaceUtil.selectWorkspaceFolder(ui, localize('azFunc.selectFunctionAppFolderNew', 'Select the folder that will contain your function app'));
|
||||
|
@ -48,6 +30,7 @@ export async function createNewProject(telemetryProperties: TelemetryProperties,
|
|||
// Only display 'supported' languages that can be debugged in VS Code
|
||||
const languagePicks: Pick[] = [
|
||||
new Pick(ProjectLanguage.JavaScript),
|
||||
new Pick(ProjectLanguage.CSharp),
|
||||
new Pick(ProjectLanguage.Java)
|
||||
];
|
||||
|
||||
|
@ -64,6 +47,9 @@ export async function createNewProject(telemetryProperties: TelemetryProperties,
|
|||
case ProjectLanguage.JavaScript:
|
||||
projectCreator = new JavaScriptProjectCreator();
|
||||
break;
|
||||
case ProjectLanguage.CSharp:
|
||||
projectCreator = new CSharpProjectCreator(outputChannel);
|
||||
break;
|
||||
default:
|
||||
throw new Error(localize('unrecognizedLanguage', 'Unrecognized language "{0}"', language));
|
||||
}
|
||||
|
@ -79,12 +65,12 @@ export async function createNewProject(telemetryProperties: TelemetryProperties,
|
|||
|
||||
const tasksJsonPath: string = path.join(vscodePath, 'tasks.json');
|
||||
if (await confirmOverwriteFile(tasksJsonPath)) {
|
||||
await fsUtil.writeFormattedJson(tasksJsonPath, projectCreator.getTasksJson(launchTaskId, funcProblemMatcher));
|
||||
await fsUtil.writeFormattedJson(tasksJsonPath, projectCreator.getTasksJson());
|
||||
}
|
||||
|
||||
const launchJsonPath: string = path.join(vscodePath, 'launch.json');
|
||||
if (await confirmOverwriteFile(launchJsonPath)) {
|
||||
await fsUtil.writeFormattedJson(launchJsonPath, projectCreator.getLaunchJson(launchTaskId));
|
||||
await fsUtil.writeFormattedJson(launchJsonPath, projectCreator.getLaunchJson());
|
||||
}
|
||||
|
||||
const settingsJsonPath: string = path.join(vscodePath, 'settings.json');
|
||||
|
@ -96,6 +82,15 @@ export async function createNewProject(telemetryProperties: TelemetryProperties,
|
|||
await fsUtil.writeFormattedJson(settingsJsonPath, settings);
|
||||
}
|
||||
|
||||
if (projectCreator.getRecommendedExtensions) {
|
||||
const extensionsJsonPath: string = path.join(vscodePath, 'extensions.json');
|
||||
if (await confirmOverwriteFile(extensionsJsonPath)) {
|
||||
await fsUtil.writeFormattedJson(extensionsJsonPath, {
|
||||
recommendations: projectCreator.getRecommendedExtensions()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (openFolder && !workspaceUtil.isFolderOpenInWorkspace(functionAppPath)) {
|
||||
// If the selected folder is not open in a workspace, open it now. NOTE: This may restart the extension host
|
||||
await vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(functionAppPath), false);
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// tslint:disable-next-line:no-require-imports
|
||||
import ps = require('ps-node');
|
||||
import * as vscode from 'vscode';
|
||||
import { MessageItem } from 'vscode';
|
||||
import { DialogResponses } from '../DialogResponses';
|
||||
import { localize } from '../localize';
|
||||
import { funcHostTaskId } from './createNewProject/IProjectCreator';
|
||||
|
||||
export async function pickFuncProcess(): Promise<string | undefined> {
|
||||
const processList: IProcess[] = await new Promise((resolve: (processList: IProcess[]) => void, reject: (e: Error) => void): void => {
|
||||
//tslint:disable-next-line:no-unsafe-any
|
||||
ps.lookup(
|
||||
{
|
||||
command: '.*dotnet.*',
|
||||
arguments: '.*Azure\.Functions.*host.*start'
|
||||
},
|
||||
(error: Error | undefined, result: IProcess[]): void => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (processList.length === 0) {
|
||||
const startHost: vscode.MessageItem = { title: localize('startHost', 'Start Functions Host') };
|
||||
const result: MessageItem | undefined = await vscode.window.showWarningMessage(localize('funcHostNotRunning', 'You must have the Functions host running in the background before you debug.'), startHost, DialogResponses.cancel);
|
||||
if (result === startHost) {
|
||||
await vscode.commands.executeCommand('workbench.action.tasks.runTask', funcHostTaskId);
|
||||
}
|
||||
return undefined;
|
||||
} else if (processList.length === 1) {
|
||||
return processList[0].pid;
|
||||
} else {
|
||||
throw new Error(localize('multipleFuncHost', 'Detected multiple processes running the Functions host. Stop all but one process in order to debug.'));
|
||||
}
|
||||
}
|
||||
|
||||
interface IProcess {
|
||||
pid: string;
|
||||
}
|
|
@ -19,6 +19,7 @@ import { deleteNode } from './commands/deleteNode';
|
|||
import { deploy } from './commands/deploy';
|
||||
import { editAppSetting } from './commands/editAppSetting';
|
||||
import { openInPortal } from './commands/openInPortal';
|
||||
import { pickFuncProcess } from './commands/pickFuncProcess';
|
||||
import { renameAppSetting } from './commands/renameAppSetting';
|
||||
import { restartFunctionApp } from './commands/restartFunctionApp';
|
||||
import { startFunctionApp } from './commands/startFunctionApp';
|
||||
|
@ -57,6 +58,7 @@ export function activate(context: vscode.ExtensionContext): void {
|
|||
|
||||
const actionHandler: AzureActionHandler = new AzureActionHandler(context, outputChannel, reporter);
|
||||
actionHandler.registerCommand('azureFunctions.refresh', async (node?: IAzureNode) => await tree.refresh(node));
|
||||
actionHandler.registerCommand('azureFunctions.pickProcess', async () => await pickFuncProcess());
|
||||
actionHandler.registerCommand('azureFunctions.loadMore', async (node: IAzureNode) => await tree.loadMore(node));
|
||||
actionHandler.registerCommand('azureFunctions.openInPortal', async (node?: IAzureNode<FunctionAppTreeItem>) => await openInPortal(tree, node));
|
||||
actionHandler.registerCommandWithCustomTelemetry('azureFunctions.createFunction', async (properties: TelemetryProperties, _measurements: TelemetryMeasurements, functionAppPath?: string, templateId?: string, functionName?: string, ...functionSettings: string[]) => {
|
||||
|
|
|
@ -40,7 +40,11 @@ export class TemplateData {
|
|||
'HttpTriggerWithParameters-JavaScript',
|
||||
'ManualTrigger-JavaScript',
|
||||
'QueueTrigger-JavaScript',
|
||||
'TimerTrigger-JavaScript'
|
||||
'TimerTrigger-JavaScript',
|
||||
'HttpTrigger-CSharp',
|
||||
'BlobTrigger-CSharp',
|
||||
'QueueTrigger-CSharp',
|
||||
'TimerTrigger-CSharp'
|
||||
];
|
||||
|
||||
private readonly _javaTemplates: string[] = [
|
||||
|
@ -80,7 +84,7 @@ export class TemplateData {
|
|||
// Will refactor the code here when templates HTTP API is ready.
|
||||
// See issue here: https://github.com/Microsoft/vscode-azurefunctions/issues/84
|
||||
const javaTemplates: Template[] = this._templatesMap[runtime].filter((t: Template) => t.language === ProjectLanguage.JavaScript);
|
||||
return javaTemplates.filter((t: Template) => this._javaTemplates.find((vt: string) => vt === convertTemplateIdToJava(t.id)));
|
||||
return javaTemplates.filter((t: Template) => this._javaTemplates.find((vt: string) => vt === removeLanguageFromId(t.id)));
|
||||
} else {
|
||||
let templates: Template[] = this._templatesMap[runtime].filter((t: Template) => t.language.toLowerCase() === language.toLowerCase());
|
||||
switch (templateFilter) {
|
||||
|
@ -181,6 +185,6 @@ export class TemplateData {
|
|||
}
|
||||
}
|
||||
|
||||
export function convertTemplateIdToJava(id: string): string {
|
||||
return id.replace('-JavaScript', '');
|
||||
export function removeLanguageFromId(id: string): string {
|
||||
return id.split('-')[0];
|
||||
}
|
||||
|
|
|
@ -8,7 +8,8 @@ import * as vscode from 'vscode';
|
|||
import { localize } from '../localize';
|
||||
|
||||
export namespace cpUtils {
|
||||
export async function executeCommand(outputChannel: vscode.OutputChannel | undefined, workingDirectory: string, command: string, ...args: string[]): Promise<void> {
|
||||
export async function executeCommand(outputChannel: vscode.OutputChannel | undefined, workingDirectory: string, command: string, ...args: string[]): Promise<string> {
|
||||
let result: string = '';
|
||||
await new Promise((resolve: () => void, reject: (e: Error) => void): void => {
|
||||
const options: cp.SpawnOptions = {
|
||||
cwd: workingDirectory,
|
||||
|
@ -16,8 +17,15 @@ export namespace cpUtils {
|
|||
};
|
||||
const childProc: cp.ChildProcess = cp.spawn(command, args, options);
|
||||
|
||||
childProc.stdout.on('data', (data: string | Buffer) => {
|
||||
data = data.toString();
|
||||
result = result.concat(data);
|
||||
if (outputChannel) {
|
||||
outputChannel.append(data);
|
||||
}
|
||||
});
|
||||
|
||||
if (outputChannel) {
|
||||
childProc.stdout.on('data', (data: string | Buffer) => outputChannel.append(data.toString()));
|
||||
childProc.stderr.on('data', (data: string | Buffer) => outputChannel.append(data.toString()));
|
||||
}
|
||||
|
||||
|
@ -30,5 +38,7 @@ export namespace cpUtils {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as fse from 'fs-extra';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { OutputChannel } from "vscode";
|
||||
import { localize } from "../localize";
|
||||
import { cpUtils } from "./cpUtils";
|
||||
import * as fsUtil from './fs';
|
||||
|
||||
export namespace dotnetUtils {
|
||||
const projectPackageId: string = 'microsoft.azurefunctions.projecttemplates';
|
||||
const functionsPackageId: string = 'azure.functions.templates';
|
||||
const templateVersion: string = '2.0.0-beta-10138';
|
||||
|
||||
// tslint:disable:no-multiline-string
|
||||
const packagesCsproj: string = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="${projectPackageId}" Version="${templateVersion}" />
|
||||
<PackageReference Include="${functionsPackageId}" Version="${templateVersion}" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
`;
|
||||
|
||||
async function validateDotnetInstalled(workingDirectory: string): Promise<void> {
|
||||
try {
|
||||
await cpUtils.executeCommand(undefined, workingDirectory, 'dotnet', '--version');
|
||||
} catch (error) {
|
||||
throw new Error(localize('azFunc.dotnetNotInstalled', 'You must have the "dotnet" cli installed to perform this operation.'));
|
||||
}
|
||||
}
|
||||
|
||||
export const funcProjectId: string = 'azureFunctionsProjectTemplates';
|
||||
|
||||
export async function validateTemplatesInstalled(outputChannel: OutputChannel, workingDirectory: string): Promise<void> {
|
||||
await validateDotnetInstalled(workingDirectory);
|
||||
const listOutput: string = await cpUtils.executeCommand(undefined, workingDirectory, 'dotnet', 'new', '--list');
|
||||
if (!listOutput.includes(funcProjectId) || !listOutput.includes('HttpTrigger')) {
|
||||
const tempFolder: string = path.join(os.tmpdir(), fsUtil.getRandomHexString());
|
||||
await fse.ensureDir(tempFolder);
|
||||
try {
|
||||
await fse.writeFile(path.join(tempFolder, 'packages.csproj'), packagesCsproj);
|
||||
|
||||
// 'dotnet new --install' doesn't allow you to specify a source when installing templates
|
||||
// Once these templates are published to Nuget.org, we can just call 'dotnet new --install' directly and skip all the tempFolder/restore stuff
|
||||
outputChannel.show();
|
||||
outputChannel.appendLine(localize('installingTemplates', 'Downloading dotnet templates for Azure Functions...'));
|
||||
await cpUtils.executeCommand(undefined, tempFolder, 'dotnet', 'restore', '--packages', '.', '--source', 'https://www.myget.org/F/azure-appservice/api/v3/index.json', '--source', 'https://api.nuget.org/v3/index.json');
|
||||
|
||||
const fullProjectPackageId: string = `${projectPackageId}.${templateVersion}`;
|
||||
outputChannel.appendLine(localize('projectPackageId', 'Installing "{0}"', fullProjectPackageId));
|
||||
await cpUtils.executeCommand(undefined, tempFolder, 'dotnet', 'new', '--install', path.join(projectPackageId, templateVersion, `${fullProjectPackageId}.nupkg`));
|
||||
|
||||
const fullFunctionsPackageId: string = `${functionsPackageId}.${templateVersion}`;
|
||||
outputChannel.appendLine(localize('functionsPackageId', 'Installing "{0}"', fullFunctionsPackageId));
|
||||
await cpUtils.executeCommand(undefined, tempFolder, 'dotnet', 'new', '--install', path.join(functionsPackageId, templateVersion, `${fullFunctionsPackageId}.nupkg`));
|
||||
|
||||
outputChannel.appendLine(localize('finishedInstallingTemplates', 'Finished installing Azure Functions templates.'));
|
||||
} finally {
|
||||
await fse.remove(tempFolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,194 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as fse from 'fs-extra';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { WorkspaceConfiguration } from 'vscode';
|
||||
import { createFunction } from '../src/commands/createFunction/createFunction';
|
||||
import { extensionPrefix, ProjectLanguage, projectLanguageSetting, ProjectRuntime, projectRuntimeSetting, TemplateFilter, templateFilterSetting } from '../src/ProjectSettings';
|
||||
import { TemplateData } from '../src/templates/TemplateData';
|
||||
import * as fsUtil from '../src/utils/fs';
|
||||
import { TestAzureAccount } from './TestAzureAccount';
|
||||
import { TestUI } from './TestUI';
|
||||
|
||||
// tslint:disable-next-line:max-func-body-length
|
||||
suite('Create JavaScript Core Function Tests', () => {
|
||||
const templateData: TemplateData = new TemplateData();
|
||||
const testFolder: string = path.join(os.tmpdir(), `azFunc.createFuncTests${fsUtil.getRandomHexString()}`);
|
||||
const outputChannel: vscode.OutputChannel = vscode.window.createOutputChannel('Azure Functions Test');
|
||||
|
||||
const projectConfiguration: WorkspaceConfiguration = vscode.workspace.getConfiguration(extensionPrefix);
|
||||
// tslint:disable-next-line:no-backbone-get-set-outside-model
|
||||
const oldTemplateFilter: string | undefined = projectConfiguration.get(templateFilterSetting);
|
||||
const oldProjectLanguage: string | undefined = projectConfiguration.get(projectLanguageSetting);
|
||||
const oldProjectRuntime: string | undefined = projectConfiguration.get(projectRuntimeSetting);
|
||||
|
||||
suiteSetup(async () => {
|
||||
await fse.ensureDir(path.join(testFolder, '.vscode'));
|
||||
// Pretend to create the parent function app
|
||||
await Promise.all([
|
||||
fse.writeFile(path.join(testFolder, 'host.json'), ''),
|
||||
fse.writeFile(path.join(testFolder, 'local.settings.json'), '{ "Values": { "AzureWebJobsStorage": "test" } }'),
|
||||
fse.writeFile(path.join(testFolder, '.vscode', 'launch.json'), '')
|
||||
]);
|
||||
await projectConfiguration.update(templateFilterSetting, TemplateFilter.Core, vscode.ConfigurationTarget.Global);
|
||||
await projectConfiguration.update(projectLanguageSetting, ProjectLanguage.JavaScript, vscode.ConfigurationTarget.Global);
|
||||
await projectConfiguration.update(projectRuntimeSetting, ProjectRuntime.one, vscode.ConfigurationTarget.Global);
|
||||
});
|
||||
|
||||
suiteTeardown(async () => {
|
||||
outputChannel.dispose();
|
||||
await fse.remove(testFolder);
|
||||
await projectConfiguration.update(templateFilterSetting, oldTemplateFilter, vscode.ConfigurationTarget.Global);
|
||||
await projectConfiguration.update(projectLanguageSetting, oldProjectLanguage, vscode.ConfigurationTarget.Global);
|
||||
await projectConfiguration.update(projectRuntimeSetting, oldProjectRuntime, vscode.ConfigurationTarget.Global);
|
||||
});
|
||||
|
||||
const blobTrigger: string = 'Blob trigger';
|
||||
test(blobTrigger, async () => {
|
||||
await testCreateFunction(
|
||||
blobTrigger,
|
||||
undefined, // New App Setting
|
||||
'connectionStringKey1',
|
||||
'connectionString',
|
||||
undefined // Use default path
|
||||
);
|
||||
}).timeout(6000);
|
||||
|
||||
const cosmosDBTrigger: string = 'Cosmos DB trigger';
|
||||
test(cosmosDBTrigger, async () => {
|
||||
await testCreateFunction(
|
||||
cosmosDBTrigger,
|
||||
undefined, // New App Setting
|
||||
'connectionStringKey2',
|
||||
'connectionString',
|
||||
'dbName',
|
||||
'collectionName',
|
||||
undefined, // Use default for 'create leases if doesn't exist'
|
||||
undefined // Use default lease name
|
||||
);
|
||||
});
|
||||
|
||||
const eventHubTrigger: string = 'Event Hub trigger';
|
||||
test(eventHubTrigger, async () => {
|
||||
await testCreateFunction(
|
||||
eventHubTrigger,
|
||||
undefined, // New App Setting
|
||||
'connectionStringKey3',
|
||||
'connectionString',
|
||||
undefined, // Use default event hub name
|
||||
undefined // Use default event hub consumer group
|
||||
);
|
||||
});
|
||||
|
||||
const genericWebhook: string = 'Generic webhook';
|
||||
test(genericWebhook, async () => {
|
||||
await testCreateFunction(genericWebhook);
|
||||
});
|
||||
|
||||
const gitHubWebhook: string = 'GitHub webhook';
|
||||
test(gitHubWebhook, async () => {
|
||||
await testCreateFunction(gitHubWebhook);
|
||||
});
|
||||
|
||||
const httpTrigger: string = 'HTTP trigger';
|
||||
test(httpTrigger, async () => {
|
||||
await testCreateFunction(
|
||||
httpTrigger,
|
||||
undefined // Use default Authorization level
|
||||
);
|
||||
});
|
||||
|
||||
const httpTriggerWithParameters: string = 'HTTP trigger with parameters';
|
||||
test(httpTriggerWithParameters, async () => {
|
||||
await testCreateFunction(
|
||||
httpTriggerWithParameters,
|
||||
undefined // Use default Authorization level
|
||||
);
|
||||
});
|
||||
|
||||
const manualTrigger: string = 'Manual trigger';
|
||||
test(manualTrigger, async () => {
|
||||
await testCreateFunction(manualTrigger);
|
||||
});
|
||||
|
||||
const queueTrigger: string = 'Queue trigger';
|
||||
test(queueTrigger, async () => {
|
||||
await testCreateFunction(
|
||||
queueTrigger,
|
||||
undefined, // New App Setting
|
||||
'connectionStringKey4',
|
||||
'connectionString',
|
||||
undefined // Use default queue name
|
||||
);
|
||||
});
|
||||
|
||||
const serviceBusQueueTrigger: string = 'Service Bus Queue trigger';
|
||||
test(serviceBusQueueTrigger, async () => {
|
||||
await testCreateFunction(
|
||||
serviceBusQueueTrigger,
|
||||
undefined, // New App Setting
|
||||
'connectionStringKey5',
|
||||
'connectionString',
|
||||
undefined, // Use default access rights
|
||||
undefined // Use default queue name
|
||||
);
|
||||
});
|
||||
|
||||
const serviceBusTopicTrigger: string = 'Service Bus Topic trigger';
|
||||
test(serviceBusTopicTrigger, async () => {
|
||||
await testCreateFunction(
|
||||
serviceBusTopicTrigger,
|
||||
undefined, // New App Setting
|
||||
'connectionStringKey6',
|
||||
'connectionString',
|
||||
undefined, // Use default access rights
|
||||
undefined, // Use default topic name
|
||||
undefined // Use default subscription name
|
||||
);
|
||||
});
|
||||
|
||||
const timerTrigger: string = 'Timer trigger';
|
||||
test(timerTrigger, async () => {
|
||||
await testCreateFunction(
|
||||
timerTrigger,
|
||||
undefined // Use default schedule
|
||||
);
|
||||
});
|
||||
|
||||
test('createFunction API', async () => {
|
||||
const templateId: string = 'HttpTrigger-JavaScript';
|
||||
const functionName: string = 'createFunctionApi';
|
||||
const authLevel: string = 'Anonymous';
|
||||
await vscode.commands.executeCommand('azureFunctions.createFunction', testFolder, templateId, functionName, authLevel);
|
||||
await validateFunction(functionName);
|
||||
}).timeout(5000);
|
||||
|
||||
async function testCreateFunction(templateName: string, ...inputs: (string | undefined) []): Promise<void> {
|
||||
// Setup common inputs
|
||||
const funcName: string = templateName.replace(/ /g, '');
|
||||
inputs.unshift(funcName); // Specify the function name
|
||||
inputs.unshift(templateName); // Select the function template
|
||||
inputs.unshift(testFolder); // Select the test func app folder
|
||||
if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
|
||||
inputs.unshift('$(file-directory) Browse...'); // If the test environment has an open workspace, select the 'Browse...' option
|
||||
}
|
||||
|
||||
const ui: TestUI = new TestUI(inputs);
|
||||
await createFunction({}, outputChannel, new TestAzureAccount(), templateData, ui);
|
||||
assert.equal(inputs.length, 0, 'Not all inputs were used.');
|
||||
|
||||
await validateFunction(funcName);
|
||||
}
|
||||
|
||||
async function validateFunction(funcName: string): Promise<void> {
|
||||
const functionPath: string = path.join(testFolder, funcName);
|
||||
assert.equal(await fse.pathExists(path.join(functionPath, 'index.js')), true, 'index.js does not exist');
|
||||
assert.equal(await fse.pathExists(path.join(functionPath, 'function.json')), true, 'function.json does not exist');
|
||||
}
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as fse from 'fs-extra';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { WorkspaceConfiguration } from 'vscode';
|
||||
import { createFunction } from '../../src/commands/createFunction/createFunction';
|
||||
import { extensionPrefix, ProjectLanguage, projectLanguageSetting, ProjectRuntime, projectRuntimeSetting, TemplateFilter, templateFilterSetting } from '../../src/ProjectSettings';
|
||||
import { TemplateData } from '../../src/templates/TemplateData';
|
||||
import * as fsUtil from '../../src/utils/fs';
|
||||
import { TestAzureAccount } from '../TestAzureAccount';
|
||||
import { TestUI } from '../TestUI';
|
||||
|
||||
export abstract class FunctionTesterBase implements vscode.Disposable {
|
||||
public testFolder: string;
|
||||
public outputChannel: vscode.OutputChannel;
|
||||
|
||||
protected abstract _language: ProjectLanguage;
|
||||
protected abstract _runtime: ProjectRuntime;
|
||||
|
||||
private _templateData: TemplateData;
|
||||
private _projectConfiguration: WorkspaceConfiguration;
|
||||
private _oldTemplateFilter: string | undefined;
|
||||
private _oldProjectLanguage: string | undefined;
|
||||
private _oldProjectRuntime: string | undefined;
|
||||
|
||||
constructor() {
|
||||
this.testFolder = path.join(os.tmpdir(), `azFunc.createFuncTests${fsUtil.getRandomHexString()}`);
|
||||
this.outputChannel = vscode.window.createOutputChannel('Azure Functions Test');
|
||||
this._templateData = new TemplateData();
|
||||
this._projectConfiguration = vscode.workspace.getConfiguration(extensionPrefix);
|
||||
}
|
||||
|
||||
public async initAsync(): Promise<void> {
|
||||
// tslint:disable-next-line:no-backbone-get-set-outside-model
|
||||
this._oldTemplateFilter = this._projectConfiguration.get(templateFilterSetting);
|
||||
this._oldProjectLanguage = this._projectConfiguration.get(projectLanguageSetting);
|
||||
this._oldProjectRuntime = this._projectConfiguration.get(projectRuntimeSetting);
|
||||
|
||||
await fse.ensureDir(path.join(this.testFolder, '.vscode'));
|
||||
// Pretend to create the parent function app
|
||||
await Promise.all([
|
||||
fse.writeFile(path.join(this.testFolder, 'host.json'), ''),
|
||||
fse.writeFile(path.join(this.testFolder, 'local.settings.json'), '{ "Values": { "AzureWebJobsStorage": "test" } }'),
|
||||
fse.writeFile(path.join(this.testFolder, '.vscode', 'launch.json'), '')
|
||||
]);
|
||||
|
||||
await this._projectConfiguration.update(templateFilterSetting, TemplateFilter.All, vscode.ConfigurationTarget.Global);
|
||||
await this._projectConfiguration.update(projectLanguageSetting, this._language, vscode.ConfigurationTarget.Global);
|
||||
await this._projectConfiguration.update(projectRuntimeSetting, this._runtime, vscode.ConfigurationTarget.Global);
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
this.outputChannel.dispose();
|
||||
await fse.remove(this.testFolder);
|
||||
|
||||
await this._projectConfiguration.update(templateFilterSetting, this._oldTemplateFilter, vscode.ConfigurationTarget.Global);
|
||||
await this._projectConfiguration.update(projectLanguageSetting, this._oldProjectLanguage, vscode.ConfigurationTarget.Global);
|
||||
await this._projectConfiguration.update(projectRuntimeSetting, this._oldProjectRuntime, vscode.ConfigurationTarget.Global);
|
||||
}
|
||||
|
||||
public async testCreateFunction(templateName: string, ...inputs: (string | undefined) []): Promise<void> {
|
||||
// Setup common inputs
|
||||
const funcName: string = templateName.replace(/ /g, '');
|
||||
inputs.unshift(funcName); // Specify the function name
|
||||
inputs.unshift(templateName); // Select the function template
|
||||
inputs.unshift(this.testFolder); // Select the test func app folder
|
||||
if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
|
||||
inputs.unshift('$(file-directory) Browse...'); // If the test environment has an open workspace, select the 'Browse...' option
|
||||
}
|
||||
|
||||
const ui: TestUI = new TestUI(inputs);
|
||||
await createFunction({}, this.outputChannel, new TestAzureAccount(), this._templateData, ui);
|
||||
assert.equal(inputs.length, 0, 'Not all inputs were used.');
|
||||
|
||||
await this.validateFunction(funcName);
|
||||
}
|
||||
|
||||
public abstract async validateFunction(funcName: string): Promise<void>;
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as fse from 'fs-extra';
|
||||
import { IHookCallbackContext, ISuiteCallbackContext } from 'mocha';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { ProjectLanguage, ProjectRuntime } from '../../src/ProjectSettings';
|
||||
import { dotnetUtils } from '../../src/utils/dotnetUtils';
|
||||
import { FunctionTesterBase } from './FunctionTesterBase';
|
||||
|
||||
class CSharpFunctionTester extends FunctionTesterBase {
|
||||
protected _language: ProjectLanguage = ProjectLanguage.CSharp;
|
||||
protected _runtime: ProjectRuntime = ProjectRuntime.beta;
|
||||
|
||||
public async validateFunction(funcName: string): Promise<void> {
|
||||
assert.equal(await fse.pathExists(path.join(this.testFolder, `${funcName}.cs`)), true, 'cs file does not exist');
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-function-expression
|
||||
suite('Create C# Function Tests', async function (this: ISuiteCallbackContext): Promise<void> {
|
||||
this.timeout(10 * 1000);
|
||||
|
||||
const csTester: CSharpFunctionTester = new CSharpFunctionTester();
|
||||
|
||||
// tslint:disable-next-line:no-function-expression
|
||||
suiteSetup(async function (this: IHookCallbackContext): Promise<void> {
|
||||
this.timeout(60 * 1000);
|
||||
await csTester.initAsync();
|
||||
await dotnetUtils.validateTemplatesInstalled(csTester.outputChannel, csTester.testFolder);
|
||||
});
|
||||
|
||||
suiteTeardown(async () => {
|
||||
await csTester.dispose();
|
||||
});
|
||||
|
||||
const blobTrigger: string = 'Blob trigger';
|
||||
test(blobTrigger, async () => {
|
||||
await csTester.testCreateFunction(
|
||||
blobTrigger,
|
||||
undefined, // New App Setting
|
||||
'connectionStringKey1',
|
||||
'connectionString',
|
||||
undefined // Use default path
|
||||
);
|
||||
});
|
||||
|
||||
const httpTrigger: string = 'HTTP trigger';
|
||||
test(httpTrigger, async () => {
|
||||
await csTester.testCreateFunction(
|
||||
httpTrigger,
|
||||
undefined // Use default Authorization level
|
||||
);
|
||||
});
|
||||
|
||||
const queueTrigger: string = 'Queue trigger';
|
||||
test(queueTrigger, async () => {
|
||||
await csTester.testCreateFunction(
|
||||
queueTrigger,
|
||||
undefined, // New App Setting
|
||||
'connectionStringKey4',
|
||||
'connectionString',
|
||||
undefined // Use default queue name
|
||||
);
|
||||
});
|
||||
|
||||
const timerTrigger: string = 'Timer trigger';
|
||||
test(timerTrigger, async () => {
|
||||
await csTester.testCreateFunction(
|
||||
timerTrigger,
|
||||
undefined // Use default schedule
|
||||
);
|
||||
});
|
||||
|
||||
test('createFunction API', async () => {
|
||||
const templateId: string = 'HttpTrigger-CSharp';
|
||||
const functionName: string = 'createFunctionApi';
|
||||
const authLevel: string = 'Anonymous';
|
||||
await vscode.commands.executeCommand('azureFunctions.createFunction', csTester.testFolder, templateId, functionName, authLevel);
|
||||
await csTester.validateFunction(functionName);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,158 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as fse from 'fs-extra';
|
||||
import { ISuiteCallbackContext } from 'mocha';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { ProjectLanguage, ProjectRuntime } from '../../src/ProjectSettings';
|
||||
import { FunctionTesterBase } from './FunctionTesterBase';
|
||||
|
||||
class JSFunctionTester extends FunctionTesterBase {
|
||||
protected _language: ProjectLanguage = ProjectLanguage.JavaScript;
|
||||
protected _runtime: ProjectRuntime = ProjectRuntime.one;
|
||||
|
||||
public async validateFunction(funcName: string): Promise<void> {
|
||||
const functionPath: string = path.join(this.testFolder, funcName);
|
||||
assert.equal(await fse.pathExists(path.join(functionPath, 'index.js')), true, 'index.js does not exist');
|
||||
assert.equal(await fse.pathExists(path.join(functionPath, 'function.json')), true, 'function.json does not exist');
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:max-func-body-length no-function-expression
|
||||
suite('Create JavaScript Function Tests', async function (this: ISuiteCallbackContext): Promise<void> {
|
||||
this.timeout(6 * 1000);
|
||||
|
||||
const jsTester: JSFunctionTester = new JSFunctionTester();
|
||||
|
||||
suiteSetup(async () => {
|
||||
await jsTester.initAsync();
|
||||
});
|
||||
|
||||
suiteTeardown(async () => {
|
||||
await jsTester.dispose();
|
||||
});
|
||||
|
||||
const blobTrigger: string = 'Blob trigger';
|
||||
test(blobTrigger, async () => {
|
||||
await jsTester.testCreateFunction(
|
||||
blobTrigger,
|
||||
undefined, // New App Setting
|
||||
'connectionStringKey1',
|
||||
'connectionString',
|
||||
undefined // Use default path
|
||||
);
|
||||
});
|
||||
|
||||
const cosmosDBTrigger: string = 'Cosmos DB trigger';
|
||||
test(cosmosDBTrigger, async () => {
|
||||
await jsTester.testCreateFunction(
|
||||
cosmosDBTrigger,
|
||||
undefined, // New App Setting
|
||||
'connectionStringKey2',
|
||||
'connectionString',
|
||||
'dbName',
|
||||
'collectionName',
|
||||
undefined, // Use default for 'create leases if doesn't exist'
|
||||
undefined // Use default lease name
|
||||
);
|
||||
});
|
||||
|
||||
const eventHubTrigger: string = 'Event Hub trigger';
|
||||
test(eventHubTrigger, async () => {
|
||||
await jsTester.testCreateFunction(
|
||||
eventHubTrigger,
|
||||
undefined, // New App Setting
|
||||
'connectionStringKey3',
|
||||
'connectionString',
|
||||
undefined, // Use default event hub name
|
||||
undefined // Use default event hub consumer group
|
||||
);
|
||||
});
|
||||
|
||||
const genericWebhook: string = 'Generic webhook';
|
||||
test(genericWebhook, async () => {
|
||||
await jsTester.testCreateFunction(genericWebhook);
|
||||
});
|
||||
|
||||
const gitHubWebhook: string = 'GitHub webhook';
|
||||
test(gitHubWebhook, async () => {
|
||||
await jsTester.testCreateFunction(gitHubWebhook);
|
||||
});
|
||||
|
||||
const httpTrigger: string = 'HTTP trigger';
|
||||
test(httpTrigger, async () => {
|
||||
await jsTester.testCreateFunction(
|
||||
httpTrigger,
|
||||
undefined // Use default Authorization level
|
||||
);
|
||||
});
|
||||
|
||||
const httpTriggerWithParameters: string = 'HTTP trigger with parameters';
|
||||
test(httpTriggerWithParameters, async () => {
|
||||
await jsTester.testCreateFunction(
|
||||
httpTriggerWithParameters,
|
||||
undefined // Use default Authorization level
|
||||
);
|
||||
});
|
||||
|
||||
const manualTrigger: string = 'Manual trigger';
|
||||
test(manualTrigger, async () => {
|
||||
await jsTester.testCreateFunction(manualTrigger);
|
||||
});
|
||||
|
||||
const queueTrigger: string = 'Queue trigger';
|
||||
test(queueTrigger, async () => {
|
||||
await jsTester.testCreateFunction(
|
||||
queueTrigger,
|
||||
undefined, // New App Setting
|
||||
'connectionStringKey4',
|
||||
'connectionString',
|
||||
undefined // Use default queue name
|
||||
);
|
||||
});
|
||||
|
||||
const serviceBusQueueTrigger: string = 'Service Bus Queue trigger';
|
||||
test(serviceBusQueueTrigger, async () => {
|
||||
await jsTester.testCreateFunction(
|
||||
serviceBusQueueTrigger,
|
||||
undefined, // New App Setting
|
||||
'connectionStringKey5',
|
||||
'connectionString',
|
||||
undefined, // Use default access rights
|
||||
undefined // Use default queue name
|
||||
);
|
||||
});
|
||||
|
||||
const serviceBusTopicTrigger: string = 'Service Bus Topic trigger';
|
||||
test(serviceBusTopicTrigger, async () => {
|
||||
await jsTester.testCreateFunction(
|
||||
serviceBusTopicTrigger,
|
||||
undefined, // New App Setting
|
||||
'connectionStringKey6',
|
||||
'connectionString',
|
||||
undefined, // Use default access rights
|
||||
undefined, // Use default topic name
|
||||
undefined // Use default subscription name
|
||||
);
|
||||
});
|
||||
|
||||
const timerTrigger: string = 'Timer trigger';
|
||||
test(timerTrigger, async () => {
|
||||
await jsTester.testCreateFunction(
|
||||
timerTrigger,
|
||||
undefined // Use default schedule
|
||||
);
|
||||
});
|
||||
|
||||
test('createFunction API', async () => {
|
||||
const templateId: string = 'HttpTrigger-JavaScript';
|
||||
const functionName: string = 'createFunctionApi';
|
||||
const authLevel: string = 'Anonymous';
|
||||
await vscode.commands.executeCommand('azureFunctions.createFunction', jsTester.testFolder, templateId, functionName, authLevel);
|
||||
await jsTester.validateFunction(functionName);
|
||||
});
|
||||
});
|
|
@ -5,17 +5,30 @@
|
|||
|
||||
import * as assert from 'assert';
|
||||
import * as fse from 'fs-extra';
|
||||
import { IHookCallbackContext, ISuiteCallbackContext } from 'mocha';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { createNewProject } from '../src/commands/createNewProject/createNewProject';
|
||||
import { ProjectLanguage } from '../src/ProjectSettings';
|
||||
import { dotnetUtils } from '../src/utils/dotnetUtils';
|
||||
import * as fsUtil from '../src/utils/fs';
|
||||
import { TestUI } from './TestUI';
|
||||
|
||||
suite('Create New Project Tests', () => {
|
||||
// tslint:disable-next-line:no-function-expression
|
||||
suite('Create New Project Tests', async function (this: ISuiteCallbackContext): Promise<void> {
|
||||
this.timeout(15 * 1000);
|
||||
|
||||
const testFolderPath: string = path.join(os.tmpdir(), `azFunc.createNewProjectTests${fsUtil.getRandomHexString()}`);
|
||||
const outputChannel: vscode.OutputChannel = vscode.window.createOutputChannel('Azure Functions Test');
|
||||
|
||||
// tslint:disable-next-line:no-function-expression
|
||||
suiteSetup(async function (this: IHookCallbackContext): Promise<void> {
|
||||
this.timeout(60 * 1000);
|
||||
await fse.ensureDir(testFolderPath);
|
||||
await dotnetUtils.validateTemplatesInstalled(outputChannel, testFolderPath);
|
||||
});
|
||||
|
||||
suiteTeardown(async () => {
|
||||
outputChannel.dispose();
|
||||
await fse.remove(testFolderPath);
|
||||
|
@ -33,23 +46,34 @@ suite('Create New Project Tests', () => {
|
|||
undefined,
|
||||
undefined
|
||||
);
|
||||
await testJavaProjectFilesExist(projectPath);
|
||||
}).timeout(60 * 1000);
|
||||
await testCommonProjectFilesExist(projectPath, true);
|
||||
assert.equal(await fse.pathExists(path.join(projectPath, 'src')), true, 'src folder does not exist');
|
||||
assert.equal(await fse.pathExists(path.join(projectPath, 'pom.xml')), true, 'pom.xml does not exist');
|
||||
});
|
||||
|
||||
const javaScriptProject: string = 'JavaScriptProject';
|
||||
test(javaScriptProject, async () => {
|
||||
const projectPath: string = path.join(testFolderPath, javaScriptProject);
|
||||
await testCreateNewProject(projectPath, ProjectLanguage.JavaScript);
|
||||
await testProjectFilesExist(projectPath);
|
||||
await testCommonProjectFilesExist(projectPath);
|
||||
});
|
||||
|
||||
const csharpProject: string = 'CSharpProject';
|
||||
test(csharpProject, async () => {
|
||||
const projectPath: string = path.join(testFolderPath, csharpProject);
|
||||
await testCreateNewProject(projectPath, ProjectLanguage.CSharp);
|
||||
await testCommonProjectFilesExist(projectPath, true);
|
||||
const projectName: string = path.basename(projectPath);
|
||||
assert.equal(await fse.pathExists(path.join(projectPath, `${projectName}.csproj`)), true, 'csproj does not exist');
|
||||
});
|
||||
|
||||
test('createNewProject API', async () => {
|
||||
const projectPath: string = path.join(testFolderPath, 'createNewProjectApi');
|
||||
await vscode.commands.executeCommand('azureFunctions.createNewProject', projectPath, 'JavaScript', false /* openFolder */);
|
||||
await testProjectFilesExist(projectPath);
|
||||
await testCommonProjectFilesExist(projectPath);
|
||||
});
|
||||
|
||||
async function testCreateNewProject(projectPath: string, language: string, ...inputs: (string | undefined)[]): Promise<void> {
|
||||
async function testCreateNewProject(projectPath: string, language: string, ...inputs: (string | undefined) []): Promise<void> {
|
||||
// Setup common inputs
|
||||
inputs.unshift(language); // Specify the function name
|
||||
inputs.unshift(projectPath); // Select the test func app folder
|
||||
|
@ -62,7 +86,7 @@ suite('Create New Project Tests', () => {
|
|||
assert.equal(inputs.length, 0, 'Not all inputs were used.');
|
||||
}
|
||||
|
||||
async function testProjectFilesExist(projectPath: string): Promise<void> {
|
||||
async function testCommonProjectFilesExist(projectPath: string, hasExtensionRecommendations: boolean = false): Promise<void> {
|
||||
assert.equal(await fse.pathExists(path.join(projectPath, '.gitignore')), true, '.gitignore does not exist');
|
||||
assert.equal(await fse.pathExists(path.join(projectPath, 'host.json')), true, 'host.json does not exist');
|
||||
assert.equal(await fse.pathExists(path.join(projectPath, 'local.settings.json')), true, 'function.json does not exist');
|
||||
|
@ -71,11 +95,8 @@ suite('Create New Project Tests', () => {
|
|||
assert.equal(await fse.pathExists(path.join(vscodePath, 'settings.json')), true, 'settings.json does not exist');
|
||||
assert.equal(await fse.pathExists(path.join(vscodePath, 'launch.json')), true, 'launch.json does not exist');
|
||||
assert.equal(await fse.pathExists(path.join(vscodePath, 'tasks.json')), true, 'tasks.json does not exist');
|
||||
}
|
||||
|
||||
async function testJavaProjectFilesExist(projectPath: string): Promise<void> {
|
||||
await testProjectFilesExist(projectPath);
|
||||
assert.equal(await fse.pathExists(path.join(projectPath, 'src')), true, 'src folder does not exist');
|
||||
assert.equal(await fse.pathExists(path.join(projectPath, 'pom.xml')), true, 'pom.xml does not exist');
|
||||
if (hasExtensionRecommendations) {
|
||||
assert.equal(await fse.pathExists(path.join(vscodePath, 'extensions.json')), true, 'extensions.json does not exist');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -20,4 +20,9 @@ suite('Template Data Tests', () => {
|
|||
const templates: Template[] = await templateData.getTemplates(ProjectLanguage.Java, ProjectRuntime.beta, TemplateFilter.Verified);
|
||||
assert.equal(templates.length, 4);
|
||||
});
|
||||
|
||||
test('Default CSharp Templates Count', async () => {
|
||||
const templates: Template[] = await templateData.getTemplates(ProjectLanguage.CSharp, ProjectRuntime.beta, TemplateFilter.Verified);
|
||||
assert.equal(templates.length, 4);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ are grateful to these developers for their contribution to open source.
|
|||
2. request-promise (https://github.com/request/request-promise)
|
||||
3. xml2js (https://github.com/Leonidas-from-XIV/node-xml2js)
|
||||
4. clipboardy (https://github.com/sindresorhus/clipboardy)
|
||||
5. ps-node (https://github.com/neekey/ps)
|
||||
|
||||
fs-extra NOTICES BEGIN HERE
|
||||
=============================
|
||||
|
@ -94,4 +95,32 @@ The above copyright notice and this permission notice shall be included in all c
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
END OF clipboardy NOTICES AND INFORMATION
|
||||
==================================
|
||||
==================================
|
||||
|
||||
ps-node NOTICES BEGIN HERE
|
||||
=============================
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Neekey
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
END OF ps-node NOTICES AND INFORMATION
|
||||
==================================
|
||||
|
|
Загрузка…
Ссылка в новой задаче