From 967d66996a3001291bd53a1dfae304ecd7d80103 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 6 Mar 2023 20:20:27 -0500 Subject: [PATCH 01/37] fix: `output: export` support for `app` --- packages/next/src/build/webpack-config.ts | 1 + .../router-reducer/fetch-server-response.ts | 10 ++++- packages/next/src/export/index.ts | 45 +++++++++++++++++-- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 8e5eba3f31590..2718ca24253ad 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -319,6 +319,7 @@ export function getDefineEnv({ }), 'process.env.__NEXT_ROUTER_BASEPATH': JSON.stringify(config.basePath), 'process.env.__NEXT_HAS_REWRITES': JSON.stringify(hasRewrites), + 'process.env.__NEXT_CONFIG_OUTPUT': JSON.stringify(config.output), 'process.env.__NEXT_I18N_SUPPORT': JSON.stringify(!!config.i18n), 'process.env.__NEXT_I18N_DOMAINS': JSON.stringify(config.i18n?.domains), 'process.env.__NEXT_ANALYTICS_ID': JSON.stringify(config.analyticsId), diff --git a/packages/next/src/client/components/router-reducer/fetch-server-response.ts b/packages/next/src/client/components/router-reducer/fetch-server-response.ts index e14b067be0671..fbd245c476111 100644 --- a/packages/next/src/client/components/router-reducer/fetch-server-response.ts +++ b/packages/next/src/client/components/router-reducer/fetch-server-response.ts @@ -35,7 +35,15 @@ export async function fetchServerResponse( } try { - const res = await fetch(url.toString(), { + let cloneUrl = new URL(url) + if (process.env.__NEXT_CONFIG_OUTPUT === 'export') { + if (cloneUrl.pathname.endsWith('/')) { + cloneUrl.pathname += 'index.rsc' + } else { + cloneUrl.pathname += +'.rsc' + } + } + const res = await fetch(cloneUrl, { // Backwards compat for older browsers. `same-origin` is the default in modern browsers. credentials: 'same-origin', headers, diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index c0eabef7eeec8..af386d4f0aade 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -31,6 +31,7 @@ import { PRERENDER_MANIFEST, SERVER_DIRECTORY, SERVER_REFERENCE_MANIFEST, + APP_PATH_ROUTES_MANIFEST, } from '../shared/lib/constants' import loadConfig from '../server/config' import { ExportPathMap, NextConfigComplete } from '../server/config-shared' @@ -238,6 +239,11 @@ export default async function exportApp( prerenderManifest = require(join(distDir, PRERENDER_MANIFEST)) } catch (_) {} + let appRoutePathManifest: any | undefined = undefined + try { + appRoutePathManifest = require(join(distDir, APP_PATH_ROUTES_MANIFEST)) + } catch (_) {} + const excludedPrerenderRoutes = new Set() const pages = options.pages || Object.keys(pagesManifest) const defaultPathMap: ExportPathMap = {} @@ -269,6 +275,21 @@ export default async function exportApp( defaultPathMap[page] = { page } } + for (var [key, value] of Object.entries(appRoutePathManifest)) { + let val = value as string + if ( + key.endsWith('/page') && + !prerenderManifest?.routes[val] && + !prerenderManifest?.dynamicRoutes[val] + ) { + defaultPathMap[val] = { + page: key, + // @ts-ignore + _isAppDir: true, + } + } + } + // Initialize the output directory const outDir = options.outdir @@ -711,7 +732,15 @@ export default async function exportApp( await Promise.all( Object.keys(prerenderManifest.routes).map(async (route) => { const { srcRoute } = prerenderManifest!.routes[route] - const pageName = srcRoute || route + let appSrcRoute = srcRoute + for (const [keyAppRoute, valueAppRoute] of Object.entries( + appRoutePathManifest + )) { + if (valueAppRoute === srcRoute) { + appSrcRoute = keyAppRoute + } + } + const pageName = appSrcRoute || route // returning notFound: true from getStaticProps will not // output html/json files during the build @@ -720,7 +749,8 @@ export default async function exportApp( } route = normalizePagePath(route) - const pagePath = getPagePath(pageName, distDir, undefined, false) + const isAppPath = Boolean(appSrcRoute) + const pagePath = getPagePath(pageName, distDir, undefined, isAppPath) const distPagesDir = join( pagePath, // strip leading / and then recurse number of nested dirs @@ -743,13 +773,20 @@ export default async function exportApp( outDir, `${route}.amp${subFolders ? `${sep}index` : ''}.html` ) - const jsonDest = join(pagesDataDir, `${route}.json`) + const jsonDest = isAppPath + ? join( + outDir, + `${route}${ + subFolders && route !== '/index' ? `${sep}index` : '' + }.rsc` + ) + : join(pagesDataDir, `${route}.json`) await promises.mkdir(dirname(htmlDest), { recursive: true }) await promises.mkdir(dirname(jsonDest), { recursive: true }) const htmlSrc = `${orig}.html` - const jsonSrc = `${orig}.json` + const jsonSrc = `${orig}${isAppPath ? '.rsc' : '.json'}` await promises.copyFile(htmlSrc, htmlDest) await promises.copyFile(jsonSrc, jsonDest) From fd30e7f2b256f9c40218fea8f9cd63f16f7ffc2b Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 6 Mar 2023 20:33:15 -0500 Subject: [PATCH 02/37] add initial test --- .../app-dir-export/app/another/page.js | 10 ++++++++++ test/integration/app-dir-export/app/favicon.ico | Bin 0 -> 25931 bytes test/integration/app-dir-export/app/layout.js | 7 +++++++ test/integration/app-dir-export/app/page.js | 10 ++++++++++ test/integration/app-dir-export/next.config.js | 10 ++++++++++ 5 files changed, 37 insertions(+) create mode 100644 test/integration/app-dir-export/app/another/page.js create mode 100644 test/integration/app-dir-export/app/favicon.ico create mode 100644 test/integration/app-dir-export/app/layout.js create mode 100644 test/integration/app-dir-export/app/page.js create mode 100644 test/integration/app-dir-export/next.config.js diff --git a/test/integration/app-dir-export/app/another/page.js b/test/integration/app-dir-export/app/another/page.js new file mode 100644 index 0000000000000..5b3fdf5824548 --- /dev/null +++ b/test/integration/app-dir-export/app/another/page.js @@ -0,0 +1,10 @@ +import Link from 'next/link' + +export default function Another() { + return ( +
+

Another

+ Visit the home page +
+ ) +} diff --git a/test/integration/app-dir-export/app/favicon.ico b/test/integration/app-dir-export/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/test/integration/app-dir-export/app/layout.js b/test/integration/app-dir-export/app/layout.js new file mode 100644 index 0000000000000..4ee00a218505a --- /dev/null +++ b/test/integration/app-dir-export/app/layout.js @@ -0,0 +1,7 @@ +export default function RootLayout({ children }) { + return ( + + {children} + + ) +} diff --git a/test/integration/app-dir-export/app/page.js b/test/integration/app-dir-export/app/page.js new file mode 100644 index 0000000000000..3997a8c7edfba --- /dev/null +++ b/test/integration/app-dir-export/app/page.js @@ -0,0 +1,10 @@ +import Link from 'next/link' + +export default function Home() { + return ( +
+

Home

+ Visit another page +
+ ) +} diff --git a/test/integration/app-dir-export/next.config.js b/test/integration/app-dir-export/next.config.js new file mode 100644 index 0000000000000..60f203d55f521 --- /dev/null +++ b/test/integration/app-dir-export/next.config.js @@ -0,0 +1,10 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'export', + trailingSlash: true, + experimental: { + appDir: true, + }, +} + +module.exports = nextConfig From af8963dbc9e4f8e6c30c56037a0067abb196097c Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 7 Mar 2023 11:17:08 -0500 Subject: [PATCH 03/37] Change `.rsc` to `.txt` --- .../router-reducer/fetch-server-response.ts | 12 ++++++++---- packages/next/src/export/index.ts | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/next/src/client/components/router-reducer/fetch-server-response.ts b/packages/next/src/client/components/router-reducer/fetch-server-response.ts index fbd245c476111..810df4938ff83 100644 --- a/packages/next/src/client/components/router-reducer/fetch-server-response.ts +++ b/packages/next/src/client/components/router-reducer/fetch-server-response.ts @@ -34,13 +34,15 @@ export async function fetchServerResponse( headers[NEXT_ROUTER_PREFETCH] = '1' } + const isNextExport = process.env.__NEXT_CONFIG_OUTPUT === 'export' + try { let cloneUrl = new URL(url) - if (process.env.__NEXT_CONFIG_OUTPUT === 'export') { + if (isNextExport) { if (cloneUrl.pathname.endsWith('/')) { - cloneUrl.pathname += 'index.rsc' + cloneUrl.pathname += 'index.txt' } else { - cloneUrl.pathname += +'.rsc' + cloneUrl.pathname += +'.txt' } } const res = await fetch(cloneUrl, { @@ -52,8 +54,10 @@ export async function fetchServerResponse( ? urlToUrlWithoutFlightMarker(res.url) : undefined + const contentType = res.headers.get('content-type') const isFlightResponse = - res.headers.get('content-type') === RSC_CONTENT_TYPE_HEADER + contentType === RSC_CONTENT_TYPE_HEADER || + (isNextExport && contentType === 'text/plain') // If fetch returns something different than flight response handle it like a mpa navigation if (!isFlightResponse) { diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index af386d4f0aade..9aaca658b3f59 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -778,7 +778,7 @@ export default async function exportApp( outDir, `${route}${ subFolders && route !== '/index' ? `${sep}index` : '' - }.rsc` + }.txt` ) : join(pagesDataDir, `${route}.json`) From aac1058eb93cbf7d647e947d2d3667c94e485577 Mon Sep 17 00:00:00 2001 From: Steven Date: Wed, 8 Mar 2023 19:44:17 -0500 Subject: [PATCH 04/37] Add if statement --- packages/next/src/export/index.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index 9aaca658b3f59..794ad0252cf59 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -275,17 +275,19 @@ export default async function exportApp( defaultPathMap[page] = { page } } - for (var [key, value] of Object.entries(appRoutePathManifest)) { - let val = value as string - if ( - key.endsWith('/page') && - !prerenderManifest?.routes[val] && - !prerenderManifest?.dynamicRoutes[val] - ) { - defaultPathMap[val] = { - page: key, - // @ts-ignore - _isAppDir: true, + if (!options.buildExport) { + for (var [key, value] of Object.entries(appRoutePathManifest)) { + let val = value as string + if ( + key.endsWith('/page') && + !prerenderManifest?.routes[val] && + !prerenderManifest?.dynamicRoutes[val] + ) { + defaultPathMap[val] = { + page: key, + // @ts-ignore + _isAppDir: true, + } } } } From 6785b0f2e960ad289ca14b1b57061859007e27ab Mon Sep 17 00:00:00 2001 From: Steven Date: Wed, 8 Mar 2023 21:50:26 -0500 Subject: [PATCH 05/37] Handle undefined --- packages/next/src/export/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index 794ad0252cf59..4b650fcadb3b6 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -239,7 +239,7 @@ export default async function exportApp( prerenderManifest = require(join(distDir, PRERENDER_MANIFEST)) } catch (_) {} - let appRoutePathManifest: any | undefined = undefined + let appRoutePathManifest: Record | undefined = undefined try { appRoutePathManifest = require(join(distDir, APP_PATH_ROUTES_MANIFEST)) } catch (_) {} @@ -275,7 +275,7 @@ export default async function exportApp( defaultPathMap[page] = { page } } - if (!options.buildExport) { + if (!options.buildExport && appRoutePathManifest) { for (var [key, value] of Object.entries(appRoutePathManifest)) { let val = value as string if ( @@ -736,7 +736,7 @@ export default async function exportApp( const { srcRoute } = prerenderManifest!.routes[route] let appSrcRoute = srcRoute for (const [keyAppRoute, valueAppRoute] of Object.entries( - appRoutePathManifest + appRoutePathManifest || {} )) { if (valueAppRoute === srcRoute) { appSrcRoute = keyAppRoute From 6c2130ebd8f0da789c8e449072344c97e9882758 Mon Sep 17 00:00:00 2001 From: Steven Date: Wed, 8 Mar 2023 22:48:30 -0500 Subject: [PATCH 06/37] Fix isAppPath --- packages/next/src/export/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index 4b650fcadb3b6..4d9bd979ced69 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -735,11 +735,13 @@ export default async function exportApp( Object.keys(prerenderManifest.routes).map(async (route) => { const { srcRoute } = prerenderManifest!.routes[route] let appSrcRoute = srcRoute + let isAppPath = false for (const [keyAppRoute, valueAppRoute] of Object.entries( appRoutePathManifest || {} )) { if (valueAppRoute === srcRoute) { appSrcRoute = keyAppRoute + isAppPath = true } } const pageName = appSrcRoute || route @@ -751,7 +753,6 @@ export default async function exportApp( } route = normalizePagePath(route) - const isAppPath = Boolean(appSrcRoute) const pagePath = getPagePath(pageName, distDir, undefined, isAppPath) const distPagesDir = join( pagePath, From a2387708c0865c2ddb708bcdc0527754b4abe478 Mon Sep 17 00:00:00 2001 From: Steven Date: Thu, 9 Mar 2023 10:44:25 -0500 Subject: [PATCH 07/37] Optimize the code --- packages/next/src/export/index.ts | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index 4d9bd979ced69..21ed9fa6c8c59 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -275,16 +275,19 @@ export default async function exportApp( defaultPathMap[page] = { page } } + const mapAppRouteToPage = new Map() if (!options.buildExport && appRoutePathManifest) { - for (var [key, value] of Object.entries(appRoutePathManifest)) { - let val = value as string + for (const [pageName, routePath] of Object.entries( + appRoutePathManifest + )) { + mapAppRouteToPage.set(routePath, pageName) if ( - key.endsWith('/page') && - !prerenderManifest?.routes[val] && - !prerenderManifest?.dynamicRoutes[val] + pageName.endsWith('/page') && + !prerenderManifest?.routes[routePath] && + !prerenderManifest?.dynamicRoutes[routePath] ) { - defaultPathMap[val] = { - page: key, + defaultPathMap[routePath] = { + page: pageName, // @ts-ignore _isAppDir: true, } @@ -734,17 +737,9 @@ export default async function exportApp( await Promise.all( Object.keys(prerenderManifest.routes).map(async (route) => { const { srcRoute } = prerenderManifest!.routes[route] - let appSrcRoute = srcRoute - let isAppPath = false - for (const [keyAppRoute, valueAppRoute] of Object.entries( - appRoutePathManifest || {} - )) { - if (valueAppRoute === srcRoute) { - appSrcRoute = keyAppRoute - isAppPath = true - } - } - const pageName = appSrcRoute || route + const appPageName = mapAppRouteToPage.get(srcRoute || '') + const pageName = appPageName || srcRoute || route + const isAppPath = Boolean(appPageName) // returning notFound: true from getStaticProps will not // output html/json files during the build From 87858c7988183574fc6b2df9e7597005167459f4 Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 10 Mar 2023 10:29:05 -0500 Subject: [PATCH 08/37] Fix typo in test --- test/integration/app-dir-export/app/another/page.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/app-dir-export/app/another/page.js b/test/integration/app-dir-export/app/another/page.js index 5b3fdf5824548..5b2ce83f7269b 100644 --- a/test/integration/app-dir-export/app/another/page.js +++ b/test/integration/app-dir-export/app/another/page.js @@ -4,7 +4,7 @@ export default function Another() { return (

Another

- Visit the home page + Visit the home page
) } From 84f43ecca39ef12164b18222a686e4c5d4b188ff Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 10 Mar 2023 11:25:06 -0500 Subject: [PATCH 09/37] Fix a bug with str concat --- .../components/router-reducer/fetch-server-response.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/next/src/client/components/router-reducer/fetch-server-response.ts b/packages/next/src/client/components/router-reducer/fetch-server-response.ts index 810df4938ff83..176c7e59a205c 100644 --- a/packages/next/src/client/components/router-reducer/fetch-server-response.ts +++ b/packages/next/src/client/components/router-reducer/fetch-server-response.ts @@ -42,7 +42,7 @@ export async function fetchServerResponse( if (cloneUrl.pathname.endsWith('/')) { cloneUrl.pathname += 'index.txt' } else { - cloneUrl.pathname += +'.txt' + cloneUrl.pathname += '.txt' } } const res = await fetch(cloneUrl, { @@ -54,10 +54,10 @@ export async function fetchServerResponse( ? urlToUrlWithoutFlightMarker(res.url) : undefined - const contentType = res.headers.get('content-type') + const contentType = res.headers.get('content-type') || '' const isFlightResponse = contentType === RSC_CONTENT_TYPE_HEADER || - (isNextExport && contentType === 'text/plain') + (isNextExport && contentType.startsWith('text/plain')) // If fetch returns something different than flight response handle it like a mpa navigation if (!isFlightResponse) { From cfbd2e1d0b4749af3664ae425a016d570f683dad Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 10 Mar 2023 12:32:30 -0500 Subject: [PATCH 10/37] Add tests --- test/integration/app-dir-export/app/page.js | 4 +- .../integration/app-dir-export/next.config.js | 2 +- .../app-dir-export/test/index.test.ts | 86 +++++++++++++++++++ 3 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 test/integration/app-dir-export/test/index.test.ts diff --git a/test/integration/app-dir-export/app/page.js b/test/integration/app-dir-export/app/page.js index 3997a8c7edfba..280fe5d65b33b 100644 --- a/test/integration/app-dir-export/app/page.js +++ b/test/integration/app-dir-export/app/page.js @@ -4,7 +4,9 @@ export default function Home() { return (

Home

- Visit another page + Visit without trailingslash +
+ Visit with trailingslash
) } diff --git a/test/integration/app-dir-export/next.config.js b/test/integration/app-dir-export/next.config.js index 60f203d55f521..d82a011dff7e8 100644 --- a/test/integration/app-dir-export/next.config.js +++ b/test/integration/app-dir-export/next.config.js @@ -1,7 +1,7 @@ /** @type {import('next').NextConfig} */ const nextConfig = { output: 'export', - trailingSlash: true, + // replace-me experimental: { appDir: true, }, diff --git a/test/integration/app-dir-export/test/index.test.ts b/test/integration/app-dir-export/test/index.test.ts new file mode 100644 index 0000000000000..c173014d1791b --- /dev/null +++ b/test/integration/app-dir-export/test/index.test.ts @@ -0,0 +1,86 @@ +/* eslint-env jest */ + +import { join } from 'path' +import fs from 'fs-extra' +import webdriver from 'next-webdriver' +import { + File, + nextBuild, + nextExport, + startStaticServer, + stopApp, +} from 'next-test-utils' + +const appDir = join(__dirname, '..') +const distDir = join(__dirname, '.next') +const exportDir = join(appDir, 'out') +const nextConfig = new File(join(appDir, 'next.config.js')) +let app: any +let appPort: number + +describe('app dir with next export', () => { + beforeAll(async () => { + await fs.remove(distDir) + await fs.remove(exportDir) + await nextBuild(appDir) + await nextExport(appDir, { outdir: exportDir }) + app = await startStaticServer(exportDir) + appPort = app.address().port + }) + afterAll(async () => { + await stopApp(app) + }) + + describe('trailingSlash true', () => { + beforeAll(async () => { + nextConfig.replace('// replace-me', 'trailingSlash: true,') + }) + afterAll(async () => { + nextConfig.restore() + }) + it('should correctly navigate between pages', async () => { + const browser = await webdriver(appPort, '/') + expect(await browser.elementByCss('h1').text()).toBe('Home') + expect(await browser.elementByCss('a').text()).toBe( + 'Visit without trailingslash' + ) + await browser.elementByCss('a').click() + expect(await browser.elementByCss('h1').text()).toBe('Another') + expect(await browser.elementByCss('a').text()).toBe('Visit the home page') + await browser.elementByCss('a').click() + expect(await browser.elementByCss('h1').text()).toBe('Home') + expect(await browser.elementByCss('a:last-of-type').text()).toBe( + 'Visit with trailingslash' + ) + await browser.elementByCss('a:last-of-type').click() + expect(await browser.elementByCss('h1').text()).toBe('Another') + expect(await browser.elementByCss('a').text()).toBe('Visit the home page') + }) + }) + describe('trailingSlash false', () => { + beforeAll(async () => { + nextConfig.replace('// replace-me', 'trailingSlash: false,') + }) + afterAll(async () => { + nextConfig.restore() + }) + it('should correctly navigate between pages', async () => { + const browser = await webdriver(appPort, '/') + expect(await browser.elementByCss('h1').text()).toBe('Home') + expect(await browser.elementByCss('a').text()).toBe( + 'Visit without trailingslash' + ) + await browser.elementByCss('a').click() + expect(await browser.elementByCss('h1').text()).toBe('Another') + expect(await browser.elementByCss('a').text()).toBe('Visit the home page') + await browser.elementByCss('a').click() + expect(await browser.elementByCss('h1').text()).toBe('Home') + expect(await browser.elementByCss('a:last-of-type').text()).toBe( + 'Visit with trailingslash' + ) + await browser.elementByCss('a:last-of-type').click() + expect(await browser.elementByCss('h1').text()).toBe('Another') + expect(await browser.elementByCss('a').text()).toBe('Visit the home page') + }) + }) +}) From 6ccf8c5e7e19bcb7f3b4dfadf4f3038bb436adf4 Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 10 Mar 2023 15:20:23 -0500 Subject: [PATCH 11/37] Refactor test --- .../app-dir-export/test/index.test.ts | 82 +++++++------------ 1 file changed, 30 insertions(+), 52 deletions(-) diff --git a/test/integration/app-dir-export/test/index.test.ts b/test/integration/app-dir-export/test/index.test.ts index c173014d1791b..cd2edba8b2fe9 100644 --- a/test/integration/app-dir-export/test/index.test.ts +++ b/test/integration/app-dir-export/test/index.test.ts @@ -31,56 +31,34 @@ describe('app dir with next export', () => { await stopApp(app) }) - describe('trailingSlash true', () => { - beforeAll(async () => { - nextConfig.replace('// replace-me', 'trailingSlash: true,') - }) - afterAll(async () => { - nextConfig.restore() - }) - it('should correctly navigate between pages', async () => { - const browser = await webdriver(appPort, '/') - expect(await browser.elementByCss('h1').text()).toBe('Home') - expect(await browser.elementByCss('a').text()).toBe( - 'Visit without trailingslash' - ) - await browser.elementByCss('a').click() - expect(await browser.elementByCss('h1').text()).toBe('Another') - expect(await browser.elementByCss('a').text()).toBe('Visit the home page') - await browser.elementByCss('a').click() - expect(await browser.elementByCss('h1').text()).toBe('Home') - expect(await browser.elementByCss('a:last-of-type').text()).toBe( - 'Visit with trailingslash' - ) - await browser.elementByCss('a:last-of-type').click() - expect(await browser.elementByCss('h1').text()).toBe('Another') - expect(await browser.elementByCss('a').text()).toBe('Visit the home page') - }) - }) - describe('trailingSlash false', () => { - beforeAll(async () => { - nextConfig.replace('// replace-me', 'trailingSlash: false,') - }) - afterAll(async () => { - nextConfig.restore() - }) - it('should correctly navigate between pages', async () => { - const browser = await webdriver(appPort, '/') - expect(await browser.elementByCss('h1').text()).toBe('Home') - expect(await browser.elementByCss('a').text()).toBe( - 'Visit without trailingslash' - ) - await browser.elementByCss('a').click() - expect(await browser.elementByCss('h1').text()).toBe('Another') - expect(await browser.elementByCss('a').text()).toBe('Visit the home page') - await browser.elementByCss('a').click() - expect(await browser.elementByCss('h1').text()).toBe('Home') - expect(await browser.elementByCss('a:last-of-type').text()).toBe( - 'Visit with trailingslash' - ) - await browser.elementByCss('a:last-of-type').click() - expect(await browser.elementByCss('h1').text()).toBe('Another') - expect(await browser.elementByCss('a').text()).toBe('Visit the home page') - }) - }) + it.each([{ trailingSlash: false }, { trailingSlash: true }])( + "should correctly navigate between pages with trailingSlash '$trailingSlash'", + async ({ trailingSlash }) => { + nextConfig.replace('// replace-me', `trailingSlash: ${trailingSlash},`) + try { + const browser = await webdriver(appPort, '/') + expect(await browser.elementByCss('h1').text()).toBe('Home') + expect(await browser.elementByCss('a').text()).toBe( + 'Visit without trailingslash' + ) + await browser.elementByCss('a').click() + expect(await browser.elementByCss('h1').text()).toBe('Another') + expect(await browser.elementByCss('a').text()).toBe( + 'Visit the home page' + ) + await browser.elementByCss('a').click() + expect(await browser.elementByCss('h1').text()).toBe('Home') + expect(await browser.elementByCss('a:last-of-type').text()).toBe( + 'Visit with trailingslash' + ) + await browser.elementByCss('a:last-of-type').click() + expect(await browser.elementByCss('h1').text()).toBe('Another') + expect(await browser.elementByCss('a').text()).toBe( + 'Visit the home page' + ) + } finally { + nextConfig.restore() + } + } + ) }) From 7ffffe107ff2ce311cc851c0ed73e33f3b64bda9 Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 10 Mar 2023 16:09:19 -0500 Subject: [PATCH 12/37] Add tests with `generateStaticParams()` --- .../app-dir-export/app/another/[slug]/page.js | 20 ++++++++ .../app-dir-export/app/another/page.js | 15 +++++- test/integration/app-dir-export/app/page.js | 17 +++++-- .../app-dir-export/test/index.test.ts | 47 +++++++++++++++---- 4 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 test/integration/app-dir-export/app/another/[slug]/page.js diff --git a/test/integration/app-dir-export/app/another/[slug]/page.js b/test/integration/app-dir-export/app/another/[slug]/page.js new file mode 100644 index 0000000000000..9c4a7e34f303e --- /dev/null +++ b/test/integration/app-dir-export/app/another/[slug]/page.js @@ -0,0 +1,20 @@ +import Link from 'next/link' + +export const dynamic = 'force-static' + +export function generateStaticParams() { + return [{ slug: 'first' }, { slug: 'second' }] +} + +export default function Page({ params }) { + return ( +
+

{params.slug}

+
    +
  • + Visit another page +
  • +
+
+ ) +} diff --git a/test/integration/app-dir-export/app/another/page.js b/test/integration/app-dir-export/app/another/page.js index 5b2ce83f7269b..5819307096ee5 100644 --- a/test/integration/app-dir-export/app/another/page.js +++ b/test/integration/app-dir-export/app/another/page.js @@ -4,7 +4,20 @@ export default function Another() { return (

Another

- Visit the home page + +
  • + Visit the home page +
  • +
  • + another page +
  • +
  • + another first page +
  • +
  • + another second page +
  • +
    ) } diff --git a/test/integration/app-dir-export/app/page.js b/test/integration/app-dir-export/app/page.js index 280fe5d65b33b..49479c80b425f 100644 --- a/test/integration/app-dir-export/app/page.js +++ b/test/integration/app-dir-export/app/page.js @@ -4,9 +4,20 @@ export default function Home() { return (

    Home

    - Visit without trailingslash -
    - Visit with trailingslash +
      +
    • + another no trailingslash +
    • +
    • + another has trailingslash +
    • +
    • + another first page +
    • +
    • + another second page +
    • +
    ) } diff --git a/test/integration/app-dir-export/test/index.test.ts b/test/integration/app-dir-export/test/index.test.ts index cd2edba8b2fe9..21cd7e08ceabe 100644 --- a/test/integration/app-dir-export/test/index.test.ts +++ b/test/integration/app-dir-export/test/index.test.ts @@ -36,26 +36,55 @@ describe('app dir with next export', () => { async ({ trailingSlash }) => { nextConfig.replace('// replace-me', `trailingSlash: ${trailingSlash},`) try { + const child = (n: number) => `li:nth-child(${n}) a` const browser = await webdriver(appPort, '/') expect(await browser.elementByCss('h1').text()).toBe('Home') - expect(await browser.elementByCss('a').text()).toBe( - 'Visit without trailingslash' + expect(await browser.elementByCss(child(1)).text()).toBe( + 'another no trailingslash' ) - await browser.elementByCss('a').click() + await browser.elementByCss(child(1)).click() + expect(await browser.elementByCss('h1').text()).toBe('Another') - expect(await browser.elementByCss('a').text()).toBe( + expect(await browser.elementByCss(child(1)).text()).toBe( 'Visit the home page' ) - await browser.elementByCss('a').click() + await browser.elementByCss(child(1)).click() + expect(await browser.elementByCss('h1').text()).toBe('Home') - expect(await browser.elementByCss('a:last-of-type').text()).toBe( - 'Visit with trailingslash' + expect(await browser.elementByCss(child(2)).text()).toBe( + 'another has trailingslash' ) - await browser.elementByCss('a:last-of-type').click() + await browser.elementByCss(child(2)).click() + expect(await browser.elementByCss('h1').text()).toBe('Another') - expect(await browser.elementByCss('a').text()).toBe( + expect(await browser.elementByCss(child(1)).text()).toBe( 'Visit the home page' ) + await browser.elementByCss(child(1)).click() + + expect(await browser.elementByCss('h1').text()).toBe('Home') + expect(await browser.elementByCss(child(3)).text()).toBe( + 'another first page' + ) + await browser.elementByCss(child(3)).click() + + expect(await browser.elementByCss('h1').text()).toBe('first') + expect(await browser.elementByCss(child(1)).text()).toBe( + 'Visit another page' + ) + await browser.elementByCss(child(1)).click() + + expect(await browser.elementByCss('h1').text()).toBe('Another') + expect(await browser.elementByCss(child(4)).text()).toBe( + 'another second page' + ) + await browser.elementByCss(child(4)).click() + + expect(await browser.elementByCss('h1').text()).toBe('second') + expect(await browser.elementByCss(child(1)).text()).toBe( + 'Visit another page' + ) + await browser.elementByCss(child(1)).click() } finally { nextConfig.restore() } From e64914b69ac60e0e38a30f94631128c335539cb8 Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 10 Mar 2023 16:11:13 -0500 Subject: [PATCH 13/37] anchor --- .../app-dir-export/test/index.test.ts | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/test/integration/app-dir-export/test/index.test.ts b/test/integration/app-dir-export/test/index.test.ts index 21cd7e08ceabe..35761b1119ad4 100644 --- a/test/integration/app-dir-export/test/index.test.ts +++ b/test/integration/app-dir-export/test/index.test.ts @@ -36,55 +36,55 @@ describe('app dir with next export', () => { async ({ trailingSlash }) => { nextConfig.replace('// replace-me', `trailingSlash: ${trailingSlash},`) try { - const child = (n: number) => `li:nth-child(${n}) a` + const a = (n: number) => `li:nth-child(${n}) a` const browser = await webdriver(appPort, '/') expect(await browser.elementByCss('h1').text()).toBe('Home') - expect(await browser.elementByCss(child(1)).text()).toBe( + expect(await browser.elementByCss(a(1)).text()).toBe( 'another no trailingslash' ) - await browser.elementByCss(child(1)).click() + await browser.elementByCss(a(1)).click() expect(await browser.elementByCss('h1').text()).toBe('Another') - expect(await browser.elementByCss(child(1)).text()).toBe( + expect(await browser.elementByCss(a(1)).text()).toBe( 'Visit the home page' ) - await browser.elementByCss(child(1)).click() + await browser.elementByCss(a(1)).click() expect(await browser.elementByCss('h1').text()).toBe('Home') - expect(await browser.elementByCss(child(2)).text()).toBe( + expect(await browser.elementByCss(a(2)).text()).toBe( 'another has trailingslash' ) - await browser.elementByCss(child(2)).click() + await browser.elementByCss(a(2)).click() expect(await browser.elementByCss('h1').text()).toBe('Another') - expect(await browser.elementByCss(child(1)).text()).toBe( + expect(await browser.elementByCss(a(1)).text()).toBe( 'Visit the home page' ) - await browser.elementByCss(child(1)).click() + await browser.elementByCss(a(1)).click() expect(await browser.elementByCss('h1').text()).toBe('Home') - expect(await browser.elementByCss(child(3)).text()).toBe( + expect(await browser.elementByCss(a(3)).text()).toBe( 'another first page' ) - await browser.elementByCss(child(3)).click() + await browser.elementByCss(a(3)).click() expect(await browser.elementByCss('h1').text()).toBe('first') - expect(await browser.elementByCss(child(1)).text()).toBe( + expect(await browser.elementByCss(a(1)).text()).toBe( 'Visit another page' ) - await browser.elementByCss(child(1)).click() + await browser.elementByCss(a(1)).click() expect(await browser.elementByCss('h1').text()).toBe('Another') - expect(await browser.elementByCss(child(4)).text()).toBe( + expect(await browser.elementByCss(a(4)).text()).toBe( 'another second page' ) - await browser.elementByCss(child(4)).click() + await browser.elementByCss(a(4)).click() expect(await browser.elementByCss('h1').text()).toBe('second') - expect(await browser.elementByCss(child(1)).text()).toBe( + expect(await browser.elementByCss(a(1)).text()).toBe( 'Visit another page' ) - await browser.elementByCss(child(1)).click() + await browser.elementByCss(a(1)).click() } finally { nextConfig.restore() } From 0d0b25455ea81a6ce98448b138bf4863d1cd410e Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 10 Mar 2023 16:25:33 -0500 Subject: [PATCH 14/37] Add delay --- .../app-dir-export/app/another/[slug]/page.js | 2 +- .../app-dir-export/test/index.test.ts | 35 +++++++++++-------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/test/integration/app-dir-export/app/another/[slug]/page.js b/test/integration/app-dir-export/app/another/[slug]/page.js index 9c4a7e34f303e..c1acd08ba9dbd 100644 --- a/test/integration/app-dir-export/app/another/[slug]/page.js +++ b/test/integration/app-dir-export/app/another/[slug]/page.js @@ -1,6 +1,6 @@ import Link from 'next/link' -export const dynamic = 'force-static' +export const dynamic = 'error' export function generateStaticParams() { return [{ slug: 'first' }, { slug: 'second' }] diff --git a/test/integration/app-dir-export/test/index.test.ts b/test/integration/app-dir-export/test/index.test.ts index 35761b1119ad4..d8da6777d9d34 100644 --- a/test/integration/app-dir-export/test/index.test.ts +++ b/test/integration/app-dir-export/test/index.test.ts @@ -9,32 +9,29 @@ import { nextExport, startStaticServer, stopApp, + waitFor, } from 'next-test-utils' const appDir = join(__dirname, '..') const distDir = join(__dirname, '.next') const exportDir = join(appDir, 'out') const nextConfig = new File(join(appDir, 'next.config.js')) -let app: any -let appPort: number +const slugPage = new File(join(appDir, 'app/another/[slug]/page.js')) +const delay = 100 describe('app dir with next export', () => { - beforeAll(async () => { - await fs.remove(distDir) - await fs.remove(exportDir) - await nextBuild(appDir) - await nextExport(appDir, { outdir: exportDir }) - app = await startStaticServer(exportDir) - appPort = app.address().port - }) - afterAll(async () => { - await stopApp(app) - }) - it.each([{ trailingSlash: false }, { trailingSlash: true }])( "should correctly navigate between pages with trailingSlash '$trailingSlash'", async ({ trailingSlash }) => { nextConfig.replace('// replace-me', `trailingSlash: ${trailingSlash},`) + await fs.remove(distDir) + await fs.remove(exportDir) + await nextBuild(appDir) + await nextExport(appDir, { outdir: exportDir }) + const app = await startStaticServer(exportDir) + const address = app.address() + const appPort = typeof address !== 'string' ? address.port : 3000 + try { const a = (n: number) => `li:nth-child(${n}) a` const browser = await webdriver(appPort, '/') @@ -43,50 +40,60 @@ describe('app dir with next export', () => { 'another no trailingslash' ) await browser.elementByCss(a(1)).click() + await waitFor(delay) expect(await browser.elementByCss('h1').text()).toBe('Another') expect(await browser.elementByCss(a(1)).text()).toBe( 'Visit the home page' ) await browser.elementByCss(a(1)).click() + await waitFor(delay) expect(await browser.elementByCss('h1').text()).toBe('Home') expect(await browser.elementByCss(a(2)).text()).toBe( 'another has trailingslash' ) await browser.elementByCss(a(2)).click() + await waitFor(delay) expect(await browser.elementByCss('h1').text()).toBe('Another') expect(await browser.elementByCss(a(1)).text()).toBe( 'Visit the home page' ) await browser.elementByCss(a(1)).click() + await waitFor(delay) expect(await browser.elementByCss('h1').text()).toBe('Home') expect(await browser.elementByCss(a(3)).text()).toBe( 'another first page' ) await browser.elementByCss(a(3)).click() + await waitFor(delay) expect(await browser.elementByCss('h1').text()).toBe('first') expect(await browser.elementByCss(a(1)).text()).toBe( 'Visit another page' ) await browser.elementByCss(a(1)).click() + await waitFor(delay) expect(await browser.elementByCss('h1').text()).toBe('Another') expect(await browser.elementByCss(a(4)).text()).toBe( 'another second page' ) await browser.elementByCss(a(4)).click() + await waitFor(delay) expect(await browser.elementByCss('h1').text()).toBe('second') expect(await browser.elementByCss(a(1)).text()).toBe( 'Visit another page' ) await browser.elementByCss(a(1)).click() + await waitFor(delay) } finally { + await stopApp(app) nextConfig.restore() + slugPage.restore() } } ) From 4b646bfed93e9afa78849a48ab6d327c5ec42852 Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 10 Mar 2023 16:38:38 -0500 Subject: [PATCH 15/37] Add tests for `dynamic` but somehow not working --- .../app-dir-export/app/another/[slug]/page.js | 2 +- .../integration/app-dir-export/next.config.js | 2 +- .../app-dir-export/test/index.test.ts | 146 ++++++++++-------- 3 files changed, 80 insertions(+), 70 deletions(-) diff --git a/test/integration/app-dir-export/app/another/[slug]/page.js b/test/integration/app-dir-export/app/another/[slug]/page.js index c1acd08ba9dbd..fe0b0fd72d8a9 100644 --- a/test/integration/app-dir-export/app/another/[slug]/page.js +++ b/test/integration/app-dir-export/app/another/[slug]/page.js @@ -1,6 +1,6 @@ import Link from 'next/link' -export const dynamic = 'error' +export const dynamic = 'auto' export function generateStaticParams() { return [{ slug: 'first' }, { slug: 'second' }] diff --git a/test/integration/app-dir-export/next.config.js b/test/integration/app-dir-export/next.config.js index d82a011dff7e8..60f203d55f521 100644 --- a/test/integration/app-dir-export/next.config.js +++ b/test/integration/app-dir-export/next.config.js @@ -1,7 +1,7 @@ /** @type {import('next').NextConfig} */ const nextConfig = { output: 'export', - // replace-me + trailingSlash: true, experimental: { appDir: true, }, diff --git a/test/integration/app-dir-export/test/index.test.ts b/test/integration/app-dir-export/test/index.test.ts index d8da6777d9d34..393059087963b 100644 --- a/test/integration/app-dir-export/test/index.test.ts +++ b/test/integration/app-dir-export/test/index.test.ts @@ -19,82 +19,92 @@ const nextConfig = new File(join(appDir, 'next.config.js')) const slugPage = new File(join(appDir, 'app/another/[slug]/page.js')) const delay = 100 -describe('app dir with next export', () => { - it.each([{ trailingSlash: false }, { trailingSlash: true }])( - "should correctly navigate between pages with trailingSlash '$trailingSlash'", - async ({ trailingSlash }) => { - nextConfig.replace('// replace-me', `trailingSlash: ${trailingSlash},`) - await fs.remove(distDir) - await fs.remove(exportDir) - await nextBuild(appDir) - await nextExport(appDir, { outdir: exportDir }) - const app = await startStaticServer(exportDir) - const address = app.address() - const appPort = typeof address !== 'string' ? address.port : 3000 +async function runTests({ + trailingSlash, + dynamic, +}: { + trailingSlash: boolean + dynamic: string +}) { + nextConfig.replace('trailingSlash: true,', `trailingSlash: ${trailingSlash},`) + slugPage.replace(`const dynamic = 'auto'`, `const dynamic = '${dynamic}'`) + await fs.remove(distDir) + await fs.remove(exportDir) + await nextBuild(appDir) + await nextExport(appDir, { outdir: exportDir }) + const app = await startStaticServer(exportDir) + const address = app.address() + const appPort = typeof address !== 'string' ? address.port : 3000 + + try { + const a = (n: number) => `li:nth-child(${n}) a` + const browser = await webdriver(appPort, '/') + expect(await browser.elementByCss('h1').text()).toBe('Home') + expect(await browser.elementByCss(a(1)).text()).toBe( + 'another no trailingslash' + ) + await browser.elementByCss(a(1)).click() + await waitFor(delay) - try { - const a = (n: number) => `li:nth-child(${n}) a` - const browser = await webdriver(appPort, '/') - expect(await browser.elementByCss('h1').text()).toBe('Home') - expect(await browser.elementByCss(a(1)).text()).toBe( - 'another no trailingslash' - ) - await browser.elementByCss(a(1)).click() - await waitFor(delay) + expect(await browser.elementByCss('h1').text()).toBe('Another') + expect(await browser.elementByCss(a(1)).text()).toBe('Visit the home page') + await browser.elementByCss(a(1)).click() + await waitFor(delay) - expect(await browser.elementByCss('h1').text()).toBe('Another') - expect(await browser.elementByCss(a(1)).text()).toBe( - 'Visit the home page' - ) - await browser.elementByCss(a(1)).click() - await waitFor(delay) + expect(await browser.elementByCss('h1').text()).toBe('Home') + expect(await browser.elementByCss(a(2)).text()).toBe( + 'another has trailingslash' + ) + await browser.elementByCss(a(2)).click() + await waitFor(delay) - expect(await browser.elementByCss('h1').text()).toBe('Home') - expect(await browser.elementByCss(a(2)).text()).toBe( - 'another has trailingslash' - ) - await browser.elementByCss(a(2)).click() - await waitFor(delay) + expect(await browser.elementByCss('h1').text()).toBe('Another') + expect(await browser.elementByCss(a(1)).text()).toBe('Visit the home page') + await browser.elementByCss(a(1)).click() + await waitFor(delay) - expect(await browser.elementByCss('h1').text()).toBe('Another') - expect(await browser.elementByCss(a(1)).text()).toBe( - 'Visit the home page' - ) - await browser.elementByCss(a(1)).click() - await waitFor(delay) + expect(await browser.elementByCss('h1').text()).toBe('Home') + expect(await browser.elementByCss(a(3)).text()).toBe('another first page') + await browser.elementByCss(a(3)).click() + await waitFor(delay) - expect(await browser.elementByCss('h1').text()).toBe('Home') - expect(await browser.elementByCss(a(3)).text()).toBe( - 'another first page' - ) - await browser.elementByCss(a(3)).click() - await waitFor(delay) + expect(await browser.elementByCss('h1').text()).toBe('first') + expect(await browser.elementByCss(a(1)).text()).toBe('Visit another page') + await browser.elementByCss(a(1)).click() + await waitFor(delay) - expect(await browser.elementByCss('h1').text()).toBe('first') - expect(await browser.elementByCss(a(1)).text()).toBe( - 'Visit another page' - ) - await browser.elementByCss(a(1)).click() - await waitFor(delay) + expect(await browser.elementByCss('h1').text()).toBe('Another') + expect(await browser.elementByCss(a(4)).text()).toBe('another second page') + await browser.elementByCss(a(4)).click() + await waitFor(delay) - expect(await browser.elementByCss('h1').text()).toBe('Another') - expect(await browser.elementByCss(a(4)).text()).toBe( - 'another second page' - ) - await browser.elementByCss(a(4)).click() - await waitFor(delay) + expect(await browser.elementByCss('h1').text()).toBe('second') + expect(await browser.elementByCss(a(1)).text()).toBe('Visit another page') + await browser.elementByCss(a(1)).click() + await waitFor(delay) + } finally { + await stopApp(app) + nextConfig.restore() + slugPage.restore() + } +} - expect(await browser.elementByCss('h1').text()).toBe('second') - expect(await browser.elementByCss(a(1)).text()).toBe( - 'Visit another page' - ) - await browser.elementByCss(a(1)).click() - await waitFor(delay) - } finally { - await stopApp(app) - nextConfig.restore() - slugPage.restore() - } +describe('app dir with next export', () => { + it.each([{ trailingSlash: false }, { trailingSlash: true }])( + "should correctly navigate between pages with trailingSlash '$trailingSlash'", + async ({ trailingSlash }) => { + await runTests({ trailingSlash, dynamic: 'auto' }) + } + ) + it.each([ + { dynamic: 'auto' }, + { dynamic: 'force-static' }, + { dynamic: 'error' }, + { dynamic: 'force-dynamic' }, + ])( + "should correctly navigate between pages with dynamic '$dynamic'", + async ({ dynamic }) => { + await runTests({ trailingSlash: true, dynamic }) } ) }) From 41aaf27645361f1c5321da04eac5dd8de8b35593 Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 10 Mar 2023 16:48:34 -0500 Subject: [PATCH 16/37] Minor change to test names --- test/integration/app-dir-export/test/index.test.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/integration/app-dir-export/test/index.test.ts b/test/integration/app-dir-export/test/index.test.ts index 393059087963b..f29600f0040dd 100644 --- a/test/integration/app-dir-export/test/index.test.ts +++ b/test/integration/app-dir-export/test/index.test.ts @@ -91,18 +91,20 @@ async function runTests({ describe('app dir with next export', () => { it.each([{ trailingSlash: false }, { trailingSlash: true }])( - "should correctly navigate between pages with trailingSlash '$trailingSlash'", + "should work with trailingSlash '$trailingSlash'", async ({ trailingSlash }) => { await runTests({ trailingSlash, dynamic: 'auto' }) } ) it.each([ { dynamic: 'auto' }, - { dynamic: 'force-static' }, { dynamic: 'error' }, - { dynamic: 'force-dynamic' }, - ])( - "should correctly navigate between pages with dynamic '$dynamic'", + { dynamic: 'force-static' }, + ])("should work with dynamic '$dynamic'", async ({ dynamic }) => { + await runTests({ trailingSlash: true, dynamic }) + }) + it.each([{ dynamic: 'force-dynamic' }])( + "should throw with dynamic '$dynamic'", async ({ dynamic }) => { await runTests({ trailingSlash: true, dynamic }) } From d0ac0e843b391f50f03ee3f21e3e8245305b3302 Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 10 Mar 2023 17:02:49 -0500 Subject: [PATCH 17/37] Fix tests --- .../app-dir-export/app/another/[slug]/page.js | 2 +- .../app-dir-export/test/index.test.ts | 25 +++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/test/integration/app-dir-export/app/another/[slug]/page.js b/test/integration/app-dir-export/app/another/[slug]/page.js index fe0b0fd72d8a9..9c4a7e34f303e 100644 --- a/test/integration/app-dir-export/app/another/[slug]/page.js +++ b/test/integration/app-dir-export/app/another/[slug]/page.js @@ -1,6 +1,6 @@ import Link from 'next/link' -export const dynamic = 'auto' +export const dynamic = 'force-static' export function generateStaticParams() { return [{ slug: 'first' }, { slug: 'second' }] diff --git a/test/integration/app-dir-export/test/index.test.ts b/test/integration/app-dir-export/test/index.test.ts index f29600f0040dd..e5ff3ad836b41 100644 --- a/test/integration/app-dir-export/test/index.test.ts +++ b/test/integration/app-dir-export/test/index.test.ts @@ -23,11 +23,21 @@ async function runTests({ trailingSlash, dynamic, }: { - trailingSlash: boolean - dynamic: string + trailingSlash?: boolean + dynamic?: string }) { - nextConfig.replace('trailingSlash: true,', `trailingSlash: ${trailingSlash},`) - slugPage.replace(`const dynamic = 'auto'`, `const dynamic = '${dynamic}'`) + if (trailingSlash) { + nextConfig.replace( + 'trailingSlash: true,', + `trailingSlash: ${trailingSlash},` + ) + } + if (dynamic) { + slugPage.replace( + `const dynamic = 'force-static'`, + `const dynamic = '${dynamic}'` + ) + } await fs.remove(distDir) await fs.remove(exportDir) await nextBuild(appDir) @@ -38,6 +48,7 @@ async function runTests({ try { const a = (n: number) => `li:nth-child(${n}) a` + console.log('[navigate]') const browser = await webdriver(appPort, '/') expect(await browser.elementByCss('h1').text()).toBe('Home') expect(await browser.elementByCss(a(1)).text()).toBe( @@ -93,7 +104,7 @@ describe('app dir with next export', () => { it.each([{ trailingSlash: false }, { trailingSlash: true }])( "should work with trailingSlash '$trailingSlash'", async ({ trailingSlash }) => { - await runTests({ trailingSlash, dynamic: 'auto' }) + await runTests({ trailingSlash }) } ) it.each([ @@ -101,12 +112,12 @@ describe('app dir with next export', () => { { dynamic: 'error' }, { dynamic: 'force-static' }, ])("should work with dynamic '$dynamic'", async ({ dynamic }) => { - await runTests({ trailingSlash: true, dynamic }) + await runTests({ dynamic }) }) it.each([{ dynamic: 'force-dynamic' }])( "should throw with dynamic '$dynamic'", async ({ dynamic }) => { - await runTests({ trailingSlash: true, dynamic }) + await runTests({ dynamic }) } ) }) From 74ca9ff691ada7d26e2a74d71addd64f9ef69cf5 Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 10 Mar 2023 19:10:18 -0500 Subject: [PATCH 18/37] Fix all teh things --- packages/next/src/build/utils.ts | 13 +++++++++++++ packages/next/src/server/app-render.tsx | 8 +++++--- test/integration/app-dir-export/test/index.test.ts | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 4d6113078e0da..48374e95ad12e 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -1479,6 +1479,19 @@ export async function isPageStatic({ }, {} ) + if (!appConfig.dynamic) { + appConfig.dynamic = 'auto' + } + + if (process.env.__NEXT_CONFIG_OUTPUT === 'export') { + if (appConfig.dynamic === 'auto') { + appConfig.dynamic = 'error' + } else if (appConfig.dynamic === 'force-dynamic') { + throw new Error( + 'export const dynamic = "force-dynamic" cannot be used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export' + ) + } + } if (appConfig.dynamic === 'force-dynamic') { appConfig.revalidate = 0 diff --git a/packages/next/src/server/app-render.tsx b/packages/next/src/server/app-render.tsx index 31f8c608ec35e..fe29b84209fcd 100644 --- a/packages/next/src/server/app-render.tsx +++ b/packages/next/src/server/app-render.tsx @@ -1208,15 +1208,17 @@ export async function renderToHTMLOrFlight( ? [DefaultNotFound] : [] - if (typeof layoutOrPageMod?.dynamic === 'string') { + const dynamic = layoutOrPageMod?.dynamic || 'auto' + + if (dynamic !== 'auto') { // the nested most config wins so we only force-static // if it's configured above any parent that configured // otherwise - if (layoutOrPageMod.dynamic === 'error') { + if (dynamic === 'error') { staticGenerationStore.dynamicShouldError = true } else { staticGenerationStore.dynamicShouldError = false - if (layoutOrPageMod.dynamic === 'force-static') { + if (dynamic === 'force-static') { staticGenerationStore.forceStatic = true } else { staticGenerationStore.forceStatic = false diff --git a/test/integration/app-dir-export/test/index.test.ts b/test/integration/app-dir-export/test/index.test.ts index e5ff3ad836b41..b243f2d46bae2 100644 --- a/test/integration/app-dir-export/test/index.test.ts +++ b/test/integration/app-dir-export/test/index.test.ts @@ -117,7 +117,7 @@ describe('app dir with next export', () => { it.each([{ dynamic: 'force-dynamic' }])( "should throw with dynamic '$dynamic'", async ({ dynamic }) => { - await runTests({ dynamic }) + expect('todo not implemented yet').toBe('todo not implemented yet') } ) }) From bcba14ed31adf2f653113c15a24287c086b735ea Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 10 Mar 2023 19:22:41 -0500 Subject: [PATCH 19/37] Add type --- packages/next/src/export/index.ts | 1 - packages/next/src/server/config-shared.ts | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index c1cf17589a502..9e96f58a09fae 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -288,7 +288,6 @@ export default async function exportApp( ) { defaultPathMap[routePath] = { page: pageName, - // @ts-ignore _isAppDir: true, } } diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index b1bc9ea11838f..5c519de1b5a42 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -242,7 +242,11 @@ export interface ExperimentalConfig { } export type ExportPathMap = { - [path: string]: { page: string; query?: Record } + [path: string]: { + page: string + query?: Record + _isAppDir?: boolean + } } /** From 491594361389823c79a4c7ba051fb67cef02fd6c Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 13 Mar 2023 09:49:54 -0400 Subject: [PATCH 20/37] Fix server side check for config --- packages/next/src/build/index.ts | 2 ++ packages/next/src/build/utils.ts | 9 ++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 9b29e8ea96240..b2a05ed47eda2 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -1282,6 +1282,7 @@ export default async function build( enableUndici: config.experimental.enableUndici, locales: config.i18n?.locales, defaultLocale: config.i18n?.defaultLocale, + nextConfigOutput: config.output, }) ) @@ -1466,6 +1467,7 @@ export default async function build( isrFlushToDisk: config.experimental.isrFlushToDisk, maxMemoryCacheSize: config.experimental.isrMemoryCacheSize, + nextConfigOutput: config.output, }) } ) diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 48374e95ad12e..eeb1460485b64 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -1329,6 +1329,7 @@ export async function isPageStatic({ isrFlushToDisk, maxMemoryCacheSize, incrementalCacheHandlerPath, + nextConfigOutput, }: { page: string distDir: string @@ -1347,6 +1348,7 @@ export async function isPageStatic({ isrFlushToDisk?: boolean maxMemoryCacheSize?: number incrementalCacheHandlerPath?: string + nextConfigOutput: 'standalone' | 'export' }): Promise<{ isStatic?: boolean isAmpOnly?: boolean @@ -1479,12 +1481,9 @@ export async function isPageStatic({ }, {} ) - if (!appConfig.dynamic) { - appConfig.dynamic = 'auto' - } - if (process.env.__NEXT_CONFIG_OUTPUT === 'export') { - if (appConfig.dynamic === 'auto') { + if (nextConfigOutput === 'export') { + if (!appConfig.dynamic || appConfig.dynamic === 'auto') { appConfig.dynamic = 'error' } else if (appConfig.dynamic === 'force-dynamic') { throw new Error( From 2059734b858538c1216b1eb16d27f9fbd79a1c64 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 13 Mar 2023 09:50:11 -0400 Subject: [PATCH 21/37] Fix client side tree-shaking for config --- .../router-reducer/fetch-server-response.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/next/src/client/components/router-reducer/fetch-server-response.ts b/packages/next/src/client/components/router-reducer/fetch-server-response.ts index 176c7e59a205c..11d70aca51d92 100644 --- a/packages/next/src/client/components/router-reducer/fetch-server-response.ts +++ b/packages/next/src/client/components/router-reducer/fetch-server-response.ts @@ -34,11 +34,9 @@ export async function fetchServerResponse( headers[NEXT_ROUTER_PREFETCH] = '1' } - const isNextExport = process.env.__NEXT_CONFIG_OUTPUT === 'export' - try { let cloneUrl = new URL(url) - if (isNextExport) { + if (process.env.__NEXT_CONFIG_OUTPUT === 'export') { if (cloneUrl.pathname.endsWith('/')) { cloneUrl.pathname += 'index.txt' } else { @@ -55,9 +53,13 @@ export async function fetchServerResponse( : undefined const contentType = res.headers.get('content-type') || '' - const isFlightResponse = - contentType === RSC_CONTENT_TYPE_HEADER || - (isNextExport && contentType.startsWith('text/plain')) + let isFlightResponse = contentType === RSC_CONTENT_TYPE_HEADER + + if (process.env.__NEXT_CONFIG_OUTPUT === 'export') { + if (!isFlightResponse) { + isFlightResponse = contentType.startsWith('text/plain') + } + } // If fetch returns something different than flight response handle it like a mpa navigation if (!isFlightResponse) { From d699b991c3b8bba04484a287f49a71595819fd43 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 13 Mar 2023 09:51:35 -0400 Subject: [PATCH 22/37] Avoid cloning url every time --- .../router-reducer/fetch-server-response.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/next/src/client/components/router-reducer/fetch-server-response.ts b/packages/next/src/client/components/router-reducer/fetch-server-response.ts index 11d70aca51d92..1e201d0022197 100644 --- a/packages/next/src/client/components/router-reducer/fetch-server-response.ts +++ b/packages/next/src/client/components/router-reducer/fetch-server-response.ts @@ -35,15 +35,16 @@ export async function fetchServerResponse( } try { - let cloneUrl = new URL(url) + let fetchUrl = url if (process.env.__NEXT_CONFIG_OUTPUT === 'export') { - if (cloneUrl.pathname.endsWith('/')) { - cloneUrl.pathname += 'index.txt' + fetchUrl = new URL(url) // clone + if (fetchUrl.pathname.endsWith('/')) { + fetchUrl.pathname += 'index.txt' } else { - cloneUrl.pathname += '.txt' + fetchUrl.pathname += '.txt' } } - const res = await fetch(cloneUrl, { + const res = await fetch(fetchUrl, { // Backwards compat for older browsers. `same-origin` is the default in modern browsers. credentials: 'same-origin', headers, From 71bdbb9363db1509883a7664d4f4f6b6d80344c3 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 13 Mar 2023 11:37:21 -0400 Subject: [PATCH 23/37] Fix static file emit (favicon.ico) --- packages/next/src/export/index.ts | 8 ++++++++ .../app-dir-export/app/another/page.js | 7 +++++-- .../app-dir-export/app/image-import/page.js | 18 ++++++++++++++++++ .../app-dir-export/app/image-import/test.png | Bin 0 -> 1545 bytes test/integration/app-dir-export/app/page.js | 3 +++ .../app-dir-export/test/index.test.ts | 13 +++++++++++++ 6 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 test/integration/app-dir-export/app/image-import/page.js create mode 100644 test/integration/app-dir-export/app/image-import/test.png diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index 9e96f58a09fae..e96b1378f7d17 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -739,6 +739,14 @@ export default async function exportApp( const pageName = appPageName || srcRoute || route const isAppPath = Boolean(appPageName) + if (appPageName && !appPageName.endsWith('/page')) { + const srcFile = join(dir, 'app', appPageName) + const destFile = join(outDir, appPageName) + await promises.mkdir(dirname(destFile), { recursive: true }) + await promises.copyFile(srcFile, destFile) + return + } + // returning notFound: true from getStaticProps will not // output html/json files during the build if (prerenderManifest!.notFoundRoutes.includes(route)) { diff --git a/test/integration/app-dir-export/app/another/page.js b/test/integration/app-dir-export/app/another/page.js index 5819307096ee5..22525ff6bd0a8 100644 --- a/test/integration/app-dir-export/app/another/page.js +++ b/test/integration/app-dir-export/app/another/page.js @@ -4,7 +4,7 @@ export default function Another() { return (

    Another

    - +
    • Visit the home page
    • @@ -17,7 +17,10 @@ export default function Another() {
    • another second page
    • - +
    • + image import page +
    • +
    ) } diff --git a/test/integration/app-dir-export/app/image-import/page.js b/test/integration/app-dir-export/app/image-import/page.js new file mode 100644 index 0000000000000..99f03cc82d30d --- /dev/null +++ b/test/integration/app-dir-export/app/image-import/page.js @@ -0,0 +1,18 @@ +import Link from 'next/link' +import img from './test.png' + +export default function ImageImport() { + return ( +
    +

    Image Import

    +
      +
    • + Visit the home page +
    • +
    • + View the image +
    • +
    +
    + ) +} diff --git a/test/integration/app-dir-export/app/image-import/test.png b/test/integration/app-dir-export/app/image-import/test.png new file mode 100644 index 0000000000000000000000000000000000000000..e14fafc5cf3bc63b70914ad20467f40f7fecd572 GIT binary patch literal 1545 zcmbVM{Xf$Q9A6%~&O#i9S`$MamWNGTo{nv7c{rq_?QHVUW~E6OQi@{ZJcmZ|?7l@Q zzFgO>LgSLHn=t)6BZh%t7EPMfk1SL z1Y86JvZ4In(b7~asTB_EYLbzJ#fBxtCqp2a7u(A{gEak&&i%OE5K&=dA02(f0EgVb zDQO?EwAgXhbPx#1STW3~N_6+*i-&gO&5gIVD)qtd)=yh(VkE{hpxOq=E?Uo-)5z*x z!Au!iA$YiLAm+*0qggP>?VsKD-2i&HQxQ3+OqX*8S}wK5H8(1QM_f{Jya%lp;-fFQ z-RxdA9ea)1aI;`EXvn#9J~1_}n?bl%WsA3~x1yF~ZJY?F%5TY1f>Os{GDi>X>C?IS zC87Oo3ZX}KJ*U`mZ%63leZQDa&ij+|L2Ig&kv$8+G!kJ)!A>IpI0!SpvZ=R*dmxwE z_A02!zif^Xi?D&?&%f0Tzbc>bI(#PkQsao89{0s~R(I*hM>py`YIH=n8s(l<+!VhFb)fj#H;uE`npo7 zY;0_#QmGRY6Algzb}0{05Qr9vi1UjyHCq}CIyy~&Xo)lk4660;XBm=IbzH;Vwux!6 z@U`%Q<6`U_r^#vHXzMH%_g}z&^bvih;Naksl&3F)p7Kn#$+goa*xhsUD|t?H%CawT z>JQ8!^fPzDF6c8waZPU1$^P~{X*y_EN`KC=6nc}~iEX#>ud*u)-GT=qZK~K!#eMKri|K2@v zeX7|gqiZ-a27vkY(m>jlb*A45J^WhNqUd5svx=i!WlyGoDxyIkDCJw8 zl1RKs=y0j+xtSIh@AZ-SU-~z%d7|iJXK0I}nj!QZ_;_V0t%N>WpH)B+RT91Kkuhzx zSp{CL@O&X!puOb5enarY#IKV0$GfaZ<5QCF#q6Ih66Bl1Pk?cT!sCl5^YK4KUf8=r z`aO#WUfA<6@Z|tBgFYm!h8b-eKV4c&$3bTW&<9YGGZ&`xG#9~EHI4;**~o$2bOc^F z)xqxjhTZjF)wtZ04Ns<6mIBW?61;SKUp&Ix#QrYF;SY_@rCeH2X2*tJ$*pAIHb zh#ej+0ZbcVCs7JzV7TsL6Jyyhc?vBAKW|d~E=#`(Epz?bhZI(;xeQ`sbe2CXvFp-!)9gAPmnDWWTsf>26XSP@ zv&2i`WrNZNf%ZoawxTiv7?Jj|6+NW@o>r`=449DMidcqyfhe1CUhQqXbvCSyC1#>! z&TQ9Zpp%MX zY5qJSn%bSF+=@PAVhp9?wWsW-al19&OZPE literal 0 HcmV?d00001 diff --git a/test/integration/app-dir-export/app/page.js b/test/integration/app-dir-export/app/page.js index 49479c80b425f..287e369bdaec0 100644 --- a/test/integration/app-dir-export/app/page.js +++ b/test/integration/app-dir-export/app/page.js @@ -17,6 +17,9 @@ export default function Home() {
  • another second page
  • +
  • + image import page +
  • ) diff --git a/test/integration/app-dir-export/test/index.test.ts b/test/integration/app-dir-export/test/index.test.ts index b243f2d46bae2..81799efac1852 100644 --- a/test/integration/app-dir-export/test/index.test.ts +++ b/test/integration/app-dir-export/test/index.test.ts @@ -93,6 +93,19 @@ async function runTests({ expect(await browser.elementByCss(a(1)).text()).toBe('Visit another page') await browser.elementByCss(a(1)).click() await waitFor(delay) + + expect(await browser.elementByCss('h1').text()).toBe('Another') + expect(await browser.elementByCss(a(5)).text()).toBe('image import page') + await browser.elementByCss(a(5)).click() + await waitFor(delay) + + expect(await browser.elementByCss('h1').text()).toBe('Image Import') + expect(await browser.elementByCss(a(2)).text()).toBe('View the image') + expect(await browser.elementByCss(a(2)).href()).toContain( + '/test.3f1a293b.png' + ) + await browser.elementByCss(a(2)).click() + await waitFor(delay) } finally { await stopApp(app) nextConfig.restore() From ab2018dc724cbe7a5f5b4d90a37fdd6eaaff5bce Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 13 Mar 2023 12:18:10 -0400 Subject: [PATCH 24/37] Fix typo in test --- test/integration/app-dir-export/app/image-import/page.js | 2 +- test/integration/app-dir-export/test/index.test.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/test/integration/app-dir-export/app/image-import/page.js b/test/integration/app-dir-export/app/image-import/page.js index 99f03cc82d30d..614ffe45b82f5 100644 --- a/test/integration/app-dir-export/app/image-import/page.js +++ b/test/integration/app-dir-export/app/image-import/page.js @@ -10,7 +10,7 @@ export default function ImageImport() { Visit the home page
  • - View the image + View the image
  • diff --git a/test/integration/app-dir-export/test/index.test.ts b/test/integration/app-dir-export/test/index.test.ts index 81799efac1852..fe9c0890d8ec6 100644 --- a/test/integration/app-dir-export/test/index.test.ts +++ b/test/integration/app-dir-export/test/index.test.ts @@ -101,11 +101,9 @@ async function runTests({ expect(await browser.elementByCss('h1').text()).toBe('Image Import') expect(await browser.elementByCss(a(2)).text()).toBe('View the image') - expect(await browser.elementByCss(a(2)).href()).toContain( + expect(await browser.elementByCss(a(2)).getAttribute('href')).toContain( '/test.3f1a293b.png' ) - await browser.elementByCss(a(2)).click() - await waitFor(delay) } finally { await stopApp(app) nextConfig.restore() From c7304019129d80b1b41b7fae0be651eee86f3036 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 13 Mar 2023 20:30:04 -0400 Subject: [PATCH 25/37] Update tests to check file output --- .../integration/app-dir-export/app/robots.txt | 2 + .../integration/app-dir-export/next.config.js | 3 ++ .../app-dir-export/test/index.test.ts | 38 +++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 test/integration/app-dir-export/app/robots.txt diff --git a/test/integration/app-dir-export/app/robots.txt b/test/integration/app-dir-export/app/robots.txt new file mode 100644 index 0000000000000..c2a49f4fb82f1 --- /dev/null +++ b/test/integration/app-dir-export/app/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / diff --git a/test/integration/app-dir-export/next.config.js b/test/integration/app-dir-export/next.config.js index 60f203d55f521..fba540176b3b9 100644 --- a/test/integration/app-dir-export/next.config.js +++ b/test/integration/app-dir-export/next.config.js @@ -5,6 +5,9 @@ const nextConfig = { experimental: { appDir: true, }, + generateBuildId() { + return 'test-build-id' + }, } module.exports = nextConfig diff --git a/test/integration/app-dir-export/test/index.test.ts b/test/integration/app-dir-export/test/index.test.ts index fe9c0890d8ec6..b5c070dc93580 100644 --- a/test/integration/app-dir-export/test/index.test.ts +++ b/test/integration/app-dir-export/test/index.test.ts @@ -1,8 +1,10 @@ /* eslint-env jest */ import { join } from 'path' +import { promisify } from 'util' import fs from 'fs-extra' import webdriver from 'next-webdriver' +import globOrig from 'glob' import { File, nextBuild, @@ -12,6 +14,7 @@ import { waitFor, } from 'next-test-utils' +const glob = promisify(globOrig) const appDir = join(__dirname, '..') const distDir = join(__dirname, '.next') const exportDir = join(appDir, 'out') @@ -124,6 +127,41 @@ describe('app dir with next export', () => { { dynamic: 'force-static' }, ])("should work with dynamic '$dynamic'", async ({ dynamic }) => { await runTests({ dynamic }) + const opts = { cwd: exportDir, nodir: true } + const files = ((await glob('**/*', opts)) as string[]) + .filter((f) => !f.startsWith('_next/static/chunks/main-app-')) + .sort() + expect(files).toEqual([ + '404.html', + '404/index.html', + '_next/static/chunks/902-f97e36a07660afd2.js', + '_next/static/chunks/app/another/[slug]/page-50aa8f87f076234b.js', + '_next/static/chunks/app/another/page-67a4cd79c77b8516.js', + '_next/static/chunks/app/image-import/page-46c0dca97a7a5cb8.js', + '_next/static/chunks/app/layout-aea7b0f4dfb75fb2.js', + '_next/static/chunks/app/page-73a72272c0754b1f.js', + '_next/static/chunks/main-25b24f330fc66c8e.js', + '_next/static/chunks/pages/_app-5b5607d0f696b287.js', + '_next/static/chunks/pages/_error-e2f15669af03eac8.js', + '_next/static/chunks/polyfills-c67a75d1b6f99dc8.js', + '_next/static/chunks/webpack-8074fabf81ca3fbd.js', + '_next/static/media/favicon.603d046c.ico', + '_next/static/media/test.3f1a293b.png', + '_next/static/test-build-id/_buildManifest.js', + '_next/static/test-build-id/_ssgManifest.js', + 'another/first/index.html', + 'another/first/index.txt', + 'another/index.html', + 'another/index.txt', + 'another/second/index.html', + 'another/second/index.txt', + 'favicon.ico', + 'image-import/index.html', + 'image-import/index.txt', + 'index.html', + 'index.txt', + 'robots.txt', + ]) }) it.each([{ dynamic: 'force-dynamic' }])( "should throw with dynamic '$dynamic'", From bcdac1a448569f0dd44ffb01b9b2ecd3781b2744 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 13 Mar 2023 20:30:16 -0400 Subject: [PATCH 26/37] Exclude `/route` --- packages/next/src/export/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index 3435fbb6ba9ba..4c23e5cd3c97c 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -740,7 +740,12 @@ export default async function exportApp( const pageName = appPageName || srcRoute || route const isAppPath = Boolean(appPageName) - if (appPageName && !appPageName.endsWith('/page')) { + if ( + appPageName && + !appPageName.endsWith('/page') && + !appPageName.endsWith('/route') + ) { + // Since `app` dir doesn't have `public`, we need to copy static assets const srcFile = join(dir, 'app', appPageName) const destFile = join(outDir, appPageName) await promises.mkdir(dirname(destFile), { recursive: true }) From f9d4435b1c945cc5dfba016afaff3a1031ca3c30 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 13 Mar 2023 21:06:04 -0400 Subject: [PATCH 27/37] Handle `/route` --- packages/next/src/export/index.ts | 28 ++++++++++--------- .../app-dir-export/test/index.test.ts | 3 +- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index 4c23e5cd3c97c..09d95173eeafa 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -738,19 +738,21 @@ export default async function exportApp( const { srcRoute } = prerenderManifest!.routes[route] const appPageName = mapAppRouteToPage.get(srcRoute || '') const pageName = appPageName || srcRoute || route - const isAppPath = Boolean(appPageName) - - if ( - appPageName && - !appPageName.endsWith('/page') && - !appPageName.endsWith('/route') - ) { - // Since `app` dir doesn't have `public`, we need to copy static assets - const srcFile = join(dir, 'app', appPageName) - const destFile = join(outDir, appPageName) - await promises.mkdir(dirname(destFile), { recursive: true }) - await promises.copyFile(srcFile, destFile) - return + let isAppPath = false + + if (appPageName) { + isAppPath = true + // TODO: Correctly handle API routes + // See https://github.com/vercel/next.js/blob/7457be0c74e64b4d0617943ed27f4d557cc916be/packages/next/src/server/future/route-handlers/app-route-route-handler.ts#L462-L468 + if (appPageName.endsWith('/route')) { + // Since `app` dir doesn't have `public`, we need to copy static assets + const filename = appPageName.slice(0, -6) + const srcFile = join(dir, 'app', filename) + const destFile = join(outDir, filename) + await promises.mkdir(dirname(destFile), { recursive: true }) + await promises.copyFile(srcFile, destFile) + return + } } // returning notFound: true from getStaticProps will not diff --git a/test/integration/app-dir-export/test/index.test.ts b/test/integration/app-dir-export/test/index.test.ts index b5c070dc93580..510c575314544 100644 --- a/test/integration/app-dir-export/test/index.test.ts +++ b/test/integration/app-dir-export/test/index.test.ts @@ -129,7 +129,7 @@ describe('app dir with next export', () => { await runTests({ dynamic }) const opts = { cwd: exportDir, nodir: true } const files = ((await glob('**/*', opts)) as string[]) - .filter((f) => !f.startsWith('_next/static/chunks/main-app-')) + .filter((f) => !f.startsWith('_next/static/chunks/main-')) .sort() expect(files).toEqual([ '404.html', @@ -140,7 +140,6 @@ describe('app dir with next export', () => { '_next/static/chunks/app/image-import/page-46c0dca97a7a5cb8.js', '_next/static/chunks/app/layout-aea7b0f4dfb75fb2.js', '_next/static/chunks/app/page-73a72272c0754b1f.js', - '_next/static/chunks/main-25b24f330fc66c8e.js', '_next/static/chunks/pages/_app-5b5607d0f696b287.js', '_next/static/chunks/pages/_error-e2f15669af03eac8.js', '_next/static/chunks/polyfills-c67a75d1b6f99dc8.js', From 2deda3fbcc09fd96b770d572c2dd02d5ccd8e034 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 13 Mar 2023 22:07:18 -0400 Subject: [PATCH 28/37] Make it work with API Route Handlers --- packages/next/src/export/index.ts | 27 ++++++++----------- .../app-dir-export/app/api/json/route.js | 3 +++ .../app-dir-export/app/api/txt/route.js | 3 +++ .../app-dir-export/test/index.test.ts | 2 ++ 4 files changed, 19 insertions(+), 16 deletions(-) create mode 100644 test/integration/app-dir-export/app/api/json/route.js create mode 100644 test/integration/app-dir-export/app/api/txt/route.js diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index 09d95173eeafa..4bcdc472d1ace 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -738,22 +738,8 @@ export default async function exportApp( const { srcRoute } = prerenderManifest!.routes[route] const appPageName = mapAppRouteToPage.get(srcRoute || '') const pageName = appPageName || srcRoute || route - let isAppPath = false - - if (appPageName) { - isAppPath = true - // TODO: Correctly handle API routes - // See https://github.com/vercel/next.js/blob/7457be0c74e64b4d0617943ed27f4d557cc916be/packages/next/src/server/future/route-handlers/app-route-route-handler.ts#L462-L468 - if (appPageName.endsWith('/route')) { - // Since `app` dir doesn't have `public`, we need to copy static assets - const filename = appPageName.slice(0, -6) - const srcFile = join(dir, 'app', filename) - const destFile = join(outDir, filename) - await promises.mkdir(dirname(destFile), { recursive: true }) - await promises.copyFile(srcFile, destFile) - return - } - } + const isAppPath = Boolean(appPageName) + const isAppRouteHandler = appPageName?.endsWith('/route') // returning notFound: true from getStaticProps will not // output html/json files during the build @@ -775,6 +761,15 @@ export default async function exportApp( ) const orig = join(distPagesDir, route) + + if (isAppRouteHandler) { + const handlerSrc = `${orig}.body` + const handlerDest = join(outDir, route) + await promises.mkdir(dirname(handlerDest), { recursive: true }) + await promises.copyFile(handlerSrc, handlerDest) + return + } + const htmlDest = join( outDir, `${route}${ diff --git a/test/integration/app-dir-export/app/api/json/route.js b/test/integration/app-dir-export/app/api/json/route.js new file mode 100644 index 0000000000000..453bd4a65a77f --- /dev/null +++ b/test/integration/app-dir-export/app/api/json/route.js @@ -0,0 +1,3 @@ +export async function GET() { + return Response.json({ answer: 42 }) +} diff --git a/test/integration/app-dir-export/app/api/txt/route.js b/test/integration/app-dir-export/app/api/txt/route.js new file mode 100644 index 0000000000000..e645e88cba9c3 --- /dev/null +++ b/test/integration/app-dir-export/app/api/txt/route.js @@ -0,0 +1,3 @@ +export async function GET() { + return new Response('this is plain text') +} diff --git a/test/integration/app-dir-export/test/index.test.ts b/test/integration/app-dir-export/test/index.test.ts index 510c575314544..46e0d33f424d2 100644 --- a/test/integration/app-dir-export/test/index.test.ts +++ b/test/integration/app-dir-export/test/index.test.ts @@ -154,6 +154,8 @@ describe('app dir with next export', () => { 'another/index.txt', 'another/second/index.html', 'another/second/index.txt', + 'api/json', + 'api/txt', 'favicon.ico', 'image-import/index.html', 'image-import/index.txt', From d42c17964551dc05fbb6e1853587eb18d839ea39 Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 14 Mar 2023 09:32:29 -0400 Subject: [PATCH 29/37] Loosen test comparison and fix `.body` exists --- packages/next/src/export/index.ts | 6 +++--- test/integration/app-dir-export/test/index.test.ts | 14 ++------------ 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index 4bcdc472d1ace..a76a14f708ff5 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -761,10 +761,10 @@ export default async function exportApp( ) const orig = join(distPagesDir, route) + const handlerSrc = `${orig}.body` + const handlerDest = join(outDir, route) - if (isAppRouteHandler) { - const handlerSrc = `${orig}.body` - const handlerDest = join(outDir, route) + if (isAppRouteHandler && (await exists(handlerSrc))) { await promises.mkdir(dirname(handlerDest), { recursive: true }) await promises.copyFile(handlerSrc, handlerDest) return diff --git a/test/integration/app-dir-export/test/index.test.ts b/test/integration/app-dir-export/test/index.test.ts index 46e0d33f424d2..206c295d998f1 100644 --- a/test/integration/app-dir-export/test/index.test.ts +++ b/test/integration/app-dir-export/test/index.test.ts @@ -129,22 +129,12 @@ describe('app dir with next export', () => { await runTests({ dynamic }) const opts = { cwd: exportDir, nodir: true } const files = ((await glob('**/*', opts)) as string[]) - .filter((f) => !f.startsWith('_next/static/chunks/main-')) + .filter((f) => !f.startsWith('_next/static/chunks/')) .sort() expect(files).toEqual([ '404.html', '404/index.html', - '_next/static/chunks/902-f97e36a07660afd2.js', - '_next/static/chunks/app/another/[slug]/page-50aa8f87f076234b.js', - '_next/static/chunks/app/another/page-67a4cd79c77b8516.js', - '_next/static/chunks/app/image-import/page-46c0dca97a7a5cb8.js', - '_next/static/chunks/app/layout-aea7b0f4dfb75fb2.js', - '_next/static/chunks/app/page-73a72272c0754b1f.js', - '_next/static/chunks/pages/_app-5b5607d0f696b287.js', - '_next/static/chunks/pages/_error-e2f15669af03eac8.js', - '_next/static/chunks/polyfills-c67a75d1b6f99dc8.js', - '_next/static/chunks/webpack-8074fabf81ca3fbd.js', - '_next/static/media/favicon.603d046c.ico', + '_next/static/media/favicon.603d046c.ico', // TODO: should this be here? '_next/static/media/test.3f1a293b.png', '_next/static/test-build-id/_buildManifest.js', '_next/static/test-build-id/_ssgManifest.js', From c4cf8b2e286be8beae2c4707967f6f9103bdeec7 Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 14 Mar 2023 09:50:22 -0400 Subject: [PATCH 30/37] Revert app-render --- packages/next/src/server/app-render.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/next/src/server/app-render.tsx b/packages/next/src/server/app-render.tsx index ad8981eb8157d..79143ccc6a010 100644 --- a/packages/next/src/server/app-render.tsx +++ b/packages/next/src/server/app-render.tsx @@ -1247,17 +1247,15 @@ export async function renderToHTMLOrFlight( ? [DefaultNotFound] : [] - const dynamic = layoutOrPageMod?.dynamic || 'auto' - - if (dynamic !== 'auto') { + if (typeof layoutOrPageMod?.dynamic === 'string') { // the nested most config wins so we only force-static // if it's configured above any parent that configured // otherwise - if (dynamic === 'error') { + if (layoutOrPageMod?.dynamic === 'error') { staticGenerationStore.dynamicShouldError = true } else { staticGenerationStore.dynamicShouldError = false - if (dynamic === 'force-static') { + if (layoutOrPageMod?.dynamic === 'force-static') { staticGenerationStore.forceStatic = true } else { staticGenerationStore.forceStatic = false From 04a916d3e5dc5e659ed18ce3b9bbb849e65cdf12 Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 14 Mar 2023 09:53:12 -0400 Subject: [PATCH 31/37] Apply suggestion from code review --- packages/next/src/export/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index a76a14f708ff5..689949bce9d5d 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -52,6 +52,7 @@ import { overrideBuiltInReactPackages, } from '../build/webpack/require-hook' import { MiddlewareManifest } from '../build/webpack/plugins/middleware-plugin' +import { isAppRouteRoute } from '../lib/is-app-route-route' loadRequireHook() if (process.env.NEXT_PREBUNDLED_REACT) { @@ -739,7 +740,7 @@ export default async function exportApp( const appPageName = mapAppRouteToPage.get(srcRoute || '') const pageName = appPageName || srcRoute || route const isAppPath = Boolean(appPageName) - const isAppRouteHandler = appPageName?.endsWith('/route') + const isAppRouteHandler = appPageName && isAppRouteRoute(appPageName) // returning notFound: true from getStaticProps will not // output html/json files during the build From 4ca9e32ea957350b3c1b93a8fb9eda303fb84d10 Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 14 Mar 2023 09:56:41 -0400 Subject: [PATCH 32/37] Use isAppPageRoute() helper --- packages/next/src/export/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index 689949bce9d5d..3cee8e72f622a 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -53,6 +53,7 @@ import { } from '../build/webpack/require-hook' import { MiddlewareManifest } from '../build/webpack/plugins/middleware-plugin' import { isAppRouteRoute } from '../lib/is-app-route-route' +import { isAppPageRoute } from '../lib/is-app-page-route' loadRequireHook() if (process.env.NEXT_PREBUNDLED_REACT) { @@ -283,7 +284,7 @@ export default async function exportApp( )) { mapAppRouteToPage.set(routePath, pageName) if ( - pageName.endsWith('/page') && + isAppPageRoute(pageName) && !prerenderManifest?.routes[routePath] && !prerenderManifest?.dynamicRoutes[routePath] ) { From 696dc17a266c81188b6f02b686d19683190d054f Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 14 Mar 2023 10:38:49 -0400 Subject: [PATCH 33/37] Change 'auto' to undefined --- test/integration/app-dir-export/test/index.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/integration/app-dir-export/test/index.test.ts b/test/integration/app-dir-export/test/index.test.ts index 206c295d998f1..46a363f032a80 100644 --- a/test/integration/app-dir-export/test/index.test.ts +++ b/test/integration/app-dir-export/test/index.test.ts @@ -38,7 +38,7 @@ async function runTests({ if (dynamic) { slugPage.replace( `const dynamic = 'force-static'`, - `const dynamic = '${dynamic}'` + `const dynamic = ${dynamic}` ) } await fs.remove(distDir) @@ -122,10 +122,10 @@ describe('app dir with next export', () => { } ) it.each([ - { dynamic: 'auto' }, - { dynamic: 'error' }, - { dynamic: 'force-static' }, - ])("should work with dynamic '$dynamic'", async ({ dynamic }) => { + { dynamic: 'undefined' }, + { dynamic: "'error'" }, + { dynamic: "'force-static'" }, + ])('should work with dynamic $dynamic', async ({ dynamic }) => { await runTests({ dynamic }) const opts = { cwd: exportDir, nodir: true } const files = ((await glob('**/*', opts)) as string[]) From bf05c8a9cd073a54a60ca5a6add926841dca8056 Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 14 Mar 2023 11:09:43 -0400 Subject: [PATCH 34/37] Add test for force-dynamic --- .../app-dir-export/test/index.test.ts | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/test/integration/app-dir-export/test/index.test.ts b/test/integration/app-dir-export/test/index.test.ts index 46a363f032a80..3a5b31a242a17 100644 --- a/test/integration/app-dir-export/test/index.test.ts +++ b/test/integration/app-dir-export/test/index.test.ts @@ -154,10 +154,23 @@ describe('app dir with next export', () => { 'robots.txt', ]) }) - it.each([{ dynamic: 'force-dynamic' }])( - "should throw with dynamic '$dynamic'", - async ({ dynamic }) => { - expect('todo not implemented yet').toBe('todo not implemented yet') + it("should throw when dynamic 'force-dynamic'", async () => { + slugPage.replace( + `const dynamic = 'force-static'`, + `const dynamic = 'force-dynamic'` + ) + await fs.remove(distDir) + await fs.remove(exportDir) + let result = { code: 0, stderr: '' } + try { + result = await nextBuild(appDir, [], { stderr: true }) + } finally { + nextConfig.restore() + slugPage.restore() } - ) + expect(result.code).toBe(1) + expect(result.stderr).toContain( + 'export const dynamic = "force-dynamic" cannot be used with "output: export".' + ) + }) }) From c14bc93f4d13bc1255c468931ac852aaa220d9cd Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 14 Mar 2023 13:16:44 -0400 Subject: [PATCH 35/37] Remove unused `?` --- packages/next/src/server/app-render.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/next/src/server/app-render.tsx b/packages/next/src/server/app-render.tsx index 79143ccc6a010..e0405ba8eefcf 100644 --- a/packages/next/src/server/app-render.tsx +++ b/packages/next/src/server/app-render.tsx @@ -1251,11 +1251,11 @@ export async function renderToHTMLOrFlight( // the nested most config wins so we only force-static // if it's configured above any parent that configured // otherwise - if (layoutOrPageMod?.dynamic === 'error') { + if (layoutOrPageMod.dynamic === 'error') { staticGenerationStore.dynamicShouldError = true } else { staticGenerationStore.dynamicShouldError = false - if (layoutOrPageMod?.dynamic === 'force-static') { + if (layoutOrPageMod.dynamic === 'force-static') { staticGenerationStore.forceStatic = true } else { staticGenerationStore.forceStatic = false From 1aa1d580ee3e625f1d17c02ecc3a914b9b10df2e Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 14 Mar 2023 13:25:45 -0400 Subject: [PATCH 36/37] Fix tests and include page name --- packages/next/src/build/utils.ts | 2 +- test/integration/app-dir-export/test/index.test.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index c39024b300a94..368fee10c3caa 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -1487,7 +1487,7 @@ export async function isPageStatic({ appConfig.dynamic = 'error' } else if (appConfig.dynamic === 'force-dynamic') { throw new Error( - 'export const dynamic = "force-dynamic" cannot be used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export' + `export const dynamic = "force-dynamic" on page "${page}" cannot be used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export` ) } } diff --git a/test/integration/app-dir-export/test/index.test.ts b/test/integration/app-dir-export/test/index.test.ts index 3a5b31a242a17..832991f55eccc 100644 --- a/test/integration/app-dir-export/test/index.test.ts +++ b/test/integration/app-dir-export/test/index.test.ts @@ -134,7 +134,8 @@ describe('app dir with next export', () => { expect(files).toEqual([ '404.html', '404/index.html', - '_next/static/media/favicon.603d046c.ico', // TODO: should this be here? + // TODO-METADATA: favicon.ico should not be here + '_next/static/media/favicon.603d046c.ico', '_next/static/media/test.3f1a293b.png', '_next/static/test-build-id/_buildManifest.js', '_next/static/test-build-id/_ssgManifest.js', @@ -170,7 +171,7 @@ describe('app dir with next export', () => { } expect(result.code).toBe(1) expect(result.stderr).toContain( - 'export const dynamic = "force-dynamic" cannot be used with "output: export".' + 'export const dynamic = "force-dynamic" on page "/another/[slug]" cannot be used with "output: export".' ) }) }) From 57cd2b95308a4117471c0bf8adc311150c780d5b Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 14 Mar 2023 14:44:03 -0400 Subject: [PATCH 37/37] Separate cases between manifest missing vs manifest corrupt --- packages/next/src/export/index.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index 3cee8e72f622a..8da9f9ff52cbd 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -54,6 +54,7 @@ import { import { MiddlewareManifest } from '../build/webpack/plugins/middleware-plugin' import { isAppRouteRoute } from '../lib/is-app-route-route' import { isAppPageRoute } from '../lib/is-app-page-route' +import isError from '../lib/is-error' loadRequireHook() if (process.env.NEXT_PREBUNDLED_REACT) { @@ -244,7 +245,19 @@ export default async function exportApp( let appRoutePathManifest: Record | undefined = undefined try { appRoutePathManifest = require(join(distDir, APP_PATH_ROUTES_MANIFEST)) - } catch (_) {} + } catch (err) { + if ( + isError(err) && + (err.code === 'ENOENT' || err.code === 'MODULE_NOT_FOUND') + ) { + // the manifest doesn't exist which will happen when using + // "pages" dir instead of "app" dir. + appRoutePathManifest = undefined + } else { + // the manifest is malformed (invalid json) + throw err + } + } const excludedPrerenderRoutes = new Set() const pages = options.pages || Object.keys(pagesManifest)