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