turbopack_mdx/
lib.rs

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