turbopack_dev_server/introspect/
mod.rs

1use std::{borrow::Cow, fmt::Display};
2
3use anyhow::Result;
4use rustc_hash::FxHashSet;
5use turbo_rcstr::{RcStr, rcstr};
6use turbo_tasks::{ReadRef, ResolvedVc, TryJoinIterExt, Vc};
7use turbo_tasks_fs::{File, json::parse_json_with_source_context};
8use turbopack_core::{
9    asset::AssetContent,
10    introspect::{Introspectable, IntrospectableChildren},
11    version::VersionedContentExt,
12};
13use turbopack_ecmascript::utils::FormatIter;
14
15use crate::source::{
16    ContentSource, ContentSourceContent, ContentSourceData, GetContentSourceContent,
17    route_tree::{RouteTree, RouteTrees, RouteType},
18};
19
20#[turbo_tasks::value(shared)]
21pub struct IntrospectionSource {
22    pub roots: FxHashSet<ResolvedVc<Box<dyn Introspectable>>>,
23}
24
25fn introspection_source() -> RcStr {
26    rcstr!("introspection-source")
27}
28
29#[turbo_tasks::value_impl]
30impl Introspectable for IntrospectionSource {
31    #[turbo_tasks::function]
32    fn ty(&self) -> Vc<RcStr> {
33        Vc::cell(introspection_source())
34    }
35
36    #[turbo_tasks::function]
37    fn title(&self) -> Vc<RcStr> {
38        Vc::cell(introspection_source())
39    }
40
41    #[turbo_tasks::function]
42    fn children(&self) -> Vc<IntrospectableChildren> {
43        Vc::cell(
44            self.roots
45                .iter()
46                .map(|root| (rcstr!("root"), *root))
47                .collect(),
48        )
49    }
50}
51
52struct HtmlEscaped<T>(T);
53
54impl<T: Display> Display for HtmlEscaped<T> {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        f.write_str(
57            &self
58                .0
59                .to_string()
60                // TODO this is pretty inefficient
61                .replace('&', "&amp;")
62                .replace('>', "&gt;")
63                .replace('<', "&lt;"),
64        )
65    }
66}
67
68struct HtmlStringEscaped<T>(T);
69
70impl<T: Display> Display for HtmlStringEscaped<T> {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        f.write_str(
73            &self
74                .0
75                .to_string()
76                // TODO this is pretty inefficient
77                .replace('&', "&amp;")
78                .replace('"', "&quot;")
79                .replace('>', "&gt;")
80                .replace('<', "&lt;"),
81        )
82    }
83}
84
85#[turbo_tasks::value_impl]
86impl ContentSource for IntrospectionSource {
87    #[turbo_tasks::function]
88    async fn get_routes(self: Vc<Self>) -> Result<Vc<RouteTree>> {
89        Ok(Vc::<RouteTrees>::cell(vec![
90            RouteTree::new_route(Vec::new(), RouteType::Exact, Vc::upcast(self))
91                .to_resolved()
92                .await?,
93            RouteTree::new_route(Vec::new(), RouteType::CatchAll, Vc::upcast(self))
94                .to_resolved()
95                .await?,
96        ])
97        .merge())
98    }
99}
100
101#[turbo_tasks::value_impl]
102impl GetContentSourceContent for IntrospectionSource {
103    #[turbo_tasks::function]
104    async fn get(
105        self: ResolvedVc<Self>,
106        path: RcStr,
107        _data: ContentSourceData,
108    ) -> Result<Vc<ContentSourceContent>> {
109        // get last segment
110        let path = &path[path.rfind('/').unwrap_or(0) + 1..];
111        let introspectable = if path.is_empty() {
112            let roots = &self.await?.roots;
113            if roots.len() == 1 {
114                *roots.iter().next().unwrap()
115            } else {
116                ResolvedVc::upcast(self)
117            }
118        } else {
119            parse_json_with_source_context(path)?
120        };
121        let internal_ty = Vc::debug_identifier(*introspectable).await?;
122        fn str_or_err(s: &Result<ReadRef<RcStr>>) -> Cow<'_, str> {
123            s.as_ref().map_or_else(
124                |e| Cow::<'_, str>::Owned(format!("ERROR: {e:?}")),
125                |d| Cow::Borrowed(&**d),
126            )
127        }
128        let ty = introspectable.ty().await;
129        let ty = str_or_err(&ty);
130        let title = introspectable.title().await;
131        let title = str_or_err(&title);
132        let details = introspectable.details().await;
133        let details = str_or_err(&details);
134        let children = introspectable.children().await?;
135        let has_children = !children.is_empty();
136        let children = children
137            .iter()
138            .map(|(name, child)| async move {
139                let ty = child.ty().await;
140                let ty = str_or_err(&ty);
141                let title = child.title().await;
142                let title = str_or_err(&title);
143                let path = serde_json::to_string(&child)?;
144                Ok(format!(
145                    "<li>{name} <!-- {title} --><a href=\"./{path}\">[{ty}] {title}</a></li>",
146                    name = HtmlEscaped(name),
147                    title = HtmlEscaped(title),
148                    path = HtmlStringEscaped(urlencoding::encode(&path)),
149                    ty = HtmlEscaped(ty),
150                ))
151            })
152            .try_join()
153            .await?;
154        let details = if details.is_empty() {
155            String::new()
156        } else if has_children {
157            format!(
158                "<details><summary><h3 style=\"display: \
159                 inline;\">Details</h3></summary><pre>{details}</pre></details>",
160                details = HtmlEscaped(details)
161            )
162        } else {
163            format!(
164                "<h3>Details</h3><pre>{details}</pre>",
165                details = HtmlEscaped(details)
166            )
167        };
168        let html: RcStr = format!(
169            "<!DOCTYPE html>
170    <html><head><title>{title}</title></head>
171    <body>
172      <h3>{internal_ty}</h3>
173      <h2>{ty}</h2>
174      <h1>{title}</h1>
175      {details}
176      <ul>{children}</ul>
177    </body>
178    </html>",
179            title = HtmlEscaped(title),
180            ty = HtmlEscaped(ty),
181            children = FormatIter(|| children.iter())
182        )
183        .into();
184        Ok(ContentSourceContent::static_content(
185            AssetContent::file(
186                File::from(html)
187                    .with_content_type(mime::TEXT_HTML_UTF_8)
188                    .into(),
189            )
190            .versioned(),
191        ))
192    }
193}