From ce46dcecb726015da4a665790e0839ecb3d7faf4 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Tue, 6 Jun 2023 12:08:20 -0400 Subject: [PATCH] Prepare RouterContentSource for basePath (vercel/turbo#5218) ### Description This is part 2 of [WEB-993](https://linear.app/vercel/issue/WEB-993) basePath support. A few of our next-specific content sources will need to be scoped under the `basePath` (like `_next/image` and `__nextjs_original-stack-frame`). These are currently served with a `RouterContentSource`, but it didn't have support for arbitrary prefixes. We _could_ have changed the subpath for these sources to include the `basePath`, but that would require reading the `next_config.base_path()` in the [source](https://github.com/vercel/next.js/blob/2b1f0d9351610b04d01638efed19252ca81d0023/packages/next-swc/crates/next-dev/src/lib.rs#L413-L423) method, and it would invalidate our entire call graph whenever the `next.config.js` changed. Not a good choice. ### Testing Instructions --- .../turbopack-dev-server/src/source/router.rs | 163 ++++++++++++++---- .../src/source/static_assets.rs | 2 + 2 files changed, 130 insertions(+), 35 deletions(-) diff --git a/crates/turbopack-dev-server/src/source/router.rs b/crates/turbopack-dev-server/src/source/router.rs index c3f7e5f0bbe8e..a82b5499846d8 100644 --- a/crates/turbopack-dev-server/src/source/router.rs +++ b/crates/turbopack-dev-server/src/source/router.rs @@ -7,22 +7,105 @@ use crate::source::ContentSourcesVc; /// Binds different ContentSources to different subpaths. A fallback /// ContentSource will serve all other subpaths. +// TODO(WEB-1151): Remove this and migrate all users to PrefixedRouterContentSource. #[turbo_tasks::value(shared)] pub struct RouterContentSource { pub routes: Vec<(String, ContentSourceVc)>, pub fallback: ContentSourceVc, } -impl RouterContentSource { - fn get_source<'s, 'a>(&'s self, path: &'a str) -> (&'s ContentSourceVc, &'a str) { - for (route, source) in self.routes.iter() { - if path.starts_with(route) { - let path = &path[route.len()..]; - return (source, path); +/// Binds different ContentSources to different subpaths. The request path must +/// begin with the prefix, which will be stripped (along with the subpath) +/// before querying the ContentSource. A fallback ContentSource will serve all +/// other subpaths, including if the request path does not include the prefix. +#[turbo_tasks::value(shared)] +pub struct PrefixedRouterContentSource { + prefix: StringVc, + routes: Vec<(String, ContentSourceVc)>, + fallback: ContentSourceVc, +} + +#[turbo_tasks::value_impl] +impl PrefixedRouterContentSourceVc { + #[turbo_tasks::function] + async fn new( + prefix: StringVc, + routes: Vec<(String, ContentSourceVc)>, + fallback: ContentSourceVc, + ) -> Result { + if cfg!(debug_assertions) { + let prefix_string = prefix.await?; + debug_assert!(prefix_string.is_empty() || prefix_string.ends_with('/')); + debug_assert!(prefix_string.starts_with('/')); + } + Ok(PrefixedRouterContentSource { + prefix, + routes, + fallback, + } + .cell()) + } +} + +/// If the `path` starts with `prefix`, then it will search each route to see if +/// any subpath matches. If so, the remaining path (after removing the prefix +/// and subpath) is used to query the matching ContentSource. If no match is +/// found, then the fallback is queried with the original path. +async fn get( + routes: &[(String, ContentSourceVc)], + fallback: &ContentSourceVc, + prefix: &str, + path: &str, + data: Value, +) -> Result { + let mut found = None; + + if let Some(path) = path.strip_prefix(prefix) { + for (subpath, source) in routes { + if let Some(path) = path.strip_prefix(subpath) { + found = Some((source, path)); + break; } } - (&self.fallback, path) } + + let (source, path) = found.unwrap_or((fallback, path)); + Ok(source.resolve().await?.get(path, data)) +} + +fn get_children( + routes: &[(String, ContentSourceVc)], + fallback: &ContentSourceVc, +) -> ContentSourcesVc { + ContentSourcesVc::cell( + routes + .iter() + .map(|r| r.1) + .chain(std::iter::once(*fallback)) + .collect(), + ) +} + +async fn get_introspection_children( + routes: &[(String, ContentSourceVc)], + fallback: &ContentSourceVc, +) -> Result { + Ok(IntrospectableChildrenVc::cell( + routes + .iter() + .cloned() + .chain(std::iter::once((String::new(), *fallback))) + .map(|(path, source)| async move { + Ok(IntrospectableVc::resolve_from(source) + .await? + .map(|i| (StringVc::cell(path), i))) + }) + .try_join() + .await? + .into_iter() + .flatten() + .collect(), + )) } #[turbo_tasks::value_impl] @@ -33,51 +116,61 @@ impl ContentSource for RouterContentSource { path: &str, data: Value, ) -> Result { - let (source, path) = self.get_source(path); - Ok(source.resolve().await?.get(path, data)) + get(&self.routes, &self.fallback, "", path, data).await } #[turbo_tasks::function] fn get_children(&self) -> ContentSourcesVc { - let mut sources = Vec::with_capacity(self.routes.len() + 1); + get_children(&self.routes, &self.fallback) + } +} - sources.extend(self.routes.iter().map(|r| r.1)); - sources.push(self.fallback); +#[turbo_tasks::value_impl] +impl Introspectable for RouterContentSource { + #[turbo_tasks::function] + fn ty(&self) -> StringVc { + StringVc::cell("router content source".to_string()) + } - ContentSourcesVc::cell(sources) + #[turbo_tasks::function] + async fn children(&self) -> Result { + get_introspection_children(&self.routes, &self.fallback).await } } -#[turbo_tasks::function] -fn introspectable_type() -> StringVc { - StringVc::cell("router content source".to_string()) +#[turbo_tasks::value_impl] +impl ContentSource for PrefixedRouterContentSource { + #[turbo_tasks::function] + async fn get( + &self, + path: &str, + data: Value, + ) -> Result { + let prefix = self.prefix.await?; + get(&self.routes, &self.fallback, &prefix, path, data).await + } + + #[turbo_tasks::function] + fn get_children(&self) -> ContentSourcesVc { + get_children(&self.routes, &self.fallback) + } } #[turbo_tasks::value_impl] -impl Introspectable for RouterContentSource { +impl Introspectable for PrefixedRouterContentSource { #[turbo_tasks::function] fn ty(&self) -> StringVc { - introspectable_type() + StringVc::cell("prefixed router content source".to_string()) + } + + #[turbo_tasks::function] + async fn details(&self) -> Result { + let prefix = self.prefix.await?; + Ok(StringVc::cell(format!("prefix: '{}'", prefix))) } #[turbo_tasks::function] async fn children(&self) -> Result { - Ok(IntrospectableChildrenVc::cell( - self.routes - .iter() - .cloned() - .chain(std::iter::once((String::new(), self.fallback))) - .map(|(path, source)| (StringVc::cell(path), source)) - .map(|(path, source)| async move { - Ok(IntrospectableVc::resolve_from(source) - .await? - .map(|i| (path, i))) - }) - .try_join() - .await? - .into_iter() - .flatten() - .collect(), - )) + get_introspection_children(&self.routes, &self.fallback).await } } diff --git a/crates/turbopack-dev-server/src/source/static_assets.rs b/crates/turbopack-dev-server/src/source/static_assets.rs index 8bf0ad5bc9915..1d5c3ccbbeaf6 100644 --- a/crates/turbopack-dev-server/src/source/static_assets.rs +++ b/crates/turbopack-dev-server/src/source/static_assets.rs @@ -22,6 +22,7 @@ pub struct StaticAssetsContentSource { #[turbo_tasks::value_impl] impl StaticAssetsContentSourceVc { + // TODO(WEB-1151): Remove this method and migrate users to `with_prefix`. #[turbo_tasks::function] pub fn new(prefix: String, dir: FileSystemPathVc) -> StaticAssetsContentSourceVc { StaticAssetsContentSourceVc::with_prefix(StringVc::cell(prefix), dir) @@ -35,6 +36,7 @@ impl StaticAssetsContentSourceVc { if cfg!(debug_assertions) { let prefix_string = prefix.await?; debug_assert!(prefix_string.is_empty() || prefix_string.ends_with('/')); + debug_assert!(!prefix_string.starts_with('/')); } Ok(StaticAssetsContentSource { prefix, dir }.cell()) }