未验证 提交 e7110a03 编写于 作者: J Joe Haddad 提交者: GitHub

Asynchronously check for type errors (#7668)

* Asynchronously check for type errors

* Add TODO

* Fix webpack invalidation

* Show TypeScript error in dev client
上级 2259a7ec
......@@ -5,6 +5,9 @@ import stripAnsi from 'strip-ansi'
import formatWebpackMessages from '../../client/dev/error-overlay/format-webpack-messages'
import { OutputState, store as consoleStore } from './store'
import forkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'
import { NormalizedMessage } from 'fork-ts-checker-webpack-plugin/lib/NormalizedMessage'
import { createCodeframeFormatter } from 'fork-ts-checker-webpack-plugin/lib/formatter/codeframeFormatter'
export function startedDevelopmentServer(appUrl: string) {
consoleStore.setState({ appUrl })
......@@ -13,13 +16,17 @@ export function startedDevelopmentServer(appUrl: string) {
let previousClient: any = null
let previousServer: any = null
type CompilerDiagnostics = {
errors: string[] | null
warnings: string[] | null
}
type WebpackStatus =
| { loading: true }
| {
| ({
loading: false
errors: string[] | null
warnings: string[] | null
}
typeChecking: boolean
} & CompilerDiagnostics)
type AmpStatus = {
message: string
......@@ -41,8 +48,9 @@ type BuildStatusStore = {
enum WebpackStatusPhase {
COMPILING = 1,
COMPILED_WITH_ERRORS = 2,
COMPILED_WITH_WARNINGS = 3,
COMPILED = 4,
TYPE_CHECKING = 3,
COMPILED_WITH_WARNINGS = 4,
COMPILED = 5,
}
function getWebpackStatusPhase(status: WebpackStatus): WebpackStatusPhase {
......@@ -52,6 +60,9 @@ function getWebpackStatusPhase(status: WebpackStatus): WebpackStatusPhase {
if (status.errors) {
return WebpackStatusPhase.COMPILED_WITH_ERRORS
}
if (status.typeChecking) {
return WebpackStatusPhase.TYPE_CHECKING
}
if (status.warnings) {
return WebpackStatusPhase.COMPILED_WITH_WARNINGS
}
......@@ -125,14 +136,36 @@ buildStore.subscribe(state => {
true
)
} else {
let { errors, warnings } = status
let { errors, warnings, typeChecking } = status
if (errors == null) {
if (typeChecking) {
consoleStore.setState(
{
...partialState,
loading: false,
typeChecking: true,
errors,
warnings,
} as OutputState,
true
)
return
}
if (errors == null && Object.keys(amp).length > 0) {
warnings = (warnings || []).concat(formatAmpMessages(amp))
if (Object.keys(amp).length > 0) {
warnings = (warnings || []).concat(formatAmpMessages(amp))
}
}
consoleStore.setState(
{ ...partialState, loading: false, errors, warnings } as OutputState,
{
...partialState,
loading: false,
typeChecking: false,
errors,
warnings,
} as OutputState,
true
)
}
......@@ -162,7 +195,12 @@ export function ampValidation(
})
}
export function watchCompiler(client: any, server: any) {
export function watchCompilers(
client: any,
server: any,
enableTypeCheckingOnClient: boolean,
onTypeChecked: (diagnostics: CompilerDiagnostics) => void
) {
if (previousClient === client && previousServer === server) {
return
}
......@@ -175,12 +213,50 @@ export function watchCompiler(client: any, server: any) {
function tapCompiler(
key: string,
compiler: any,
hasTypeChecking: boolean,
onEvent: (status: WebpackStatus) => void
) {
let tsMessagesPromise: Promise<CompilerDiagnostics> | undefined
let tsMessagesResolver: (diagnostics: CompilerDiagnostics) => void
compiler.hooks.invalid.tap(`NextJsInvalid-${key}`, () => {
tsMessagesPromise = undefined
onEvent({ loading: true })
})
if (hasTypeChecking) {
const typescriptFormatter = createCodeframeFormatter({})
compiler.hooks.beforeCompile.tap(`NextJs-${key}-StartTypeCheck`, () => {
tsMessagesPromise = new Promise(resolve => {
tsMessagesResolver = msgs => resolve(msgs)
})
})
forkTsCheckerWebpackPlugin
.getCompilerHooks(compiler)
.receive.tap(
`NextJs-${key}-afterTypeScriptCheck`,
(diagnostics: NormalizedMessage[], lints: NormalizedMessage[]) => {
const allMsgs = [...diagnostics, ...lints]
const format = (message: NormalizedMessage) =>
typescriptFormatter(message, true)
const errors = allMsgs
.filter(msg => msg.severity === 'error')
.map(format)
const warnings = allMsgs
.filter(msg => msg.severity === 'warning')
.map(format)
tsMessagesResolver({
errors: errors.length ? errors : null,
warnings: warnings.length ? warnings : null,
})
}
)
}
compiler.hooks.done.tap(`NextJsDone-${key}`, (stats: any) => {
buildStore.setState({ amp: {} })
......@@ -188,18 +264,53 @@ export function watchCompiler(client: any, server: any) {
stats.toJson({ all: false, warnings: true, errors: true })
)
const hasErrors = errors && errors.length
const hasWarnings = warnings && warnings.length
onEvent({
loading: false,
errors: errors && errors.length ? errors : null,
warnings: warnings && warnings.length ? warnings : null,
typeChecking: hasTypeChecking,
errors: hasErrors ? errors : null,
warnings: hasWarnings ? warnings : null,
})
const typePromise = tsMessagesPromise
if (!hasErrors && typePromise) {
typePromise.then(typeMessages => {
if (typePromise !== tsMessagesPromise) {
// a new compilation started so we don't care about this
return
}
stats.compilation.errors.push(...(typeMessages.errors || []))
stats.compilation.warnings.push(...(typeMessages.warnings || []))
onTypeChecked({
errors: stats.compilation.errors.length
? stats.compilation.errors
: null,
warnings: stats.compilation.warnings.length
? stats.compilation.warnings
: null,
})
onEvent({
loading: false,
typeChecking: false,
errors: typeMessages.errors,
warnings: hasWarnings
? [...warnings, ...(typeMessages.warnings || [])]
: typeMessages.warnings,
})
})
}
})
}
tapCompiler('client', client, status =>
tapCompiler('client', client, enableTypeCheckingOnClient, status =>
buildStore.setState({ client: status })
)
tapCompiler('server', server, status =>
tapCompiler('server', server, false, status =>
buildStore.setState({ server: status })
)
......
......@@ -9,6 +9,7 @@ export type OutputState =
| { loading: true }
| {
loading: false
typeChecking: boolean
errors: string[] | null
warnings: string[] | null
}
......@@ -76,8 +77,13 @@ store.subscribe(state => {
return
}
Log.ready('compiled successfully')
if (state.appUrl) {
Log.info(`ready on ${state.appUrl}`)
if (state.typeChecking) {
Log.info('bundled successfully, waiting for typecheck results ...')
return
}
Log.ready(
'compiled successfully' +
(state.appUrl ? ` (ready on ${state.appUrl})` : '')
)
})
......@@ -547,7 +547,7 @@ export default async function getBaseWebpackConfig(
useTypeScript &&
new ForkTsCheckerWebpackPlugin({
typescript: typeScriptPath,
async: false,
async: dev,
useTypescriptIncrementalApi: true,
checkSyntacticErrors: true,
tsconfig: tsConfigPath,
......
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'
import { NormalizedMessage } from 'fork-ts-checker-webpack-plugin/lib/NormalizedMessage'
import webpack from 'webpack'
export function Apply(compiler: webpack.Compiler) {
const hooks = ForkTsCheckerWebpackPlugin.getCompilerHooks(compiler)
let additionalFiles: string[] = []
hooks.receive.tap('ForkTsCheckerWatcherHook', function(
diagnostics: NormalizedMessage[],
lints: NormalizedMessage[]
) {
additionalFiles = [
...new Set([
...diagnostics.map(d => d.file!),
...lints.map(l => l.file!),
]),
].filter(Boolean)
})
compiler.hooks.afterCompile.tap('ForkTsCheckerWatcherHook', function(
compilation
) {
additionalFiles.forEach(file => compilation.fileDependencies.add(file))
})
}
......@@ -122,6 +122,7 @@ export default function connect (options) {
var isFirstCompilation = true
var mostRecentCompilationHash = null
var hasCompileErrors = false
let deferredBuildError = null
function clearOutdatedErrors () {
// Clean up outdated compile errors, if any.
......@@ -130,6 +131,8 @@ function clearOutdatedErrors () {
console.clear()
}
}
deferredBuildError = null
}
// Successful compilation.
......@@ -141,9 +144,13 @@ function handleSuccess () {
// Attempt to apply hot updates or reload.
if (isHotUpdate) {
tryApplyUpdates(function onHotUpdateSuccess () {
// Only dismiss it when we're sure it's a hot update.
// Otherwise it would flicker right before the reload.
ErrorOverlay.dismissBuildError()
if (deferredBuildError) {
deferredBuildError()
} else {
// Only dismiss it when we're sure it's a hot update.
// Otherwise it would flicker right before the reload.
ErrorOverlay.dismissBuildError()
}
})
}
}
......@@ -220,22 +227,44 @@ function processMessage (e) {
handleAvailableHash(obj.hash)
}
if (obj.warnings.length > 0) {
handleWarnings(obj.warnings)
}
const { errors, warnings } = obj
const hasErrors = Boolean(errors && errors.length)
if (obj.errors.length > 0) {
const hasWarnings = Boolean(warnings && warnings.length)
if (hasErrors) {
// When there is a compilation error coming from SSR we have to reload the page on next successful compile
if (obj.action === 'sync') {
hadRuntimeError = true
}
handleErrors(obj.errors)
handleErrors(errors)
break
} else if (hasWarnings) {
handleWarnings(warnings)
}
handleSuccess()
break
}
case 'typeChecked': {
const [{ errors, warnings }] = obj.data
const hasErrors = Boolean(errors && errors.length)
const hasWarnings = Boolean(warnings && warnings.length)
if (hasErrors) {
if (canApplyUpdates()) {
handleErrors(errors)
} else {
deferredBuildError = () => handleErrors(errors)
}
} else if (hasWarnings) {
handleWarnings(warnings)
}
break
}
default: {
if (customHmrEventHandler) {
customHmrEventHandler(obj)
......
......@@ -14,11 +14,10 @@ import {
import { NEXT_PROJECT_ROOT_DIST_CLIENT } from '../lib/constants'
import { route } from 'next-server/dist/server/router'
import { createPagesMapping, createEntrypoints } from '../build/entries'
import { watchCompiler } from '../build/output'
import { watchCompilers } from '../build/output'
import { findPageFile } from './lib/find-page-file'
import { recursiveDelete } from '../lib/recursive-delete'
import { promisify } from 'util'
import * as ForkTsCheckerWatcherHook from '../build/webpack/plugins/fork-ts-checker-watcher-hook'
import fs from 'fs'
const access = promisify(fs.access)
......@@ -310,13 +309,15 @@ export default class HotReloader {
}
async prepareBuildTools (multiCompiler) {
watchCompiler(multiCompiler.compilers[0], multiCompiler.compilers[1])
const tsConfigPath = join(this.dir, 'tsconfig.json')
const useTypeScript = await fileExists(tsConfigPath)
if (useTypeScript) {
ForkTsCheckerWatcherHook.Apply(multiCompiler.compilers[0])
}
watchCompilers(
multiCompiler.compilers[0],
multiCompiler.compilers[1],
useTypeScript,
({ errors, warnings }) => this.send('typeChecked', { errors, warnings })
)
// This plugin watches for changes to _document.js and notifies the client side that it should reload the page
multiCompiler.compilers[1].hooks.done.tap(
......@@ -494,7 +495,7 @@ export default class HotReloader {
return []
}
send (action, ...args) {
send = (action, ...args) => {
this.webpackHotMiddleware.publish({ action, data: args })
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册