Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Asynchronously check for type errors #7668

Merged
merged 5 commits into from
Jun 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 126 additions & 15 deletions packages/next/build/output/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -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
)
}
Expand Down Expand Up @@ -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
}
Expand All @@ -175,31 +213,104 @@ 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: {} })

const { errors, warnings } = formatWebpackMessages(
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 })
)

Expand Down
12 changes: 9 additions & 3 deletions packages/next/build/output/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type OutputState =
| { loading: true }
| {
loading: false
typeChecking: boolean
errors: string[] | null
warnings: string[] | null
}
Expand Down Expand Up @@ -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})` : '')
)
})
2 changes: 1 addition & 1 deletion packages/next/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ export default async function getBaseWebpackConfig(
useTypeScript &&
new ForkTsCheckerWebpackPlugin({
typescript: typeScriptPath,
async: false,
async: dev,
useTypescriptIncrementalApi: true,
checkSyntacticErrors: true,
tsconfig: tsConfigPath,
Expand Down

This file was deleted.

45 changes: 37 additions & 8 deletions packages/next/client/dev/error-overlay/hot-dev-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -130,6 +131,8 @@ function clearOutdatedErrors () {
console.clear()
}
}

deferredBuildError = null
}

// Successful compilation.
Expand All @@ -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()
}
})
}
}
Expand Down Expand Up @@ -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)
Expand Down
Loading