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

Replace fork-ts-checker-webpack-plugin with faster alternative #13529

Merged
merged 4 commits into from
May 29, 2020
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
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 @@ -972,28 +969,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
}
59 changes: 59 additions & 0 deletions packages/next/lib/typescript/runTypeCheck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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 regexIgnoredFile = /[\\/]__(?:tests|mocks)__[\\/]|(?:spec|test)\.[^\\/]+$/
const allDiagnostics = ts
.getPreEmitDiagnostics(program)
.concat(result.diagnostics)
.filter((d) => !(d.file && regexIgnoredFile.test(d.file.fileName)))

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
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ describe('TypeScript with error handling options', () => {
} else {
expect(stdout).not.toContain('Compiled successfully')
expect(stderr).toContain('Failed to compile.')
expect(stderr).toContain('./pages/index.tsx:2:31')
expect(stderr).toContain("not assignable to type 'boolean'")
}
}
Expand Down
Loading