vite-plugin-linter
Version:
Plugin for linting files with Vite
458 lines (448 loc) • 14.1 kB
JavaScript
;
const pluginutils = require('@rollup/pluginutils');
const linterPluginBuild = require('./shared/vite-plugin-linter.Dk---30a.cjs');
const chokidar = require('chokidar');
const fs = require('fs');
const path = require('path');
const lintWorkerThread = require('./lintWorkerThread.cjs');
const eslint = require('eslint');
const ts = require('typescript');
require('url');
require('vite');
require('worker_threads');
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
const fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
const path__default = /*#__PURE__*/_interopDefaultCompat(path);
const ts__default = /*#__PURE__*/_interopDefaultCompat(ts);
const servePluginName = "vite-plugin-linter-serve";
const clientEventName = "eslint-warn";
const clientJs = `
if (import.meta.hot) {
fetch("/eslint.json")
.then(r => r.json())
.then(response => {
if (response) {
for (let line of response) {
console.warn(line);
}
}
})
.catch(e => console.error(e));
import.meta.hot.on("${clientEventName}", d => console.warn(d));
}
`;
function linterPluginServe(options = {}, fileFilter) {
let devServer = null;
const includeMode = options.serve?.includeMode ?? "processedFiles";
let injectedFile = null;
let lintFiles = [];
let processingTimeout;
let workersByLinterName = {};
let dataByFileNameByLinterName = {};
for (const linter of options.linters) {
dataByFileNameByLinterName[linter.name] = {};
}
async function getFormattedOutput(linter) {
const dataByFileName = dataByFileNameByLinterName[linter.name];
const allData = [];
for (const file of Object.keys(dataByFileName)) {
allData.push(dataByFileName[file]);
}
if (allData.length > 0) {
return await linter.format(allData);
}
return "";
}
async function onWorkerMessage(message, pluginContext) {
const dataByFileName = dataByFileNameByLinterName[message.linterName];
for (const file of message.files) {
if (file in message.result.serve) {
dataByFileName[file] = message.result.serve[file];
} else if (file in dataByFileName) {
delete dataByFileName[file];
}
}
const linter = options.linters.find((l) => l.name === message.linterName);
const output = await getFormattedOutput(linter);
if (output) {
pluginContext.warn(output);
if (devServer) {
devServer.ws.send({
event: clientEventName,
data: output,
type: "custom"
});
}
}
}
async function processFiles() {
const files = [...lintFiles];
if (includeMode !== "filesInFolder") {
lintFiles = [];
}
for (const linter of options.linters) {
workersByLinterName[linter.name].postMessage(files);
}
}
function watchDirectory(directory) {
function onChange(fsPath) {
const normalizedPath = linterPluginBuild.normalizePath(fsPath);
let changed = false;
if (fileFilter(fsPath)) {
if (includeMode === "filesInFolder" && !lintFiles.includes(normalizedPath)) {
lintFiles.push(normalizedPath);
}
changed = true;
} else if (fs__default.existsSync(fsPath) && fs__default.lstatSync(fsPath).isDirectory()) {
const children = linterPluginBuild.readAllFiles(fsPath, fileFilter).map(
(f) => linterPluginBuild.normalizePath(f)
);
if (includeMode === "filesInFolder") {
for (const child of children) {
if (!lintFiles.includes(child)) {
lintFiles.push(child);
changed = true;
}
}
for (let index = lintFiles.length - 1; index >= 0; index--) {
const file = lintFiles[index];
if (file.startsWith(normalizedPath) && !children.includes(file)) {
lintFiles.splice(index, 1);
changed = true;
}
}
}
for (const linter of options.linters) {
const dataByFileName = dataByFileNameByLinterName[linter.name];
for (const file of Object.keys(dataByFileName)) {
if (file.startsWith(normalizedPath) && !children.includes(file)) {
delete dataByFileName[file];
}
}
}
}
return changed;
}
let watchTimeout;
let paths = [];
function onEvent(fsPath) {
clearTimeout(watchTimeout);
if (!paths.includes(fsPath)) {
paths.push(fsPath);
}
watchTimeout = setTimeout(() => {
let changed = false;
for (const path2 of paths) {
if (onChange(path2)) {
changed = true;
}
}
if (includeMode === "filesInFolder" && changed) {
processFiles();
}
paths = [];
}, 100);
}
if (process.platform === "linux") {
chokidar.watch(directory, {
ignored: /node_modules/,
ignoreInitial: true,
persistent: false
}).on("all", (event, fsPath) => {
switch (event) {
case "add":
onEvent(fsPath);
break;
case "unlink":
const parentDirPath = path__default.resolve(fsPath, "..");
onEvent(parentDirPath);
break;
}
});
} else {
fs__default.watch(
directory,
{ persistent: false, recursive: true },
(event, fileName) => {
if (fileName) {
onEvent(path__default.join(directory, fileName));
}
}
);
}
}
return {
apply: "serve",
name: servePluginName,
buildStart() {
workersByLinterName = lintWorkerThread.createWorkerThreads(
"serve",
servePluginName,
options.linters
);
for (const linterName of Object.keys(workersByLinterName)) {
const worker = workersByLinterName[linterName];
worker.on("message", (message) => onWorkerMessage(message, this));
}
const currentDirectory = process.cwd();
watchDirectory(currentDirectory);
if (includeMode === "filesInFolder") {
lintFiles = linterPluginBuild.readAllFiles(currentDirectory, fileFilter).map(
(f) => linterPluginBuild.normalizePath(f)
);
setTimeout(() => processFiles());
}
},
configureServer(server) {
devServer = server;
devServer.middlewares.use(async (req, res, next) => {
if (req.url === "/eslint.json") {
const outputs = [];
for (const linter of options.linters) {
const output = await getFormattedOutput(linter);
if (output) {
outputs.push(output);
}
}
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Content-Type", "application/json");
res.write(JSON.stringify(outputs), "utf-8");
res.end();
} else {
next();
}
});
},
getLinter(name) {
return options.linters.find((l) => l.name === name);
},
load(id) {
const file = linterPluginBuild.normalizePath(id);
try {
if (options.injectFile) {
if (file === linterPluginBuild.normalizePath(options.injectFile)) {
const content = fs__default.readFileSync(id);
return content + clientJs;
}
} else if (injectedFile === null && !file.startsWith("node_modules/") && fs__default.existsSync(id) || file === injectedFile) {
const content = fs__default.readFileSync(id);
injectedFile = file;
return content + clientJs;
}
} catch (ex) {
console.warn(`Could not open file ${id}`, ex);
}
return null;
},
async transform(code, id) {
if (!fileFilter(id) || includeMode === "filesInFolder") {
return null;
}
const file = linterPluginBuild.normalizePath(id);
if (fs__default.existsSync(file)) {
lintFiles.push(file);
}
const pluginContext = this;
clearTimeout(processingTimeout);
processingTimeout = setTimeout(
() => processFiles().catch((ex) => pluginContext.error(ex)),
1e3
);
return null;
}
};
}
function linterPlugin(options = {}) {
const fileFilter = pluginutils.createFilter(
options.include,
options.exclude ?? /node_modules/
);
const plugins = [];
if (!options.serve?.disable) {
plugins.push(linterPluginServe(options, fileFilter));
}
if (!options.build?.disable || global.vitePluginLinter?.mode === "lintCommand") {
plugins.push(linterPluginBuild.linterPluginBuild(options, fileFilter));
}
return plugins;
}
const defaultBuildOptions = {
cache: false,
fix: false
};
const defaultServeOptions = {
cache: true,
cacheLocation: "./node_modules/.cache/.eslintcache",
fix: false
};
class EsLinter {
name = "EsLinter";
eslint = null;
formatter = null;
options;
constructor(options) {
if (options?.configEnv.command === "build") {
this.options = { ...defaultBuildOptions, ...options.buildOptions };
} else {
this.options = { ...defaultServeOptions, ...options?.serveOptions };
}
if (this.options.clearCacheOnStart) {
const cachePath = this.options.cacheLocation ?? ".eslintcache";
if (fs__default.existsSync(cachePath)) {
fs__default.unlinkSync(cachePath);
}
}
}
async format(results) {
if (!this.eslint) {
await this.loadLinter();
}
if (!this.formatter) {
await this.loadFormatter();
}
return this.formatter.format(results);
}
async lintBuild(files) {
return await this.lint(files);
}
async lintServe(files, output) {
const reports = await this.lint(files);
const result = {};
for (const report of reports) {
if (report.errorCount > 0 || report.warningCount > 0) {
result[linterPluginBuild.normalizePath(report.filePath)] = report;
}
}
output(result);
}
async lint(files) {
if (!this.eslint) {
await this.loadLinter();
}
const lintFiles = [];
for (const file of files) {
if (!await this.eslint.isPathIgnored(file)) {
lintFiles.push(file);
}
}
const reports = await this.eslint.lintFiles(lintFiles);
if (this.options.fix && reports) {
eslint.ESLint.outputFixes(reports);
}
return reports;
}
async loadLinter() {
const { clearCacheOnStart, formatter, ...esLintOptions } = this.options;
const esLint = await eslint.loadESLint();
this.eslint = new esLint(esLintOptions);
}
async loadFormatter() {
switch (typeof this.options.formatter) {
case "string":
this.formatter = await this.eslint.loadFormatter(
this.options.formatter
);
break;
case "function":
this.formatter = this.options.formatter;
break;
default:
this.formatter = await this.eslint.loadFormatter("stylish");
}
}
}
const defaultOptions = {
configFilePath: "tsconfig.json",
noEmit: true
};
class TypeScriptLinter {
name = "TypeScriptLinter";
formatHost = {
getCanonicalFileName: (f) => f,
getCurrentDirectory: process.cwd,
getNewLine: () => "\n"
};
options;
optionsLoadedFromFile = false;
watchingFiles = [];
watcher = null;
constructor(options) {
this.options = { ...defaultOptions, ...options };
}
async format(results) {
return ts__default.formatDiagnosticsWithColorAndContext(results, this.formatHost);
}
async lintBuild(files) {
if (!this.optionsLoadedFromFile) {
this.loadOptions();
}
const allFiles = files.concat(this.getCustomTypeRootFiles());
const program = ts__default.createProgram(allFiles, this.options);
return ts__default.getPreEmitDiagnostics(program);
}
lintServe(files, output) {
if (!this.optionsLoadedFromFile) {
this.loadOptions();
this.watchingFiles = this.watchingFiles.concat(
this.getCustomTypeRootFiles()
);
}
if (files.some((f) => !this.watchingFiles.includes(f))) {
this.watchingFiles = this.watchingFiles.concat(files).filter(linterPluginBuild.onlyUnique);
if (this.watcher) {
this.watcher.close();
}
const host = ts__default.createWatchCompilerHost(
this.watchingFiles,
this.options,
ts__default.sys,
void 0,
(diagnostic) => {
if (diagnostic.category !== ts__default.DiagnosticCategory.Message && diagnostic.file) {
output({
[linterPluginBuild.normalizePath(diagnostic.file.fileName)]: diagnostic
});
}
},
(diagnostic, newLine, options, errorCount) => {
if (errorCount !== void 0 && errorCount <= 0) {
output({});
}
}
);
this.watcher = ts__default.createWatchProgram(host);
}
}
// Fix for ts api not respecting typeRoots option
getCustomTypeRootFiles() {
let files = [];
if (this.options.typeRoots) {
for (const root of this.options.typeRoots) {
if (!root.includes("node_modules")) {
files = files.concat(linterPluginBuild.readAllFiles(root, (f) => f.endsWith(".d.ts")));
}
}
}
return files;
}
loadOptions() {
this.optionsLoadedFromFile = true;
if (!this.options.configFilePath) {
return;
}
const configPath = path__default.resolve(process.cwd(), this.options.configFilePath);
const configContents = fs__default.readFileSync(configPath).toString();
const configResult = ts__default.parseConfigFileTextToJson(
configPath,
configContents
);
const compilerOptions = ts__default.convertCompilerOptionsFromJson(
configResult.config["compilerOptions"] || {},
process.cwd()
);
this.options = { ...compilerOptions.options, ...this.options };
}
}
exports.EsLinter = EsLinter;
exports.TypeScriptLinter = TypeScriptLinter;
exports.linterPlugin = linterPlugin;