Converted checkCopyright and checkImports to Gulp Plugins
This commit is contained in:
Родитель
f3b1a3f899
Коммит
9adec70d2a
31
gulpfile.js
31
gulpfile.js
|
@ -10,6 +10,9 @@ var path = require('path');
|
|||
var runSequence = require("run-sequence");
|
||||
var ts = require('gulp-typescript');
|
||||
var mocha = require('gulp-mocha');
|
||||
var GulpExtras = require("./tools/gulp-extras");
|
||||
var copyright = GulpExtras.checkCopyright;
|
||||
var imports = GulpExtras.checkImports;
|
||||
|
||||
var srcPath = 'src';
|
||||
var outPath = 'out';
|
||||
|
@ -22,10 +25,12 @@ var sources = [
|
|||
// TODO: The file property should point to the generated source (this implementation adds an extra folder to the path)
|
||||
// We should also make sure that we always generate urls in all the path properties (We shouldn't have \\s. This seems to
|
||||
// be an issue on Windows platforms)
|
||||
gulp.task('build', ['checkImports', 'checkCopyright'], function () {
|
||||
gulp.task('build', function () {
|
||||
var tsProject = ts.createProject('tsconfig.json');
|
||||
return tsProject.src()
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(copyright())
|
||||
.pipe(imports())
|
||||
.pipe(ts(tsProject))
|
||||
.pipe(sourcemaps.write('.', {
|
||||
includeContent: false,
|
||||
|
@ -72,28 +77,16 @@ function test() {
|
|||
gulp.task('build-test', ['build'], test);
|
||||
gulp.task('test', test);
|
||||
|
||||
function runCustomVerification(pathInTools, errorMessage, cb) {
|
||||
var checkProcess = child_process.fork(path.join(__dirname, "tools", pathInTools),
|
||||
{
|
||||
cwd: path.resolve(__dirname, "src"),
|
||||
stdio: "inherit"
|
||||
});
|
||||
checkProcess.on("error", cb);
|
||||
checkProcess.on("exit", function (code, signal) {
|
||||
if (code || signal) {
|
||||
cb(new Error(errorMessage));
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
gulp.task('checkImports', function (cb) {
|
||||
runCustomVerification("checkCasing.js", "Mismatches found in import casing", cb);
|
||||
var tsProject = ts.createProject('tsconfig.json');
|
||||
return tsProject.src()
|
||||
.pipe(imports());
|
||||
});
|
||||
|
||||
gulp.task('checkCopyright', function (cb) {
|
||||
runCustomVerification("checkCopyright.js", "Some source code files don't have the expected copyright notice", cb);
|
||||
var tsProject = ts.createProject('tsconfig.json');
|
||||
return tsProject.src()
|
||||
.pipe(copyright());
|
||||
});
|
||||
|
||||
gulp.task('watch-build-test', ['build', 'build-test'], function () {
|
||||
|
|
302
package.json
302
package.json
|
@ -1,155 +1,155 @@
|
|||
{
|
||||
"name": "vscode-react-native",
|
||||
"displayName": "React Native Tools",
|
||||
"version": "0.1.1",
|
||||
"private": true,
|
||||
"publisher": "vsmobile",
|
||||
"icon": "images/icon.svg",
|
||||
"galleryBanner": {
|
||||
"color": "#3B3738",
|
||||
"theme": "dark"
|
||||
},
|
||||
"description": "Code-hinting, debugging and integrated commands for React Native",
|
||||
"bugs": "https://github.com/Microsoft/vscode-react-native/issues",
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Microsoft/vscode-react-native"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^0.10.1"
|
||||
},
|
||||
"categories": [
|
||||
"Debuggers",
|
||||
"Other"
|
||||
"name": "vscode-react-native",
|
||||
"displayName": "React Native Tools",
|
||||
"version": "0.1.1",
|
||||
"private": true,
|
||||
"publisher": "vsmobile",
|
||||
"icon": "images/icon.svg",
|
||||
"galleryBanner": {
|
||||
"color": "#3B3738",
|
||||
"theme": "dark"
|
||||
},
|
||||
"description": "Code-hinting, debugging and integrated commands for React Native",
|
||||
"bugs": "https://github.com/Microsoft/vscode-react-native/issues",
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Microsoft/vscode-react-native"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^0.10.1"
|
||||
},
|
||||
"categories": [
|
||||
"Debuggers",
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [
|
||||
"workspaceContains:package.json"
|
||||
],
|
||||
"main": "./out/extension/rn-extension",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "reactNative.runAndroid",
|
||||
"title": "React Native: Run Android"
|
||||
},
|
||||
{
|
||||
"command": "reactNative.runIos",
|
||||
"title": "React Native: Run iOS"
|
||||
},
|
||||
{
|
||||
"command": "reactNative.startPackager",
|
||||
"title": "React Native: Start Packager"
|
||||
},
|
||||
{
|
||||
"command": "reactNative.stopPackager",
|
||||
"title": "React Native: Stop Packager"
|
||||
}
|
||||
],
|
||||
"activationEvents": [
|
||||
"workspaceContains:package.json"
|
||||
],
|
||||
"main": "./out/extension/rn-extension",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "reactNative.runAndroid",
|
||||
"title": "React Native: Run Android"
|
||||
},
|
||||
{
|
||||
"command": "reactNative.runIos",
|
||||
"title": "React Native: Run iOS"
|
||||
},
|
||||
{
|
||||
"command": "reactNative.startPackager",
|
||||
"title": "React Native: Start Packager"
|
||||
},
|
||||
{
|
||||
"command": "reactNative.stopPackager",
|
||||
"title": "React Native: Stop Packager"
|
||||
}
|
||||
"debuggers": [
|
||||
{
|
||||
"type": "reactnative",
|
||||
"label": "React Native",
|
||||
"program": "./out/debugger/nodeDebugWrapper.js",
|
||||
"runtime": "node",
|
||||
"enableBrekapointsFor": {
|
||||
"languageIds": [
|
||||
"javascript",
|
||||
"typescript",
|
||||
"javascriptreact",
|
||||
"typescriptreact"
|
||||
]
|
||||
},
|
||||
"initialConfigurations": [
|
||||
{
|
||||
"name": "Debug Android",
|
||||
"program": "${workspaceRoot}/.vscode/launchReactNative.js",
|
||||
"type": "reactnative",
|
||||
"request": "launch",
|
||||
"platform": "android",
|
||||
"internalDebuggerPort": 9090,
|
||||
"sourceMaps": true,
|
||||
"outDir": "${workspaceRoot}/.vscode/.react"
|
||||
},
|
||||
{
|
||||
"name": "Debug iOS",
|
||||
"program": "${workspaceRoot}/.vscode/launchReactNative.js",
|
||||
"type": "reactnative",
|
||||
"request": "launch",
|
||||
"platform": "ios",
|
||||
"target": "iPhone 5s",
|
||||
"internalDebuggerPort": 9090,
|
||||
"sourceMaps": true,
|
||||
"outDir": "${workspaceRoot}/.vscode/.react"
|
||||
}
|
||||
],
|
||||
"debuggers": [
|
||||
{
|
||||
"type": "reactnative",
|
||||
"label": "React Native",
|
||||
"program": "./out/debugger/nodeDebugWrapper.js",
|
||||
"runtime": "node",
|
||||
"enableBrekapointsFor": {
|
||||
"languageIds": [
|
||||
"javascript",
|
||||
"typescript",
|
||||
"javascriptreact",
|
||||
"typescriptreact"
|
||||
]
|
||||
},
|
||||
"initialConfigurations": [
|
||||
{
|
||||
"name": "Debug Android",
|
||||
"program": "${workspaceRoot}/.vscode/launchReactNative.js",
|
||||
"type": "reactnative",
|
||||
"request": "launch",
|
||||
"platform": "android",
|
||||
"internalDebuggerPort": 9090,
|
||||
"sourceMaps": true,
|
||||
"outDir": "${workspaceRoot}/.vscode/.react"
|
||||
},
|
||||
{
|
||||
"name": "Debug iOS",
|
||||
"program": "${workspaceRoot}/.vscode/launchReactNative.js",
|
||||
"type": "reactnative",
|
||||
"request": "launch",
|
||||
"platform": "ios",
|
||||
"target": "iPhone 5s",
|
||||
"internalDebuggerPort": 9090,
|
||||
"sourceMaps": true,
|
||||
"outDir": "${workspaceRoot}/.vscode/.react"
|
||||
}
|
||||
],
|
||||
"configurationAttributes": {
|
||||
"launch": {
|
||||
"required": [
|
||||
"platform",
|
||||
"program"
|
||||
],
|
||||
"properties": {
|
||||
"platform": {
|
||||
"type": "string",
|
||||
"description": "The platform ('ios' or 'android') to target"
|
||||
},
|
||||
"program": {
|
||||
"type": "string",
|
||||
"description": "The path to launchReactNative.js in the vscode folder"
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "'simulator', 'device', or the name of the emulator to run on"
|
||||
},
|
||||
"internalDebuggerPort": {
|
||||
"type": "number",
|
||||
"description": "A port to be used to enable automatic reloading of breakpoints when sourcemaps change.",
|
||||
"default": 9090
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"configurationAttributes": {
|
||||
"launch": {
|
||||
"required": [
|
||||
"platform",
|
||||
"program"
|
||||
],
|
||||
"properties": {
|
||||
"platform": {
|
||||
"type": "string",
|
||||
"description": "The platform ('ios' or 'android') to target"
|
||||
},
|
||||
"program": {
|
||||
"type": "string",
|
||||
"description": "The path to launchReactNative.js in the vscode folder"
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "'simulator', 'device', or the name of the emulator to run on"
|
||||
},
|
||||
"internalDebuggerPort": {
|
||||
"type": "number",
|
||||
"description": "A port to be used to enable automatic reloading of breakpoints when sourcemaps change.",
|
||||
"default": 9090
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node node_modules/react-native/local-cli/cli.js start",
|
||||
"compile": "node ./node_modules/vscode/bin/compile -watch -p ./",
|
||||
"vscode:prepublish": "gulp",
|
||||
"test": "node ./node_modules/vscode/bin/test"
|
||||
},
|
||||
"extensionDependencies": [
|
||||
"andreweinand.node-debug"
|
||||
],
|
||||
"dependencies": {
|
||||
"applicationinsights": "0.15.8",
|
||||
"extract-opts": "2.2.0",
|
||||
"getmac": "1.0.7",
|
||||
"options": "0.0.6",
|
||||
"q": "1.4.1",
|
||||
"semver": "5.1.0",
|
||||
"typechecker": "2.0.8",
|
||||
"ultron": "1.0.2",
|
||||
"winreg": "0.0.16",
|
||||
"ws": "1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"del": "2.2.0",
|
||||
"gulp": "^3.9.0",
|
||||
"gulp-mocha": "^2.2.0",
|
||||
"gulp-sourcemaps": "^1.5.2",
|
||||
"gulp-tslint": "^3.3.1",
|
||||
"gulp-typescript": "^2.8.0",
|
||||
"gulp-util": "^3.0.5",
|
||||
"mocha-teamcity-reporter": "^1.0.0",
|
||||
"node-find-files": "0.0.4",
|
||||
"run-sequence": "^1.1.5",
|
||||
"sinon": "^1.17.3",
|
||||
"should": "^8.2.2",
|
||||
"tslint": "^2.5.1",
|
||||
"typescript": "^1.7.5",
|
||||
"vsce": "1.0.0",
|
||||
"vscode": "^0.10.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node node_modules/react-native/local-cli/cli.js start",
|
||||
"compile": "node ./node_modules/vscode/bin/compile -watch -p ./",
|
||||
"vscode:prepublish": "gulp",
|
||||
"test": "node ./node_modules/vscode/bin/test"
|
||||
},
|
||||
"extensionDependencies": [
|
||||
"andreweinand.node-debug"
|
||||
],
|
||||
"dependencies": {
|
||||
"applicationinsights": "0.15.8",
|
||||
"extract-opts": "2.2.0",
|
||||
"getmac": "1.0.7",
|
||||
"options": "0.0.6",
|
||||
"q": "1.4.1",
|
||||
"semver": "5.1.0",
|
||||
"typechecker": "2.0.8",
|
||||
"ultron": "1.0.2",
|
||||
"winreg": "0.0.16",
|
||||
"ws": "1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"del": "2.2.0",
|
||||
"gulp": "^3.9.0",
|
||||
"gulp-mocha": "^2.2.0",
|
||||
"gulp-sourcemaps": "^1.5.2",
|
||||
"gulp-tslint": "^3.3.1",
|
||||
"gulp-typescript": "^2.8.0",
|
||||
"gulp-util": "^3.0.5",
|
||||
"mocha-teamcity-reporter": "^1.0.0",
|
||||
"run-sequence": "^1.1.5",
|
||||
"should": "^8.2.2",
|
||||
"sinon": "^1.17.3",
|
||||
"through2": "^2.0.1",
|
||||
"tslint": "^2.5.1",
|
||||
"typescript": "^1.7.5",
|
||||
"vsce": "1.0.0",
|
||||
"vscode": "^0.10.7"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for details.
|
||||
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
var os = require("os");
|
||||
var child_process = require("child_process");
|
||||
|
||||
// Recursively find all instances of 'import [...] from "[...]";'
|
||||
if (os.platform() === "win32") {
|
||||
child_process.exec("findstr /sir /C:\"import.*from\" *.ts", parseOutput);
|
||||
} else {
|
||||
child_process.exec("grep -Ri 'import.*from' .", parseOutput);
|
||||
}
|
||||
|
||||
function parseOutput(err, out, stderr) {
|
||||
// Extract out the filename containing the match,
|
||||
// and the relative path of the file it is searching for
|
||||
var regex = /(\s|^)([^\n:]*):.*from ["'](\.[^"']*)["'];/g;
|
||||
var imports = [];
|
||||
out.replace(regex, function (all, _, file, from) {
|
||||
imports.push({ path: file, relative: from });
|
||||
});
|
||||
checkImports(imports);
|
||||
}
|
||||
|
||||
function checkImports(imports) {
|
||||
// Check to see if the import references a source file
|
||||
// with an exact match of a name, and complain otherwise
|
||||
imports.map(function (i) {
|
||||
i.resolved = path.resolve(path.dirname(i.path), i.relative + ".ts");
|
||||
return i;
|
||||
}).filter(function (i) {
|
||||
var dirpath = path.dirname(i.resolved);
|
||||
try {
|
||||
var entries = fs.readdirSync(dirpath);
|
||||
return entries.indexOf(path.basename(i.resolved)) === -1;
|
||||
} catch (exception) {
|
||||
console.log("Missing folder for import in " + i.path + ": " + dirpath);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}).forEach(function (i) {
|
||||
console.log("Missing file for import in " + i.path + ": " + i.relative);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for details.
|
||||
"use strict";
|
||||
/// <reference path="../../node_modules/vscode/typings/node.d.ts" />
|
||||
/// <reference path="../../src/typings/q/q.d.ts" />
|
||||
var FindFiles = require("node-find-files");
|
||||
var fs = require("fs");
|
||||
var Q = require("q");
|
||||
var path_module = require("path");
|
||||
var CopyrightVerifier = (function () {
|
||||
function CopyrightVerifier() {
|
||||
this.foundMissing = false;
|
||||
}
|
||||
CopyrightVerifier.prototype.findIn = function (path) {
|
||||
var defer = Q.defer();
|
||||
var foundFiles = [];
|
||||
var finder = new FindFiles({
|
||||
rootFolder: path,
|
||||
filterFunction: function (path, stat) {
|
||||
// match only filename
|
||||
return path.match(CopyrightVerifier.SOURCE_CODE_FILE_PATTERN) && !path.match(CopyrightVerifier.EXCLUDED_FILE_PATTERN);
|
||||
}
|
||||
});
|
||||
finder.on("match", function (filePath, fileStats) {
|
||||
var contents = fs.readFileSync(filePath).toString().replace(/\r\n/g, "\n");
|
||||
if (contents.indexOf(CopyrightVerifier.UTB_BYTE_ORDER_MARKER) === 0) {
|
||||
contents = contents.substr(1);
|
||||
}
|
||||
if (!(contents.indexOf(CopyrightVerifier.COPYRIGHT_NOTICE) === 0)) {
|
||||
foundFiles.push(filePath);
|
||||
}
|
||||
});
|
||||
finder.on("complete", function () {
|
||||
return defer.resolve(foundFiles);
|
||||
});
|
||||
finder.on("patherror", function (err, strPath) {
|
||||
defer.reject("Error for Path " + strPath + " " + err + "\nStackTrace: " + err.stack);
|
||||
});
|
||||
finder.on("error", function (err) {
|
||||
defer.reject("Global Error " + err);
|
||||
});
|
||||
finder.startSearch();
|
||||
return defer.promise;
|
||||
};
|
||||
CopyrightVerifier.prototype.findInAll = function (paths) {
|
||||
var _this = this;
|
||||
return Q.all(paths.map(function (path) { return _this.findIn(path_module.resolve(path)); })).then(function (invalids) {
|
||||
return [].concat.apply([], invalids);
|
||||
});
|
||||
};
|
||||
CopyrightVerifier.prototype.verify = function () {
|
||||
var paths = [];
|
||||
for (var _i = 0; _i < arguments.length; _i++) {
|
||||
paths[_i - 0] = arguments[_i];
|
||||
}
|
||||
return this.findInAll(paths).done(function (filePaths) {
|
||||
if (filePaths.length !== 0) {
|
||||
process.stderr.write("Found files which don't match the expected copyright notice:\n");
|
||||
filePaths.forEach(function (filePath) { return process.stderr.write("\t" + filePath + "\n"); });
|
||||
process.exit(1);
|
||||
}
|
||||
else {
|
||||
process.exit(0);
|
||||
}
|
||||
}, function (reason) {
|
||||
process.stderr.write("Uknown error while trying to check files copyright: " + reason);
|
||||
process.exit(1);
|
||||
});
|
||||
};
|
||||
CopyrightVerifier.UTB_BYTE_ORDER_MARKER = "\ufeff";
|
||||
CopyrightVerifier.SOURCE_CODE_FILE_PATTERN = /.*\.(ts|js)$/;
|
||||
CopyrightVerifier.EXCLUDED_FILE_PATTERN = /.*\.d\.ts/;
|
||||
CopyrightVerifier.COPYRIGHT_NOTICE = "// Copyright (c) Microsoft Corporation. All rights reserved.\n" +
|
||||
"// Licensed under the MIT license. See LICENSE file in the project root for details.\n";
|
||||
return CopyrightVerifier;
|
||||
}());
|
||||
new CopyrightVerifier().verify("../src", "../tools");
|
||||
|
||||
//# sourceMappingURL=checkCopyright.js.map
|
|
@ -0,0 +1,101 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for details.
|
||||
"use strict";
|
||||
|
||||
var fs = require("fs");
|
||||
var gutil = require("gulp-util");
|
||||
var path = require("path");
|
||||
var through = require("through2");
|
||||
|
||||
/**
|
||||
* Pretty logger using gutil.log
|
||||
* @param {string} pluginName Name of the pluginName
|
||||
* @param {Object} file A gulp file to report on
|
||||
* @param {string} message The error message to display
|
||||
*/
|
||||
var logError = function(pluginName, file, message) {
|
||||
var sourcePath = path.relative(__dirname, file.path).replace("../","");
|
||||
gutil.log(`[${gutil.colors.cyan(pluginName)}] ${gutil.colors.red("error")} ${sourcePath}: ${message}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to return a list of matches for a given pattern
|
||||
* @param {string|RegExp} pattern The pattern to match against
|
||||
* @param {string} text The text to search
|
||||
* @returns {string[]} An array of matches
|
||||
*/
|
||||
var find = function(pattern, text) {
|
||||
var results = [];
|
||||
text.toString().replace(pattern, function(match) {
|
||||
results.push(match);
|
||||
});
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
/**
|
||||
* Plugin to verify the Microsoft copyright notice is present
|
||||
*/
|
||||
var checkCopyright = function() {
|
||||
var re = /\/\/ Copyright \(c\) Microsoft Corporation. All rights reserved.\s+\/\/ Licensed under the MIT license. See LICENSE file in the project root for details.\s+/
|
||||
|
||||
return through.obj(function(file, encoding, callback) {
|
||||
if (file.isBuffer() && !file.path.endsWith(".d.ts")) {
|
||||
var fileContents = file.contents.toString(encoding);
|
||||
var matches = re.exec(fileContents);
|
||||
|
||||
if (!matches) {
|
||||
logError("check-copyright", file, "missing copyright notice");
|
||||
}
|
||||
}
|
||||
|
||||
callback(null, file);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to check if a file exists case sensitive
|
||||
* @param {string} filePath The path to check
|
||||
* @returns {boolean} If the path exists case sensitive
|
||||
*/
|
||||
var existsCaseSensitive = function(filePath) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
var fileName = path.basename(filePath);
|
||||
return fs.readdirSync(path.dirname(filePath)).indexOf(fileName) !== -1;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Plugin to verify if import statements use correct casing
|
||||
*/
|
||||
var checkImports = function() {
|
||||
var re = /(?:\s|^)(?:[^\n:]*).*from ["'](\.[^"']*)["'];/g;
|
||||
|
||||
return through.obj(function(file, encoding, callback) {
|
||||
if (file.isBuffer() && !file.path.endsWith(".d.ts")) {
|
||||
var fileContents = file.contents.toString(encoding);
|
||||
var importStatements = find(re, fileContents);
|
||||
var workingDirectory = path.dirname(file.path);
|
||||
|
||||
importStatements.forEach(function(importStatement) {
|
||||
var modulePath = re.exec(importStatement);
|
||||
if (modulePath && modulePath.length === 2) {
|
||||
var moduleFilePath = path.resolve(workingDirectory, modulePath[1] + ".ts");
|
||||
|
||||
if (!existsCaseSensitive(moduleFilePath)) {
|
||||
logError("check-imports", file, `unresolved import: ${modulePath[1]}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
callback(null, file);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
checkCopyright: checkCopyright,
|
||||
checkImports: checkImports
|
||||
}
|
Загрузка…
Ссылка в новой задаче