From 5231531d3e805e72346a32bd075eddaccd28d962 Mon Sep 17 00:00:00 2001 From: fi3ework Date: Sun, 28 Mar 2021 23:15:34 +0800 Subject: [PATCH] feat: support programmatic type check --- src/apiMode.ts | 101 +++++++++++++++++++++++++++++++++++++++++++++++++ src/cliMode.ts | 73 +++++++++++++++++++++++++++++++++++ src/main.ts | 90 ++++++++++--------------------------------- 3 files changed, 195 insertions(+), 69 deletions(-) create mode 100644 src/apiMode.ts create mode 100644 src/cliMode.ts diff --git a/src/apiMode.ts b/src/apiMode.ts new file mode 100644 index 00000000..54158b6e --- /dev/null +++ b/src/apiMode.ts @@ -0,0 +1,101 @@ +import ts from 'typescript' +import type { UserConfig, ViteDevServer } from 'vite' + +interface DiagnoseOptions { + root: string + tsconfigPath: string +} + +const formatHost: ts.FormatDiagnosticsHost = { + getCanonicalFileName: (path) => path, + getCurrentDirectory: ts.sys.getCurrentDirectory, + getNewLine: () => ts.sys.newLine, +} + +function reportDiagnostic(diagnostic: ts.Diagnostic) { + console.error( + 'Error', + diagnostic.code, + ':', + ts.flattenDiagnosticMessageText(diagnostic.messageText, formatHost.getNewLine()) + ) +} + +/** + * Prints a diagnostic every time the watch status changes. + * This is mainly for messages like "Starting compilation" or "Compilation completed". + */ +function reportWatchStatusChanged(diagnostic: ts.Diagnostic) { + console.info(ts.formatDiagnostic(diagnostic, formatHost)) +} + +export function createDiagnosis(userOptions: Partial = {}) { + let overlay = true // Vite default to true + let err: string | null = null + + return { + config: (config: UserConfig) => { + const hmr = config.server?.hmr + if (typeof hmr === 'object' && hmr.overlay === false) { + overlay = true + } + }, + configureServer(server: ViteDevServer) { + const finalConfig: DiagnoseOptions = { + root: process.cwd(), + tsconfigPath: 'tsconfig.json', + ...userOptions, + } + + const configFile = ts.findConfigFile( + finalConfig.root, + ts.sys.fileExists, + finalConfig.tsconfigPath + ) + + if (!configFile) { + throw new Error("Could not find a valid 'tsconfig.json'.") + } + + // const { config } = ts.readConfigFile(configFile, ts.sys.readFile) + // const { options } = ts.parseJsonConfigFileContent(config, ts.sys, finalConfig.root) + // force --noEmit + // options.noEmit = true + + // https://github.com/microsoft/TypeScript/issues/32385 + const createProgram = ts.createEmitAndSemanticDiagnosticsBuilderProgram + const host = ts.createWatchCompilerHost( + configFile, + { noEmit: true }, + ts.sys, + createProgram, + reportDiagnostic, + reportWatchStatusChanged + ) + + // You can technically override any given hook on the host, though you probably + // don't need to. + // Note that we're assuming `origCreateProgram` and `origPostProgramCreate` + // doesn't use `this` at all. + // const origCreateProgram = host.createProgram + // @ts-ignore + // host.createProgram = (rootNames: ReadonlyArray, options, host, oldProgram) => { + // console.log("** We're about to create the program! **") + // return origCreateProgram(rootNames, options, host, oldProgram) + // } + + // const origPostProgramCreate = host.afterProgramCreate + + // host.afterProgramCreate = (program) => { + // console.log('** We finished making the program! **') + // origPostProgramCreate!(program) + // } + + // `createWatchProgram` creates an initial program, watches files, and updates + // the program over time. + ts.createWatchProgram(host) + }, + } +} + +export const diagnose = createDiagnosis() diff --git a/src/cliMode.ts b/src/cliMode.ts new file mode 100644 index 00000000..9997ba43 --- /dev/null +++ b/src/cliMode.ts @@ -0,0 +1,73 @@ +import ts from 'typescript' +import type { UserConfig, ViteDevServer } from 'vite' +import { exec, ChildProcess, spawn } from 'child_process' + +const placeHolders = { + tscStart: '', + tscEnd: ' error.', + tscWatchStart: 'File change detected. Starting incremental compilation...', + tscWatchEnd: '. Watching for file changes.', +} + +function findOutputEnd(data: string): null | number { + const regResult = /Found (\d+) error. Watching for file changes/.exec(data) + if (!regResult) return null + return Number(regResult[1]) +} + +function createTscProcess() { + let overlay = true // Vite default to true + let err: string | null = null + + return { + config: (config: UserConfig) => { + const hmr = config.server?.hmr + if (typeof hmr === 'object' && hmr.overlay === false) { + overlay = true + } + }, + configureServer: (server: ViteDevServer) => { + const tsProc = exec('tsc --noEmit --watch', { cwd: server.config.root }) + // const tsProc = spawn('tsc', ['--noEmit', '--watch'], { cwd: root, stdio: 'pipe' }) + // diagnosticCount++ + tsProc.stdout!.on('data', (data) => { + const dataStr = data.toString() + const parsedError = findOutputEnd(dataStr) + if (parsedError === 0 || parsedError === null) { + err = null + if (!overlay) return + server.ws.send({ + type: 'update', + updates: [], + }) + return + } + + if (parsedError > 0) { + err = dataStr + if (!overlay) return + server.ws.send({ + type: 'error', + err: { + message: 'error msg', + stack: 'a/b/c/d', + id: 'fork-ts-checker', + frame: 'frame', + plugin: 'tsc', + pluginCode: 'code', + // loc: , + }, + }) + return + } + + // do not clear stdout + // if (dataStr === '\x1Bc') return + // console.log(data) + // process.stdout.pipe(data) + }) + }, + } +} + +export const tscProcess = createTscProcess() diff --git a/src/main.ts b/src/main.ts index 2f894ee1..e7edf9c5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,7 @@ // import ts from 'typescript' -import { exec, ChildProcess, spawn } from 'child_process' -import { ViteDevServer, Plugin } from 'vite' -import logUpdate from 'log-update' +import { Plugin } from 'vite' +import { tscProcess } from './cliMode' +import { diagnose } from './apiMode' interface PluginOptions { /** @@ -17,88 +17,40 @@ interface PluginOptions { * */ errorOverlay?: boolean -} - -const placeHolders = { - tscStart: '', - tscEnd: ' error.', - tscWatchStart: 'File change detected. Starting incremental compilation...', - tscWatchEnd: '. Watching for file changes.', -} - -function setOutputState(data: string): 'start' | 'middle' | 'end' { - if (data.includes(placeHolders.tscWatchStart)) { - return 'start' - } - - if (data.includes(placeHolders.tscWatchEnd)) { - return 'end' - } - - return 'middle' + /** + * + */ + mode?: 'cli' | 'api' } export function plugin(userOptions?: PluginOptions): Plugin { let hasVueTsc = false - let err: string | null = null try { require.resolve('vue-tsc') hasVueTsc = true } catch {} - const options: PluginOptions = { - vueTsc: userOptions?.vueTsc ?? hasVueTsc, - } + const mode = userOptions?.mode || 'api' return { name: 'fork-ts-checker', + config: (config) => { + if (mode === 'cli') { + tscProcess.config(config) + } else { + // diagnose.config(config) + } + }, configureServer(server) { - const root = server.config.root - // let diagnosticCount = 0 - // let outputing = false - - const tsProc = spawn('tsc', ['--noEmit', '--watch'], { cwd: root, stdio: 'pipe' }) - // const tsProc = exec('tsc --noEmit --watch', { cwd: root }) - // diagnosticCount++ - - tsProc.stdout.on('data', (data) => { - const dataStr = data.toString() - if (dataStr.includes('Found 1 error')) { - err = dataStr - server.ws.send({ - type: 'error', - err: { - message: 'dataStr', - stack: '', - // id: 'sdf', - // frame: strip((err as RollupError).frame || ''), - // plugin: (err as RollupError).plugin, - // pluginCode: (err as RollupError).pluginCode, - // loc: (err as RollupError).loc, - }, - }) - } - - if (dataStr.includes('Found 0 error')) { - err = null - server.ws.send({ - type: 'update', - updates: [], - }) - } - // do not clear stdout - // if (dataStr === '\x1Bc') return - // console.log(data) - // process.stdout.pipe(data) - }) - - // const is = require.resolve + if (mode === 'cli') { + tscProcess.configureServer(server) + } else { + diagnose.configureServer(server) + } - // return a post hook that is called after internal middlewares are - // installed return () => { server.middlewares.use((req, res, next) => { - next(undefined) + next() }) } },