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#[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 pub provider_import_source: Option<RcStr>,
46 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), 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::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 source: IssueSource,
242 reason: RcStr,
244 mdx_rule_id: RcStr,
246 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}