Skip to content

Commit

Permalink
feat: support vue-tsc watch mode
Browse files Browse the repository at this point in the history
  • Loading branch information
fi3ework committed Mar 24, 2022
1 parent 8afcd3e commit 6871f55
Show file tree
Hide file tree
Showing 17 changed files with 646 additions and 173 deletions.
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@
"lint-staged": "^11.0.0",
"minimist": "^1.2.5",
"nodemon": "^2.0.15",
"optionator": "^0.9.1",
"playwright-chromium": "^1.19.2",
"prettier": "^2.3.2",
"prettier-plugin-svelte": "^2.6.0",
Expand All @@ -69,7 +68,7 @@
"strip-ansi": "^7.0.0",
"svelte": "^3.46.3",
"ts-jest": "^27.1.3",
"typescript": "^4.2.2",
"typescript": "^4.6.2",
"vite": "^2.7.13",
"vite-plugin-checker": "workspace:*",
"ws": "^8.5.0",
Expand Down
4 changes: 3 additions & 1 deletion packages/vite-plugin-checker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
"esbuild": "^0.14.13",
"npm-run-all": "^4.1.5",
"optionator": "^0.9.1",
"vls": "^0.7.2"
"vls": "^0.7.2",
"vue-tsc": "0.33.5",
"@volar/vue-typescript": "^0.33.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ async function getDiagnostics(

console.log(logChunk)
return { initialErrorCount, initialWarningCount }
} catch (err) {
} catch (err: any) {
console.error(err.stack)
return { initialErrorCount, initialWarningCount }
}
Expand Down
170 changes: 159 additions & 11 deletions packages/vite-plugin-checker/src/checkers/vueTsc/main.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,143 @@
import { Checker } from '../../Checker'
import os from 'os'
import path from 'path'
import invariant from 'tiny-invariant'
import ts from 'typescript'
import { parentPort } from 'worker_threads'

import type { CreateDiagnostic } from '../../types'
import { Checker } from '../../Checker'
import {
consoleLog,
diagnosticToRuntimeError,
diagnosticToTerminalLog,
ensureCall,
normalizeTsDiagnostic,
toViteCustomPayload,
wrapCheckerSummary,
} from '../../logger'
import { ACTION_TYPES, CreateDiagnostic, DiagnosticLevel, DiagnosticToRuntime } from '../../types'
import { prepareVueTsc } from './prepareVueTsc'

