2019-03-19 00:21:18 +01:00
|
|
|
import chalk from 'chalk'
|
|
|
|
import textTable from 'next/dist/compiled/text-table'
|
2019-02-27 15:12:40 +01:00
|
|
|
import createStore from 'next/dist/compiled/unistore'
|
2019-03-19 00:21:18 +01:00
|
|
|
import stripAnsi from 'strip-ansi'
|
2019-02-16 17:09:49 +01:00
|
|
|
|
2019-06-05 20:15:42 +02:00
|
|
|
import formatWebpackMessages from '../../client/dev/error-overlay/format-webpack-messages'
|
2019-03-19 00:21:18 +01:00
|
|
|
import { OutputState, store as consoleStore } from './store'
|
2019-06-26 20:54:23 +02:00
|
|
|
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'
|
2019-02-16 17:09:49 +01:00
|
|
|
|
|
|
|
export function startedDevelopmentServer(appUrl: string) {
|
2019-03-19 00:21:18 +01:00
|
|
|
consoleStore.setState({ appUrl })
|
2019-02-16 17:09:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
let previousClient: any = null
|
|
|
|
let previousServer: any = null
|
|
|
|
|
2019-09-11 19:06:48 +02:00
|
|
|
type CompilerDiagnosticsWithFile = {
|
|
|
|
errors: { file: string | undefined; message: string }[] | null
|
|
|
|
warnings: { file: string | undefined; message: string }[] | null
|
|
|
|
}
|
|
|
|
|
2019-06-26 20:54:23 +02:00
|
|
|
type CompilerDiagnostics = {
|
|
|
|
errors: string[] | null
|
|
|
|
warnings: string[] | null
|
|
|
|
}
|
|
|
|
|
2019-02-16 17:09:49 +01:00
|
|
|
type WebpackStatus =
|
|
|
|
| { loading: true }
|
2019-06-26 20:54:23 +02:00
|
|
|
| ({
|
2019-02-16 17:09:49 +01:00
|
|
|
loading: false
|
2019-06-26 20:54:23 +02:00
|
|
|
typeChecking: boolean
|
|
|
|
} & CompilerDiagnostics)
|
2019-02-16 17:09:49 +01:00
|
|
|
|
2019-03-19 00:21:18 +01:00
|
|
|
type AmpStatus = {
|
|
|
|
message: string
|
|
|
|
line: number
|
|
|
|
col: number
|
|
|
|
specUrl: string | null
|
2019-09-03 17:11:22 +02:00
|
|
|
code: string
|
2019-03-19 00:21:18 +01:00
|
|
|
}
|
|
|
|
|
2019-10-04 17:26:44 +02:00
|
|
|
export type AmpPageStatus = {
|
2019-03-19 00:21:18 +01:00
|
|
|
[page: string]: { errors: AmpStatus[]; warnings: AmpStatus[] }
|
|
|
|
}
|
|
|
|
|
|
|
|
type BuildStatusStore = {
|
2019-02-16 17:09:49 +01:00
|
|
|
client: WebpackStatus
|
|
|
|
server: WebpackStatus
|
2019-03-19 00:21:18 +01:00
|
|
|
amp: AmpPageStatus
|
2019-02-16 17:09:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
enum WebpackStatusPhase {
|
|
|
|
COMPILING = 1,
|
|
|
|
COMPILED_WITH_ERRORS = 2,
|
2019-06-26 20:54:23 +02:00
|
|
|
TYPE_CHECKING = 3,
|
|
|
|
COMPILED_WITH_WARNINGS = 4,
|
|
|
|
COMPILED = 5,
|
2019-02-16 17:09:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
function getWebpackStatusPhase(status: WebpackStatus): WebpackStatusPhase {
|
|
|
|
if (status.loading) {
|
|
|
|
return WebpackStatusPhase.COMPILING
|
|
|
|
}
|
|
|
|
if (status.errors) {
|
|
|
|
return WebpackStatusPhase.COMPILED_WITH_ERRORS
|
|
|
|
}
|
2019-06-26 20:54:23 +02:00
|
|
|
if (status.typeChecking) {
|
|
|
|
return WebpackStatusPhase.TYPE_CHECKING
|
|
|
|
}
|
2019-02-16 17:09:49 +01:00
|
|
|
if (status.warnings) {
|
|
|
|
return WebpackStatusPhase.COMPILED_WITH_WARNINGS
|
|
|
|
}
|
|
|
|
return WebpackStatusPhase.COMPILED
|
|
|
|
}
|
|
|
|
|
2019-03-26 22:21:27 +01:00
|
|
|
export function formatAmpMessages(amp: AmpPageStatus) {
|
2019-03-19 00:21:18 +01:00
|
|
|
let output = chalk.bold('Amp Validation') + '\n\n'
|
|
|
|
let messages: string[][] = []
|
|
|
|
|
|
|
|
const chalkError = chalk.red('error')
|
|
|
|
function ampError(page: string, error: AmpStatus) {
|
|
|
|
messages.push([page, chalkError, error.message, error.specUrl || ''])
|
|
|
|
}
|
|
|
|
|
|
|
|
const chalkWarn = chalk.yellow('warn')
|
|
|
|
function ampWarn(page: string, warn: AmpStatus) {
|
|
|
|
messages.push([page, chalkWarn, warn.message, warn.specUrl || ''])
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const page in amp) {
|
2019-09-03 17:11:22 +02:00
|
|
|
let { errors, warnings } = amp[page]
|
|
|
|
|
|
|
|
const devOnlyFilter = (err: AmpStatus) => err.code !== 'DEV_MODE_ONLY'
|
|
|
|
errors = errors.filter(devOnlyFilter)
|
|
|
|
warnings = warnings.filter(devOnlyFilter)
|
|
|
|
if (!(errors.length || warnings.length)) {
|
|
|
|
// Skip page with no non-dev warnings
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2019-03-19 00:21:18 +01:00
|
|
|
if (errors.length) {
|
|
|
|
ampError(page, errors[0])
|
|
|
|
for (let index = 1; index < errors.length; ++index) {
|
|
|
|
ampError('', errors[index])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (warnings.length) {
|
|
|
|
ampWarn(errors.length ? '' : page, warnings[0])
|
|
|
|
for (let index = 1; index < warnings.length; ++index) {
|
|
|
|
ampWarn('', warnings[index])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
messages.push(['', '', '', ''])
|
|
|
|
}
|
|
|
|
|
2019-09-03 17:11:22 +02:00
|
|
|
if (!messages.length) {
|
|
|
|
return ''
|
|
|
|
}
|
|
|
|
|
2019-03-19 00:21:18 +01:00
|
|
|
output += textTable(messages, {
|
|
|
|
align: ['l', 'l', 'l', 'l'],
|
|
|
|
stringLength(str: string) {
|
|
|
|
return stripAnsi(str).length
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
return output
|
|
|
|
}
|
|
|
|
|
|
|
|
const buildStore = createStore<BuildStatusStore>()
|
2019-02-16 17:09:49 +01:00
|
|
|
|
2019-03-19 00:21:18 +01:00
|
|
|
buildStore.subscribe(state => {
|
|
|
|
const { amp, client, server } = state
|
2019-02-16 17:09:49 +01:00
|
|
|
|
|
|
|
const [{ status }] = [
|
|
|
|
{ status: client, phase: getWebpackStatusPhase(client) },
|
|
|
|
{ status: server, phase: getWebpackStatusPhase(server) },
|
|
|
|
].sort((a, b) => a.phase.valueOf() - b.phase.valueOf())
|
|
|
|
|
2019-03-19 00:21:18 +01:00
|
|
|
const { bootstrap: bootstrapping, appUrl } = consoleStore.getState()
|
2019-02-16 17:09:49 +01:00
|
|
|
if (bootstrapping && status.loading) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-03-19 00:21:18 +01:00
|
|
|
let partialState: Partial<OutputState> = {
|
2019-02-16 17:09:49 +01:00
|
|
|
bootstrap: false,
|
|
|
|
appUrl: appUrl!,
|
|
|
|
}
|
2019-03-19 00:21:18 +01:00
|
|
|
|
|
|
|
if (status.loading) {
|
|
|
|
consoleStore.setState(
|
|
|
|
{ ...partialState, loading: true } as OutputState,
|
|
|
|
true
|
|
|
|
)
|
|
|
|
} else {
|
2019-06-26 20:54:23 +02:00
|
|
|
let { errors, warnings, typeChecking } = status
|
|
|
|
|
|
|
|
if (errors == null) {
|
|
|
|
if (typeChecking) {
|
|
|
|
consoleStore.setState(
|
|
|
|
{
|
|
|
|
...partialState,
|
|
|
|
loading: false,
|
|
|
|
typeChecking: true,
|
|
|
|
errors,
|
|
|
|
warnings,
|
|
|
|
} as OutputState,
|
|
|
|
true
|
|
|
|
)
|
|
|
|
return
|
|
|
|
}
|
2019-03-19 00:21:18 +01:00
|
|
|
|
2019-06-26 20:54:23 +02:00
|
|
|
if (Object.keys(amp).length > 0) {
|
2019-10-30 04:49:40 +01:00
|
|
|
warnings = (warnings || []).concat(formatAmpMessages(amp) || [])
|
2019-09-03 17:11:22 +02:00
|
|
|
if (!warnings.length) warnings = null
|
2019-06-26 20:54:23 +02:00
|
|
|
}
|
2019-03-19 00:21:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
consoleStore.setState(
|
2019-06-26 20:54:23 +02:00
|
|
|
{
|
|
|
|
...partialState,
|
|
|
|
loading: false,
|
|
|
|
typeChecking: false,
|
|
|
|
errors,
|
|
|
|
warnings,
|
|
|
|
} as OutputState,
|
2019-03-19 00:21:18 +01:00
|
|
|
true
|
|
|
|
)
|
|
|
|
}
|
2019-02-16 17:09:49 +01:00
|
|
|
})
|
|
|
|
|
2019-03-19 00:21:18 +01:00
|
|
|
export function ampValidation(
|
|
|
|
page: string,
|
|
|
|
errors: AmpStatus[],
|
|
|
|
warnings: AmpStatus[]
|
|
|
|
) {
|
|
|
|
const { amp } = buildStore.getState()
|
|
|
|
if (!(errors.length || warnings.length)) {
|
|
|
|
buildStore.setState({
|
|
|
|
amp: Object.keys(amp)
|
|
|
|
.filter(k => k !== page)
|
|
|
|
.sort()
|
2019-11-11 04:24:53 +01:00
|
|
|
// eslint-disable-next-line no-sequences
|
2019-03-19 00:21:18 +01:00
|
|
|
.reduce((a, c) => ((a[c] = amp[c]), a), {} as any),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const newAmp: AmpPageStatus = { ...amp, [page]: { errors, warnings } }
|
|
|
|
buildStore.setState({
|
|
|
|
amp: Object.keys(newAmp)
|
|
|
|
.sort()
|
2019-11-11 04:24:53 +01:00
|
|
|
// eslint-disable-next-line no-sequences
|
2019-03-19 00:21:18 +01:00
|
|
|
.reduce((a, c) => ((a[c] = newAmp[c]), a), {} as any),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-06-26 20:54:23 +02:00
|
|
|
export function watchCompilers(
|
|
|
|
client: any,
|
|
|
|
server: any,
|
|
|
|
enableTypeCheckingOnClient: boolean,
|
|
|
|
onTypeChecked: (diagnostics: CompilerDiagnostics) => void
|
|
|
|
) {
|
2019-02-16 17:09:49 +01:00
|
|
|
if (previousClient === client && previousServer === server) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-03-19 00:21:18 +01:00
|
|
|
buildStore.setState({
|
2019-02-16 17:09:49 +01:00
|
|
|
client: { loading: true },
|
|
|
|
server: { loading: true },
|
|
|
|
})
|
|
|
|
|
|
|
|
function tapCompiler(
|
|
|
|
key: string,
|
|
|
|
compiler: any,
|
2019-06-26 20:54:23 +02:00
|
|
|
hasTypeChecking: boolean,
|
2019-02-16 17:09:49 +01:00
|
|
|
onEvent: (status: WebpackStatus) => void
|
|
|
|
) {
|
2019-09-11 19:06:48 +02:00
|
|
|
let tsMessagesPromise: Promise<CompilerDiagnosticsWithFile> | undefined
|
|
|
|
let tsMessagesResolver: (diagnostics: CompilerDiagnosticsWithFile) => void
|
2019-06-26 20:54:23 +02:00
|
|
|
|
2019-02-16 17:09:49 +01:00
|
|
|
compiler.hooks.invalid.tap(`NextJsInvalid-${key}`, () => {
|
2019-06-26 20:54:23 +02:00
|
|
|
tsMessagesPromise = undefined
|
2019-02-16 17:09:49 +01:00
|
|
|
onEvent({ loading: true })
|
|
|
|
})
|
|
|
|
|
2019-06-26 20:54:23 +02:00
|
|
|
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')
|
2019-09-11 19:06:48 +02:00
|
|
|
.map(d => ({
|
2019-09-17 04:30:09 +02:00
|
|
|
file: (d.file || '').replace(/\\/g, '/'),
|
2019-09-11 19:06:48 +02:00
|
|
|
message: format(d),
|
|
|
|
}))
|
2019-06-26 20:54:23 +02:00
|
|
|
const warnings = allMsgs
|
|
|
|
.filter(msg => msg.severity === 'warning')
|
2019-09-11 19:06:48 +02:00
|
|
|
.map(d => ({
|
2019-09-17 04:30:09 +02:00
|
|
|
file: (d.file || '').replace(/\\/g, '/'),
|
2019-09-11 19:06:48 +02:00
|
|
|
message: format(d),
|
|
|
|
}))
|
2019-06-26 20:54:23 +02:00
|
|
|
|
|
|
|
tsMessagesResolver({
|
|
|
|
errors: errors.length ? errors : null,
|
|
|
|
warnings: warnings.length ? warnings : null,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2019-02-16 17:09:49 +01:00
|
|
|
compiler.hooks.done.tap(`NextJsDone-${key}`, (stats: any) => {
|
2019-03-19 00:21:18 +01:00
|
|
|
buildStore.setState({ amp: {} })
|
|
|
|
|
2019-02-16 17:09:49 +01:00
|
|
|
const { errors, warnings } = formatWebpackMessages(
|
|
|
|
stats.toJson({ all: false, warnings: true, errors: true })
|
|
|
|
)
|
|
|
|
|
2020-01-08 17:30:53 +01:00
|
|
|
const hasErrors = !!errors?.length
|
|
|
|
const hasWarnings = !!warnings?.length
|
2019-06-26 20:54:23 +02:00
|
|
|
|
2019-02-16 17:09:49 +01:00
|
|
|
onEvent({
|
|
|
|
loading: false,
|
2019-06-26 20:54:23 +02:00
|
|
|
typeChecking: hasTypeChecking,
|
|
|
|
errors: hasErrors ? errors : null,
|
|
|
|
warnings: hasWarnings ? warnings : null,
|
2019-02-16 17:09:49 +01:00
|
|
|
})
|
2019-06-26 20:54:23 +02:00
|
|
|
|
|
|
|
const typePromise = tsMessagesPromise
|
|
|
|
|
|
|
|
if (!hasErrors && typePromise) {
|
|
|
|
typePromise.then(typeMessages => {
|
|
|
|
if (typePromise !== tsMessagesPromise) {
|
|
|
|
// a new compilation started so we don't care about this
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-09-11 19:06:48 +02:00
|
|
|
const reportFiles = stats.compilation.modules
|
2019-09-17 04:30:09 +02:00
|
|
|
.map((m: any) => (m.resource || '').replace(/\\/g, '/'))
|
2019-09-11 19:06:48 +02:00
|
|
|
.filter(Boolean)
|
|
|
|
|
|
|
|
let filteredErrors = typeMessages.errors
|
|
|
|
? typeMessages.errors
|
|
|
|
.filter(({ file }) => file && reportFiles.includes(file))
|
|
|
|
.map(({ message }) => message)
|
|
|
|
: null
|
|
|
|
if (filteredErrors && filteredErrors.length < 1) {
|
|
|
|
filteredErrors = null
|
|
|
|
}
|
|
|
|
let filteredWarnings = typeMessages.warnings
|
|
|
|
? typeMessages.warnings
|
|
|
|
.filter(({ file }) => file && reportFiles.includes(file))
|
|
|
|
.map(({ message }) => message)
|
|
|
|
: null
|
|
|
|
if (filteredWarnings && filteredWarnings.length < 1) {
|
|
|
|
filteredWarnings = null
|
|
|
|
}
|
|
|
|
|
|
|
|
stats.compilation.errors.push(...(filteredErrors || []))
|
|
|
|
stats.compilation.warnings.push(...(filteredWarnings || []))
|
2019-06-26 20:54:23 +02:00
|
|
|
onTypeChecked({
|
|
|
|
errors: stats.compilation.errors.length
|
|
|
|
? stats.compilation.errors
|
|
|
|
: null,
|
|
|
|
warnings: stats.compilation.warnings.length
|
|
|
|
? stats.compilation.warnings
|
|
|
|
: null,
|
|
|
|
})
|
|
|
|
|
|
|
|
onEvent({
|
|
|
|
loading: false,
|
|
|
|
typeChecking: false,
|
2019-09-11 19:06:48 +02:00
|
|
|
errors: filteredErrors,
|
2019-06-26 20:54:23 +02:00
|
|
|
warnings: hasWarnings
|
2019-09-11 19:06:48 +02:00
|
|
|
? [...warnings, ...(filteredWarnings || [])]
|
|
|
|
: filteredWarnings,
|
2019-06-26 20:54:23 +02:00
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
2019-02-16 17:09:49 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-06-26 20:54:23 +02:00
|
|
|
tapCompiler('client', client, enableTypeCheckingOnClient, status =>
|
2019-03-19 00:21:18 +01:00
|
|
|
buildStore.setState({ client: status })
|
2019-02-16 17:09:49 +01:00
|
|
|
)
|
2019-06-26 20:54:23 +02:00
|
|
|
tapCompiler('server', server, false, status =>
|
2019-03-19 00:21:18 +01:00
|
|
|
buildStore.setState({ server: status })
|
2019-02-16 17:09:49 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
previousClient = client
|
|
|
|
previousServer = server
|
|
|
|
}
|