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(
124                this.source.ident().path().await?.clone_value(),
125                "MDX processing",
126            )
127            .await?
128            .connect()
129            .await?
130            .content)
131    }
132}
133
134#[turbo_tasks::function(operation)]
135fn transform_process_operation(asset: ResolvedVc<MdxTransformedAsset>) -> Vc<MdxTransformResult> {
136    asset.process()
137}
138
139#[turbo_tasks::value_impl]
140impl MdxTransformedAsset {
141    #[turbo_tasks::function]
142    async fn process(&self) -> Result<Vc<MdxTransformResult>> {
143        let content = self.source.content().await?;
144        let transform_options = self.options.await?;
145
146        let AssetContent::File(file) = &*content else {
147            anyhow::bail!("Unexpected mdx asset content");
148        };
149
150        let FileContent::Content(file) = &*file.await? else {
151            anyhow::bail!("Not able to read mdx file content");
152        };
153
154        let jsx_runtime = if let Some(runtime) = &transform_options.jsx_runtime {
155            match runtime.as_str() {
156                "automatic" => Some(mdxjs::JsxRuntime::Automatic),
157                "classic" => Some(mdxjs::JsxRuntime::Classic),
158                _ => None,
159            }
160        } else {
161            None
162        };
163
164        let parse_options = match transform_options.mdx_type {
165            Some(MdxParseConstructs::Gfm) => MdxParseOptions::gfm(),
166            _ => MdxParseOptions::default(),
167        };
168
169        let options = Options {
170            parse: parse_options,
171            development: transform_options.development.unwrap_or(false),
172            provider_import_source: transform_options
173                .provider_import_source
174                .clone()
175                .map(RcStr::into_owned),
176            jsx: transform_options.jsx.unwrap_or(false), // true means 'preserve' jsx syntax.
177            jsx_runtime,
178            jsx_import_source: transform_options
179                .jsx_import_source
180                .clone()
181                .map(RcStr::into_owned),
182            filepath: Some(self.source.ident().path().await?.to_string()),
183            ..Default::default()
184        };
185
186        let result = compile(&file.content().to_str()?, &options);
187
188        match result {
189            Ok(mdx_jsx_component) => Ok(MdxTransformResult {
190                content: AssetContent::file(File::from(Rope::from(mdx_jsx_component)).into())
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}
290
291pub fn register() {
292    turbo_tasks::register();
293    turbo_tasks_fs::register();
294    turbopack_core::register();
295    turbopack_ecmascript::register();
296    include!(concat!(env!("OUT_DIR"), "/register.rs"));
297}