diff --git a/.github/workflows/cancel.yml b/.github/workflows/cancel.yml new file mode 100644 index 0000000000000..ce58d11cab7c9 --- /dev/null +++ b/.github/workflows/cancel.yml @@ -0,0 +1,17 @@ +name: Cancel +on: + pull_request_target: + types: + - edited + - synchronize + +jobs: + cancel: + name: 'Cancel Previous Runs' + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - uses: styfle/cancel-workflow-action@0.5.0 + with: + workflow_id: 444921, 444987 + access_token: ${{ github.token }} diff --git a/errors/next-export-no-build-id.md b/errors/next-export-no-build-id.md new file mode 100644 index 0000000000000..affed88c30803 --- /dev/null +++ b/errors/next-export-no-build-id.md @@ -0,0 +1,10 @@ +# Could not find a production build + +#### Why This Error Occurred + +When running `next export` a production build is needed. + +#### Possible Ways to Fix It + +- Run `next build` to create a production build before running `next export`. +- If your intention was to run the development server run `next dev` instead. diff --git a/errors/production-start-no-build-id.md b/errors/production-start-no-build-id.md new file mode 100644 index 0000000000000..c6c9bfdb93f57 --- /dev/null +++ b/errors/production-start-no-build-id.md @@ -0,0 +1,10 @@ +# Could not find a production build + +#### Why This Error Occurred + +When running `next start` or a custom server in production mode a production build is needed. + +#### Possible Ways to Fix It + +- Run `next build` to create a production build before booting up the production server. +- If your intention was to run the development server run `next dev` instead. diff --git a/examples/with-apollo/pages/client-only.js b/examples/with-apollo/pages/client-only.js index 29b450a1e3612..d61675d3ed950 100644 --- a/examples/with-apollo/pages/client-only.js +++ b/examples/with-apollo/pages/client-only.js @@ -8,7 +8,7 @@ const ClientOnlyPage = (props) => (
- ℹ️ This page shows how use Apollo only in the client. If you{' '} + ℹ️ This page shows how to use Apollo only in the client. If you{' '} reload this page, you will see a loader since Apollo didn't fetch any data on the server. This is useful when the page doesn't have SEO requirements or blocking data fetching requirements. diff --git a/examples/with-docker/Dockerfile.multistage b/examples/with-docker/Dockerfile.multistage index c877cd18753cc..a69f05c2c6526 100644 --- a/examples/with-docker/Dockerfile.multistage +++ b/examples/with-docker/Dockerfile.multistage @@ -14,7 +14,7 @@ RUN yarn install --production --frozen-lockfile # Stage 2: And then copy over node_modules, etc from that stage to the smaller base image -FROM mhart/alpine-node:base as production +FROM mhart/alpine-node:slim as production WORKDIR /app diff --git a/examples/with-electron/README.md b/examples/with-electron/README.md index 4c46b82fc230c..384df5a1d4907 100644 --- a/examples/with-electron/README.md +++ b/examples/with-electron/README.md @@ -1,8 +1,8 @@ # Electron application example -This example show how you can use Next.js inside an Electron application to avoid a lot of configuration, use Next.js router as view and use server-render to speed up the initial render of the application. +This example shows how you can use Next.js inside an Electron application to avoid a lot of configuration. It uses the Next.js router as view and server-render to speed up the initial render of the application. -For development it's going to run a HTTP server and let Next.js handle routing. In production it use `next export` to pre-generate HTML static files and use them in your app instead of running an HTTP server. +For development it's going to run an HTTP server and let Next.js handle routing. In production it uses `next export` to pre-generate HTML static files and uses them in your app instead of running an HTTP server. **You can find a detailed documentation about how to build Electron apps with Next.js [here](https://leo.im/2017/electron-next)!** diff --git a/examples/with-facebook-pixel/README.md b/examples/with-facebook-pixel/README.md index e45a7183ccdef..516dfee2ebe26 100644 --- a/examples/with-facebook-pixel/README.md +++ b/examples/with-facebook-pixel/README.md @@ -1,6 +1,6 @@ ## Example app using Facebook Pixel -This example shows how to use Next.js along with Facebook Pixel. A [custom `App`](https://nextjs.org/docs/advanced-features/custom-app) is used to track route changes and send page views to Facebook Pixel. This example uses [react-facebook-pixel](https://www.npmjs.com/package/react-facebook-pixel). +This example shows how to use Next.js along with Facebook Pixel. A custom [\_document](https://nextjs.org/docs/advanced-features/custom-document) is used to inject [base code](https://developers.facebook.com/docs/facebook-pixel/implementation/?locale=en_US). A [\_app](https://nextjs.org/docs/advanced-features/custom-app) is used to track route changes and send page views to Facebook Pixel. ## Deploy your own @@ -24,6 +24,6 @@ Next, copy the `.env.local.example` file in this directory to `.env.local` (whic cp .env.local.example .env.local ``` -Set the `NEXT_PUBLIC_FACEBOOK_PIXEL_ID` variable in `.env.local` to match your facebook app's pixel ID. If not specified, tracking will be disabled. +Set the `NEXT_PUBLIC_FACEBOOK_PIXEL_ID` variable in `.env.local` to match your facebook app's pixel ID. Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). diff --git a/examples/with-facebook-pixel/package.json b/examples/with-facebook-pixel/package.json index d907d0ca638be..8f291e6496380 100644 --- a/examples/with-facebook-pixel/package.json +++ b/examples/with-facebook-pixel/package.json @@ -9,8 +9,7 @@ "dependencies": { "next": "latest", "react": "^16.13.1", - "react-dom": "^16.13.1", - "react-facebook-pixel": "^1.0.3" + "react-dom": "^16.13.1" }, "license": "MIT" } diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 8accd67c3ac00..df932988f8d40 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -364,15 +364,6 @@ export default async function getBaseWebpackConfig( ...nodePathList, // Support for NODE_PATH environment variable ], alias: { - // These aliases make sure the wrapper module is not included in the bundles - // Which makes bundles slightly smaller, but also skips parsing a module that we know will result in this alias - 'next/head': 'next/dist/next-server/lib/head.js', - 'next/router': 'next/dist/client/router.js', - 'next/experimental-script': config.experimental.scriptLoader - ? 'next/dist/client/experimental-script.js' - : '', - 'next/config': 'next/dist/next-server/lib/runtime-config.js', - 'next/dynamic': 'next/dist/next-server/lib/dynamic.js', next: NEXT_PROJECT_ROOT, ...(isWebpack5 && !isServer ? { diff --git a/packages/next/cli/next-export.ts b/packages/next/cli/next-export.ts index f13533bd26095..272877f9207d7 100755 --- a/packages/next/cli/next-export.ts +++ b/packages/next/cli/next-export.ts @@ -62,7 +62,7 @@ const nextExport: cliCommand = (argv) => { exportApp(dir, options) .then(() => { - printAndExit('Export successful', 0) + printAndExit(`Export successful. Files written to ${options.outdir}`, 0) }) .catch((err) => { printAndExit(err) diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts index a0c5d26f7d106..010651fdf59cb 100644 --- a/packages/next/export/index.ts +++ b/packages/next/export/index.ts @@ -164,9 +164,11 @@ export default async function exportApp( Log.info(`using build directory: ${distDir}`) } - if (!existsSync(distDir)) { + const buildIdFile = join(distDir, BUILD_ID_FILE) + + if (!existsSync(buildIdFile)) { throw new Error( - `Build directory ${distDir} does not exist. Make sure you run "next build" before running "next start" or "next export".` + `Could not find a production build in the '${distDir}' directory. Try building your app with 'next build' before starting the static export. https://err.sh/vercel/next.js/next-export-no-build-id` ) } @@ -186,7 +188,7 @@ export default async function exportApp( ) } - const buildId = readFileSync(join(distDir, BUILD_ID_FILE), 'utf8') + const buildId = readFileSync(buildIdFile, 'utf8') const pagesManifest = !options.pages && (require(join( diff --git a/packages/next/lib/load-custom-routes.ts b/packages/next/lib/load-custom-routes.ts index bf7372db39a14..9b5fcb9e3b1de 100644 --- a/packages/next/lib/load-custom-routes.ts +++ b/packages/next/lib/load-custom-routes.ts @@ -466,12 +466,14 @@ export default async function loadCustomRoutes( destination: '/:file', permanent: true, locale: config.i18n ? false : undefined, + internal: true, } as Redirect, { source: '/:notfile((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/\\.]+)', destination: '/:notfile/', permanent: true, locale: config.i18n ? false : undefined, + internal: true, } as Redirect ) if (config.basePath) { @@ -481,6 +483,7 @@ export default async function loadCustomRoutes( permanent: true, basePath: false, locale: config.i18n ? false : undefined, + internal: true, } as Redirect) } } else { @@ -489,6 +492,7 @@ export default async function loadCustomRoutes( destination: '/:path+', permanent: true, locale: config.i18n ? false : undefined, + internal: true, } as Redirect) if (config.basePath) { redirects.unshift({ @@ -497,6 +501,7 @@ export default async function loadCustomRoutes( permanent: true, basePath: false, locale: config.i18n ? false : undefined, + internal: true, } as Redirect) } } diff --git a/packages/next/next-server/lib/post-process.ts b/packages/next/next-server/lib/post-process.ts index d20b9ba42ddd4..63714c5342db7 100644 --- a/packages/next/next-server/lib/post-process.ts +++ b/packages/next/next-server/lib/post-process.ts @@ -1,7 +1,8 @@ import { parse, HTMLElement } from 'node-html-parser' import { OPTIMIZED_FONT_PROVIDERS } from './constants' -const MIDDLEWARE_TIME_BUDGET = 10 +const MIDDLEWARE_TIME_BUDGET = + parseInt(process.env.__POST_PROCESS_MIDDLEWARE_TIME_BUDGET || '', 10) || 10 const MAXIMUM_IMAGE_PRELOADS = 2 const IMAGE_PRELOAD_SIZE_THRESHOLD = 2500 diff --git a/packages/next/next-server/server/api-utils.ts b/packages/next/next-server/server/api-utils.ts index 76eb4095c5822..8436622fa74e7 100644 --- a/packages/next/next-server/server/api-utils.ts +++ b/packages/next/next-server/server/api-utils.ts @@ -56,7 +56,7 @@ export async function apiResolver( ) // Parsing of body - if (bodyParser) { + if (bodyParser && !apiReq.body) { apiReq.body = await parseBody( apiReq, config.api && config.api.bodyParser && config.api.bodyParser.sizeLimit diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index d766f51a1c1df..7a8d2e9aafc59 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -720,6 +720,7 @@ export default class Server { const redirects = this.customRoutes.redirects.map((redirect) => { const redirectRoute = getCustomRoute(redirect, 'redirect') return { + internal: redirectRoute.internal, type: redirectRoute.type, match: redirectRoute.match, statusCode: redirectRoute.statusCode, @@ -1898,7 +1899,7 @@ export default class Server { } catch (err) { if (!fs.existsSync(buildIdFile)) { throw new Error( - `Could not find a valid build in the '${this.distDir}' directory! Try building your app with 'next build' before starting the server.` + `Could not find a production build in the '${this.distDir}' directory. Try building your app with 'next build' before starting the production server. https://err.sh/vercel/next.js/production-start-no-build-id` ) } diff --git a/packages/next/next-server/server/router.ts b/packages/next/next-server/server/router.ts index effed50a4ea1f..e8b22b09d1eb6 100644 --- a/packages/next/next-server/server/router.ts +++ b/packages/next/next-server/server/router.ts @@ -24,6 +24,7 @@ export type Route = { statusCode?: number name: string requireBasePath?: false + internal?: true fn: ( req: IncomingMessage, res: ServerResponse, @@ -197,7 +198,11 @@ export default class Router { const activeBasePath = keepBasePath ? this.basePath : '' if (keepLocale) { - if (!localePathResult.detectedLocale && parsedUrl.query.__nextLocale) { + if ( + !testRoute.internal && + parsedUrl.query.__nextLocale && + !localePathResult.detectedLocale + ) { currentPathname = `${activeBasePath}/${parsedUrl.query.__nextLocale}${ currentPathnameNoBasePath === '/' ? '' : currentPathnameNoBasePath }` diff --git a/run-tests.js b/run-tests.js index a3c39f519f5f9..4dc82a1979486 100644 --- a/run-tests.js +++ b/run-tests.js @@ -195,6 +195,7 @@ const configuredTestTypes = [UNIT_TEST_EXT] ...(isAzure ? { HEADLESS: 'true', + __POST_PROCESS_MIDDLEWARE_TIME_BUDGET: '50', } : {}), ...(usePolling diff --git a/test/integration/api-body-parser/pages/api/index.js b/test/integration/api-body-parser/pages/api/index.js new file mode 100644 index 0000000000000..28ef9244fcefe --- /dev/null +++ b/test/integration/api-body-parser/pages/api/index.js @@ -0,0 +1,5 @@ +export default ({ method, body }, res) => { + if (method === 'POST') { + res.status(200).json(body) + } +} diff --git a/test/integration/api-body-parser/server.js b/test/integration/api-body-parser/server.js new file mode 100644 index 0000000000000..646c0dc3a4e6b --- /dev/null +++ b/test/integration/api-body-parser/server.js @@ -0,0 +1,28 @@ +const next = require('next') +const bodyParser = require('body-parser') +const express = require('express') + +const dev = process.env.NODE_ENV !== 'production' +const dir = __dirname +const port = process.env.PORT || 3000 + +const app = next({ dev, dir }) +const handleNextRequests = app.getRequestHandler() + +app.prepare().then(() => { + const server = express() + + server.use(bodyParser.json({ limit: '5mb' })) + + server.all('*', (req, res) => { + handleNextRequests(req, res) + }) + + server.listen(port, (err) => { + if (err) { + throw err + } + + console.log(`> Ready on http://localhost:${port}`) + }) +}) diff --git a/test/integration/api-body-parser/test/index.test.js b/test/integration/api-body-parser/test/index.test.js new file mode 100644 index 0000000000000..ea90f43bdbcf8 --- /dev/null +++ b/test/integration/api-body-parser/test/index.test.js @@ -0,0 +1,72 @@ +/* eslint-env jest */ + +import { join } from 'path' +import { + killApp, + findPort, + launchApp, + fetchViaHTTP, + initNextServerScript, +} from 'next-test-utils' +import clone from 'clone' +import getPort from 'get-port' + +jest.setTimeout(1000 * 60 * 2) +const appDir = join(__dirname, '../') +let appPort + +let app +let server +jest.setTimeout(1000 * 60 * 2) + +const context = {} + +function runTests() { + it('should parse JSON body', async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort, {}) + const data = await makeRequest() + expect(data).toEqual([{ title: 'Nextjs' }]) + killApp(app) + }) + + it('should not throw if request body is already parsed in custom middleware', async () => { + await startServer() + const data = await makeRequest() + expect(data).toEqual([{ title: 'Nextjs' }]) + killApp(server) + }) +} + +async function makeRequest() { + const data = await fetchViaHTTP(appPort, '/api', null, { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify([{ title: 'Nextjs' }]), + }).then((res) => res.ok && res.json()) + + return data +} + +const startServer = async (optEnv = {}, opts) => { + const scriptPath = join(appDir, 'server.js') + context.appPort = appPort = await getPort() + const env = Object.assign( + {}, + clone(process.env), + { PORT: `${appPort}` }, + optEnv + ) + + server = await initNextServerScript( + scriptPath, + /ready on/i, + env, + /ReferenceError: options is not defined/, + opts + ) +} + +runTests() diff --git a/test/integration/custom-routes/test/index.test.js b/test/integration/custom-routes/test/index.test.js index 4c23b44882d99..eba6e200df983 100644 --- a/test/integration/custom-routes/test/index.test.js +++ b/test/integration/custom-routes/test/index.test.js @@ -644,6 +644,7 @@ const runTests = (isDev = false) => { ), source: '/:path+/', statusCode: 308, + internal: true, }, { destination: '/:lang/about', diff --git a/test/integration/export-no-build/next.config.js b/test/integration/export-no-build/next.config.js new file mode 100644 index 0000000000000..1f568747f8676 --- /dev/null +++ b/test/integration/export-no-build/next.config.js @@ -0,0 +1,39 @@ +module.exports = { + onDemandEntries: { + // Make sure entries are not getting disposed. + maxInactiveAge: 1000 * 60 * 60, + }, + rewrites() { + // add a rewrite so the code isn't dead-code eliminated + return [ + { + source: '/some-rewrite', + destination: '/', + }, + ] + }, + redirects() { + return [ + { + source: '/redirect/me/to-about/:lang', + destination: '/:lang/about', + permanent: false, + }, + { + source: '/nonexistent', + destination: '/about', + permanent: false, + }, + { + source: '/shadowed-page', + destination: '/about', + permanent: false, + }, + { + source: '/redirect-query-test/:path', + destination: '/about?foo=:path', + permanent: false, + }, + ] + }, +} diff --git a/test/integration/export-no-build/test/index.test.js b/test/integration/export-no-build/test/index.test.js new file mode 100644 index 0000000000000..bd3ca813b4d81 --- /dev/null +++ b/test/integration/export-no-build/test/index.test.js @@ -0,0 +1,13 @@ +/* eslint-env jest */ +import { nextExport } from 'next-test-utils' +import { join } from 'path' +const appDir = join(__dirname, '../') +jest.setTimeout(1000 * 60 * 5) + +describe('next export without build', () => { + it('should show error when there is no production build', async () => { + const result = await nextExport(appDir, {}, { stderr: true, stdout: true }) + console.log(result.stdout, result.stderr) + expect(result.stderr).toMatch(/Could not find a production build in the/) + }) +}) diff --git a/test/integration/i18n-support/test/shared.js b/test/integration/i18n-support/test/shared.js index b6ceb35b48f13..b4c41bf022915 100644 --- a/test/integration/i18n-support/test/shared.js +++ b/test/integration/i18n-support/test/shared.js @@ -406,6 +406,26 @@ export function runTests(ctx) { } }) + it('should apply trailingSlash redirect correctly', async () => { + for (const [testPath, path, hostname, query] of [ + ['/first/', '/first', 'localhost', {}], + ['/en/', '/en', 'localhost', {}], + ['/en/another/', '/en/another', 'localhost', {}], + ['/fr/', '/fr', 'localhost', {}], + ['/fr/another/', '/fr/another', 'localhost', {}], + ]) { + const res = await fetchViaHTTP(ctx.appPort, testPath, undefined, { + redirect: 'manual', + }) + expect(res.status).toBe(308) + + const parsed = url.parse(res.headers.get('location'), true) + expect(parsed.pathname).toBe(path) + expect(parsed.hostname).toBe(hostname) + expect(parsed.query).toEqual(query) + } + }) + it('should apply redirects correctly', async () => { for (const [path, shouldRedirect, locale] of [ ['/en-US/redirect-1', true], diff --git a/test/integration/production-start-no-build/next.config.js b/test/integration/production-start-no-build/next.config.js new file mode 100644 index 0000000000000..1f568747f8676 --- /dev/null +++ b/test/integration/production-start-no-build/next.config.js @@ -0,0 +1,39 @@ +module.exports = { + onDemandEntries: { + // Make sure entries are not getting disposed. + maxInactiveAge: 1000 * 60 * 60, + }, + rewrites() { + // add a rewrite so the code isn't dead-code eliminated + return [ + { + source: '/some-rewrite', + destination: '/', + }, + ] + }, + redirects() { + return [ + { + source: '/redirect/me/to-about/:lang', + destination: '/:lang/about', + permanent: false, + }, + { + source: '/nonexistent', + destination: '/about', + permanent: false, + }, + { + source: '/shadowed-page', + destination: '/about', + permanent: false, + }, + { + source: '/redirect-query-test/:path', + destination: '/about?foo=:path', + permanent: false, + }, + ] + }, +} diff --git a/test/integration/production-start-no-build/test/index.test.js b/test/integration/production-start-no-build/test/index.test.js new file mode 100644 index 0000000000000..01e30da1f38b0 --- /dev/null +++ b/test/integration/production-start-no-build/test/index.test.js @@ -0,0 +1,17 @@ +/* eslint-env jest */ +import { nextServer } from 'next-test-utils' +import { join } from 'path' +const appDir = join(__dirname, '../') +jest.setTimeout(1000 * 60 * 5) + +describe('Production Usage without production build', () => { + it('should show error when there is no production build', async () => { + expect(() => { + nextServer({ + dir: appDir, + dev: false, + quiet: true, + }) + }).toThrow(/Could not find a production build in the/) + }) +})