diff --git a/examples/i18n-routing/pages/gsp/index.js b/examples/i18n-routing/pages/gsp/index.js
index 4243a6034c467..905742584018e 100644
--- a/examples/i18n-routing/pages/gsp/index.js
+++ b/examples/i18n-routing/pages/gsp/index.js
@@ -7,7 +7,7 @@ export default function GspPage(props) {
return (
-
getServerSideProps page
+
getStaticProps page
Current locale: {props.locale}
Default locale: {defaultLocale}
Configured locales: {JSON.stringify(props.locales)}
diff --git a/examples/using-preact/next.config.js b/examples/using-preact/next.config.js
index c55926dfd847c..14a8224307a61 100644
--- a/examples/using-preact/next.config.js
+++ b/examples/using-preact/next.config.js
@@ -1,3 +1,4 @@
+const preact = require('preact')
const withPrefresh = require('@prefresh/next')
module.exports = withPrefresh({
@@ -25,14 +26,32 @@ module.exports = withPrefresh({
const aliases = config.resolve.alias || (config.resolve.alias = {})
aliases.react = aliases['react-dom'] = 'preact/compat'
- // Automatically inject Preact DevTools:
- if (dev && !isServer) {
- const entry = config.entry
- config.entry = () =>
- entry().then((entries) => {
- entries['main.js'] = ['preact/debug'].concat(entries['main.js'] || [])
- return entries
- })
+ if (dev) {
+ if (isServer) {
+ // Remove circular `__self` and `__source` props only meant for development
+ let oldVNodeHook = preact.options.vnode
+ preact.options.vnode = (vnode) => {
+ const props = vnode.props
+ if (props != null) {
+ if ('__self' in props) props.__self = null
+ if ('__source' in props) props.__source = null
+ }
+
+ if (oldVNodeHook) {
+ oldVNodeHook(vnode)
+ }
+ }
+ } else {
+ // Automatically inject Preact DevTools:
+ const entry = config.entry
+ config.entry = () =>
+ entry().then((entries) => {
+ entries['main.js'] = ['preact/debug'].concat(
+ entries['main.js'] || []
+ )
+ return entries
+ })
+ }
}
return config
diff --git a/examples/using-preact/package.json b/examples/using-preact/package.json
index 14a28fae73bae..e60ee7e876bc7 100644
--- a/examples/using-preact/package.json
+++ b/examples/using-preact/package.json
@@ -6,17 +6,15 @@
"build": "next build",
"start": "next start"
},
- "devDependencies": {
- "react-refresh": "^0.8.3"
- },
+ "devDependencies": {},
"dependencies": {
- "@prefresh/next": "^0.3.0",
- "next": "^9.4.0",
- "preact": "^10.4.4",
- "preact-render-to-string": "^5.1.9",
- "react": "github:preact-compat/react#1.0.0",
- "react-dom": "github:preact-compat/react-dom#1.0.0",
- "react-ssr-prepass": "npm:preact-ssr-prepass@^1.0.1"
+ "@prefresh/next": "^1.3.0",
+ "next": "10.0.0",
+ "preact": "^10.5.4",
+ "preact-render-to-string": "^5.1.10",
+ "react": "npm:@preact/compat@^0.0.3",
+ "react-dom": "npm:@preact/compat@^0.0.3",
+ "react-ssr-prepass": "npm:preact-ssr-prepass@^1.1.2"
},
"license": "MIT"
}
diff --git a/examples/using-preact/pages/index.js b/examples/using-preact/pages/index.js
index 90d95353b509d..62301b996922a 100644
--- a/examples/using-preact/pages/index.js
+++ b/examples/using-preact/pages/index.js
@@ -4,9 +4,23 @@ export default function Home() {
return (
Hello World.{' '}
-
-
About
-
+
)
}
diff --git a/examples/using-preact/pages/ssg.js b/examples/using-preact/pages/ssg.js
new file mode 100644
index 0000000000000..58adabc4b5b77
--- /dev/null
+++ b/examples/using-preact/pages/ssg.js
@@ -0,0 +1,9 @@
+export default function SSG({ framework }) {
+ return
{framework} ssg example
+}
+
+export function getStaticProps() {
+ return {
+ props: { framework: 'preact' },
+ }
+}
diff --git a/examples/using-preact/pages/ssr.js b/examples/using-preact/pages/ssr.js
new file mode 100644
index 0000000000000..695e329a85918
--- /dev/null
+++ b/examples/using-preact/pages/ssr.js
@@ -0,0 +1,9 @@
+export default function SSR({ framework }) {
+ return
{framework} ssr example
+}
+
+export function getServerSideProps() {
+ return {
+ props: { framework: 'preact' },
+ }
+}
diff --git a/examples/with-magic/next.config.js b/examples/with-magic/next.config.js
deleted file mode 100644
index 5b8efdfbd80c2..0000000000000
--- a/examples/with-magic/next.config.js
+++ /dev/null
@@ -1,5 +0,0 @@
-module.exports = {
- devIndicators: {
- autoPrerender: false,
- },
-}
diff --git a/examples/with-next-page-transitions/pages/about.js b/examples/with-next-page-transitions/pages/about.js
index 074cf4075cb53..8cc10dc4f0220 100644
--- a/examples/with-next-page-transitions/pages/about.js
+++ b/examples/with-next-page-transitions/pages/about.js
@@ -40,4 +40,6 @@ About.defaultProps = {
pageTransitionReadyToEnter: () => {},
}
+About.pageTransitionDelayEnter = true
+
export default About
diff --git a/lerna.json b/lerna.json
index 298d1f4531f9a..52becfda12de1 100644
--- a/lerna.json
+++ b/lerna.json
@@ -17,5 +17,5 @@
"registry": "https://registry.npmjs.org/"
}
},
- "version": "10.0.0"
+ "version": "10.0.1-canary.6"
}
diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json
index 1f8c7a8c085de..6a330f1a72111 100644
--- a/packages/create-next-app/package.json
+++ b/packages/create-next-app/package.json
@@ -1,6 +1,6 @@
{
"name": "create-next-app",
- "version": "10.0.0",
+ "version": "10.0.1-canary.6",
"keywords": [
"react",
"next",
diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json
index b371f75dc8a6e..8668765a45fa0 100644
--- a/packages/eslint-plugin-next/package.json
+++ b/packages/eslint-plugin-next/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/eslint-plugin-next",
- "version": "10.0.0",
+ "version": "10.0.1-canary.6",
"description": "ESLint plugin for NextJS.",
"main": "lib/index.js",
"license": "MIT",
diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json
index 1c448034b3b4d..3bcff4d50a192 100644
--- a/packages/next-bundle-analyzer/package.json
+++ b/packages/next-bundle-analyzer/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/bundle-analyzer",
- "version": "10.0.0",
+ "version": "10.0.1-canary.6",
"main": "index.js",
"license": "MIT",
"repository": {
diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json
index e0e8d94a99895..93581fa5127a9 100644
--- a/packages/next-codemod/package.json
+++ b/packages/next-codemod/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/codemod",
- "version": "10.0.0",
+ "version": "10.0.1-canary.6",
"license": "MIT",
"dependencies": {
"chalk": "4.1.0",
diff --git a/packages/next-env/package.json b/packages/next-env/package.json
index 3ce238ffa075b..e3c242c583024 100644
--- a/packages/next-env/package.json
+++ b/packages/next-env/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/env",
- "version": "10.0.0",
+ "version": "10.0.1-canary.6",
"keywords": [
"react",
"next",
diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json
index 3018732251645..f7433417576ef 100644
--- a/packages/next-mdx/package.json
+++ b/packages/next-mdx/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/mdx",
- "version": "10.0.0",
+ "version": "10.0.1-canary.6",
"main": "index.js",
"license": "MIT",
"repository": {
diff --git a/packages/next-plugin-google-analytics/package.json b/packages/next-plugin-google-analytics/package.json
index 810bf474b5944..b604f51e75b21 100644
--- a/packages/next-plugin-google-analytics/package.json
+++ b/packages/next-plugin-google-analytics/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/plugin-google-analytics",
- "version": "10.0.0",
+ "version": "10.0.1-canary.6",
"repository": {
"url": "vercel/next.js",
"directory": "packages/next-plugin-google-analytics"
diff --git a/packages/next-plugin-sentry/package.json b/packages/next-plugin-sentry/package.json
index 7a4b76cbbf4d6..048ca8abbaf5a 100644
--- a/packages/next-plugin-sentry/package.json
+++ b/packages/next-plugin-sentry/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/plugin-sentry",
- "version": "10.0.0",
+ "version": "10.0.1-canary.6",
"repository": {
"url": "vercel/next.js",
"directory": "packages/next-plugin-sentry"
diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json
index 0aa11303f2a83..7faf05be4a459 100644
--- a/packages/next-plugin-storybook/package.json
+++ b/packages/next-plugin-storybook/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/plugin-storybook",
- "version": "10.0.0",
+ "version": "10.0.1-canary.6",
"repository": {
"url": "vercel/next.js",
"directory": "packages/next-plugin-storybook"
diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json
index cbdea2d3558fa..b7703c6ee8bcc 100644
--- a/packages/next-polyfill-module/package.json
+++ b/packages/next-polyfill-module/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/polyfill-module",
- "version": "10.0.0",
+ "version": "10.0.1-canary.6",
"description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)",
"main": "dist/polyfill-module.js",
"license": "MIT",
diff --git a/packages/next-polyfill-module/src/index.js b/packages/next-polyfill-module/src/index.js
index 8fb8b7cb42c3d..73b6c052f089c 100644
--- a/packages/next-polyfill-module/src/index.js
+++ b/packages/next-polyfill-module/src/index.js
@@ -31,6 +31,7 @@ if (!('trimEnd' in String.prototype)) {
*/
if (!('description' in Symbol.prototype)) {
Object.defineProperty(Symbol.prototype, 'description', {
+ configurable: true,
get: function get() {
var m = /\((.*)\)/.exec(this.toString())
return m ? m[1] : undefined
diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json
index 4cab925816dd7..3a6f5f8ef4edd 100644
--- a/packages/next-polyfill-nomodule/package.json
+++ b/packages/next-polyfill-nomodule/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/polyfill-nomodule",
- "version": "10.0.0",
+ "version": "10.0.1-canary.6",
"description": "A polyfill for non-dead, nomodule browsers.",
"main": "dist/polyfill-nomodule.js",
"license": "MIT",
diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts
index 909283becc48a..a25898130740c 100644
--- a/packages/next/build/index.ts
+++ b/packages/next/build/index.ts
@@ -808,7 +808,7 @@ export default async function build(
}
}
- if (isSsg && !isFallback) {
+ if (isSsg) {
// remove non-locale prefixed variant from defaultMap
delete defaultMap[page]
}
diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts
index 5c049139d3458..3e552304226b1 100644
--- a/packages/next/build/webpack-config.ts
+++ b/packages/next/build/webpack-config.ts
@@ -969,9 +969,6 @@ export default async function getBaseWebpackConfig(
'process.env.__NEXT_BUILD_INDICATOR': JSON.stringify(
config.devIndicators.buildActivity
),
- 'process.env.__NEXT_PRERENDER_INDICATOR': JSON.stringify(
- config.devIndicators.autoPrerender
- ),
'process.env.__NEXT_PLUGINS': JSON.stringify(
config.experimental.plugins
),
@@ -1196,7 +1193,6 @@ export default async function getBaseWebpackConfig(
trailingSlash: config.trailingSlash,
modern: config.experimental.modern,
buildActivity: config.devIndicators.buildActivity,
- autoPrerender: config.devIndicators.autoPrerender,
plugins: config.experimental.plugins,
reactStrictMode: config.reactStrictMode,
reactMode: config.experimental.reactMode,
diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts
index 5b24329dd36b3..f5d06c37a95af 100644
--- a/packages/next/build/webpack/loaders/next-serverless-loader.ts
+++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts
@@ -85,6 +85,17 @@ const nextServerlessLoader: loader.Loader = function () {
.reduce((prev, key) => {
let value = query[key]
+ // if the value matches the default value we can't rely
+ // on the parsed params, this is used to signal if we need
+ // to parse x-now-route-matches or not
+ const isDefaultValue = Array.isArray(value)
+ ? value.every((val, idx) => val === defaultRouteMatches[key][idx])
+ : value === defaultRouteMatches[key]
+
+ if (isDefaultValue || typeof value === 'undefined') {
+ hasValidParams = false
+ }
+
${
''
// non-provided optional values should be undefined so normalize
@@ -127,6 +138,7 @@ const nextServerlessLoader: loader.Loader = function () {
}
`
: ''
+
const envLoading = `
const { processEnv } = require('@next/env')
processEnv(${Buffer.from(loadedEnvFiles, 'base64').toString()})
@@ -155,6 +167,7 @@ const nextServerlessLoader: loader.Loader = function () {
const dynamicRouteMatcher = pageIsDynamicRoute
? `
const dynamicRouteMatcher = getRouteMatcher(getRouteRegex("${page}"))
+ const defaultRouteMatches = dynamicRouteMatcher("${page}")
`
: ''
@@ -230,10 +243,12 @@ const nextServerlessLoader: loader.Loader = function () {
let locales = i18n.locales
let defaultLocale = i18n.defaultLocale
let detectedLocale = detectLocaleCookie(req, i18n.locales)
- let acceptPreferredLocale = accept.language(
- req.headers['accept-language'],
- i18n.locales
- )
+ let acceptPreferredLocale = i18n.localeDetection !== false
+ ? accept.language(
+ req.headers['accept-language'],
+ i18n.locales
+ )
+ : detectedLocale
const { host } = req.headers || {}
// remove port from host and remove port if present
@@ -355,7 +370,10 @@ const nextServerlessLoader: loader.Loader = function () {
return
}
- detectedLocale = detectedLocale || defaultLocale
+ detectedLocale =
+ localePathResult.detectedLocale ||
+ (detectedDomain && detectedDomain.defaultLocale) ||
+ defaultLocale
`
: `
const i18n = {}
@@ -387,8 +405,6 @@ const nextServerlessLoader: loader.Loader = function () {
${defaultRouteRegex}
- ${normalizeDynamicRouteParams}
-
${handleRewrites}
export default async (req, res) => {
@@ -399,7 +415,9 @@ const nextServerlessLoader: loader.Loader = function () {
// to ensure we are using the correct values
const trustQuery = req.headers['${vercelHeader}']
const parsedUrl = handleRewrites(parseUrl(req.url, true))
+ let hasValidParams = true
+ ${normalizeDynamicRouteParams}
${handleBasePath}
const params = ${
@@ -484,7 +502,6 @@ const nextServerlessLoader: loader.Loader = function () {
${dynamicRouteMatcher}
${defaultRouteRegex}
- ${normalizeDynamicRouteParams}
${handleRewrites}
export let config = compMod['confi' + 'g'] || (compMod.then && compMod.then(mod => mod['confi' + 'g'])) || {}
@@ -517,6 +534,9 @@ const nextServerlessLoader: loader.Loader = function () {
const fromExport = renderMode === 'export' || renderMode === true;
const nextStartMode = renderMode === 'passthrough'
+ let hasValidParams = true
+
+ ${normalizeDynamicRouteParams}
setLazyProp({ req }, 'cookies', getCookieParser(req))
@@ -620,6 +640,7 @@ const nextServerlessLoader: loader.Loader = function () {
`
: `const params = {};`
}
+
${
// Temporary work around: `x-now-route-matches` is a platform header
// _only_ set for `Prerender` requests. We should move this logic
@@ -627,7 +648,7 @@ const nextServerlessLoader: loader.Loader = function () {
// removing reliance on `req.url` and using `req.query` instead
// (which is needed for "custom routes" anyway).
pageIsDynamicRoute
- ? `const nowParams = req.headers && req.headers["x-now-route-matches"]
+ ? `const nowParams = !hasValidParams && req.headers && req.headers["x-now-route-matches"]
? getRouteMatcher(
(function() {
const { re, groups, routeKeys } = getRouteRegex("${page}");
@@ -784,7 +805,10 @@ const nextServerlessLoader: loader.Loader = function () {
getStaticPaths: undefined,
getServerSideProps: undefined,
Component: NotFoundComponent,
- err: undefined
+ err: undefined,
+ locale: detectedLocale,
+ locales,
+ defaultLocale: i18n.defaultLocale,
}))
sendPayload(req, res, result, 'html', ${
diff --git a/packages/next/client/dev/prerender-indicator.js b/packages/next/client/dev/prerender-indicator.js
deleted file mode 100644
index 0e395afb738e5..0000000000000
--- a/packages/next/client/dev/prerender-indicator.js
+++ /dev/null
@@ -1,204 +0,0 @@
-import Router from '../router'
-
-export default function initializeBuildWatcher() {
- const shadowHost = document.createElement('div')
- shadowHost.id = '__next-prerender-indicator'
- // Make sure container is fixed and on a high zIndex so it shows
- shadowHost.style.position = 'fixed'
- shadowHost.style.bottom = '20px'
- shadowHost.style.right = '10px'
- shadowHost.style.width = 0
- shadowHost.style.height = 0
- shadowHost.style.zIndex = 99998
- shadowHost.style.transition = 'all 100ms ease'
-
- document.body.appendChild(shadowHost)
-
- let shadowRoot
- let prefix = ''
-
- if (shadowHost.attachShadow) {
- shadowRoot = shadowHost.attachShadow({ mode: 'open' })
- } else {
- // If attachShadow is undefined then the browser does not support
- // the Shadow DOM, we need to prefix all the names so there
- // will be no conflicts
- shadowRoot = shadowHost
- prefix = '__next-prerender-indicator-'
- }
-
- // Container
- const container = createContainer(prefix)
- shadowRoot.appendChild(container)
-
- // CSS
- const css = createCss(prefix)
- shadowRoot.appendChild(css)
-
- const expandEl = container.querySelector('a')
- const closeEl = container.querySelector(`#${prefix}close`)
-
- // State
- const dismissKey = '__NEXT_DISMISS_PRERENDER_INDICATOR'
- const dismissUntil = parseInt(window.localStorage.getItem(dismissKey), 10)
- const dismissed = dismissUntil > new Date().getTime()
-
- let isVisible = !dismissed && window.__NEXT_DATA__.nextExport
-
- function updateContainer() {
- if (isVisible) {
- container.classList.add(`${prefix}visible`)
- } else {
- container.classList.remove(`${prefix}visible`)
- }
- }
- const expandedClass = `${prefix}expanded`
- let toggleTimeout
-
- const toggleExpand = (expand = true) => {
- clearTimeout(toggleTimeout)
-
- toggleTimeout = setTimeout(() => {
- if (expand) {
- expandEl.classList.add(expandedClass)
- closeEl.style.display = 'flex'
- } else {
- expandEl.classList.remove(expandedClass)
- closeEl.style.display = 'none'
- }
- }, 50)
- }
-
- closeEl.addEventListener('click', () => {
- const oneHourAway = new Date().getTime() + 1 * 60 * 60 * 1000
- window.localStorage.setItem(dismissKey, oneHourAway + '')
- isVisible = false
- updateContainer()
- })
- closeEl.addEventListener('mouseenter', () => toggleExpand())
- closeEl.addEventListener('mouseleave', () => toggleExpand(false))
- expandEl.addEventListener('mouseenter', () => toggleExpand())
- expandEl.addEventListener('mouseleave', () => toggleExpand(false))
-
- Router.events.on('routeChangeComplete', () => {
- isVisible = window.next.isPrerendered
- updateContainer()
- })
- updateContainer()
-}
-
-function createContainer(prefix) {
- const container = document.createElement('div')
- container.id = `${prefix}container`
- container.innerHTML = `
-
- ×
-
-
-
-
-
-
-
-
- Prerendered Page
-
-
- `
- return container
-}
-
-function createCss(prefix) {
- const css = document.createElement('style')
- css.textContent = `
- #${prefix}container {
- position: absolute;
- display: none;
- bottom: 10px;
- right: 15px;
- }
-
- #${prefix}close {
- top: -10px;
- right: -10px;
- border: none;
- width: 18px;
- height: 18px;
- color: #333333;
- font-size: 16px;
- cursor: pointer;
- display: none;
- position: absolute;
- background: #ffffff;
- border-radius: 100%;
- align-items: center;
- flex-direction: column;
- justify-content: center;
- }
-
- #${prefix}container a {
- color: inherit;
- text-decoration: none;
- width: 15px;
- height: 23px;
- overflow: hidden;
-
- border-radius: 3px;
- background: #fff;
- color: #000;
- font: initial;
- cursor: pointer;
- letter-spacing: initial;
- text-shadow: initial;
- text-transform: initial;
- visibility: initial;
- font-size: 14px;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
-
- padding: 4px 2px;
- align-items: center;
- box-shadow: 0 11px 40px 0 rgba(0, 0, 0, 0.25), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
-
- display: flex;
- transition: opacity 0.1s ease, bottom 0.1s ease, width 0.3s ease;
- animation: ${prefix}fade-in 0.1s ease-in-out;
- }
-
- #${prefix}icon-wrapper {
- width: 140px;
- height: 20px;
- display: flex;
- flex-shrink: 0;
- align-items: center;
- position: relative;
- }
-
- #${prefix}icon-wrapper svg {
- flex-shrink: 0;
- margin-right: 3px;
- }
-
- #${prefix}container a.${prefix}expanded {
- width: 135px;
- }
-
- #${prefix}container.${prefix}visible {
- display: flex;
- bottom: 10px;
- opacity: 1;
- }
-
- @keyframes ${prefix}fade-in {
- from {
- bottom: 0px;
- opacity: 0;
- }
- to {
- bottom: 10px;
- opacity: 1;
- }
- }
- `
-
- return css
-}
diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx
index f861def1a3002..5c06ed1de05b4 100644
--- a/packages/next/client/image.tsx
+++ b/packages/next/client/image.tsx
@@ -13,6 +13,15 @@ const loaders = new Map
string>([
type LoaderKey = 'imgix' | 'cloudinary' | 'akamai' | 'default'
+const VALID_LAYOUT_VALUES = [
+ 'fill',
+ 'fixed',
+ 'intrinsic',
+ 'responsive',
+ undefined,
+] as const
+type LayoutValue = typeof VALID_LAYOUT_VALUES[number]
+
type ImageData = {
deviceSizes: number[]
imageSizes: number[]
@@ -31,8 +40,18 @@ type ImageProps = Omit<
loading?: LoadingValue
unoptimized?: boolean
} & (
- | { width: number | string; height: number | string; unsized?: false }
- | { width?: number | string; height?: number | string; unsized: true }
+ | {
+ width?: never
+ height?: never
+ /** @deprecated Use `layout="fill"` instead */
+ unsized: true
+ }
+ | { width?: never; height?: never; layout: 'fill' }
+ | {
+ width: number | string
+ height: number | string
+ layout?: Exclude
+ }
)
const imageData: ImageData = process.env.__NEXT_IMAGE_OPTS as any
@@ -44,8 +63,9 @@ const {
domains: configDomains,
} = imageData
// sort smallest to largest
+const allSizes = [...configDeviceSizes, ...configImageSizes]
configDeviceSizes.sort((a, b) => a - b)
-configImageSizes.sort((a, b) => a - b)
+allSizes.sort((a, b) => a - b)
let cachedObserver: IntersectionObserver
@@ -86,34 +106,40 @@ function unLazifyImage(lazyImage: HTMLImageElement): void {
lazyImage.classList.remove('__lazy')
}
-function getDeviceSizes(width: number | undefined): number[] {
- if (typeof width !== 'number') {
- return configDeviceSizes
- }
- if (configImageSizes.includes(width)) {
- return [width]
- }
- const widths: number[] = []
- for (let size of configDeviceSizes) {
- widths.push(size)
- if (size >= width) {
- break
- }
+function getSizes(
+ width: number | undefined,
+ layout: LayoutValue
+): { sizes: number[]; kind: 'w' | 'x' } {
+ if (
+ typeof width !== 'number' ||
+ layout === 'fill' ||
+ layout === 'responsive'
+ ) {
+ return { sizes: configDeviceSizes, kind: 'w' }
}
- return widths
+
+ const sizes = [
+ ...new Set(
+ [width, width * 2, width * 3].map(
+ (w) => allSizes.find((p) => p >= w) || allSizes[allSizes.length - 1]
+ )
+ ),
+ ]
+ return { sizes, kind: 'x' }
}
function computeSrc(
src: string,
unoptimized: boolean,
+ layout: LayoutValue,
width?: number,
quality?: number
): string {
if (unoptimized) {
return src
}
- const widths = getDeviceSizes(width)
- const largest = widths[widths.length - 1]
+ const { sizes } = getSizes(width, layout)
+ const largest = sizes[sizes.length - 1]
return callLoader({ src, width: largest, quality })
}
@@ -131,6 +157,7 @@ function callLoader(loaderProps: CallLoaderProps) {
type SrcSetData = {
src: string
unoptimized: boolean
+ layout: LayoutValue
width?: number
quality?: number
}
@@ -138,6 +165,7 @@ type SrcSetData = {
function generateSrcSet({
src,
unoptimized,
+ layout,
width,
quality,
}: SrcSetData): string | undefined {
@@ -147,14 +175,21 @@ function generateSrcSet({
return undefined
}
- return getDeviceSizes(width)
- .map((w) => `${callLoader({ src, width: w, quality })} ${w}w`)
+ const { sizes, kind } = getSizes(width, layout)
+ return sizes
+ .map(
+ (size, i) =>
+ `${callLoader({ src, width: size, quality })} ${
+ kind === 'w' ? size : i + 1
+ }${kind}`
+ )
.join(', ')
}
type PreloadData = {
src: string
unoptimized: boolean
+ layout: LayoutValue
width: number | undefined
sizes?: string
quality?: number
@@ -162,8 +197,9 @@ type PreloadData = {
function generatePreload({
src,
- width,
unoptimized = false,
+ layout,
+ width,
sizes,
quality,
}: PreloadData): ReactElement {
@@ -176,9 +212,15 @@ function generatePreload({
@@ -205,19 +247,40 @@ export default function Image({
quality,
width,
height,
- unsized,
- ...rest
+ ...all
}: ImageProps) {
const thisEl = useRef(null)
+ let rest: Partial = all
+ let layout: NonNullable = sizes ? 'responsive' : 'intrinsic'
+ let unsized = false
+ if ('unsized' in rest) {
+ unsized = Boolean(rest.unsized)
+ // Remove property so it's not spread into image:
+ delete rest['unsized']
+ } else if ('layout' in rest) {
+ // Override default layout if the user specified one:
+ if (rest.layout) layout = rest.layout
+
+ // Remove property so it's not spread into image:
+ delete rest['layout']
+ }
+
if (process.env.NODE_ENV !== 'production') {
if (!src) {
throw new Error(
`Image is missing required "src" property. Make sure you pass "src" in props to the \`next/image\` component. Received: ${JSON.stringify(
- { width, height, quality, unsized }
+ { width, height, quality }
)}`
)
}
+ if (!VALID_LAYOUT_VALUES.includes(layout)) {
+ throw new Error(
+ `Image with src "${src}" has invalid "layout" property. Provided "${layout}" should be one of ${VALID_LAYOUT_VALUES.map(
+ String
+ ).join(',')}.`
+ )
+ }
if (!VALID_LOADING_VALUES.includes(loading)) {
throw new Error(
`Image with src "${src}" has invalid "loading" property. Provided "${loading}" should be one of ${VALID_LOADING_VALUES.map(
@@ -227,7 +290,12 @@ export default function Image({
}
if (priority && loading === 'lazy') {
throw new Error(
- `Image with src "${src}" has both "priority" and "loading=lazy" properties. Only one should be used.`
+ `Image with src "${src}" has both "priority" and "loading='lazy'" properties. Only one should be used.`
+ )
+ }
+ if (unsized) {
+ throw new Error(
+ `Image with src "${src}" has deprecated "unsized" property, which was removed in favor of the "layout='fill'" property`
)
}
}
@@ -265,63 +333,112 @@ export default function Image({
const heightInt = getInt(height)
const qualityInt = getInt(quality)
- let divStyle: React.CSSProperties | undefined
- let imgStyle: React.CSSProperties | undefined
- let wrapperStyle: React.CSSProperties | undefined
+ let wrapperStyle: JSX.IntrinsicElements['div']['style'] | undefined
+ let sizerStyle: JSX.IntrinsicElements['div']['style'] | undefined
+ let sizerSvg: string | undefined
+ let imgStyle: JSX.IntrinsicElements['img']['style'] = {
+ visibility: lazy ? 'hidden' : 'visible',
+
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ bottom: 0,
+ right: 0,
+
+ boxSizing: 'border-box',
+ padding: 0,
+ border: 'none',
+ margin: 'auto',
+
+ display: 'block',
+ width: 0,
+ height: 0,
+ minWidth: '100%',
+ maxWidth: '100%',
+ minHeight: '100%',
+ maxHeight: '100%',
+ }
if (
typeof widthInt !== 'undefined' &&
typeof heightInt !== 'undefined' &&
- !unsized
+ layout !== 'fill'
) {
- //
//
const quotient = heightInt / widthInt
- const ratio = isNaN(quotient) ? 1 : quotient * 100
- wrapperStyle = {
- maxWidth: '100%',
- width: widthInt,
- }
- divStyle = {
- position: 'relative',
- paddingBottom: `${ratio}%`,
- }
- imgStyle = {
- visibility: lazy ? 'hidden' : 'visible',
- height: '100%',
- left: '0',
- position: 'absolute',
- top: '0',
- width: '100%',
+ const paddingTop = isNaN(quotient) ? '100%' : `${quotient * 100}%`
+ if (layout === 'responsive') {
+ //
+ wrapperStyle = {
+ display: 'block',
+ overflow: 'hidden',
+ position: 'relative',
+
+ boxSizing: 'border-box',
+ margin: 0,
+ }
+ sizerStyle = { display: 'block', boxSizing: 'border-box', paddingTop }
+ } else if (layout === 'intrinsic') {
+ //
+ wrapperStyle = {
+ display: 'inline-block',
+ maxWidth: '100%',
+ overflow: 'hidden',
+ position: 'relative',
+ boxSizing: 'border-box',
+ margin: 0,
+ }
+ sizerStyle = {
+ boxSizing: 'border-box',
+ display: 'block',
+ maxWidth: '100%',
+ }
+ sizerSvg = ` `
+ } else if (layout === 'fixed') {
+ //
+ wrapperStyle = {
+ overflow: 'hidden',
+ boxSizing: 'border-box',
+ display: 'inline-block',
+ position: 'relative',
+ width: widthInt,
+ height: heightInt,
+ }
}
} else if (
typeof widthInt === 'undefined' &&
typeof heightInt === 'undefined' &&
- unsized
+ layout === 'fill'
) {
- //
- if (process.env.NODE_ENV !== 'production') {
- if (priority) {
- //
- console.warn(
- `Image with src "${src}" has both "priority" and "unsized" properties. Only one should be used.`
- )
- }
+ //
+ wrapperStyle = {
+ display: 'block',
+ overflow: 'hidden',
+
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ bottom: 0,
+ right: 0,
+
+ boxSizing: 'border-box',
+ margin: 0,
}
} else {
//
if (process.env.NODE_ENV !== 'production') {
throw new Error(
- `Image with src "${src}" must use "width" and "height" properties or "unsized" property.`
+ `Image with src "${src}" must use "width" and "height" properties or "layout='fill'" property.`
)
}
}
// Generate attribute values
- const imgSrc = computeSrc(src, unoptimized, widthInt, qualityInt)
+ const imgSrc = computeSrc(src, unoptimized, layout, widthInt, qualityInt)
const imgSrcSet = generateSrcSet({
src,
- width: widthInt,
unoptimized,
+ layout,
+ width: widthInt,
quality: qualityInt,
})
@@ -355,27 +472,45 @@ export default function Image({
// it's too late for preloads
const shouldPreload = priority && typeof window === 'undefined'
+ if (unsized) {
+ wrapperStyle = undefined
+ sizerStyle = undefined
+ imgStyle = undefined
+ }
return (
-
- {shouldPreload
- ? generatePreload({
- src,
- width: widthInt,
- unoptimized,
- sizes,
- quality: qualityInt,
- })
- : ''}
-
-
+ {shouldPreload
+ ? generatePreload({
+ src,
+ layout,
+ unoptimized,
+ width: widthInt,
+ sizes,
+ quality: qualityInt,
+ })
+ : null}
+ {sizerStyle ? (
+
+ {sizerSvg ? (
+
+ ) : null}
+
+ ) : null}
+
)
}
@@ -389,7 +524,8 @@ function normalizeSrc(src: string) {
}
function imgixLoader({ root, src, width, quality }: LoaderProps): string {
- const params = ['auto=format', 'w=' + width]
+ // Demo: https://static.imgix.net/daisy.png?format=auto&fit=max&w=300
+ const params = ['auto=format', 'fit=max', 'w=' + width]
let paramsString = ''
if (quality) {
params.push('q=' + quality)
@@ -406,7 +542,8 @@ function akamaiLoader({ root, src, width }: LoaderProps): string {
}
function cloudinaryLoader({ root, src, width, quality }: LoaderProps): string {
- const params = ['f_auto', 'w_' + width]
+ // Demo: https://res.cloudinary.com/demo/image/upload/w_300,c_limit/turtles.jpg
+ const params = ['f_auto', 'c_limit', 'w_' + width]
let paramsString = ''
if (quality) {
params.push('q_' + quality)
diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx
index 54f178e673b39..4c28435d64a84 100644
--- a/packages/next/client/index.tsx
+++ b/packages/next/client/index.tsx
@@ -18,7 +18,11 @@ import * as envConfig from '../next-server/lib/runtime-config'
import type { NEXT_DATA } from '../next-server/lib/utils'
import { getURL, loadGetInitialProps, ST } from '../next-server/lib/utils'
import initHeadManager from './head-manager'
-import PageLoader, { looseToArray, StyleSheetTuple } from './page-loader'
+import PageLoader, {
+ INITIAL_CSS_LOAD_ERROR,
+ looseToArray,
+ StyleSheetTuple,
+} from './page-loader'
import measureWebVitals from './performance-relayer'
import { createRouter, makePublicRouterInstance } from './router'
@@ -92,11 +96,21 @@ if (process.env.__NEXT_I18N_SUPPORT) {
detectDomainLocale,
} = require('../next-server/lib/i18n/detect-domain-locale') as typeof import('../next-server/lib/i18n/detect-domain-locale')
+ const {
+ parseRelativeUrl,
+ } = require('../next-server/lib/router/utils/parse-relative-url') as typeof import('../next-server/lib/router/utils/parse-relative-url')
+
+ const {
+ formatUrl,
+ } = require('../next-server/lib/router/utils/format-url') as typeof import('../next-server/lib/router/utils/format-url')
+
if (locales) {
- const localePathResult = normalizeLocalePath(asPath, locales)
+ const parsedAs = parseRelativeUrl(asPath)
+ const localePathResult = normalizeLocalePath(parsedAs.pathname, locales)
if (localePathResult.detectedLocale) {
- asPath = asPath.substr(localePathResult.detectedLocale.length + 1) || '/'
+ parsedAs.pathname = localePathResult.pathname
+ asPath = formatUrl(parsedAs)
} else {
// derive the default locale if it wasn't detected in the asPath
// since we don't prerender static pages with all possible default
@@ -276,6 +290,9 @@ export default async (opts: { webpackHMR?: any } = {}) => {
}
}
} catch (error) {
+ if (INITIAL_CSS_LOAD_ERROR in error) {
+ throw error
+ }
// This catches errors like throwing in the top level of a module
initialErr = error
}
diff --git a/packages/next/client/link.tsx b/packages/next/client/link.tsx
index 08d3ff085d5d8..2592816f44c09 100644
--- a/packages/next/client/link.tsx
+++ b/packages/next/client/link.tsx
@@ -261,8 +261,6 @@ function Link(props: React.PropsWithChildren) {
}
const p = props.prefetch !== false
- const [childElm, setChildElm] = React.useState()
-
const router = useRouter()
const pathname = (router && router.pathname) || '/'
@@ -276,25 +274,8 @@ function Link(props: React.PropsWithChildren) {
}
}, [pathname, props.href, props.as])
- React.useEffect(() => {
- if (
- p &&
- IntersectionObserver &&
- childElm &&
- childElm.tagName &&
- isLocalURL(href)
- ) {
- // Join on an invalid URI character
- const isPrefetched = prefetched[href + '%' + as]
- if (!isPrefetched) {
- return listenToIntersections(childElm, () => {
- prefetch(router, href, as)
- })
- }
- }
- }, [p, childElm, href, as, router])
-
let { children, replace, shallow, scroll, locale } = props
+
// Deprecated. Warning shown by propType check. If the children provided is a string ( example) we wrap it in an tag
if (typeof children === 'string') {
children = {children}
@@ -302,22 +283,49 @@ function Link(props: React.PropsWithChildren) {
// This will return the first child, if multiple are provided it will throw an error
const child: any = Children.only(children)
+ const childRef: any = child && typeof child === 'object' && child.ref
+
+ const cleanup = React.useRef<() => void>()
+ const setRef = React.useCallback(
+ (el: Element) => {
+ // cleanup previous event handlers
+ if (cleanup.current) {
+ cleanup.current()
+ cleanup.current = undefined
+ }
+
+ if (p && IntersectionObserver && el && el.tagName && isLocalURL(href)) {
+ // Join on an invalid URI character
+ const isPrefetched = prefetched[href + '%' + as]
+ if (!isPrefetched) {
+ cleanup.current = listenToIntersections(el, () => {
+ prefetch(router, href, as, {
+ locale:
+ typeof locale !== 'undefined'
+ ? locale
+ : router && router.locale,
+ })
+ })
+ }
+ }
+
+ if (childRef) {
+ if (typeof childRef === 'function') childRef(el)
+ else if (typeof childRef === 'object') {
+ childRef.current = el
+ }
+ }
+ },
+ [p, childRef, href, as, router, locale]
+ )
+
const childProps: {
onMouseEnter?: React.MouseEventHandler
onClick: React.MouseEventHandler
href?: string
ref?: any
} = {
- ref: (el: any) => {
- if (el) setChildElm(el)
-
- if (child && typeof child === 'object' && child.ref) {
- if (typeof child.ref === 'function') child.ref(el)
- else if (typeof child.ref === 'object') {
- child.ref.current = el
- }
- }
- },
+ ref: setRef,
onClick: (e: React.MouseEvent) => {
if (child.props && typeof child.props.onClick === 'function') {
child.props.onClick(e)
diff --git a/packages/next/client/next-dev.js b/packages/next/client/next-dev.js
index 1b6a1a3e307a2..dc5b3625b1824 100644
--- a/packages/next/client/next-dev.js
+++ b/packages/next/client/next-dev.js
@@ -4,7 +4,6 @@ import EventSourcePolyfill from './dev/event-source-polyfill'
import initOnDemandEntries from './dev/on-demand-entries-client'
import initWebpackHMR from './dev/webpack-hot-middleware-client'
import initializeBuildWatcher from './dev/dev-build-watcher'
-import initializePrerenderIndicator from './dev/prerender-indicator'
import { displayContent } from './dev/fouc'
import { getEventSourceWrapper } from './dev/error-overlay/eventsource'
import * as querystring from '../next-server/lib/router/utils/querystring'
@@ -80,13 +79,6 @@ initNext({ webpackHMR })
buildIndicatorHandler = handler
})
}
- if (
- process.env.__NEXT_PRERENDER_INDICATOR &&
- // disable by default in electron
- !(typeof process !== 'undefined' && 'electron' in process.versions)
- ) {
- initializePrerenderIndicator()
- }
// delay rendering until after styles have been applied in development
displayContent(() => {
diff --git a/packages/next/client/page-loader.ts b/packages/next/client/page-loader.ts
index 58d6ccf647d31..c3340e3b20b49 100644
--- a/packages/next/client/page-loader.ts
+++ b/packages/next/client/page-loader.ts
@@ -1,15 +1,14 @@
import { ComponentType } from 'react'
import type { ClientSsgManifest } from '../build'
import type { ClientBuildManifest } from '../build/webpack/plugins/build-manifest-plugin'
-import mitt from '../next-server/lib/mitt'
import type { MittEmitter } from '../next-server/lib/mitt'
+import mitt from '../next-server/lib/mitt'
import {
addBasePath,
- markLoadingError,
- interpolateAs,
addLocale,
+ interpolateAs,
+ markLoadingError,
} from '../next-server/lib/router/router'
-
import getAssetPathFromRoute from '../next-server/lib/router/utils/get-asset-path-from-route'
import { isDynamicRoute } from '../next-server/lib/router/utils/is-dynamic'
import { parseRelativeUrl } from '../next-server/lib/router/utils/parse-relative-url'
@@ -17,22 +16,6 @@ import { parseRelativeUrl } from '../next-server/lib/router/utils/parse-relative
export const looseToArray = (input: any): T[] =>
[].slice.call(input)
-function getInitialStylesheets(): StyleSheetTuple[] {
- return looseToArray(document.styleSheets)
- .filter(
- (el: CSSStyleSheet) =>
- el.ownerNode &&
- (el.ownerNode as Element).tagName === 'LINK' &&
- (el.ownerNode as Element).hasAttribute('data-n-p')
- )
- .map((sheet) => ({
- href: (sheet.ownerNode as Element).getAttribute('href')!,
- text: looseToArray(sheet.cssRules)
- .map((r) => r.cssText)
- .join(''),
- }))
-}
-
function hasRel(rel: string, link?: HTMLLinkElement) {
try {
link = document.createElement('link')
@@ -44,6 +27,8 @@ function pageLoadError(route: string) {
return markLoadingError(new Error(`Error loading ${route}`))
}
+export const INITIAL_CSS_LOAD_ERROR = Symbol('INITIAL_CSS_LOAD_ERROR')
+
const relPrefetch =
hasRel('preload') && !hasRel('prefetch')
? // https://caniuse.com/#feat=link-rel-preload
@@ -203,7 +188,12 @@ export default class PageLoader {
* @param {string} href the route href (file-system path)
* @param {string} asPath the URL as shown in browser (virtual path); used for dynamic routes
*/
- getDataHref(href: string, asPath: string, ssg: boolean, locale?: string) {
+ getDataHref(
+ href: string,
+ asPath: string,
+ ssg: boolean,
+ locale?: string | false
+ ) {
const { pathname: hrefPathname, query, search } = parseRelativeUrl(href)
const { pathname: asPathname } = parseRelativeUrl(asPath)
const route = normalizeRoute(hrefPathname)
@@ -229,7 +219,7 @@ export default class PageLoader {
* @param {string} href the route href (file-system path)
* @param {string} asPath the URL as shown in browser (virtual path); used for dynamic routes
*/
- prefetchData(href: string, asPath: string, locale?: string) {
+ prefetchData(href: string, asPath: string, locale?: string | false) {
const { pathname: hrefPathname } = parseRelativeUrl(href)
const route = normalizeRoute(hrefPathname)
return this.promisedSsgManifest!.then(
@@ -408,7 +398,9 @@ export default class PageLoader {
// should resolve instantly.
Promise.all(cssFiles.map((d) => fetchStyleSheet(d))).catch(
(err) => {
- if (isInitialLoad) return getInitialStylesheets()
+ if (isInitialLoad) {
+ Object.defineProperty(err, INITIAL_CSS_LOAD_ERROR, {})
+ }
throw err
}
)
diff --git a/packages/next/lib/load-custom-routes.ts b/packages/next/lib/load-custom-routes.ts
index 0f94004e934ce..57220c567751a 100644
--- a/packages/next/lib/load-custom-routes.ts
+++ b/packages/next/lib/load-custom-routes.ts
@@ -22,7 +22,7 @@ export type Header = {
headers: Array<{ key: string; value: string }>
}
-const allowedStatusCodes = new Set([301, 302, 303, 307, 308])
+export const allowedStatusCodes = new Set([301, 302, 303, 307, 308])
export function getRedirectStatus(route: Redirect): number {
return (
diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts
index 728a78d9a2de0..a626c36dcd1a6 100644
--- a/packages/next/next-server/lib/router/router.ts
+++ b/packages/next/next-server/lib/router/router.ts
@@ -62,7 +62,10 @@ export function addLocale(
defaultLocale?: string
) {
if (process.env.__NEXT_I18N_SUPPORT) {
- return locale && locale !== defaultLocale && !path.startsWith('/' + locale)
+ return locale &&
+ locale !== defaultLocale &&
+ !path.startsWith('/' + locale + '/') &&
+ path !== '/' + locale
? addPathPrefix(path, '/' + locale)
: path
}
@@ -71,7 +74,8 @@ export function addLocale(
export function delLocale(path: string, locale?: string) {
if (process.env.__NEXT_I18N_SUPPORT) {
- return locale && path.startsWith('/' + locale)
+ return locale &&
+ (path.startsWith('/' + locale + '/') || path === '/' + locale)
? path.substr(locale.length + 1) || '/'
: path
}
@@ -269,6 +273,7 @@ export type NextRouter = BaseRouter &
export type PrefetchOptions = {
priority?: boolean
+ locale?: string | false
}
export type PrivateRouteInfo = {
@@ -853,6 +858,12 @@ export default class Router implements BaseRouter {
window.scrollTo((options as any)._N_X, (options as any)._N_Y)
}
}
+
+ if (process.env.__NEXT_I18N_SUPPORT) {
+ if (this.locale) {
+ document.documentElement.lang = this.locale
+ }
+ }
Router.events.emit('routeChangeComplete', as)
return true
@@ -1171,6 +1182,26 @@ export default class Router implements BaseRouter {
let { pathname } = parsed
+ if (process.env.__NEXT_I18N_SUPPORT) {
+ const normalizeLocalePath = require('../i18n/normalize-locale-path')
+ .normalizeLocalePath as typeof import('../i18n/normalize-locale-path').normalizeLocalePath
+
+ if (options.locale === false) {
+ pathname = normalizeLocalePath!(pathname, this.locales).pathname
+ parsed.pathname = pathname
+ url = formatWithValidation(parsed)
+
+ let parsedAs = parseRelativeUrl(asPath)
+ const localePathResult = normalizeLocalePath!(
+ parsedAs.pathname,
+ this.locales
+ )
+ parsedAs.pathname = localePathResult.pathname
+ options.locale = localePathResult.detectedLocale || options.locale
+ asPath = formatWithValidation(parsedAs)
+ }
+ }
+
const pages = await this.pageLoader.getPageList()
parsed = this._resolveHref(parsed, pages) as typeof parsed
@@ -1190,7 +1221,7 @@ export default class Router implements BaseRouter {
this.pageLoader.prefetchData(
url,
asPath,
- this.locale,
+ typeof options.locale !== 'undefined' ? options.locale : this.locale,
this.defaultLocale
),
this.pageLoader[options.priority ? 'loadPage' : 'prefetch'](route),
diff --git a/packages/next/next-server/lib/router/utils/prepare-destination.ts b/packages/next/next-server/lib/router/utils/prepare-destination.ts
index 90df77c446c5b..9eb46a80c9bad 100644
--- a/packages/next/next-server/lib/router/utils/prepare-destination.ts
+++ b/packages/next/next-server/lib/router/utils/prepare-destination.ts
@@ -19,6 +19,10 @@ export default function prepareDestination(
port?: string
} & ReturnType = {} as any
+ // clone query so we don't modify the original
+ query = Object.assign({}, query)
+ delete query.__nextLocale
+
if (destination.startsWith('/')) {
parsedDestination = parseRelativeUrl(destination)
} else {
diff --git a/packages/next/next-server/server/config.ts b/packages/next/next-server/server/config.ts
index a4fa310e3b328..9700fe1c4d3e7 100644
--- a/packages/next/next-server/server/config.ts
+++ b/packages/next/next-server/server/config.ts
@@ -25,15 +25,14 @@ const defaultConfig: { [key: string]: any } = {
compress: true,
analyticsId: process.env.VERCEL_ANALYTICS_ID || '',
images: {
- deviceSizes: [320, 420, 768, 1024, 1200],
- imageSizes: [],
+ deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
+ imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
domains: [],
path: '/_next/image',
loader: 'default',
},
devIndicators: {
buildActivity: true,
- autoPrerender: true,
},
onDemandEntries: {
maxInactiveAge: 60 * 1000,
@@ -220,14 +219,14 @@ function assignDefaults(userConfig: { [key: string]: any }) {
if (typeof images !== 'object') {
throw new Error(
- `Specified images should be an object received ${typeof images}.\nSee more info here: https://err.sh/nextjs/invalid-images-config`
+ `Specified images should be an object received ${typeof images}.\nSee more info here: https://err.sh/next.js/invalid-images-config`
)
}
if (images.domains) {
if (!Array.isArray(images.domains)) {
throw new Error(
- `Specified images.domains should be an Array received ${typeof images.domains}.\nSee more info here: https://err.sh/nextjs/invalid-images-config`
+ `Specified images.domains should be an Array received ${typeof images.domains}.\nSee more info here: https://err.sh/next.js/invalid-images-config`
)
}
@@ -244,7 +243,7 @@ function assignDefaults(userConfig: { [key: string]: any }) {
throw new Error(
`Specified images.domains should be an Array of strings received invalid values (${invalid.join(
', '
- )}).\nSee more info here: https://err.sh/nextjs/invalid-images-config`
+ )}).\nSee more info here: https://err.sh/next.js/invalid-images-config`
)
}
}
@@ -252,7 +251,7 @@ function assignDefaults(userConfig: { [key: string]: any }) {
const { deviceSizes } = images
if (!Array.isArray(deviceSizes)) {
throw new Error(
- `Specified images.deviceSizes should be an Array received ${typeof deviceSizes}.\nSee more info here: https://err.sh/nextjs/invalid-images-config`
+ `Specified images.deviceSizes should be an Array received ${typeof deviceSizes}.\nSee more info here: https://err.sh/next.js/invalid-images-config`
)
}
@@ -270,7 +269,7 @@ function assignDefaults(userConfig: { [key: string]: any }) {
throw new Error(
`Specified images.deviceSizes should be an Array of numbers that are between 1 and 10000, received invalid values (${invalid.join(
', '
- )}).\nSee more info here: https://err.sh/nextjs/invalid-images-config`
+ )}).\nSee more info here: https://err.sh/next.js/invalid-images-config`
)
}
}
@@ -278,7 +277,7 @@ function assignDefaults(userConfig: { [key: string]: any }) {
const { imageSizes } = images
if (!Array.isArray(imageSizes)) {
throw new Error(
- `Specified images.imageSizes should be an Array received ${typeof imageSizes}.\nSee more info here: https://err.sh/nextjs/invalid-images-config`
+ `Specified images.imageSizes should be an Array received ${typeof imageSizes}.\nSee more info here: https://err.sh/next.js/invalid-images-config`
)
}
@@ -296,7 +295,7 @@ function assignDefaults(userConfig: { [key: string]: any }) {
throw new Error(
`Specified images.imageSizes should be an Array of numbers that are between 1 and 10000, received invalid values (${invalid.join(
', '
- )}).\nSee more info here: https://err.sh/nextjs/invalid-images-config`
+ )}).\nSee more info here: https://err.sh/next.js/invalid-images-config`
)
}
}
@@ -316,13 +315,13 @@ function assignDefaults(userConfig: { [key: string]: any }) {
if (i18nType !== 'object') {
throw new Error(
- `Specified i18n should be an object received ${i18nType}.\nSee more info here: https://err.sh/nextjs/invalid-i18n-config`
+ `Specified i18n should be an object received ${i18nType}.\nSee more info here: https://err.sh/next.js/invalid-i18n-config`
)
}
if (!Array.isArray(i18n.locales)) {
throw new Error(
- `Specified i18n.locales should be an Array received ${typeof i18n.locales}.\nSee more info here: https://err.sh/nextjs/invalid-i18n-config`
+ `Specified i18n.locales should be an Array received ${typeof i18n.locales}.\nSee more info here: https://err.sh/next.js/invalid-i18n-config`
)
}
@@ -330,7 +329,7 @@ function assignDefaults(userConfig: { [key: string]: any }) {
if (!i18n.defaultLocale || defaultLocaleType !== 'string') {
throw new Error(
- `Specified i18n.defaultLocale should be a string.\nSee more info here: https://err.sh/nextjs/invalid-i18n-config`
+ `Specified i18n.defaultLocale should be a string.\nSee more info here: https://err.sh/next.js/invalid-i18n-config`
)
}
@@ -374,14 +373,14 @@ function assignDefaults(userConfig: { [key: string]: any }) {
.map((item: any) => JSON.stringify(item))
.join(
'\n'
- )}\n\ndomains value must follow format { domain: 'example.fr', defaultLocale: 'fr', locales: ['fr'] }.\nSee more info here: https://err.sh/nextjs/invalid-i18n-config`
+ )}\n\ndomains value must follow format { domain: 'example.fr', defaultLocale: 'fr', locales: ['fr'] }.\nSee more info here: https://err.sh/next.js/invalid-i18n-config`
)
}
}
if (!Array.isArray(i18n.locales)) {
throw new Error(
- `Specified i18n.locales must be an array of locale strings e.g. ["en-US", "nl-NL"] received ${typeof i18n.locales}.\nSee more info here: https://err.sh/nextjs/invalid-i18n-config`
+ `Specified i18n.locales must be an array of locale strings e.g. ["en-US", "nl-NL"] received ${typeof i18n.locales}.\nSee more info here: https://err.sh/next.js/invalid-i18n-config`
)
}
@@ -402,7 +401,7 @@ function assignDefaults(userConfig: { [key: string]: any }) {
if (!i18n.locales.includes(i18n.defaultLocale)) {
throw new Error(
- `Specified i18n.defaultLocale should be included in i18n.locales.\nSee more info here: https://err.sh/nextjs/invalid-i18n-config`
+ `Specified i18n.defaultLocale should be included in i18n.locales.\nSee more info here: https://err.sh/next.js/invalid-i18n-config`
)
}
@@ -412,14 +411,14 @@ function assignDefaults(userConfig: { [key: string]: any }) {
...i18n.locales.filter((locale: string) => locale !== i18n.defaultLocale),
]
- const localeDetectionType = typeof i18n.locales.localeDetection
+ const localeDetectionType = typeof i18n.localeDetection
if (
localeDetectionType !== 'boolean' &&
localeDetectionType !== 'undefined'
) {
throw new Error(
- `Specified i18n.localeDetection should be undefined or a boolean received ${localeDetectionType}.\nSee more info here: https://err.sh/nextjs/invalid-i18n-config`
+ `Specified i18n.localeDetection should be undefined or a boolean received ${localeDetectionType}.\nSee more info here: https://err.sh/next.js/invalid-i18n-config`
)
}
}
diff --git a/packages/next/next-server/server/image-optimizer.ts b/packages/next/next-server/server/image-optimizer.ts
index 238fb247cd2a6..e6f8d05936273 100644
--- a/packages/next/next-server/server/image-optimizer.ts
+++ b/packages/next/next-server/server/image-optimizer.ts
@@ -18,8 +18,8 @@ const PNG = 'image/png'
const JPEG = 'image/jpeg'
const GIF = 'image/gif'
const SVG = 'image/svg+xml'
-const MIME_TYPES = [/* AVIF, */ WEBP, PNG, JPEG]
const CACHE_VERSION = 1
+const MODERN_TYPES = [/* AVIF, */ WEBP]
const ANIMATABLE_TYPES = [WEBP, PNG, GIF]
const VECTOR_TYPES = [SVG]
@@ -49,7 +49,7 @@ export async function imageOptimizer(
const { headers } = req
const { url, w, q } = parsedUrl.query
- const mimeType = mediaType(headers.accept, MIME_TYPES) || ''
+ const mimeType = getSupportedMimeType(MODERN_TYPES, headers.accept)
let href: string
if (!url) {
@@ -259,6 +259,8 @@ export async function imageOptimizer(
try {
const transformer = sharp(upstreamBuffer)
+ transformer.rotate() // auto rotate based on EXIF data
+
const { width: metaWidth } = await transformer.metadata()
if (metaWidth && metaWidth > width) {
@@ -294,6 +296,11 @@ export async function imageOptimizer(
return { finished: true }
}
+function getSupportedMimeType(options: string[], accept = ''): string {
+ const mimeType = mediaType(accept, options)
+ return accept.includes(mimeType) ? mimeType : ''
+}
+
function getHash(items: (string | number | undefined)[]) {
const hash = createHash('sha256')
for (let item of items) {
diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts
index 2a116b19ad1ff..8b6b4c3c4b79a 100644
--- a/packages/next/next-server/server/next-server.ts
+++ b/packages/next/next-server/server/next-server.ts
@@ -312,10 +312,10 @@ export default class Server {
const { pathname, ...parsed } = parseUrl(req.url || '/')
let defaultLocale = i18n.defaultLocale
let detectedLocale = detectLocaleCookie(req, i18n.locales)
- let acceptPreferredLocale = accept.language(
- req.headers['accept-language'],
- i18n.locales
- )
+ let acceptPreferredLocale =
+ i18n.localeDetection !== false
+ ? accept.language(req.headers['accept-language'], i18n.locales)
+ : detectedLocale
const { host } = req?.headers || {}
// remove port from host and remove port if present
@@ -429,7 +429,11 @@ export default class Server {
res.end()
return
}
- parsedUrl.query.__nextLocale = detectedLocale || defaultLocale
+
+ parsedUrl.query.__nextLocale =
+ localePathResult.detectedLocale ||
+ detectedDomain?.defaultLocale ||
+ defaultLocale
}
res.statusCode = 200
@@ -966,8 +970,19 @@ export default class Server {
// if basePath is defined require it be present
if (basePath) {
- if (pathParts[0] !== basePath.substr(1)) return { finished: false }
- pathParts.shift()
+ const basePathParts = basePath.split('/')
+ // remove first empty value
+ basePathParts.shift()
+
+ if (
+ !basePathParts.every((part: string, idx: number) => {
+ return part === pathParts[idx]
+ })
+ ) {
+ return { finished: false }
+ }
+
+ pathParts.splice(0, basePathParts.length)
}
const path = `/${pathParts.join('/')}`
diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx
index d497c70036ac4..44ec6c5c03751 100644
--- a/packages/next/next-server/server/render.tsx
+++ b/packages/next/next-server/server/render.tsx
@@ -52,6 +52,7 @@ import { FontManifest, getFontDefinitionFromManifest } from './font-utils'
import { LoadComponentsReturnType, ManifestItem } from './load-components'
import { normalizePagePath } from './normalize-page-path'
import optimizeAmp from './optimize-amp'
+import { allowedStatusCodes } from '../../lib/load-custom-routes'
function noRouter() {
const message =
@@ -307,38 +308,60 @@ const invalidKeysMsg = (methodName: string, invalidKeys: string[]) => {
type Redirect = {
permanent: boolean
destination: string
+ statusCode?: number
}
-function checkRedirectValues(redirect: Redirect, req: IncomingMessage) {
- const { destination, permanent } = redirect
- let invalidPermanent = typeof permanent !== 'boolean'
- let invalidDestination = typeof destination !== 'string'
+function checkRedirectValues(
+ redirect: Redirect,
+ req: IncomingMessage,
+ method: 'getStaticProps' | 'getServerSideProps'
+) {
+ const { destination, permanent, statusCode } = redirect
+ let errors: string[] = []
+
+ const hasStatusCode = typeof statusCode !== 'undefined'
+ const hasPermanent = typeof permanent !== 'undefined'
+
+ if (hasPermanent && hasStatusCode) {
+ errors.push(`\`permanent\` and \`statusCode\` can not both be provided`)
+ } else if (hasPermanent && typeof permanent !== 'boolean') {
+ errors.push(`\`permanent\` must be \`true\` or \`false\``)
+ } else if (hasStatusCode && !allowedStatusCodes.has(statusCode!)) {
+ errors.push(
+ `\`statusCode\` must undefined or one of ${[...allowedStatusCodes].join(
+ ', '
+ )}`
+ )
+ }
+ const destinationType = typeof destination
- if (invalidPermanent || invalidDestination) {
+ if (destinationType !== 'string') {
+ errors.push(
+ `\`destination\` should be string but received ${destinationType}`
+ )
+ }
+
+ if (errors.length > 0) {
throw new Error(
- `Invalid redirect object returned from getStaticProps for ${req.url}\n` +
- `Expected${
- invalidPermanent
- ? ` \`permanent\` to be boolean but received ${typeof permanent}`
- : ''
- }${invalidPermanent && invalidDestination ? ' and' : ''}${
- invalidDestination
- ? ` \`destinatino\` to be string but received ${typeof destination}`
- : ''
- }\n` +
+ `Invalid redirect object returned from ${method} for ${req.url}\n` +
+ errors.join(' and ') +
+ '\n' +
`See more info here: https://err.sh/vercel/next.js/invalid-redirect-gssp`
)
}
}
function handleRedirect(res: ServerResponse, redirect: Redirect) {
- const statusCode = redirect.permanent
+ const statusCode = redirect.statusCode
+ ? redirect.statusCode
+ : redirect.permanent
? PERMANENT_REDIRECT_STATUS
: TEMPORARY_REDIRECT_STATUS
- if (redirect.permanent) {
+ if (statusCode === PERMANENT_REDIRECT_STATUS) {
res.setHeader('Refresh', `0;url=${redirect.destination}`)
}
+
res.statusCode = statusCode
res.setHeader('Location', redirect.destination)
res.end()
@@ -654,7 +677,7 @@ export async function renderToHTML(
data.redirect &&
typeof data.redirect === 'object'
) {
- checkRedirectValues(data.redirect, req)
+ checkRedirectValues(data.redirect, req, 'getStaticProps')
if (isBuildTimeSSG) {
throw new Error(
@@ -791,7 +814,7 @@ export async function renderToHTML(
}
if ('redirect' in data && typeof data.redirect === 'object') {
- checkRedirectValues(data.redirect, req)
+ checkRedirectValues(data.redirect, req, 'getServerSideProps')
if (isDataReq) {
;(data as any).props = {
diff --git a/packages/next/next-server/server/router.ts b/packages/next/next-server/server/router.ts
index 9c858ab724bc3..ba78dee256f31 100644
--- a/packages/next/next-server/server/router.ts
+++ b/packages/next/next-server/server/router.ts
@@ -177,6 +177,17 @@ export default class Router {
currentPathname = replaceBasePath(this.basePath, currentPathname!)
}
+ // re-add locale for custom-routes to allow matching against
+ if (
+ isCustomRoute &&
+ (req as any).__nextStrippedLocale &&
+ parsedUrl.query.__nextLocale
+ ) {
+ currentPathname = `/${parsedUrl.query.__nextLocale}${
+ currentPathname === '/' ? '' : currentPathname
+ }`
+ }
+
const newParams = testRoute.match(currentPathname)
// Check if the match function matched
diff --git a/packages/next/package.json b/packages/next/package.json
index 59c1a4edd2a2a..799b48c7e2cd4 100644
--- a/packages/next/package.json
+++ b/packages/next/package.json
@@ -1,6 +1,6 @@
{
"name": "next",
- "version": "10.0.0",
+ "version": "10.0.1-canary.6",
"description": "The React Framework",
"main": "./dist/server/next.js",
"license": "MIT",
@@ -79,10 +79,10 @@
"@babel/runtime": "7.11.2",
"@babel/types": "7.11.5",
"@hapi/accept": "5.0.1",
- "@next/env": "10.0.0",
- "@next/polyfill-module": "10.0.0",
- "@next/react-dev-overlay": "10.0.0",
- "@next/react-refresh-utils": "10.0.0",
+ "@next/env": "10.0.1-canary.6",
+ "@next/polyfill-module": "10.0.1-canary.6",
+ "@next/react-dev-overlay": "10.0.1-canary.6",
+ "@next/react-refresh-utils": "10.0.1-canary.6",
"ast-types": "0.13.2",
"babel-plugin-transform-define": "2.0.0",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
@@ -129,7 +129,7 @@
"sharp": "0.26.2"
},
"devDependencies": {
- "@next/polyfill-nomodule": "10.0.0",
+ "@next/polyfill-nomodule": "10.0.1-canary.6",
"@taskr/clear": "1.1.0",
"@taskr/esnext": "1.1.0",
"@taskr/watch": "1.1.0",
diff --git a/packages/next/pages/_document.tsx b/packages/next/pages/_document.tsx
index 0b129671e54a4..4b0334d927835 100644
--- a/packages/next/pages/_document.tsx
+++ b/packages/next/pages/_document.tsx
@@ -163,8 +163,11 @@ export class Head extends Component<
dynamicImports,
} = this.context
const cssFiles = files.allFiles.filter((f) => f.endsWith('.css'))
- const sharedFiles = new Set(files.sharedFiles)
+ const sharedFiles: Set = new Set(files.sharedFiles)
+ // Unmanaged files are CSS files that will be handled directly by the
+ // webpack runtime (`mini-css-extract-plugin`).
+ let unmangedFiles: Set = new Set([])
let dynamicCssFiles = dedupe(
dynamicImports.filter((f) => f.file.endsWith('.css'))
).map((f) => f.file)
@@ -173,13 +176,14 @@ export class Head extends Component<
dynamicCssFiles = dynamicCssFiles.filter(
(f) => !(existing.has(f) || sharedFiles.has(f))
)
+ unmangedFiles = new Set(dynamicCssFiles)
cssFiles.push(...dynamicCssFiles)
}
const cssLinkElements: JSX.Element[] = []
cssFiles.forEach((file) => {
const isSharedFile = sharedFiles.has(file)
-
+ const isUnmanagedFile = unmangedFiles.has(file)
cssLinkElements.push(
)
})
diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json
index 2701d42e089d0..0b0f345fa3fdd 100644
--- a/packages/react-dev-overlay/package.json
+++ b/packages/react-dev-overlay/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/react-dev-overlay",
- "version": "10.0.0",
+ "version": "10.0.1-canary.6",
"description": "A development-only overlay for developing React applications.",
"repository": {
"url": "vercel/next.js",
diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json
index 40ecae8aa38fa..152c6431e223b 100644
--- a/packages/react-refresh-utils/package.json
+++ b/packages/react-refresh-utils/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/react-refresh-utils",
- "version": "10.0.0",
+ "version": "10.0.1-canary.6",
"description": "An experimental package providing utilities for React Refresh.",
"repository": {
"url": "vercel/next.js",
diff --git a/test/integration/basepath/pages/absolute-url-basepath.js b/test/integration/basepath/pages/absolute-url-basepath.js
index 330865a6b5bc6..2eb79686863cb 100644
--- a/test/integration/basepath/pages/absolute-url-basepath.js
+++ b/test/integration/basepath/pages/absolute-url-basepath.js
@@ -1,5 +1,6 @@
import React from 'react'
import Link from 'next/link'
+import { useRouter } from 'next/router'
export async function getServerSideProps({ query: { port } }) {
if (!port) {
@@ -9,10 +10,14 @@ export async function getServerSideProps({ query: { port } }) {
}
export default function Page({ port }) {
+ const router = useRouter()
return (
<>
-
- http://localhost:{port}/docs/something-else
+
+
+ http://localhost:{port}
+ {router.basePath}/something-else
+
>
)
diff --git a/test/integration/basepath/pages/invalid-manual-basepath.js b/test/integration/basepath/pages/invalid-manual-basepath.js
index 6c188e0834e8a..1f5d165ce7a38 100644
--- a/test/integration/basepath/pages/invalid-manual-basepath.js
+++ b/test/integration/basepath/pages/invalid-manual-basepath.js
@@ -1,8 +1,9 @@
import Link from 'next/link'
+import { useRouter } from 'next/router'
export default () => (
<>
-
+
Hello World
diff --git a/test/integration/basepath/test/index.test.js b/test/integration/basepath/test/index.test.js
index bc12692064d23..a643c53d8a381 100644
--- a/test/integration/basepath/test/index.test.js
+++ b/test/integration/basepath/test/index.test.js
@@ -4,13 +4,10 @@ import webdriver from 'next-webdriver'
import { join, resolve } from 'path'
import url from 'url'
import {
- nextServer,
launchApp,
findPort,
killApp,
nextBuild,
- startApp,
- stopApp,
waitFor,
check,
getBrowserBodyText,
@@ -36,6 +33,9 @@ jest.setTimeout(1000 * 60 * 2)
const appDir = join(__dirname, '..')
let externalApp
+let app
+let appPort
+let basePath = '/docs'
const nextConfig = new File(resolve(appDir, 'next.config.js'))
@@ -52,10 +52,327 @@ afterAll(async () => {
externalApp.close()
})
-const runTests = (context, dev = false) => {
+const runTests = (dev = false) => {
if (dev) {
+ describe('Hot Module Reloading', () => {
+ describe('delete a page and add it back', () => {
+ it('should load the page properly', async () => {
+ const contactPagePath = join(
+ __dirname,
+ '../',
+ 'pages',
+ 'hmr',
+ 'contact.js'
+ )
+ const newContactPagePath = join(
+ __dirname,
+ '../',
+ 'pages',
+ 'hmr',
+ '_contact.js'
+ )
+ let browser
+ try {
+ browser = await webdriver(appPort, `${basePath}/hmr/contact`)
+ const text = await browser.elementByCss('p').text()
+ expect(text).toBe('This is the contact page.')
+
+ // Rename the file to mimic a deleted page
+ renameSync(contactPagePath, newContactPagePath)
+
+ await check(
+ () => getBrowserBodyText(browser),
+ /This page could not be found/
+ )
+
+ // Rename the file back to the original filename
+ renameSync(newContactPagePath, contactPagePath)
+
+ // wait until the page comes back
+ await check(
+ () => getBrowserBodyText(browser),
+ /This is the contact page/
+ )
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ if (existsSync(newContactPagePath)) {
+ renameSync(newContactPagePath, contactPagePath)
+ }
+ }
+ })
+ })
+
+ describe('editing a page', () => {
+ it('should detect the changes and display it', async () => {
+ let browser
+ try {
+ browser = await webdriver(appPort, `${basePath}/hmr/about`)
+ const text = await browser.elementByCss('p').text()
+ expect(text).toBe('This is the about page.')
+
+ const aboutPagePath = join(
+ __dirname,
+ '../',
+ 'pages',
+ 'hmr',
+ 'about.js'
+ )
+
+ const originalContent = readFileSync(aboutPagePath, 'utf8')
+ const editedContent = originalContent.replace(
+ 'This is the about page',
+ 'COOL page'
+ )
+
+ // change the content
+ writeFileSync(aboutPagePath, editedContent, 'utf8')
+
+ await check(() => getBrowserBodyText(browser), /COOL page/)
+
+ // add the original content
+ writeFileSync(aboutPagePath, originalContent, 'utf8')
+
+ await check(
+ () => getBrowserBodyText(browser),
+ /This is the about page/
+ )
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should not reload unrelated pages', async () => {
+ let browser
+ try {
+ browser = await webdriver(appPort, `${basePath}/hmr/counter`)
+ const text = await browser
+ .elementByCss('button')
+ .click()
+ .elementByCss('button')
+ .click()
+ .elementByCss('p')
+ .text()
+ expect(text).toBe('COUNT: 2')
+
+ const aboutPagePath = join(
+ __dirname,
+ '../',
+ 'pages',
+ 'hmr',
+ 'about.js'
+ )
+
+ const originalContent = readFileSync(aboutPagePath, 'utf8')
+ const editedContent = originalContent.replace(
+ 'This is the about page',
+ 'COOL page'
+ )
+
+ // Change the about.js page
+ writeFileSync(aboutPagePath, editedContent, 'utf8')
+
+ // wait for 5 seconds
+ await waitFor(5000)
+
+ // Check whether the this page has reloaded or not.
+ const newText = await browser.elementByCss('p').text()
+ expect(newText).toBe('COUNT: 2')
+
+ // restore the about page content.
+ writeFileSync(aboutPagePath, originalContent, 'utf8')
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ // Added because of a regression in react-hot-loader, see issues: #4246 #4273
+ // Also: https://github.com/zeit/styled-jsx/issues/425
+ it('should update styles correctly', async () => {
+ let browser
+ try {
+ browser = await webdriver(appPort, `${basePath}/hmr/style`)
+ const pTag = await browser.elementByCss('.hmr-style-page p')
+ const initialFontSize = await pTag.getComputedCss('font-size')
+
+ expect(initialFontSize).toBe('100px')
+
+ const pagePath = join(__dirname, '../', 'pages', 'hmr', 'style.js')
+
+ const originalContent = readFileSync(pagePath, 'utf8')
+ const editedContent = originalContent.replace('100px', '200px')
+
+ // Change the page
+ writeFileSync(pagePath, editedContent, 'utf8')
+
+ try {
+ // Check whether the this page has reloaded or not.
+ await check(async () => {
+ const editedPTag = await browser.elementByCss(
+ '.hmr-style-page p'
+ )
+ return editedPTag.getComputedCss('font-size')
+ }, /200px/)
+ } finally {
+ // Finally is used so that we revert the content back to the original regardless of the test outcome
+ // restore the about page content.
+ writeFileSync(pagePath, originalContent, 'utf8')
+ }
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ // Added because of a regression in react-hot-loader, see issues: #4246 #4273
+ // Also: https://github.com/zeit/styled-jsx/issues/425
+ it('should update styles in a stateful component correctly', async () => {
+ let browser
+ const pagePath = join(
+ __dirname,
+ '../',
+ 'pages',
+ 'hmr',
+ 'style-stateful-component.js'
+ )
+ const originalContent = readFileSync(pagePath, 'utf8')
+ try {
+ browser = await webdriver(
+ appPort,
+ `${basePath}/hmr/style-stateful-component`
+ )
+ const pTag = await browser.elementByCss('.hmr-style-page p')
+ const initialFontSize = await pTag.getComputedCss('font-size')
+
+ expect(initialFontSize).toBe('100px')
+ const editedContent = originalContent.replace('100px', '200px')
+
+ // Change the page
+ writeFileSync(pagePath, editedContent, 'utf8')
+
+ // Check whether the this page has reloaded or not.
+ await check(async () => {
+ const editedPTag = await browser.elementByCss('.hmr-style-page p')
+ return editedPTag.getComputedCss('font-size')
+ }, /200px/)
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ writeFileSync(pagePath, originalContent, 'utf8')
+ }
+ })
+
+ // Added because of a regression in react-hot-loader, see issues: #4246 #4273
+ // Also: https://github.com/zeit/styled-jsx/issues/425
+ it('should update styles in a dynamic component correctly', async () => {
+ let browser = null
+ let secondBrowser = null
+ const pagePath = join(
+ __dirname,
+ '../',
+ 'components',
+ 'hmr',
+ 'dynamic.js'
+ )
+ const originalContent = readFileSync(pagePath, 'utf8')
+ try {
+ browser = await webdriver(
+ appPort,
+ `${basePath}/hmr/style-dynamic-component`
+ )
+ const div = await browser.elementByCss('#dynamic-component')
+ const initialClientClassName = await div.getAttribute('class')
+ const initialFontSize = await div.getComputedCss('font-size')
+
+ expect(initialFontSize).toBe('100px')
+
+ const initialHtml = await renderViaHTTP(
+ appPort,
+ `${basePath}/hmr/style-dynamic-component`
+ )
+ expect(initialHtml.includes('100px')).toBeTruthy()
+
+ const $initialHtml = cheerio.load(initialHtml)
+ const initialServerClassName = $initialHtml(
+ '#dynamic-component'
+ ).attr('class')
+
+ expect(
+ initialClientClassName === initialServerClassName
+ ).toBeTruthy()
+
+ const editedContent = originalContent.replace('100px', '200px')
+
+ // Change the page
+ writeFileSync(pagePath, editedContent, 'utf8')
+
+ // wait for 5 seconds
+ await waitFor(5000)
+
+ secondBrowser = await webdriver(
+ appPort,
+ `${basePath}/hmr/style-dynamic-component`
+ )
+ // Check whether the this page has reloaded or not.
+ const editedDiv = await secondBrowser.elementByCss(
+ '#dynamic-component'
+ )
+ const editedClientClassName = await editedDiv.getAttribute('class')
+ const editedFontSize = await editedDiv.getComputedCss('font-size')
+ const browserHtml = await secondBrowser
+ .elementByCss('html')
+ .getAttribute('innerHTML')
+
+ expect(editedFontSize).toBe('200px')
+ expect(browserHtml.includes('font-size:200px;')).toBe(true)
+ expect(browserHtml.includes('font-size:100px;')).toBe(false)
+
+ const editedHtml = await renderViaHTTP(
+ appPort,
+ `${basePath}/hmr/style-dynamic-component`
+ )
+ expect(editedHtml.includes('200px')).toBeTruthy()
+ const $editedHtml = cheerio.load(editedHtml)
+ const editedServerClassName = $editedHtml(
+ '#dynamic-component'
+ ).attr('class')
+
+ expect(editedClientClassName === editedServerClassName).toBe(true)
+ } finally {
+ // Finally is used so that we revert the content back to the original regardless of the test outcome
+ // restore the about page content.
+ writeFileSync(pagePath, originalContent, 'utf8')
+
+ if (browser) {
+ await browser.close()
+ }
+
+ if (secondBrowser) {
+ secondBrowser.close()
+ }
+ }
+ })
+ })
+ })
+
+ it('should respect basePath in amphtml link rel', async () => {
+ const html = await renderViaHTTP(appPort, `${basePath}/amp-hybrid`)
+ const $ = cheerio.load(html)
+ const expectedAmpHtmlUrl = `${basePath}/amp-hybrid?amp=1`
+ expect($('link[rel=amphtml]').first().attr('href')).toBe(
+ expectedAmpHtmlUrl
+ )
+ })
+
it('should render error in dev overlay correctly', async () => {
- const browser = await webdriver(context.appPort, '/docs/hello')
+ const browser = await webdriver(appPort, `${basePath}/hello`)
await browser.elementByCss('#trigger-error').click()
expect(await hasRedbox(browser)).toBe(true)
@@ -79,11 +396,11 @@ const runTests = (context, dev = false) => {
const routesManifest = await fs.readJSON(
join(appDir, '.next/routes-manifest.json')
)
- expect(routesManifest.basePath).toBe('/docs')
+ expect(routesManifest.basePath).toBe(basePath)
})
it('should prefetch pages correctly when manually called', async () => {
- const browser = await webdriver(context.appPort, '/docs/other-page')
+ const browser = await webdriver(appPort, `${basePath}/other-page`)
await browser.eval('window.next.router.prefetch("/gssp")')
await check(
@@ -107,7 +424,7 @@ const runTests = (context, dev = false) => {
})
it('should prefetch pages correctly in viewport with ', async () => {
- const browser = await webdriver(context.appPort, '/docs/hello')
+ const browser = await webdriver(appPort, `${basePath}/hello`)
await browser.eval('window.next.router.prefetch("/gssp")')
await check(
@@ -136,56 +453,53 @@ const runTests = (context, dev = false) => {
}
it('should 404 for public file without basePath', async () => {
- const res = await fetchViaHTTP(context.appPort, '/data.txt')
+ const res = await fetchViaHTTP(appPort, '/data.txt')
expect(res.status).toBe(404)
})
it('should serve public file with basePath correctly', async () => {
- const res = await fetchViaHTTP(context.appPort, '/docs/data.txt')
+ const res = await fetchViaHTTP(appPort, `${basePath}/data.txt`)
expect(res.status).toBe(200)
expect(await res.text()).toBe('hello world')
})
it('should rewrite with basePath by default', async () => {
- const html = await renderViaHTTP(context.appPort, '/docs/rewrite-1')
+ const html = await renderViaHTTP(appPort, `${basePath}/rewrite-1`)
expect(html).toContain('getServerSideProps')
})
it('should not rewrite without basePath without disabling', async () => {
- const res = await fetchViaHTTP(context.appPort, '/rewrite-1')
+ const res = await fetchViaHTTP(appPort, '/rewrite-1')
expect(res.status).toBe(404)
})
it('should not rewrite with basePath when set to false', async () => {
// won't 404 as it matches the dynamic [slug] route
- const html = await renderViaHTTP(
- context.appPort,
- '/docs/rewrite-no-basePath'
- )
+ const html = await renderViaHTTP(appPort, `${basePath}/rewrite-no-basePath`)
expect(html).toContain('slug')
})
it('should rewrite without basePath when set to false', async () => {
- const html = await renderViaHTTP(context.appPort, '/rewrite-no-basePath')
+ const html = await renderViaHTTP(appPort, '/rewrite-no-basePath')
expect(html).toContain('hello from external')
})
it('should redirect with basePath by default', async () => {
const res = await fetchViaHTTP(
- context.appPort,
- '/docs/redirect-1',
+ appPort,
+ `${basePath}/redirect-1`,
undefined,
{
redirect: 'manual',
}
)
const { pathname } = url.parse(res.headers.get('location') || '')
- expect(pathname).toBe('/docs/somewhere-else')
+ expect(pathname).toBe(`${basePath}/somewhere-else`)
expect(res.status).toBe(307)
})
it('should not redirect without basePath without disabling', async () => {
- const res = await fetchViaHTTP(context.appPort, '/redirect-1', undefined, {
+ const res = await fetchViaHTTP(appPort, '/redirect-1', undefined, {
redirect: 'manual',
})
expect(res.status).toBe(404)
@@ -193,16 +507,13 @@ const runTests = (context, dev = false) => {
it('should not redirect with basePath when set to false', async () => {
// won't 404 as it matches the dynamic [slug] route
- const html = await renderViaHTTP(
- context.appPort,
- '/docs/rewrite-no-basePath'
- )
+ const html = await renderViaHTTP(appPort, `${basePath}/rewrite-no-basePath`)
expect(html).toContain('slug')
})
it('should redirect without basePath when set to false', async () => {
const res = await fetchViaHTTP(
- context.appPort,
+ appPort,
'/redirect-no-basepath',
undefined,
{
@@ -216,66 +527,69 @@ const runTests = (context, dev = false) => {
//
it('should add header with basePath by default', async () => {
- const res = await fetchViaHTTP(context.appPort, '/docs/add-header')
+ const res = await fetchViaHTTP(appPort, `${basePath}/add-header`)
expect(res.headers.get('x-hello')).toBe('world')
})
it('should not add header without basePath without disabling', async () => {
- const res = await fetchViaHTTP(context.appPort, '/add-header')
+ const res = await fetchViaHTTP(appPort, '/add-header')
expect(res.headers.get('x-hello')).toBe(null)
})
it('should not add header with basePath when set to false', async () => {
const res = await fetchViaHTTP(
- context.appPort,
- '/docs/add-header-no-basepath'
+ appPort,
+ `${basePath}/add-header-no-basepath`
)
expect(res.headers.get('x-hello')).toBe(null)
})
it('should add header without basePath when set to false', async () => {
- const res = await fetchViaHTTP(context.appPort, '/add-header-no-basepath')
+ const res = await fetchViaHTTP(appPort, '/add-header-no-basepath')
expect(res.headers.get('x-hello')).toBe('world')
})
it('should not update URL for a 404', async () => {
- const browser = await webdriver(context.appPort, '/missing')
+ const browser = await webdriver(appPort, '/missing')
const pathname = await browser.eval(() => window.location.pathname)
expect(await browser.eval(() => window.next.router.asPath)).toBe('/missing')
expect(pathname).toBe('/missing')
})
it('should handle 404 urls that start with basePath', async () => {
- const browser = await webdriver(context.appPort, '/docshello')
+ const browser = await webdriver(appPort, `${basePath}hello`)
expect(await browser.eval(() => window.next.router.asPath)).toBe(
- '/docshello'
+ `${basePath}hello`
)
expect(await browser.eval(() => window.location.pathname)).toBe(
- '/docshello'
+ `${basePath}hello`
)
})
it('should navigate back to a non-basepath 404 that starts with basepath', async () => {
- const browser = await webdriver(context.appPort, '/docshello')
+ const browser = await webdriver(appPort, `${basePath}hello`)
await browser.eval(() => (window.navigationMarker = true))
await browser.eval(() => window.next.router.push('/hello'))
await browser.waitForElementByCss('#pathname')
await browser.back()
- check(() => browser.eval(() => window.location.pathname), '/docshello')
+ check(
+ () => browser.eval(() => window.location.pathname),
+ `${basePath}hello`
+ )
expect(await browser.eval(() => window.next.router.asPath)).toBe(
- '/docshello'
+ `${basePath}hello`
)
expect(await browser.eval(() => window.navigationMarker)).toBe(true)
})
it('should update dynamic params after mount correctly', async () => {
- const browser = await webdriver(context.appPort, '/docs/hello-dynamic')
+ const browser = await webdriver(appPort, `${basePath}/hello-dynamic`)
const text = await browser.elementByCss('#slug').text()
expect(text).toContain('slug: hello-dynamic')
})
it('should navigate to index page with getStaticProps', async () => {
- const browser = await webdriver(context.appPort, '/docs/hello')
+ const browser = await webdriver(appPort, `${basePath}/hello`)
await browser.eval('window.beforeNavigate = "hi"')
await browser.elementByCss('#index-gsp').click()
@@ -296,7 +610,7 @@ const runTests = (context, dev = false) => {
const href = url.parse(fullHref).pathname
if (
- href.startsWith('/docs/_next/data') &&
+ href.startsWith(`${basePath}/_next/data`) &&
href.endsWith('index.json') &&
!href.endsWith('index/index.json')
) {
@@ -309,7 +623,7 @@ const runTests = (context, dev = false) => {
})
it('should navigate to nested index page with getStaticProps', async () => {
- const browser = await webdriver(context.appPort, '/docs/hello')
+ const browser = await webdriver(appPort, `${basePath}/hello`)
await browser.eval('window.beforeNavigate = "hi"')
await browser.elementByCss('#nested-index-gsp').click()
@@ -330,7 +644,7 @@ const runTests = (context, dev = false) => {
const href = url.parse(fullHref).pathname
if (
- href.startsWith('/docs/_next/data') &&
+ href.startsWith(`${basePath}/_next/data`) &&
href.endsWith('index/index.json')
) {
found = true
@@ -342,17 +656,17 @@ const runTests = (context, dev = false) => {
})
it('should work with nested folder with same name as basePath', async () => {
- const html = await renderViaHTTP(context.appPort, '/docs/docs/another')
+ const html = await renderViaHTTP(appPort, `${basePath}/docs/another`)
expect(html).toContain('hello from another')
- const browser = await webdriver(context.appPort, '/docs/hello')
+ const browser = await webdriver(appPort, `${basePath}/hello`)
await browser.eval('window.next.router.push("/docs/another")')
await check(() => browser.elementByCss('p').text(), /hello from another/)
})
it('should work with normal dynamic page', async () => {
- const browser = await webdriver(context.appPort, '/docs/hello')
+ const browser = await webdriver(appPort, `${basePath}/hello`)
await browser.elementByCss('#dynamic-link').click()
await check(
() => browser.eval(() => document.documentElement.innerHTML),
@@ -361,15 +675,15 @@ const runTests = (context, dev = false) => {
})
it('should work with hash links', async () => {
- const browser = await webdriver(context.appPort, '/docs/hello')
+ const browser = await webdriver(appPort, `${basePath}/hello`)
await browser.elementByCss('#hashlink').click()
const url = new URL(await browser.eval(() => window.location.href))
- expect(url.pathname).toBe('/docs/hello')
+ expect(url.pathname).toBe(`${basePath}/hello`)
expect(url.hash).toBe('#hashlink')
})
it('should work with catch-all page', async () => {
- const browser = await webdriver(context.appPort, '/docs/hello')
+ const browser = await webdriver(appPort, `${basePath}/hello`)
await browser.elementByCss('#catchall-link').click()
await check(
() => browser.eval(() => document.documentElement.innerHTML),
@@ -379,30 +693,30 @@ const runTests = (context, dev = false) => {
it('should redirect trailing slash correctly', async () => {
const res = await fetchViaHTTP(
- context.appPort,
- '/docs/hello/',
+ appPort,
+ `${basePath}/hello/`,
{},
{ redirect: 'manual' }
)
expect(res.status).toBe(308)
const { pathname } = new URL(res.headers.get('location'))
- expect(pathname).toBe('/docs/hello')
+ expect(pathname).toBe(`${basePath}/hello`)
})
it('should redirect trailing slash on root correctly', async () => {
const res = await fetchViaHTTP(
- context.appPort,
- '/docs/',
+ appPort,
+ `${basePath}/`,
{},
{ redirect: 'manual' }
)
expect(res.status).toBe(308)
const { pathname } = new URL(res.headers.get('location'))
- expect(pathname).toBe('/docs')
+ expect(pathname).toBe(`${basePath}`)
})
it('should navigate an absolute url', async () => {
- const browser = await webdriver(context.appPort, `/docs/absolute-url`)
+ const browser = await webdriver(appPort, `${basePath}/absolute-url`)
await browser.waitForElementByCss('#absolute-link').click()
await check(
() => browser.eval(() => window.location.origin),
@@ -412,8 +726,8 @@ const runTests = (context, dev = false) => {
it('should navigate an absolute local url with basePath', async () => {
const browser = await webdriver(
- context.appPort,
- `/docs/absolute-url-basepath?port=${context.appPort}`
+ appPort,
+ `${basePath}/absolute-url-basepath?port=${appPort}`
)
await browser.eval(() => (window._didNotNavigate = true))
await browser.waitForElementByCss('#absolute-link').click()
@@ -427,8 +741,8 @@ const runTests = (context, dev = false) => {
it('should navigate an absolute local url without basePath', async () => {
const browser = await webdriver(
- context.appPort,
- `/docs/absolute-url-no-basepath?port=${context.appPort}`
+ appPort,
+ `${basePath}/absolute-url-no-basepath?port=${appPort}`
)
await browser.waitForElementByCss('#absolute-link').click()
await check(
@@ -442,8 +756,8 @@ const runTests = (context, dev = false) => {
it('should 404 when manually adding basePath with ', async () => {
const browser = await webdriver(
- context.appPort,
- '/docs/invalid-manual-basepath'
+ appPort,
+ `${basePath}/invalid-manual-basepath`
)
await browser.eval('window.beforeNav = "hi"')
await browser.elementByCss('#other-page-link').click()
@@ -459,9 +773,9 @@ const runTests = (context, dev = false) => {
})
it('should 404 when manually adding basePath with router.push', async () => {
- const browser = await webdriver(context.appPort, '/docs/hello')
+ const browser = await webdriver(appPort, `${basePath}/hello`)
await browser.eval('window.beforeNav = "hi"')
- await browser.eval('window.next.router.push("/docs/other-page")')
+ await browser.eval(`window.next.router.push("${basePath}/other-page")`)
await check(() => browser.eval('window.beforeNav'), {
test(content) {
@@ -474,9 +788,9 @@ const runTests = (context, dev = false) => {
})
it('should 404 when manually adding basePath with router.replace', async () => {
- const browser = await webdriver(context.appPort, '/docs/hello')
+ const browser = await webdriver(appPort, `${basePath}/hello`)
await browser.eval('window.beforeNav = "hi"')
- await browser.eval('window.next.router.replace("/docs/other-page")')
+ await browser.eval(`window.next.router.replace("${basePath}/other-page")`)
await check(() => browser.eval('window.beforeNav'), {
test(content) {
@@ -489,7 +803,7 @@ const runTests = (context, dev = false) => {
})
it('should show the hello page under the /docs prefix', async () => {
- const browser = await webdriver(context.appPort, '/docs/hello')
+ const browser = await webdriver(appPort, `${basePath}/hello`)
try {
const text = await browser.elementByCss('h1').text()
expect(text).toBe('Hello World')
@@ -499,7 +813,7 @@ const runTests = (context, dev = false) => {
})
it('should have correct router paths on first load of /', async () => {
- const browser = await webdriver(context.appPort, '/docs')
+ const browser = await webdriver(appPort, `${basePath}`)
await browser.waitForElementByCss('#pathname')
const pathname = await browser.elementByCss('#pathname').text()
expect(pathname).toBe('/')
@@ -508,7 +822,7 @@ const runTests = (context, dev = false) => {
})
it('should have correct router paths on first load of /hello', async () => {
- const browser = await webdriver(context.appPort, '/docs/hello')
+ const browser = await webdriver(appPort, `${basePath}/hello`)
await browser.waitForElementByCss('#pathname')
const pathname = await browser.elementByCss('#pathname').text()
expect(pathname).toBe('/hello')
@@ -517,7 +831,7 @@ const runTests = (context, dev = false) => {
})
it('should fetch data for getStaticProps without reloading', async () => {
- const browser = await webdriver(context.appPort, '/docs/hello')
+ const browser = await webdriver(appPort, `${basePath}/hello`)
await browser.eval('window.beforeNavigate = true')
await browser.elementByCss('#gsp-link').click()
await browser.waitForElementByCss('#gsp')
@@ -531,7 +845,7 @@ const runTests = (context, dev = false) => {
})
it('should fetch data for getServerSideProps without reloading', async () => {
- const browser = await webdriver(context.appPort, '/docs/hello')
+ const browser = await webdriver(appPort, `${basePath}/hello`)
await browser.eval('window.beforeNavigate = true')
await browser.elementByCss('#gssp-link').click()
await browser.waitForElementByCss('#gssp')
@@ -547,27 +861,27 @@ const runTests = (context, dev = false) => {
})
it('should have correct href for a link', async () => {
- const browser = await webdriver(context.appPort, '/docs/hello')
+ const browser = await webdriver(appPort, `${basePath}/hello`)
const href = await browser.elementByCss('a').getAttribute('href')
const { pathname } = url.parse(href)
- expect(pathname).toBe('/docs/other-page')
+ expect(pathname).toBe(`${basePath}/other-page`)
})
it('should have correct href for a link to /', async () => {
- const browser = await webdriver(context.appPort, '/docs/link-to-root')
+ const browser = await webdriver(appPort, `${basePath}/link-to-root`)
const href = await browser.elementByCss('#link-back').getAttribute('href')
const { pathname } = url.parse(href)
- expect(pathname).toBe('/docs')
+ expect(pathname).toBe(`${basePath}`)
})
it('should show 404 for page not under the /docs prefix', async () => {
- const text = await renderViaHTTP(context.appPort, '/hello')
+ const text = await renderViaHTTP(appPort, '/hello')
expect(text).not.toContain('Hello World')
expect(text).toContain('This page could not be found')
})
it('should show the other-page page under the /docs prefix', async () => {
- const browser = await webdriver(context.appPort, '/docs/other-page')
+ const browser = await webdriver(appPort, `${basePath}/other-page`)
try {
const text = await browser.elementByCss('h1').text()
expect(text).toBe('Hello Other')
@@ -577,13 +891,13 @@ const runTests = (context, dev = false) => {
})
it('should have basePath field on Router', async () => {
- const html = await renderViaHTTP(context.appPort, '/docs/hello')
+ const html = await renderViaHTTP(appPort, `${basePath}/hello`)
const $ = cheerio.load(html)
- expect($('#base-path').text()).toBe('/docs')
+ expect($('#base-path').text()).toBe(`${basePath}`)
})
it('should navigate to the page without refresh', async () => {
- const browser = await webdriver(context.appPort, '/docs/hello')
+ const browser = await webdriver(appPort, `${basePath}/hello`)
try {
await browser.eval('window.itdidnotrefresh = "hello"')
const text = await browser
@@ -601,7 +915,7 @@ const runTests = (context, dev = false) => {
})
it('should use urls with basepath in router events', async () => {
- const browser = await webdriver(context.appPort, '/docs/hello')
+ const browser = await webdriver(appPort, `${basePath}/hello`)
try {
await browser.eval('window._clearEventLog()')
await browser
@@ -611,9 +925,9 @@ const runTests = (context, dev = false) => {
const eventLog = await browser.eval('window._getEventLog()')
expect(eventLog).toEqual([
- ['routeChangeStart', '/docs/other-page'],
- ['beforeHistoryChange', '/docs/other-page'],
- ['routeChangeComplete', '/docs/other-page'],
+ ['routeChangeStart', `${basePath}/other-page`],
+ ['beforeHistoryChange', `${basePath}/other-page`],
+ ['routeChangeComplete', `${basePath}/other-page`],
])
} finally {
await browser.close()
@@ -621,15 +935,15 @@ const runTests = (context, dev = false) => {
})
it('should use urls with basepath in router events for hash changes', async () => {
- const browser = await webdriver(context.appPort, '/docs/hello')
+ const browser = await webdriver(appPort, `${basePath}/hello`)
try {
await browser.eval('window._clearEventLog()')
await browser.elementByCss('#hash-change').click()
const eventLog = await browser.eval('window._getEventLog()')
expect(eventLog).toEqual([
- ['hashChangeStart', '/docs/hello#some-hash'],
- ['hashChangeComplete', '/docs/hello#some-hash'],
+ ['hashChangeStart', `${basePath}/hello#some-hash`],
+ ['hashChangeComplete', `${basePath}/hello#some-hash`],
])
} finally {
await browser.close()
@@ -637,7 +951,7 @@ const runTests = (context, dev = false) => {
})
it('should use urls with basepath in router events for cancelled routes', async () => {
- const browser = await webdriver(context.appPort, '/docs/hello')
+ const browser = await webdriver(appPort, `${basePath}/hello`)
try {
await browser.eval('window._clearEventLog()')
await browser
@@ -649,11 +963,11 @@ const runTests = (context, dev = false) => {
const eventLog = await browser.eval('window._getEventLog()')
expect(eventLog).toEqual([
- ['routeChangeStart', '/docs/slow-route'],
- ['routeChangeError', 'Route Cancelled', true, '/docs/slow-route'],
- ['routeChangeStart', '/docs/other-page'],
- ['beforeHistoryChange', '/docs/other-page'],
- ['routeChangeComplete', '/docs/other-page'],
+ ['routeChangeStart', `${basePath}/slow-route`],
+ ['routeChangeError', 'Route Cancelled', true, `${basePath}/slow-route`],
+ ['routeChangeStart', `${basePath}/other-page`],
+ ['beforeHistoryChange', `${basePath}/other-page`],
+ ['routeChangeComplete', `${basePath}/other-page`],
])
} finally {
await browser.close()
@@ -661,7 +975,7 @@ const runTests = (context, dev = false) => {
})
it('should use urls with basepath in router events for failed route change', async () => {
- const browser = await webdriver(context.appPort, '/docs/hello')
+ const browser = await webdriver(appPort, `${basePath}/hello`)
try {
await browser.eval('window._clearEventLog()')
await browser.elementByCss('#error-route').click()
@@ -670,12 +984,12 @@ const runTests = (context, dev = false) => {
const eventLog = await browser.eval('window._getEventLog()')
expect(eventLog).toEqual([
- ['routeChangeStart', '/docs/error-route'],
+ ['routeChangeStart', `${basePath}/error-route`],
[
'routeChangeError',
'Failed to load static props',
null,
- '/docs/error-route',
+ `${basePath}/error-route`,
],
])
} finally {
@@ -684,7 +998,7 @@ const runTests = (context, dev = false) => {
})
it('should allow URL query strings without refresh', async () => {
- const browser = await webdriver(context.appPort, '/docs/hello?query=true')
+ const browser = await webdriver(appPort, `${basePath}/hello?query=true`)
try {
await browser.eval('window.itdidnotrefresh = "hello"')
await new Promise((resolve, reject) => {
@@ -702,7 +1016,7 @@ const runTests = (context, dev = false) => {
})
it('should correctly replace state when same asPath but different url', async () => {
- const browser = await webdriver(context.appPort, '/docs')
+ const browser = await webdriver(appPort, `${basePath}`)
try {
await browser.elementByCss('#hello-link').click()
await browser.waitForElementByCss('#something-else-link')
@@ -719,386 +1033,94 @@ const runTests = (context, dev = false) => {
}
describe('basePath development', () => {
- let server
-
- let context = {}
-
beforeAll(async () => {
- context.appPort = await findPort()
- server = await launchApp(join(__dirname, '..'), context.appPort, {
+ appPort = await findPort()
+ app = await launchApp(join(__dirname, '..'), appPort, {
env: { __NEXT_TEST_WITH_DEVTOOL: 1 },
})
})
- afterAll(async () => {
- await killApp(server)
- })
-
- runTests(context, true)
-
- describe('Hot Module Reloading', () => {
- describe('delete a page and add it back', () => {
- it('should load the page properly', async () => {
- const contactPagePath = join(
- __dirname,
- '../',
- 'pages',
- 'hmr',
- 'contact.js'
- )
- const newContactPagePath = join(
- __dirname,
- '../',
- 'pages',
- 'hmr',
- '_contact.js'
- )
- let browser
- try {
- browser = await webdriver(context.appPort, '/docs/hmr/contact')
- const text = await browser.elementByCss('p').text()
- expect(text).toBe('This is the contact page.')
-
- // Rename the file to mimic a deleted page
- renameSync(contactPagePath, newContactPagePath)
-
- await check(
- () => getBrowserBodyText(browser),
- /This page could not be found/
- )
+ afterAll(() => killApp(app))
- // Rename the file back to the original filename
- renameSync(newContactPagePath, contactPagePath)
-
- // wait until the page comes back
- await check(
- () => getBrowserBodyText(browser),
- /This is the contact page/
- )
- } finally {
- if (browser) {
- await browser.close()
- }
- if (existsSync(newContactPagePath)) {
- renameSync(newContactPagePath, contactPagePath)
- }
- }
- })
- })
-
- describe('editing a page', () => {
- it('should detect the changes and display it', async () => {
- let browser
- try {
- browser = await webdriver(context.appPort, '/docs/hmr/about')
- const text = await browser.elementByCss('p').text()
- expect(text).toBe('This is the about page.')
-
- const aboutPagePath = join(
- __dirname,
- '../',
- 'pages',
- 'hmr',
- 'about.js'
- )
-
- const originalContent = readFileSync(aboutPagePath, 'utf8')
- const editedContent = originalContent.replace(
- 'This is the about page',
- 'COOL page'
- )
-
- // change the content
- writeFileSync(aboutPagePath, editedContent, 'utf8')
-
- await check(() => getBrowserBodyText(browser), /COOL page/)
-
- // add the original content
- writeFileSync(aboutPagePath, originalContent, 'utf8')
-
- await check(
- () => getBrowserBodyText(browser),
- /This is the about page/
- )
- } finally {
- if (browser) {
- await browser.close()
- }
- }
- })
-
- it('should not reload unrelated pages', async () => {
- let browser
- try {
- browser = await webdriver(context.appPort, '/docs/hmr/counter')
- const text = await browser
- .elementByCss('button')
- .click()
- .elementByCss('button')
- .click()
- .elementByCss('p')
- .text()
- expect(text).toBe('COUNT: 2')
-
- const aboutPagePath = join(
- __dirname,
- '../',
- 'pages',
- 'hmr',
- 'about.js'
- )
-
- const originalContent = readFileSync(aboutPagePath, 'utf8')
- const editedContent = originalContent.replace(
- 'This is the about page',
- 'COOL page'
- )
-
- // Change the about.js page
- writeFileSync(aboutPagePath, editedContent, 'utf8')
-
- // wait for 5 seconds
- await waitFor(5000)
-
- // Check whether the this page has reloaded or not.
- const newText = await browser.elementByCss('p').text()
- expect(newText).toBe('COUNT: 2')
-
- // restore the about page content.
- writeFileSync(aboutPagePath, originalContent, 'utf8')
- } finally {
- if (browser) {
- await browser.close()
- }
- }
- })
-
- // Added because of a regression in react-hot-loader, see issues: #4246 #4273
- // Also: https://github.com/zeit/styled-jsx/issues/425
- it('should update styles correctly', async () => {
- let browser
- try {
- browser = await webdriver(context.appPort, '/docs/hmr/style')
- const pTag = await browser.elementByCss('.hmr-style-page p')
- const initialFontSize = await pTag.getComputedCss('font-size')
-
- expect(initialFontSize).toBe('100px')
-
- const pagePath = join(__dirname, '../', 'pages', 'hmr', 'style.js')
-
- const originalContent = readFileSync(pagePath, 'utf8')
- const editedContent = originalContent.replace('100px', '200px')
-
- // Change the page
- writeFileSync(pagePath, editedContent, 'utf8')
-
- try {
- // Check whether the this page has reloaded or not.
- await check(async () => {
- const editedPTag = await browser.elementByCss('.hmr-style-page p')
- return editedPTag.getComputedCss('font-size')
- }, /200px/)
- } finally {
- // Finally is used so that we revert the content back to the original regardless of the test outcome
- // restore the about page content.
- writeFileSync(pagePath, originalContent, 'utf8')
- }
- } finally {
- if (browser) {
- await browser.close()
- }
- }
- })
-
- // Added because of a regression in react-hot-loader, see issues: #4246 #4273
- // Also: https://github.com/zeit/styled-jsx/issues/425
- it('should update styles in a stateful component correctly', async () => {
- let browser
- const pagePath = join(
- __dirname,
- '../',
- 'pages',
- 'hmr',
- 'style-stateful-component.js'
- )
- const originalContent = readFileSync(pagePath, 'utf8')
- try {
- browser = await webdriver(
- context.appPort,
- '/docs/hmr/style-stateful-component'
- )
- const pTag = await browser.elementByCss('.hmr-style-page p')
- const initialFontSize = await pTag.getComputedCss('font-size')
-
- expect(initialFontSize).toBe('100px')
- const editedContent = originalContent.replace('100px', '200px')
-
- // Change the page
- writeFileSync(pagePath, editedContent, 'utf8')
-
- // Check whether the this page has reloaded or not.
- await check(async () => {
- const editedPTag = await browser.elementByCss('.hmr-style-page p')
- return editedPTag.getComputedCss('font-size')
- }, /200px/)
- } finally {
- if (browser) {
- await browser.close()
- }
- writeFileSync(pagePath, originalContent, 'utf8')
- }
- })
-
- // Added because of a regression in react-hot-loader, see issues: #4246 #4273
- // Also: https://github.com/zeit/styled-jsx/issues/425
- it('should update styles in a dynamic component correctly', async () => {
- let browser = null
- let secondBrowser = null
- const pagePath = join(
- __dirname,
- '../',
- 'components',
- 'hmr',
- 'dynamic.js'
- )
- const originalContent = readFileSync(pagePath, 'utf8')
- try {
- browser = await webdriver(
- context.appPort,
- '/docs/hmr/style-dynamic-component'
- )
- const div = await browser.elementByCss('#dynamic-component')
- const initialClientClassName = await div.getAttribute('class')
- const initialFontSize = await div.getComputedCss('font-size')
-
- expect(initialFontSize).toBe('100px')
-
- const initialHtml = await renderViaHTTP(
- context.appPort,
- '/docs/hmr/style-dynamic-component'
- )
- expect(initialHtml.includes('100px')).toBeTruthy()
-
- const $initialHtml = cheerio.load(initialHtml)
- const initialServerClassName = $initialHtml(
- '#dynamic-component'
- ).attr('class')
-
- expect(initialClientClassName === initialServerClassName).toBeTruthy()
-
- const editedContent = originalContent.replace('100px', '200px')
-
- // Change the page
- writeFileSync(pagePath, editedContent, 'utf8')
-
- // wait for 5 seconds
- await waitFor(5000)
-
- secondBrowser = await webdriver(
- context.appPort,
- '/docs/hmr/style-dynamic-component'
- )
- // Check whether the this page has reloaded or not.
- const editedDiv = await secondBrowser.elementByCss(
- '#dynamic-component'
- )
- const editedClientClassName = await editedDiv.getAttribute('class')
- const editedFontSize = await editedDiv.getComputedCss('font-size')
- const browserHtml = await secondBrowser
- .elementByCss('html')
- .getAttribute('innerHTML')
-
- expect(editedFontSize).toBe('200px')
- expect(browserHtml.includes('font-size:200px;')).toBe(true)
- expect(browserHtml.includes('font-size:100px;')).toBe(false)
-
- const editedHtml = await renderViaHTTP(
- context.appPort,
- '/docs/hmr/style-dynamic-component'
- )
- expect(editedHtml.includes('200px')).toBeTruthy()
- const $editedHtml = cheerio.load(editedHtml)
- const editedServerClassName = $editedHtml('#dynamic-component').attr(
- 'class'
- )
-
- expect(editedClientClassName === editedServerClassName).toBe(true)
- } finally {
- // Finally is used so that we revert the content back to the original regardless of the test outcome
- // restore the about page content.
- writeFileSync(pagePath, originalContent, 'utf8')
-
- if (browser) {
- await browser.close()
- }
+ runTests(true)
+})
- if (secondBrowser) {
- secondBrowser.close()
- }
- }
- })
+describe('basePath production', () => {
+ beforeAll(async () => {
+ await nextBuild(appDir, [], {
+ env: {
+ EXTERNAL_APP: `http://localhost:${externalApp.address().port}`,
+ },
})
+ appPort = await findPort()
+ app = await nextStart(appDir, appPort)
})
+ afterAll(() => killApp(app))
+
+ runTests()
it('should respect basePath in amphtml link rel', async () => {
- const html = await renderViaHTTP(context.appPort, '/docs/amp-hybrid')
+ const html = await renderViaHTTP(appPort, `${basePath}/amp-hybrid`)
const $ = cheerio.load(html)
- const expectedAmpHtmlUrl = '/docs/amp-hybrid?amp=1'
+ const expectedAmpHtmlUrl = `${basePath}/amp-hybrid.amp`
expect($('link[rel=amphtml]').first().attr('href')).toBe(expectedAmpHtmlUrl)
})
})
-describe('basePath production', () => {
- let context = {}
- let server
- let app
+describe('multi-level basePath development', () => {
+ beforeAll(async () => {
+ basePath = '/hello/world'
+ nextConfig.replace(`basePath: '/docs'`, `basePath: '/hello/world'`)
+ appPort = await findPort()
+ app = await launchApp(join(__dirname, '..'), appPort, {
+ env: { __NEXT_TEST_WITH_DEVTOOL: 1 },
+ })
+ })
+ afterAll(async () => {
+ basePath = '/docs'
+ nextConfig.replace(`basePath: '/hello/world'`, `basePath: '/docs'`)
+ await killApp(app)
+ })
+
+ runTests(true)
+})
+describe('multi-level basePath production', () => {
beforeAll(async () => {
+ basePath = '/hello/world'
+ nextConfig.replace(`basePath: '/docs'`, `basePath: '/hello/world'`)
await nextBuild(appDir, [], {
env: {
EXTERNAL_APP: `http://localhost:${externalApp.address().port}`,
},
})
- app = nextServer({
- dir: join(__dirname, '../'),
- dev: false,
- quiet: true,
- })
-
- server = await startApp(app)
- context.appPort = server.address().port
+ appPort = await findPort()
+ app = await nextStart(appDir, appPort)
})
-
- afterAll(() => stopApp(server))
-
- runTests(context)
-
- it('should respect basePath in amphtml link rel', async () => {
- const html = await renderViaHTTP(context.appPort, '/docs/amp-hybrid')
- const $ = cheerio.load(html)
- const expectedAmpHtmlUrl = '/docs/amp-hybrid.amp'
- expect($('link[rel=amphtml]').first().attr('href')).toBe(expectedAmpHtmlUrl)
+ afterAll(async () => {
+ basePath = '/docs'
+ nextConfig.replace(`basePath: '/hello/world'`, `basePath: '/docs'`)
+ await killApp(app)
})
+
+ runTests()
})
describe('basePath serverless', () => {
- let context = {}
- let app
-
beforeAll(async () => {
await nextConfig.replace(
'// replace me',
`target: 'experimental-serverless-trace',`
)
await nextBuild(appDir)
- context.appPort = await findPort()
- app = await nextStart(appDir, context.appPort)
+ appPort = await findPort()
+ app = await nextStart(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
await nextConfig.restore()
})
- runTests(context)
+ runTests()
it('should always strip basePath in serverless-loader', async () => {
const appPort = await findPort()
diff --git a/test/integration/build-output/test/index.test.js b/test/integration/build-output/test/index.test.js
index 7e32ab2dbb4f8..f58ce529aa060 100644
--- a/test/integration/build-output/test/index.test.js
+++ b/test/integration/build-output/test/index.test.js
@@ -95,16 +95,16 @@ describe('Build Output', () => {
expect(indexSize.endsWith('B')).toBe(true)
// should be no bigger than 60.8 kb
- expect(parseFloat(indexFirstLoad) - 61.2).toBeLessThanOrEqual(0)
+ expect(parseFloat(indexFirstLoad) - 61.3).toBeLessThanOrEqual(0)
expect(indexFirstLoad.endsWith('kB')).toBe(true)
expect(parseFloat(err404Size) - 3.5).toBeLessThanOrEqual(0)
expect(err404Size.endsWith('kB')).toBe(true)
- expect(parseFloat(err404FirstLoad) - 64.4).toBeLessThanOrEqual(0)
+ expect(parseFloat(err404FirstLoad) - 64.5).toBeLessThanOrEqual(0)
expect(err404FirstLoad.endsWith('kB')).toBe(true)
- expect(parseFloat(sharedByAll) - 61).toBeLessThanOrEqual(0)
+ expect(parseFloat(sharedByAll) - 61.1).toBeLessThanOrEqual(0)
expect(sharedByAll.endsWith('kB')).toBe(true)
if (_appSize.endsWith('kB')) {
diff --git a/test/integration/gssp-redirect/pages/gsp-blog/[post].js b/test/integration/gssp-redirect/pages/gsp-blog/[post].js
index cd3fae7419a73..65c129cbe99e5 100644
--- a/test/integration/gssp-redirect/pages/gsp-blog/[post].js
+++ b/test/integration/gssp-redirect/pages/gsp-blog/[post].js
@@ -25,10 +25,27 @@ export const getStaticProps = ({ params }) => {
destination = params.post.split('dest-').pop().replace(/_/g, '/')
}
+ let permanent = undefined
+ let statusCode = undefined
+
+ if (params.post.includes('statusCode-')) {
+ permanent = parseInt(
+ params.post.split('statusCode-').pop().split('-').shift(),
+ 10
+ )
+ }
+
+ if (params.post.includes('permanent')) {
+ permanent = true
+ } else if (!statusCode) {
+ permanent = false
+ }
+
return {
redirect: {
destination,
- permanent: params.post.includes('permanent'),
+ permanent,
+ statusCode,
},
}
}
diff --git a/test/integration/gssp-redirect/pages/gssp-blog/[post].js b/test/integration/gssp-redirect/pages/gssp-blog/[post].js
index d01d19f31ae49..0afeb687d1be0 100644
--- a/test/integration/gssp-redirect/pages/gssp-blog/[post].js
+++ b/test/integration/gssp-redirect/pages/gssp-blog/[post].js
@@ -15,10 +15,27 @@ export const getServerSideProps = ({ params }) => {
destination = params.post.split('dest-').pop().replace(/_/g, '/')
}
+ let permanent = undefined
+ let statusCode = undefined
+
+ if (params.post.includes('statusCode-')) {
+ statusCode = parseInt(
+ params.post.split('statusCode-').pop().split('-').shift(),
+ 10
+ )
+ }
+
+ if (params.post.includes('permanent')) {
+ permanent = true
+ } else if (!statusCode) {
+ permanent = false
+ }
+
return {
redirect: {
destination,
- permanent: params.post.includes('permanent'),
+ permanent,
+ statusCode,
},
}
}
diff --git a/test/integration/gssp-redirect/test/index.test.js b/test/integration/gssp-redirect/test/index.test.js
index 4852aac6ad91d..964f0afb00429 100644
--- a/test/integration/gssp-redirect/test/index.test.js
+++ b/test/integration/gssp-redirect/test/index.test.js
@@ -55,6 +55,40 @@ const runTests = () => {
expect(res.headers.get('refresh')).toMatch(/url=\/404/)
})
+ it('should apply statusCode 301 redirect when visited directly for GSSP page', async () => {
+ const res = await fetchViaHTTP(
+ appPort,
+ '/gssp-blog/redirect-statusCode-301',
+ undefined,
+ {
+ redirect: 'manual',
+ }
+ )
+ expect(res.status).toBe(301)
+
+ const { pathname } = url.parse(res.headers.get('location'))
+
+ expect(pathname).toBe('/404')
+ expect(res.headers.get('refresh')).toBe(null)
+ })
+
+ it('should apply statusCode 303 redirect when visited directly for GSSP page', async () => {
+ const res = await fetchViaHTTP(
+ appPort,
+ '/gssp-blog/redirect-statusCode-303',
+ undefined,
+ {
+ redirect: 'manual',
+ }
+ )
+ expect(res.status).toBe(303)
+
+ const { pathname } = url.parse(res.headers.get('location'))
+
+ expect(pathname).toBe('/404')
+ expect(res.headers.get('refresh')).toBe(null)
+ })
+
it('should apply redirect when fallback GSP page is visited directly (internal dynamic)', async () => {
const browser = await webdriver(
appPort,
diff --git a/test/integration/i18n-support/next.config.js b/test/integration/i18n-support/next.config.js
index e6aec3f6e6bab..2493a70593d7e 100644
--- a/test/integration/i18n-support/next.config.js
+++ b/test/integration/i18n-support/next.config.js
@@ -1,6 +1,7 @@
module.exports = {
// target: 'experimental-serverless-trace',
i18n: {
+ // localeDetection: false,
locales: ['nl-NL', 'nl-BE', 'nl', 'fr-BE', 'fr', 'en-US', 'en'],
defaultLocale: 'en-US',
domains: [
@@ -20,4 +21,70 @@ module.exports = {
},
],
},
+ async redirects() {
+ return [
+ {
+ source: '/en-US/redirect',
+ destination: '/somewhere-else',
+ permanent: false,
+ },
+ {
+ source: '/nl/redirect',
+ destination: '/somewhere-else',
+ permanent: false,
+ },
+ {
+ source: '/redirect',
+ destination: '/somewhere-else',
+ permanent: false,
+ },
+ ]
+ },
+ async rewrites() {
+ return [
+ {
+ source: '/en-US/rewrite',
+ destination: '/another',
+ },
+ {
+ source: '/nl/rewrite',
+ destination: '/another',
+ },
+ {
+ source: '/rewrite',
+ destination: '/another',
+ },
+ ]
+ },
+ async headers() {
+ return [
+ {
+ source: '/en-US/add-header',
+ headers: [
+ {
+ key: 'x-hello',
+ value: 'world',
+ },
+ ],
+ },
+ {
+ source: '/nl/add-header',
+ headers: [
+ {
+ key: 'x-hello',
+ value: 'world',
+ },
+ ],
+ },
+ {
+ source: '/add-header',
+ headers: [
+ {
+ key: 'x-hello',
+ value: 'world',
+ },
+ ],
+ },
+ ]
+ },
}
diff --git a/test/integration/i18n-support/pages/404.js b/test/integration/i18n-support/pages/404.js
index f228ca559b1c4..d5056521f6f55 100644
--- a/test/integration/i18n-support/pages/404.js
+++ b/test/integration/i18n-support/pages/404.js
@@ -7,10 +7,12 @@ export default function NotFound(props) {
)
}
-export const getStaticProps = ({ locale }) => {
+export const getStaticProps = ({ locale, locales, defaultLocale }) => {
return {
props: {
locale,
+ locales,
+ defaultLocale,
is404: true,
},
}
diff --git a/test/integration/i18n-support/pages/_app.js b/test/integration/i18n-support/pages/_app.js
new file mode 100644
index 0000000000000..51785b1cc8ca5
--- /dev/null
+++ b/test/integration/i18n-support/pages/_app.js
@@ -0,0 +1,19 @@
+if (typeof window !== 'undefined') {
+ window.caughtWarns = []
+
+ const origWarn = window.console.warn
+ const origError = window.console.error
+
+ window.console.warn = function (...args) {
+ window.caughtWarns.push(args.join(' '))
+ origWarn(...args)
+ }
+ window.console.error = function (...args) {
+ window.caughtWarns.push(args.join(' '))
+ origError(...args)
+ }
+}
+
+export default function MyApp({ Component, pageProps }) {
+ return
+}
diff --git a/test/integration/i18n-support/pages/another.js b/test/integration/i18n-support/pages/another.js
index d7d1febc49580..20cdbe8c89a30 100644
--- a/test/integration/i18n-support/pages/another.js
+++ b/test/integration/i18n-support/pages/another.js
@@ -9,6 +9,7 @@ export default function Page(props) {
another page
{JSON.stringify(props)}
{router.locale}
+ {router.defaultLocale}
{JSON.stringify(router.locales)}
{JSON.stringify(router.query)}
{router.pathname}
diff --git a/test/integration/i18n-support/pages/frank.js b/test/integration/i18n-support/pages/frank.js
new file mode 100644
index 0000000000000..56d0ebaea4ff1
--- /dev/null
+++ b/test/integration/i18n-support/pages/frank.js
@@ -0,0 +1,56 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+export default function Page(props) {
+ const router = useRouter()
+
+ return (
+ <>
+ frank page
+ {JSON.stringify(props)}
+ {router.locale}
+ {JSON.stringify(router.locales)}
+ {JSON.stringify(router.query)}
+ {router.pathname}
+ {router.asPath}
+
+ to /another
+
+
+
+ to /gsp
+
+
+
+ to /gsp/fallback/first
+
+
+
+ to /gsp/fallback/hello
+
+
+
+ to /gsp/no-fallback/first
+
+
+
+ to /gssp
+
+
+
+ to /gssp/first
+
+
+ >
+ )
+}
+
+export const getStaticProps = ({ locale, locales, defaultLocale }) => {
+ return {
+ props: {
+ locale,
+ locales,
+ defaultLocale,
+ },
+ }
+}
diff --git a/test/integration/i18n-support/pages/gsp/fallback/[slug].js b/test/integration/i18n-support/pages/gsp/fallback/[slug].js
index d0f6e91503912..c45fe9a10c8a8 100644
--- a/test/integration/i18n-support/pages/gsp/fallback/[slug].js
+++ b/test/integration/i18n-support/pages/gsp/fallback/[slug].js
@@ -11,6 +11,7 @@ export default function Page(props) {
gsp page
{JSON.stringify(props)}
{router.locale}
+ {router.defaultLocale}
{JSON.stringify(router.locales)}
{JSON.stringify(router.query)}
{router.pathname}
@@ -24,6 +25,11 @@ export default function Page(props) {
}
export const getStaticProps = ({ params, locale, locales, defaultLocale }) => {
+ // ensure getStaticProps isn't called without params
+ if (!params || !params.slug) {
+ throw new Error(`missing params ${JSON.stringify(params)}`)
+ }
+
return {
props: {
params,
diff --git a/test/integration/i18n-support/pages/gsp/no-fallback/[slug].js b/test/integration/i18n-support/pages/gsp/no-fallback/[slug].js
index a97bd749443a1..91a4d74bea583 100644
--- a/test/integration/i18n-support/pages/gsp/no-fallback/[slug].js
+++ b/test/integration/i18n-support/pages/gsp/no-fallback/[slug].js
@@ -11,6 +11,7 @@ export default function Page(props) {
gsp page
{JSON.stringify(props)}
{router.locale}
+ {router.defaultLocale}
{JSON.stringify(router.locales)}
{JSON.stringify(router.query)}
{router.pathname}
@@ -24,6 +25,11 @@ export default function Page(props) {
}
export const getStaticProps = ({ params, locale, locales, defaultLocale }) => {
+ // ensure getStaticProps isn't called without params
+ if (!params || !params.slug) {
+ throw new Error(`missing params ${JSON.stringify(params)}`)
+ }
+
return {
props: {
params,
diff --git a/test/integration/i18n-support/pages/index.js b/test/integration/i18n-support/pages/index.js
index 345e004151c9f..c5cbdf31d99f2 100644
--- a/test/integration/i18n-support/pages/index.js
+++ b/test/integration/i18n-support/pages/index.js
@@ -9,6 +9,7 @@ export default function Page(props) {
index page
{JSON.stringify(props)}
{router.locale}
+ {router.defaultLocale}
{JSON.stringify(router.locales)}
{JSON.stringify(router.query)}
{router.pathname}
diff --git a/test/integration/i18n-support/pages/links.js b/test/integration/i18n-support/pages/links.js
index 9e358b7900206..247a92d33dad5 100644
--- a/test/integration/i18n-support/pages/links.js
+++ b/test/integration/i18n-support/pages/links.js
@@ -10,6 +10,7 @@ export default function Page(props) {
links page
{JSON.stringify(props)}
{router.locale}
+ {router.defaultLocale}
{JSON.stringify(router.locales)}
{JSON.stringify(router.query)}
{router.pathname}
diff --git a/test/integration/i18n-support/pages/not-found/blocking-fallback/[slug].js b/test/integration/i18n-support/pages/not-found/blocking-fallback/[slug].js
index 7257f7422968d..c17c1250573e7 100644
--- a/test/integration/i18n-support/pages/not-found/blocking-fallback/[slug].js
+++ b/test/integration/i18n-support/pages/not-found/blocking-fallback/[slug].js
@@ -22,6 +22,11 @@ export default function Page(props) {
}
export const getStaticProps = ({ params, locale, locales }) => {
+ // ensure getStaticProps isn't called without params
+ if (!params || !params.slug) {
+ throw new Error(`missing params ${JSON.stringify(params)}`)
+ }
+
if (locale === 'en' || locale === 'nl') {
return {
notFound: true,
diff --git a/test/integration/i18n-support/pages/not-found/fallback/[slug].js b/test/integration/i18n-support/pages/not-found/fallback/[slug].js
index 0fde7256d1aae..6bca78d173b1b 100644
--- a/test/integration/i18n-support/pages/not-found/fallback/[slug].js
+++ b/test/integration/i18n-support/pages/not-found/fallback/[slug].js
@@ -24,6 +24,11 @@ export default function Page(props) {
}
export const getStaticProps = ({ params, locale, locales }) => {
+ // ensure getStaticProps isn't called without params
+ if (!params || !params.slug) {
+ throw new Error(`missing params ${JSON.stringify(params)}`)
+ }
+
if (locale === 'en' || locale === 'nl') {
return {
notFound: true,
diff --git a/test/integration/i18n-support/test/index.test.js b/test/integration/i18n-support/test/index.test.js
index ae28017776676..8ece675dcab96 100644
--- a/test/integration/i18n-support/test/index.test.js
+++ b/test/integration/i18n-support/test/index.test.js
@@ -1,5 +1,6 @@
/* eslint-env jest */
+import http from 'http'
import url from 'url'
import fs from 'fs-extra'
import cheerio from 'cheerio'
@@ -17,6 +18,8 @@ import {
File,
waitFor,
normalizeRegEx,
+ getPageFileFromPagesManifest,
+ check,
} from 'next-test-utils'
jest.setTimeout(1000 * 60 * 2)
@@ -38,6 +41,49 @@ async function addDefaultLocaleCookie(browser) {
}
function runTests(isDev) {
+ it('should have correct values for non-prefixed path', async () => {
+ for (const paths of [
+ ['/links', '/links'],
+ ['/another', '/another'],
+ ['/gsp/fallback/first', '/gsp/fallback/[slug]'],
+ ['/gsp/no-fallback/first', '/gsp/no-fallback/[slug]'],
+ ]) {
+ const [asPath, pathname] = paths
+
+ const res = await fetchViaHTTP(appPort, asPath, undefined, {
+ redirect: 'manual',
+ headers: {
+ 'accept-language': 'fr',
+ },
+ })
+
+ expect(res.status).toBe(200)
+ const $ = cheerio.load(await res.text())
+ expect($('html').attr('lang')).toBe('en-US')
+ expect($('#router-locale').text()).toBe('en-US')
+ expect($('#router-default-locale').text()).toBe('en-US')
+ expect(JSON.parse($('#router-locales').text())).toEqual(locales)
+ expect($('#router-pathname').text()).toBe(pathname)
+ expect($('#router-as-path').text()).toBe(asPath)
+ }
+ })
+
+ it('should not have hydration mis-match from hash', async () => {
+ const browser = await webdriver(appPort, '/en#')
+
+ expect(await browser.elementByCss('html').getAttribute('lang')).toBe('en')
+ expect(await browser.elementByCss('#router-locale').text()).toBe('en')
+ expect(await browser.elementByCss('#router-default-locale').text()).toBe(
+ 'en-US'
+ )
+ expect(
+ JSON.parse(await browser.elementByCss('#router-locales').text())
+ ).toEqual(locales)
+ expect(await browser.elementByCss('#router-pathname').text()).toBe('/')
+ expect(await browser.elementByCss('#router-as-path').text()).toBe('/')
+ expect(await browser.eval('window.caughtWarns')).toEqual([])
+ })
+
if (!isDev) {
it('should add i18n config to routes-manifest', async () => {
const routesManifest = await fs.readJSON(
@@ -126,6 +172,11 @@ function runTests(isDev) {
initialRevalidateSeconds: false,
srcRoute: '/not-found/fallback/[slug]',
},
+ '/frank': {
+ dataRoute: `/_next/data/${buildId}/frank.json`,
+ initialRevalidateSeconds: false,
+ srcRoute: null,
+ },
'/gsp': {
dataRoute: `/_next/data/${buildId}/gsp.json`,
srcRoute: null,
@@ -193,11 +244,94 @@ function runTests(isDev) {
})
}
+ it('should apply redirects correctly', async () => {
+ for (const path of ['/redirect', '/en-US/redirect', '/nl/redirect']) {
+ const res = await fetchViaHTTP(appPort, path, undefined, {
+ redirect: 'manual',
+ })
+ expect(res.status).toBe(307)
+
+ const parsed = url.parse(res.headers.get('location'), true)
+ expect(parsed.pathname).toBe('/somewhere-else')
+ expect(parsed.query).toEqual({})
+ }
+ })
+
+ it('should apply headers correctly', async () => {
+ for (const path of ['/add-header', '/en-US/add-header', '/nl/add-header']) {
+ const res = await fetchViaHTTP(appPort, path, undefined, {
+ redirect: 'manual',
+ })
+ expect(res.status).toBe(404)
+ expect(res.headers.get('x-hello')).toBe('world')
+ }
+ })
+
+ it('should apply rewrites correctly', async () => {
+ const checks = [
+ {
+ locale: 'en-US',
+ path: '/rewrite',
+ },
+ {
+ locale: 'en-US',
+ path: '/en-US/rewrite',
+ },
+ {
+ locale: 'nl',
+ path: '/nl/rewrite',
+ },
+ ]
+
+ for (const check of checks) {
+ const res = await fetchViaHTTP(appPort, check.path, undefined, {
+ redirect: 'manual',
+ })
+
+ expect(res.status).toBe(200)
+
+ const html = await res.text()
+ const $ = cheerio.load(html)
+ expect($('html').attr('lang')).toBe(check.locale)
+ expect($('#router-locale').text()).toBe(check.locale)
+ expect($('#router-pathname').text()).toBe('/another')
+ expect($('#router-as-path').text()).toBe('/rewrite')
+ }
+ })
+
it('should navigate with locale prop correctly', async () => {
const browser = await webdriver(appPort, '/links?nextLocale=fr')
await addDefaultLocaleCookie(browser)
await browser.eval('window.beforeNav = 1')
+ if (!isDev) {
+ await browser.eval(`(function() {
+ document.querySelector('#to-gsp').scrollIntoView()
+ document.querySelector('#to-fallback-first').scrollIntoView()
+ document.querySelector('#to-no-fallback-first').scrollIntoView()
+ })()`)
+
+ await check(async () => {
+ for (const dataPath of [
+ '/fr/gsp.json',
+ '/fr/gsp/fallback/first.json',
+ '/fr/gsp/fallback/hello.json',
+ ]) {
+ const found = await browser.eval(`(function() {
+ const links = [].slice.call(document.querySelectorAll('link'))
+
+ for (var i = 0; i < links.length; i++) {
+ if (links[i].href.indexOf("${dataPath}") > -1) {
+ return true
+ }
+ }
+ return false
+ })()`)
+ return found ? 'yes' : 'no'
+ }
+ }, 'yes')
+ }
+
expect(await browser.elementByCss('#router-pathname').text()).toBe('/links')
expect(await browser.elementByCss('#router-as-path').text()).toBe(
'/links?nextLocale=fr'
@@ -209,6 +343,9 @@ function runTests(isDev) {
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({ nextLocale: 'fr' })
+ expect(await browser.elementByCss('html').getAttribute('lang')).toBe(
+ 'en-US'
+ )
await browser.elementByCss('#to-another').click()
await browser.waitForElementByCss('#another')
@@ -226,6 +363,7 @@ function runTests(isDev) {
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({})
+ expect(await browser.elementByCss('html').getAttribute('lang')).toBe('fr')
let parsedUrl = url.parse(await browser.eval('window.location.href'), true)
expect(parsedUrl.pathname).toBe('/fr/another')
@@ -245,6 +383,9 @@ function runTests(isDev) {
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({ nextLocale: 'fr' })
+ expect(await browser.elementByCss('html').getAttribute('lang')).toBe(
+ 'en-US'
+ )
parsedUrl = url.parse(await browser.eval('window.location.href'), true)
expect(parsedUrl.pathname).toBe('/links')
@@ -266,16 +407,19 @@ function runTests(isDev) {
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({})
+ expect(await browser.elementByCss('html').getAttribute('lang')).toBe('fr')
parsedUrl = url.parse(await browser.eval('window.location.href'), true)
expect(parsedUrl.pathname).toBe('/fr/another')
expect(parsedUrl.query).toEqual({})
expect(await browser.eval('window.beforeNav')).toBe(1)
+ expect(await browser.eval('window.caughtWarns')).toEqual([])
})
it('should navigate with locale prop correctly GSP', async () => {
const browser = await webdriver(appPort, '/links?nextLocale=nl')
await addDefaultLocaleCookie(browser)
+ await browser.eval('window.beforeNav = 1')
expect(await browser.elementByCss('#router-pathname').text()).toBe('/links')
expect(await browser.elementByCss('#router-as-path').text()).toBe(
@@ -288,6 +432,9 @@ function runTests(isDev) {
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({ nextLocale: 'nl' })
+ expect(await browser.elementByCss('html').getAttribute('lang')).toBe(
+ 'en-US'
+ )
await browser.elementByCss('#to-fallback-first').click()
await browser.waitForElementByCss('#gsp')
@@ -305,6 +452,7 @@ function runTests(isDev) {
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({ slug: 'first' })
+ expect(await browser.elementByCss('html').getAttribute('lang')).toBe('nl')
let parsedUrl = url.parse(await browser.eval('window.location.href'), true)
expect(parsedUrl.pathname).toBe('/nl/gsp/fallback/first')
@@ -324,6 +472,9 @@ function runTests(isDev) {
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({ nextLocale: 'nl' })
+ expect(await browser.elementByCss('html').getAttribute('lang')).toBe(
+ 'en-US'
+ )
parsedUrl = url.parse(await browser.eval('window.location.href'), true)
expect(parsedUrl.pathname).toBe('/links')
@@ -345,10 +496,13 @@ function runTests(isDev) {
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({ slug: 'first' })
+ expect(await browser.elementByCss('html').getAttribute('lang')).toBe('nl')
parsedUrl = url.parse(await browser.eval('window.location.href'), true)
expect(parsedUrl.pathname).toBe('/nl/gsp/fallback/first')
expect(parsedUrl.query).toEqual({})
+ expect(await browser.eval('window.beforeNav')).toBe(1)
+ expect(await browser.eval('window.caughtWarns')).toEqual([])
})
it('should navigate with locale false correctly', async () => {
@@ -356,6 +510,34 @@ function runTests(isDev) {
await addDefaultLocaleCookie(browser)
await browser.eval('window.beforeNav = 1')
+ if (!isDev) {
+ await browser.eval(`(function() {
+ document.querySelector('#to-gsp').scrollIntoView()
+ document.querySelector('#to-fallback-first').scrollIntoView()
+ document.querySelector('#to-no-fallback-first').scrollIntoView()
+ })()`)
+
+ await check(async () => {
+ for (const dataPath of [
+ '/fr/gsp.json',
+ '/fr/gsp/fallback/first.json',
+ '/fr/gsp/fallback/hello.json',
+ ]) {
+ const found = await browser.eval(`(function() {
+ const links = [].slice.call(document.querySelectorAll('link'))
+
+ for (var i = 0; i < links.length; i++) {
+ if (links[i].href.indexOf("${dataPath}") > -1) {
+ return true
+ }
+ }
+ return false
+ })()`)
+ return found ? 'yes' : 'no'
+ }
+ }, 'yes')
+ }
+
expect(await browser.elementByCss('#router-pathname').text()).toBe(
'/locale-false'
)
@@ -369,6 +551,9 @@ function runTests(isDev) {
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({ nextLocale: 'fr' })
+ expect(await browser.elementByCss('html').getAttribute('lang')).toBe(
+ 'en-US'
+ )
await browser.elementByCss('#to-another').click()
await browser.waitForElementByCss('#another')
@@ -386,6 +571,7 @@ function runTests(isDev) {
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({})
+ expect(await browser.elementByCss('html').getAttribute('lang')).toBe('fr')
let parsedUrl = url.parse(await browser.eval('window.location.href'), true)
expect(parsedUrl.pathname).toBe('/fr/another')
@@ -407,6 +593,9 @@ function runTests(isDev) {
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({ nextLocale: 'fr' })
+ expect(await browser.elementByCss('html').getAttribute('lang')).toBe(
+ 'en-US'
+ )
parsedUrl = url.parse(await browser.eval('window.location.href'), true)
expect(parsedUrl.pathname).toBe('/locale-false')
@@ -428,16 +617,19 @@ function runTests(isDev) {
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({})
+ expect(await browser.elementByCss('html').getAttribute('lang')).toBe('fr')
parsedUrl = url.parse(await browser.eval('window.location.href'), true)
expect(parsedUrl.pathname).toBe('/fr/another')
expect(parsedUrl.query).toEqual({})
expect(await browser.eval('window.beforeNav')).toBe(1)
+ expect(await browser.eval('window.caughtWarns')).toEqual([])
})
it('should navigate with locale false correctly GSP', async () => {
const browser = await webdriver(appPort, '/locale-false?nextLocale=nl')
await addDefaultLocaleCookie(browser)
+ await browser.eval('window.beforeNav = 1')
expect(await browser.elementByCss('#router-pathname').text()).toBe(
'/locale-false'
@@ -452,6 +644,9 @@ function runTests(isDev) {
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({ nextLocale: 'nl' })
+ expect(await browser.elementByCss('html').getAttribute('lang')).toBe(
+ 'en-US'
+ )
await browser.elementByCss('#to-fallback-first').click()
await browser.waitForElementByCss('#gsp')
@@ -469,6 +664,7 @@ function runTests(isDev) {
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({ slug: 'first' })
+ expect(await browser.elementByCss('html').getAttribute('lang')).toBe('nl')
let parsedUrl = url.parse(await browser.eval('window.location.href'), true)
expect(parsedUrl.pathname).toBe('/nl/gsp/fallback/first')
@@ -490,6 +686,9 @@ function runTests(isDev) {
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({ nextLocale: 'nl' })
+ expect(await browser.elementByCss('html').getAttribute('lang')).toBe(
+ 'en-US'
+ )
parsedUrl = url.parse(await browser.eval('window.location.href'), true)
expect(parsedUrl.pathname).toBe('/locale-false')
@@ -511,10 +710,13 @@ function runTests(isDev) {
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({ slug: 'first' })
+ expect(await browser.elementByCss('html').getAttribute('lang')).toBe('nl')
parsedUrl = url.parse(await browser.eval('window.location.href'), true)
expect(parsedUrl.pathname).toBe('/nl/gsp/fallback/first')
expect(parsedUrl.query).toEqual({})
+ expect(await browser.eval('window.beforeNav')).toBe(1)
+ expect(await browser.eval('window.caughtWarns')).toEqual([])
})
it('should update asPath on the client correctly', async () => {
@@ -895,6 +1097,30 @@ function runTests(isDev) {
}
})
+ it('should transition on client properly for page that starts with locale', async () => {
+ const browser = await webdriver(appPort, '/fr')
+ await browser.eval(`(function() {
+ window.beforeNav = 1
+ window.next.router.push('/frank')
+ })()`)
+
+ await browser.waitForElementByCss('#frank')
+
+ expect(await browser.elementByCss('#router-locale').text()).toBe('fr')
+ expect(
+ JSON.parse(await browser.elementByCss('#router-locales').text())
+ ).toEqual(locales)
+ expect(
+ JSON.parse(await browser.elementByCss('#router-query').text())
+ ).toEqual({})
+ expect(await browser.elementByCss('#router-pathname').text()).toBe('/frank')
+ expect(await browser.elementByCss('#router-as-path').text()).toBe('/frank')
+ expect(
+ url.parse(await browser.eval(() => window.location.href)).pathname
+ ).toBe('/fr/frank')
+ expect(await browser.eval('window.beforeNav')).toBe(1)
+ })
+
it('should 404 for GSP that returned notFound on client-transition', async () => {
const browser = await webdriver(appPort, '/en')
await browser.eval(`(function() {
@@ -1435,7 +1661,7 @@ function runTests(isDev) {
it('should load getStaticProps non-fallback correctly another locale via cookie', async () => {
const html = await renderViaHTTP(
appPort,
- '/gsp/no-fallback/second',
+ '/nl-NL/gsp/no-fallback/second',
{},
{
headers: {
@@ -1633,6 +1859,135 @@ describe('i18n Support', () => {
await killApp(app)
})
+ it('should have correct props for blocking notFound', async () => {
+ const serverFile = getPageFileFromPagesManifest(
+ appDir,
+ '/not-found/blocking-fallback/[slug]'
+ )
+ const appPort = await findPort()
+ const mod = require(join(appDir, '.next/serverless', serverFile))
+
+ const server = http.createServer(async (req, res) => {
+ try {
+ await mod.render(req, res)
+ } catch (err) {
+ res.statusCode = 500
+ res.end('internal err')
+ }
+ })
+
+ await new Promise((resolve, reject) => {
+ server.listen(appPort, (err) => (err ? reject(err) : resolve()))
+ })
+ console.log('listening on', appPort)
+
+ const res = await fetchViaHTTP(
+ appPort,
+ '/nl/not-found/blocking-fallback/first'
+ )
+ server.close()
+
+ expect(res.status).toBe(404)
+
+ const $ = cheerio.load(await res.text())
+ const props = JSON.parse($('#props').text())
+
+ expect($('#not-found').text().length > 0).toBe(true)
+ expect(props).toEqual({
+ is404: true,
+ locale: 'nl',
+ locales,
+ defaultLocale: 'en-US',
+ })
+ })
+
runTests()
})
+
+ describe('with localeDetection disabled', () => {
+ beforeAll(async () => {
+ await fs.remove(join(appDir, '.next'))
+ nextConfig.replace('// localeDetection', 'localeDetection')
+
+ await nextBuild(appDir)
+ appPort = await findPort()
+ app = await nextStart(appDir, appPort)
+ })
+ afterAll(async () => {
+ nextConfig.restore()
+ await killApp(app)
+ })
+
+ it('should have localeDetection in routes-manifest', async () => {
+ const routesManifest = await fs.readJSON(
+ join(appDir, '.next/routes-manifest.json')
+ )
+
+ expect(routesManifest.i18n).toEqual({
+ localeDetection: false,
+ locales: ['en-US', 'nl-NL', 'nl-BE', 'nl', 'fr-BE', 'fr', 'en'],
+ defaultLocale: 'en-US',
+ domains: [
+ {
+ http: true,
+ domain: 'example.be',
+ defaultLocale: 'nl-BE',
+ locales: ['nl', 'nl-NL', 'nl-BE'],
+ },
+ {
+ http: true,
+ domain: 'example.fr',
+ defaultLocale: 'fr',
+ locales: ['fr-BE'],
+ },
+ ],
+ })
+ })
+
+ it('should not detect locale from accept-language', async () => {
+ const res = await fetchViaHTTP(
+ appPort,
+ '/',
+ {},
+ {
+ redirect: 'manual',
+ headers: {
+ 'accept-language': 'fr',
+ },
+ }
+ )
+
+ expect(res.status).toBe(200)
+ const $ = cheerio.load(await res.text())
+ expect($('html').attr('lang')).toBe('en-US')
+ expect($('#router-locale').text()).toBe('en-US')
+ expect(JSON.parse($('#router-locales').text())).toEqual(locales)
+ expect($('#router-pathname').text()).toBe('/')
+ expect($('#router-as-path').text()).toBe('/')
+ })
+
+ it('should set locale from detected path', async () => {
+ for (const locale of locales) {
+ const res = await fetchViaHTTP(
+ appPort,
+ `/${locale}`,
+ {},
+ {
+ redirect: 'manual',
+ headers: {
+ 'accept-language': 'en-US,en;q=0.9',
+ },
+ }
+ )
+
+ expect(res.status).toBe(200)
+ const $ = cheerio.load(await res.text())
+ expect($('html').attr('lang')).toBe(locale)
+ expect($('#router-locale').text()).toBe(locale)
+ expect(JSON.parse($('#router-locales').text())).toEqual(locales)
+ expect($('#router-pathname').text()).toBe('/')
+ expect($('#router-as-path').text()).toBe('/')
+ }
+ })
+ })
})
diff --git a/test/integration/image-component/basic/next.config.js b/test/integration/image-component/basic/next.config.js
index cdd95710263b7..5d8fb97cde549 100644
--- a/test/integration/image-component/basic/next.config.js
+++ b/test/integration/image-component/basic/next.config.js
@@ -1,7 +1,7 @@
module.exports = {
images: {
deviceSizes: [480, 1024, 1600, 2000],
- imageSizes: [16, 64],
+ imageSizes: [16, 32, 48, 64],
path: 'https://example.com/myaccount/',
loader: 'imgix',
},
diff --git a/test/integration/image-component/basic/pages/client-side.js b/test/integration/image-component/basic/pages/client-side.js
index ddd00fd2fe3e9..98f17df0cd3f6 100644
--- a/test/integration/image-component/basic/pages/client-side.js
+++ b/test/integration/image-component/basic/pages/client-side.js
@@ -54,11 +54,11 @@ const ClientSide = () => {
height={400}
/>
{
height={400}
/>
{
expect(await browser.elementById('basic-image').getAttribute('src')).toBe(
- 'https://example.com/myaccount/foo.jpg?auto=format&w=480&q=60'
+ 'https://example.com/myaccount/foo.jpg?auto=format&fit=max&w=1024&q=60'
)
})
it('should correctly generate src even if preceding slash is included in prop', async () => {
expect(
await browser.elementById('preceding-slash-image').getAttribute('src')
- ).toBe('https://example.com/myaccount/fooslash.jpg?auto=format&w=480')
+ ).toBe(
+ 'https://example.com/myaccount/fooslash.jpg?auto=format&fit=max&w=1024'
+ )
})
it('should add a srcset based on the loader', async () => {
expect(
await browser.elementById('basic-image').getAttribute('srcset')
- ).toBe('https://example.com/myaccount/foo.jpg?auto=format&w=480&q=60 480w')
+ ).toBe(
+ 'https://example.com/myaccount/foo.jpg?auto=format&fit=max&w=480&q=60 1x, https://example.com/myaccount/foo.jpg?auto=format&fit=max&w=1024&q=60 2x'
+ )
})
it('should add a srcset even with preceding slash in prop', async () => {
expect(
await browser.elementById('preceding-slash-image').getAttribute('srcset')
- ).toBe('https://example.com/myaccount/fooslash.jpg?auto=format&w=480 480w')
+ ).toBe(
+ 'https://example.com/myaccount/fooslash.jpg?auto=format&fit=max&w=480 1x, https://example.com/myaccount/fooslash.jpg?auto=format&fit=max&w=1024 2x'
+ )
})
it('should use imageSizes when width matches, not deviceSizes from next.config.js', async () => {
expect(await browser.elementById('icon-image-16').getAttribute('src')).toBe(
- 'https://example.com/myaccount/icon.png?auto=format&w=16'
+ 'https://example.com/myaccount/icon.png?auto=format&fit=max&w=48'
)
expect(
await browser.elementById('icon-image-16').getAttribute('srcset')
- ).toBe('https://example.com/myaccount/icon.png?auto=format&w=16 16w')
- expect(await browser.elementById('icon-image-64').getAttribute('src')).toBe(
- 'https://example.com/myaccount/icon.png?auto=format&w=64'
+ ).toBe(
+ 'https://example.com/myaccount/icon.png?auto=format&fit=max&w=16 1x, https://example.com/myaccount/icon.png?auto=format&fit=max&w=32 2x, https://example.com/myaccount/icon.png?auto=format&fit=max&w=48 3x'
+ )
+ expect(await browser.elementById('icon-image-32').getAttribute('src')).toBe(
+ 'https://example.com/myaccount/icon.png?auto=format&fit=max&w=480'
)
expect(
- await browser.elementById('icon-image-64').getAttribute('srcset')
- ).toBe('https://example.com/myaccount/icon.png?auto=format&w=64 64w')
+ await browser.elementById('icon-image-32').getAttribute('srcset')
+ ).toBe(
+ 'https://example.com/myaccount/icon.png?auto=format&fit=max&w=32 1x, https://example.com/myaccount/icon.png?auto=format&fit=max&w=64 2x, https://example.com/myaccount/icon.png?auto=format&fit=max&w=480 3x'
+ )
})
it('should support the unoptimized attribute', async () => {
expect(
@@ -80,10 +90,10 @@ function runTests() {
function lazyLoadingTests() {
it('should have loaded the first image immediately', async () => {
expect(await browser.elementById('lazy-top').getAttribute('src')).toBe(
- 'https://example.com/myaccount/foo1.jpg?auto=format&w=1024'
+ 'https://example.com/myaccount/foo1.jpg?auto=format&fit=max&w=2000'
)
expect(await browser.elementById('lazy-top').getAttribute('srcset')).toBe(
- 'https://example.com/myaccount/foo1.jpg?auto=format&w=480 480w, https://example.com/myaccount/foo1.jpg?auto=format&w=1024 1024w'
+ 'https://example.com/myaccount/foo1.jpg?auto=format&fit=max&w=1024 1x, https://example.com/myaccount/foo1.jpg?auto=format&fit=max&w=2000 2x'
)
})
it('should not have loaded the second image immediately', async () => {
@@ -111,11 +121,11 @@ function lazyLoadingTests() {
await check(() => {
return browser.elementById('lazy-mid').getAttribute('src')
- }, 'https://example.com/myaccount/foo2.jpg?auto=format&w=480')
+ }, 'https://example.com/myaccount/foo2.jpg?auto=format&fit=max&w=1024')
await check(() => {
return browser.elementById('lazy-mid').getAttribute('srcset')
- }, 'https://example.com/myaccount/foo2.jpg?auto=format&w=480 480w')
+ }, 'https://example.com/myaccount/foo2.jpg?auto=format&fit=max&w=480 1x, https://example.com/myaccount/foo2.jpg?auto=format&fit=max&w=1024 2x')
})
it('should not have loaded the third image after scrolling down', async () => {
expect(
@@ -160,17 +170,17 @@ function lazyLoadingTests() {
await waitFor(200)
expect(
await browser.elementById('lazy-without-attribute').getAttribute('src')
- ).toBe('https://example.com/myaccount/foo4.jpg?auto=format&w=1024')
+ ).toBe('https://example.com/myaccount/foo4.jpg?auto=format&fit=max&w=2000')
expect(
await browser.elementById('lazy-without-attribute').getAttribute('srcset')
).toBe(
- 'https://example.com/myaccount/foo4.jpg?auto=format&w=480 480w, https://example.com/myaccount/foo4.jpg?auto=format&w=1024 1024w'
+ 'https://example.com/myaccount/foo4.jpg?auto=format&fit=max&w=1024 1x, https://example.com/myaccount/foo4.jpg?auto=format&fit=max&w=1600 2x, https://example.com/myaccount/foo4.jpg?auto=format&fit=max&w=2000 3x'
)
})
it('should load the fifth image eagerly, without scrolling', async () => {
expect(await browser.elementById('eager-loading').getAttribute('src')).toBe(
- 'https://example.com/myaccount/foo5.jpg?auto=format&w=2000'
+ 'https://example.com/myaccount/foo5.jpg?auto=format&fit=max&w=2000'
)
expect(
await browser.elementById('eager-loading').getAttribute('srcset')
@@ -210,14 +220,14 @@ describe('Image Component Tests', () => {
it('should add a preload tag for a priority image', async () => {
expect(
await hasPreloadLinkMatchingUrl(
- 'https://example.com/myaccount/withpriority.png?auto=format&w=480&q=60'
+ 'https://example.com/myaccount/withpriority.png?auto=format&fit=max&w=1024&q=60'
)
).toBe(true)
})
it('should add a preload tag for a priority image with preceding slash', async () => {
expect(
await hasPreloadLinkMatchingUrl(
- 'https://example.com/myaccount/fooslash.jpg?auto=format&w=480'
+ 'https://example.com/myaccount/fooslash.jpg?auto=format&fit=max&w=1024'
)
).toBe(true)
})
@@ -231,7 +241,7 @@ describe('Image Component Tests', () => {
it('should add a preload tag for a priority image, with quality', async () => {
expect(
await hasPreloadLinkMatchingUrl(
- 'https://example.com/myaccount/withpriority.png?auto=format&w=480&q=60'
+ 'https://example.com/myaccount/withpriority.png?auto=format&fit=max&w=1024&q=60'
)
).toBe(true)
})
@@ -248,7 +258,7 @@ describe('Image Component Tests', () => {
it('should NOT add a preload tag for a priority image', async () => {
expect(
await hasPreloadLinkMatchingUrl(
- 'https://example.com/myaccount/withpriorityclient.png?auto=format'
+ 'https://example.com/myaccount/withpriorityclient.png?auto=format&fit=max'
)
).toBe(false)
})
@@ -284,11 +294,13 @@ describe('Image Component Tests', () => {
browser = await webdriver(appPort, '/missing-observer')
expect(
await browser.elementById('lazy-no-observer').getAttribute('src')
- ).toBe('https://example.com/myaccount/foox.jpg?auto=format&w=1024')
+ ).toBe(
+ 'https://example.com/myaccount/foox.jpg?auto=format&fit=max&w=2000'
+ )
expect(
await browser.elementById('lazy-no-observer').getAttribute('srcset')
).toBe(
- 'https://example.com/myaccount/foox.jpg?auto=format&w=480 480w, https://example.com/myaccount/foox.jpg?auto=format&w=1024 1024w'
+ 'https://example.com/myaccount/foox.jpg?auto=format&fit=max&w=1024 1x, https://example.com/myaccount/foox.jpg?auto=format&fit=max&w=2000 2x'
)
})
})
@@ -308,11 +320,13 @@ describe('Image Component Tests', () => {
browser = await webdriver(appPort, '/missing-observer')
expect(
await browser.elementById('lazy-no-observer').getAttribute('src')
- ).toBe('https://example.com/myaccount/foox.jpg?auto=format&w=1024')
+ ).toBe(
+ 'https://example.com/myaccount/foox.jpg?auto=format&fit=max&w=2000'
+ )
expect(
await browser.elementById('lazy-no-observer').getAttribute('srcset')
).toBe(
- 'https://example.com/myaccount/foox.jpg?auto=format&w=480 480w, https://example.com/myaccount/foox.jpg?auto=format&w=1024 1024w'
+ 'https://example.com/myaccount/foox.jpg?auto=format&fit=max&w=1024 1x, https://example.com/myaccount/foox.jpg?auto=format&fit=max&w=2000 2x'
)
})
})
diff --git a/test/integration/image-component/default/pages/index.js b/test/integration/image-component/default/pages/index.js
index bdbd9edc72b7d..7001dafe7671e 100644
--- a/test/integration/image-component/default/pages/index.js
+++ b/test/integration/image-component/default/pages/index.js
@@ -4,9 +4,8 @@ import Image from 'next/image'
const Page = () => {
return (
-
Hello World
+
Home Page
-
This is the index page
)
diff --git a/test/integration/image-component/default/pages/invalid-src.js b/test/integration/image-component/default/pages/invalid-src.js
index d9920d016b95c..2251163d4d591 100644
--- a/test/integration/image-component/default/pages/invalid-src.js
+++ b/test/integration/image-component/default/pages/invalid-src.js
@@ -4,8 +4,8 @@ import Image from 'next/image'
const Page = () => {
return (
-
Hello World
-
+
Invalid Source
+
)
}
diff --git a/test/integration/image-component/default/pages/invalid-unsized.js b/test/integration/image-component/default/pages/invalid-unsized.js
new file mode 100644
index 0000000000000..2a776a5aa94f0
--- /dev/null
+++ b/test/integration/image-component/default/pages/invalid-unsized.js
@@ -0,0 +1,13 @@
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+ )
+}
+
+export default Page
diff --git a/test/integration/image-component/default/pages/layout-fill.js b/test/integration/image-component/default/pages/layout-fill.js
new file mode 100644
index 0000000000000..299e357175a4f
--- /dev/null
+++ b/test/integration/image-component/default/pages/layout-fill.js
@@ -0,0 +1,20 @@
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+
Layout Fill
+
+
+
+
Layout Fill
+
+
+
+
Layout Fill
+
+ )
+}
+
+export default Page
diff --git a/test/integration/image-component/default/pages/layout-fixed.js b/test/integration/image-component/default/pages/layout-fixed.js
new file mode 100644
index 0000000000000..c4a64a8ac4e33
--- /dev/null
+++ b/test/integration/image-component/default/pages/layout-fixed.js
@@ -0,0 +1,41 @@
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+
Layout Fixed
+
+
+
+
+
Layout Fixed
+
+ )
+}
+
+export default Page
diff --git a/test/integration/image-component/default/pages/layout-intrinsic.js b/test/integration/image-component/default/pages/layout-intrinsic.js
new file mode 100644
index 0000000000000..631706707de34
--- /dev/null
+++ b/test/integration/image-component/default/pages/layout-intrinsic.js
@@ -0,0 +1,41 @@
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+
Layout Intrinsic
+
+
+
+
+
Layout Intrinsic
+
+ )
+}
+
+export default Page
diff --git a/test/integration/image-component/default/pages/layout-responsive.js b/test/integration/image-component/default/pages/layout-responsive.js
new file mode 100644
index 0000000000000..7e849c419e2cd
--- /dev/null
+++ b/test/integration/image-component/default/pages/layout-responsive.js
@@ -0,0 +1,41 @@
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+
Layout Responsive
+
+
+
+
+
Layout Responsive
+
+ )
+}
+
+export default Page
diff --git a/test/integration/image-component/default/pages/rotated.js b/test/integration/image-component/default/pages/rotated.js
new file mode 100644
index 0000000000000..14dd07b143e3b
--- /dev/null
+++ b/test/integration/image-component/default/pages/rotated.js
@@ -0,0 +1,14 @@
+import Image from 'next/image'
+import React from 'react'
+
+const Page = () => {
+ return (
+
+
Hello World
+
+
This is the rotated page
+
+ )
+}
+
+export default Page
diff --git a/test/integration/image-component/default/public/exif-rotation.jpg b/test/integration/image-component/default/public/exif-rotation.jpg
new file mode 100644
index 0000000000000..5470a458f0933
Binary files /dev/null and b/test/integration/image-component/default/public/exif-rotation.jpg differ
diff --git a/test/integration/image-component/default/public/wide.png b/test/integration/image-component/default/public/wide.png
new file mode 100644
index 0000000000000..b7bb4dc1497ba
Binary files /dev/null and b/test/integration/image-component/default/public/wide.png differ
diff --git a/test/integration/image-component/default/test/index.test.js b/test/integration/image-component/default/test/index.test.js
index ff5ce85c820bc..df114e57b784b 100644
--- a/test/integration/image-component/default/test/index.test.js
+++ b/test/integration/image-component/default/test/index.test.js
@@ -10,6 +10,7 @@ import {
check,
hasRedbox,
getRedboxHeader,
+ waitFor,
} from 'next-test-utils'
import webdriver from 'next-webdriver'
import fs from 'fs-extra'
@@ -35,30 +36,38 @@ async function hasImageMatchingUrl(browser, url) {
return foundMatch
}
+async function getComputed(browser, id, prop) {
+ const val = await browser.eval(`document.getElementById('${id}').${prop}`)
+ if (typeof val === 'number') {
+ return val
+ }
+ if (typeof val === 'string') {
+ return parseInt(val, 10)
+ }
+ return null
+}
+
+async function getSrc(browser, id) {
+ const src = await browser.elementById(id).getAttribute('src')
+ if (src) {
+ const url = new URL(src)
+ return url.href.slice(url.origin.length)
+ }
+}
+
+function getRatio(width, height) {
+ return Math.round((height / width) * 1000)
+}
+
function runTests(mode) {
it('should load the images', async () => {
let browser
try {
browser = await webdriver(appPort, '/')
- await check(async () => {
- const result = await browser.eval(
- `document.getElementById('basic-image').naturalWidth`
- )
-
- if (result === 0) {
- throw new Error('Incorrectly loaded image')
- }
-
- return 'result-correct'
- }, /result-correct/)
-
- await browser.eval(
- 'document.getElementById("unsized-image").scrollIntoView()'
- )
await check(async () => {
const result = await browser.eval(
- `document.getElementById('unsized-image').naturalWidth`
+ `document.getElementById('basic-image').naturalWidth`
)
if (result === 0) {
@@ -71,7 +80,7 @@ function runTests(mode) {
expect(
await hasImageMatchingUrl(
browser,
- `http://localhost:${appPort}/_next/image?url=%2Ftest.jpg&w=420&q=75`
+ `http://localhost:${appPort}/_next/image?url=%2Ftest.jpg&w=1200&q=75`
)
).toBe(true)
} finally {
@@ -102,6 +111,187 @@ function runTests(mode) {
}
})
+ it('should work with layout-fixed so resizing window does not resize image', async () => {
+ let browser
+ try {
+ browser = await webdriver(appPort, '/layout-fixed')
+ const width = 1200
+ const height = 700
+ const delta = 250
+ const id = 'fixed1'
+ expect(await getSrc(browser, id)).toBe(
+ '/_next/image?url=%2Fwide.png&w=3840&q=75'
+ )
+ expect(await browser.elementById(id).getAttribute('srcset')).toBe(
+ '/_next/image?url=%2Fwide.png&w=1200&q=75 1x, /_next/image?url=%2Fwide.png&w=3840&q=75 2x'
+ )
+ await browser.setDimensions({
+ width: width + delta,
+ height: height + delta,
+ })
+ expect(await getComputed(browser, id, 'width')).toBe(width)
+ expect(await getComputed(browser, id, 'height')).toBe(height)
+ await browser.setDimensions({
+ width: width - delta,
+ height: height - delta,
+ })
+ expect(await getComputed(browser, id, 'width')).toBe(width)
+ expect(await getComputed(browser, id, 'height')).toBe(height)
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should work with layout-intrinsic so resizing window maintains image aspect ratio', async () => {
+ let browser
+ try {
+ browser = await webdriver(appPort, '/layout-intrinsic')
+ const width = 1200
+ const height = 700
+ const delta = 250
+ const id = 'intrinsic1'
+ expect(await getSrc(browser, id)).toBe(
+ '/_next/image?url=%2Fwide.png&w=3840&q=75'
+ )
+ expect(await browser.elementById(id).getAttribute('srcset')).toBe(
+ '/_next/image?url=%2Fwide.png&w=1200&q=75 1x, /_next/image?url=%2Fwide.png&w=3840&q=75 2x'
+ )
+ await browser.setDimensions({
+ width: width + delta,
+ height: height + delta,
+ })
+ expect(await getComputed(browser, id, 'width')).toBe(width)
+ expect(await getComputed(browser, id, 'height')).toBe(height)
+ await browser.setDimensions({
+ width: width - delta,
+ height: height - delta,
+ })
+ const newWidth = await getComputed(browser, id, 'width')
+ const newHeight = await getComputed(browser, id, 'height')
+ expect(newWidth).toBeLessThan(width)
+ expect(newHeight).toBeLessThan(height)
+ expect(getRatio(newWidth, newHeight)).toBe(getRatio(width, height))
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should work with layout-responsive so resizing window maintains image aspect ratio', async () => {
+ let browser
+ try {
+ browser = await webdriver(appPort, '/layout-responsive')
+ const width = 1200
+ const height = 700
+ const delta = 250
+ const id = 'responsive1'
+ expect(await getSrc(browser, id)).toBe(
+ '/_next/image?url=%2Fwide.png&w=3840&q=75'
+ )
+ expect(await browser.elementById(id).getAttribute('srcset')).toBe(
+ '/_next/image?url=%2Fwide.png&w=640&q=75 640w, /_next/image?url=%2Fwide.png&w=750&q=75 750w, /_next/image?url=%2Fwide.png&w=828&q=75 828w, /_next/image?url=%2Fwide.png&w=1080&q=75 1080w, /_next/image?url=%2Fwide.png&w=1200&q=75 1200w, /_next/image?url=%2Fwide.png&w=1920&q=75 1920w, /_next/image?url=%2Fwide.png&w=2048&q=75 2048w, /_next/image?url=%2Fwide.png&w=3840&q=75 3840w'
+ )
+ await browser.setDimensions({
+ width: width + delta,
+ height: height + delta,
+ })
+ expect(await getComputed(browser, id, 'width')).toBeGreaterThan(width)
+ expect(await getComputed(browser, id, 'height')).toBeGreaterThan(height)
+ await browser.setDimensions({
+ width: width - delta,
+ height: height - delta,
+ })
+ const newWidth = await getComputed(browser, id, 'width')
+ const newHeight = await getComputed(browser, id, 'height')
+ expect(newWidth).toBeLessThan(width)
+ expect(newHeight).toBeLessThan(height)
+ expect(getRatio(newWidth, newHeight)).toBe(getRatio(width, height))
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should work with layout-fill to fill the parent but NOT stretch with viewport', async () => {
+ let browser
+ try {
+ browser = await webdriver(appPort, '/layout-fill')
+ const width = 600
+ const height = 350
+ const delta = 150
+ const id = 'fill1'
+ expect(await getSrc(browser, id)).toBe(
+ '/_next/image?url=%2Fwide.png&w=3840&q=75'
+ )
+ expect(await browser.elementById(id).getAttribute('srcset')).toBe(
+ '/_next/image?url=%2Fwide.png&w=640&q=75 640w, /_next/image?url=%2Fwide.png&w=750&q=75 750w, /_next/image?url=%2Fwide.png&w=828&q=75 828w, /_next/image?url=%2Fwide.png&w=1080&q=75 1080w, /_next/image?url=%2Fwide.png&w=1200&q=75 1200w, /_next/image?url=%2Fwide.png&w=1920&q=75 1920w, /_next/image?url=%2Fwide.png&w=2048&q=75 2048w, /_next/image?url=%2Fwide.png&w=3840&q=75 3840w'
+ )
+ await browser.setDimensions({
+ width: width + delta,
+ height: height + delta,
+ })
+ expect(await getComputed(browser, id, 'width')).toBe(width)
+ expect(await getComputed(browser, id, 'height')).toBe(height)
+ await browser.setDimensions({
+ width: width - delta,
+ height: height - delta,
+ })
+ const newWidth = await getComputed(browser, id, 'width')
+ const newHeight = await getComputed(browser, id, 'height')
+ expect(newWidth).toBe(width)
+ expect(newHeight).toBe(height)
+ expect(getRatio(newWidth, newHeight)).toBe(getRatio(width, height))
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should work with layout-fill to fill the parent and stretch with viewport', async () => {
+ let browser
+ try {
+ browser = await webdriver(appPort, '/layout-fill')
+ const id = 'fill2'
+ const width = await getComputed(browser, id, 'width')
+ const height = await getComputed(browser, id, 'height')
+ await browser.eval(`document.getElementById("${id}").scrollIntoView()`)
+ expect(await getSrc(browser, id)).toBe(
+ '/_next/image?url=%2Fwide.png&w=3840&q=75'
+ )
+ expect(await browser.elementById(id).getAttribute('srcset')).toBe(
+ '/_next/image?url=%2Fwide.png&w=640&q=75 640w, /_next/image?url=%2Fwide.png&w=750&q=75 750w, /_next/image?url=%2Fwide.png&w=828&q=75 828w, /_next/image?url=%2Fwide.png&w=1080&q=75 1080w, /_next/image?url=%2Fwide.png&w=1200&q=75 1200w, /_next/image?url=%2Fwide.png&w=1920&q=75 1920w, /_next/image?url=%2Fwide.png&w=2048&q=75 2048w, /_next/image?url=%2Fwide.png&w=3840&q=75 3840w'
+ )
+ expect(await getComputed(browser, id, 'width')).toBe(width)
+ expect(await getComputed(browser, id, 'height')).toBe(height)
+ const delta = 150
+ const largeWidth = width + delta
+ const largeHeight = height + delta
+ await browser.setDimensions({
+ width: largeWidth,
+ height: largeHeight,
+ })
+ expect(await getComputed(browser, id, 'width')).toBe(largeWidth)
+ expect(await getComputed(browser, id, 'height')).toBe(largeHeight)
+ const smallWidth = width - delta
+ const smallHeight = height - delta
+ await browser.setDimensions({
+ width: smallWidth,
+ height: smallHeight,
+ })
+ expect(await getComputed(browser, id, 'width')).toBe(smallWidth)
+ expect(await getComputed(browser, id, 'height')).toBe(smallHeight)
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
if (mode === 'dev') {
it('should show missing src error', async () => {
const browser = await webdriver(appPort, '/missing-src')
@@ -120,6 +310,53 @@ function runTests(mode) {
'Invalid src prop (https://google.com/test.png) on `next/image`, hostname "google.com" is not configured under images in your `next.config.js`'
)
})
+
+ it('should show invalid unsized error', async () => {
+ const browser = await webdriver(appPort, '/invalid-unsized')
+
+ await hasRedbox(browser)
+ expect(await getRedboxHeader(browser)).toContain(
+ 'Image with src "/test.png" has deprecated "unsized" property, which was removed in favor of the "layout=\'fill\'" property'
+ )
+ })
+ }
+
+ // Tests that use the `unsized` attribute:
+ if (mode !== 'dev') {
+ it('should correctly rotate image', async () => {
+ let browser
+ try {
+ browser = await webdriver(appPort, '/rotated')
+
+ const id = 'exif-rotation-image'
+
+ // Wait for image to load:
+ await check(async () => {
+ const result = await browser.eval(
+ `document.getElementById(${JSON.stringify(id)}).naturalWidth`
+ )
+
+ if (result < 1) {
+ throw new Error('Image not ready')
+ }
+
+ return 'result-correct'
+ }, /result-correct/)
+
+ await waitFor(1000)
+
+ const computedWidth = await getComputed(browser, id, 'width')
+ const computedHeight = await getComputed(browser, id, 'height')
+ expect(getRatio(computedWidth, computedHeight) / 1000.0).toBeCloseTo(
+ 1.333,
+ 1
+ )
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
}
}
diff --git a/test/integration/image-component/typescript/pages/invalid.tsx b/test/integration/image-component/typescript/pages/invalid.tsx
index 1c5f586651bc3..2a1daf83e1186 100644
--- a/test/integration/image-component/typescript/pages/invalid.tsx
+++ b/test/integration/image-component/typescript/pages/invalid.tsx
@@ -4,7 +4,7 @@ import Image from 'next/image'
const Invalid = () => {
return (
-
Hello World
+
Invalid TS
{
return (
-
Hello World
+
Valid TS
+ />
-
+ />
+
+
+
+ width={500}
+ height={500}
+ />
+ width={500}
+ height={500}
+ />
This is valid usage of the Image component
)
diff --git a/test/integration/image-component/typescript/test/index.test.js b/test/integration/image-component/typescript/test/index.test.js
index 16424caff5ed9..ca0b6bb158729 100644
--- a/test/integration/image-component/typescript/test/index.test.js
+++ b/test/integration/image-component/typescript/test/index.test.js
@@ -45,14 +45,14 @@ describe('TypeScript Image Component', () => {
const html = await renderViaHTTP(appPort, '/valid', {})
expect(html).toMatch(/This is valid usage of the Image component/)
expect(output).not.toMatch(
- /must use "width" and "height" properties or "unsized" property/
+ /must use "width" and "height" properties or "layout='fill'" property/
)
})
it('should print error when invalid Image usage', async () => {
await renderViaHTTP(appPort, '/invalid', {})
expect(output).toMatch(
- /must use "width" and "height" properties or "unsized" property/
+ /must use "width" and "height" properties or "layout='fill'" property/
)
})
})
diff --git a/test/integration/image-optimizer/test/index.test.js b/test/integration/image-optimizer/test/index.test.js
index 15869a9750dbc..3eff52b1d8c86 100644
--- a/test/integration/image-optimizer/test/index.test.js
+++ b/test/integration/image-optimizer/test/index.test.js
@@ -19,7 +19,7 @@ jest.setTimeout(1000 * 60 * 2)
const appDir = join(__dirname, '../')
const imagesDir = join(appDir, '.next', 'cache', 'images')
const nextConfig = new File(join(appDir, 'next.config.js'))
-const largeSize = 1024
+const largeSize = 1080 // defaults defined in server/config.ts
let appPort
let app
@@ -88,6 +88,26 @@ function runTests({ w, isDev, domains }) {
expect(actual).toMatch(expected)
})
+ it('should maintain jpg format for old Safari', async () => {
+ const accept =
+ 'image/png,image/svg+xml,image/*;q=0.8,video/*;q=0.8,*/*;q=0.5'
+ const query = { w, q: 90, url: '/test.jpg' }
+ const opts = { headers: { accept } }
+ const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
+ expect(res.status).toBe(200)
+ expect(res.headers.get('Content-Type')).toContain('image/jpeg')
+ })
+
+ it('should maintain png format for old Safari', async () => {
+ const accept =
+ 'image/png,image/svg+xml,image/*;q=0.8,video/*;q=0.8,*/*;q=0.5'
+ const query = { w, q: 75, url: '/test.png' }
+ const opts = { headers: { accept } }
+ const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
+ expect(res.status).toBe(200)
+ expect(res.headers.get('Content-Type')).toContain('image/png')
+ })
+
it('should fail when url is missing', async () => {
const query = { w, q: 100 }
const res = await fetchViaHTTP(appPort, '/_next/image', query, {})
@@ -173,24 +193,15 @@ function runTests({ w, isDev, domains }) {
)
})
- it('should resize relative url and webp accept header', async () => {
+ it('should resize relative url and webp Firefox accept header', async () => {
const query = { url: '/test.png', w, q: 80 }
- const opts = { headers: { accept: 'image/webp' } }
+ const opts = { headers: { accept: 'image/webp,*/*' } }
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toBe('image/webp')
await expectWidth(res, w)
})
- it('should resize relative url and jpeg accept header', async () => {
- const query = { url: '/test.png', w, q: 80 }
- const opts = { headers: { accept: 'image/jpeg' } }
- const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
- expect(res.status).toBe(200)
- expect(res.headers.get('Content-Type')).toBe('image/jpeg')
- await expectWidth(res, w)
- })
-
it('should resize relative url and png accept header', async () => {
const query = { url: '/test.png', w, q: 80 }
const opts = { headers: { accept: 'image/png' } }
@@ -227,9 +238,11 @@ function runTests({ w, isDev, domains }) {
await expectWidth(res, w)
})
- it('should resize relative url and wildcard accept header as webp', async () => {
+ it('should resize relative url and Chrome accept header as webp', async () => {
const query = { url: '/test.png', w, q: 80 }
- const opts = { headers: { accept: 'image/*' } }
+ const opts = {
+ headers: { accept: 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8' },
+ }
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toBe('image/webp')
@@ -428,7 +441,7 @@ describe('Image Optimizer', () => {
const domains = ['localhost', 'example.com']
describe('dev support w/o next.config.js', () => {
- const size = 320 // defaults defined in server/config.ts
+ const size = 384 // defaults defined in server/config.ts
beforeAll(async () => {
appPort = await findPort()
app = await launchApp(appDir, appPort)
@@ -465,7 +478,7 @@ describe('Image Optimizer', () => {
})
describe('Server support w/o next.config.js', () => {
- const size = 320 // defaults defined in server/config.ts
+ const size = 384 // defaults defined in server/config.ts
beforeAll(async () => {
await nextBuild(appDir)
appPort = await findPort()
@@ -544,7 +557,8 @@ describe('Image Optimizer', () => {
await fs.remove(imagesDir)
})
it('should 404 when loader is not default', async () => {
- const query = { w: 320, q: 90, url: '/test.svg' }
+ const size = 384 // defaults defined in server/config.ts
+ const query = { w: size, q: 90, url: '/test.svg' }
const opts = { headers: { accept: 'image/webp' } }
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(404)
diff --git a/test/integration/link-ref/pages/click-away-race-condition.js b/test/integration/link-ref/pages/click-away-race-condition.js
new file mode 100644
index 0000000000000..a5b0d51e281be
--- /dev/null
+++ b/test/integration/link-ref/pages/click-away-race-condition.js
@@ -0,0 +1,50 @@
+import React, { useCallback, useEffect, useRef, useState } from 'react'
+import Link from 'next/link'
+
+const useClickAway = (ref, onClickAway) => {
+ useEffect(() => {
+ const handler = (event) => {
+ const el = ref.current
+
+ // when menu is open and clicked inside menu, A is expected to be false
+ // when menu is open and clicked outside menu, A is expected to be true
+ console.log('A', el && !el.contains(event.target))
+
+ el && !el.contains(event.target) && onClickAway(event)
+ }
+
+ document.addEventListener('click', handler)
+
+ return () => {
+ document.removeEventListener('click', handler)
+ }
+ }, [onClickAway, ref])
+}
+
+export default function App() {
+ const [open, setOpen] = useState(false)
+
+ const menuRef = useRef(null)
+
+ const onClickAway = useCallback(() => {
+ console.log('click away, open', open)
+ if (open) {
+ setOpen(false)
+ }
+ }, [open])
+
+ useClickAway(menuRef, onClickAway)
+
+ return (
+
+
setOpen(true)}>
+ Open Menu
+
+ {open && (
+
+ )}
+
+ )
+}
diff --git a/test/integration/link-ref/test/index.test.js b/test/integration/link-ref/test/index.test.js
index 0251c868f9faf..588517f9745ae 100644
--- a/test/integration/link-ref/test/index.test.js
+++ b/test/integration/link-ref/test/index.test.js
@@ -49,6 +49,15 @@ const didPrefetch = async (pathname) => {
await browser.close()
}
+function runCommonTests() {
+ // See https://github.com/vercel/next.js/issues/18437
+ it('should not have a race condition with a click handler', async () => {
+ const browser = await webdriver(appPort, '/click-away-race-condition')
+ await browser.elementByCss('#click-me').click()
+ await browser.waitForElementByCss('#the-menu')
+ })
+}
+
describe('Invalid hrefs', () => {
describe('dev mode', () => {
beforeAll(async () => {
@@ -57,6 +66,8 @@ describe('Invalid hrefs', () => {
})
afterAll(() => killApp(app))
+ runCommonTests()
+
it('should not show error for function component with forwardRef', async () => {
await noError('/function')
})
@@ -82,6 +93,8 @@ describe('Invalid hrefs', () => {
})
afterAll(() => killApp(app))
+ runCommonTests()
+
it('should preload with forwardRef', async () => {
await didPrefetch('/function')
})
diff --git a/test/integration/nullish-config/test/index.test.js b/test/integration/nullish-config/test/index.test.js
index 0f1c73c30187e..dbbd49a9dad7e 100644
--- a/test/integration/nullish-config/test/index.test.js
+++ b/test/integration/nullish-config/test/index.test.js
@@ -23,9 +23,6 @@ const runTests = () => {
amp: {
canonicalBase: undefined,
},
- devIndicators: {
- autoPrerender: undefined,
- },
}
`
)
@@ -47,9 +44,6 @@ const runTests = () => {
amp: {
canonicalBase: null,
},
- devIndicators: {
- autoPrerender: null,
- },
}
`
)
diff --git a/test/integration/production/components/dynamic-css/with-css.js b/test/integration/production/components/dynamic-css/with-css.js
index d988353142973..0fedd10900bb2 100644
--- a/test/integration/production/components/dynamic-css/with-css.js
+++ b/test/integration/production/components/dynamic-css/with-css.js
@@ -1,3 +1,7 @@
import styles from './with-css.module.css'
-export default () => With CSS
+export default () => (
+
+ With CSS
+
+)
diff --git a/test/integration/production/components/dynamic-css/with-css.module.css b/test/integration/production/components/dynamic-css/with-css.module.css
index 69fda78f79397..077039d0e7431 100644
--- a/test/integration/production/components/dynamic-css/with-css.module.css
+++ b/test/integration/production/components/dynamic-css/with-css.module.css
@@ -1,3 +1,4 @@
.content {
color: inherit;
+ display: flex;
}
diff --git a/test/integration/production/pages/dynamic/pagechange1.js b/test/integration/production/pages/dynamic/pagechange1.js
new file mode 100644
index 0000000000000..c2d38159e4c78
--- /dev/null
+++ b/test/integration/production/pages/dynamic/pagechange1.js
@@ -0,0 +1,12 @@
+import dynamic from 'next/dynamic'
+
+const Hello = dynamic(import('../../components/dynamic-css/with-css'))
+
+export default function PageChange1() {
+ return (
+
+ PageChange1
+
+
+ )
+}
diff --git a/test/integration/production/pages/dynamic/pagechange2.js b/test/integration/production/pages/dynamic/pagechange2.js
new file mode 100644
index 0000000000000..f4ebbd1b3e7b3
--- /dev/null
+++ b/test/integration/production/pages/dynamic/pagechange2.js
@@ -0,0 +1,12 @@
+import dynamic from 'next/dynamic'
+
+const Hello = dynamic(import('../../components/dynamic-css/with-css'))
+
+export default function PageChange2() {
+ return (
+
+ PageChange2
+
+
+ )
+}
diff --git a/test/integration/production/test/dynamic.js b/test/integration/production/test/dynamic.js
index 61e4cfaa56f86..3a10ab3eb410f 100644
--- a/test/integration/production/test/dynamic.js
+++ b/test/integration/production/test/dynamic.js
@@ -40,6 +40,28 @@ export default (context, render) => {
expect(cssFiles.length).toBe(1)
})
+ it('should not remove css styles for same css file between page transitions', async () => {
+ let browser
+ try {
+ browser = await webdriver(context.appPort, '/dynamic/pagechange1')
+ await check(() => browser.elementByCss('body').text(), /PageChange1/)
+ const firstElement = await browser.elementById('with-css')
+ const css1 = await firstElement.getComputedCss('display')
+ expect(css1).toBe('flex')
+ await browser.eval(() =>
+ window.next.router.push('/dynamic/pagechange2')
+ )
+ await check(() => browser.elementByCss('body').text(), /PageChange2/)
+ const secondElement = await browser.elementById('with-css')
+ const css2 = await secondElement.getComputedCss('display')
+ expect(css2).toBe(css1)
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
// It seem to be abnormal, dynamic CSS modules are completely self-sufficient, so shared styles are copied across files
it('should output two css files even in case of three css module files while one is shared across files', async () => {
const $ = await get$('/dynamic/shared-css-module')
diff --git a/test/integration/serverless/test/index.test.js b/test/integration/serverless/test/index.test.js
index 3a62a349afb5d..7b8eb2d3302f6 100644
--- a/test/integration/serverless/test/index.test.js
+++ b/test/integration/serverless/test/index.test.js
@@ -280,6 +280,21 @@ describe('Serverless', () => {
expect(data.query).toEqual({ slug: paramRaw })
})
+ it('should have the correct query string for a now route with invalid matches but correct path', async () => {
+ const paramRaw = 'dr/first'
+ const html = await fetchViaHTTP(appPort, `/dr/first`, '', {
+ headers: {
+ 'x-now-route-matches': qs.stringify({
+ 1: encodeURIComponent(paramRaw),
+ }),
+ },
+ }).then((res) => res.text())
+ const $ = cheerio.load(html)
+ const data = JSON.parse($('#__NEXT_DATA__').html())
+
+ expect(data.query).toEqual({ slug: 'first' })
+ })
+
it('should have the correct query string for a catch all now route', async () => {
const paramRaw = ['nested % 1', 'nested/2']
diff --git a/test/integration/size-limit/test/index.test.js b/test/integration/size-limit/test/index.test.js
index dc5a102f9ed7b..522fc5680ee42 100644
--- a/test/integration/size-limit/test/index.test.js
+++ b/test/integration/size-limit/test/index.test.js
@@ -100,7 +100,7 @@ describe('Production response size', () => {
)
// These numbers are without gzip compression!
- const delta = responseSizesBytes - 171 * 1024
+ const delta = responseSizesBytes - 172 * 1024
expect(delta).toBeLessThanOrEqual(1024) // don't increase size more than 1kb
expect(delta).toBeGreaterThanOrEqual(-1024) // don't decrease size more than 1kb without updating target
})
diff --git a/test/isolated/config.test.js b/test/isolated/config.test.js
index 370d179deedc9..640b218507abf 100644
--- a/test/isolated/config.test.js
+++ b/test/isolated/config.test.js
@@ -104,22 +104,14 @@ describe('config', () => {
it('Should ignore configs set to `undefined`', () => {
const config = loadConfig(PHASE_DEVELOPMENT_SERVER, null, {
target: undefined,
- devIndicators: {
- autoPrerender: undefined,
- },
})
expect(config.target).toBe('server')
- expect(config.devIndicators.autoPrerender).toBe(true)
})
it('Should ignore configs set to `null`', () => {
const config = loadConfig(PHASE_DEVELOPMENT_SERVER, null, {
target: null,
- devIndicators: {
- autoPrerender: null,
- },
})
expect(config.target).toBe('server')
- expect(config.devIndicators.autoPrerender).toBe(true)
})
})
diff --git a/test/lib/next-webdriver.d.ts b/test/lib/next-webdriver.d.ts
index fe87e0363ab76..08587bdf1b710 100644
--- a/test/lib/next-webdriver.d.ts
+++ b/test/lib/next-webdriver.d.ts
@@ -17,6 +17,7 @@ interface Chain {
back: () => Chain
forward: () => Chain
refresh: () => Chain
+ setDimensions: (opts: { height: number; width: number }) => Chain
close: () => Chain
quit: () => Chain
}
diff --git a/test/lib/wd-chain.js b/test/lib/wd-chain.js
index 48924f13e343f..2dbbdabb6541f 100644
--- a/test/lib/wd-chain.js
+++ b/test/lib/wd-chain.js
@@ -126,6 +126,12 @@ export default class Chain {
return this.updateChain(() => this.browser.navigate().refresh())
}
+ setDimensions({ height, width }) {
+ return this.updateChain(() =>
+ this.browser.manage().window().setRect({ width, height, x: 0, y: 0 })
+ )
+ }
+
close() {
return this.updateChain(() => Promise.resolve())
}