diff --git a/packages/next/src/build/webpack/plugins/next-types-plugin/index.ts b/packages/next/src/build/webpack/plugins/next-types-plugin/index.ts index 3d98e30cf6501..41576f1e9dddc 100644 --- a/packages/next/src/build/webpack/plugins/next-types-plugin/index.ts +++ b/packages/next/src/build/webpack/plugins/next-types-plugin/index.ts @@ -107,11 +107,17 @@ if ('${method}' in entry) { '${method}' > >() + ${ + '' + // Adding void to support never return type without explicit return: + // e.g. notFound() will interrupt the execution but the handler return type is inferred as void. + // x-ref: https://github.com/microsoft/TypeScript/issues/16608#issuecomment-309327984 + } checkFields< Diff< { __tag__: '${method}', - __return_type__: Response | Promise + __return_type__: Response | void | never | Promise }, { __tag__: '${method}', @@ -428,7 +434,7 @@ declare module 'next/link' { import type { LinkProps as OriginalLinkProps } from 'next/dist/client/link.js' import type { AnchorHTMLAttributes, DetailedHTMLProps } from 'react' import type { UrlObject } from 'url' - + type LinkRestProps = Omit< Omit< DetailedHTMLProps< diff --git a/packages/next/src/trace/trace-uploader.ts b/packages/next/src/trace/trace-uploader.ts index f6753ed636a57..04158097b1cfd 100644 --- a/packages/next/src/trace/trace-uploader.ts +++ b/packages/next/src/trace/trace-uploader.ts @@ -2,7 +2,6 @@ import findUp from 'next/dist/compiled/find-up' import fsPromise from 'fs/promises' import child_process from 'child_process' import assert from 'assert' -// @ts-ignore import fetch from 'next/dist/compiled/node-fetch' import os from 'os' import { createInterface } from 'readline' diff --git a/packages/next/types/misc.d.ts b/packages/next/types/misc.d.ts index e98312232711d..fdd55470a27af 100644 --- a/packages/next/types/misc.d.ts +++ b/packages/next/types/misc.d.ts @@ -43,6 +43,12 @@ declare module 'next/dist/compiled/@next/react-refresh-utils/dist/ReactRefreshWe export = m } +declare module 'next/dist/compiled/node-fetch' { + import fetch from 'node-fetch' + export * from 'node-fetch' + export default fetch +} + declare module 'next/dist/compiled/node-html-parser' { export * from 'node-html-parser' } diff --git a/test/development/api-route-errors/app/pages/api/error.js b/test/development/api-route-errors/app/pages/api/error.js deleted file mode 100644 index 62270eec6b0b7..0000000000000 --- a/test/development/api-route-errors/app/pages/api/error.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function error(req, res) { - throw new Error() -} diff --git a/test/development/api-route-errors/app/pages/api/uncaught-exception.js b/test/development/api-route-errors/app/pages/api/uncaught-exception.js deleted file mode 100644 index 847bc854279f5..0000000000000 --- a/test/development/api-route-errors/app/pages/api/uncaught-exception.js +++ /dev/null @@ -1,6 +0,0 @@ -export default function uncaughtException(req, res) { - setTimeout(() => { - throw new Error('uncaught exception') - }, 0) - res.send('hello') -} diff --git a/test/development/api-route-errors/app/pages/api/unhandled-rejection.js b/test/development/api-route-errors/app/pages/api/unhandled-rejection.js deleted file mode 100644 index 3a17a62880aec..0000000000000 --- a/test/development/api-route-errors/app/pages/api/unhandled-rejection.js +++ /dev/null @@ -1,4 +0,0 @@ -export default function unhandledRejection(req, res) { - Promise.reject(new Error('unhandled rejection')) - res.send('hello') -} diff --git a/test/development/api-route-errors/index.test.ts b/test/development/api-route-errors/index.test.ts deleted file mode 100644 index a02e76893c833..0000000000000 --- a/test/development/api-route-errors/index.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import stripAnsi from 'next/dist/compiled/strip-ansi' -import { createNext, FileRef } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' -import { check, renderViaHTTP } from 'next-test-utils' -import { join } from 'path' - -describe('api-route-errors cli output', () => { - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: { - pages: new FileRef(join(__dirname, 'app/pages')), - }, - dependencies: {}, - }) - }) - afterAll(() => next.destroy()) - - test('error', async () => { - const outputIndex = next.cliOutput.length - await renderViaHTTP(next.url, '/api/error') - await check(() => next.cliOutput.slice(outputIndex), /pages\/api/) - - const output = stripAnsi(next.cliOutput.slice(outputIndex)) - // Location - expect(output).toContain('pages/api/error.js (2:8) @ error') - // Stack - expect(output).toContain('pages/api/error.js:6:11') - // Source code - expect(output).toContain('1 | export default function error(req, res) {') - }) - - test('uncaught exception', async () => { - const outputIndex = next.cliOutput.length - await renderViaHTTP(next.url, '/api/uncaught-exception') - await check(() => next.cliOutput.slice(outputIndex), /pages\/api/) - - const output = stripAnsi(next.cliOutput.slice(outputIndex)) - // Location - expect(output).toContain('pages/api/uncaught-exception.js (3:10) @ Timeout') - // Stack - expect(output).toContain('pages/api/uncaught-exception.js:7:15') - // Source code - expect(output).toContain( - '1 | export default function uncaughtException(req, res) {' - ) - }) - - test('unhandled rejection', async () => { - const outputIndex = next.cliOutput.length - await renderViaHTTP(next.url, '/api/unhandled-rejection') - await check(() => next.cliOutput.slice(outputIndex), /pages\/api/) - - const output = stripAnsi(next.cliOutput.slice(outputIndex)) - // Location - expect(output).toContain( - 'pages/api/unhandled-rejection.js (2:17) @ unhandledRejection' - ) - // Stack - expect(output).toContain('pages/api/unhandled-rejection.js:6:20') - // Source code - expect(output).toContain( - '1 | export default function unhandledRejection(req, res) ' - ) - }) -}) diff --git a/test/development/app-dir/app-routes-error/app/lowercase/delete/route.js b/test/development/app-dir/app-routes-error/app/lowercase/delete/route.js new file mode 100644 index 0000000000000..91a5652eeaa18 --- /dev/null +++ b/test/development/app-dir/app-routes-error/app/lowercase/delete/route.js @@ -0,0 +1 @@ +export { DELETE as delete } from '../../../hello' diff --git a/test/development/app-dir/app-routes-error/app/lowercase/get/route.js b/test/development/app-dir/app-routes-error/app/lowercase/get/route.js new file mode 100644 index 0000000000000..1cea4e8ff38fa --- /dev/null +++ b/test/development/app-dir/app-routes-error/app/lowercase/get/route.js @@ -0,0 +1 @@ +export { GET as get } from '../../../hello' diff --git a/test/development/app-dir/app-routes-error/app/lowercase/head/route.js b/test/development/app-dir/app-routes-error/app/lowercase/head/route.js new file mode 100644 index 0000000000000..07fbba4929097 --- /dev/null +++ b/test/development/app-dir/app-routes-error/app/lowercase/head/route.js @@ -0,0 +1 @@ +export { HEAD as head } from '../../../hello' diff --git a/test/development/app-dir/app-routes-error/app/lowercase/options/route.js b/test/development/app-dir/app-routes-error/app/lowercase/options/route.js new file mode 100644 index 0000000000000..d96dc0220013c --- /dev/null +++ b/test/development/app-dir/app-routes-error/app/lowercase/options/route.js @@ -0,0 +1 @@ +export { OPTIONS as options } from '../../../hello' diff --git a/test/development/app-dir/app-routes-error/app/lowercase/patch/route.js b/test/development/app-dir/app-routes-error/app/lowercase/patch/route.js new file mode 100644 index 0000000000000..3127323743bf9 --- /dev/null +++ b/test/development/app-dir/app-routes-error/app/lowercase/patch/route.js @@ -0,0 +1 @@ +export { PATCH as patch } from '../../../hello' diff --git a/test/development/app-dir/app-routes-error/app/lowercase/post/route.js b/test/development/app-dir/app-routes-error/app/lowercase/post/route.js new file mode 100644 index 0000000000000..10e243a309189 --- /dev/null +++ b/test/development/app-dir/app-routes-error/app/lowercase/post/route.js @@ -0,0 +1 @@ +export { POST as post } from '../../../hello' diff --git a/test/development/app-dir/app-routes-error/app/lowercase/put/route.js b/test/development/app-dir/app-routes-error/app/lowercase/put/route.js new file mode 100644 index 0000000000000..0faf51fe7d1dc --- /dev/null +++ b/test/development/app-dir/app-routes-error/app/lowercase/put/route.js @@ -0,0 +1 @@ +export { PUT as put } from '../../../hello' diff --git a/test/development/app-dir/app-routes-error/hello.js b/test/development/app-dir/app-routes-error/hello.js new file mode 100644 index 0000000000000..12c1f6e39474b --- /dev/null +++ b/test/development/app-dir/app-routes-error/hello.js @@ -0,0 +1,15 @@ +const helloHandler = async () => { + if (typeof WebSocket === 'undefined') { + throw new Error('missing WebSocket constructor!!') + } + + return new Response('hello, world') +} + +export const GET = helloHandler +export const HEAD = helloHandler +export const OPTIONS = helloHandler +export const POST = helloHandler +export const PUT = helloHandler +export const DELETE = helloHandler +export const PATCH = helloHandler diff --git a/test/development/app-dir/app-routes-error/index.test.ts b/test/development/app-dir/app-routes-error/index.test.ts new file mode 100644 index 0000000000000..8e7c6ec3e5ccf --- /dev/null +++ b/test/development/app-dir/app-routes-error/index.test.ts @@ -0,0 +1,40 @@ +import { createNextDescribe } from 'e2e-utils' +import { check } from 'next-test-utils' + +createNextDescribe( + 'app-dir - app routes errors', + { + files: __dirname, + }, + ({ next }) => { + describe('bad lowercase exports', () => { + it.each([ + ['get'], + ['head'], + ['options'], + ['post'], + ['put'], + ['delete'], + ['patch'], + ])( + 'should print an error when using lowercase %p in dev', + async (method: string) => { + await next.fetch('/lowercase/' + method) + + await check(() => { + expect(next.cliOutput).toContain( + `Detected lowercase method '${method}' in` + ) + expect(next.cliOutput).toContain( + `Export the uppercase '${method.toUpperCase()}' method name to fix this error.` + ) + expect(next.cliOutput).toMatch( + /Detected lowercase method '.+' in '.+\/route\.js'\. Export the uppercase '.+' method name to fix this error\./ + ) + return 'yes' + }, 'yes') + } + ) + }) + } +) diff --git a/test/e2e/app-dir/app-routes/app-custom-routes.test.ts b/test/e2e/app-dir/app-routes/app-custom-routes.test.ts index 83596d1bcc04a..cc151af7feea0 100644 --- a/test/e2e/app-dir/app-routes/app-custom-routes.test.ts +++ b/test/e2e/app-dir/app-routes/app-custom-routes.test.ts @@ -589,45 +589,25 @@ createNextDescribe( }) if (isNextDev) { - describe('lowercase exports', () => { - it.each([ - ['get'], - ['head'], - ['options'], - ['post'], - ['put'], - ['delete'], - ['patch'], - ])( - 'should print an error when using lowercase %p in dev', - async (method: string) => { - await next.fetch(basePath + '/lowercase/' + method) - - await check(() => { - expect(next.cliOutput).toContain( - `Detected lowercase method '${method}' in` - ) - expect(next.cliOutput).toContain( - `Export the uppercase '${method.toUpperCase()}' method name to fix this error.` - ) - expect(next.cliOutput).toMatch( - /Detected lowercase method '.+' in '.+\/route\.ts'\. Export the uppercase '.+' method name to fix this error\./ - ) - return 'yes' - }, 'yes') - } - ) - }) - describe('invalid exports', () => { + beforeAll(async () => { + await next.patchFile( + 'app/default/route.ts', + `\ + export { GET as default } from '../../handlers/hello' + ` + ) + }) + afterAll(async () => { + await next.deleteFile('app/default/route.ts') + }) it('should print an error when exporting a default handler in dev', async () => { - const res = await next.fetch(basePath + '/default') - - // Ensure we get a 405 (Method Not Allowed) response when there is no - // exported handler for the GET method. - expect(res.status).toEqual(405) + await check(async () => { + const res = await next.fetch(basePath + '/default') - await check(() => { + // Ensure we get a 405 (Method Not Allowed) response when there is no + // exported handler for the GET method. + expect(res.status).toEqual(405) expect(next.cliOutput).toMatch( /Detected default export in '.+\/route\.ts'\. Export a named export for each HTTP method instead\./ ) diff --git a/test/e2e/app-dir/app-routes/app/default/route.ts b/test/e2e/app-dir/app-routes/app/default/route.ts deleted file mode 100644 index 83aee80041deb..0000000000000 --- a/test/e2e/app-dir/app-routes/app/default/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { GET as default } from '../../handlers/hello' diff --git a/test/e2e/app-dir/app-routes/app/lowercase/delete/route.ts b/test/e2e/app-dir/app-routes/app/lowercase/delete/route.ts deleted file mode 100644 index c2f8249b31c69..0000000000000 --- a/test/e2e/app-dir/app-routes/app/lowercase/delete/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { DELETE as delete } from '../../../handlers/hello' diff --git a/test/e2e/app-dir/app-routes/app/lowercase/get/route.ts b/test/e2e/app-dir/app-routes/app/lowercase/get/route.ts deleted file mode 100644 index 7735df10a0b69..0000000000000 --- a/test/e2e/app-dir/app-routes/app/lowercase/get/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { GET as get } from '../../../handlers/hello' diff --git a/test/e2e/app-dir/app-routes/app/lowercase/head/route.ts b/test/e2e/app-dir/app-routes/app/lowercase/head/route.ts deleted file mode 100644 index 89f48c695a0d7..0000000000000 --- a/test/e2e/app-dir/app-routes/app/lowercase/head/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { HEAD as head } from '../../../handlers/hello' diff --git a/test/e2e/app-dir/app-routes/app/lowercase/options/route.ts b/test/e2e/app-dir/app-routes/app/lowercase/options/route.ts deleted file mode 100644 index 59b63b1a3ad39..0000000000000 --- a/test/e2e/app-dir/app-routes/app/lowercase/options/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { OPTIONS as options } from '../../../handlers/hello' diff --git a/test/e2e/app-dir/app-routes/app/lowercase/patch/route.ts b/test/e2e/app-dir/app-routes/app/lowercase/patch/route.ts deleted file mode 100644 index 91315bbf156db..0000000000000 --- a/test/e2e/app-dir/app-routes/app/lowercase/patch/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { PATCH as patch } from '../../../handlers/hello' diff --git a/test/e2e/app-dir/app-routes/app/lowercase/post/route.ts b/test/e2e/app-dir/app-routes/app/lowercase/post/route.ts deleted file mode 100644 index 2340ade758519..0000000000000 --- a/test/e2e/app-dir/app-routes/app/lowercase/post/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { POST as post } from '../../../handlers/hello' diff --git a/test/e2e/app-dir/app-routes/app/lowercase/put/route.ts b/test/e2e/app-dir/app-routes/app/lowercase/put/route.ts deleted file mode 100644 index b1cda20a07d42..0000000000000 --- a/test/e2e/app-dir/app-routes/app/lowercase/put/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { PUT as put } from '../../../handlers/hello' diff --git a/test/e2e/app-dir/app-routes/app/mixed-response/route.ts b/test/e2e/app-dir/app-routes/app/mixed-response/route.ts new file mode 100644 index 0000000000000..d1207a641b918 --- /dev/null +++ b/test/e2e/app-dir/app-routes/app/mixed-response/route.ts @@ -0,0 +1,12 @@ +import { redirect } from 'next/navigation' +import { NextResponse } from 'next/server' + +export async function GET() { + if (process.env.COND_1) { + return NextResponse.json({ a: '1' }) + } else if (process.env.COND_2) { + redirect('/no-response') + } else { + return new Response('3') + } +} diff --git a/test/e2e/app-dir/app-routes/helpers.ts b/test/e2e/app-dir/app-routes/helpers.ts index 77df81d91164d..3ac89053a2da8 100644 --- a/test/e2e/app-dir/app-routes/helpers.ts +++ b/test/e2e/app-dir/app-routes/helpers.ts @@ -60,10 +60,10 @@ type Cookies = { export function getRequestMeta( headersOrCookies: | Headers - | import('node-fetch').Headers | Cookies | ReadonlyHeaders | ReadonlyRequestCookies + | import('next/dist/compiled/node-fetch').Headers ): Record { const headerOrCookie = headersOrCookies.get(KEY) if (!headerOrCookie) return {} diff --git a/test/e2e/app-dir/app-routes/next.config.js b/test/e2e/app-dir/app-routes/next.config.js index 7670c4ad0ebb5..1bdf8c320e8a9 100644 --- a/test/e2e/app-dir/app-routes/next.config.js +++ b/test/e2e/app-dir/app-routes/next.config.js @@ -1,11 +1,7 @@ /** * @type {import('next').NextConfig} */ -const config = { - typescript: { - ignoreBuildErrors: true, - }, -} +const config = {} if (process.env.BASE_PATH) { config.basePath = process.env.BASE_PATH