From c1ac2ee028e3053dbeb3bf40d0a310afe8f3147c Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Tue, 21 Feb 2023 19:40:50 -0500 Subject: [PATCH] Implement Rewrite Headers support during routing (#3897) --- crates/next-core/src/router_source.rs | 11 ++-- .../next/image/basic/input/pages/index.js | 1 - .../next/router/redirect/input/next.config.js | 12 ++++ .../next/router/redirect/input/pages/index.js | 17 +++++ .../next/router/rewrite/input/next.config.js | 11 ++++ .../next/router/rewrite/input/pages/foo.js | 16 +++++ crates/turbopack-dev-server/src/http.rs | 12 +++- crates/turbopack-dev-server/src/source/mod.rs | 62 ++++++++++++------- .../src/source/resolve.rs | 14 ++++- .../turbopack-dev-server/src/update/stream.rs | 4 +- .../src/render/render_static.rs | 4 +- 11 files changed, 128 insertions(+), 36 deletions(-) create mode 100644 crates/next-dev-tests/tests/integration/next/router/redirect/input/next.config.js create mode 100644 crates/next-dev-tests/tests/integration/next/router/redirect/input/pages/index.js create mode 100644 crates/next-dev-tests/tests/integration/next/router/rewrite/input/next.config.js create mode 100644 crates/next-dev-tests/tests/integration/next/router/rewrite/input/pages/foo.js diff --git a/crates/next-core/src/router_source.rs b/crates/next-core/src/router_source.rs index 07d9790038672..1f9eb98a1d556 100644 --- a/crates/next-core/src/router_source.rs +++ b/crates/next-core/src/router_source.rs @@ -7,7 +7,7 @@ use turbopack_core::{ }; use turbopack_dev_server::source::{ ContentSource, ContentSourceContent, ContentSourceData, ContentSourceDataVary, - ContentSourceResultVc, ContentSourceVc, NeededData, ProxyResult, RewriteVc, + ContentSourceResultVc, ContentSourceVc, HeaderListVc, NeededData, ProxyResult, RewriteBuilder, }; use turbopack_node::execution_context::ExecutionContextVc; @@ -110,11 +110,12 @@ impl ContentSource for NextRouterContentSource { .inner .get(path, Value::new(ContentSourceData::default())), RouterResult::Rewrite(data) => { - // TODO: We can't set response headers on the returned content. + let mut rewrite = RewriteBuilder::new(data.url.clone()).content_source(this.inner); + if !data.headers.is_empty() { + rewrite = rewrite.response_headers(HeaderListVc::new(data.headers.clone())); + } ContentSourceResultVc::exact( - ContentSourceContent::Rewrite(RewriteVc::new(data.url.clone(), this.inner)) - .cell() - .into(), + ContentSourceContent::Rewrite(rewrite.build()).cell().into(), ) } RouterResult::FullMiddleware(data) => ContentSourceResultVc::exact( diff --git a/crates/next-dev-tests/tests/integration/next/image/basic/input/pages/index.js b/crates/next-dev-tests/tests/integration/next/image/basic/input/pages/index.js index 7d715c4fb90aa..3b71fb061bad0 100644 --- a/crates/next-dev-tests/tests/integration/next/image/basic/input/pages/index.js +++ b/crates/next-dev-tests/tests/integration/next/image/basic/input/pages/index.js @@ -27,7 +27,6 @@ export default function Home() { } function runTests() { - console.log(document.querySelectorAll("img")); it("it should link to imported image", function () { const img = document.querySelector("#imported"); expect(img.src).toContain(encodeURIComponent("_next/static/assets")); diff --git a/crates/next-dev-tests/tests/integration/next/router/redirect/input/next.config.js b/crates/next-dev-tests/tests/integration/next/router/redirect/input/next.config.js new file mode 100644 index 0000000000000..3155971386037 --- /dev/null +++ b/crates/next-dev-tests/tests/integration/next/router/redirect/input/next.config.js @@ -0,0 +1,12 @@ +/** @type {import('next').NextConfig} */ +module.exports = { + async redirects() { + return [ + { + source: "/foo", + destination: "https://example.vercel.sh/", + permanent: true, + }, + ]; + }, +}; diff --git a/crates/next-dev-tests/tests/integration/next/router/redirect/input/pages/index.js b/crates/next-dev-tests/tests/integration/next/router/redirect/input/pages/index.js new file mode 100644 index 0000000000000..3c686b8ebb153 --- /dev/null +++ b/crates/next-dev-tests/tests/integration/next/router/redirect/input/pages/index.js @@ -0,0 +1,17 @@ +import { useEffect } from "react"; + +export default function Foo() { + useEffect(() => { + // Only run on client + import("@turbo/pack-test-harness").then(runTests); + }); + + return "index"; +} + +function runTests() { + it("it should display foo, not index", async () => { + const res = await fetch("/foo"); + expect(res.url).toBe("https://example.vercel.sh/"); + }); +} diff --git a/crates/next-dev-tests/tests/integration/next/router/rewrite/input/next.config.js b/crates/next-dev-tests/tests/integration/next/router/rewrite/input/next.config.js new file mode 100644 index 0000000000000..8b19e2be748fd --- /dev/null +++ b/crates/next-dev-tests/tests/integration/next/router/rewrite/input/next.config.js @@ -0,0 +1,11 @@ +/** @type {import('next').NextConfig} */ +module.exports = { + async rewrites() { + return [ + { + source: "/", + destination: "/foo", + }, + ]; + }, +}; diff --git a/crates/next-dev-tests/tests/integration/next/router/rewrite/input/pages/foo.js b/crates/next-dev-tests/tests/integration/next/router/rewrite/input/pages/foo.js new file mode 100644 index 0000000000000..7626257bb6dad --- /dev/null +++ b/crates/next-dev-tests/tests/integration/next/router/rewrite/input/pages/foo.js @@ -0,0 +1,16 @@ +import { useEffect } from "react"; + +export default function Foo() { + useEffect(() => { + // Only run on client + import("@turbo/pack-test-harness").then(runTests); + }); + + return "foo"; +} + +function runTests() { + it("it should display foo, not index", () => { + expect(document.getElementById("__next").textContent).toBe("foo"); + }); +} diff --git a/crates/turbopack-dev-server/src/http.rs b/crates/turbopack-dev-server/src/http.rs index 43b1af0cfad64..883535bb7c5f5 100644 --- a/crates/turbopack-dev-server/src/http.rs +++ b/crates/turbopack-dev-server/src/http.rs @@ -18,6 +18,7 @@ enum GetFromSourceResult { content: FileContentReadRef, status_code: u16, headers: HeaderListReadRef, + header_overwrites: HeaderListReadRef, }, HttpProxy(ProxyResultReadRef), NotFound, @@ -33,13 +34,14 @@ async fn get_from_source( ) -> Result { Ok( match &*resolve_source_request(source, request, issue_repoter).await? { - ResolveSourceRequestResult::Static(static_content_vc) => { + ResolveSourceRequestResult::Static(static_content_vc, header_overwrites) => { let static_content = static_content_vc.await?; if let AssetContent::File(file) = &*static_content.content.content().await? { GetFromSourceResult::Static { content: file.await?, status_code: static_content.status_code, headers: static_content.headers.await?, + header_overwrites: header_overwrites.await?, } } else { GetFromSourceResult::NotFound @@ -69,6 +71,7 @@ pub async fn process_request_with_content_source( content, status_code, headers, + header_overwrites, } => { if let FileContent::Content(file) = &**content { let mut response = Response::builder().status(*status_code); @@ -82,6 +85,13 @@ pub async fn process_request_with_content_source( ); } + for (header_name, header_value) in header_overwrites.iter() { + header_map.insert( + HeaderName::try_from(header_name.clone())?, + hyper::header::HeaderValue::try_from(header_value)?, + ); + } + if let Some(content_type) = file.content_type() { header_map.append( "content-type", diff --git a/crates/turbopack-dev-server/src/source/mod.rs b/crates/turbopack-dev-server/src/source/mod.rs index 15c141ab67b33..d0ecd5466522e 100644 --- a/crates/turbopack-dev-server/src/source/mod.rs +++ b/crates/turbopack-dev-server/src/source/mod.rs @@ -168,7 +168,12 @@ pub struct HeaderList(Vec<(String, String)>); #[turbo_tasks::value_impl] impl HeaderListVc { #[turbo_tasks::function] - pub fn empty() -> HeaderListVc { + pub fn new(headers: Vec<(String, String)>) -> Self { + HeaderList(headers).cell() + } + + #[turbo_tasks::function] + pub fn empty() -> Self { HeaderList(vec![]).cell() } } @@ -497,32 +502,45 @@ pub struct Rewrite { /// A [ContentSource] from which to restart the lookup process. This _does /// not_ need to be the original content source. Having [None] source will - /// restart the lookup process from the original ContentSource. + /// restart the lookup process from the original root ContentSource. pub source: Option, + + /// A [Headers] which will be appended to the eventual, fully resolved + /// content result. This overwrites any previous matching headers. + pub response_headers: Option, } -#[turbo_tasks::value_impl] -impl RewriteVc { - /// Creates a new [RewriteVc] and starts lookup from the provided - /// [ContentSource]. - #[turbo_tasks::function] - pub fn new(path_query: String, source: ContentSourceVc) -> RewriteVc { - debug_assert!(path_query.starts_with('/')); - Rewrite { - path_and_query: path_query, - source: Some(source), +pub struct RewriteBuilder { + rewrite: Rewrite, +} + +impl RewriteBuilder { + pub fn new(path_and_query: String) -> Self { + Self { + rewrite: Rewrite { + path_and_query, + source: None, + response_headers: None, + }, } - .cell() } - /// Creates a new [RewriteVc] and restarts lookup from the root. - #[turbo_tasks::function] - pub fn new_path_query(path_query: String) -> RewriteVc { - debug_assert!(path_query.starts_with('/')); - Rewrite { - path_and_query: path_query, - source: None, - } - .cell() + /// Sets the [ContentSource] from which to restart the lookup process. + /// Without a source, the lookup will restart from the original root + /// ContentSource. + pub fn content_source(mut self, source: ContentSourceVc) -> Self { + self.rewrite.source = Some(source); + self + } + + /// Sets response headers to append to the eventual, fully resolved content + /// result. + pub fn response_headers(mut self, headers: HeaderListVc) -> Self { + self.rewrite.response_headers = Some(headers); + self + } + + pub fn build(self) -> RewriteVc { + self.rewrite.cell() } } diff --git a/crates/turbopack-dev-server/src/source/resolve.rs b/crates/turbopack-dev-server/src/source/resolve.rs index 9dc0e241350f2..3b549ddac471a 100644 --- a/crates/turbopack-dev-server/src/source/resolve.rs +++ b/crates/turbopack-dev-server/src/source/resolve.rs @@ -13,7 +13,7 @@ use super::{ query::Query, request::SourceRequest, ContentSourceContent, ContentSourceDataVary, ContentSourceResult, ContentSourceVc, - ProxyResultVc, StaticContentVc, + HeaderListVc, ProxyResultVc, StaticContentVc, }; use crate::{ handle_issues, @@ -26,7 +26,7 @@ use crate::{ #[turbo_tasks::value(serialization = "none")] pub enum ResolveSourceRequestResult { NotFound, - Static(StaticContentVc), + Static(StaticContentVc, HeaderListVc), HttpProxy(ProxyResultVc), } @@ -44,6 +44,7 @@ pub async fn resolve_source_request( let original_path = request.uri.path().to_string(); let mut current_asset_path = urlencoding::decode(&original_path[1..])?.into_owned(); let mut request_overwrites = (*request).clone(); + let mut response_header_overwrites = Vec::new(); loop { let result = current_source.get(¤t_asset_path, Value::new(data)); handle_issues( @@ -79,6 +80,9 @@ pub async fn resolve_source_request( current_source = new_source.resolve().await?; request_overwrites.uri = new_uri; + if let Some(headers) = &rewrite.response_headers { + response_header_overwrites.extend(headers.await?.iter().cloned()); + } current_asset_path = new_asset_path; data = ContentSourceData::default(); } // _ => , @@ -86,7 +90,11 @@ pub async fn resolve_source_request( break Ok(ResolveSourceRequestResult::NotFound.cell()) } ContentSourceContent::Static(static_content) => { - break Ok(ResolveSourceRequestResult::Static(*static_content).cell()) + break Ok(ResolveSourceRequestResult::Static( + *static_content, + HeaderListVc::new(response_header_overwrites), + ) + .cell()) } ContentSourceContent::HttpProxy(proxy_result) => { break Ok(ResolveSourceRequestResult::HttpProxy(*proxy_result).cell()) diff --git a/crates/turbopack-dev-server/src/update/stream.rs b/crates/turbopack-dev-server/src/update/stream.rs index 4f2c641bc0da4..42a1c2f720ee0 100644 --- a/crates/turbopack-dev-server/src/update/stream.rs +++ b/crates/turbopack-dev-server/src/update/stream.rs @@ -41,7 +41,7 @@ async fn get_update_stream_item( let content = get_content(); match &*content.await? { - ResolveSourceRequestResult::Static(static_content) => { + ResolveSourceRequestResult::Static(static_content, _) => { let resolved_content = static_content.await?.content; let from = from.get(); let update = resolved_content.update(from); @@ -138,7 +138,7 @@ impl UpdateStream { // We can ignore issues reported in content here since [compute_update_stream] // will handle them let version = match &*content.await? { - ResolveSourceRequestResult::Static(static_content) => { + ResolveSourceRequestResult::Static(static_content, _) => { static_content.await?.content.version() } _ => NotFoundVersionVc::new().into(), diff --git a/crates/turbopack-node/src/render/render_static.rs b/crates/turbopack-node/src/render/render_static.rs index e1027176da1a0..179c79cf041a2 100644 --- a/crates/turbopack-node/src/render/render_static.rs +++ b/crates/turbopack-node/src/render/render_static.rs @@ -7,7 +7,7 @@ use turbopack_core::{ }; use turbopack_dev_server::{ html::DevHtmlAssetVc, - source::{HeaderListVc, RewriteVc}, + source::{HeaderListVc, RewriteBuilder, RewriteVc}, }; use turbopack_ecmascript::{chunk::EcmascriptChunkPlaceablesVc, EcmascriptModuleAssetVc}; @@ -120,7 +120,7 @@ async fn run_static_operation( .context("receiving from node.js process")? { RenderStaticIncomingMessage::Rewrite { path } => { - StaticResultVc::rewrite(RewriteVc::new_path_query(path)) + StaticResultVc::rewrite(RewriteBuilder::new(path).build()) } RenderStaticIncomingMessage::Response { status_code,