1#![feature(min_specialization)]
2#![feature(arbitrary_self_types)]
3#![feature(arbitrary_self_types_pointers)]
4
5use anyhow::Result;
6use mdxjs::{MdxParseOptions, Options, compile};
7use serde::Deserialize;
8use turbo_rcstr::{RcStr, rcstr};
9use turbo_tasks::{ResolvedVc, ValueDefault, Vc};
10use turbo_tasks_fs::{File, FileContent, FileSystemPath, rope::Rope};
11use turbopack_core::{
12 asset::{Asset, AssetContent},
13 context::AssetContext,
14 ident::AssetIdent,
15 issue::{
16 Issue, IssueExt, IssueSource, IssueStage, OptionIssueSource, OptionStyledString,
17 StyledString,
18 },
19 source::Source,
20 source_pos::SourcePos,
21 source_transform::SourceTransform,
22};
23
24#[turbo_tasks::value(shared, operation)]
25#[derive(Hash, Debug, Clone, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub enum MdxParseConstructs {
28 Commonmark,
29 Gfm,
30}
31
32#[turbo_tasks::value(shared, operation)]
36#[derive(Hash, Debug, Clone, Deserialize)]
37#[serde(rename_all = "camelCase", default)]
38pub struct MdxTransformOptions {
39 pub development: Option<bool>,
40 pub jsx: Option<bool>,
41 pub jsx_runtime: Option<RcStr>,
42 pub jsx_import_source: Option<RcStr>,
43 pub provider_import_source: Option<RcStr>,
47 pub mdx_type: Option<MdxParseConstructs>,
49}
50
51impl Default for MdxTransformOptions {
52 fn default() -> Self {
53 Self {
54 development: Some(true),
55 jsx: Some(false),
56 jsx_runtime: None,
57 jsx_import_source: None,
58 provider_import_source: None,
59 mdx_type: Some(MdxParseConstructs::Commonmark),
60 }
61 }
62}
63
64#[turbo_tasks::value_impl]
65impl MdxTransformOptions {
66 #[turbo_tasks::function]
67 fn default_private() -> Vc<Self> {
68 Self::cell(Default::default())
69 }
70}
71
72impl ValueDefault for MdxTransformOptions {
73 fn value_default() -> Vc<Self> {
74 Self::default_private()
75 }
76}
77
78#[turbo_tasks::value]
79pub struct MdxTransform {
80 options: ResolvedVc<MdxTransformOptions>,
81}
82
83#[turbo_tasks::value_impl]
84impl MdxTransform {
85 #[turbo_tasks::function]
86 pub fn new(options: ResolvedVc<MdxTransformOptions>) -> Vc<Self> {
87 MdxTransform { options }.cell()
88 }
89}
90
91#[turbo_tasks::value_impl]
92impl SourceTransform for MdxTransform {
93 #[turbo_tasks::function]
94 fn transform(
95 &self,
96 source: ResolvedVc<Box<dyn Source>>,
97 _asset_context: ResolvedVc<Box<dyn AssetContext>>,
98 ) -> Vc<Box<dyn Source>> {
99 Vc::upcast(
100 MdxTransformedAsset {
101 options: self.options,
102 source,
103 }
104 .cell(),
105 )
106 }
107}
108
109#[turbo_tasks::value]
110struct MdxTransformedAsset {
111 options: ResolvedVc<MdxTransformOptions>,
112 source: ResolvedVc<Box<dyn Source>>,
113}
114
115#[turbo_tasks::value_impl]
116impl Source for MdxTransformedAsset {
117 #[turbo_tasks::function]
118 fn ident(&self) -> Vc<AssetIdent> {
119 self.source.ident().rename_as(rcstr!("*.tsx"))
120 }
121
122 #[turbo_tasks::function]
123 async fn description(&self) -> Result<Vc<RcStr>> {
124 let inner = self.source.description().await?;
125 Ok(Vc::cell(format!("MDX transform of {}", inner).into()))
126 }
127}
128
129#[turbo_tasks::value_impl]
130impl Asset for MdxTransformedAsset {
131 #[turbo_tasks::function]
132 async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
133 Ok(*self.process().await?.content)
134 }
135}
136
137#[turbo_tasks::value_impl]
138impl MdxTransformedAsset {
139 #[turbo_tasks::function]
140 async fn process(&self) -> Result<Vc<MdxTransformResult>> {
141 let content = self.source.content().await?;
142 let transform_options = self.options.await?;
143
144 let AssetContent::File(file) = &*content else {
145 anyhow::bail!("Unexpected mdx asset content");
146 };
147
148 let FileContent::Content(file) = &*file.await? else {
149 anyhow::bail!("Not able to read mdx file content");
150 };
151
152 let jsx_runtime = if let Some(runtime) = &transform_options.jsx_runtime {
153 match runtime.as_str() {
154 "automatic" => Some(mdxjs::JsxRuntime::Automatic),
155 "classic" => Some(mdxjs::JsxRuntime::Classic),
156 _ => None,
157 }
158 } else {
159 None
160 };
161
162 let parse_options = match transform_options.mdx_type {
163 Some(MdxParseConstructs::Gfm) => MdxParseOptions::gfm(),
164 _ => MdxParseOptions::default(),
165 };
166
167 let options = Options {
168 parse: parse_options,
169 development: transform_options.development.unwrap_or(false),
170 provider_import_source: transform_options
171 .provider_import_source
172 .clone()
173 .map(RcStr::into_owned),
174 jsx: transform_options.jsx.unwrap_or(false), jsx_runtime,
176 jsx_import_source: transform_options
177 .jsx_import_source
178 .clone()
179 .map(RcStr::into_owned),
180 filepath: Some(self.source.ident().path().await?.to_string()),
181 ..Default::default()
182 };
183
184 let result = compile(&file.content().to_str()?, &options);
185
186 match result {
187 Ok(mdx_jsx_component) => Ok(MdxTransformResult {
188 content: AssetContent::file(
189 FileContent::Content(File::from(Rope::from(mdx_jsx_component))).cell(),
190 )
191 .to_resolved()
192 .await?,
193 }
194 .cell()),
195 Err(err) => {
196 let source = match err.place {
197 Some(p) => {
198 let (start, end) = match *p {
199 markdown::message::Place::Position(p) => (
202 SourcePos {
203 line: (p.start.line - 1) as u32,
204 column: (p.start.column - 1) as u32,
205 },
206 SourcePos {
207 line: (p.end.line - 1) as u32,
208 column: (p.end.column - 1) as u32,
209 },
210 ),
211 markdown::message::Place::Point(p) => {
212 let p = SourcePos {
213 line: (p.line - 1) as u32,
214 column: (p.column - 1) as u32,
215 };
216 (p, p)
217 }
218 };
219
220 IssueSource::from_line_col(self.source, start, end)
221 }
222 None => IssueSource::from_source_only(self.source),
223 };
224
225 MdxIssue {
226 source,
227 reason: RcStr::from(err.reason),
228 mdx_rule_id: RcStr::from(*err.rule_id),
229 mdx_source: RcStr::from(*err.source),
230 }
231 .resolved_cell()
232 .emit();
233
234 Ok(MdxTransformResult {
235 content: AssetContent::File(FileContent::NotFound.resolved_cell())
236 .resolved_cell(),
237 }
238 .cell())
239 }
240 }
241 }
242}
243
244#[turbo_tasks::value]
245struct MdxTransformResult {
246 content: ResolvedVc<AssetContent>,
247}
248
249#[turbo_tasks::value]
250struct MdxIssue {
251 source: IssueSource,
253 reason: RcStr,
255 mdx_rule_id: RcStr,
257 mdx_source: RcStr,
259}
260
261#[turbo_tasks::value_impl]
262impl Issue for MdxIssue {
263 #[turbo_tasks::function]
264 fn file_path(&self) -> Vc<FileSystemPath> {
265 self.source.file_path()
266 }
267
268 #[turbo_tasks::function]
269 fn source(&self) -> Vc<OptionIssueSource> {
270 Vc::cell(Some(self.source))
271 }
272
273 #[turbo_tasks::function]
274 fn stage(self: Vc<Self>) -> Vc<IssueStage> {
275 IssueStage::Parse.cell()
276 }
277
278 #[turbo_tasks::function]
279 fn title(self: Vc<Self>) -> Vc<StyledString> {
280 StyledString::Text(rcstr!("MDX Parse Error")).cell()
281 }
282
283 #[turbo_tasks::function]
284 fn description(&self) -> Vc<OptionStyledString> {
285 Vc::cell(Some(
286 StyledString::Text(self.reason.clone()).resolved_cell(),
287 ))
288 }
289}