-
Notifications
You must be signed in to change notification settings - Fork 2.2k
match next.js client side routing (buildManifest rewrites & devPagesManifest HMR) #3701
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
Changes from all commits
2b562cf
5dcd243
aa7fd5e
0177e72
3d9321b
e9294f2
302eda9
3116321
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 |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| ((manifest) => { | ||
| // adapted from https://github.com/vercel/next.js/blob/canary/packages/next/src/build/webpack/plugins/build-manifest-plugin.ts#L54-L54 | ||
| function processRoute(rewrite) { | ||
| // omit external rewrite destinations since these aren't | ||
| // handled client-side | ||
| if (!rewrite.destination.startsWith("/")) { | ||
| delete rewrite.destination; | ||
| } | ||
| return rewrite; | ||
| } | ||
|
|
||
| manifest.__rewrites.beforeFiles.map(processRoute); | ||
| manifest.__rewrites.afterFiles.map(processRoute); | ||
| manifest.__rewrites.fallback.map(processRoute); | ||
|
|
||
| self.__BUILD_MANIFEST = manifest; | ||
| self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB(); | ||
| })($$MANIFEST$$); |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,6 +1,11 @@ | ||||||
| use anyhow::Result; | ||||||
| use mime::APPLICATION_JSON; | ||||||
| use turbo_tasks::{primitives::StringsVc, TryFlatMapRecursiveJoinIterExt, TryJoinIterExt}; | ||||||
| use anyhow::{Context, Result}; | ||||||
| use indexmap::IndexMap; | ||||||
| use mime::{APPLICATION_JAVASCRIPT_UTF_8, APPLICATION_JSON}; | ||||||
| use serde::Serialize; | ||||||
| use turbo_tasks::{ | ||||||
| primitives::{StringVc, StringsVc}, | ||||||
| TryFlatMapRecursiveJoinIterExt, TryJoinIterExt, | ||||||
| }; | ||||||
| use turbo_tasks_fs::File; | ||||||
| use turbopack_core::asset::AssetContentVc; | ||||||
| use turbopack_dev_server::source::{ | ||||||
|
|
@@ -11,15 +16,23 @@ use turbopack_node::render::{ | |||||
| node_api_source::NodeApiContentSourceVc, rendered_source::NodeRenderContentSourceVc, | ||||||
| }; | ||||||
|
|
||||||
| use crate::{ | ||||||
| embed_js::next_js_file, | ||||||
| next_config::{NextConfigVc, RewritesReadRef}, | ||||||
| util::get_asset_path_from_route, | ||||||
| }; | ||||||
|
|
||||||
| /// A content source which creates the next.js `_devPagesManifest.json` and | ||||||
| /// `_devMiddlewareManifest.json` which are used for client side navigation. | ||||||
| #[turbo_tasks::value(shared)] | ||||||
| pub struct DevManifestContentSource { | ||||||
| pub page_roots: Vec<ContentSourceVc>, | ||||||
| pub next_config: NextConfigVc, | ||||||
| } | ||||||
|
|
||||||
| #[turbo_tasks::value_impl] | ||||||
| impl DevManifestContentSourceVc { | ||||||
| /// Recursively find all routes in the `page_roots` content sources. | ||||||
| #[turbo_tasks::function] | ||||||
| async fn find_routes(self) -> Result<StringsVc> { | ||||||
| let this = &*self.await?; | ||||||
|
|
@@ -61,10 +74,73 @@ impl DevManifestContentSourceVc { | |||||
| .flatten() | ||||||
| .collect::<Vec<_>>(); | ||||||
|
|
||||||
| routes.sort(); | ||||||
| routes.sort_by_cached_key(|s| s.split('/').map(PageSortKey::from).collect::<Vec<_>>()); | ||||||
|
|
||||||
| Ok(StringsVc::cell(routes)) | ||||||
| } | ||||||
|
|
||||||
| /// Recursively find all pages in the `page_roots` content sources | ||||||
| /// (excluding api routes). | ||||||
| #[turbo_tasks::function] | ||||||
| async fn find_pages(self) -> Result<StringsVc> { | ||||||
| let routes = &*self.find_routes().await?; | ||||||
|
|
||||||
| // we don't need to sort as it's already sorted by `find_routes` | ||||||
| let pages = routes | ||||||
| .iter() | ||||||
| .filter(|s| !s.starts_with("/api")) | ||||||
| .cloned() | ||||||
| .collect(); | ||||||
|
|
||||||
| Ok(StringsVc::cell(pages)) | ||||||
| } | ||||||
|
|
||||||
| /// Create a build manifest with all pages. | ||||||
| #[turbo_tasks::function] | ||||||
| async fn create_build_manifest(self) -> Result<StringVc> { | ||||||
| let this = &*self.await?; | ||||||
|
|
||||||
| let sorted_pages = &*self.find_pages().await?; | ||||||
| let routes = sorted_pages | ||||||
| .iter() | ||||||
| .map(|p| { | ||||||
| ( | ||||||
| p, | ||||||
| vec![format!( | ||||||
| "_next/static/chunks/pages/{}", | ||||||
| get_asset_path_from_route(p.split_at(1).1, ".js") | ||||||
| )], | ||||||
| ) | ||||||
| }) | ||||||
| .collect(); | ||||||
|
|
||||||
| let manifest = BuildManifest { | ||||||
| rewrites: this.next_config.rewrites().await?, | ||||||
| sorted_pages, | ||||||
| routes, | ||||||
| }; | ||||||
|
|
||||||
| let manifest = next_js_file("entry/manifest/buildManifest.js") | ||||||
| .await? | ||||||
| .as_content() | ||||||
| .context("embedded buildManifest file missing")? | ||||||
| .content() | ||||||
| .to_str()? | ||||||
| .replace("$$MANIFEST$$", &serde_json::to_string(&manifest)?); | ||||||
|
|
||||||
| Ok(StringVc::cell(manifest)) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| #[derive(Serialize)] | ||||||
| #[serde(rename_all = "camelCase")] | ||||||
| struct BuildManifest<'a> { | ||||||
| #[serde(rename = "__rewrites")] | ||||||
| rewrites: RewritesReadRef, | ||||||
| sorted_pages: &'a Vec<String>, | ||||||
|
|
||||||
| #[serde(flatten)] | ||||||
| routes: IndexMap<&'a String, Vec<String>>, | ||||||
| } | ||||||
|
|
||||||
| #[turbo_tasks::value_impl] | ||||||
|
|
@@ -75,25 +151,53 @@ impl ContentSource for DevManifestContentSource { | |||||
| path: &str, | ||||||
| _data: turbo_tasks::Value<ContentSourceData>, | ||||||
| ) -> Result<ContentSourceResultVc> { | ||||||
| let manifest_content = match path { | ||||||
| let manifest_file = match path { | ||||||
| "_next/static/development/_devPagesManifest.json" => { | ||||||
| let pages = &*self_vc.find_routes().await?; | ||||||
|
|
||||||
| serde_json::to_string(&serde_json::json!({ | ||||||
| File::from(serde_json::to_string(&serde_json::json!({ | ||||||
| "pages": pages, | ||||||
| }))? | ||||||
| }))?) | ||||||
| .with_content_type(APPLICATION_JSON) | ||||||
| } | ||||||
| "_next/static/development/_buildManifest.js" => { | ||||||
| let build_manifest = &*self_vc.create_build_manifest().await?; | ||||||
|
|
||||||
| File::from(build_manifest.as_str()).with_content_type(APPLICATION_JAVASCRIPT_UTF_8) | ||||||
| } | ||||||
| "_next/static/development/_devMiddlewareManifest.json" => { | ||||||
| // empty middleware manifest | ||||||
| "[]".to_string() | ||||||
| File::from("[]").with_content_type(APPLICATION_JSON) | ||||||
| } | ||||||
| _ => return Ok(ContentSourceResultVc::not_found()), | ||||||
| }; | ||||||
|
|
||||||
| let file = File::from(manifest_content).with_content_type(APPLICATION_JSON); | ||||||
|
|
||||||
| Ok(ContentSourceResultVc::exact( | ||||||
| ContentSourceContentVc::static_content(AssetContentVc::from(file).into()).into(), | ||||||
| ContentSourceContentVc::static_content(AssetContentVc::from(manifest_file).into()) | ||||||
| .into(), | ||||||
| )) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// PageSortKey is necessary because the next.js client code looks for matches | ||||||
| /// in the order the pages are sent in the manifest,if they're sorted | ||||||
| /// alphabetically this means \[slug] and \[\[catchall]] routes are prioritized | ||||||
| /// over fixed paths, so we have to override the ordering with this. | ||||||
| #[derive(Ord, PartialOrd, Eq, PartialEq)] | ||||||
| enum PageSortKey { | ||||||
| Static(String), | ||||||
| Slug, | ||||||
| CatchAll, | ||||||
| } | ||||||
|
|
||||||
| impl From<&str> for PageSortKey { | ||||||
|
Contributor
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 think we can bound the lifetime of
Suggested change
Contributor
Author
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 assuming the
Contributor
Author
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. yeah error: lifetime may not live long enough
--> crates\next-core\src\manifest.rs:77:39
|
77 | routes.sort_by_cached_key(|s| s.split('/').map(PageSortKey::from).collect::<Vec<_>>());
| -- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
| ||
| |return type of closure is Vec<PageSortKey<'2>>
| has type `&'1 std::string::String` |
||||||
| fn from(value: &str) -> Self { | ||||||
| if value.starts_with("[[") && value.ends_with("]]") { | ||||||
| PageSortKey::CatchAll | ||||||
| } else if value.starts_with('[') && value.ends_with(']') { | ||||||
| PageSortKey::Slug | ||||||
| } else { | ||||||
| PageSortKey::Static(value.to_string()) | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
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.
We should be creating this on the Next.js side so that we don't have to call
loadCustomRoutestwice x-ref: https://github.com/vercel/next.js/blob/7654d7bc2d95c8b3dc18cf5e94695375c733e74c/packages/next/src/server/lib/route-resolver.ts#L28There 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.
We load the whole config a second time to call the route resolver right now unfortunately
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.
@ijjk is there any chance we could get something implemented on the next.js side that would allow the whole config to be serialized to JSON (after loading custom routes probably)?