turbopack_dev_server/introspect/
mod.rs1use 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 .replace('&', "&")
54 .replace('>', ">")
55 .replace('<', "<"),
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 .replace('&', "&")
70 .replace('"', """)
71 .replace('>', ">")
72 .replace('<', "<"),
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 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}