UNPKG

vite-plugin-linter

Version:

Plugin for linting files with Vite

458 lines (448 loc) 14.1 kB
'use strict'; 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;