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