ember-template-lint
Version:
Linter for Ember or Handlebars templates.
301 lines (253 loc) • 8.22 kB
JavaScript
;
// 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 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: () => {},
};
async function buildLinterOptions(workingDir, filePath, filename = '', isReadingStdin) {
if (isReadingStdin) {
let filePath = filename;
let moduleId = filePath.slice(0, -4);
let source = await getStdin();
return { source, filePath, moduleId };
} else {
let moduleId = filePath.slice(0, -4);
let resolvedFilePath = path.resolve(workingDir, filePath);
let source = await readFile(resolvedFilePath, { encoding: 'utf8' });
return { source, filePath, moduleId };
}
}
function lintSource(linter, options, shouldFix) {
if (shouldFix) {
let { isFixed, output, messages } = linter.verifyAndFix(options);
if (isFixed) {
fs.writeFileSync(options.filePath, output);
}
return messages;
} else {
return linter.verify(options);
}
}
function executeGlobby(workingDir, pattern, ignore) {
// `--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) => filePath.slice(-4) === '.hbs');
}
function expandFileGlobs(workingDir, filePatterns, ignorePattern, glob = executeGlobby) {
let result = new Set();
for (const pattern of filePatterns) {
let isHBS = pattern.slice(-4) === '.hbs';
let isLiteralPath = !isGlob(pattern) && fs.existsSync(path.resolve(workingDir, pattern));
if (isHBS && 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,
},
json: {
describe: 'Format output as json',
boolean: true,
},
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',
boolean: true,
},
'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,
},
})
.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: filePath.slice(0, -4), only: [...failingRules] });
}
}
let pendingListString = JSON.stringify(pendingList, null, 2);
if (options.json) {
console.log(pendingListString);
} else {
console.log(
'Add the following to your `.template-lintrc.js` file to mark these files as pending.\n\n'
);
console.log(`pending: ${pendingListString}`);
}
}
async function run() {
let options = parseArgv(process.argv.slice(2));
let positional = options._;
let config;
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 linter;
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;
}
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 messages = lintSource(linter, linterOptions, options.fix);
resultsAccumulator.push(...messages);
}
let results = processResults(resultsAccumulator);
if (results.errorCount > 0) {
process.exitCode = 1;
}
if (options.printPending) {
return printPending(results, options);
} else {
if (results.errorCount || results.warningCount) {
let Printer = require('../lib/printers/default');
let printer = new Printer(options);
printer.print(results);
}
}
}
// exports are for easier unit testing
module.exports = {
_parseArgv: parseArgv,
_expandFileGlobs: expandFileGlobs,
_getFilesToLint: getFilesToLint,
};
if (require.main === module) {
run();
}