Commiting all latest code from
https://github.com/Azure/azure-sdk-for-node and making this to be the new home for this extension
This commit is contained in:
Родитель
01757c3726
Коммит
39130f577b
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": false,
|
||||||
|
"commonjs": true,
|
||||||
|
"es6": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true
|
||||||
|
},
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-const-assign": "warn",
|
||||||
|
"no-this-before-super": "warn",
|
||||||
|
"no-undef": "warn",
|
||||||
|
"no-unreachable": "warn",
|
||||||
|
"no-unused-vars": "warn",
|
||||||
|
"constructor-super": "warn",
|
||||||
|
"valid-typeof": "warn"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
|
// for the documentation about the extensions.json format
|
||||||
|
"recommendations": [
|
||||||
|
"dbaeumer.vscode-eslint"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
// A launch configuration that launches the extension inside a new window
|
||||||
|
{
|
||||||
|
"version": "0.1.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch Extension",
|
||||||
|
"type": "extensionHost",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeExecutable": "${execPath}",
|
||||||
|
"args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
|
||||||
|
"stopOnEntry": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Launch Tests",
|
||||||
|
"type": "extensionHost",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeExecutable": "${execPath}",
|
||||||
|
"args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/test" ],
|
||||||
|
"stopOnEntry": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
// Place your settings in this file to overwrite default and user settings.
|
||||||
|
{
|
||||||
|
"typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
.vscode/**
|
||||||
|
.vscode-test/**
|
||||||
|
test/**
|
||||||
|
.gitignore
|
||||||
|
jsconfig.json
|
||||||
|
vsc-extension-quickstart.md
|
||||||
|
.eslintrc.json
|
43
README.md
43
README.md
|
@ -1,3 +1,44 @@
|
||||||
# Contributing
|
# Azure Node Essentials
|
||||||
|
|
||||||
|
This extension provides tools for NodeJs developers working with Azure SDKs.
|
||||||
|
|
||||||
|
## Feature List
|
||||||
|
|
||||||
|
1. Project and file scaffolding
|
||||||
|
* `yo azure-node` to create
|
||||||
|
* Javascript or Typescript project with package.json set up to target Azure SDKs
|
||||||
|
* Empty .js or .ts files
|
||||||
|
* pre populated .tsconfig or .jsconfig files
|
||||||
|
* create a service principal
|
||||||
|
1. Snippets for some common operations such as authentication, creating a service principal.
|
||||||
|
* `loginInt` : generate code for interactive login
|
||||||
|
* `loginPwd` : generate code for logging in with username and password
|
||||||
|
* `loginSp` : generate code for logging in with a service principal
|
||||||
|
* `spCreate` : generate code to create a service principal
|
||||||
|
1. Code generation scenarios
|
||||||
|
* command `Azure-Node: Generate code for template deployment` : generate code for template deployment
|
||||||
|
|
||||||
|
### Sample workflow
|
||||||
|
|
||||||
|
1. Open VS code with an empty workspace (empty folder)
|
||||||
|
1. Bring up VS code command palette, invoke `yo`
|
||||||
|
1. Choose `azure-node` generator and invoke it.
|
||||||
|
1. Choose `* app` to invoke the main generator (the sub generators for files are listed at the root level)
|
||||||
|
1. Choose a Javascript project and proceed.
|
||||||
|
1. This should initialize your project and install npm dependencies
|
||||||
|
1. Meanwhile, open the folder in VSCode and navigate to index.js
|
||||||
|
1. Notice that package.json has been set up and index.js has boiler plate code for authentication.
|
||||||
|
1. Place caret on the line after `// TODO: Write your application logic here.`
|
||||||
|
1. From VS Code's command palette, invoke `>Azure-Node: Generate code for template deployment`
|
||||||
|
1. The extension generates code for template deployment in the file currently active in the vscode editor and adds required dependencies to package.json.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
The following package/extension dependencies are auto installed when you install this extension.
|
||||||
|
|
||||||
|
1. [vscode yo](https://marketplace.visualstudio.com/items?itemName=samverschueren.yo)
|
||||||
|
1. [generator-azure-node](https://github.com/Azure/generator-azure-node)
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Change Log for Azure Node Essentials
|
||||||
|
|
||||||
|
## 0.2.3 [2017-02-08]
|
||||||
|
|
||||||
|
1. Bug fixes for Mac
|
||||||
|
1. updates to readme
|
||||||
|
1. check for generator package's version and upgrade if not latest
|
||||||
|
|
||||||
|
## 0.2.0 - 0.2.2 [2017-02-07]
|
||||||
|
|
||||||
|
1. No product change, updates to readme and other metadata about the project.
|
||||||
|
|
||||||
|
## 0.1.0 [2017-02-07]
|
||||||
|
|
||||||
|
1. Project and file scaffolding
|
||||||
|
1. Snippets for some common operations such as authentication, creating a service principal.
|
||||||
|
1. Code generation
|
||||||
|
* generate code for template deployment
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es6",
|
||||||
|
"lib": [
|
||||||
|
"es6"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
{
|
||||||
|
"name": "azurenodeessentials",
|
||||||
|
"displayName": "Azure Node Essentials",
|
||||||
|
"description": "Azure Node SDK Essentials for VS Code",
|
||||||
|
"version": "0.2.3",
|
||||||
|
"publisher": "azuresdkteam",
|
||||||
|
"engines": {
|
||||||
|
"vscode": "^1.8.0"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Azure/azure-node-essentials"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"azure",
|
||||||
|
"cloud",
|
||||||
|
"azure-sdk"
|
||||||
|
],
|
||||||
|
"icon": "resources/azure-icon.png",
|
||||||
|
"galleryBanner": {
|
||||||
|
"color": "#00abec",
|
||||||
|
"theme": "light"
|
||||||
|
},
|
||||||
|
"categories": [
|
||||||
|
"Other",
|
||||||
|
"Snippets"
|
||||||
|
],
|
||||||
|
"activationEvents": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"main": "./src/extension",
|
||||||
|
"contributes": {
|
||||||
|
"snippets": [
|
||||||
|
{
|
||||||
|
"language": "javascript",
|
||||||
|
"path": "./src/snippets/snippets.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "typescript",
|
||||||
|
"path": "./src/snippets/snippets.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"command": "Azure-Node.template-deploy",
|
||||||
|
"title": "Generate code for template deployment",
|
||||||
|
"category": "Azure-Node"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scripts": {},
|
||||||
|
"dependencies": {
|
||||||
|
"npm": "*",
|
||||||
|
"escodegen": "^1.8.1",
|
||||||
|
"esprima": "^3.1.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^2.0.3",
|
||||||
|
"vscode": "^1.0.0",
|
||||||
|
"mocha": "^2.3.3",
|
||||||
|
"eslint": "^3.6.0",
|
||||||
|
"@types/node": "^6.0.40",
|
||||||
|
"@types/mocha": "^2.2.32"
|
||||||
|
},
|
||||||
|
"extensionDependencies": [
|
||||||
|
"samverschueren.yo"
|
||||||
|
]
|
||||||
|
}
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 4.2 KiB |
|
@ -0,0 +1,108 @@
|
||||||
|
var esprima = require('esprima');
|
||||||
|
var escodegen = require('escodegen');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates require('') statements to be inserted.
|
||||||
|
*
|
||||||
|
* This scans through the current document and determines the minimal non-duplicated list
|
||||||
|
* of imports we need to generate.
|
||||||
|
*
|
||||||
|
* @param {any} document the current file
|
||||||
|
* @returns a Set of require statements to be inserted.
|
||||||
|
*/
|
||||||
|
exports.generateRequireStatements = function generateRequireStatements(document, requiredModules) {
|
||||||
|
var foundImportsGroup = false;
|
||||||
|
var existingModules = new Set();
|
||||||
|
var requireStatementMatcher = /var\s*\w+\s+=\s+require\(\'([a-zA-Z0-9_-]*)\'\);*\s*/;
|
||||||
|
var insertionLine = 0;
|
||||||
|
|
||||||
|
// gather modules that are already imported in the document
|
||||||
|
for (var index = 0; index < document.lineCount; index++) {
|
||||||
|
var line = document.lineAt(index);
|
||||||
|
var matches = line.text.match(requireStatementMatcher);
|
||||||
|
if (matches) {
|
||||||
|
if (!foundImportsGroup) {
|
||||||
|
foundImportsGroup = true;
|
||||||
|
}
|
||||||
|
// as long as it matches the require statement, collect the list of imported modules.
|
||||||
|
existingModules.add(matches[1]);
|
||||||
|
} else if (foundImportsGroup) {
|
||||||
|
// we've already discovered a require statement previously and this new line no longer matches the pattern.
|
||||||
|
// we've gone past the require statement group. Do not loop through entire source text.
|
||||||
|
// record this line number, as we'll insert our require statements here. then, bail out of reading the source document.
|
||||||
|
insertionLine = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// difference between existing modules and ones we require, are the ones we need to insert.
|
||||||
|
var modulesToInsert = requiredModules.filter(x => !existingModules.has(x));
|
||||||
|
|
||||||
|
// generate code for importing modules.
|
||||||
|
var requireStatements = new Set();
|
||||||
|
for (var module of modulesToInsert) {
|
||||||
|
var name = this.getNameAssignmentForModule(module);
|
||||||
|
var statement = this.generateRequireStatement(name, module)
|
||||||
|
requireStatements.add(statement);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = {
|
||||||
|
line: insertionLine,
|
||||||
|
code: requireStatements
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a require('') statement with given information.
|
||||||
|
*
|
||||||
|
* This will not perform any additional checks. If you need to generate only
|
||||||
|
* if module is not already imported, use `generateRequireStatements`
|
||||||
|
*/
|
||||||
|
exports.generateRequireStatement = function generateRequireStatement(name, packageName) {
|
||||||
|
if (!name) {
|
||||||
|
name = packageName;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = `var ${name} = require('${packageName}');\r\n`;
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: do this programmatically. Normalize 'name', strip dashes and other illegal chars. use pascal casing.
|
||||||
|
exports.getNameAssignmentForModule = function getNameAssignmentForModule(module) {
|
||||||
|
switch (module) {
|
||||||
|
case 'azure-arm-resource':
|
||||||
|
return 'ResourceManagement';
|
||||||
|
case 'ms-rest':
|
||||||
|
return 'msRest';
|
||||||
|
case 'ms-rest-azure':
|
||||||
|
return 'msRestAzure';
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.generateNewLine = function generateNewLine() {
|
||||||
|
return this.generateCode('\r\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
// parse the text blob and emit code from the AST.
|
||||||
|
exports.generateCode = function generateCode(text) {
|
||||||
|
var ast = esprima.parse(text, { raw: true, tokens: true, range: true, comment: true });
|
||||||
|
ast = escodegen.attachComments(ast, ast.comments, ast.tokens);
|
||||||
|
|
||||||
|
var codegenOptions = {
|
||||||
|
comment: true,
|
||||||
|
format: {
|
||||||
|
indent: {
|
||||||
|
style: ' '
|
||||||
|
},
|
||||||
|
preserveBlankLines: true,
|
||||||
|
},
|
||||||
|
sourceCode: text
|
||||||
|
};
|
||||||
|
|
||||||
|
var code = escodegen.generate(ast, codegenOptions);
|
||||||
|
return code;
|
||||||
|
};
|
|
@ -0,0 +1,48 @@
|
||||||
|
var codegenerator = require('./codegen');
|
||||||
|
|
||||||
|
const deployTemplateFunctionName = 'deployTemplate';
|
||||||
|
|
||||||
|
// Generates NodeJs code for arm template deployment.
|
||||||
|
exports.deployTemplate = function deployTemplate() {
|
||||||
|
var text = `function ${deployTemplateFunctionName}(credentials, callback){\
|
||||||
|
// TODO: initialize these variables
|
||||||
|
var subscriptionId;\
|
||||||
|
var resourceGroupName;\
|
||||||
|
var deploymentName;\
|
||||||
|
var templateFilePath;\
|
||||||
|
var templateParametersFilePath;\
|
||||||
|
var template;\
|
||||||
|
var templateParameters;\
|
||||||
|
var parameters = {\
|
||||||
|
template: template,\
|
||||||
|
parameters: templateParameters,\
|
||||||
|
mode: \'Complete\'\
|
||||||
|
};\
|
||||||
|
\r\n
|
||||||
|
try {\
|
||||||
|
template = JSON.parse(fs.readFileSync(templateFilePath));\
|
||||||
|
templateParameters = JSON.parse(fs.readFileSync(templateParametersFilePath));\
|
||||||
|
} catch (error) {\
|
||||||
|
console.error('Encountered error parsing template file:', error);\
|
||||||
|
}\
|
||||||
|
\r\n
|
||||||
|
var resourceClient = new ResourceManagement.ResourceManagementClient(credentials, subscriptionId);\
|
||||||
|
resourceClient.deployments.createOrUpdate(resourceGroupName, deploymentName, parameters, callback);\
|
||||||
|
}`;
|
||||||
|
|
||||||
|
return codegenerator.generateCode(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.generateRequireStatements = function generateRequireStatements(document) {
|
||||||
|
const requiredModules = ['fs', 'azure-arm-resource', 'ms-rest', 'ms-rest-azure'];
|
||||||
|
return codegenerator.generateRequireStatements(document, requiredModules);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.deployTemplateCallSite = function deployTemplateCallSite() {
|
||||||
|
var text = `${deployTemplateFunctionName}(credentials, function(err, result){ });`;
|
||||||
|
return codegenerator.generateCode(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getPackageDependencies = function getPackageDependencies() {
|
||||||
|
return ['azure-arm-resource', 'ms-rest', 'ms-rest-azure'];
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
var fs = require('fs');
|
||||||
|
var npm = require('npm');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes byte order marker. This catches EF BB BF (the UTF-8 BOM)
|
||||||
|
* because the buffer-to-string conversion in `fs.readFile()`
|
||||||
|
* translates it to FEFF, the UTF-16 BOM.
|
||||||
|
*/
|
||||||
|
exports.stripBOM = function stripBOM(content) {
|
||||||
|
if (Buffer.isBuffer(content)) {
|
||||||
|
content = content.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.charCodeAt(0) === 0xFEFF || content.charCodeAt(0) === 0xFFFE) {
|
||||||
|
content = content.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a parsed JSON from the given local file path
|
||||||
|
*/
|
||||||
|
exports.parseJson = function parseJson(filePath) {
|
||||||
|
let result = null;
|
||||||
|
if (!filePath || (filePath && typeof filePath.valueOf() !== 'string')) {
|
||||||
|
let err = new Error('a local file path to package.json is required and must be of type string.');
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
result = JSON.parse(this.stripBOM(fs.readFileSync(filePath, 'utf8')));
|
||||||
|
return Promise.resolve(result);
|
||||||
|
} catch (err) {
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add given list of dependencies to package.json
|
||||||
|
*/
|
||||||
|
exports.addDependenciesIfRequired = function addDependenciesIfRequired(filePath, packages) {
|
||||||
|
this.parseJson(filePath).then(function (jsonObject) {
|
||||||
|
if (jsonObject) {
|
||||||
|
var existingPackages = new Set();
|
||||||
|
for (var item in jsonObject.dependencies) {
|
||||||
|
existingPackages.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
var packagesToInsert = packages.filter(x => !existingPackages.has(x));
|
||||||
|
var promises = packagesToInsert.map((pkgName) => updateDependency(pkgName));
|
||||||
|
|
||||||
|
Promise.all(promises).then(function (entries) {
|
||||||
|
if (!jsonObject.dependencies) {
|
||||||
|
jsonObject.dependencies = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var item in entries) {
|
||||||
|
var kvp = entries[item].split(':');
|
||||||
|
jsonObject.dependencies[kvp[0]] = kvp[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(jsonObject, null, ' '));
|
||||||
|
}).catch(function (err) {
|
||||||
|
return console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a package name as string, pings npm about the package and
|
||||||
|
* returns a well formed package dependency entry that can be inserted in package.json
|
||||||
|
*/
|
||||||
|
function updateDependency(pkgName) {
|
||||||
|
var promise = new Promise(function (resolve, reject) {
|
||||||
|
npm.load(function (err) {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
npm.commands.view([pkgName, 'name', 'version'], true, function (err, info) {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = JSON.parse(JSON.stringify(info));
|
||||||
|
var dependency = json[Object.keys(json)[0]];
|
||||||
|
var entry = `${dependency.name}:^${dependency.version}`;
|
||||||
|
return resolve(entry);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
|
@ -0,0 +1,66 @@
|
||||||
|
var fs = require('fs');
|
||||||
|
var path = require('path');
|
||||||
|
var vscode = require('vscode');
|
||||||
|
var codegen = require('../codegen/codgen.template-deploy');
|
||||||
|
var jsonEditor = require('../codegen/jsoneditor');
|
||||||
|
|
||||||
|
exports.createCommand = function createCommand() {
|
||||||
|
vscode.commands.registerCommand('Azure-Node.template-deploy', function () {
|
||||||
|
|
||||||
|
if (!vscode.window.activeTextEditor) {
|
||||||
|
vscode.window.showInformationMessage(`please open a .js file in the editor and then use this code generator command.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update package.json
|
||||||
|
updatePackageJson();
|
||||||
|
|
||||||
|
// generate code in current document
|
||||||
|
return generateCodeInEditor();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function updatePackageJson() {
|
||||||
|
// TODO: search rootDir\package.json
|
||||||
|
var filePath = path.join(vscode.workspace.rootPath, 'package.json');
|
||||||
|
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
var packages = codegen.getPackageDependencies();
|
||||||
|
jsonEditor.addDependenciesIfRequired(filePath, packages);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function generateCodeInEditor() {
|
||||||
|
// generate code to be inserted.
|
||||||
|
const document = vscode.window.activeTextEditor.document;
|
||||||
|
const lineCount = document.lineCount;
|
||||||
|
var importsAndLineNumber = codegen.generateRequireStatements(document);
|
||||||
|
var methodBody = codegen.deployTemplate();
|
||||||
|
var callsite = codegen.deployTemplateCallSite();
|
||||||
|
|
||||||
|
vscode.window.activeTextEditor.edit((builder) => {
|
||||||
|
// insert import statements.
|
||||||
|
// Insertion point is the line where import group ends.
|
||||||
|
if (importsAndLineNumber) {
|
||||||
|
var importPos = new vscode.Position(importsAndLineNumber.line, 0);
|
||||||
|
var imports = importsAndLineNumber.code;
|
||||||
|
for (var importStatement of imports) {
|
||||||
|
builder.insert(importPos, importStatement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert code for template deployment.
|
||||||
|
const range = new vscode.Range(new vscode.Position(lineCount, 0), new vscode.Position(lineCount + 1, 0));
|
||||||
|
builder.replace(range, methodBody);
|
||||||
|
|
||||||
|
// fix callsite to invoke the function that was newly generated.
|
||||||
|
const currentPos = new vscode.Position(vscode.window.activeTextEditor.selection.active.line, 0);
|
||||||
|
builder.insert(currentPos, callsite);
|
||||||
|
});
|
||||||
|
|
||||||
|
// format the entire document.
|
||||||
|
// the code we inserted was generated as well-formatted but indenting is relative to the existing text
|
||||||
|
// in the document. Since we didn't examine existing text and are unaware of the indent depth where
|
||||||
|
// generated code will be inserted, we have to reformat the whole document. If this leads to performance issues, we'll revisit this logic.
|
||||||
|
return vscode.commands.executeCommand("editor.action.formatDocument");
|
||||||
|
};
|
|
@ -0,0 +1,107 @@
|
||||||
|
let vscode = require('vscode');
|
||||||
|
let path = require('path');
|
||||||
|
let fs = require('fs');
|
||||||
|
let utils = require('./utils');
|
||||||
|
|
||||||
|
// TODO: find a way to have the extension auto update the package dependency, instead of having an update to VSCode extension.
|
||||||
|
// TODO: try to optimize time spent in this method.
|
||||||
|
// instead of enumerating all the installed npm packages every time on activation. drop a file to this extension's install path
|
||||||
|
// after installing dependencies. after the first time, just check for the presence of that sentinel file and bail.
|
||||||
|
// that does not ensure the dependency will always exist though.
|
||||||
|
// TODO: check for package version updates and re-install if necessary.
|
||||||
|
|
||||||
|
// Called once to activate the extension.
|
||||||
|
// The only thing this extension needs to do is to ensure that a certain npm package is installed globally.
|
||||||
|
// Ideally this should be done during install time, but VSCode does not support install time tasks.
|
||||||
|
// So, we do this on activation. Ideally, this is a one time task.
|
||||||
|
function activate(context) {
|
||||||
|
|
||||||
|
// Register commands.
|
||||||
|
var commandFilesPath = path.join(context.extensionPath, 'src', 'commands');
|
||||||
|
fs.readdir(commandFilesPath, (err, files) => {
|
||||||
|
files.forEach((file) => {
|
||||||
|
context.subscriptions.push(
|
||||||
|
require('./commands/' + path.basename(file, '.js')).createCommand()
|
||||||
|
);
|
||||||
|
console.log(path.basename(file, '.js') + ' command added');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Install dependencies.
|
||||||
|
ensureDependenciesAreInstalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureDependenciesAreInstalled() {
|
||||||
|
// Download and install template generator package.
|
||||||
|
var extensionName = 'Azure-Node-Essentials';
|
||||||
|
var generatorPackageName = 'generator-azure-node';
|
||||||
|
var generatorPackageVersion = '0.1.0'; // TODO: query npm and obtain latest version. That would mean updating package would not require an extension update.
|
||||||
|
|
||||||
|
utils.isNodeInstalled().then(function (result) {
|
||||||
|
if (!result) {
|
||||||
|
vscode.window.showInformationMessage(`Please install NodeJS and then run ${extensionName} extension.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check the npm cache on this machine and determine if our dependency is present.
|
||||||
|
// if the dependency is present and is the latest version, there is nothing more to do.
|
||||||
|
// if it is not present or is not the latest, install it from npm.
|
||||||
|
utils.npmList().then(function (listOfPackages) {
|
||||||
|
var generatorPackage = listOfPackages.find(function (item) {
|
||||||
|
return item.startsWith(generatorPackageName);
|
||||||
|
});
|
||||||
|
|
||||||
|
var packageInfo = { present: false, needsUpgrade: false };
|
||||||
|
if (generatorPackage) {
|
||||||
|
packageInfo.present = true;
|
||||||
|
packageInfo.needsUpgrade = generatorPackage.split('@')[1] !== generatorPackageVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(packageInfo);
|
||||||
|
}).then(function (packageInfo) {
|
||||||
|
if (!packageInfo.present || packageInfo.needsUpgrade) {
|
||||||
|
var options = {
|
||||||
|
global: true
|
||||||
|
};
|
||||||
|
|
||||||
|
var actionPerformed;
|
||||||
|
if (packageInfo.needsUpgrade) {
|
||||||
|
actionPerformed = 'upgrade';
|
||||||
|
} else {
|
||||||
|
actionPerformed = 'install';
|
||||||
|
// indicate to the user that the extension needs to perform some one-time startup tasks
|
||||||
|
vscode.window.showInformationMessage(`${extensionName} is installing dependencies...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
var installTask = utils.npmInstall([generatorPackageName], options);
|
||||||
|
return installTask.then(
|
||||||
|
function onFulfilled(value) {
|
||||||
|
return { action: actionPerformed, status: value };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var result = { action: 'pre-installed', status: true }; // (pre-installed, nothing to do.)
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}).then(function (result) {
|
||||||
|
if (result && result.status === true) {
|
||||||
|
if (result.action === 'install') {
|
||||||
|
vscode.window.showInformationMessage(`${extensionName} successfully installed dependencies and is ready for use.`);
|
||||||
|
} else if (result.action === 'upgrade') {
|
||||||
|
vscode.window.setStatusBarMessage(`${extensionName} successfully upgraded dependencies in the background.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(function (err) {
|
||||||
|
vscode.window.showInformationMessage(`An error occurred while ${extensionName} was installing dependencies. ${err}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.activate = activate;
|
||||||
|
|
||||||
|
function deactivate() {
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.deactivate = deactivate;
|
|
@ -0,0 +1,136 @@
|
||||||
|
{
|
||||||
|
"Login Interactively": {
|
||||||
|
"prefix": "loginInt",
|
||||||
|
"body": "msRestAzure.interactiveLogin(function(${1:err}, ${2:credentials}) {\n\tif (err) {\n\t\tconsole.log(err);\n\t\treturn;\n\t}\n\t$0\n});",
|
||||||
|
"description": "Interactively login to Azure account"
|
||||||
|
},
|
||||||
|
"Login with Service Principal": {
|
||||||
|
"prefix": "loginSp",
|
||||||
|
"body": "msRestAzure.loginWithServicePrincipalSecret(${1:clientId}, ${2:secret}, ${3:domain}, function(${4:err}, ${5:credentials}) {\n\tif (err) {\n\t\tconsole.log(err);\n\t\treturn;\n\t}\n\t$0\n});",
|
||||||
|
"description": "Login with Service principal into Azure account"
|
||||||
|
},
|
||||||
|
"Login with Password": {
|
||||||
|
"prefix": "loginPwd",
|
||||||
|
"body": "msRestAzure.loginWithUsernamePassword(${1:username}, ${2:password}, function(${3:err}, ${4:credentials}) {\n\tif (err) {\n\t\tconsole.log(err);\n\t\treturn;\n\t}\n\t$0\n});",
|
||||||
|
"description": "Login with username and password into Azure account"
|
||||||
|
},
|
||||||
|
"Create Service Principal": {
|
||||||
|
"prefix": "spCreate",
|
||||||
|
"body": ["'use strict';",
|
||||||
|
"",
|
||||||
|
// Documentation text @0-indent
|
||||||
|
"// Steps:",
|
||||||
|
"// 1. Create AD application",
|
||||||
|
"// 2. Create SP on top of the AD application",
|
||||||
|
"// 3. Assigning the Contributor role to the SP",
|
||||||
|
"",
|
||||||
|
"function createServicePrincipal() {",
|
||||||
|
// imports @1-indent
|
||||||
|
"\tvar msrestazure = require('ms-rest-azure');",
|
||||||
|
"\tvar graph = require('azure-graph');",
|
||||||
|
"\tvar authorization = require('azure-arm-authorization');",
|
||||||
|
"\tvar util = require('util');",
|
||||||
|
"\tvar moment = require('moment');",
|
||||||
|
"",
|
||||||
|
// variable declarations @1-indent
|
||||||
|
"\t// TODO: Initialize these variables",
|
||||||
|
"\tvar ${0:tenantId;}",
|
||||||
|
"\tvar ${1:subscriptionId;}",
|
||||||
|
"\tvar ${2:passwordForSp;}",
|
||||||
|
"\tvar ${3:displayName;}",
|
||||||
|
"",
|
||||||
|
"\tvar homePage = 'http://' + displayName + ':8080';",
|
||||||
|
"\tvar identifierUris = [ homePage ];",
|
||||||
|
"\tvar roleId = 'b24988ac-6180-42a0-ab88-20f7382dd24c'; // contributor role",
|
||||||
|
"\tvar scope = '/subscriptions/' + subscriptionId;",
|
||||||
|
"\tvar roleDefinitionId = scope + '/providers/Microsoft.Authorization/roleDefinitions/' + roleId;",
|
||||||
|
"\tvar loginOptions = {",
|
||||||
|
"\t\tdomain: tenantId",
|
||||||
|
"\t};",
|
||||||
|
"",
|
||||||
|
"\tmsrestazure.interactiveLogin(loginOptions, function(err, creds) {",
|
||||||
|
"\t\tif (err) {",
|
||||||
|
"\t\t\tconsole.log('Error occured in interactive login: \\n' + util.inspect(err, { depth: null }));",
|
||||||
|
"\t\t\treturn;",
|
||||||
|
"\t\t}",
|
||||||
|
"",
|
||||||
|
// more variable declarations @2-indent
|
||||||
|
"\t\tvar options = {",
|
||||||
|
"\t\t\tdomain: tenantId,",
|
||||||
|
"\t\t\ttokenAudience: 'graph',",
|
||||||
|
"\t\t\tusername: creds.username,",
|
||||||
|
"\t\t\ttokenCache: creds.tokenCache,",
|
||||||
|
"\t\t\tenvironment: creds.environment",
|
||||||
|
"\t\t};",
|
||||||
|
"\t\tvar credsForGraph = new msrestazure.DeviceTokenCredentials(options);",
|
||||||
|
"\t\tvar graphClient = new graph(credsForGraph, tenantId);",
|
||||||
|
"\t\tvar startDate = new Date(Date.now());",
|
||||||
|
"\t\tvar endDate = new Date(startDate.toISOString());",
|
||||||
|
"\t\tvar m = moment(endDate);",
|
||||||
|
"\t\tm.add(1, 'years');",
|
||||||
|
"\t\tendDate = new Date(m.toISOString());",
|
||||||
|
"\t\tvar applicationCreateParameters = {",
|
||||||
|
"\t\t\tavailableToOtherTenants: false,",
|
||||||
|
"\t\t\tdisplayName: displayName,",
|
||||||
|
"\t\t\thomePage: homePage,",
|
||||||
|
"\t\t\tidentifierUris: identifierUris,",
|
||||||
|
"\t\t\tpasswordCredentials: [{",
|
||||||
|
"\t\t\t\tstartDate: startDate,",
|
||||||
|
"\t\t\t\tendDate: endDate,",
|
||||||
|
"\t\t\t\tkeyId: msrestazure.generateUuid(),",
|
||||||
|
"\t\t\t\tvalue: passwordForSp",
|
||||||
|
"\t\t\t}]",
|
||||||
|
"\t\t};",
|
||||||
|
"",
|
||||||
|
// 1. Create AD application @2-indent
|
||||||
|
"\t\tgraphClient.applications.create(applicationCreateParameters, function (err, application, req, res) {",
|
||||||
|
"\t\t\tif (err) {",
|
||||||
|
"\t\t\t\tconsole.log('Error occured while creating the application: \\n' + util.inspect(err, { depth: null }));",
|
||||||
|
"\t\t\t\treturn;",
|
||||||
|
"\t\t\t}",
|
||||||
|
"",
|
||||||
|
// 2. Create SP @3-indent
|
||||||
|
"\t\t\tvar servicePrincipalCreateParameters = {",
|
||||||
|
"\t\t\t\tappId: application.appId,",
|
||||||
|
"\t\t\t\taccountEnabled: true",
|
||||||
|
"\t\t\t};",
|
||||||
|
"",
|
||||||
|
"\t\t\tgraphClient.servicePrincipals.create(servicePrincipalCreateParameters, function (err, sp, req, res) {",
|
||||||
|
"\t\t\t\tif (err) {",
|
||||||
|
"\t\t\t\t\tconsole.log('Error occured while creating the servicePrincipal: \\n' + util.inspect(err, { depth: null }));",
|
||||||
|
"\t\t\t\t\treturn;",
|
||||||
|
"\t\t\t\t}",
|
||||||
|
"",
|
||||||
|
// 3. SP role assignment @4-indent
|
||||||
|
"\t\t\t\tvar authorizationClient = new authorization(creds, subscriptionId, null);",
|
||||||
|
"\t\t\t\tvar assignmentGuid = msrestazure.generateUuid();",
|
||||||
|
"\t\t\t\tvar roleCreateParams = {",
|
||||||
|
"\t\t\t\t\tproperties: {",
|
||||||
|
"\t\t\t\t\t\tprincipalId: sp.objectId,",
|
||||||
|
"\t\t\t\t\t\troleDefinitionId: roleDefinitionId,",
|
||||||
|
"\t\t\t\t\t\tscope: scope",
|
||||||
|
"\t\t\t\t\t}",
|
||||||
|
"\t\t\t\t};",
|
||||||
|
"",
|
||||||
|
"\t\t\t\tauthorizationClient.roleAssignments.create(scope, assignmentGuid, roleCreateParams, function (err, roleAssignment, req, res) {",
|
||||||
|
"\t\t\t\t\tif (err) {",
|
||||||
|
"\t\t\t\t\t\tconsole.log('\\nError occured while creating the roleAssignment: \\n' + util.inspect(err, { depth: null }));",
|
||||||
|
"\t\t\t\t\t\treturn;",
|
||||||
|
"\t\t\t\t\t}",
|
||||||
|
"",
|
||||||
|
// Print SP information to console
|
||||||
|
"\t\t\t\t\tconsole.log('>>>>>>>>>>>\\nSuccessfully created the role assignment for the servicePrincipal.\\n');",
|
||||||
|
"\t\t\t\t\tconsole.log('>>>>>>>>>>>\\nIn future for login you will need the following info:');",
|
||||||
|
"\t\t\t\t\tconsole.log('ServicePrincipal Id (SPN): ' + sp.appId);",
|
||||||
|
"\t\t\t\t\tconsole.log('ServicePincipal Password: ' + passwordForSp);",
|
||||||
|
"\t\t\t\t\tconsole.log('Tenant Id for ServicePrincipal: ' + tenantId);",
|
||||||
|
"\t\t\t\t\tconsole.log('>>>>>>>>>>>\\n');",
|
||||||
|
"\t\t\t\t});", // close sp role assignment
|
||||||
|
"\t\t\t});", // close sp create
|
||||||
|
"\t\t});", // close AD application create
|
||||||
|
"\t});", // close interactive login
|
||||||
|
"}" // close function createServicePrincipal
|
||||||
|
],
|
||||||
|
"description": "Create a service principal"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
let exec = require('child_process').exec;
|
||||||
|
|
||||||
|
// checks if there exists a valid installation of NodeJs on this machine
|
||||||
|
exports.isNodeInstalled = function isNodeInstalled() {
|
||||||
|
var cmdString = "node -v";
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
exec(cmdString, (error, stdout) => {
|
||||||
|
if (error) {
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
if (stdout.startsWith('v')) {
|
||||||
|
return resolve(true);
|
||||||
|
}
|
||||||
|
return resolve(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// lists all globally installed npm packages.
|
||||||
|
exports.npmList = function npmList(path) {
|
||||||
|
var global = false;
|
||||||
|
if (!path) global = true;
|
||||||
|
var cmdString = "npm ls --depth=0 " + (global ? "-g " : " ");
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
exec(cmdString, { cwd: path ? path : "/" }, (error, stdout) => {
|
||||||
|
if (error && !stdout) {
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
var packages = [];
|
||||||
|
packages = stdout.split('\n');
|
||||||
|
|
||||||
|
packages = packages.filter(function (item) {
|
||||||
|
if (item.match(/^\+--.+/g) != null || item.match(/^├──.+/g) != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (item.match(/^`--.+/g) != null || item.match(/^└──.+/g) != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
packages = packages.map(function (item) {
|
||||||
|
// windows
|
||||||
|
if (item.match(/^\+--.+/g) != null) {
|
||||||
|
return item.replace(/^\+--\s/g, "");
|
||||||
|
}
|
||||||
|
if (item.match(/^`--.+/g) != null) {
|
||||||
|
return item.replace(/^`--\s/g, "");
|
||||||
|
}
|
||||||
|
// mac
|
||||||
|
if (item.match(/^├──.+/g) != null) {
|
||||||
|
return item.replace(/^├──\s/g, "");
|
||||||
|
}
|
||||||
|
if (item.match(/^└──.+/g) != null) {
|
||||||
|
return item.replace(/^└──\s/g, "");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
resolve(packages);
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// install given list of npm packages to the global location.
|
||||||
|
exports.npmInstall = function npmInstall(packages, opts) {
|
||||||
|
if (packages.length == 0 || !packages || !packages.length) { Promise.reject("No packages found"); }
|
||||||
|
if (typeof packages == "string") packages = [packages];
|
||||||
|
if (!opts) opts = {};
|
||||||
|
var cmdString = "npm install " + packages.join(" ") + " "
|
||||||
|
+ (opts.global ? " -g" : "")
|
||||||
|
+ (opts.save ? " --save" : "")
|
||||||
|
+ (opts.saveDev ? " --saveDev" : "");
|
||||||
|
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
exec(cmdString, { cwd: opts.cwd ? opts.cwd : "/" }, (error) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
} else {
|
||||||
|
resolve(true); // return success.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,24 @@
|
||||||
|
/* global suite, test */
|
||||||
|
|
||||||
|
//
|
||||||
|
// Note: This example test is leveraging the Mocha test framework.
|
||||||
|
// Please refer to their documentation on https://mochajs.org/ for help.
|
||||||
|
//
|
||||||
|
|
||||||
|
// The module 'assert' provides assertion methods from node
|
||||||
|
var assert = require('assert');
|
||||||
|
|
||||||
|
// You can import and use all API from the 'vscode' module
|
||||||
|
// as well as import your extension to test it
|
||||||
|
var vscode = require('vscode');
|
||||||
|
var myExtension = require('../extension');
|
||||||
|
|
||||||
|
// Defines a Mocha test suite to group tests of similar kind together
|
||||||
|
suite("Extension Tests", function() {
|
||||||
|
|
||||||
|
// Defines a Mocha unit test
|
||||||
|
test("Something 1", function() {
|
||||||
|
assert.equal(-1, [1, 2, 3].indexOf(5));
|
||||||
|
assert.equal(-1, [1, 2, 3].indexOf(0));
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,22 @@
|
||||||
|
//
|
||||||
|
// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING
|
||||||
|
//
|
||||||
|
// This file is providing the test runner to use when running extension tests.
|
||||||
|
// By default the test runner in use is Mocha based.
|
||||||
|
//
|
||||||
|
// You can provide your own test runner if you want to override it by exporting
|
||||||
|
// a function run(testRoot: string, clb: (error:Error) => void) that the extension
|
||||||
|
// host can call to run the tests. The test runner is expected to use console.log
|
||||||
|
// to report the results back to the caller. When the tests are finished, return
|
||||||
|
// a possible error to the callback or null if none.
|
||||||
|
|
||||||
|
var testRunner = require('vscode/lib/testrunner');
|
||||||
|
|
||||||
|
// You can directly control Mocha options by uncommenting the following lines
|
||||||
|
// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info
|
||||||
|
testRunner.configure({
|
||||||
|
ui: 'tdd', // the TDD UI is being used in extension.test.js (suite, test, etc.)
|
||||||
|
useColors: true // colored output from test results
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = testRunner;
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Welcome to your first VS Code Extension
|
||||||
|
|
||||||
|
## What's in the folder
|
||||||
|
* This folder contains all of the files necessary for your extension
|
||||||
|
* `package.json` - this is the manifest file in which you declare your extension and command.
|
||||||
|
The sample plugin registers a command and defines its title and command name. With this information
|
||||||
|
VS Code can show the command in the command palette. It doesn’t yet need to load the plugin.
|
||||||
|
* `extension.js` - this is the main file where you will provide the implementation of your command.
|
||||||
|
The file exports one function, `activate`, which is called the very first time your extension is
|
||||||
|
activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`.
|
||||||
|
We pass the function containing the implementation of the command as the second parameter to
|
||||||
|
`registerCommand`.
|
||||||
|
|
||||||
|
## Get up and running straight away
|
||||||
|
* press `F5` to open a new window with your extension loaded
|
||||||
|
* run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`
|
||||||
|
* set breakpoints in your code inside extension.ts to debug your extension
|
||||||
|
* find output from your extension in the debug console
|
||||||
|
|
||||||
|
## Make changes
|
||||||
|
* you can relaunch the extension from the debug toolbar after changing code in `extension.js`
|
||||||
|
* you can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes
|
||||||
|
|
||||||
|
## Explore the API
|
||||||
|
* you can open the full set of our API when you open the file `node_modules/vscode/vscode.d.ts`
|
||||||
|
|
||||||
|
## Run tests
|
||||||
|
* open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Launch Tests`
|
||||||
|
* press `F5` to run the tests in a new window with your extension loaded
|
||||||
|
* see the output of the test result in the debug console
|
||||||
|
* make changes to `test/extension.test.js` or create new test files inside the `test` folder
|
||||||
|
* by convention, the test runner will only consider files matching the name pattern `**.test.js`
|
||||||
|
* you can create folders inside the `test` folder to structure your tests any way you want
|
Загрузка…
Ссылка в новой задаче