diff --git a/packages/next/README.md b/packages/next/README.md index f637710fe75418ed3fa7aa9a931b85d3c81fcc3c..c20c3fd7817c1f62538df2200270e6e707484269 100644 --- a/packages/next/README.md +++ b/packages/next/README.md @@ -2316,6 +2316,22 @@ To learn more about TypeScript checkout its [documentation](https://www.typescri > **Note**: Next.js does not enable TypeScript's `strict` mode by default. > When you feel comfortable with TypeScript, you may turn this option on in your `tsconfig.json`. +> **Note**: By default, Next.js reports TypeScript errors during development for pages you are actively working on. +> TypeScript errors for inactive pages do not block the development process. +> Trying to run `next build` for an app that has TypeScript errors on any page will fail. +> +> If you don't want to leverage this behavior and prefer to do type checks manually, set the following options in your `next.config.js`: +> +> ```js +> // next.config.js +> module.exports = { +> typescript: { +> ignoreDevErrors: true, +> ignoreBuildErrors: true, +> }, +> } +> ``` + ### Exported types Next.js provides `NextPage` type that can be used for pages in the `pages` directory. `NextPage` adds definitions for [`getInitialProps`](#fetching-data-and-component-lifecycle) so that it can be used without any extra typing needed. diff --git a/packages/next/build/output/store.ts b/packages/next/build/output/store.ts index fdc0f24480502d08f1e9cf02feb4bbd723231bf2..185e29e81e2e3327e069a8521e78bd359ba18aa9 100644 --- a/packages/next/build/output/store.ts +++ b/packages/next/build/output/store.ts @@ -78,7 +78,7 @@ store.subscribe(state => { } if (state.typeChecking) { - Log.info('bundled successfully, waiting for typecheck results ...') + Log.info('bundled successfully, waiting for typecheck results...') return } diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index fd7f1ce58e11a4abd0a7cca3f490a300a5f6a1d8..ec50bea7f612c911aed1520e1590243a1ccc22d6 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -126,6 +126,9 @@ export default async function getBaseWebpackConfig( const useTypeScript = Boolean( typeScriptPath && (await fileExists(tsConfigPath)) ) + const ignoreTypeScriptErrors = dev + ? config.typescript && config.typescript.ignoreDevErrors + : config.typescript && config.typescript.ignoreBuildErrors const resolveConfig = { // Disable .mjs for node_modules bundling @@ -845,6 +848,7 @@ export default async function getBaseWebpackConfig( }), !isServer && useTypeScript && + !ignoreTypeScriptErrors && new ForkTsCheckerWebpackPlugin( PnpWebpackPlugin.forkTsCheckerOptions({ typescript: typeScriptPath, diff --git a/packages/next/server/hot-reloader.ts b/packages/next/server/hot-reloader.ts index 4533aa8d49f25213248d798a7f92e27d1010c84c..77c0d6b765331e702fb8b5ea6401e5be1471b923 100644 --- a/packages/next/server/hot-reloader.ts +++ b/packages/next/server/hot-reloader.ts @@ -349,11 +349,13 @@ export default class HotReloader { async prepareBuildTools(multiCompiler: webpack.MultiCompiler) { const tsConfigPath = join(this.dir, 'tsconfig.json') const useTypeScript = await fileExists(tsConfigPath) + const ignoreTypeScriptErrors = + this.config.typescript && this.config.typescript.ignoreDevErrors watchCompilers( multiCompiler.compilers[0], multiCompiler.compilers[1], - useTypeScript, + useTypeScript && !ignoreTypeScriptErrors, ({ errors, warnings }) => this.send('typeChecked', { errors, warnings }) ) diff --git a/test/integration/typescript-ignore-errors/next.config.js b/test/integration/typescript-ignore-errors/next.config.js new file mode 100644 index 0000000000000000000000000000000000000000..4ba52ba2c8df6758685c8f65f490306b5c44eb76 --- /dev/null +++ b/test/integration/typescript-ignore-errors/next.config.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/test/integration/typescript-ignore-errors/pages/index.tsx b/test/integration/typescript-ignore-errors/pages/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0e404ead32ef62b1a5b48717ca7a8c2b362d3223 --- /dev/null +++ b/test/integration/typescript-ignore-errors/pages/index.tsx @@ -0,0 +1,2 @@ +// Below type error is intentional, it helps check typescript → ignoreDevErrors / ignoreBuildErrors flags in next.config.js +export default (): boolean => 'Index page' diff --git a/test/integration/typescript-ignore-errors/test/index.test.js b/test/integration/typescript-ignore-errors/test/index.test.js new file mode 100644 index 0000000000000000000000000000000000000000..e05d16a7b9555fdb1c3eaff74fa8c1af35d28ed3 --- /dev/null +++ b/test/integration/typescript-ignore-errors/test/index.test.js @@ -0,0 +1,84 @@ +/* eslint-env jest */ +/* global jasmine */ +import { join } from 'path' +import { + renderViaHTTP, + nextBuild, + findPort, + launchApp, + killApp, + File +} from 'next-test-utils' + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2 + +const appDir = join(__dirname, '..') +const nextConfigFile = new File(join(appDir, 'next.config.js')) + +describe('TypeScript with error handling options', () => { + for (const ignoreDevErrors of [false, true]) { + for (const ignoreBuildErrors of [false, true]) { + describe(`ignoreDevErrors: ${ignoreDevErrors}, ignoreBuildErrors: ${ignoreBuildErrors}`, () => { + beforeAll(() => { + const nextConfig = { + typescript: { ignoreDevErrors, ignoreBuildErrors } + } + nextConfigFile.replace('{}', JSON.stringify(nextConfig)) + }) + afterAll(() => { + nextConfigFile.restore() + }) + + it( + ignoreDevErrors + ? 'Next renders the page in dev despite type errors' + : 'Next dev does not render the page in dev because of type errors', + async () => { + let app + let output = '' + try { + const appPort = await findPort() + app = await launchApp(appDir, appPort, { + onStdout: msg => (output += msg), + onStderr: msg => (output += msg) + }) + await renderViaHTTP(appPort, '') + + if (ignoreDevErrors) { + expect(output).not.toContain('waiting for typecheck results...') + expect(output).not.toContain("not assignable to type 'boolean'") + } else { + expect(output).toContain('waiting for typecheck results...') + expect(output).toContain("not assignable to type 'boolean'") + } + } finally { + await killApp(app) + } + } + ) + + it( + ignoreBuildErrors + ? 'Next builds the application despite type errors' + : 'Next fails to build the application despite type errors', + async () => { + const { stdout, stderr } = await nextBuild(appDir, [], { + stdout: true, + stderr: true + }) + + if (ignoreBuildErrors) { + expect(stdout).toContain('Compiled successfully') + expect(stderr).not.toContain('Failed to compile.') + expect(stderr).not.toContain("not assignable to type 'boolean'") + } else { + expect(stdout).not.toContain('Compiled successfully') + expect(stderr).toContain('Failed to compile.') + expect(stderr).toContain("not assignable to type 'boolean'") + } + } + ) + }) + } + } +}) diff --git a/test/integration/typescript-ignore-errors/tsconfig.json b/test/integration/typescript-ignore-errors/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..1442a271910703dd5060f7a7393d0e478c8388b5 --- /dev/null +++ b/test/integration/typescript-ignore-errors/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "module": "esnext", + "jsx": "preserve", + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true + }, + "exclude": ["node_modules"], + "include": ["next-env.d.ts", "pages"] +} diff --git a/test/integration/typescript/test/index.test.js b/test/integration/typescript/test/index.test.js index 13eb5c41c3b91328dd69a0adbcea7433efe0c13e..8dac7383e58d9da2aa2cda43ed047bb2f502514f 100644 --- a/test/integration/typescript/test/index.test.js +++ b/test/integration/typescript/test/index.test.js @@ -17,6 +17,11 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2 const appDir = join(__dirname, '..') let appPort let app +let output + +const handleOutput = msg => { + output += msg +} async function get$ (path, query) { const html = await renderViaHTTP(appPort, path, query) @@ -26,8 +31,12 @@ async function get$ (path, query) { describe('TypeScript Features', () => { describe('default behavior', () => { beforeAll(async () => { + output = '' appPort = await findPort() - app = await launchApp(appDir, appPort) + app = await launchApp(appDir, appPort, { + onStdout: handleOutput, + onStderr: handleOutput + }) }) afterAll(() => killApp(app)) @@ -36,6 +45,10 @@ describe('TypeScript Features', () => { expect($('body').text()).toMatch(/Hello World/) }) + it('should report type checking to stdout', async () => { + expect(output).toContain('waiting for typecheck results...') + }) + it('should not fail to render when an inactive page has an error', async () => { await killApp(app) let evilFile = join(appDir, 'pages', 'evil.tsx')