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