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, IssueDescriptionExt, IssueExt, IssueSource, IssueStage, OptionIssueSource,
15        OptionStyledString, 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: ResolvedVc<Self>) -> Result<Vc<AssetContent>> {
121        let this = self.await?;
122        Ok(*transform_process_operation(self)
123            .issue_file_path(this.source.ident().path().owned().await?, "MDX processing")
124            .await?
125            .connect()
126            .await?
127            .content)
128    }
129}
130
131#[turbo_tasks::function(operation)]
132fn transform_process_operation(asset: ResolvedVc<MdxTransformedAsset>) -> Vc<MdxTransformResult> {
133    asset.process()
134}
135
136#[turbo_tasks::value_impl]
137impl MdxTransformedAsset {
138    #[turbo_tasks::function]
139    async fn process(&self) -> Result<Vc<MdxTransformResult>> {
140        let content = self.source.content().await?;
141        let transform_options = self.options.await?;
142
143        let AssetContent::File(file) = &*content else {
144            anyhow::bail!("Unexpected mdx asset content");
145        };
146
147        let FileContent::Content(file) = &*file.await? else {
148            anyhow::bail!("Not able to read mdx file content");
149        };
150
151        let jsx_runtime = if let Some(runtime) = &transform_options.jsx_runtime {
152            match runtime.as_str() {
153                "automatic" => Some(mdxjs::JsxRuntime::Automatic),
154                "classic" => Some(mdxjs::JsxRuntime::Classic),
155                _ => None,
156            }
157        } else {
158            None
159        };
160
161        let parse_options = match transform_options.mdx_type {
162            Some(MdxParseConstructs::Gfm) => MdxParseOptions::gfm(),
163            _ => MdxParseOptions::default(),
164        };
165
166        let options = Options {
167            parse: parse_options,
168            development: transform_options.development.unwrap_or(false),
169            provider_import_source: transform_options
170                .provider_import_source
171                .clone()
172                .map(RcStr::into_owned),
173            jsx: transform_options.jsx.unwrap_or(false), // true means 'preserve' jsx syntax.
174            jsx_runtime,
175            jsx_import_source: transform_options
176                .jsx_import_source
177                .clone()
178                .map(RcStr::into_owned),
179            filepath: Some(self.source.ident().path().await?.to_string()),
180            ..Default::default()
181        };
182
183        let result = compile(&file.content().to_str()?, &options);
184
185        match result {
186            Ok(mdx_jsx_component) => Ok(MdxTransformResult {
187                content: AssetContent::file(File::from(Rope::from(mdx_jsx_component)).into())
188                    .to_resolved()
189                    .await?,
190            }
191            .cell()),
192            Err(err) => {
193                let source = match err.place {
194                    Some(p) => {
195                        let (start, end) = match *p {
196                            // markdown's positions are 1-indexed, SourcePos is 0-indexed.
197                            // Both end positions point to the first character after the range
198                            markdown::message::Place::Position(p) => (
199                                SourcePos {
200                                    line: (p.start.line - 1) as u32,
201                                    column: (p.start.column - 1) as u32,
202                                },
203                                SourcePos {
204                                    line: (p.end.line - 1) as u32,
205                                    column: (p.end.column - 1) as u32,
206                                },
207                            ),
208                            markdown::message::Place::Point(p) => {
209                                let p = SourcePos {
210                                    line: (p.line - 1) as u32,
211                                    column: (p.column - 1) as u32,
212                                };
213                                (p, p)
214                            }
215                        };
216
217                        IssueSource::from_line_col(self.source, start, end)
218                    }
219                    None => IssueSource::from_source_only(self.source),
220                };
221
222                MdxIssue {
223                    source,
224                    reason: RcStr::from(err.reason),
225                    mdx_rule_id: RcStr::from(*err.rule_id),
226                    mdx_source: RcStr::from(*err.source),
227                }
228                .resolved_cell()
229                .emit();
230
231                Ok(MdxTransformResult {
232                    content: AssetContent::File(FileContent::NotFound.resolved_cell())
233                        .resolved_cell(),
234                }
235                .cell())
236            }
237        }
238    }
239}
240
241#[turbo_tasks::value]
242struct MdxTransformResult {
243    content: ResolvedVc<AssetContent>,
244}
245
246#[turbo_tasks::value]
247struct MdxIssue {
248    /// Place of message.
249    source: IssueSource,
250    /// Reason for message (should use markdown).
251    reason: RcStr,
252    /// Category of message.
253    mdx_rule_id: RcStr,
254    /// Namespace of message.
255    mdx_source: RcStr,
256}
257
258#[turbo_tasks::value_impl]
259impl Issue for MdxIssue {
260    #[turbo_tasks::function]
261    fn file_path(&self) -> Vc<FileSystemPath> {
262        self.source.file_path()
263    }
264
265    #[turbo_tasks::function]
266    fn source(&self) -> Vc<OptionIssueSource> {
267        Vc::cell(Some(self.source))
268    }
269
270    #[turbo_tasks::function]
271    fn stage(self: Vc<Self>) -> Vc<IssueStage> {
272        IssueStage::Parse.cell()
273    }
274
275    #[turbo_tasks::function]
276    fn title(self: Vc<Self>) -> Vc<StyledString> {
277        StyledString::Text(rcstr!("MDX Parse Error")).cell()
278    }
279
280    #[turbo_tasks::function]
281    fn description(&self) -> Vc<OptionStyledString> {
282        Vc::cell(Some(
283            StyledString::Text(self.reason.clone()).resolved_cell(),
284        ))
285    }
286}
287
288pub fn register() {
289    turbo_tasks::register();
290    turbo_tasks_fs::register();
291    turbopack_core::register();
292    turbopack_ecmascript::register();
293    include!(concat!(env!("OUT_DIR"), "/register.rs"));
294}