Skip to content

Commit

Permalink
Prepare RouterContentSource for basePath (vercel/turborepo#5218)
Browse files Browse the repository at this point in the history
### 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

<!--
  Give a quick description of steps to test your changes.
-->
  • Loading branch information
jridgewell authored Jun 6, 2023
1 parent 3d6dd97 commit 57e28dc
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 35 deletions.
163 changes: 128 additions & 35 deletions crates/turbopack-dev-server/src/source/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
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<ContentSourceData>,
) -> Result<ContentSourceResultVc> {
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<IntrospectableChildrenVc> {
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]
Expand All @@ -33,51 +116,61 @@ impl ContentSource for RouterContentSource {
path: &str,
data: Value<ContentSourceData>,
) -> Result<ContentSourceResultVc> {
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<IntrospectableChildrenVc> {
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<ContentSourceData>,
) -> Result<ContentSourceResultVc> {
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<StringVc> {
let prefix = self.prefix.await?;
Ok(StringVc::cell(format!("prefix: '{}'", prefix)))
}

#[turbo_tasks::function]
async fn children(&self) -> Result<IntrospectableChildrenVc> {
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
}
}
2 changes: 2 additions & 0 deletions crates/turbopack-dev-server/src/source/static_assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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())
}
Expand Down

0 comments on commit 57e28dc

Please sign in to comment.