Skip to content

Commit 10e144c

Browse files
committed
First-class MDX support
1 parent 393f497 commit 10e144c

21 files changed

+1524
-646
lines changed

Cargo.lock

+117-108
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/macros/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ napi = ["dep:napi", "dep:napi-derive", "dep:crossbeam-channel"]
99

1010
[dependencies]
1111
indexmap = "1.9.2"
12-
swc_core = { version = "6.0.1", features = [
12+
swc_core = { version = "9", features = [
1313
"common",
1414
"common_ahash",
1515
"common_sourcemap",

crates/macros/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ pub enum JsValue {
377377
}
378378

379379
pub struct Evaluator<'a> {
380-
constants: HashMap<Id, Result<JsValue, Span>>,
380+
pub constants: HashMap<Id, Result<JsValue, Span>>,
381381
source_map: &'a SourceMap,
382382
}
383383

packages/configs/default/index.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"@parcel/transformer-worklet",
99
"..."
1010
],
11-
"*.{js,mjs,jsm,jsx,es6,cjs,ts,tsx}": [
11+
"*.mdx": ["@parcel/transformer-js"],
12+
"*.{js,mjs,jsm,jsx,es6,cjs,ts,tsx,mdx}": [
1213
"@parcel/transformer-babel",
1314
"@parcel/transformer-js",
1415
"@parcel/transformer-react-refresh-wrap"
@@ -33,7 +34,6 @@
3334
"*.pug": ["@parcel/transformer-pug"],
3435
"*.coffee": ["@parcel/transformer-coffeescript"],
3536
"*.elm": ["@parcel/transformer-elm"],
36-
"*.mdx": ["@parcel/transformer-mdx"],
3737
"*.vue": ["@parcel/transformer-vue"],
3838
"template:*.vue": ["@parcel/transformer-vue"],
3939
"script:*.vue": ["@parcel/transformer-vue"],

packages/configs/default/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@
6262
"@parcel/transformer-inline-string": "2.13.3",
6363
"@parcel/transformer-jsonld": "2.13.3",
6464
"@parcel/transformer-less": "2.13.3",
65-
"@parcel/transformer-mdx": "2.13.3",
6665
"@parcel/transformer-pug": "2.13.3",
6766
"@parcel/transformer-sass": "2.13.3",
6867
"@parcel/transformer-stylus": "2.13.3",

packages/core/core/src/Transformation.js

+5
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,11 @@ export default class Transformation {
569569
configKeyPath?: string,
570570
parcelConfig: ParcelConfig,
571571
): Promise<$ReadOnlyArray<TransformerResult | UncommittedAsset>> {
572+
if (asset.transformers.has(transformerName)) {
573+
return [asset];
574+
}
575+
asset.transformers.add(transformerName);
576+
572577
const logger = new PluginLogger({origin: transformerName});
573578
const tracer = new PluginTracer({
574579
origin: transformerName,

packages/core/core/src/UncommittedAsset.js

+2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export default class UncommittedAsset {
5555
idBase: ?string;
5656
invalidations: Invalidations;
5757
generate: ?() => Promise<GenerateOutput>;
58+
transformers: Set<string>;
5859

5960
constructor({
6061
value,
@@ -74,6 +75,7 @@ export default class UncommittedAsset {
7475
this.isASTDirty = isASTDirty || false;
7576
this.idBase = idBase;
7677
this.invalidations = invalidations || createInvalidations();
78+
this.transformers = new Set();
7779
}
7880

7981
/*
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"dependencies": {
3-
"react": "*"
3+
"react": "^19"
44
}
55
}
+215-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
1-
const assert = require('assert');
2-
const path = require('path');
3-
const {bundle, run} = require('@parcel/test-utils');
1+
import assert from 'assert';
2+
import path from 'path';
3+
import {
4+
bundle,
5+
run,
6+
overlayFS,
7+
fsFixture,
8+
assertBundles,
9+
} from '@parcel/test-utils';
10+
import React from 'react';
11+
import ReactDOM from 'react-dom/server';
412

513
describe('mdx', function () {
14+
let count = 0;
15+
let dir;
16+
beforeEach(async () => {
17+
dir = path.join(__dirname, 'mdx', '' + ++count);
18+
await overlayFS.mkdirp(dir);
19+
});
20+
21+
after(async () => {
22+
await overlayFS.rimraf(path.join(__dirname, 'mdx'));
23+
});
24+
625
it('should support bundling MDX', async function () {
726
let b = await bundle(path.join(__dirname, '/integration/mdx/index.mdx'));
827

928
let output = await run(b);
1029
assert.equal(typeof output.default, 'function');
11-
assert(output.default.isMDXComponent);
1230
});
1331

1432
it('should support bundling MDX with React 17', async function () {
@@ -18,6 +36,198 @@ describe('mdx', function () {
1836

1937
let output = await run(b);
2038
assert.equal(typeof output.default, 'function');
21-
assert(output.default.isMDXComponent);
39+
});
40+
41+
it('should expose static exports on asset.meta', async function () {
42+
await fsFixture(overlayFS, dir)`
43+
index.mdx:
44+
export const navTitle = 'Hello';
45+
46+
# Testing
47+
48+
foo bar
49+
`;
50+
51+
let b = await bundle(path.join(dir, 'index.mdx'), {inputFS: overlayFS});
52+
let asset = b.getBundles()[0].getMainEntry();
53+
54+
assert.deepEqual(asset.meta.ssgMeta.exports, {
55+
navTitle: 'Hello',
56+
});
57+
});
58+
59+
it('should expose table of contents on asset.meta', async function () {
60+
await fsFixture(overlayFS, dir)`
61+
index.mdx:
62+
# Testing
63+
64+
foo bar
65+
66+
## Subtitle
67+
68+
another paragraph
69+
70+
### Sub subtitle
71+
72+
yo
73+
74+
## Another subtitle
75+
76+
yay
77+
`;
78+
79+
let b = await bundle(path.join(dir, 'index.mdx'), {inputFS: overlayFS});
80+
let asset = b.getBundles()[0].getMainEntry();
81+
82+
assert.deepEqual(asset.meta.ssgMeta.tableOfContents, [
83+
{
84+
level: 1,
85+
title: 'Testing',
86+
children: [
87+
{
88+
level: 2,
89+
title: 'Subtitle',
90+
children: [
91+
{
92+
level: 3,
93+
title: 'Sub subtitle',
94+
children: [],
95+
},
96+
],
97+
},
98+
{
99+
level: 2,
100+
title: 'Another subtitle',
101+
children: [],
102+
},
103+
],
104+
},
105+
]);
106+
});
107+
108+
it('should support dependencies', async function () {
109+
await fsFixture(overlayFS, dir)`
110+
index.mdx:
111+
Testing [urls](another.mdx).
112+
113+
<audio src="some.mp3" />
114+
115+
another.mdx:
116+
Another mdx file with an image.
117+
118+
![alt](img.png)
119+
120+
img.png:
121+
122+
some.mp3:
123+
`;
124+
125+
let b = await bundle(path.join(dir, 'index.mdx'), {inputFS: overlayFS});
126+
assertBundles(
127+
b,
128+
[
129+
{
130+
name: 'index.js',
131+
assets: ['index.mdx', 'bundle-url.js'],
132+
},
133+
{
134+
name: 'another.js',
135+
assets: ['another.mdx', 'bundle-url.js'],
136+
},
137+
{
138+
assets: ['img.png'],
139+
},
140+
{
141+
assets: ['some.mp3'],
142+
},
143+
],
144+
{skipNodeModules: true},
145+
);
146+
});
147+
148+
it('should support code block props', async function () {
149+
await fsFixture(overlayFS, dir)`
150+
index.mdx:
151+
\`\`\`tsx boolean string="hi" value={2}
152+
console.log("hi");
153+
\`\`\`
154+
`;
155+
156+
let b = await bundle(path.join(dir, 'index.mdx'), {inputFS: overlayFS});
157+
let output = await run(b);
158+
let codeBlockProps;
159+
function CodeBlock(v) {
160+
codeBlockProps = v;
161+
return <pre>{v.children}</pre>;
162+
}
163+
let res = ReactDOM.renderToStaticMarkup(
164+
React.createElement(output.default, {components: {CodeBlock}}),
165+
);
166+
assert.equal(res, '<pre>console.log(&quot;hi&quot;);</pre>');
167+
assert.deepEqual(codeBlockProps, {
168+
boolean: true,
169+
string: 'hi',
170+
value: 2,
171+
lang: 'tsx',
172+
children: 'console.log("hi");',
173+
});
174+
});
175+
176+
it('should support rendering code blocks', async function () {
177+
await fsFixture(overlayFS, dir)`
178+
index.mdx:
179+
\`\`\`tsx render
180+
<div>Hello</div>
181+
\`\`\`
182+
package.json:
183+
{"dependencies": {"react": "^19"}}
184+
`;
185+
186+
let b = await bundle(path.join(dir, 'index.mdx'), {inputFS: overlayFS});
187+
let output = await run(b);
188+
let res = ReactDOM.renderToStaticMarkup(
189+
React.createElement(output.default),
190+
);
191+
assert.equal(
192+
res,
193+
'<pre><code class="language-tsx">&lt;div&gt;Hello&lt;/div&gt;</code></pre><div>Hello</div>',
194+
);
195+
});
196+
197+
it('should support rendering CSS', async function () {
198+
await fsFixture(overlayFS, dir)`
199+
index.mdx:
200+
\`\`\`css render
201+
.foo { color: red }
202+
\`\`\`
203+
`;
204+
205+
let b = await bundle(path.join(dir, 'index.mdx'), {inputFS: overlayFS});
206+
assertBundles(
207+
b,
208+
[
209+
{
210+
name: 'index.js',
211+
assets: ['index.mdx', 'mdx-components.jsx'],
212+
},
213+
{
214+
name: 'index.css',
215+
assets: ['index.mdx'],
216+
},
217+
],
218+
{skipNodeModules: true},
219+
);
220+
221+
let output = await run(b);
222+
let res = ReactDOM.renderToStaticMarkup(
223+
React.createElement(output.default),
224+
);
225+
assert.equal(
226+
res,
227+
'<pre><code class="language-css">.foo { color: red }</code></pre>',
228+
);
229+
230+
let css = await overlayFS.readFile(b.getBundles()[1].filePath, 'utf8');
231+
assert(css.includes('color: red'));
22232
});
23233
});

packages/core/integration-tests/test/react-ssg.js

+79
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,83 @@ describe('react static', function () {
190190
),
191191
);
192192
});
193+
194+
it('should support MDX', async function () {
195+
await fsFixture(overlayFS, dir)`
196+
index.mdx:
197+
import {Layout} from './Layout';
198+
export default Layout;
199+
200+
export const title = 'Home';
201+
202+
# Testing
203+
204+
Hello this is a test.
205+
206+
## Sub title
207+
208+
Yo.
209+
210+
another.mdx:
211+
import {Layout} from './Layout';
212+
export default Layout;
213+
214+
# Another page
215+
216+
Hello this is a test.
217+
218+
Layout.jsx:
219+
function Toc({toc}) {
220+
return toc?.length ? <ul>{toc.map((t, i) => <li key={i}>{t.title}<Toc toc={t.children} /></li>)}</ul> : null;
221+
}
222+
223+
export function Layout({children, pages, currentPage}) {
224+
return (
225+
<html>
226+
<head>
227+
<title>{currentPage.meta.exports.title ?? currentPage.meta.tableOfContents?.[0].title}</title>
228+
</head>
229+
<body>
230+
<nav>
231+
{pages.map(page => <a key={page.url} href={page.url}>
232+
{page.meta.exports.title ?? page.meta.tableOfContents?.[0].title}
233+
</a>)}
234+
</nav>
235+
<aside>
236+
<Toc toc={currentPage.meta.tableOfContents} />
237+
</aside>
238+
<main>
239+
{children}
240+
</main>
241+
</body>
242+
</html>
243+
)
244+
}
245+
`;
246+
247+
let b = await bundle(path.join(dir, '/*.mdx'), {
248+
inputFS: overlayFS,
249+
targets: ['default'],
250+
});
251+
252+
let output = await overlayFS.readFile(b.getBundles()[0].filePath, 'utf8');
253+
assert(output.includes('<title>Home</title>'));
254+
assert(
255+
output.includes(
256+
'<a href="/index.html">Home</a><a href="/another.html">Another page</a>',
257+
),
258+
);
259+
assert(
260+
output.includes('<ul><li>Testing<ul><li>Sub title</li></ul></li></ul>'),
261+
);
262+
263+
output = await overlayFS.readFile(b.getBundles()[1].filePath, 'utf8');
264+
assert(output.includes('<title>Another page</title>'));
265+
assert(
266+
output.includes(
267+
'<a href="/index.html">Home</a><a href="/another.html">Another page</a>',
268+
),
269+
);
270+
assert(output.includes('<ul><li>Another page</li></ul>'));
271+
});
193272
});

0 commit comments

Comments
 (0)