Skip to content

Commit

Permalink
First-class MDX support
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett committed Dec 28, 2024
1 parent 393f497 commit 10e144c
Show file tree
Hide file tree
Showing 21 changed files with 1,524 additions and 646 deletions.
225 changes: 117 additions & 108 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ napi = ["dep:napi", "dep:napi-derive", "dep:crossbeam-channel"]

[dependencies]
indexmap = "1.9.2"
swc_core = { version = "6.0.1", features = [
swc_core = { version = "9", features = [
"common",
"common_ahash",
"common_sourcemap",
Expand Down
2 changes: 1 addition & 1 deletion crates/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ pub enum JsValue {
}

pub struct Evaluator<'a> {
constants: HashMap<Id, Result<JsValue, Span>>,
pub constants: HashMap<Id, Result<JsValue, Span>>,
source_map: &'a SourceMap,
}

Expand Down
4 changes: 2 additions & 2 deletions packages/configs/default/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"@parcel/transformer-worklet",
"..."
],
"*.{js,mjs,jsm,jsx,es6,cjs,ts,tsx}": [
"*.mdx": ["@parcel/transformer-js"],
"*.{js,mjs,jsm,jsx,es6,cjs,ts,tsx,mdx}": [
"@parcel/transformer-babel",
"@parcel/transformer-js",
"@parcel/transformer-react-refresh-wrap"
Expand All @@ -33,7 +34,6 @@
"*.pug": ["@parcel/transformer-pug"],
"*.coffee": ["@parcel/transformer-coffeescript"],
"*.elm": ["@parcel/transformer-elm"],
"*.mdx": ["@parcel/transformer-mdx"],
"*.vue": ["@parcel/transformer-vue"],
"template:*.vue": ["@parcel/transformer-vue"],
"script:*.vue": ["@parcel/transformer-vue"],
Expand Down
1 change: 0 additions & 1 deletion packages/configs/default/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@
"@parcel/transformer-inline-string": "2.13.3",
"@parcel/transformer-jsonld": "2.13.3",
"@parcel/transformer-less": "2.13.3",
"@parcel/transformer-mdx": "2.13.3",
"@parcel/transformer-pug": "2.13.3",
"@parcel/transformer-sass": "2.13.3",
"@parcel/transformer-stylus": "2.13.3",
Expand Down
5 changes: 5 additions & 0 deletions packages/core/core/src/Transformation.js
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,11 @@ export default class Transformation {
configKeyPath?: string,
parcelConfig: ParcelConfig,
): Promise<$ReadOnlyArray<TransformerResult | UncommittedAsset>> {
if (asset.transformers.has(transformerName)) {
return [asset];
}
asset.transformers.add(transformerName);

const logger = new PluginLogger({origin: transformerName});
const tracer = new PluginTracer({
origin: transformerName,
Expand Down
2 changes: 2 additions & 0 deletions packages/core/core/src/UncommittedAsset.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export default class UncommittedAsset {
idBase: ?string;
invalidations: Invalidations;
generate: ?() => Promise<GenerateOutput>;
transformers: Set<string>;

constructor({
value,
Expand All @@ -74,6 +75,7 @@ export default class UncommittedAsset {
this.isASTDirty = isASTDirty || false;
this.idBase = idBase;
this.invalidations = invalidations || createInvalidations();
this.transformers = new Set();
}

/*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"dependencies": {
"react": "*"
"react": "^19"
}
}
220 changes: 215 additions & 5 deletions packages/core/integration-tests/test/mdx.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,32 @@
const assert = require('assert');
const path = require('path');
const {bundle, run} = require('@parcel/test-utils');
import assert from 'assert';
import path from 'path';
import {
bundle,
run,
overlayFS,
fsFixture,
assertBundles,
} from '@parcel/test-utils';
import React from 'react';
import ReactDOM from 'react-dom/server';

describe('mdx', function () {
let count = 0;
let dir;
beforeEach(async () => {
dir = path.join(__dirname, 'mdx', '' + ++count);
await overlayFS.mkdirp(dir);
});

after(async () => {
await overlayFS.rimraf(path.join(__dirname, 'mdx'));
});

it('should support bundling MDX', async function () {
let b = await bundle(path.join(__dirname, '/integration/mdx/index.mdx'));

let output = await run(b);
assert.equal(typeof output.default, 'function');
assert(output.default.isMDXComponent);
});

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

let output = await run(b);
assert.equal(typeof output.default, 'function');
assert(output.default.isMDXComponent);
});

it('should expose static exports on asset.meta', async function () {
await fsFixture(overlayFS, dir)`
index.mdx:
export const navTitle = 'Hello';
# Testing
foo bar
`;

let b = await bundle(path.join(dir, 'index.mdx'), {inputFS: overlayFS});
let asset = b.getBundles()[0].getMainEntry();

assert.deepEqual(asset.meta.ssgMeta.exports, {
navTitle: 'Hello',
});
});

it('should expose table of contents on asset.meta', async function () {
await fsFixture(overlayFS, dir)`
index.mdx:
# Testing
foo bar
## Subtitle
another paragraph
### Sub subtitle
yo
## Another subtitle
yay
`;

let b = await bundle(path.join(dir, 'index.mdx'), {inputFS: overlayFS});
let asset = b.getBundles()[0].getMainEntry();

assert.deepEqual(asset.meta.ssgMeta.tableOfContents, [
{
level: 1,
title: 'Testing',
children: [
{
level: 2,
title: 'Subtitle',
children: [
{
level: 3,
title: 'Sub subtitle',
children: [],
},
],
},
{
level: 2,
title: 'Another subtitle',
children: [],
},
],
},
]);
});

it('should support dependencies', async function () {
await fsFixture(overlayFS, dir)`
index.mdx:
Testing [urls](another.mdx).
<audio src="some.mp3" />
another.mdx:
Another mdx file with an image.
![alt](img.png)
img.png:
some.mp3:
`;

let b = await bundle(path.join(dir, 'index.mdx'), {inputFS: overlayFS});
assertBundles(
b,
[
{
name: 'index.js',
assets: ['index.mdx', 'bundle-url.js'],
},
{
name: 'another.js',
assets: ['another.mdx', 'bundle-url.js'],
},
{
assets: ['img.png'],
},
{
assets: ['some.mp3'],
},
],
{skipNodeModules: true},
);
});

it('should support code block props', async function () {
await fsFixture(overlayFS, dir)`
index.mdx:
\`\`\`tsx boolean string="hi" value={2}
console.log("hi");
\`\`\`
`;

let b = await bundle(path.join(dir, 'index.mdx'), {inputFS: overlayFS});
let output = await run(b);
let codeBlockProps;
function CodeBlock(v) {
codeBlockProps = v;
return <pre>{v.children}</pre>;
}
let res = ReactDOM.renderToStaticMarkup(
React.createElement(output.default, {components: {CodeBlock}}),
);
assert.equal(res, '<pre>console.log(&quot;hi&quot;);</pre>');
assert.deepEqual(codeBlockProps, {
boolean: true,
string: 'hi',
value: 2,
lang: 'tsx',
children: 'console.log("hi");',
});
});

it('should support rendering code blocks', async function () {
await fsFixture(overlayFS, dir)`
index.mdx:
\`\`\`tsx render
<div>Hello</div>
\`\`\`
package.json:
{"dependencies": {"react": "^19"}}
`;

let b = await bundle(path.join(dir, 'index.mdx'), {inputFS: overlayFS});
let output = await run(b);
let res = ReactDOM.renderToStaticMarkup(
React.createElement(output.default),
);
assert.equal(
res,
'<pre><code class="language-tsx">&lt;div&gt;Hello&lt;/div&gt;</code></pre><div>Hello</div>',
);
});

it('should support rendering CSS', async function () {
await fsFixture(overlayFS, dir)`
index.mdx:
\`\`\`css render
.foo { color: red }
\`\`\`
`;

let b = await bundle(path.join(dir, 'index.mdx'), {inputFS: overlayFS});
assertBundles(
b,
[
{
name: 'index.js',
assets: ['index.mdx', 'mdx-components.jsx'],
},
{
name: 'index.css',
assets: ['index.mdx'],
},
],
{skipNodeModules: true},
);

let output = await run(b);
let res = ReactDOM.renderToStaticMarkup(
React.createElement(output.default),
);
assert.equal(
res,
'<pre><code class="language-css">.foo { color: red }</code></pre>',
);

let css = await overlayFS.readFile(b.getBundles()[1].filePath, 'utf8');
assert(css.includes('color: red'));
});
});
79 changes: 79 additions & 0 deletions packages/core/integration-tests/test/react-ssg.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,83 @@ describe('react static', function () {
),
);
});

it('should support MDX', async function () {
await fsFixture(overlayFS, dir)`
index.mdx:
import {Layout} from './Layout';
export default Layout;
export const title = 'Home';
# Testing
Hello this is a test.
## Sub title
Yo.
another.mdx:
import {Layout} from './Layout';
export default Layout;
# Another page
Hello this is a test.
Layout.jsx:
function Toc({toc}) {
return toc?.length ? <ul>{toc.map((t, i) => <li key={i}>{t.title}<Toc toc={t.children} /></li>)}</ul> : null;
}
export function Layout({children, pages, currentPage}) {
return (
<html>
<head>
<title>{currentPage.meta.exports.title ?? currentPage.meta.tableOfContents?.[0].title}</title>
</head>
<body>
<nav>
{pages.map(page => <a key={page.url} href={page.url}>
{page.meta.exports.title ?? page.meta.tableOfContents?.[0].title}
</a>)}
</nav>
<aside>
<Toc toc={currentPage.meta.tableOfContents} />
</aside>
<main>
{children}
</main>
</body>
</html>
)
}
`;

let b = await bundle(path.join(dir, '/*.mdx'), {
inputFS: overlayFS,
targets: ['default'],
});

let output = await overlayFS.readFile(b.getBundles()[0].filePath, 'utf8');
assert(output.includes('<title>Home</title>'));
assert(
output.includes(
'<a href="/index.html">Home</a><a href="/another.html">Another page</a>',
),
);
assert(
output.includes('<ul><li>Testing<ul><li>Sub title</li></ul></li></ul>'),
);

output = await overlayFS.readFile(b.getBundles()[1].filePath, 'utf8');
assert(output.includes('<title>Another page</title>'));
assert(
output.includes(
'<a href="/index.html">Home</a><a href="/another.html">Another page</a>',
),
);
assert(output.includes('<ul><li>Another page</li></ul>'));
});
});
Loading

0 comments on commit 10e144c

Please sign in to comment.