markuplint
Version:
An HTML linter for all markup developers
180 lines (179 loc) • 7.38 kB
JavaScript
import { promises as fs } from 'node:fs';
import path from 'node:path';
import { resolveFiles } from '@markuplint/file-resolver';
import { ViolationCollector } from '@markuplint/ml-core';
import { MLEngine } from '../api/index.js';
import { log } from '../debug.js';
import { output } from './output.js';
/**
* Executes the markuplint linting command against the given files.
*
* Resolves file targets, creates an {@link MLEngine} for each file, collects
* violations, and outputs results in the requested format. When the `--fix`
* flag is set, overwrites files with their auto-fixed content.
*
* @param files - The list of file targets (paths or inline source code) to lint.
* @param options - CLI options controlling output format, fix mode, locale, and other behaviors.
* @param apiOptions - Optional overrides for the underlying API (e.g., custom rules or config).
* @returns `true` if any errors were found (or warnings exceeded the limit), `false` otherwise.
*/
export async function command(files, options, apiOptions) {
const fix = options.fix;
const configFile = options.config &&
(path.isAbsolute(options.config) ? options.config : path.resolve(process.cwd(), options.config));
const locale = options.locale;
const searchConfig = options.searchConfig;
const ignoreExt = options.ignoreExt;
const importPresetRules = options.importPresetRules;
const verbose = options.verbose;
const ignoreGlob = options.includeNodeModules ? undefined : 'node_modules/**';
const fileList = await resolveFiles(files, ignoreGlob);
if (fileList.length === 0 && !options.allowEmptyInput) {
process.stderr.write('Markuplint: No target files.\n');
// Error
return true;
}
if (log.enabled) {
log('File list: %O', fileList.map(f => f.path));
log('Config: %s', configFile ?? 'N/A');
log('Fix option: %s', fix);
}
const format = options.format?.toLowerCase().trim();
let hasError = false;
let totalWarningCount = 0;
const collector = new ViolationCollector(options.maxCount);
const processedFiles = [];
const skippedFiles = [];
const filesContent = new Map();
const severityParseError = options.severityParseError.toLowerCase();
const severity = {
parseError: ['error', 'warning', 'off'].includes(severityParseError)
? severityParseError
: true,
};
for (const file of fileList) {
// Check if collector is already locked (max-count reached)
if (collector.isLocked()) {
log('Skipping file due to max-count limit: %s', file.path);
skippedFiles.push(file.path);
continue;
}
const engine = new MLEngine(file, {
configFile,
fix,
locale,
noSearchConfig: !searchConfig,
ignoreExt,
importPresetRules,
debug: verbose,
severity,
...apiOptions,
});
if (options.showConfig != null) {
const isDetails = options.showConfig === 'details';
const configSet = await engine.resolveConfig(false);
let data;
if (isDetails) {
const files = [...configSet.files].toReversed();
const [configurationFile, ...dependencies] = files;
data = {
target: file.path,
computedConfig: configSet.config,
configurationFile: configurationFile,
dependencies,
plugins: configSet.plugins,
errors: configSet.errs,
};
}
else {
data = configSet.config;
}
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
return false;
}
const result = await engine.exec();
if (!result) {
continue;
}
// Progressive出力が有効でJSON形式でない場合
if (options.progressiveOutput && format !== 'json') {
// 即座に出力
output({
violations: result.violations,
filePath: result.filePath,
sourceCode: result.sourceCode,
fixedCode: result.fixedCode,
status: 'processed',
}, options);
}
else {
// 従来の動作:メモリに蓄積
processedFiles.push(result.filePath);
filesContent.set(result.filePath, {
sourceCode: result.sourceCode,
fixedCode: result.fixedCode,
});
}
// Add violations to collector
collector.pushWithFile(result.filePath, ...result.violations);
const errorCount = result.violations.filter(v => v.severity === 'error').length;
const warningCount = result.violations.filter(v => v.severity === 'warning').length;
// Track total warning count across all files
totalWarningCount += warningCount;
if (!hasError && (errorCount > 0 || (warningCount > 0 && !options.allowWarnings))) {
hasError = true;
}
if (fix) {
log('Overwrite file: %s', result.filePath);
await fs.writeFile(result.filePath, result.fixedCode, { encoding: 'utf8' });
}
}
// Output results
if (format === 'json') {
const jsonOutput = collector.toArray();
process.stdout.write(JSON.stringify(jsonOutput, null, 2) + '\n');
return false;
}
// Progressive出力が無効の場合のみループ後に出力
if (!options.progressiveOutput) {
// For standard/simple/github output, group violations by file
const violationsByFile = collector.groupByFile();
// Output per file - include processed files without violations
for (const filePath of processedFiles) {
const violations = violationsByFile.get(filePath) || [];
const content = filesContent.get(filePath) || { sourceCode: '', fixedCode: '' };
if (violations.length === 0 && !options.problemOnly) {
log('Output reports');
output({
violations,
filePath,
sourceCode: content.sourceCode,
fixedCode: content.fixedCode,
status: 'processed',
}, options);
}
else if (violations.length > 0) {
log('Output reports');
output({
violations,
filePath,
sourceCode: content.sourceCode,
fixedCode: content.fixedCode,
status: 'processed',
}, options);
}
}
// Output skipped files
if (!options.problemOnly) {
for (const filePath of skippedFiles) {
log('Output skipped file report');
output({ violations: [], filePath, sourceCode: '', fixedCode: '', status: 'skipped' }, options);
}
}
}
// Check if max-warnings limit is exceeded (ESLint compatible)
if (!hasError && options.maxWarnings >= 0 && totalWarningCount > options.maxWarnings) {
hasError = true;
}
return hasError;
}