diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 2ef7065c86ac7..5972191b47e7a 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -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 { @@ -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') @@ -174,7 +174,8 @@ export default async function build(dir: string, conf = null): Promise { 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) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index e70eff4e4127c..e80916944cac4 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -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' @@ -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 @@ -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' diff --git a/packages/next/lib/typescript/TypeScriptCompileError.ts b/packages/next/lib/typescript/TypeScriptCompileError.ts new file mode 100644 index 0000000000000..60021e37b58f8 --- /dev/null +++ b/packages/next/lib/typescript/TypeScriptCompileError.ts @@ -0,0 +1 @@ +export class TypeScriptCompileError extends Error {} diff --git a/packages/next/lib/typescript/diagnosticFormatter.ts b/packages/next/lib/typescript/diagnosticFormatter.ts new file mode 100644 index 0000000000000..5870bf7f36f8a --- /dev/null +++ b/packages/next/lib/typescript/diagnosticFormatter.ts @@ -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 { + 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 +} diff --git a/packages/next/lib/typescript/runTypeCheck.ts b/packages/next/lib/typescript/runTypeCheck.ts new file mode 100644 index 0000000000000..c789632162320 --- /dev/null +++ b/packages/next/lib/typescript/runTypeCheck.ts @@ -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 { + 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 } +} diff --git a/packages/next/lib/typescript/writeConfigurationDefaults.ts b/packages/next/lib/typescript/writeConfigurationDefaults.ts index 6cde93eaeb045..e6c6b19cdd1e1 100644 --- a/packages/next/lib/typescript/writeConfigurationDefaults.ts +++ b/packages/next/lib/typescript/writeConfigurationDefaults.ts @@ -56,6 +56,23 @@ function getDesiredCompilerOptions( return o } +export function getRequiredConfiguration( + ts: typeof import('typescript') +): Partial { + const res: Partial = {} + + 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, diff --git a/packages/next/lib/verifyTypeScriptSetup.ts b/packages/next/lib/verifyTypeScriptSetup.ts index 5beac612c604b..4ae5fa9971821 100644 --- a/packages/next/lib/verifyTypeScriptSetup.ts +++ b/packages/next/lib/verifyTypeScriptSetup.ts @@ -1,3 +1,4 @@ +import chalk from 'next/dist/compiled/chalk' import path from 'path' import { FatalTypeScriptError } from './typescript/FatalTypeScriptError' import { getTypeScriptIntent } from './typescript/getTypeScriptIntent' @@ -5,20 +6,23 @@ 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 { + pagesDir: string, + typeCheckPreflight: boolean +): Promise { 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 @@ -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) } diff --git a/packages/next/package.json b/packages/next/package.json index e3a59cad7984a..3a748fe71a5cc 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -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", diff --git a/packages/next/server/next-dev-server.ts b/packages/next/server/next-dev-server.ts index 70a5c7afa8baa..9253473284672 100644 --- a/packages/next/server/next-dev-server.ts +++ b/packages/next/server/next-dev-server.ts @@ -252,7 +252,7 @@ export default class DevServer extends Server { } async prepare(): Promise { - await verifyTypeScriptSetup(this.dir, this.pagesDir!) + await verifyTypeScriptSetup(this.dir, this.pagesDir!, false) await this.loadCustomRoutes() if (this.customRoutes) { diff --git a/packages/next/types/misc.d.ts b/packages/next/types/misc.d.ts index f909b4ca46392..d49a6a0b8cf2a 100644 --- a/packages/next/types/misc.d.ts +++ b/packages/next/types/misc.d.ts @@ -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 - >( - settings: T - ) => T & { - resolveModuleNameModule?: string - resolveTypeReferenceDirectiveModule?: string - } - } + class PnpWebpackPlugin extends webpack.Plugin {} export = PnpWebpackPlugin } diff --git a/test/integration/typescript-ignore-errors/test/index.test.js b/test/integration/typescript-ignore-errors/test/index.test.js index 948730b4d0b7c..d5e9da88f135f 100644 --- a/test/integration/typescript-ignore-errors/test/index.test.js +++ b/test/integration/typescript-ignore-errors/test/index.test.js @@ -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'") } } diff --git a/yarn.lock b/yarn.lock index 3b43e1897da34..a1ed5e50377ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3493,7 +3493,7 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -anymatch@^3.0.3, anymatch@~3.1.1: +anymatch@^3.0.3: version "3.1.1" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" dependencies: @@ -3771,7 +3771,7 @@ aws4@^1.6.0, aws4@^1.8.0: version "1.9.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.0.tgz#24390e6ad61386b0a747265754d2a17219de862c" -babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: +babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" dependencies: @@ -3988,10 +3988,6 @@ binary-extensions@^1.0.0: version "1.13.1" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" -binary-extensions@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" - bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -4086,7 +4082,7 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1, braces@~3.0.2: +braces@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" dependencies: @@ -4648,20 +4644,6 @@ chokidar@^1.7.0: optionalDependencies: fsevents "^1.0.0" -chokidar@^3.3.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450" - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.3.0" - optionalDependencies: - fsevents "~2.1.2" - chownr@^1.1.1, chownr@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142" @@ -7146,19 +7128,6 @@ forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" -fork-ts-checker-webpack-plugin@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-3.1.1.tgz#a1642c0d3e65f50c2cc1742e9c0a80f441f86b19" - dependencies: - babel-code-frame "^6.22.0" - chalk "^2.4.1" - chokidar "^3.3.0" - micromatch "^3.1.10" - minimatch "^3.0.4" - semver "^5.6.0" - tapable "^1.0.0" - worker-rpc "^0.1.0" - form-data@^2.3.1: version "2.5.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" @@ -7287,10 +7256,6 @@ fsevents@^2.1.2: version "2.1.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" -fsevents@~2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805" - fstream@^1.0.0, fstream@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" @@ -7518,7 +7483,7 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@^5.0.0, glob-parent@~5.1.0: +glob-parent@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2" dependencies: @@ -8286,12 +8251,6 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - dependencies: - binary-extensions "^2.0.0" - is-buffer@^1.0.2, is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -8439,7 +8398,7 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: +is-glob@^4.0.0, is-glob@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" dependencies: @@ -10460,10 +10419,6 @@ microbundle@0.11.0: tslib "^1.9.0" typescript ">=2.8.3" -microevent.ts@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0" - micromatch@^2.1.5, micromatch@^2.3.11: version "2.3.11" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" @@ -10986,7 +10941,7 @@ normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" -normalize-path@^3.0.0, normalize-path@~3.0.0: +normalize-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -11800,7 +11755,7 @@ performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" -picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.0.7: +picomatch@^2.0.4, picomatch@^2.0.5: version "2.2.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a" @@ -13358,12 +13313,6 @@ readdirp@^2.0.0, readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" -readdirp@~3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.3.0.tgz#984458d13a1e42e2e9f5841b129e162f369aff17" - dependencies: - picomatch "^2.0.7" - realpath-native@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" @@ -16240,12 +16189,6 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" -worker-rpc@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/worker-rpc/-/worker-rpc-0.1.1.tgz#cb565bd6d7071a8f16660686051e969ad32f54d5" - dependencies: - microevent.ts "~0.1.1" - wrap-ansi@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"