Skip to content

Commit 942e33d

Browse files
match next.js client side routing (buildManifest rewrites & devPagesManifest HMR) (vercel/turborepo#3701)
1 parent 47eff0d commit 942e33d

File tree

9 files changed

+293
-33
lines changed

9 files changed

+293
-33
lines changed

crates/next-core/js/src/entry/config/next.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import loadConfig from "next/dist/server/config";
2+
import loadCustomRoutes from "next/dist/lib/load-custom-routes";
23
import { PHASE_DEVELOPMENT_SERVER } from "next/dist/shared/lib/constants";
34
import assert from "assert";
45

@@ -13,6 +14,12 @@ const loadNextConfig = async (silent) => {
1314

1415
nextConfig.generateBuildId = await nextConfig.generateBuildId?.();
1516

17+
const customRoutes = await loadCustomRoutes(nextConfig);
18+
19+
nextConfig.headers = customRoutes.headers;
20+
nextConfig.rewrites = customRoutes.rewrites;
21+
nextConfig.redirects = customRoutes.redirects;
22+
1623
// TODO: these functions takes arguments, have to be supported in a different way
1724
nextConfig.exportPathMap = nextConfig.exportPathMap && {};
1825
nextConfig.webpack = nextConfig.webpack && {};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
((manifest) => {
2+
// adapted from https://github.com/vercel/next.js/blob/canary/packages/next/src/build/webpack/plugins/build-manifest-plugin.ts#L54-L54
3+
function processRoute(rewrite) {
4+
// omit external rewrite destinations since these aren't
5+
// handled client-side
6+
if (!rewrite.destination.startsWith("/")) {
7+
delete rewrite.destination;
8+
}
9+
return rewrite;
10+
}
11+
12+
manifest.__rewrites.beforeFiles.map(processRoute);
13+
manifest.__rewrites.afterFiles.map(processRoute);
14+
manifest.__rewrites.fallback.map(processRoute);
15+
16+
self.__BUILD_MANIFEST = manifest;
17+
self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB();
18+
})($$MANIFEST$$);

crates/next-core/js/src/entry/next-hydrate.tsx

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,6 @@ async function loadPageChunk(assetPrefix: string, chunkPath: string) {
5858
};
5959

6060
const pagePath = window.__NEXT_DATA__.page;
61-
window.__BUILD_MANIFEST = {
62-
[pagePath]: [],
63-
__rewrites: {
64-
beforeFiles: [],
65-
afterFiles: [],
66-
fallback: [],
67-
} as any,
68-
sortedPages: [pagePath, "/_app"],
69-
};
70-
7161
window.__NEXT_P.push(["/_app", () => _app]);
7262
window.__NEXT_P.push([pagePath, () => page]);
7363

@@ -79,10 +69,34 @@ async function loadPageChunk(assetPrefix: string, chunkPath: string) {
7969
// during hydration. To make this dependency clearer, we pass `router` as an
8070
// explicit argument instead of relying on the `router` import binding.
8171
subscribeToCurrentPageData({ assetPrefix, router });
72+
subscribeToPageManifest({ assetPrefix });
8273

8374
console.debug("The page has been hydrated");
8475
})().catch((err) => console.error(err));
8576

77+
function subscribeToPageManifest({ assetPrefix }: { assetPrefix: string }) {
78+
// adapted from https://github.com/vercel/next.js/blob/836ac9cc7f290e95b564a61341fa95a5f4f0327e/packages/next/src/client/next-dev.ts#L57
79+
subscribeToUpdate(
80+
{
81+
path: "_next/static/development/_devPagesManifest.json",
82+
},
83+
(update) => {
84+
if (["restart", "partial"].includes(update.type)) {
85+
return;
86+
}
87+
88+
fetch(`${assetPrefix}/_next/static/development/_devPagesManifest.json`)
89+
.then((res) => res.json())
90+
.then((manifest) => {
91+
window.__DEV_PAGES_MANIFEST = manifest;
92+
})
93+
.catch((err) => {
94+
console.log(`Failed to fetch devPagesManifest`, err);
95+
});
96+
}
97+
);
98+
}
99+
86100
/**
87101
* Subscribes to the current page's data updates from the HMR server.
88102
*

crates/next-core/js/src/entry/router.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import type { Ipc } from "@vercel/turbopack-next/ipc/index";
22
import type { IncomingMessage, ServerResponse } from "node:http";
33
import { Buffer } from "node:buffer";
44
import { createServer, makeRequest } from "@vercel/turbopack-next/ipc/server";
5-
import loadNextConfig from "@vercel/turbopack-next/entry/config/next";
65
import { makeResolver } from "next/dist/server/router.js";
6+
import loadConfig from "next/dist/server/config";
7+
import { PHASE_DEVELOPMENT_SERVER } from "next/dist/shared/lib/constants";
78

89
import "next/dist/server/node-polyfill-fetch.js";
910

@@ -48,10 +49,22 @@ type MiddlewareHeadersResponse = {
4849
};
4950

5051
let resolveRouteMemo: Promise<
51-
(req: IncomingMessage, res: ServerResponse) => Promise<unknown>
52+
(req: IncomingMessage, res: ServerResponse) => Promise<void>
5253
>;
53-
async function getResolveRoute(dir: string) {
54-
const nextConfig = await loadNextConfig(true);
54+
55+
async function getResolveRoute(
56+
dir: string
57+
): ReturnType<
58+
typeof import("next/dist/server/lib/route-resolver").makeResolver
59+
> {
60+
const nextConfig = await loadConfig(
61+
PHASE_DEVELOPMENT_SERVER,
62+
process.cwd(),
63+
undefined,
64+
undefined,
65+
true
66+
);
67+
5568
return await makeResolver(dir, nextConfig);
5669
}
5770

@@ -113,7 +126,7 @@ export default async function route(
113126
async function handleClientResponse(
114127
_ipc: Ipc<RouterRequest, IpcOutgoingMessage>,
115128
clientResponse: IncomingMessage
116-
): Promise<MessageData | void> {
129+
): Promise<MessageData> {
117130
if (clientResponse.headers["x-nextjs-route-result"] === "1") {
118131
clientResponse.setEncoding("utf8");
119132
// We're either a redirect or a rewrite

crates/next-core/js/src/internal/page-server-handler.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export default function startHandler({
119119
devFiles: [],
120120
ampDevFiles: [],
121121
polyfillFiles: [],
122-
lowPriorityFiles: [],
122+
lowPriorityFiles: ["static/development/_buildManifest.js"],
123123
rootMainFiles: [],
124124
ampFirstPages: [],
125125
};

crates/next-core/src/manifest.rs

Lines changed: 115 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
use anyhow::Result;
2-
use mime::APPLICATION_JSON;
3-
use turbo_tasks::{primitives::StringsVc, TryFlatMapRecursiveJoinIterExt, TryJoinIterExt};
1+
use anyhow::{Context, Result};
2+
use indexmap::IndexMap;
3+
use mime::{APPLICATION_JAVASCRIPT_UTF_8, APPLICATION_JSON};
4+
use serde::Serialize;
5+
use turbo_tasks::{
6+
primitives::{StringVc, StringsVc},
7+
TryFlatMapRecursiveJoinIterExt, TryJoinIterExt,
8+
};
49
use turbo_tasks_fs::File;
510
use turbopack_core::asset::AssetContentVc;
611
use turbopack_dev_server::source::{
@@ -11,15 +16,23 @@ use turbopack_node::render::{
1116
node_api_source::NodeApiContentSourceVc, rendered_source::NodeRenderContentSourceVc,
1217
};
1318

19+
use crate::{
20+
embed_js::next_js_file,
21+
next_config::{NextConfigVc, RewritesReadRef},
22+
util::get_asset_path_from_route,
23+
};
24+
1425
/// A content source which creates the next.js `_devPagesManifest.json` and
1526
/// `_devMiddlewareManifest.json` which are used for client side navigation.
1627
#[turbo_tasks::value(shared)]
1728
pub struct DevManifestContentSource {
1829
pub page_roots: Vec<ContentSourceVc>,
30+
pub next_config: NextConfigVc,
1931
}
2032

2133
#[turbo_tasks::value_impl]
2234
impl DevManifestContentSourceVc {
35+
/// Recursively find all routes in the `page_roots` content sources.
2336
#[turbo_tasks::function]
2437
async fn find_routes(self) -> Result<StringsVc> {
2538
let this = &*self.await?;
@@ -61,10 +74,73 @@ impl DevManifestContentSourceVc {
6174
.flatten()
6275
.collect::<Vec<_>>();
6376

64-
routes.sort();
77+
routes.sort_by_cached_key(|s| s.split('/').map(PageSortKey::from).collect::<Vec<_>>());
6578

6679
Ok(StringsVc::cell(routes))
6780
}
81+
82+
/// Recursively find all pages in the `page_roots` content sources
83+
/// (excluding api routes).
84+
#[turbo_tasks::function]
85+
async fn find_pages(self) -> Result<StringsVc> {
86+
let routes = &*self.find_routes().await?;
87+
88+
// we don't need to sort as it's already sorted by `find_routes`
89+
let pages = routes
90+
.iter()
91+
.filter(|s| !s.starts_with("/api"))
92+
.cloned()
93+
.collect();
94+
95+
Ok(StringsVc::cell(pages))
96+
}
97+
98+
/// Create a build manifest with all pages.
99+
#[turbo_tasks::function]
100+
async fn create_build_manifest(self) -> Result<StringVc> {
101+
let this = &*self.await?;
102+
103+
let sorted_pages = &*self.find_pages().await?;
104+
let routes = sorted_pages
105+
.iter()
106+
.map(|p| {
107+
(
108+
p,
109+
vec![format!(
110+
"_next/static/chunks/pages/{}",
111+
get_asset_path_from_route(p.split_at(1).1, ".js")
112+
)],
113+
)
114+
})
115+
.collect();
116+
117+
let manifest = BuildManifest {
118+
rewrites: this.next_config.rewrites().await?,
119+
sorted_pages,
120+
routes,
121+
};
122+
123+
let manifest = next_js_file("entry/manifest/buildManifest.js")
124+
.await?
125+
.as_content()
126+
.context("embedded buildManifest file missing")?
127+
.content()
128+
.to_str()?
129+
.replace("$$MANIFEST$$", &serde_json::to_string(&manifest)?);
130+
131+
Ok(StringVc::cell(manifest))
132+
}
133+
}
134+
135+
#[derive(Serialize)]
136+
#[serde(rename_all = "camelCase")]
137+
struct BuildManifest<'a> {
138+
#[serde(rename = "__rewrites")]
139+
rewrites: RewritesReadRef,
140+
sorted_pages: &'a Vec<String>,
141+
142+
#[serde(flatten)]
143+
routes: IndexMap<&'a String, Vec<String>>,
68144
}
69145

70146
#[turbo_tasks::value_impl]
@@ -75,25 +151,53 @@ impl ContentSource for DevManifestContentSource {
75151
path: &str,
76152
_data: turbo_tasks::Value<ContentSourceData>,
77153
) -> Result<ContentSourceResultVc> {
78-
let manifest_content = match path {
154+
let manifest_file = match path {
79155
"_next/static/development/_devPagesManifest.json" => {
80156
let pages = &*self_vc.find_routes().await?;
81157

82-
serde_json::to_string(&serde_json::json!({
158+
File::from(serde_json::to_string(&serde_json::json!({
83159
"pages": pages,
84-
}))?
160+
}))?)
161+
.with_content_type(APPLICATION_JSON)
162+
}
163+
"_next/static/development/_buildManifest.js" => {
164+
let build_manifest = &*self_vc.create_build_manifest().await?;
165+
166+
File::from(build_manifest.as_str()).with_content_type(APPLICATION_JAVASCRIPT_UTF_8)
85167
}
86168
"_next/static/development/_devMiddlewareManifest.json" => {
87169
// empty middleware manifest
88-
"[]".to_string()
170+
File::from("[]").with_content_type(APPLICATION_JSON)
89171
}
90172
_ => return Ok(ContentSourceResultVc::not_found()),
91173
};
92174

93-
let file = File::from(manifest_content).with_content_type(APPLICATION_JSON);
94-
95175
Ok(ContentSourceResultVc::exact(
96-
ContentSourceContentVc::static_content(AssetContentVc::from(file).into()).into(),
176+
ContentSourceContentVc::static_content(AssetContentVc::from(manifest_file).into())
177+
.into(),
97178
))
98179
}
99180
}
181+
182+
/// PageSortKey is necessary because the next.js client code looks for matches
183+
/// in the order the pages are sent in the manifest,if they're sorted
184+
/// alphabetically this means \[slug] and \[\[catchall]] routes are prioritized
185+
/// over fixed paths, so we have to override the ordering with this.
186+
#[derive(Ord, PartialOrd, Eq, PartialEq)]
187+
enum PageSortKey {
188+
Static(String),
189+
Slug,
190+
CatchAll,
191+
}
192+
193+
impl From<&str> for PageSortKey {
194+
fn from(value: &str) -> Self {
195+
if value.starts_with("[[") && value.ends_with("]]") {
196+
PageSortKey::CatchAll
197+
} else if value.starts_with('[') && value.ends_with(']') {
198+
PageSortKey::Slug
199+
} else {
200+
PageSortKey::Static(value.to_string())
201+
}
202+
}
203+
}

0 commit comments

Comments
 (0)