UNPKG

ember-template-lint

Version:
317 lines (282 loc) 9.44 kB
// Helpers for the CLI binary. import ci from 'ci-info'; import { globbySync } from 'globby'; import isGlob from 'is-glob'; import micromatch from 'micromatch'; import { findUpSync } from 'find-up'; import fs from 'node:fs'; import path from 'node:path'; import yargs from 'yargs'; import { parseFilePath, SOON_DEPRECATED, isDTS } from '../-private/file-path.js'; import camelize from './camelize.js'; const STDIN = '/dev/stdin'; class NoMatchingFilesError extends Error { constructor(...params) { super(...params); this.name = 'NoMatchingFilesError'; } } function shouldIgnoreFileDueToConfig(filePath, config) { return config['ignore']?.some((match) => match(filePath)) ?? false; } function getVersion() { const closestPackage = findUpSync('package.json'); // we can't use relative URL resolution because of bundling const json = JSON.parse(fs.readFileSync(closestPackage)); return json.version; } export function parseArgv(_argv) { const specifiedOptions = { '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', }, 'print-full-path': { describe: 'Prints full file paths in error messages', boolean: true, }, 'output-file': { describe: 'Specify file to write report to', type: 'string', }, 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, }, '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: !ci.isCI, boolean: true, }, 'compact-todo': { describe: 'Compacts the .lint-todo storage file, removing extraneous todos', 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, }, 'print-config': { describe: 'Print the configuration for the given file', default: false, boolean: true, }, 'max-warnings': { describe: 'Number of warnings to trigger nonzero exit code', type: 'number', }, 'no-error-on-unmatched-pattern': { describe: 'Prevent errors when pattern is unmatched', boolean: true, }, 'report-unused-disable-directives': { describe: 'Report unused disable directives', boolean: true, }, }; let parser = yargs() .scriptName('ember-template-lint') .usage('$0 [options] [files..]') .options(specifiedOptions) .help() .version('version', getVersion()); parser.parserConfiguration({ 'greedy-arrays': false, }); if (_argv.length === 0) { parser.showHelp(); parser.exit(1); } else { let options = parser.parse(_argv); // TODO: Eventually use yargs strict() or strictOptions() to disallow unknown options (blocked by some inconsistencies in how we tell yargs about our options). const possibleOptionNames = getPossibleOptionNames(specifiedOptions); for (const optionName of Object.keys(options)) { if ( !['$0', '_'].includes(optionName) && // Built-in yargs options. !possibleOptionNames.includes(optionName) ) { console.error(`Unknown option: --${optionName}`); parser.exit(1); return; } } if (options.workingDirectory === '.') { options.workingDirectory = process.cwd(); } return options; } } export function expandFileGlobs( workingDir, filePatterns, ignorePattern, config, glob = executeGlobby, errorOnUnmatchedPattern = true ) { let result = new Set(); for (const pattern of filePatterns) { let isLiteralPath = !isGlob(pattern) && isFile(path.resolve(workingDir, pattern)); if (isLiteralPath) { // If `--no-ignore-pattern` is passed, the ignorePatter is `[false]`. let isIgnored = !(ignorePattern?.length === 1 && ignorePattern[0] === false) && micromatch.isMatch(pattern, ignorePattern); if (!isIgnored && !shouldIgnoreFileDueToConfig(pattern, config)) { result.add(pattern); } continue; } const globResults = glob(workingDir, pattern, ignorePattern); if (errorOnUnmatchedPattern && (!globResults || globResults.length === 0)) { throw new NoMatchingFilesError(`No files matching the pattern were found: "${pattern}"`); } for (const filePath of globResults) { if (!shouldIgnoreFileDueToConfig(filePath, config)) { result.add(filePath); } } } return result; } export function filePatternsShouldIncludeSTDIN(filePatterns) { return filePatterns.length === 0 || filePatterns.includes('-') || filePatterns.includes(STDIN); } export function getFilesToLint( workingDir, filePatterns, ignorePattern, errorOnUnmatchedPattern, config, _console ) { let files; if (filePatternsShouldIncludeSTDIN(filePatterns)) { files = new Set([STDIN]); } else { files = expandFileGlobs( workingDir, filePatterns, ignorePattern, config, executeGlobby, errorOnUnmatchedPattern ); } const counts = new Map(); for (const file of files) { const { ext } = parseFilePath(file); const count = counts.get(ext) || 0; counts.set(ext, count + 1); } let fileCounts = ''; for (const [ext, count] of counts.entries()) { fileCounts += `\t${ext}: ${count}\n`; } _console.log(`Linting ${files.size} Total Files with TemplateLint\n${fileCounts}`); return files; } /** * @param {Object} specifiedOptions - options passed to yargs (option names should be in dasherized format) * @returns {String[]} a list of all possible CLI option names */ export function getPossibleOptionNames(specifiedOptions) { const optionAliases = Object.values(specifiedOptions) .map((option) => option.alias) .filter((option) => option !== undefined); const dasherizedOptionNames = [...Object.keys(specifiedOptions), ...optionAliases]; const camelizedOptionNames = dasherizedOptionNames.map((name) => camelize(name)); const negatedDasherizedOptionNames = dasherizedOptionNames.map((name) => name.startsWith('no-') ? name.slice(3) : `no-${name}` ); const negatedCamelizedOptionNames = negatedDasherizedOptionNames.map((name) => camelize(name)); return [ ...dasherizedOptionNames, ...camelizedOptionNames, // Since yargs `boolean-negation` option is enabled (by default), assume any option can be passed with `no`/negated prefix. ...negatedDasherizedOptionNames, ...negatedCamelizedOptionNames, ]; } function executeGlobby(workingDir, pattern, ignore) { let supportedExtensions = new Set(['.gjs', '.gts', '.handlebars', '.hbs', ...SOON_DEPRECATED]); // `--no-ignore-pattern` results in `ignorePattern === [false]` let options = ignore[0] === false ? { cwd: workingDir } : { cwd: workingDir, gitignore: true, ignore }; return globbySync(pattern, options).filter((filePath) => { const { shortExt, ext } = parseFilePath(filePath); return supportedExtensions.has(shortExt) && !isDTS(ext); }); } function isFile(possibleFile) { try { let stat = fs.statSync(possibleFile); return stat.isFile(); } catch { return false; } }