Skip to main content

turbopack_dev_server/source/
mod.rs

1pub mod asset_graph;
2pub mod combined;
3pub mod headers;
4pub mod issue_context;
5pub mod lazy_instantiated;
6pub mod query;
7pub mod request;
8pub mod resolve;
9pub mod route_tree;
10pub mod router;
11pub mod static_assets;
12pub mod wrapping_source;
13
14use std::collections::BTreeSet;
15
16use anyhow::Result;
17use bincode::{Decode, Encode};
18use futures::{TryStreamExt, stream::Stream as StreamTrait};
19use turbo_rcstr::RcStr;
20use turbo_tasks::{
21    Completion, NonLocalValue, OperationVc, ResolvedVc, Upcast, Vc, trace::TraceRawVcs,
22    util::SharedError,
23};
24use turbo_tasks_bytes::{Bytes, Stream, StreamRead};
25use turbo_tasks_fs::FileSystemPath;
26use turbo_tasks_hash::{DeterministicHash, DeterministicHasher, Xxh3Hash64Hasher};
27use turbopack_core::version::{Version, VersionedContent};
28
29use crate::source::{
30    headers::Headers, issue_context::IssueFilePathContentSource, query::Query,
31    route_tree::RouteTree,
32};
33
34/// The result of proxying a request to another HTTP server.
35#[turbo_tasks::value(shared, operation)]
36pub struct ProxyResult {
37    /// The HTTP status code to return.
38    pub status: u16,
39    /// Headers arranged as contiguous (name, value) pairs.
40    pub headers: Vec<(RcStr, RcStr)>,
41    /// The body to return.
42    #[turbo_tasks(trace_ignore)]
43    pub body: Body,
44}
45
46#[turbo_tasks::value_impl]
47impl Version for ProxyResult {
48    #[turbo_tasks::function]
49    async fn id(&self) -> Result<Vc<RcStr>> {
50        let mut hash = Xxh3Hash64Hasher::new();
51        hash.write_u16(self.status);
52        for (name, value) in &self.headers {
53            name.deterministic_hash(&mut hash);
54            value.deterministic_hash(&mut hash);
55        }
56        let mut read = self.body.read();
57        while let Some(chunk) = read.try_next().await? {
58            hash.write_bytes(&chunk);
59        }
60        Ok(Vc::cell(hash.finish().to_string().into()))
61    }
62}
63
64/// Receives the actual content for a [`ContentSource`].
65#[turbo_tasks::value_trait]
66pub trait GetContentSourceContent {
67    /// Specifies data requirements for the [`get`][Self::get] function. Restricting data passed
68    /// allows improved caching of the [`get`][Self::get] method.
69    #[turbo_tasks::function]
70    fn vary(self: Vc<Self>) -> Vc<ContentSourceDataVary> {
71        ContentSourceDataVary::default().cell()
72    }
73
74    #[turbo_tasks::function]
75    fn get(self: Vc<Self>, path: RcStr, data: ContentSourceData) -> Vc<ContentSourceContent>;
76}
77
78#[turbo_tasks::value(transparent)]
79pub struct GetContentSourceContents(Vec<ResolvedVc<Box<dyn GetContentSourceContent>>>);
80
81#[turbo_tasks::value]
82pub struct StaticContent {
83    pub content: ResolvedVc<Box<dyn VersionedContent>>,
84    pub status_code: u16,
85    pub headers: ResolvedVc<HeaderList>,
86}
87
88#[turbo_tasks::value(shared)]
89/// The content of a result that is returned by [`GetContentSourceContent::get`].
90// TODO: add a `Dynamic` variant in future to allow streaming and server responses
91pub enum ContentSourceContent {
92    NotFound,
93    Static(ResolvedVc<StaticContent>),
94    HttpProxy(OperationVc<ProxyResult>),
95    Rewrite(ResolvedVc<Rewrite>),
96    /// Continue with the next route
97    Next,
98}
99
100/// This trait can be emitted as collectible and will be applied after the
101/// request is handled and it's ensured that it finishes before the next request
102/// is handled.
103#[turbo_tasks::value_trait]
104pub trait ContentSourceSideEffect {
105    #[turbo_tasks::function]
106    fn apply(self: Vc<Self>) -> Vc<Completion>;
107}
108
109#[turbo_tasks::value_impl]
110impl GetContentSourceContent for ContentSourceContent {
111    #[turbo_tasks::function]
112    fn get(self: Vc<Self>, _path: RcStr, _data: ContentSourceData) -> Vc<ContentSourceContent> {
113        self
114    }
115}
116
117#[turbo_tasks::value_impl]
118impl ContentSourceContent {
119    #[turbo_tasks::function]
120    pub async fn static_content(
121        content: ResolvedVc<Box<dyn VersionedContent>>,
122    ) -> Result<Vc<ContentSourceContent>> {
123        Ok(ContentSourceContent::Static(
124            StaticContent {
125                content,
126                status_code: 200,
127                headers: HeaderList::empty().to_resolved().await?,
128            }
129            .resolved_cell(),
130        )
131        .cell())
132    }
133
134    #[turbo_tasks::function]
135    pub fn not_found() -> Vc<ContentSourceContent> {
136        ContentSourceContent::NotFound.cell()
137    }
138}
139
140/// A list of headers arranged as contiguous (name, value) pairs.
141#[turbo_tasks::value(transparent)]
142pub struct HeaderList(Vec<(RcStr, RcStr)>);
143
144#[turbo_tasks::value_impl]
145impl HeaderList {
146    #[turbo_tasks::function]
147    pub fn new(headers: Vec<(RcStr, RcStr)>) -> Vc<Self> {
148        HeaderList(headers).cell()
149    }
150
151    #[turbo_tasks::function]
152    pub fn empty() -> Vc<Self> {
153        HeaderList(vec![]).cell()
154    }
155}
156
157/// Additional info passed to the [`ContentSource`]. It was extracted from the http request.
158///
159/// Note that you might not receive information that has not been requested via
160/// [`GetContentSourceContent::vary`]. So make sure to request all information that's needed.
161#[turbo_tasks::task_input]
162#[derive(PartialEq, Eq, TraceRawVcs, Clone, Debug, Hash, Default, Encode, Decode)]
163pub struct ContentSourceData {
164    /// HTTP method, if requested.
165    pub method: Option<RcStr>,
166    /// The full url (including query string), if requested.
167    pub url: Option<RcStr>,
168    /// The full url (including query string) before rewrites where applied, if
169    /// requested.
170    pub original_url: Option<RcStr>,
171    /// Query string items, if requested.
172    pub query: Option<Query>,
173    /// raw query string, if requested. Does not include the `?`.
174    pub raw_query: Option<RcStr>,
175    /// HTTP headers, might contain multiple headers with the same name, if
176    /// requested.
177    pub headers: Option<Headers>,
178    /// Raw HTTP headers, might contain multiple headers with the same name, if
179    /// requested.
180    pub raw_headers: Option<Vec<(RcStr, RcStr)>>,
181    /// Request body, if requested.
182    pub body: Option<ResolvedVc<Body>>,
183    /// See [ContentSourceDataVary::cache_buster].
184    pub cache_buster: u64,
185}
186
187pub type BodyChunk = Result<Bytes, SharedError>;
188
189/// A request body.
190#[turbo_tasks::value(shared)]
191#[derive(Default, Clone, Debug)]
192pub struct Body {
193    #[turbo_tasks(trace_ignore)]
194    chunks: Stream<BodyChunk>,
195}
196
197impl Body {
198    /// Creates a new body from a list of chunks.
199    pub fn new(chunks: Vec<BodyChunk>) -> Self {
200        Self {
201            chunks: Stream::new_closed(chunks),
202        }
203    }
204
205    /// Returns an iterator over the body's chunks.
206    pub fn read(&self) -> StreamRead<BodyChunk> {
207        self.chunks.read()
208    }
209
210    pub fn from_stream<T: StreamTrait<Item = BodyChunk> + Send + Unpin + 'static>(
211        source: T,
212    ) -> Self {
213        Self {
214            chunks: Stream::from(source),
215        }
216    }
217}
218
219impl<T: Into<Bytes>> From<T> for Body {
220    fn from(value: T) -> Self {
221        Body::new(vec![Ok(value.into())])
222    }
223}
224
225/// Filter function that describes which information is required.
226#[derive(Debug, Clone, PartialEq, Eq, TraceRawVcs, Hash, NonLocalValue, Encode, Decode)]
227pub enum ContentSourceDataFilter {
228    All,
229    Subset(BTreeSet<String>),
230}
231
232impl ContentSourceDataFilter {
233    /// Merges the filtering to get a filter that covers both filters.
234    pub fn extend(&mut self, other: &ContentSourceDataFilter) {
235        match self {
236            ContentSourceDataFilter::All => {}
237            ContentSourceDataFilter::Subset(set) => match other {
238                ContentSourceDataFilter::All => *self = ContentSourceDataFilter::All,
239                ContentSourceDataFilter::Subset(set2) => set.extend(set2.iter().cloned()),
240            },
241        }
242    }
243
244    /// Merges the filtering to get a filter that covers both filters. Works on
245    /// [`Option<ContentSourceDataFilter>`][ContentSourceDataFilter] where [`None`] behaves as a
246    /// no-op empty filter.
247    pub fn extend_options(
248        this: &mut Option<ContentSourceDataFilter>,
249        other: &Option<ContentSourceDataFilter>,
250    ) {
251        if let Some(this) = this.as_mut() {
252            if let Some(other) = other.as_ref() {
253                this.extend(other);
254            }
255        } else {
256            this.clone_from(other);
257        }
258    }
259
260    /// Returns true if the filter contains the given key.
261    pub fn contains(&self, key: &str) -> bool {
262        match self {
263            ContentSourceDataFilter::All => true,
264            ContentSourceDataFilter::Subset(set) => set.contains(key),
265        }
266    }
267
268    /// Returns true if the first argument at least contains all values that the
269    /// second argument would contain.
270    pub fn fulfills(
271        this: &Option<ContentSourceDataFilter>,
272        other: &Option<ContentSourceDataFilter>,
273    ) -> bool {
274        match (this, other) {
275            (_, None) => true,
276            (None, Some(_)) => false,
277            (Some(this), Some(other)) => match (this, other) {
278                (ContentSourceDataFilter::All, _) => true,
279                (_, ContentSourceDataFilter::All) => false,
280                (ContentSourceDataFilter::Subset(this), ContentSourceDataFilter::Subset(other)) => {
281                    this.is_superset(other)
282                }
283            },
284        }
285    }
286}
287
288/// Describes additional information that need to be sent to requests to [`ContentSource`]. By
289/// sending these information [`ContentSource`] responses are cached-keyed by them and they can
290/// access them.
291#[turbo_tasks::value(shared)]
292#[derive(Debug, Default, Clone, Hash)]
293pub struct ContentSourceDataVary {
294    pub method: bool,
295    pub url: bool,
296    pub original_url: bool,
297    pub query: Option<ContentSourceDataFilter>,
298    pub raw_query: bool,
299    pub headers: Option<ContentSourceDataFilter>,
300    pub raw_headers: bool,
301    pub body: bool,
302    /// When true, a `cache_buster` value is added to the [ContentSourceData].
303    /// This value will be different on every request, which ensures the
304    /// content is never cached.
305    pub cache_buster: bool,
306    pub placeholder_for_future_extensions: (),
307}
308
309impl ContentSourceDataVary {
310    /// Merges two vary specification to create a combination of both that cover all information
311    /// requested by either one
312    pub fn extend(&mut self, other: &ContentSourceDataVary) {
313        let ContentSourceDataVary {
314            method,
315            url,
316            original_url,
317            query,
318            raw_query,
319            headers,
320            raw_headers,
321            body,
322            cache_buster,
323            placeholder_for_future_extensions: _,
324        } = self;
325        *method = *method || other.method;
326        *url = *url || other.url;
327        *original_url = *original_url || other.original_url;
328        *body = *body || other.body;
329        *cache_buster = *cache_buster || other.cache_buster;
330        *raw_query = *raw_query || other.raw_query;
331        *raw_headers = *raw_headers || other.raw_headers;
332        ContentSourceDataFilter::extend_options(query, &other.query);
333        ContentSourceDataFilter::extend_options(headers, &other.headers);
334    }
335
336    /// Returns true if `self` at least contains all values that the argument would contain.
337    pub fn fulfills(&self, other: &ContentSourceDataVary) -> bool {
338        // All fields must be used!
339        let ContentSourceDataVary {
340            method,
341            url,
342            original_url,
343            query,
344            raw_query,
345            headers,
346            raw_headers,
347            body,
348            cache_buster,
349            placeholder_for_future_extensions: _,
350        } = self;
351        if other.method && !method {
352            return false;
353        }
354        if other.url && !url {
355            return false;
356        }
357        if other.original_url && !original_url {
358            return false;
359        }
360        if other.body && !body {
361            return false;
362        }
363        if other.raw_query && !raw_query {
364            return false;
365        }
366        if other.raw_headers && !raw_headers {
367            return false;
368        }
369        if other.cache_buster && !cache_buster {
370            return false;
371        }
372        if !ContentSourceDataFilter::fulfills(query, &other.query) {
373            return false;
374        }
375        if !ContentSourceDataFilter::fulfills(headers, &other.headers) {
376            return false;
377        }
378        true
379    }
380}
381
382/// A source of content that the dev server uses to respond to http requests.
383#[turbo_tasks::value_trait]
384pub trait ContentSource {
385    #[turbo_tasks::function]
386    fn get_routes(self: Vc<Self>) -> Vc<RouteTree>;
387
388    /// Gets any content sources wrapped in this content source.
389    #[turbo_tasks::function]
390    fn get_children(self: Vc<Self>) -> Vc<ContentSources> {
391        ContentSources::empty()
392    }
393}
394
395pub trait ContentSourceExt {
396    fn issue_file_path(
397        self: Vc<Self>,
398        file_path: FileSystemPath,
399        description: RcStr,
400    ) -> Vc<Box<dyn ContentSource>>;
401}
402
403impl<T> ContentSourceExt for T
404where
405    T: Upcast<Box<dyn ContentSource>>,
406{
407    fn issue_file_path(
408        self: Vc<Self>,
409        file_path: FileSystemPath,
410        description: RcStr,
411    ) -> Vc<Box<dyn ContentSource>> {
412        Vc::upcast(IssueFilePathContentSource::new_file_path(
413            file_path,
414            description,
415            Vc::upcast_non_strict(self),
416        ))
417    }
418}
419
420#[turbo_tasks::value(transparent)]
421pub struct ContentSources(Vec<ResolvedVc<Box<dyn ContentSource>>>);
422
423#[turbo_tasks::value_impl]
424impl ContentSources {
425    #[turbo_tasks::function]
426    pub fn empty() -> Vc<Self> {
427        Vc::cell(Vec::new())
428    }
429}
430
431/// An empty ContentSource implementation that responds with NotFound for every
432/// request.
433#[turbo_tasks::value]
434pub struct NoContentSource;
435
436#[turbo_tasks::value_impl]
437impl NoContentSource {
438    #[turbo_tasks::function]
439    pub fn new() -> Vc<Self> {
440        NoContentSource.cell()
441    }
442}
443#[turbo_tasks::value_impl]
444impl ContentSource for NoContentSource {
445    #[turbo_tasks::function]
446    fn get_routes(&self) -> Vc<RouteTree> {
447        RouteTree::empty()
448    }
449}
450
451#[derive(Debug, Clone, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
452pub enum RewriteType {
453    Location {
454        /// The new path and query used to lookup content. This _does not_ need to be the original
455        /// path or query.
456        path_and_query: RcStr,
457    },
458    ContentSource {
459        /// [`ContentSource`]s from which to restart the lookup process. This _does not_ need to be
460        /// the original content source.
461        source: OperationVc<Box<dyn ContentSource>>,
462        /// The new path and query used to lookup content. This _does not_ need
463        /// to be the original path or query.
464        path_and_query: RcStr,
465    },
466    Sources {
467        /// [`GetContentSourceContent`]s from which to restart the lookup process. This _does not_
468        /// need to be the original content source.
469        sources: OperationVc<GetContentSourceContents>,
470    },
471}
472
473/// A rewrite returned from a [ContentSource]. This tells the dev server to
474/// update its parsed url, path, and queries with this new information.
475#[turbo_tasks::value(shared)]
476#[derive(Debug)]
477pub struct Rewrite {
478    pub ty: RewriteType,
479
480    /// A [Headers] which will be appended to the eventual, fully resolved
481    /// content result. This overwrites any previous matching headers.
482    pub response_headers: Option<ResolvedVc<HeaderList>>,
483
484    /// A [HeaderList] which will overwrite the values used during the lookup
485    /// process. All headers not present in this list will be deleted.
486    pub request_headers: Option<ResolvedVc<HeaderList>>,
487}
488
489pub struct RewriteBuilder {
490    rewrite: Rewrite,
491}
492
493impl RewriteBuilder {
494    pub fn new(path_and_query: RcStr) -> Self {
495        Self {
496            rewrite: Rewrite {
497                ty: RewriteType::Location { path_and_query },
498                response_headers: None,
499                request_headers: None,
500            },
501        }
502    }
503
504    pub fn new_source_with_path_and_query(
505        source: OperationVc<Box<dyn ContentSource>>,
506        path_and_query: RcStr,
507    ) -> Self {
508        Self {
509            rewrite: Rewrite {
510                ty: RewriteType::ContentSource {
511                    source,
512                    path_and_query,
513                },
514                response_headers: None,
515                request_headers: None,
516            },
517        }
518    }
519
520    pub fn new_sources(sources: OperationVc<GetContentSourceContents>) -> Self {
521        Self {
522            rewrite: Rewrite {
523                ty: RewriteType::Sources { sources },
524                response_headers: None,
525                request_headers: None,
526            },
527        }
528    }
529
530    /// Sets response headers to append to the eventual, fully resolved content
531    /// result.
532    pub fn response_headers(mut self, headers: ResolvedVc<HeaderList>) -> Self {
533        self.rewrite.response_headers = Some(headers);
534        self
535    }
536
537    /// Sets request headers to overwrite the headers used during the lookup
538    /// process.
539    pub fn request_headers(mut self, headers: ResolvedVc<HeaderList>) -> Self {
540        self.rewrite.request_headers = Some(headers);
541        self
542    }
543
544    pub fn build(self) -> Vc<Rewrite> {
545        self.rewrite.cell()
546    }
547}