Skip to content

Commit

Permalink
Next 13 fixes (#5175)
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesdaniels committed Oct 31, 2022
1 parent 87e8f0c commit 2a675d8
Show file tree
Hide file tree
Showing 11 changed files with 261 additions and 164 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,8 @@
- Releases RTDB Emulator v4.11.0: Wire protocol update for `startAfter`, `endBefore`.
- Changes `superstatic` dependency to `v8`, addressing Hosting emulator issues on Windows.
- Fixes internal library that was not being correctly published.
- Add support for Next.js 13 in firebase deploy.
- Next.js routes with revalidate are now handled by the a backing Cloud Function.
- Adds `--disable-triggers` flag to RTDB write commands.
- Default enables experiment to skip deploying unmodified functions (#5192)
- Default enables experiment to allow parameterized functions codebases (#5192)
2 changes: 2 additions & 0 deletions scripts/webframeworks-deploy-tests/hosting/.gitignore
Expand Up @@ -34,3 +34,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

.vscode
5 changes: 5 additions & 0 deletions scripts/webframeworks-deploy-tests/hosting/app/bar/page.tsx
@@ -0,0 +1,5 @@
export const revalidate = 60;

export default function Bar() {
return <>Bar</>;
}
3 changes: 3 additions & 0 deletions scripts/webframeworks-deploy-tests/hosting/app/foo/page.tsx
@@ -0,0 +1,3 @@
export default function Foo() {
return <>Foo</>;
}
8 changes: 8 additions & 0 deletions scripts/webframeworks-deploy-tests/hosting/app/layout.tsx
@@ -0,0 +1,8 @@
export default function RootLayout({ children }: any) {
return (
<html>
<head></head>
<body>{children}</body>
</html>
)
}
3 changes: 3 additions & 0 deletions scripts/webframeworks-deploy-tests/hosting/next.config.js
Expand Up @@ -2,6 +2,9 @@
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
experimental: {
appDir: true
},
}

module.exports = nextConfig
277 changes: 146 additions & 131 deletions scripts/webframeworks-deploy-tests/hosting/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion scripts/webframeworks-deploy-tests/hosting/package.json
Expand Up @@ -9,7 +9,7 @@
"lint": "next lint"
},
"dependencies": {
"next": "12.3.1",
"next": "13.0.0",
"react": "18.2.0",
"react-dom": "18.2.0"
},
Expand Down
24 changes: 20 additions & 4 deletions scripts/webframeworks-deploy-tests/hosting/tsconfig.json
@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
Expand All @@ -13,8 +17,20 @@
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
"incremental": true,
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}
2 changes: 1 addition & 1 deletion scripts/webframeworks-deploy-tests/tests.ts
Expand Up @@ -45,7 +45,7 @@ describe("webframeworks deploy", function (this) {
const result = await setOptsAndDeploy();

expect(result.stdout, "deploy result").to.match(/file upload complete/);
expect(result.stdout, "deploy result").to.match(/found 16 files/);
expect(result.stdout, "deploy result").to.match(/found 20 files/);
expect(result.stdout, "deploy result").to.match(/Deploy complete!/);
});
});
97 changes: 70 additions & 27 deletions src/frameworks/next/index.ts
@@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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.
*/
Expand All @@ -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
Expand All @@ -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()));
Expand All @@ -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)
)
Expand Down Expand Up @@ -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" }
Expand All @@ -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);
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));
}
}
}
}
}
Expand Down

0 comments on commit 2a675d8

Please sign in to comment.