Skip to content

Commit

Permalink
feat: support programmatic type check
Browse files Browse the repository at this point in the history
  • Loading branch information
fi3ework committed Mar 28, 2021
1 parent af77873 commit 5231531
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 69 deletions.
101 changes: 101 additions & 0 deletions src/apiMode.ts
Original file line number Diff line number Diff line change
@@ -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<DiagnoseOptions> = {}) {
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<string>, 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()
73 changes: 73 additions & 0 deletions src/cliMode.ts
Original file line number Diff line number Diff line change
@@ -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()
90 changes: 21 additions & 69 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand All @@ -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()
})
}
},
Expand Down

0 comments on commit 5231531

Please sign in to comment.