interface IEntry {
${
diff --git a/packages/next/cli/next-dev.ts b/packages/next/cli/next-dev.ts
index 1f0fc1ab875d..b0a689c84f66 100755
--- a/packages/next/cli/next-dev.ts
+++ b/packages/next/cli/next-dev.ts
@@ -380,9 +380,9 @@ If you cannot make the changes above, but still want to try out\nNext.js v13 wit
eventCliSession(distDir, rawNextConfig as NextConfigComplete, {
webpackVersion: 5,
cliCommand: 'dev',
- isSrcDir:
- (!!pagesDir && path.relative(dir, pagesDir).startsWith('src')) ||
- (!!appDir && path.relative(dir, appDir).startsWith('src')),
+ isSrcDir: path
+ .relative(dir, pagesDir || appDir || '')
+ .startsWith('src'),
hasNowJson: !!(await findUp('now.json', { cwd: dir })),
isCustomServer: false,
turboFlag: true,
diff --git a/packages/next/client/components/app-router-headers.ts b/packages/next/client/components/app-router-headers.ts
index 1eb2747dd1cd..de00963e625e 100644
--- a/packages/next/client/components/app-router-headers.ts
+++ b/packages/next/client/components/app-router-headers.ts
@@ -3,3 +3,9 @@ export const NEXT_ROUTER_STATE_TREE = 'Next-Router-State-Tree' as const
export const NEXT_ROUTER_PREFETCH = 'Next-Router-Prefetch' as const
export const RSC_VARY_HEADER =
`${RSC}, ${NEXT_ROUTER_STATE_TREE}, ${NEXT_ROUTER_PREFETCH}` as const
+
+export const FLIGHT_PARAMETERS = [
+ [RSC],
+ [NEXT_ROUTER_STATE_TREE],
+ [NEXT_ROUTER_PREFETCH],
+] as const
diff --git a/packages/next/client/components/react-dev-overlay/hot-reloader-client.tsx b/packages/next/client/components/react-dev-overlay/hot-reloader-client.tsx
index f5a4f91f9fa8..0a5cbb9c6a78 100644
--- a/packages/next/client/components/react-dev-overlay/hot-reloader-client.tsx
+++ b/packages/next/client/components/react-dev-overlay/hot-reloader-client.tsx
@@ -124,7 +124,7 @@ function tryApplyUpdates(
return
}
- function handleApplyUpdates(err: any, updatedModules: any) {
+ function handleApplyUpdates(err: any, updatedModules: any[] | null) {
if (err || RuntimeErrorHandler.hadRuntimeError || !updatedModules) {
if (err) {
console.warn(
@@ -144,7 +144,7 @@ function tryApplyUpdates(
return
}
- const hasUpdates = Boolean(updatedModules.length)
+ const hasUpdates = Boolean(updatedModules?.length)
if (typeof onHotUpdateSuccess === 'function') {
// Maybe we want to do something.
onHotUpdateSuccess(hasUpdates)
@@ -175,8 +175,8 @@ function tryApplyUpdates(
// @ts-expect-error module.hot exists
module.hot
.check(/* autoApply */ false)
- .then((updatedModules: any) => {
- const hasUpdates = Boolean(updatedModules.length)
+ .then((updatedModules: any[] | null) => {
+ const hasUpdates = Boolean(updatedModules?.length)
if (typeof onBeforeUpdate === 'function') {
onBeforeUpdate(hasUpdates)
}
diff --git a/packages/next/client/dev/error-overlay/hot-dev-client.js b/packages/next/client/dev/error-overlay/hot-dev-client.js
index 697c99945c21..fa613a6ee03d 100644
--- a/packages/next/client/dev/error-overlay/hot-dev-client.js
+++ b/packages/next/client/dev/error-overlay/hot-dev-client.js
@@ -337,7 +337,7 @@ function tryApplyUpdates(onBeforeHotUpdate, onHotUpdateSuccess) {
return
}
- const hasUpdates = Boolean(updatedModules.length)
+ const hasUpdates = Boolean(updatedModules?.length)
if (typeof onHotUpdateSuccess === 'function') {
// Maybe we want to do something.
onHotUpdateSuccess(hasUpdates)
@@ -368,7 +368,7 @@ function tryApplyUpdates(onBeforeHotUpdate, onHotUpdateSuccess) {
.check(/* autoApply */ false)
.then((updatedModules) => {
if (typeof onBeforeHotUpdate === 'function') {
- const hasUpdates = Boolean(updatedModules.length)
+ const hasUpdates = Boolean(updatedModules?.length)
onBeforeHotUpdate(hasUpdates)
}
return module.hot.apply()
diff --git a/packages/next/client/normalize-trailing-slash.ts b/packages/next/client/normalize-trailing-slash.ts
index d2405675a1de..11547936af6b 100644
--- a/packages/next/client/normalize-trailing-slash.ts
+++ b/packages/next/client/normalize-trailing-slash.ts
@@ -6,7 +6,7 @@ import { parsePath } from '../shared/lib/router/utils/parse-path'
* in `next.config.js`.
*/
export const normalizePathTrailingSlash = (path: string) => {
- if (!path.startsWith('/')) {
+ if (!path.startsWith('/') || process.env.__NEXT_MANUAL_TRAILING_SLASH) {
return path
}
diff --git a/packages/next/package.json b/packages/next/package.json
index 53d96414645d..da1ae1710729 100644
--- a/packages/next/package.json
+++ b/packages/next/package.json
@@ -1,6 +1,6 @@
{
"name": "next",
- "version": "13.0.5-canary.3",
+ "version": "13.0.5-canary.4",
"description": "The React Framework",
"main": "./dist/server/next.js",
"license": "MIT",
@@ -75,7 +75,7 @@
]
},
"dependencies": {
- "@next/env": "13.0.5-canary.3",
+ "@next/env": "13.0.5-canary.4",
"@swc/helpers": "0.4.14",
"caniuse-lite": "^1.0.30001406",
"postcss": "8.4.14",
@@ -126,11 +126,11 @@
"@hapi/accept": "5.0.2",
"@napi-rs/cli": "2.12.0",
"@napi-rs/triples": "1.1.0",
- "@next/polyfill-module": "13.0.5-canary.3",
- "@next/polyfill-nomodule": "13.0.5-canary.3",
- "@next/react-dev-overlay": "13.0.5-canary.3",
- "@next/react-refresh-utils": "13.0.5-canary.3",
- "@next/swc": "13.0.5-canary.3",
+ "@next/polyfill-module": "13.0.5-canary.4",
+ "@next/polyfill-nomodule": "13.0.5-canary.4",
+ "@next/react-dev-overlay": "13.0.5-canary.4",
+ "@next/react-refresh-utils": "13.0.5-canary.4",
+ "@next/swc": "13.0.5-canary.4",
"@segment/ajv-human-errors": "2.1.2",
"@taskr/clear": "1.1.0",
"@taskr/esnext": "1.1.0",
diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx
index a3ca3cda425b..9c2723e8ec50 100644
--- a/packages/next/server/app-render.tsx
+++ b/packages/next/server/app-render.tsx
@@ -42,6 +42,7 @@ import {
NEXT_ROUTER_PREFETCH,
NEXT_ROUTER_STATE_TREE,
RSC,
+ FLIGHT_PARAMETERS,
} from '../client/components/app-router-headers'
import type { StaticGenerationStore } from '../client/components/static-generation-async-storage'
@@ -704,12 +705,6 @@ function getScriptNonceFromHeader(cspHeaderValue: string): string | undefined {
return nonce
}
-export const FLIGHT_PARAMETERS = [
- [RSC],
- [NEXT_ROUTER_STATE_TREE],
- [NEXT_ROUTER_PREFETCH],
-] as const
-
function headersWithoutFlight(headers: IncomingHttpHeaders) {
const newHeaders = { ...headers }
for (const param of FLIGHT_PARAMETERS) {
@@ -999,13 +994,17 @@ export async function renderToHTMLOrFlight(
filePath,
serverCSSForEntries
)
- const cacheBustingUrlSuffix = dev ? `?ts=${Date.now()}` : ''
const styles = cssHrefs
? cssHrefs.map((href, index) => (
{
- // Add extra cache busting (DEV only) for https://github.com/vercel/next.js/issues/5860
- // See also https://bugs.webkit.org/show_bug.cgi?id=187726
- const cacheBustingUrlSuffix = dev ? `?ts=${Date.now()}` : ''
-
return (
<>
{preloadedFontFiles.map((fontFile) => {
@@ -1347,7 +1342,12 @@ export async function renderToHTMLOrFlight(
? stylesheets.map((href, index) => (
!this.appPathRoutes![route]
+ ),
})
)
.send()
diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json
index 753dd1163778..c76016103393 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": "13.0.5-canary.3",
+ "version": "13.0.5-canary.4",
"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 e68b764481a9..2a6a6999b5bd 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": "13.0.5-canary.3",
+ "version": "13.0.5-canary.4",
"description": "An experimental package providing utilities for React Refresh.",
"repository": {
"url": "vercel/next.js",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f94b6011783a..819b1ed56419 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -410,7 +410,7 @@ importers:
packages/eslint-config-next:
specifiers:
- '@next/eslint-plugin-next': 13.0.5-canary.3
+ '@next/eslint-plugin-next': 13.0.5-canary.4
'@rushstack/eslint-patch': ^1.1.3
'@typescript-eslint/parser': ^5.42.0
eslint-import-resolver-node: ^0.3.6
@@ -478,12 +478,12 @@ importers:
'@hapi/accept': 5.0.2
'@napi-rs/cli': 2.12.0
'@napi-rs/triples': 1.1.0
- '@next/env': 13.0.5-canary.3
- '@next/polyfill-module': 13.0.5-canary.3
- '@next/polyfill-nomodule': 13.0.5-canary.3
- '@next/react-dev-overlay': 13.0.5-canary.3
- '@next/react-refresh-utils': 13.0.5-canary.3
- '@next/swc': 13.0.5-canary.3
+ '@next/env': 13.0.5-canary.4
+ '@next/polyfill-module': 13.0.5-canary.4
+ '@next/polyfill-nomodule': 13.0.5-canary.4
+ '@next/react-dev-overlay': 13.0.5-canary.4
+ '@next/react-refresh-utils': 13.0.5-canary.4
+ '@next/swc': 13.0.5-canary.4
'@segment/ajv-human-errors': 2.1.2
'@swc/helpers': 0.4.14
'@taskr/clear': 1.1.0
diff --git a/test/e2e/app-dir/app-middleware.test.ts b/test/e2e/app-dir/app-middleware.test.ts
index 91b79a72f52a..4c1caccd53d8 100644
--- a/test/e2e/app-dir/app-middleware.test.ts
+++ b/test/e2e/app-dir/app-middleware.test.ts
@@ -1,7 +1,7 @@
/* eslint-env jest */
import { NextInstance } from 'test/lib/next-modes/base'
-import { fetchViaHTTP } from 'next-test-utils'
+import { fetchViaHTTP, renderViaHTTP } from 'next-test-utils'
import { createNext, FileRef } from 'e2e-utils'
import cheerio from 'cheerio'
import path from 'path'
@@ -18,10 +18,6 @@ describe('app-dir with middleware', () => {
beforeAll(async () => {
next = await createNext({
files: new FileRef(path.join(__dirname, 'app-middleware')),
- dependencies: {
- react: 'experimental',
- 'react-dom': 'experimental',
- },
})
})
@@ -122,3 +118,41 @@ describe('app-dir with middleware', () => {
})
})
})
+
+describe('app dir middleware without pages dir', () => {
+ if ((global as any).isNextDeploy) {
+ it('should skip next deploy for now', () => {})
+ return
+ }
+
+ let next: NextInstance
+
+ afterAll(() => next.destroy())
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ app: new FileRef(path.join(__dirname, 'app-middleware/app')),
+ 'next.config.js': new FileRef(
+ path.join(__dirname, 'app-middleware/next.config.js')
+ ),
+ 'middleware.js': `
+ import { NextResponse } from 'next/server'
+
+ export async function middleware(request) {
+ return new NextResponse('redirected')
+ }
+
+ export const config = {
+ matcher: '/headers'
+ }
+ `,
+ },
+ })
+ })
+
+ it(`Updates headers`, async () => {
+ const html = await renderViaHTTP(next.url, '/headers')
+
+ expect(html).toContain('redirected')
+ })
+})
diff --git a/test/e2e/app-dir/app-middleware/next.config.js b/test/e2e/app-dir/app-middleware/next.config.js
index a928ea943ce2..4ab660b86b15 100644
--- a/test/e2e/app-dir/app-middleware/next.config.js
+++ b/test/e2e/app-dir/app-middleware/next.config.js
@@ -1,7 +1,6 @@
module.exports = {
experimental: {
appDir: true,
- legacyBrowsers: false,
- browsersListForSwc: true,
+ allowMiddlewareResponseBody: true,
},
}
diff --git a/test/e2e/app-dir/app/app/dynamic-pages-route-app-overlap/app-dir/page.js b/test/e2e/app-dir/app/app/dynamic-pages-route-app-overlap/app-dir/page.js
new file mode 100644
index 000000000000..2b529331402f
--- /dev/null
+++ b/test/e2e/app-dir/app/app/dynamic-pages-route-app-overlap/app-dir/page.js
@@ -0,0 +1,7 @@
+export default function Page() {
+ return (
+
+ hello from app/dynamic-pages-route-app-overlap/app-dir/page
+
+ )
+}
diff --git a/test/e2e/app-dir/app/pages/dynamic-pages-route-app-overlap.js b/test/e2e/app-dir/app/pages/dynamic-pages-route-app-overlap.js
new file mode 100644
index 000000000000..725caae59813
--- /dev/null
+++ b/test/e2e/app-dir/app/pages/dynamic-pages-route-app-overlap.js
@@ -0,0 +1,9 @@
+import Link from 'next/link'
+
+export default function Page() {
+ return (
+
+ To /dynamic-pages-route-app-overlap/app-dir
+
+ )
+}
diff --git a/test/e2e/app-dir/app/pages/dynamic-pages-route-app-overlap/[slug].js b/test/e2e/app-dir/app/pages/dynamic-pages-route-app-overlap/[slug].js
new file mode 100644
index 000000000000..80f7029a0281
--- /dev/null
+++ b/test/e2e/app-dir/app/pages/dynamic-pages-route-app-overlap/[slug].js
@@ -0,0 +1,7 @@
+export default function Page() {
+ return (
+
+ hello from pages/dynamic-pages-route-app-overlap/[slug]
+
+ )
+}
diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts
index 8f04fdfb6269..215d1af06884 100644
--- a/test/e2e/app-dir/index.test.ts
+++ b/test/e2e/app-dir/index.test.ts
@@ -299,20 +299,6 @@ describe('app dir', () => {
expect(html).toContain('hello from app/partial-match-[id]. ID is: 123')
})
- // This is a workaround to fix https://github.com/vercel/next.js/issues/5860
- // TODO: remove this workaround when https://bugs.webkit.org/show_bug.cgi?id=187726 is fixed.
- it('should use cache busting when loading css (dev only)', async () => {
- const html = await renderViaHTTP(next.url, '/')
- const $ = cheerio.load(html)
- const links = $('link[rel=stylesheet]')
- links.each((_, link) => {
- const href = $(link).attr('href')
- isDev
- ? expect(href).toMatch(/\?ts=/)
- : expect(href).not.toMatch(/\?ts=/)
- })
- })
-
describe('rewrites', () => {
// TODO-APP: rewrite url is broken
it('should support rewrites on initial load', async () => {
@@ -615,6 +601,29 @@ describe('app dir', () => {
await browser.close()
}
})
+
+ it('should navigate to pages dynamic route from pages page if it overlaps with an app page', async () => {
+ const browser = await webdriver(
+ next.url,
+ '/dynamic-pages-route-app-overlap'
+ )
+
+ try {
+ // Click the link.
+ await browser.elementById('pages-link').click()
+ expect(await browser.waitForElementByCss('#pages-text').text()).toBe(
+ 'hello from pages/dynamic-pages-route-app-overlap/[slug]'
+ )
+
+ // When refreshing the browser, the app page should be rendered
+ await browser.refresh()
+ expect(await browser.waitForElementByCss('#app-text').text()).toBe(
+ 'hello from app/dynamic-pages-route-app-overlap/app-dir/page'
+ )
+ } finally {
+ await browser.close()
+ }
+ })
})
describe('server components', () => {
@@ -1514,7 +1523,7 @@ describe('app dir', () => {
const html = await renderViaHTTP(next.url, '/loading-bug/hi')
// The link tag should be included together with loading
expect(html).toMatch(
- /Loading...<\/h2>/
+ /Loading...<\/h2>/
)
})
@@ -1574,6 +1583,66 @@ describe('app dir', () => {
).toBe('50px')
})
})
+
+ if (isDev) {
+ describe('HMR', () => {
+ it('should support HMR for CSS imports in server components', async () => {
+ const filePath = 'app/css/css-page/style.css'
+ const origContent = await next.readFile(filePath)
+
+ // h1 should be red
+ const browser = await webdriver(next.url, '/css/css-page')
+ expect(
+ await browser.eval(
+ `window.getComputedStyle(document.querySelector('h1')).color`
+ )
+ ).toBe('rgb(255, 0, 0)')
+
+ try {
+ await next.patchFile(filePath, origContent.replace('red', 'blue'))
+
+ // Wait for HMR to trigger
+ await new Promise((resolve) => setTimeout(resolve, 2000))
+
+ expect(
+ await browser.eval(
+ `window.getComputedStyle(document.querySelector('h1')).color`
+ )
+ ).toBe('rgb(0, 0, 255)')
+ } finally {
+ await next.patchFile(filePath, origContent)
+ }
+ })
+
+ it('should support HMR for CSS imports in client components', async () => {
+ const filePath = 'app/css/css-client/client-page.css'
+ const origContent = await next.readFile(filePath)
+
+ // h1 should be red
+ const browser = await webdriver(next.url, '/css/css-client')
+ expect(
+ await browser.eval(
+ `window.getComputedStyle(document.querySelector('h1')).color`
+ )
+ ).toBe('rgb(255, 0, 0)')
+
+ try {
+ await next.patchFile(filePath, origContent.replace('red', 'blue'))
+
+ // Wait for HMR to trigger
+ await new Promise((resolve) => setTimeout(resolve, 2000))
+
+ expect(
+ await browser.eval(
+ `window.getComputedStyle(document.querySelector('h1')).color`
+ )
+ ).toBe('rgb(0, 0, 255)')
+ } finally {
+ await next.patchFile(filePath, origContent)
+ }
+ })
+ })
+ }
})
describe('searchParams prop', () => {
diff --git a/test/e2e/skip-trailing-slash-redirect/app/pages/index.js b/test/e2e/skip-trailing-slash-redirect/app/pages/index.js
index 04fbd07fee5b..8cf5943c3cd9 100644
--- a/test/e2e/skip-trailing-slash-redirect/app/pages/index.js
+++ b/test/e2e/skip-trailing-slash-redirect/app/pages/index.js
@@ -8,6 +8,10 @@ export default function Page(props) {
to another
+
+ to another
+
+
to /blog/first
diff --git a/test/e2e/skip-trailing-slash-redirect/index.test.ts b/test/e2e/skip-trailing-slash-redirect/index.test.ts
index e31d7a6909fc..cb3e14c15f54 100644
--- a/test/e2e/skip-trailing-slash-redirect/index.test.ts
+++ b/test/e2e/skip-trailing-slash-redirect/index.test.ts
@@ -174,6 +174,54 @@ describe('skip-trailing-slash-redirect', () => {
expect(await res.text()).toContain('another page')
})
+ it('should not apply trailing slash to links on client', async () => {
+ const browser = await webdriver(next.url, '/')
+ await browser.eval('window.beforeNav = 1')
+
+ expect(
+ new URL(
+ await browser.elementByCss('#to-another').getAttribute('href'),
+ 'http://n'
+ ).pathname
+ ).toBe('/another')
+
+ await browser.elementByCss('#to-another').click()
+ await browser.waitForElementByCss('#another')
+
+ expect(await browser.eval('window.location.pathname')).toBe('/another')
+
+ await browser.back().waitForElementByCss('#to-another')
+
+ expect(
+ new URL(
+ await browser
+ .elementByCss('#to-another-with-slash')
+ .getAttribute('href'),
+ 'http://n'
+ ).pathname
+ ).toBe('/another/')
+
+ await browser.elementByCss('#to-another-with-slash').click()
+ await browser.waitForElementByCss('#another')
+
+ expect(await browser.eval('window.location.pathname')).toBe('/another/')
+
+ await browser.back().waitForElementByCss('#to-another')
+ expect(await browser.eval('window.beforeNav')).toBe(1)
+ })
+
+ it('should not apply trailing slash on load on client', async () => {
+ let browser = await webdriver(next.url, '/another')
+ await check(() => browser.eval('next.router.isReady ? "yes": "no"'), 'yes')
+
+ expect(await browser.eval('location.pathname')).toBe('/another')
+
+ browser = await webdriver(next.url, '/another/')
+ await check(() => browser.eval('next.router.isReady ? "yes": "no"'), 'yes')
+
+ expect(await browser.eval('location.pathname')).toBe('/another/')
+ })
+
it('should respond to index correctly', async () => {
const res = await fetchViaHTTP(next.url, '/', undefined, {
redirect: 'manual',
diff --git a/test/e2e/streaming-ssr/index.test.ts b/test/e2e/streaming-ssr/index.test.ts
index 958bec7e0a5c..9f500fa2a17c 100644
--- a/test/e2e/streaming-ssr/index.test.ts
+++ b/test/e2e/streaming-ssr/index.test.ts
@@ -2,6 +2,7 @@ import { join } from 'path'
import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'test/lib/next-modes/base'
import {
+ check,
fetchViaHTTP,
findPort,
initNextServerScript,
@@ -16,7 +17,7 @@ const react18Deps = {
const isNextProd = !(global as any).isNextDev && !(global as any).isNextDeploy
-describe('react 18 streaming SSR with custom next configs', () => {
+describe('streaming SSR with custom next configs', () => {
let next: NextInstance
beforeAll(async () => {
@@ -74,10 +75,37 @@ describe('react 18 streaming SSR with custom next configs', () => {
const html = await renderViaHTTP(next.url, '/multi-byte')
expect(html).toContain('マルチバイト'.repeat(28))
})
+
+ if ((global as any).isNextDev) {
+ it('should work with custom document', async () => {
+ await next.patchFile(
+ 'pages/_document.js',
+ `
+ import { Html, Head, Main, NextScript } from 'next/document'
+
+ export default function Document() {
+ return (
+
+
+
+
+
+
+
+ )
+ }
+ `
+ )
+ await check(async () => {
+ return await renderViaHTTP(next.url, '/')
+ }, /index/)
+ await next.deleteFile('pages/_document.js')
+ })
+ }
})
if (isNextProd) {
- describe('react 18 streaming SSR with custom server', () => {
+ describe('streaming SSR with custom server', () => {
let next
let server
let appPort