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
Next 13 fixes #5175
Next 13 fixes #5175
Changes from all commits
0e5a7e0
4db6cb6
578346a
1e8665d
7037376
de2a50b
b6a5ab9
6853f71
e0bdfc1
a4f5e41
8acb5a9
7bae042
f4be89c
c266bd6
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 |
---|---|---|
|
@@ -34,3 +34,5 @@ yarn-error.log* | |
# typescript | ||
*.tsbuildinfo | ||
next-env.d.ts | ||
|
||
.vscode |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export const revalidate = 60; | ||
|
||
export default function Bar() { | ||
return <>Bar</>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function Foo() { | ||
return <>Foo</>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export default function RootLayout({ children }: any) { | ||
return ( | ||
<html> | ||
<head></head> | ||
<body>{children}</body> | ||
</html> | ||
) | ||
} |
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,12 @@ | ||
import { execSync } from "child_process"; | ||
import { readFile, mkdir, copyFile, stat } from "fs/promises"; | ||
import { dirname, extname, join } from "path"; | ||
import { readFile, mkdir, copyFile } from "fs/promises"; | ||
import { dirname, join } from "path"; | ||
import type { Header, Rewrite, Redirect } from "next/dist/lib/load-custom-routes"; | ||
import type { NextConfig } from "next"; | ||
import { copy, mkdirp, pathExists } from "fs-extra"; | ||
import { pathToFileURL, parse } from "url"; | ||
import { existsSync } from "fs"; | ||
|
||
import { | ||
BuildResult, | ||
createServerResponseProxy, | ||
|
@@ -20,6 +21,7 @@ import { gte } from "semver"; | |
import { IncomingMessage, ServerResponse } from "http"; | ||
import { logger } from "../../logger"; | ||
import { FirebaseError } from "../../error"; | ||
import { fileExistsSync } from "../../fsutils"; | ||
|
||
// Next.js's exposed interface is incomplete here | ||
// TODO see if there's a better way to grab this | ||
|
@@ -47,10 +49,14 @@ export const name = "Next.js"; | |
export const support = SupportLevel.Experimental; | ||
export const type = FrameworkType.MetaFramework; | ||
|
||
function getNextVersion(cwd: string) { | ||
function getNextVersion(cwd: string): string | undefined { | ||
return findDependency("next", { cwd, depth: 0, omitDev: false })?.version; | ||
} | ||
|
||
function getReactVersion(cwd: string): string | undefined { | ||
return findDependency("react-dom", { cwd, omitDev: false })?.version; | ||
} | ||
|
||
/** | ||
* Returns whether this codebase is a Next.js backend. | ||
*/ | ||
|
@@ -67,6 +73,12 @@ export async function discover(dir: string) { | |
export async function build(dir: string): Promise<BuildResult> { | ||
const { default: nextBuild } = relativeRequire(dir, "next/dist/build"); | ||
|
||
const reactVersion = getReactVersion(dir); | ||
if (reactVersion && gte(reactVersion, "18.0.0")) { | ||
// This needs to be set for Next build to succeed with React 18 | ||
process.env.__NEXT_REACT_ROOT = "true"; | ||
} | ||
|
||
await nextBuild(dir, null, false, false, true).catch((e) => { | ||
// Err on the side of displaying this error, since this is likely a bug in | ||
// the developer's code that we want to display immediately | ||
|
@@ -89,6 +101,10 @@ export async function build(dir: string): Promise<BuildResult> { | |
const exportDetailBuffer = exportDetailExists ? await readFile(exportDetailPath) : undefined; | ||
const exportDetailJson = exportDetailBuffer && JSON.parse(exportDetailBuffer.toString()); | ||
if (exportDetailJson?.success) { | ||
const appPathRoutesManifestPath = join(dir, distDir, "app-path-routes-manifest.json"); | ||
const appPathRoutesManifestJSON = fileExistsSync(appPathRoutesManifestPath) | ||
? await readFile(appPathRoutesManifestPath).then((it) => JSON.parse(it.toString())) | ||
: {}; | ||
const prerenderManifestJSON = await readFile( | ||
join(dir, distDir, "prerender-manifest.json") | ||
).then((it) => JSON.parse(it.toString())); | ||
|
@@ -100,10 +116,15 @@ export async function build(dir: string): Promise<BuildResult> { | |
).then((it) => JSON.parse(it.toString())); | ||
const prerenderedRoutes = Object.keys(prerenderManifestJSON.routes); | ||
const dynamicRoutes = Object.keys(prerenderManifestJSON.dynamicRoutes); | ||
const unrenderedPages = Object.keys(pagesManifestJSON).filter( | ||
const unrenderedPages = [ | ||
...Object.keys(pagesManifestJSON), | ||
// TODO flush out fully rendered detection with a app directory (Next 13) | ||
// we shouldn't go too crazy here yet, as this is currently an expiriment | ||
...Object.values<string>(appPathRoutesManifestJSON), | ||
].filter( | ||
(it) => | ||
!( | ||
["/_app", "/_error", "/_document", "/404"].includes(it) || | ||
["/_app", "/", "/_error", "/_document", "/404"].includes(it) || | ||
prerenderedRoutes.includes(it) || | ||
dynamicRoutes.includes(it) | ||
) | ||
|
@@ -150,7 +171,7 @@ export async function init(setup: any) { | |
choices: ["JavaScript", "TypeScript"], | ||
}); | ||
execSync( | ||
`npx --yes create-next-app@latest -e hello-world ${setup.hosting.source} ${ | ||
`npx --yes create-next-app@latest -e hello-world ${setup.hosting.source} --use-npm ${ | ||
language === "TypeScript" ? "--ts" : "" | ||
}`, | ||
{ stdio: "inherit" } | ||
|
@@ -176,42 +197,64 @@ export async function ɵcodegenPublicDirectory(sourceDir: string, destDir: strin | |
} | ||
await copy(join(sourceDir, distDir, "static"), join(destDir, "_next", "static")); | ||
|
||
const serverPagesDir = join(sourceDir, distDir, "server", "pages"); | ||
await copy(serverPagesDir, destDir, { | ||
filter: async (filename) => { | ||
const status = await stat(filename); | ||
if (status.isDirectory()) return true; | ||
return extname(filename) === ".html"; | ||
}, | ||
}); | ||
// Copy over the default html files | ||
for (const file of ["index.html", "404.html", "500.html"]) { | ||
const pagesPath = join(sourceDir, distDir, "server", "pages", file); | ||
if (await pathExists(pagesPath)) { | ||
await copyFile(pagesPath, join(destDir, file)); | ||
continue; | ||
} | ||
const appPath = join(sourceDir, distDir, "server", "app", file); | ||
if (await pathExists(appPath)) { | ||
await copyFile(appPath, join(destDir, file)); | ||
} | ||
} | ||
|
||
const prerenderManifestBuffer = await readFile( | ||
join(sourceDir, distDir, "prerender-manifest.json") | ||
); | ||
const prerenderManifest = JSON.parse(prerenderManifestBuffer.toString()); | ||
// TODO drop from hosting if revalidate | ||
for (const route in prerenderManifest.routes) { | ||
if (prerenderManifest.routes[route]) { | ||
for (const path in prerenderManifest.routes) { | ||
if (prerenderManifest.routes[path]) { | ||
// Skip ISR in the deploy to hosting | ||
const { initialRevalidateSeconds } = prerenderManifest.routes[path]; | ||
if (initialRevalidateSeconds) { | ||
continue; | ||
} | ||
|
||
// TODO(jamesdaniels) explore oppertunity to simplify this now that we | ||
// are defaulting cleanURLs to true for frameworks | ||
|
||
// / => index.json => index.html => index.html | ||
// /foo => foo.json => foo.html | ||
const parts = route | ||
const parts = path | ||
.split("/") | ||
.slice(1) | ||
.filter((it) => !!it); | ||
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. 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. I was likely overly defensive here, worrying aboth leading/trailing slash, I'll check my assumptions. that said, we're pushing cleanURLs to be true by default now when paired with hosting.source, I can probably simplify this code. I'll do that in a follow on so I unblock Next 13 from working 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. Took on as a TODO to explore simplifying |
||
const partsOrIndex = parts.length > 0 ? parts : ["index"]; | ||
const dataPath = `${join(...partsOrIndex)}.json`; | ||
const htmlPath = `${join(...partsOrIndex)}.html`; | ||
await mkdir(join(destDir, dirname(htmlPath)), { recursive: true }); | ||
await copyFile( | ||
join(sourceDir, distDir, "server", "pages", htmlPath), | ||
join(destDir, htmlPath) | ||
); | ||
const dataRoute = prerenderManifest.routes[route].dataRoute; | ||
const pagesHtmlPath = join(sourceDir, distDir, "server", "pages", htmlPath); | ||
if (await pathExists(pagesHtmlPath)) { | ||
await copyFile(pagesHtmlPath, join(destDir, htmlPath)); | ||
} else { | ||
const appHtmlPath = join(sourceDir, distDir, "server", "app", htmlPath); | ||
if (await pathExists(appHtmlPath)) { | ||
await copyFile(appHtmlPath, join(destDir, htmlPath)); | ||
} | ||
} | ||
const dataRoute = prerenderManifest.routes[path].dataRoute; | ||
await mkdir(join(destDir, dirname(dataRoute)), { recursive: true }); | ||
await copyFile( | ||
join(sourceDir, distDir, "server", "pages", dataPath), | ||
join(destDir, dataRoute) | ||
); | ||
const pagesDataPath = join(sourceDir, distDir, "server", "pages", dataPath); | ||
if (await pathExists(pagesDataPath)) { | ||
await copyFile(pagesDataPath, join(destDir, dataRoute)); | ||
} else { | ||
const appDataPath = join(sourceDir, distDir, "server", "app", dataPath); | ||
if (await pathExists(appDataPath)) { | ||
await copyFile(appDataPath, join(destDir, dataRoute)); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
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.
Should this not be
of
?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.
it's not an iterable, it's an object:
{ "/foo": { ... }, "/bar": { ... }}
so in is correct, though with the linting rule (to ensure we don't iterate over private) it's a little verbose... perhaps
const path of Object.keys(prerenderManifest)
orconst [path, route] of Object.entries(prerenderManifest.routes)
would be stylistically preferable?