UNPKG

ember-template-lint

Version:
463 lines (396 loc) 12.7 kB
#!/usr/bin/env node 'use strict'; // Use V8's code cache to speed up instantiation time: require('v8-compile-cache'); // eslint-disable-line import/no-unassigned-import const fs = require('fs'); const path = require('path'); const { promisify } = require('util'); const { getTodoStorageDirPath, getTodoConfig, validateConfig, } = require('@ember-template-lint/todo-utils'); const chalk = require('chalk'); const getStdin = require('get-stdin'); const globby = require('globby'); const isGlob = require('is-glob'); const micromatch = require('micromatch'); const Linter = require('../lib'); const processResults = require('../lib/helpers/process-results'); const readFile = promisify(fs.readFile); const STDIN = '/dev/stdin'; const NOOP_CONSOLE = { log: () => {}, warn: () => {}, error: () => {}, }; function removeExt(filePath) { return filePath.slice(0, -path.extname(filePath).length); } async function buildLinterOptions(workingDir, filePath, filename = '', isReadingStdin) { if (isReadingStdin) { let filePath = filename; let moduleId = removeExt(filePath); let source = await getStdin(); return { source, filePath, moduleId }; } else { let moduleId = removeExt(filePath); let resolvedFilePath = path.resolve(workingDir, filePath); let source = await readFile(resolvedFilePath, { encoding: 'utf8' }); return { source, filePath, moduleId }; } } function executeGlobby(workingDir, pattern, ignore) { let supportedExtensions = new Set(['.hbs', '.handlebars']); // `--no-ignore-pattern` results in `ignorePattern === [false]` let options = ignore[0] === false ? { cwd: workingDir } : { cwd: workingDir, gitignore: true, ignore }; return globby .sync(pattern, options) .filter((filePath) => supportedExtensions.has(path.extname(filePath))); } function isFile(possibleFile) { try { let stat = fs.statSync(possibleFile); return stat.isFile(); } catch { return false; } } function expandFileGlobs(workingDir, filePatterns, ignorePattern, glob = executeGlobby) { let result = new Set(); for (const pattern of filePatterns) { let isLiteralPath = !isGlob(pattern) && isFile(path.resolve(workingDir, pattern)); if (isLiteralPath) { let isIgnored = micromatch.isMatch(pattern, ignorePattern); if (!isIgnored) { result.add(pattern); } continue; } for (const filePath of glob(workingDir, pattern, ignorePattern)) { result.add(filePath); } } return result; } function getFilesToLint(workingDir, filePatterns, ignorePattern = []) { let files; if (filePatterns.length === 0 || filePatterns.includes('-') || filePatterns.includes(STDIN)) { files = new Set([STDIN]); } else { files = expandFileGlobs(workingDir, filePatterns, ignorePattern); } return files; } function parseArgv(_argv) { let parser = require('yargs') .scriptName('ember-template-lint') .usage('$0 [options] [files..]') .options({ 'config-path': { describe: 'Define a custom config path', default: '.template-lintrc.js', type: 'string', }, config: { describe: 'Define a custom configuration to be used - (e.g. \'{ "rules": { "no-implicit-this": "error" } }\') ', type: 'string', }, quiet: { describe: 'Ignore warnings and only show errors', boolean: true, }, rule: { describe: 'Specify a rule and its severity to add that rule to loaded rules - (e.g. `no-implicit-this:error` or `rule:["error", { "allow": ["some-helper"] }]`)', type: 'string', }, filename: { describe: 'Used to indicate the filename to be assumed for contents from STDIN', type: 'string', }, fix: { describe: 'Fix any errors that are reported as fixable', boolean: true, default: false, }, format: { describe: 'Specify format to be used in printing output', type: 'string', default: 'pretty', }, json: { describe: 'Format output as json', deprecated: 'Use --format=json instead', boolean: true, }, 'output-file': { describe: 'Specify file to write report to', type: 'string', implies: 'format', }, verbose: { describe: 'Output errors with source description', boolean: true, }, 'working-directory': { alias: 'cwd', describe: 'Path to a directory that should be considered as the current working directory.', type: 'string', // defaulting to `.` here to refer to `process.cwd()`, setting the default to `process.cwd()` itself // would make our snapshots unstable (and make the help output unaligned since most directory paths // are fairly deep) default: '.', }, 'no-config-path': { describe: 'Does not use the local template-lintrc, will use a blank template-lintrc instead', boolean: true, }, 'print-pending': { describe: 'Print list of formatted rules for use with `pending` in config file (deprecated)', boolean: true, hidden: true, }, 'update-todo': { describe: 'Update list of linting todos by transforming lint errors to todos', default: false, boolean: true, }, 'include-todo': { describe: 'Include todos in the results', default: false, boolean: true, }, 'clean-todo': { describe: 'Remove expired and invalid todo files', default: false, boolean: true, }, 'todo-days-to-warn': { describe: 'Number of days after its creation date that a todo transitions into a warning', type: 'number', }, 'todo-days-to-error': { describe: 'Number of days after its creation date that a todo transitions into an error', type: 'number', }, 'ignore-pattern': { describe: 'Specify custom ignore pattern (can be disabled with --no-ignore-pattern)', type: 'array', default: ['**/dist/**', '**/tmp/**', '**/node_modules/**'], }, 'no-inline-config': { describe: 'Prevent inline configuration comments from changing config or rules', boolean: true, }, 'max-warnings': { describe: 'Number of warnings to trigger nonzero exit code', type: 'number', }, }) .help() .version(); parser.parserConfiguration({ 'greedy-arrays': false, }); if (_argv.length === 0) { parser.showHelp(); parser.exit(1); } else { let options = parser.parse(_argv); if (options.workingDirectory === '.') { options.workingDirectory = process.cwd(); } return options; } } const PENDING_RULES = new Set(['invalid-pending-module', 'invalid-pending-module-rule']); function printPending(results, options) { let pendingList = []; for (let filePath in results.files) { let fileResults = results.files[filePath]; let failingRules = fileResults.messages.reduce((memo, error) => { if (!PENDING_RULES.has(error.rule)) { memo.add(error.rule); } return memo; }, new Set()); if (failingRules.size > 0) { pendingList.push({ moduleId: removeExt(filePath), only: [...failingRules] }); } } let pendingListString = JSON.stringify(pendingList, null, 2); if (options.json) { console.log(pendingListString); } else { console.log(chalk.yellow('WARNING: Print pending is deprecated. Use --update-todo instead.\n')); console.log( 'Add the following to your `.template-lintrc.js` file to mark these files as pending.\n\n' ); console.log(`pending: ${pendingListString}`); } } function getTodoConfigFromCommandLineOptions(options) { let todoConfig = {}; if (Number.isInteger(options.todoDaysToWarn)) { todoConfig.warn = options.todoDaysToWarn || undefined; } if (Number.isInteger(options.todoDaysToError)) { todoConfig.error = options.todoDaysToError || undefined; } return todoConfig; } function _isOverridingConfig(options) { return Boolean( options.config || options.rule || options.inlineConfig === false || options.configPath !== '.template-lintrc.js' ); } async function run() { let options = parseArgv(process.argv.slice(2)); let positional = options._; let config; let isOverridingConfig = _isOverridingConfig(options); if (options.config) { try { config = JSON.parse(options.config); } catch { console.error('Could not parse specified `--config` as JSON'); process.exitCode = 1; return; } } if (options['no-config-path'] !== undefined) { options.configPath = false; } let todoConfigResult = validateConfig(options.workingDirectory); if (!todoConfigResult.isValid) { console.error(todoConfigResult.message); process.exitCode = 1; return; } let linter; let todoInfo = { added: 0, removed: 0, todoConfig: getTodoConfig( options.workingDirectory, 'ember-template-lint', getTodoConfigFromCommandLineOptions(options) ), }; try { linter = new Linter({ workingDir: options.workingDirectory, configPath: options.configPath, config, rule: options.rule, allowInlineConfig: !options.noInlineConfig, console: options.quiet || options.json ? NOOP_CONSOLE : console, }); } catch (error) { console.error(error.message); process.exitCode = 1; return; } if ( linter.config.pending.length > 0 && fs.existsSync(getTodoStorageDirPath(options.workingDirectory)) ) { console.error( 'Cannot use the `pending` config option in conjunction with lint todos. Please run with `--update-pending` to migrate to the new todos functionality.' ); process.exitCode = 1; return; } if (linter.config.pending.length > 0 && options.updateTodo) { console.error( 'Cannot use the `pending` config option in conjunction with `--update-todo`. Please remove the `pending` option from your config and re-run the command.' ); process.exitCode = 1; return; } if ((options.todoDaysToWarn || options.todoDaysToError) && !options.updateTodo) { console.error( 'Using `--todo-days-to-warn` or `--todo-days-to-error` is only valid when the `--update-todo` option is being used.' ); process.exitCode = 1; return; } let filePaths = getFilesToLint(options.workingDirectory, positional, options.ignorePattern); let resultsAccumulator = []; for (let relativeFilePath of filePaths) { let linterOptions = await buildLinterOptions( options.workingDirectory, relativeFilePath, options.filename, filePaths.has(STDIN) ); let fileResults; if (options.fix) { let { isFixed, output, messages } = await linter.verifyAndFix(linterOptions); if (isFixed) { fs.writeFileSync(linterOptions.filePath, output, { encoding: 'utf-8' }); } fileResults = messages; } else { fileResults = await linter.verify(linterOptions); } if (options.updateTodo) { let { addedCount, removedCount } = linter.updateTodo( linterOptions, fileResults, todoInfo.todoConfig, isOverridingConfig ); todoInfo.added += addedCount; todoInfo.removed += removedCount; } if (!filePaths.has(STDIN)) { fileResults = linter.processTodos( linterOptions, fileResults, todoInfo.todoConfig, options.fix || options.cleanTodo, isOverridingConfig ); } resultsAccumulator.push(...fileResults); } let results = processResults(resultsAccumulator); if ( results.errorCount > 0 || (!options.quiet && options.maxWarnings && results.warningCount > options.maxWarnings) ) { process.exitCode = 1; } if (options.printPending) { return printPending(results, options); } else { let hasErrors = results.errorCount > 0; let hasWarnings = results.warningCount > 0; let hasTodos = options.includeTodo && results.todoCount; let hasUpdatedTodos = options.updateTodo; let Printer = require('../lib/formatters/default'); let printer = new Printer({ ...options, hasResultData: hasErrors || hasWarnings || hasTodos || hasUpdatedTodos, }); printer.print(results, todoInfo); } } // exports are for easier unit testing module.exports = { _parseArgv: parseArgv, _expandFileGlobs: expandFileGlobs, _getFilesToLint: getFilesToLint, }; if (require.main === module) { run(); }