turbopack_dev_server/introspect/
mod.rs

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