// TODO: watch mode is not supported for now
const createDiagnostic: CreateDiagnostic<'vueTsc'> = (pluginConfig) => {
let overlay = true
let terminal = true
let currDiagnostics: DiagnosticToRuntime[] = []

return {
config: (config) => {
//
config: ({ enableOverlay, enableTerminal }) => {
overlay = enableOverlay
terminal = enableTerminal
},
configureServer(server) {
//
configureServer({ root }) {
invariant(pluginConfig.vueTsc, 'config.typescript should be `false`')

const { tsDirTo } = prepareVueTsc()
// const tsDirTo = path.resolve(__dirname, 'typescript-vue-tsc')

const vueTs = require(path.resolve(tsDirTo, 'lib/tsc.js'))

const finalConfig =
pluginConfig.vueTsc === true
? { root, tsconfigPath: 'tsconfig.json' }
: {
root: pluginConfig.vueTsc.root ?? root,
tsconfigPath: pluginConfig.vueTsc.tsconfigPath ?? 'tsconfig.json',
}

let configFile: string | undefined

configFile = vueTs.findConfigFile(
finalConfig.root,
vueTs.sys.fileExists,
finalConfig.tsconfigPath
)

if (configFile === undefined) {
throw Error(
`Failed to find a valid tsconfig.json: ${finalConfig.tsconfigPath} at ${finalConfig.root} is not a valid tsconfig`
)
}

let logChunk = ''

// https://github.com/microsoft/TypeScript/blob/a545ab1ac2cb24ff3b1aaf0bfbfb62c499742ac2/src/compiler/watch.ts#L12-L28
const reportDiagnostic = (diagnostic: ts.Diagnostic) => {
const normalizedDiagnostic = normalizeTsDiagnostic(diagnostic)
if (normalizedDiagnostic === null) {
return
}

currDiagnostics.push(diagnosticToRuntimeError(normalizedDiagnostic))
logChunk += os.EOL + diagnosticToTerminalLog(normalizedDiagnostic, 'TypeScript')
}

const reportWatchStatusChanged: ts.WatchStatusReporter = (
diagnostic,
newLine,
options,
errorCount
// eslint-disable-next-line max-params
) => {
if (diagnostic.code === 6031) return
// https://github.com/microsoft/TypeScript/issues/32542
// https://github.com/microsoft/TypeScript/blob/dc237b317ed4bbccd043ddda802ffde00362a387/src/compiler/diagnosticMessages.json#L4086-L4088
switch (diagnostic.code) {
case 6031:
case 6032:
// clear current error and use the newer errors
logChunk = ''
// currErr = null
currDiagnostics = []
return
case 6193: // 1 Error
case 6194: // 0 errors or 2+ errors
if (overlay) {
parentPort?.postMessage({
type: ACTION_TYPES.overlayError,
payload: toViteCustomPayload('typescript', currDiagnostics),
})
}
}

ensureCall(() => {
if (errorCount === 0) {
logChunk = ''
}

if (terminal) {
consoleLog(
logChunk +
os.EOL +
wrapCheckerSummary('TypeScript', diagnostic.messageText.toString())
)
}
})
}

// https://github.com/microsoft/TypeScript/issues/32385
// https://github.com/microsoft/TypeScript/pull/33082/files
const createProgram = vueTs.createSemanticDiagnosticsBuilderProgram

if (typeof pluginConfig.vueTsc === 'object' && pluginConfig.vueTsc.buildMode) {
// const host = ts.createSolutionBuilderWithWatchHost(
// ts.sys,
// createProgram,
// reportDiagnostic,
// undefined,
// reportWatchStatusChanged
// )
// ts.createSolutionBuilderWithWatch(host, [configFile], {}).build()
} else {
const host = vueTs.createWatchCompilerHost(
configFile,
{ noEmit: true },
vueTs.sys,
createProgram,
reportDiagnostic,
reportWatchStatusChanged
)

vueTs.createWatchProgram(host)
}
},
}
}
Expand All @@ -19,18 +147,38 @@ export class VueTscChecker extends Checker<'vueTsc'> {
super({
name: 'vueTsc',
absFilePath: __filename,
build: { buildBin: ['vue-tsc', ['--noEmit']] },
build: {
buildBin: (config) => {
if (typeof config.typescript === 'object') {
const { root, tsconfigPath, buildMode } = config.typescript

// Compiler option '--noEmit' may not be used with '--build'
let args = [buildMode ? '-b' : '--noEmit']

// Custom config path
if (tsconfigPath) {
const fullConfigPath = root ? path.join(root, tsconfigPath) : tsconfigPath
args = args.concat(['-p', fullConfigPath])
}

return ['vue-tsc', args]
}

return ['vue-tsc', ['--noEmit']]
},
},
createDiagnostic,
})
}

public init() {
const createServeAndBuild = super.initMainThread()
module.exports.createServeAndBuild = createServeAndBuild

super.initWorkerThread()
}
}

const vueTscChecker = new VueTscChecker()
vueTscChecker.prepare()
vueTscChecker.init()
const tscChecker = new VueTscChecker()
tscChecker.prepare()
tscChecker.init()
90 changes: 90 additions & 0 deletions packages/vite-plugin-checker/src/checkers/vueTsc/prepareVueTsc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import fs from 'fs'
import path from 'path'

const proxyPath = require.resolve('vue-tsc/out/proxy')

const textToReplace: { target: string; replacement: string }[] = [
{
target: `ts.supportedTSExtensions = [[".ts", ".tsx", ".d.ts"], [".cts", ".d.cts"], [".mts", ".d.mts"]];`,
replacement: `ts.supportedTSExtensions = [[".ts", ".tsx", ".d.ts"], [".cts", ".d.cts"], [".mts", ".d.mts"], [".vue"]];`,
},
{
target: `ts.supportedJSExtensions = [[".js", ".jsx"], [".mjs"], [".cjs"]];`,
replacement: `ts.supportedJSExtensions = [[".js", ".jsx"], [".mjs"], [".cjs"], [".vue"]];`,
},

{
target: `var allSupportedExtensions = [[".ts", ".tsx", ".d.ts", ".js", ".jsx"], [".cts", ".d.cts", ".cjs"], [".mts", ".d.mts", ".mjs"]];`,
replacement: `var allSupportedExtensions = [[".ts", ".tsx", ".d.ts", ".js", ".jsx"], [".cts", ".d.cts", ".cjs"], [".mts", ".d.mts", ".mjs"], [".vue"]];`,
},

// proxy createProgram apis
{
target: `function createIncrementalProgram(_a) {`,
replacement: `function createIncrementalProgram(_a) { console.error('incremental mode is not yet supported'); throw 'incremental mode is not yet supported';`,
},
{
target: `function createProgram(rootNamesOrOptions, _options, _host, _oldProgram, _configFileParsingDiagnostics) {`,
replacement: `function createProgram(rootNamesOrOptions, _options, _host, _oldProgram, _configFileParsingDiagnostics) { return require(${JSON.stringify(
proxyPath
)}).createProgramProxy(...arguments);`,
},
{
target: `ts.executeCommandLine(ts.sys, ts.noop, ts.sys.args);`,
replacement: `module.exports = ts`,
},
]

export function prepareVueTsc() {
// 1. copy typescript to folder
const tsDirTo = path.resolve(__dirname, 'typescript-vue-tsc')
let shouldPrepare = false
const exist = fs.existsSync(tsDirTo)
if (exist) {
const toDirVersion = require(path.resolve(tsDirTo, 'package.json')).version
const tsVersion = require('typescript/package.json').version
if (toDirVersion !== tsVersion) {
shouldPrepare = true
fs.rmdirSync(tsDirTo)
}
} else {
shouldPrepare = true
}

if (shouldPrepare) {
fs.mkdirSync(tsDirTo)
const tsPath = path.resolve(require.resolve('typescript'), '../..')
copyDirRecursively(tsPath, tsDirTo)
// 2. sync modification of lib/tsc.js with vue-tsc
const tscJs = require.resolve(path.resolve(tsDirTo, 'lib/tsc.js'))
modifyFileText(tscJs, textToReplace)
}

return { tsDirTo }
}

function copyDirRecursively(src: string, dest: string) {
const files = fs.readdirSync(src, { withFileTypes: true })
for (const file of files) {
const srcPath = path.join(src, file.name)
const destPath = path.join(dest, file.name)
if (file.isDirectory()) {
fs.mkdirSync(destPath, { recursive: true })
copyDirRecursively(srcPath, destPath)
} else {
fs.copyFileSync(srcPath, destPath)
}
}
}

function modifyFileText(
filePath: string,
textToReplace: { target: string; replacement: string }[]
) {
const text = fs.readFileSync(filePath, 'utf8')
let newText = text
for (const { target, replacement } of textToReplace) {
newText = newText.replace(target, replacement)
}
fs.writeFileSync(filePath, newText)
}
40 changes: 21 additions & 19 deletions packages/vite-plugin-checker/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ import type { VlsOptions } from './checkers/vls/initParams'

/* ----------------------------- userland plugin options ----------------------------- */

interface TsConfigOptions {
/**
* path to tsconfig.json file
*/
tsconfigPath: string
/**
* root path of cwd
*/
root: string
/**
* root path of cwd
*/
buildMode: boolean
}

/**
* TypeScript checker configuration
* @default true
Expand All @@ -14,28 +29,15 @@ export type TscConfig =
* - set to `true` to enable type checking with default configuration
* - set to `false` to disable type checking, you can also remove `config.typescript` directly
*/
| boolean
| Partial<{
/**
* path to tsconfig.json file
*/
tsconfigPath: string
/**
* root path of cwd
*/
root: string
/**
* root path of cwd
*/
buildMode: boolean
}>
boolean | Partial<TsConfigOptions>

/** vue-tsc checker configuration */
export type VueTscConfig =
| boolean
| Partial<{
// TODO: support vue-tsc config
}>
/**
* - set to `true` to enable type checking with default configuration
* - set to `false` to disable type checking, you can also remove `config.vueTsc` directly
*/
boolean | Partial<Omit<TsConfigOptions, 'build'>>

/** vls checker configuration */
export type VlsConfig = boolean | DeepPartial<VlsOptions>
Expand Down
1 change: 1 addition & 0 deletions packages/vite-plugin-checker/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function createScript<T extends Partial<BuildInCheckers>>({
type: ACTION_TYPES.configureServer,
payload: serverConfig,
}

worker.postMessage(configureServerAction)
},
}
Expand Down
2 changes: 1 addition & 1 deletion playground/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@types/react-dom": "^17.0.0",
"eslint": "^7.28.0",
"@typescript-eslint/eslint-plugin": "^4.15.2",
"typescript": "^4.1.2",
"typescript": "^4.6.2",
"vite": "^2.7.13",
"vite-plugin-checker": "workspace:*"
}
Expand Down
Loading

0 comments on commit 6871f55

Please sign in to comment.