Skip to content

Commit

Permalink
Replace fork-ts-checker-webpack-plugin with faster alternative
Browse files Browse the repository at this point in the history
  • Loading branch information
Timer committed May 29, 2020
1 parent a7ae54d commit 1783384
Show file tree
Hide file tree
Showing 11 changed files with 183 additions and 107 deletions.
9 changes: 5 additions & 4 deletions packages/next/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { __ApiPreviewProps } from '../next-server/server/api-utils'
import loadConfig, {
isTargetLikeServerless,
} from '../next-server/server/config'
import { BuildManifest } from '../next-server/server/get-page-files'
import { normalizePagePath } from '../next-server/server/normalize-page-path'
import * as ciEnvironment from '../telemetry/ci-info'
import {
Expand All @@ -64,17 +65,16 @@ import createSpinner from './spinner'
import {
collectPages,
getJsPageSizeInKb,
getNamedExports,
hasCustomGetInitialProps,
isPageStatic,
PageInfo,
printCustomRoutes,
printTreeView,
getNamedExports,
} from './utils'
import getBaseWebpackConfig from './webpack-config'
import { writeBuildId } from './write-build-id'
import { PagesManifest } from './webpack/plugins/pages-manifest-plugin'
import { BuildManifest } from '../next-server/server/get-page-files'
import { writeBuildId } from './write-build-id'

const staticCheckWorker = require.resolve('./utils')

Expand Down Expand Up @@ -174,7 +174,8 @@ export default async function build(dir: string, conf = null): Promise<void> {

eventNextPlugins(path.resolve(dir)).then((events) => telemetry.record(events))

await verifyTypeScriptSetup(dir, pagesDir)
const ignoreTypeScriptErrors = Boolean(config.typescript?.ignoreBuildErrors)
await verifyTypeScriptSetup(dir, pagesDir, !ignoreTypeScriptErrors)

try {
await promises.stat(publicDir)
Expand Down
21 changes: 0 additions & 21 deletions packages/next/build/webpack-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import ReactRefreshWebpackPlugin from '@next/react-refresh-utils/ReactRefreshWebpackPlugin'
import crypto from 'crypto'
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'
import { readFileSync } from 'fs'
import chalk from 'next/dist/compiled/chalk'
import TerserPlugin from 'next/dist/compiled/terser-webpack-plugin'
Expand Down Expand Up @@ -247,8 +246,6 @@ export default async function getBaseWebpackConfig(
const useTypeScript = Boolean(
typeScriptPath && (await fileExists(tsConfigPath))
)
const ignoreTypeScriptErrors =
dev || Boolean(config.typescript?.ignoreBuildErrors)

let jsConfig
// jsconfig is a subset of tsconfig
Expand Down Expand Up @@ -967,28 +964,10 @@ export default async function getBaseWebpackConfig(
new ProfilingPlugin({
tracer,
}),
!dev &&
!isServer &&
useTypeScript &&
!ignoreTypeScriptErrors &&
new ForkTsCheckerWebpackPlugin(
PnpWebpackPlugin.forkTsCheckerOptions({
typescript: typeScriptPath,
async: false,
useTypescriptIncrementalApi: true,
checkSyntacticErrors: true,
tsconfig: tsConfigPath,
reportFiles: ['**', '!**/__tests__/**', '!**/?(*.)(spec|test).*'],
compilerOptions: { isolatedModules: true, noEmit: true },
silent: true,
formatter: 'codeframe',
})
),
config.experimental.modern &&
!isServer &&
!dev &&
new NextEsmPlugin({
excludedPlugins: ['ForkTsCheckerWebpackPlugin'],
filename: (getFileName: Function | string) => (...args: any[]) => {
const name =
typeof getFileName === 'function'
Expand Down
1 change: 1 addition & 0 deletions packages/next/lib/typescript/TypeScriptCompileError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class TypeScriptCompileError extends Error {}
75 changes: 75 additions & 0 deletions packages/next/lib/typescript/diagnosticFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { codeFrameColumns } from '@babel/code-frame'
import chalk from 'next/dist/compiled/chalk'
import path from 'path'

export enum DiagnosticCategory {
Warning = 0,
Error = 1,
Suggestion = 2,
Message = 3,
}

export async function getFormattedDiagnostic(
ts: typeof import('typescript'),
baseDir: string,
diagnostic: import('typescript').Diagnostic
): Promise<string> {
let message = ''

const reason = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')
const category = diagnostic.category
switch (category) {
// Warning
case DiagnosticCategory.Warning: {
message += chalk.yellow.bold('Type warning') + ': '
break
}
// Error
case DiagnosticCategory.Error: {
message += chalk.red.bold('Type error') + ': '
break
}
// 2 = Suggestion, 3 = Message
case DiagnosticCategory.Suggestion:
case DiagnosticCategory.Message:
default: {
message += chalk.cyan.bold(category === 2 ? 'Suggestion' : 'Info') + ': '
break
}
}
message += reason + '\n'

if (diagnostic.file) {
const pos = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!)
const line = pos.line + 1
const character = pos.character + 1

let fileName = path.posix.normalize(
path.relative(baseDir, diagnostic.file.fileName).replace(/\\/, '/')
)
if (!fileName.startsWith('.')) {
fileName = './' + fileName
}

message =
chalk.cyan(fileName) +
':' +
chalk.yellow(line.toString()) +
':' +
chalk.yellow(character.toString()) +
'\n' +
message

message +=
'\n' +
codeFrameColumns(
diagnostic.file.getFullText(diagnostic.file.getSourceFile()),
{
start: { line: line, column: character },
},
{ forceColor: true }
)
}

return message
}
57 changes: 57 additions & 0 deletions packages/next/lib/typescript/runTypeCheck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
DiagnosticCategory,
getFormattedDiagnostic,
} from './diagnosticFormatter'
import { getTypeScriptConfiguration } from './getTypeScriptConfiguration'
import { TypeScriptCompileError } from './TypeScriptCompileError'
import { getRequiredConfiguration } from './writeConfigurationDefaults'

export interface TypeCheckResult {
hasWarnings: boolean
warnings?: string[]
}

export async function runTypeCheck(
ts: typeof import('typescript'),
baseDir: string,
tsConfigPath: string
): Promise<TypeCheckResult> {
const effectiveConfiguration = await getTypeScriptConfiguration(
ts,
tsConfigPath
)

if (effectiveConfiguration.fileNames.length < 1) {
return { hasWarnings: false }
}
const requiredConfig = getRequiredConfiguration(ts)

const program = ts.createProgram(effectiveConfiguration.fileNames, {
...effectiveConfiguration.options,
...requiredConfig,
noEmit: true,
})
const result = program.emit()

const allDiagnostics = ts
.getPreEmitDiagnostics(program)
.concat(result.diagnostics)

const firstError =
allDiagnostics.find(
(d) => d.category === DiagnosticCategory.Error && Boolean(d.file)
) ?? allDiagnostics.find((d) => d.category === DiagnosticCategory.Error)

if (firstError) {
throw new TypeScriptCompileError(
await getFormattedDiagnostic(ts, baseDir, firstError)
)
}

const warnings = await Promise.all(
allDiagnostics
.filter((d) => d.category === DiagnosticCategory.Warning)
.map((d) => getFormattedDiagnostic(ts, baseDir, d))
)
return { hasWarnings: true, warnings }
}
17 changes: 17 additions & 0 deletions packages/next/lib/typescript/writeConfigurationDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,23 @@ function getDesiredCompilerOptions(
return o
}

export function getRequiredConfiguration(
ts: typeof import('typescript')
): Partial<import('typescript').CompilerOptions> {
const res: Partial<import('typescript').CompilerOptions> = {}

const desiredCompilerOptions = getDesiredCompilerOptions(ts)
for (const optionKey of Object.keys(desiredCompilerOptions)) {
const ev = desiredCompilerOptions[optionKey]
if (!('value' in ev)) {
continue
}
res[optionKey] = ev.parsedValue ?? ev.value
}

return res
}

export async function writeConfigurationDefaults(
ts: typeof import('typescript'),
tsConfigPath: string,
Expand Down
24 changes: 19 additions & 5 deletions packages/next/lib/verifyTypeScriptSetup.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
import chalk from 'next/dist/compiled/chalk'
import path from 'path'
import { FatalTypeScriptError } from './typescript/FatalTypeScriptError'
import { getTypeScriptIntent } from './typescript/getTypeScriptIntent'
import {
hasNecessaryDependencies,
NecessaryDependencies,
} from './typescript/hasNecessaryDependencies'
import { runTypeCheck, TypeCheckResult } from './typescript/runTypeCheck'
import { TypeScriptCompileError } from './typescript/TypeScriptCompileError'
import { writeAppTypeDeclarations } from './typescript/writeAppTypeDeclarations'
import { writeConfigurationDefaults } from './typescript/writeConfigurationDefaults'

export async function verifyTypeScriptSetup(
dir: string,
pagesDir: string
): Promise<void> {
pagesDir: string,
typeCheckPreflight: boolean
): Promise<TypeCheckResult | boolean> {
const tsConfigPath = path.join(dir, 'tsconfig.json')

try {
// Check if the project uses TypeScript:
const intent = await getTypeScriptIntent(dir, pagesDir)
if (!intent) {
return
return false
}
const firstTimeSetup = intent.firstTimeSetup

Expand All @@ -35,9 +39,19 @@ export async function verifyTypeScriptSetup(
// Write out the necessary `next-env.d.ts` file to correctly register
// Next.js' types:
await writeAppTypeDeclarations(dir)

if (typeCheckPreflight) {
// Verify the project passes type-checking before we go to webpack phase:
return await runTypeCheck(ts, dir, tsConfigPath)
}
return true
} catch (err) {
// This is a special error that should not show its stack trace:
if (err instanceof FatalTypeScriptError) {
// These are special errors that should not show a stack trace:
if (err instanceof TypeScriptCompileError) {
console.error(chalk.red('Failed to compile.\n'))
console.error(err.message)
process.exit(1)
} else if (err instanceof FatalTypeScriptError) {
console.error(err.message)
process.exit(1)
}
Expand Down
1 change: 0 additions & 1 deletion packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@
"chokidar": "2.1.8",
"css-loader": "3.5.3",
"find-cache-dir": "3.3.1",
"fork-ts-checker-webpack-plugin": "3.1.1",
"jest-worker": "24.9.0",
"loader-utils": "2.0.0",
"mini-css-extract-plugin": "0.8.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/next/server/next-dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ export default class DevServer extends Server {
}

async prepare(): Promise<void> {
await verifyTypeScriptSetup(this.dir, this.pagesDir!)
await verifyTypeScriptSetup(this.dir, this.pagesDir!, false)
await this.loadCustomRoutes()

if (this.customRoutes) {
Expand Down
12 changes: 1 addition & 11 deletions packages/next/types/misc.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,18 +249,8 @@ declare module 'autodll-webpack-plugin' {

declare module 'pnp-webpack-plugin' {
import webpack from 'webpack'
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'

class PnpWebpackPlugin extends webpack.Plugin {
static forkTsCheckerOptions: <
T extends Partial<ForkTsCheckerWebpackPlugin.Options>
>(
settings: T
) => T & {
resolveModuleNameModule?: string
resolveTypeReferenceDirectiveModule?: string
}
}
class PnpWebpackPlugin extends webpack.Plugin {}

export = PnpWebpackPlugin
}
Expand Down
Loading

0 comments on commit 1783384

Please sign in to comment.