salesforce-lightning-cli
Version:
Lightning CLI Heroku Plugin
293 lines (258 loc) • 10.3 kB
JavaScript
/*
* Copyright (C) 2016 salesforce.com, inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
;
var glob = require('glob');
var path = require('path');
var fs = require('fs');
var linter = require('eslint').linter;
var defaultConfig = require('./config');
var defaultStyle = require('./code-style-rules');
var objectAssign = require('object-assign');
var formatter = require('eslint-friendly-formatter');
var { SfdxError } = require("@salesforce/core");
var SINGLETON_FILE_REGEXP = /(Controller|Renderer|Helper|Provider|Test|Model)\.js$/;
/**
* @callback LoggingFn
* @param {string} message The message to log.
* @returns {void}
*/
/**
* @callback LoggingErrorFn
* @param {string} message The message to log.
* @returns {never} The SDFX error logger terminates after being called, so you can only call this once.
*/
/**
* this computes the first position after all the comments (multi-line and single-line)
* from the top of the code
* @param {string} code The source of the file being processed.
*/
function afterCommentsPosition(code) {
var position = 0;
var match;
do {
// /\*.*?\*/
match = code.match(/^(\s*(\/\*([\s\S]*?)\*\/)?\s*)/);
if (!match || !match[1]) {
match = code.match(/^(\s*\/\/.*\s*)/);
}
if (match && match[1]) {
position += match[1].length;
code = code.slice(match[1].length);
}
} while (match);
return position;
}
/**
* @param {string} code The source of the file being processed.
*/
function processSingletonCode(code) {
// transform `({...})` into `"use strict"; exports = ({...});`
var pos = afterCommentsPosition(code);
if (code.charAt(pos) === '(') {
code = code.slice(0, pos) + '"use strict"; exports = ' + code.slice(pos);
pos = code.lastIndexOf(')') + 1;
if (code.charAt(pos) !== ';') {
code = code.slice(0, pos) + ';' + code.slice(pos);
}
}
return code;
}
function processFunctionCode(code) {
// transform `function () {}` into `"use strict"; exports = function () {};`
var pos = afterCommentsPosition(code);
if (code.indexOf('function', pos) === pos) {
code = code.slice(0, pos) + '"use strict"; exports = ' + code.slice(pos);
pos = code.lastIndexOf('}') + 1;
if (code.charAt(pos) !== ';') {
code = code.slice(0, pos) + ';' + code.slice(pos);
}
}
return code;
}
function processFile(src, config, options) {
var messages = linter.verify(src, config, {
allowInlineConfig: true, // TODO: internal code should be linted with this set to false
quiet: true
});
if (!options.verbose) {
messages = messages.filter(function (msg) {
return msg.severity > 1;
});
}
// eslint docs say that linter.verify will return null if no errors are found,
// but it actually returns an empty array. Also it is possible
// that the filter above will return an empty array.
//
// In either case an empty result should return null,
// not an empty array
if(messages.length > 0){
return messages;
} else {
return null
}
}
/**
* Generates the human readable error message for the whole program.
* @param {Array} programLintResults The lint results.
*/
function generateErrorMessage(programLintResults) {
const errorArr = [];
programLintResults.forEach(fileLintResult => {
errorArr.push('FILE: ' + fileLintResult.file + ':');
errorArr.push(formatter([{
messages: fileLintResult.result,
filePath: fileLintResult.file
}]));
});
return "Linting issue(s)\n" + errorArr.join('\n');
}
function handleCleanJson() {
// JSON mode with no errors prints nothing because `{status:0}` is automatically logged by SfdxCommand.
return;
}
function handleErrorJson(errorLogger, lintResults) {
// Any output is a lint error (or warning).
// Stringify these messages for the SfdxError.
const error = new SfdxError("Linting issue(s)", "LintError");
error.data = JSON.stringify(lintResults);
errorLogger(error);
return;
}
function handleCleanReadable(log) {
// Inform of a successful lint when otherwise there is nothing to output.
log('No lint errors.');
}
function handleErrorReadable(errorLogger, lintResults) {
// Any output is a lint error (or warning).
errorLogger(generateErrorMessage(lintResults));
}
/**
* @param cwd {string} The working directory.
* @param opts {Object.<string, any>} The command line flags object.
* @param logger {Object} A set of logging functions.
* @param logger.trace {LoggingFn} A way to log a trace message.
* @param logger.debug {LoggingFn} A way to log a debug message.
* @param logger.log {LoggingFn} A way to log an info message.
* @param logger.logJson {LoggingFn} A way to log a JSON info message.
* @param logger.warn {LoggingFn} A way to log a warning message.
* @param logger.error {LoggingErrorFn} A way to log an error message.
* Can only be called once because the SFDX error logger will terminate after a single call.
* When we port to TypeScript, this function typedef should return never so the compiler understands code after the call is dead.
* For now, make sure you return after every error call.
* @param logger.errorJson {LoggingErrorFn} A way to log an error message.
* Can only be called once because the SFDX error logger will terminate after a single call.
* When we port to TypeScript, this function typedef should return never so the compiler understands code after the call is dead.
* For now, make sure you return after every error call.
* @returns A array of { file, result } objects for every file with an error. The array is empty if there were no errors.
*/
module.exports = function (cwd, opts, logger) {
var ignore = [
// these are the things that we know for sure we never want to inspect
'**/node_modules/**',
'**/jsdoc/**',
'**/htdocs/**',
'**/invalidTest/**',
'**/purposelyInvalid/**',
'**/invalidTestData/**',
'**/validationTest/**',
'**/lintTest/**',
'**/target/**',
'**/parseError/**',
'**/*.junk.js',
'**/*_mock?.js'
].concat(opts.ignore ? [opts.ignore] : []);
var globOptions = {
silent: true,
cwd: cwd,
nodir: true,
realpath: true,
ignore: ignore
};
var patterns = [opts.files || '**/*.js'];
var config = {};
objectAssign(config, defaultConfig);
config.rules = objectAssign({}, defaultConfig.rules);
if (opts.config) {
logger.log('Applying custom rules from ' + opts.config);
var customStyle = require(path.join(cwd, opts.config));
if (customStyle && customStyle.rules) {
Object.keys(customStyle.rules).forEach(function (name) {
if (Object.hasOwnProperty.call(defaultStyle.rules, name)) {
config.rules[name] = customStyle.rules[name];
logger.debug(' -> Rule: ' + name + ' is now set to ' + JSON.stringify(config.rules[name]));
} else {
logger.debug(' -> Ignoring non-style rule: ' + name);
}
});
}
}
logger.debug('Search for "' + patterns.join('" or "') + '" in folder "' + cwd + '"');
logger.debug(' -> Ignoring: ' + ignore.join(','));
var files = [];
// for blt-like structures we look for aura structures
// and we search from there to narrow down the search.
var folders = glob.sync('**/*.{app,cmp,lib}', globOptions);
folders = folders.map(function (v) {
return path.dirname(v);
});
folders = folders.filter(function (v, i) {
return folders.indexOf(v) === i;
});
folders.forEach(function (folder) {
globOptions.cwd = folder;
patterns.forEach(function (pattern) {
files = files.concat(glob.sync(pattern, globOptions));
});
});
// deduping...
files = files.filter(function (v, i) {
return files.indexOf(v) === i;
});
var lintResults = [];
// Terminate early if no files were found. Warn because it's likely not the desired effect.
if (files.length === 0) {
logger.warn('Did not find matching files.');
return;
}
logger.log('Found ' + files.length + ' matching files.\n----------------');
files.forEach(function (file) {
var source = fs.readFileSync(file, 'utf8');
// in some cases, we need to massage the source before linting it
if (SINGLETON_FILE_REGEXP.test(file)) {
source = processSingletonCode(source);
}
else {
source = processFunctionCode(source);
}
var singleFileResults = processFile(source, config, opts);
if (singleFileResults) {
lintResults.push({
file: file,
result: singleFileResults
});
}
});
// Figure out what we should output.
// Trading efficiency for being explicitly complete.
const isCleanJson = opts.json && lintResults.length === 0;
const isErrorJson = opts.json && lintResults.length > 0;
const isCleanReadable = !opts.json && lintResults.length === 0;
const isErrorReadable = !opts.json && lintResults.length > 0;
if (isCleanJson) { handleCleanJson(); return; }
if (isErrorJson) { handleErrorJson(logger.errorJson, lintResults); return; }
if (isCleanReadable) { handleCleanReadable(logger.log); return; }
if (isErrorReadable) { handleErrorReadable(logger.error, lintResults); return; }
}