Skip to content

Commit

Permalink
feat: add output: export support for appDir (#47022)
Browse files Browse the repository at this point in the history
We can now support `next export` for `appDir` because of the new config added in #46744.
fixes NEXT-775 ([NEXT-775](https://linear.app/vercel/issue/NEXT-775))
  • Loading branch information
styfle committed Mar 14, 2023
1 parent c27b546 commit b590ec3
Show file tree
Hide file tree
Showing 18 changed files with 396 additions and 8 deletions.
2 changes: 2 additions & 0 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1282,6 +1282,7 @@ export default async function build(
enableUndici: config.experimental.enableUndici,
locales: config.i18n?.locales,
defaultLocale: config.i18n?.defaultLocale,
nextConfigOutput: config.output,
})
)

Expand Down Expand Up @@ -1466,6 +1467,7 @@ export default async function build(
isrFlushToDisk: config.experimental.isrFlushToDisk,
maxMemoryCacheSize:
config.experimental.isrMemoryCacheSize,
nextConfigOutput: config.output,
})
}
)
Expand Down
12 changes: 12 additions & 0 deletions packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,7 @@ export async function isPageStatic({
isrFlushToDisk,
maxMemoryCacheSize,
incrementalCacheHandlerPath,
nextConfigOutput,
}: {
page: string
distDir: string
Expand All @@ -1347,6 +1348,7 @@ export async function isPageStatic({
isrFlushToDisk?: boolean
maxMemoryCacheSize?: number
incrementalCacheHandlerPath?: string
nextConfigOutput: 'standalone' | 'export'
}): Promise<{
isStatic?: boolean
isAmpOnly?: boolean
Expand Down Expand Up @@ -1480,6 +1482,16 @@ export async function isPageStatic({
{}
)

if (nextConfigOutput === 'export') {
if (!appConfig.dynamic || appConfig.dynamic === 'auto') {
appConfig.dynamic = 'error'
} else if (appConfig.dynamic === 'force-dynamic') {
throw new Error(
`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`
)
}
}

if (appConfig.dynamic === 'force-dynamic') {
appConfig.revalidate = 0
}
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,16 @@ export async function fetchServerResponse(
}

try {
const res = await fetch(url.toString(), {
let fetchUrl = url
if (process.env.__NEXT_CONFIG_OUTPUT === 'export') {
fetchUrl = new URL(url) // clone
if (fetchUrl.pathname.endsWith('/')) {
fetchUrl.pathname += 'index.txt'
} else {
fetchUrl.pathname += '.txt'
}
}
const res = await fetch(fetchUrl, {
// Backwards compat for older browsers. `same-origin` is the default in modern browsers.
credentials: 'same-origin',
headers,
Expand All @@ -44,8 +53,14 @@ export async function fetchServerResponse(
? urlToUrlWithoutFlightMarker(res.url)
: undefined

const isFlightResponse =
res.headers.get('content-type') === RSC_CONTENT_TYPE_HEADER
const contentType = res.headers.get('content-type') || ''
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) {
Expand Down
67 changes: 63 additions & 4 deletions packages/next/src/export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -51,6 +52,9 @@ import {
overrideBuiltInReactPackages,
} 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'
import isError from '../lib/is-error'

loadRequireHook()
if (process.env.NEXT_PREBUNDLED_REACT) {
Expand Down Expand Up @@ -238,6 +242,23 @@ export default async function exportApp(
prerenderManifest = require(join(distDir, PRERENDER_MANIFEST))
} catch (_) {}

let appRoutePathManifest: Record<string, string> | undefined = undefined
try {
appRoutePathManifest = require(join(distDir, APP_PATH_ROUTES_MANIFEST))
} 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<string>()
const pages = options.pages || Object.keys(pagesManifest)
const defaultPathMap: ExportPathMap = {}
Expand Down Expand Up @@ -269,6 +290,25 @@ export default async function exportApp(
defaultPathMap[page] = { page }
}

const mapAppRouteToPage = new Map<string, string>()
if (!options.buildExport && appRoutePathManifest) {
for (const [pageName, routePath] of Object.entries(
appRoutePathManifest
)) {
mapAppRouteToPage.set(routePath, pageName)
if (
isAppPageRoute(pageName) &&
!prerenderManifest?.routes[routePath] &&
!prerenderManifest?.dynamicRoutes[routePath]
) {
defaultPathMap[routePath] = {
page: pageName,
_isAppDir: true,
}
}
}
}

// Initialize the output directory
const outDir = options.outdir

Expand Down Expand Up @@ -711,7 +751,10 @@ export default async function exportApp(
await Promise.all(
Object.keys(prerenderManifest.routes).map(async (route) => {
const { srcRoute } = prerenderManifest!.routes[route]
const pageName = srcRoute || route
const appPageName = mapAppRouteToPage.get(srcRoute || '')
const pageName = appPageName || srcRoute || route
const isAppPath = Boolean(appPageName)
const isAppRouteHandler = appPageName && isAppRouteRoute(appPageName)

// returning notFound: true from getStaticProps will not
// output html/json files during the build
Expand All @@ -720,7 +763,7 @@ export default async function exportApp(
}
route = normalizePagePath(route)

const pagePath = getPagePath(pageName, distDir, undefined, false)
const pagePath = getPagePath(pageName, distDir, undefined, isAppPath)
const distPagesDir = join(
pagePath,
// strip leading / and then recurse number of nested dirs
Expand All @@ -733,6 +776,15 @@ export default async function exportApp(
)

const orig = join(distPagesDir, route)
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
}

const htmlDest = join(
outDir,
`${route}${
Expand All @@ -743,13 +795,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` : ''
}.txt`
)
: 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)
Expand Down
6 changes: 5 additions & 1 deletion packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,11 @@ export interface ExperimentalConfig {
}

export type ExportPathMap = {
[path: string]: { page: string; query?: Record<string, string | string[]> }
[path: string]: {
page: string
query?: Record<string, string | string[]>
_isAppDir?: boolean
}
}

/**
Expand Down
20 changes: 20 additions & 0 deletions test/integration/app-dir-export/app/another/[slug]/page.js
Original file line number Diff line number Diff line change
@@ -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 (
<main>
<h1>{params.slug}</h1>
<ul>
<li>
<Link href="/another">Visit another page</Link>
</li>
</ul>
</main>
)
}
26 changes: 26 additions & 0 deletions test/integration/app-dir-export/app/another/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Link from 'next/link'

export default function Another() {
return (
<main>
<h1>Another</h1>
<ul>
<li>
<Link href="/">Visit the home page</Link>
</li>
<li>
<Link href="/another">another page</Link>
</li>
<li>
<Link href="/another/first">another first page</Link>
</li>
<li>
<Link href="/another/second">another second page</Link>
</li>
<li>
<Link href="/image-import">image import page</Link>
</li>
</ul>
</main>
)
}
3 changes: 3 additions & 0 deletions test/integration/app-dir-export/app/api/json/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function GET() {
return Response.json({ answer: 42 })
}
3 changes: 3 additions & 0 deletions test/integration/app-dir-export/app/api/txt/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function GET() {
return new Response('this is plain text')
}
Binary file added test/integration/app-dir-export/app/favicon.ico
Binary file not shown.
18 changes: 18 additions & 0 deletions test/integration/app-dir-export/app/image-import/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Link from 'next/link'
import img from './test.png'

export default function ImageImport() {
return (
<main>
<h1>Image Import</h1>
<ul>
<li>
<Link href="/">Visit the home page</Link>
</li>
<li>
<a href={img.src}>View the image</a>
</li>
</ul>
</main>
)
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions test/integration/app-dir-export/app/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
26 changes: 26 additions & 0 deletions test/integration/app-dir-export/app/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Link from 'next/link'

export default function Home() {
return (
<main>
<h1>Home</h1>
<ul>
<li>
<Link href="/another">another no trailingslash</Link>
</li>
<li>
<Link href="/another/">another has trailingslash</Link>
</li>
<li>
<Link href="/another/first">another first page</Link>
</li>
<li>
<Link href="/another/second">another second page</Link>
</li>
<li>
<Link href="/image-import">image import page</Link>
</li>
</ul>
</main>
)
}
2 changes: 2 additions & 0 deletions test/integration/app-dir-export/app/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
User-agent: *
Allow: /
13 changes: 13 additions & 0 deletions test/integration/app-dir-export/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
trailingSlash: true,
experimental: {
appDir: true,
},
generateBuildId() {
return 'test-build-id'
},
}

module.exports = nextConfig

0 comments on commit b590ec3

Please sign in to comment.