Skip to main content

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 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/// Subset of mdxjs::Options to allow to inherit turbopack's jsx-related configs
33/// into mdxjs. This is thin, near straightforward subset of mdxjs::Options to
34/// enable turbo tasks.
35#[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    /// The path to a module providing Components to mdx modules.
44    /// The provider must export a useMDXComponents, which is called to access
45    /// an object of components.
46    pub provider_import_source: Option<RcStr>,
47    /// Determines how to parse mdx contents.
48    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), // true means 'preserve' jsx syntax.
175            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's positions are 1-indexed, SourcePos is 0-indexed.
200                            // Both end positions point to the first character after the range
201                            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    /// Place of message.
252    source: IssueSource,
253    /// Reason for message (should use markdown).
254    reason: RcStr,
255    /// Category of message.
256    mdx_rule_id: RcStr,
257    /// Namespace of message.
258    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}