-
Notifications
You must be signed in to change notification settings - Fork 27.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add output: export
support for appDir
#47022
Changes from all commits
967d669
fd30e7f
af8963d
aac1058
6785b0f
6c2130e
a238770
b781f70
87858c7
84f43ec
cfbd2e1
6ccf8c5
7ffffe1
e64914b
0d0b254
4b646bf
41aaf27
d0ac0e8
74ca9ff
bcba14e
316a297
4915943
2059734
d699b99
71bdbb9
ab2018d
5933150
c730401
bcdac1a
f9d4435
2deda3f
d42c179
f8d072b
c4cf8b2
04a916d
4ca9e32
696dc17
bf05c8a
fa94a43
c14bc93
1aa1d58
57cd2b9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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' | ||
|
@@ -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) { | ||
|
@@ -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 = {} | ||
|
@@ -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 | ||
|
||
|
@@ -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 | ||
|
@@ -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 | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We might need to error here if custom headers are set in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, I created NEXT-823 to track this 👍 |
||
return | ||
} | ||
|
||
const htmlDest = join( | ||
outDir, | ||
`${route}${ | ||
|
@@ -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` | ||
huozhi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
: 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) | ||
|
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> | ||
) | ||
} |
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> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export async function GET() { | ||
return Response.json({ answer: 42 }) | ||
} |
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') | ||
} |
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> | ||
) | ||
} |
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> | ||
) | ||
} |
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> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
User-agent: * | ||
Allow: / |
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe use
fs.exists
here instead so that if there's an error in the manifest it doesn't accidentally fall through.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@timneutkens Should I also make that change to the prerender manifest on line 241 above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated in 57cd2b9