turbopack_dev_server/introspect/
mod.rs1use 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 .replace('&', "&")
62 .replace('>', ">")
63 .replace('<', "<"),
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 .replace('&', "&")
78 .replace('"', """)
79 .replace('>', ">")
80 .replace('<', "<"),
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 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}