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#[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 pub provider_import_source: Option<RcStr>,
45 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), 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::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 source: IssueSource,
243 reason: RcStr,
245 mdx_rule_id: RcStr,
247 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}