diff --git a/Cargo.lock b/Cargo.lock index 6240c95e8c020..53e9e2718f1f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2908,6 +2908,7 @@ version = "0.1.0" dependencies = [ "anyhow", "indexmap", + "mime", "rand", "serde", "serde_json", diff --git a/crates/next-core/Cargo.toml b/crates/next-core/Cargo.toml index 465b09129030a..327af9d4b2340 100644 --- a/crates/next-core/Cargo.toml +++ b/crates/next-core/Cargo.toml @@ -11,6 +11,7 @@ bench = false [dependencies] anyhow = "1.0.47" indexmap = { workspace = true, features = ["serde"] } +mime = "0.3.16" rand = "0.8.5" serde = "1.0.136" serde_json = "1.0.85" diff --git a/crates/next-core/js/src/dev/hmr-client.ts b/crates/next-core/js/src/dev/hmr-client.ts index 74c00cdb35377..4b9a5aa539f65 100644 --- a/crates/next-core/js/src/dev/hmr-client.ts +++ b/crates/next-core/js/src/dev/hmr-client.ts @@ -300,7 +300,7 @@ function triggerUpdate(msg: ServerMessage) { // They must be reloaded here instead. function subscribeToInitialCssChunksUpdates(assetPrefix: string) { const initialCssChunkLinks: NodeListOf = - document.head.querySelectorAll("link"); + document.head.querySelectorAll(`link[rel="stylesheet"]`); const cssChunkPrefix = `${assetPrefix}/`; initialCssChunkLinks.forEach((link) => { const href = link.href; diff --git a/crates/next-core/js/src/entry/next-hydrate.tsx b/crates/next-core/js/src/entry/next-hydrate.tsx index c5f579540f74c..d22b4ad4f1a86 100644 --- a/crates/next-core/js/src/entry/next-hydrate.tsx +++ b/crates/next-core/js/src/entry/next-hydrate.tsx @@ -20,8 +20,19 @@ import * as page from "."; assetPrefix, }); + const pagePath = window.__NEXT_DATA__.page; + window.__BUILD_MANIFEST = { + [pagePath]: [], + __rewrites: { + beforeFiles: [], + afterFiles: [], + fallback: [], + } as any, + sortedPages: [pagePath, "/_app"], + }; + window.__NEXT_P.push(["/_app", () => _app]); - window.__NEXT_P.push([window.__NEXT_DATA__.page, () => page]); + window.__NEXT_P.push([pagePath, () => page]); console.debug("Hydrating the page"); diff --git a/crates/next-core/js/src/entry/server-renderer.tsx b/crates/next-core/js/src/entry/server-renderer.tsx index 4842c7669e15e..488d21bc91973 100644 --- a/crates/next-core/js/src/entry/server-renderer.tsx +++ b/crates/next-core/js/src/entry/server-renderer.tsx @@ -98,7 +98,7 @@ async function runOperation( ...otherExports, }, pathname: renderData.path, - buildId: "", + buildId: "development", /* RenderOptsPartial */ runtimeConfig: {}, diff --git a/crates/next-core/src/lib.rs b/crates/next-core/src/lib.rs index a3d7f5b201dfa..c62500c499300 100644 --- a/crates/next-core/src/lib.rs +++ b/crates/next-core/src/lib.rs @@ -6,6 +6,7 @@ mod app_source; mod embed_js; pub mod env; mod fallback; +pub mod manifest; pub mod next_client; mod next_client_component; pub mod next_image; diff --git a/crates/next-core/src/manifest.rs b/crates/next-core/src/manifest.rs new file mode 100644 index 0000000000000..802464d071114 --- /dev/null +++ b/crates/next-core/src/manifest.rs @@ -0,0 +1,82 @@ +use anyhow::Result; +use indexmap::IndexSet; +use mime::APPLICATION_JSON; +use turbo_tasks::primitives::StringsVc; +use turbo_tasks_fs::File; +use turbopack_core::asset::AssetContentVc; +use turbopack_dev_server::source::{ + ContentSource, ContentSourceContent, ContentSourceData, ContentSourceResultVc, ContentSourceVc, +}; +use turbopack_node::{ + node_api_source::NodeApiContentSourceVc, node_rendered_source::NodeRenderContentSourceVc, +}; + +/// 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, +} + +#[turbo_tasks::value_impl] +impl DevManifestContentSourceVc { + #[turbo_tasks::function] + async fn find_routes(self) -> Result { + let this = &*self.await?; + let mut queue = this.page_roots.clone(); + let mut routes = IndexSet::new(); + + while let Some(content_source) = queue.pop() { + queue.extend(content_source.get_children().await?.iter()); + + if let Some(api_source) = NodeApiContentSourceVc::resolve_from(content_source).await? { + routes.insert(format!("/{}", api_source.get_pathname().await?)); + + continue; + } + + if let Some(page_source) = + NodeRenderContentSourceVc::resolve_from(content_source).await? + { + routes.insert(format!("/{}", page_source.get_pathname().await?)); + + continue; + } + } + + routes.sort(); + + Ok(StringsVc::cell(routes.into_iter().collect())) + } +} + +#[turbo_tasks::value_impl] +impl ContentSource for DevManifestContentSource { + #[turbo_tasks::function] + async fn get( + self_vc: DevManifestContentSourceVc, + path: &str, + _data: turbo_tasks::Value, + ) -> Result { + let manifest_content = match path { + "_next/static/development/_devPagesManifest.json" => { + let pages = &*self_vc.find_routes().await?; + + serde_json::to_string(&serde_json::json!({ + "pages": pages, + }))? + } + "_next/static/development/_devMiddlewareManifest.json" => { + // empty middleware manifest + "[]".to_string() + } + _ => return Ok(ContentSourceResultVc::not_found()), + }; + + let file = File::from(manifest_content).with_content_type(APPLICATION_JSON); + + Ok(ContentSourceResultVc::exact( + ContentSourceContent::Static(AssetContentVc::from(file).into()).cell(), + )) + } +} diff --git a/crates/next-core/src/server_rendered_source.rs b/crates/next-core/src/server_rendered_source.rs index c0dd11ae6e112..0458a6df758bf 100644 --- a/crates/next-core/src/server_rendered_source.rs +++ b/crates/next-core/src/server_rendered_source.rs @@ -173,6 +173,7 @@ async fn create_server_rendered_source_for_file( create_node_api_source( specificity, server_root, + pathname, path_regex, SsrEntry { context, diff --git a/crates/next-dev/src/lib.rs b/crates/next-dev/src/lib.rs index 587c470a1dcb2..8097182dc69c3 100644 --- a/crates/next-dev/src/lib.rs +++ b/crates/next-dev/src/lib.rs @@ -18,7 +18,8 @@ use anyhow::{anyhow, Context, Result}; use devserver_options::DevServerOptions; use next_core::{ create_app_source, create_server_rendered_source, create_web_entry_source, env::load_env, - next_image::NextImageContentSourceVc, source_map::NextSourceMapTraceContentSourceVc, + manifest::DevManifestContentSource, next_image::NextImageContentSourceVc, + source_map::NextSourceMapTraceContentSourceVc, }; use owo_colors::OwoColorize; use turbo_tasks::{ @@ -325,8 +326,18 @@ async fn source( .into(); let static_source = StaticAssetsContentSourceVc::new(String::new(), project_path.join("public")).into(); - let main_source = - CombinedContentSourceVc::new(vec![static_source, app_source, rendered_source, web_source]); + let manifest_source = DevManifestContentSource { + page_roots: vec![app_source, rendered_source], + } + .cell() + .into(); + let main_source = CombinedContentSourceVc::new(vec![ + manifest_source, + static_source, + app_source, + rendered_source, + web_source, + ]); let introspect = IntrospectionSource { roots: HashSet::from([main_source.into()]), } diff --git a/crates/turbopack-dev-server/src/source/combined.rs b/crates/turbopack-dev-server/src/source/combined.rs index 9a4796a6b64d9..898436767d18f 100644 --- a/crates/turbopack-dev-server/src/source/combined.rs +++ b/crates/turbopack-dev-server/src/source/combined.rs @@ -6,6 +6,7 @@ use super::{ specificity::SpecificityReadRef, ContentSource, ContentSourceData, ContentSourceResultVc, ContentSourceVc, }; +use crate::source::ContentSourcesVc; /// Combines multiple [ContentSource]s by trying all content sources in order. /// First [ContentSource] that responds with something other than NotFound will @@ -50,6 +51,11 @@ impl ContentSource for CombinedContentSource { Ok(ContentSourceResultVc::not_found()) } } + + #[turbo_tasks::function] + fn get_children(&self) -> ContentSourcesVc { + ContentSourcesVc::cell(self.sources.clone()) + } } #[turbo_tasks::function] diff --git a/crates/turbopack-dev-server/src/source/conditional.rs b/crates/turbopack-dev-server/src/source/conditional.rs index 6ece2d34e9dd1..6028df36a30e5 100644 --- a/crates/turbopack-dev-server/src/source/conditional.rs +++ b/crates/turbopack-dev-server/src/source/conditional.rs @@ -8,6 +8,7 @@ use turbopack_core::introspect::{Introspectable, IntrospectableChildrenVc, Intro use super::{ ContentSource, ContentSourceContent, ContentSourceData, ContentSourceResultVc, ContentSourceVc, }; +use crate::source::ContentSourcesVc; /// Combines two [ContentSource]s like the [CombinedContentSource], but only /// allows to serve from the second source when the first source has @@ -99,6 +100,11 @@ impl ContentSource for ConditionalContentSource { Ok(second) } } + + #[turbo_tasks::function] + fn get_children(&self) -> ContentSourcesVc { + ContentSourcesVc::cell(vec![self.activator, self.action]) + } } #[turbo_tasks::function] diff --git a/crates/turbopack-dev-server/src/source/mod.rs b/crates/turbopack-dev-server/src/source/mod.rs index 088c3bb21cad3..6be2a2d214ec3 100644 --- a/crates/turbopack-dev-server/src/source/mod.rs +++ b/crates/turbopack-dev-server/src/source/mod.rs @@ -306,6 +306,22 @@ pub trait ContentSource { /// arguments, so we want to make the arguments contain as little /// information as possible to increase cache hit ratio. fn get(&self, path: &str, data: Value) -> ContentSourceResultVc; + + /// Gets any content sources wrapped in this content source. + fn get_children(&self) -> ContentSourcesVc { + ContentSourcesVc::empty() + } +} + +#[turbo_tasks::value(transparent)] +pub struct ContentSources(Vec); + +#[turbo_tasks::value_impl] +impl ContentSourcesVc { + #[turbo_tasks::function] + pub fn empty() -> Self { + ContentSourcesVc::cell(Vec::new()) + } } /// An empty ContentSource implementation that responds with NotFound for every diff --git a/crates/turbopack-dev-server/src/source/router.rs b/crates/turbopack-dev-server/src/source/router.rs index f558b0fe6a67a..3b9e8a49ad1a3 100644 --- a/crates/turbopack-dev-server/src/source/router.rs +++ b/crates/turbopack-dev-server/src/source/router.rs @@ -3,6 +3,7 @@ use turbo_tasks::{primitives::StringVc, TryJoinIterExt, Value}; use turbopack_core::introspect::{Introspectable, IntrospectableChildrenVc, IntrospectableVc}; use super::{ContentSource, ContentSourceData, ContentSourceResultVc, ContentSourceVc}; +use crate::source::ContentSourcesVc; /// Binds different ContentSources to different subpaths. A fallback /// ContentSource will serve all other subpaths. @@ -31,6 +32,16 @@ impl ContentSource for RouterContentSource { let (source, path) = self.get_source(path); source.get(path, data) } + + #[turbo_tasks::function] + fn get_children(&self) -> ContentSourcesVc { + let mut sources = Vec::with_capacity(self.routes.len() + 1); + + sources.extend(self.routes.iter().map(|r| r.1)); + sources.push(self.fallback); + + ContentSourcesVc::cell(sources) + } } #[turbo_tasks::function] diff --git a/crates/turbopack-node/src/node_api_source.rs b/crates/turbopack-node/src/node_api_source.rs index 264a625394b9f..1c6140b731b43 100644 --- a/crates/turbopack-node/src/node_api_source.rs +++ b/crates/turbopack-node/src/node_api_source.rs @@ -22,6 +22,7 @@ use crate::path_regex::PathRegexVc; pub fn create_node_api_source( specificity: SpecificityVc, server_root: FileSystemPathVc, + pathname: StringVc, path_regex: PathRegexVc, entry: NodeEntryVc, runtime_entries: EcmascriptChunkPlaceablesVc, @@ -29,6 +30,7 @@ pub fn create_node_api_source( NodeApiContentSource { specificity, server_root, + pathname, path_regex, entry, runtime_entries, @@ -44,14 +46,23 @@ pub fn create_node_api_source( /// for Node.js execution during rendering. The `chunking_context` should emit /// to this directory. #[turbo_tasks::value] -struct NodeApiContentSource { +pub struct NodeApiContentSource { specificity: SpecificityVc, server_root: FileSystemPathVc, + pathname: StringVc, path_regex: PathRegexVc, entry: NodeEntryVc, runtime_entries: EcmascriptChunkPlaceablesVc, } +#[turbo_tasks::value_impl] +impl NodeApiContentSourceVc { + #[turbo_tasks::function] + pub async fn get_pathname(self) -> Result { + Ok(self.await?.pathname) + } +} + impl NodeApiContentSource { /// Checks if a path matches the regular expression async fn is_matching_path(&self, path: &str) -> Result { diff --git a/crates/turbopack-node/src/node_rendered_source.rs b/crates/turbopack-node/src/node_rendered_source.rs index ab6137d236a90..3461c0ef1d65c 100644 --- a/crates/turbopack-node/src/node_rendered_source.rs +++ b/crates/turbopack-node/src/node_rendered_source.rs @@ -67,7 +67,7 @@ pub fn create_node_rendered_source( /// see [create_node_rendered_source] #[turbo_tasks::value] -struct NodeRenderContentSource { +pub struct NodeRenderContentSource { specificity: SpecificityVc, server_root: FileSystemPathVc, pathname: StringVc, @@ -77,6 +77,14 @@ struct NodeRenderContentSource { fallback_page: DevHtmlAssetVc, } +#[turbo_tasks::value_impl] +impl NodeRenderContentSourceVc { + #[turbo_tasks::function] + pub async fn get_pathname(self) -> Result { + Ok(self.await?.pathname) + } +} + impl NodeRenderContentSource { /// Checks if a path matches the regular expression async fn is_matching_path(&self, path: &str) -> Result { diff --git a/crates/turbopack-node/src/source_map/content_source.rs b/crates/turbopack-node/src/source_map/content_source.rs index bb9e7e1ca6ac6..f7103782d7095 100644 --- a/crates/turbopack-node/src/source_map/content_source.rs +++ b/crates/turbopack-node/src/source_map/content_source.rs @@ -8,7 +8,7 @@ use turbopack_core::{ }; use turbopack_dev_server::source::{ ContentSource, ContentSourceContent, ContentSourceData, ContentSourceDataVary, - ContentSourceResultVc, ContentSourceVc, + ContentSourceResultVc, ContentSourceVc, ContentSourcesVc, }; use url::Url; @@ -100,6 +100,11 @@ impl ContentSource for NextSourceMapTraceContentSource { ContentSourceContent::Static(traced.content().into()).cell(), )) } + + #[turbo_tasks::function] + fn get_children(&self) -> ContentSourcesVc { + ContentSourcesVc::cell(vec![self.asset_source]) + } } #[turbo_tasks::value_impl]