UNPKG

vite-esbuild-typescript-checker

Version:
218 lines (175 loc) 7.07 kB
import { IssueCustom } from "./worker"; import { ErrorPayload, WebSocketServer } from "vite"; import { IssueWebpackError } from "fork-ts-checker-webpack-plugin/lib/issue/IssueWebpackError"; import chalk from "chalk"; import moment from "moment"; import process from "process"; import path from "path"; import fs from "fs"; import os from "os"; import { Worker } from "worker_threads"; import { BabelCodeFrameOptions } from "fork-ts-checker-webpack-plugin/lib/formatter"; import { codeFrameColumns } from "@babel/code-frame"; import { Issue } from "fork-ts-checker-webpack-plugin/lib/issue"; import { TypeScriptReporterOptions } from "fork-ts-checker-webpack-plugin/lib/typescript-reporter/TypeScriptReporterOptions"; import { EsLintReporterOptions } from "fork-ts-checker-webpack-plugin/lib/eslint-reporter/EsLintReporterOptions"; export type WorkerCallback = (err: any, result?: any) => any; export type Formatter = (issue: Issue, options?: BabelCodeFrameOptions) => string; export class Helper { public changedFiles: Array<string> = []; public deletedFiles: Array<string> = []; public timeout: NodeJS.Timeout | undefined = undefined; public worker: Worker | undefined; public config: PluginConfig; constructor(config: PluginConfig) { this.config = config; this.timeout = undefined; this.worker = undefined; } runWorker(path: string, cb: WorkerCallback, workerData: object | null = null) { const worker = new Worker(path, { workerData }); worker.on('message', cb.bind(null, null)); worker.on('error', cb); worker.on('exit', (exitCode) => { if (exitCode === 0) { return null; } return cb(new Error(`Worker has stopped with code ${exitCode}`)); }); return worker; } workerStart(ws?: WebSocketServer, watch: boolean = false) { this.config.checker.async = watch; this.worker = this.runWorker(path.resolve(__dirname, 'worker.js'), (err, message) => { if (err) { console.log(err); process.exit(); return null; } this.handleMessage(message, ws) }, this.config.checker); this.worker.postMessage({ type: 'changedFiles', files: { changedFiles: this.changedFiles, deletedFiles: this.deletedFiles } }); } clearFiles() { this.changedFiles = []; this.deletedFiles = []; if (this.timeout) clearTimeout(this.timeout); } addFile = (path: string) => { if (!path.endsWith('.vue') && !path.endsWith('.ts') && !path.endsWith('.js')) return; if (this.changedFiles.indexOf(path) === -1) { this.changedFiles.push(path); if (this.timeout) clearTimeout(this.timeout); this.timeout = setTimeout(this.changeFiles.bind(this), 300); } } deleteFile = (path: string) => { if (!path.endsWith('.vue') && !path.endsWith('.ts') && !path.endsWith('.js') && !path.endsWith('vite.config.ts')) return; if (this.deletedFiles.indexOf(path) === -1) { this.deletedFiles.push(path); if (this.timeout) clearTimeout(this.timeout); this.timeout = setTimeout(this.changeFiles.bind(this), 300); } } changeFiles() { this.worker?.postMessage({ type: 'changedFiles', files: { changedFiles: this.changedFiles, deletedFiles: this.deletedFiles } }); this.clearFiles(); } getPayloadError(issues: IssueCustom): ErrorPayload { return { type: 'error', err: issueToViteError(issues) }; } handleMessage(message: any, ws?: WebSocketServer) { if (message.type === 'issueList') { const issues = message.issues.map((issue: IssueCustom) => { issue.formatted = createCodeFrameFormatter({ highlightCode: false, forceColor: false })(issue); issue.formattedColor = createCodeFrameFormatter({ highlightCode: true })(issue); const error = new IssueWebpackError(issue.formattedColor, issue); console.log([ chalk.red(`ERROR in ${error.file}`), `${error.message}` ].join("\n")); return issue; }); if (ws && issues[0] && this.config.vite.overlay) { ws.send(this.getPayloadError(issues[0])); } if (issues.length) console.log(chalk.red(`[${moment().format('Y-MM-DD H:mm:ss')}] Found ${issues.length} errors.`)); console.log(chalk.green(`[${moment().format('Y-MM-DD H:mm:ss')}] Types check done in ${message.time}ms.`)); } } } export function issueToViteError( issue: IssueCustom ): ErrorPayload['err'] { let loc: ErrorPayload['err']['loc'] if (issue.location) { loc = { file: issue.file, line: issue.location.start.line, column: issue.location.start.column ?? 0, } } return { message: issue.formatted ?? '', stack: "", id: issue.file, frame: 'issue.stripedCodeFrame', plugin: `fork-ts-checker-vite-plugin`, loc, } } export function createCodeFrameFormatter(options?: BabelCodeFrameOptions): Formatter { const basicFormatter = createBasicFormatter(); return function codeFrameFormatter(issue) { const source = issue.file && fs.existsSync(issue.file) && fs.readFileSync(issue.file, 'utf-8'); let frame = ''; if (source && issue.location) { frame = codeFrameColumns(source, issue.location, { highlightCode: true, ...(options || {}), }) .split('\n') .map((line: string) => ' ' + line) .join(os.EOL); } const lines = [basicFormatter(issue, { highlightCode: true, ...(options || {}), })]; if (frame) { lines.push(frame); } return lines.join(os.EOL); }; } export function createBasicFormatter(): Formatter { return function basicFormatter(issue, options?: BabelCodeFrameOptions) { return (options?.highlightCode ? chalk.grey(issue.code + ': ') : issue.code + ': ') + issue.message; }; } export interface PluginConfig { vite: { overlay: boolean }, checker: ForkTsCheckerWebpackPluginOptions } export interface ForkTsCheckerWebpackPluginOptions { async?: boolean; typescript?: TypeScriptReporterOptions; eslint?: EsLintReporterOptions; formatter?: 'codeframe' | 'basic'; }