UNPKG

salesforce-lightning-cli

Version:

Lightning CLI Heroku Plugin

293 lines (258 loc) 10.3 kB
/* * 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. */ "use strict"; 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; } }