fix: app dir with next dev and output: export (#47171)

This PR is a follow up to PR #47022 which broke `next dev`.

A test has been added to confirm `next dev` works as expected.

fix NEXT-825 ([link](https://linear.app/vercel/issue/NEXT-825)) ([NEXT-825](https://linear.app/vercel/issue/NEXT-825))
This commit is contained in:
Steven 2023-03-15 20:36:08 -04:00 committed by GitHub
parent 0e91549397
commit 9a89c4933d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 43 additions and 22 deletions

View file

@ -39,12 +39,14 @@ export async function fetchServerResponse(
try { try {
let fetchUrl = url let fetchUrl = url
if (process.env.__NEXT_CONFIG_OUTPUT === 'export') { if (process.env.NODE_ENV === 'production') {
fetchUrl = new URL(url) // clone if (process.env.__NEXT_CONFIG_OUTPUT === 'export') {
if (fetchUrl.pathname.endsWith('/')) { fetchUrl = new URL(url) // clone
fetchUrl.pathname += 'index.txt' if (fetchUrl.pathname.endsWith('/')) {
} else { fetchUrl.pathname += 'index.txt'
fetchUrl.pathname += '.txt' } else {
fetchUrl.pathname += '.txt'
}
} }
} }
const res = await fetch(fetchUrl, { const res = await fetch(fetchUrl, {
@ -59,9 +61,11 @@ export async function fetchServerResponse(
const contentType = res.headers.get('content-type') || '' const contentType = res.headers.get('content-type') || ''
let isFlightResponse = contentType === RSC_CONTENT_TYPE_HEADER let isFlightResponse = contentType === RSC_CONTENT_TYPE_HEADER
if (process.env.__NEXT_CONFIG_OUTPUT === 'export') { if (process.env.NODE_ENV === 'production') {
if (!isFlightResponse) { if (process.env.__NEXT_CONFIG_OUTPUT === 'export') {
isFlightResponse = contentType.startsWith('text/plain') if (!isFlightResponse) {
isFlightResponse = contentType.startsWith('text/plain')
}
} }
} }

View file

@ -535,6 +535,7 @@ export default class DevServer extends Server {
}) })
if ( if (
!isAppPath &&
pageName.startsWith('/api/') && pageName.startsWith('/api/') &&
this.nextConfig.output === 'export' this.nextConfig.output === 'export'
) { ) {
@ -1535,6 +1536,7 @@ export default class DevServer extends Server {
staticPaths?: string[] staticPaths?: string[]
fallbackMode?: false | 'static' | 'blocking' fallbackMode?: false | 'static' | 'blocking'
}> { }> {
const isAppPath = Boolean(originalAppPath)
// we lazy load the staticPaths to prevent the user // we lazy load the staticPaths to prevent the user
// from waiting on them for the page to load in dev mode // from waiting on them for the page to load in dev mode
@ -1561,7 +1563,7 @@ export default class DevServer extends Server {
locales, locales,
defaultLocale, defaultLocale,
originalAppPath, originalAppPath,
isAppPath: !!originalAppPath, isAppPath,
requestHeaders, requestHeaders,
incrementalCacheHandlerPath: incrementalCacheHandlerPath:
this.nextConfig.experimental.incrementalCacheHandlerPath, this.nextConfig.experimental.incrementalCacheHandlerPath,
@ -1579,7 +1581,7 @@ export default class DevServer extends Server {
) )
.then((res) => { .then((res) => {
const { paths: staticPaths = [], fallback } = res.value const { paths: staticPaths = [], fallback } = res.value
if (this.nextConfig.output === 'export') { if (!isAppPath && this.nextConfig.output === 'export') {
if (fallback === 'blocking') { if (fallback === 'blocking') {
throw new Error( throw new Error(
'getStaticPaths with "fallback: blocking" cannot be used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export' 'getStaticPaths with "fallback: blocking" cannot be used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export'

View file

@ -7,6 +7,9 @@ import webdriver from 'next-webdriver'
import globOrig from 'glob' import globOrig from 'glob'
import { import {
File, File,
findPort,
killApp,
launchApp,
nextBuild, nextBuild,
nextExport, nextExport,
startStaticServer, startStaticServer,
@ -20,12 +23,13 @@ const distDir = join(__dirname, '.next')
const exportDir = join(appDir, 'out') const exportDir = join(appDir, 'out')
const nextConfig = new File(join(appDir, 'next.config.js')) const nextConfig = new File(join(appDir, 'next.config.js'))
const slugPage = new File(join(appDir, 'app/another/[slug]/page.js')) const slugPage = new File(join(appDir, 'app/another/[slug]/page.js'))
const delay = 100
async function runTests({ async function runTests({
isDev,
trailingSlash, trailingSlash,
dynamic, dynamic,
}: { }: {
isDev?: boolean
trailingSlash?: boolean trailingSlash?: boolean
dynamic?: string dynamic?: string
}) { }) {
@ -43,12 +47,18 @@ async function runTests({
} }
await fs.remove(distDir) await fs.remove(distDir)
await fs.remove(exportDir) await fs.remove(exportDir)
await nextBuild(appDir) const delay = isDev ? 500 : 100
await nextExport(appDir, { outdir: exportDir }) const appPort = await findPort()
const app = await startStaticServer(exportDir) let stopOrKill: () => Promise<void>
const address = app.address() if (isDev) {
const appPort = typeof address !== 'string' ? address.port : 3000 const app = await launchApp(appDir, appPort)
stopOrKill = async () => await killApp(app)
} else {
await nextBuild(appDir)
await nextExport(appDir, { outdir: exportDir })
const app = await startStaticServer(exportDir, null, appPort)
stopOrKill = async () => await stopApp(app)
}
try { try {
const a = (n: number) => `li:nth-child(${n}) a` const a = (n: number) => `li:nth-child(${n}) a`
console.log('[navigate]') console.log('[navigate]')
@ -108,15 +118,20 @@ async function runTests({
'/test.3f1a293b.png' '/test.3f1a293b.png'
) )
} finally { } finally {
await stopApp(app) await stopOrKill()
nextConfig.restore() nextConfig.restore()
slugPage.restore() slugPage.restore()
} }
} }
describe('app dir with next export', () => { describe('app dir with output export', () => {
it.each([{ trailingSlash: false }, { trailingSlash: true }])( it.each([
"should work with trailingSlash '$trailingSlash'", { isDev: true, trailingSlash: false },
{ isDev: true, trailingSlash: true },
{ isDev: false, trailingSlash: false },
{ isDev: false, trailingSlash: true },
])(
"should work with isDev '$isDev' and trailingSlash '$trailingSlash'",
async ({ trailingSlash }) => { async ({ trailingSlash }) => {
await runTests({ trailingSlash }) await runTests({ trailingSlash })
} }