import { InitializeParams, InitializeRequest, InitializeResult, createProtocolConnection, StreamMessageReader, StreamMessageWriter, Logger, DidOpenTextDocumentNotification, Diagnostic, DiagnosticSeverity, createConnection, ServerCapabilities } from 'vscode-languageserver/node'; import { Duplex } from 'stream'; import { VLS } from 'vls'; import { getInitParams } from '../initParams'; import fs from 'fs'; import { URI } from 'vscode-uri'; import glob from 'glob'; import path from 'path'; import chalk from 'chalk'; import { codeFrameColumns, SourceLocation } from '@babel/code-frame'; import { Range } from 'vscode-languageclient'; export type LogLevel = typeof logLevels[number]; export const logLevels = ['ERROR', 'WARN', 'INFO', 'HINT'] as const; const logLevel2Severity = { ERROR: DiagnosticSeverity.Error, WARN: DiagnosticSeverity.Warning, INFO: DiagnosticSeverity.Information, HINT: DiagnosticSeverity.Hint }; export async function diagnostics(workspace: string | null, paths: string[], logLevel: LogLevel) { console.log('===================================='); console.log('Getting Vetur diagnostics'); let workspaceUri; if (workspace) { const absPath = path.resolve(process.cwd(), workspace); console.log(`Loading Vetur in workspace path: ${chalk.green(absPath)}`); workspaceUri = URI.file(absPath); } else { console.log(`Loading Vetur in current directory: ${chalk.green(process.cwd())}`); workspaceUri = URI.file(process.cwd()); } const errCount = await getDiagnostics(workspaceUri, paths, logLevel2Severity[logLevel]); console.log('===================================='); if (errCount === 0) { console.log(chalk.green(`VTI found no error`)); process.exit(0); } else { console.log(chalk.red(`VTI found ${errCount} ${errCount === 1 ? 'error' : 'errors'}`)); process.exit(1); } } class NullLogger implements Logger { error(_message: string): void {} warn(_message: string): void {} info(_message: string): void {} log(_message: string): void {} } class TestStream extends Duplex { _write(chunk: string, _encoding: string, done: () => void) { this.emit('data', chunk); done(); } _read(_size: number) {} } async function prepareClientConnection(workspaceUri: URI) { const up = new TestStream(); const down = new TestStream(); const logger = new NullLogger(); const clientConnection = createProtocolConnection(new StreamMessageReader(down), new StreamMessageWriter(up), logger); const serverConnection = createConnection(new StreamMessageReader(up), new StreamMessageWriter(down)); const vls = new VLS(serverConnection as any); serverConnection.onInitialize(async (params: InitializeParams): Promise => { await vls.init(params); console.log('Vetur initialized'); console.log('===================================='); return { capabilities: vls.capabilities as ServerCapabilities }; }); vls.listen(); clientConnection.listen(); const init = getInitParams(workspaceUri); await clientConnection.sendRequest(InitializeRequest.type as unknown as string, init); return clientConnection; } function range2Location(range: Range): SourceLocation { return { start: { line: range.start.line + 1, column: range.start.character + 1 }, end: { line: range.end.line + 1, column: range.end.character + 1 } }; } async function getDiagnostics(workspaceUri: URI, paths: string[], severity: DiagnosticSeverity) { const clientConnection = await prepareClientConnection(workspaceUri); let files: string[]; if (paths.length === 0) { files = glob.sync('**/*.vue', { cwd: workspaceUri.fsPath, ignore: ['node_modules/**'] }); } else { // Could use `flatMap` once available: const listOfPaths = paths.map(inputPath => { const absPath = path.resolve(workspaceUri.fsPath, inputPath); if (fs.lstatSync(absPath).isFile()) { return [inputPath]; } const directory = URI.file(absPath); const directoryFiles = glob.sync('**/*.vue', { cwd: directory.fsPath, ignore: ['node_modules/**'] }); return directoryFiles.map(f => path.join(inputPath, f)); }); files = listOfPaths.reduce((acc: string[], paths) => [...acc, ...paths], []); } if (files.length === 0) { console.log('No input files'); return 0; } console.log(''); console.log('Getting diagnostics from: ', files, '\n'); const absFilePaths = files.map(f => path.resolve(workspaceUri.fsPath, f)); let errCount = 0; for (const absFilePath of absFilePaths) { const fileText = fs.readFileSync(absFilePath, 'utf-8'); await clientConnection.sendNotification(DidOpenTextDocumentNotification.type, { textDocument: { languageId: 'vue', uri: URI.file(absFilePath).toString(), version: 1, text: fileText } }); try { let res = (await clientConnection.sendRequest('$/getDiagnostics', { uri: URI.file(absFilePath).toString() })) as Diagnostic[]; /** * Ignore eslint errors for now */ res = res.filter(r => r.source !== 'eslint-plugin-vue').filter(r => r.severity && r.severity <= severity); if (res.length > 0) { res.forEach(d => { const location = range2Location(d.range); console.log( `${chalk.green('File')} : ${chalk.green(absFilePath)}:${location.start.line}:${location.start.column}` ); if (d.severity === DiagnosticSeverity.Error) { console.log(`${chalk.red('Error')}: ${d.message.trim()}`); errCount++; } else { console.log(`${chalk.yellow('Warn')} : ${d.message.trim()}`); } console.log(codeFrameColumns(fileText, location)); }); console.log(''); } } catch (err) { console.error((err as Error).stack); } } return errCount; }