Skip to content

Commit dbf14d9

Browse files
committed
Fixes path issue
See frontarm/mdx-util#68
1 parent 815c8df commit dbf14d9

File tree

10 files changed

+411
-0
lines changed

10 files changed

+411
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

LICENSE.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2017-present James K. Nelson
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# `mdx.macro`
2+
3+
[![Babel Macro](https://img.shields.io/badge/babel--macro-%F0%9F%8E%A3-f5da55.svg?style=flat-square)](https://github.com/kentcdodds/babel-plugin-macros)
4+
5+
[![npm version](https://img.shields.io/npm/v/mdx.macro.svg)](https://www.npmjs.com/package/mdx.macro)
6+
7+
A babel macro for converting mdx into components, using the [@mdx-js/mdx](https://github.com/mdx-js/mdx#readme) package.
8+
9+
## Installation
10+
11+
Just install `mdx.macro`. It includes @mdx-js/mdx and @mdx-js/tag as dependencies.
12+
13+
```bash
14+
npm install mdx.macro --save
15+
```
16+
17+
## Usage
18+
19+
You have three options for importing your MDX:
20+
21+
- Dynamic import: `importMDX('./document.mdx')`
22+
- Synchronous import: `importMDX.sync('./document.mdx')`
23+
- The `mdx` tag: <code>mdx\`Create a \*\*component\*\* from a template literal\`</code>
24+
25+
### Dynamic import
26+
27+
Returns a promise to a component, which can be used with [React.lazy()](https://reactjs.org/docs/code-splitting.html#reactlazy)
28+
29+
```js
30+
import { importMDX } from './mdx.macro'
31+
32+
const MyDocument = React.lazy(() => importMDX('./my-document.mdx'))
33+
34+
ReactDOM.render(
35+
<React.Suspense fallback={<div>Loading...</div>}>
36+
<MyDocument />
37+
</React.Suspense>,
38+
document.getElementById('root')
39+
)
40+
```
41+
42+
Works by creating a temporary file under your project's `node_modules/.cache/mdx.macro` directory, and importing that.
43+
44+
```js
45+
import { importMDX } from './mdx.macro'
46+
47+
const promiseToMyDocument = importMDX('./my-document.mdx')
48+
49+
↓ ↓ ↓ ↓ ↓ ↓
50+
51+
const promiseToMyDocument = import('.cache/mdx.macro/my-document.hash1234.mdx.js')
52+
```
53+
54+
55+
### Synchronous import
56+
57+
Returns a component that can be used directly.
58+
59+
```js
60+
import { importMDX } from './mdx.macro'
61+
62+
const MyDocument = importMDX.sync('./my-document.mdx')
63+
64+
ReactDOM.render(
65+
<MyDocument />,
66+
document.getElementById('root')
67+
)
68+
```
69+
70+
Works by creating a temporary file under your project's `node_modules/.cache/mdx.macro` directory, and importing that.
71+
72+
```js
73+
import { importMDX } from './mdx.macro'
74+
75+
const MyDocument = importMDX.sync('./my-document.mdx')
76+
77+
↓ ↓ ↓ ↓ ↓ ↓
78+
79+
import _MyDocument from '.cache/mdx.macro/my-document.hash1234.mdx.js'
80+
81+
const MyDocument = _MyDocument
82+
```
83+
84+
85+
### Tagged Template Literals
86+
87+
Replaces an `mdx` tagged template literal with a document component. It also adds an `import` for `MDXTag`, but *doesn't* add an import for `React`.
88+
89+
```js
90+
import { mdx } from './mdx.macro'
91+
92+
const MyDocument = mdx`
93+
# Don't Panic
94+
95+
Since we decided a few weeks ago to adopt the leaf as legal tender, we have, of course, all become immensely rich.
96+
`
97+
98+
↓ ↓ ↓ ↓ ↓ ↓
99+
100+
import { MDXTag } from '@mdx-js/tag'
101+
102+
const MyDocument = ({ components, ...props }) => (
103+
<MDXTag name="wrapper" components={components}>
104+
<MDXTag name="h1" components={components}>{`Don't Panic`}</MDXTag>
105+
<MDXTag name="p" components={components}>
106+
<MDXTag
107+
name="em"
108+
components={components}
109+
parentName="p"
110+
>{`Since we decided a few weeks ago to adopt the leaf as legal tender, we have, of course, all become immensely rich.`}</MDXTag>
111+
</MDXTag>
112+
</MDXTag>
113+
)
114+
```
115+
116+
## Caveats
117+
118+
Currently, changes to imported files aren't detected, even within create-react-app. Any PR that fixes this would be welcome!
119+
120+
## License
121+
122+
MIT

__fixtures__/basic-example/code.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { mdx } from '../../mdx.macro'
2+
3+
const Document = mdx`
4+
import { test } from './test.js'
5+
6+
# Don't Panic
7+
8+
*Since we decided a few weeks ago to adopt the leaf as legal tender, we have, of course, all become immensely rich.*
9+
10+
<SomeComponent test={test} />
11+
`

__fixtures__/basic-example/output.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { MDXTag } from '@mdx-js/tag';
2+
import { test as _test } from './test.js';
3+
4+
const Document = ({
5+
components,
6+
...props
7+
}) => <MDXTag name="wrapper" components={components}>
8+
<MDXTag name="h1" components={components}>{`Don't Panic`}</MDXTag>
9+
<MDXTag name="p" components={components}><MDXTag name="em" components={components} parentName="p">{`Since we decided a few weeks ago to adopt the leaf as legal tender, we have, of course, all become immensely rich.`}</MDXTag></MDXTag>
10+
<SomeComponent test={_test} /></MDXTag>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { mdx } from '../../mdx.macro'
2+
3+
const DocumentA = mdx`
4+
import { test } from './test.js'
5+
6+
## This is some MDX source
7+
8+
<SomeComponent test={test} />
9+
`
10+
11+
const DocumentB = mdx`
12+
import { test } from './test.js'
13+
14+
## This is some MDX source
15+
16+
<SomeComponent test={test} />
17+
`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { MDXTag } from '@mdx-js/tag';
2+
import { test as _test2 } from './test.js';
3+
import { test as _test } from './test.js';
4+
5+
const DocumentA = ({
6+
components,
7+
...props
8+
}) => <MDXTag name="wrapper" components={components}>
9+
<MDXTag name="h2" components={components}>{`This is some MDX source`}</MDXTag>
10+
<SomeComponent test={_test} /></MDXTag>;
11+
12+
const DocumentB = ({
13+
components,
14+
...props
15+
}) => <MDXTag name="wrapper" components={components}>
16+
<MDXTag name="h2" components={components}>{`This is some MDX source`}</MDXTag>
17+
<SomeComponent test={_test2} /></MDXTag>;

mdx.macro.js

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
const fs = require('fs-extra');
2+
const path = require('path');
3+
const { createMacro } = require('babel-plugin-macros');
4+
const babelPresetReactApp = require('babel-preset-react-app');
5+
const findCacheDir = require('find-cache-dir');
6+
const revHash = require('rev-hash');
7+
const mdx = require('@mdx-js/mdx');
8+
const parse = require('@babel/parser').parse;
9+
const traverse = require('babel-traverse').default;
10+
11+
const cacheDir = findCacheDir({ name: 'mdx.macro' });
12+
const renamedSymbol = Symbol('renamed');
13+
14+
function writeTempFile(name, content) {
15+
let nameParts = path.basename(name).split('.');
16+
nameParts.splice(1, 0, revHash(content));
17+
nameParts.push('js');
18+
let pathname = path.resolve(cacheDir, nameParts.join('.'));
19+
fs.ensureDirSync(cacheDir);
20+
fs.writeFileSync(pathname, content);
21+
22+
// Remove the path up to and including `node_modules`, so that
23+
// create-react-app won't complain about the file being somewhere else.
24+
return pathname.replace(/^.*\/node_modules\//, '');
25+
}
26+
27+
function mdxMacro({ babel, references, state }) {
28+
let { importMDX = [], mdx = [] } = references;
29+
30+
importMDX.forEach((referencePath) => {
31+
if (referencePath.parentPath.type === 'CallExpression') {
32+
importAsync({ referencePath, state, babel });
33+
} else if (referencePath.parentPath.type === 'MemberExpression' && referencePath.parentPath.node.property.name === 'sync') {
34+
hasSyncReferences = true;
35+
importSync({ referencePath, state, babel });
36+
} else {
37+
throw new Error(`This is not supported: \`${referencePath.findParent(babel.types.isExpression).getSource()}\`. Please see the mdx.macro documentation`);
38+
}
39+
});
40+
41+
let hasInlineMDX = false;
42+
mdx.forEach((referencePath) => {
43+
hasInlineMDX = true;
44+
inlineMDX({ referencePath, state, babel });
45+
});
46+
47+
if (hasInlineMDX) {
48+
let program = state.file.path;
49+
let mdxTagImport = babel.transformSync(`import { MDXTag } from '@mdx-js/tag'`, {
50+
ast: true,
51+
filename: 'mdx.macro/mdxTagImport.js'
52+
});
53+
program.node.body.unshift(mdxTagImport.ast.program.body[0]);
54+
}
55+
}
56+
57+
function importAsync({ babel, referencePath, state }) {
58+
let { types: t } = babel;
59+
let {
60+
file: {
61+
opts: { filename }
62+
}
63+
} = state;
64+
let documentFilename = referencePath.parentPath.node.arguments[0].value;
65+
66+
let pathname = transform({ babel, filename, documentFilename });
67+
68+
// Replace the `importMDX.async()` call with a dynamic import()
69+
referencePath.parentPath.replaceWith(t.callExpression(t.import(), [t.stringLiteral(pathname)]));
70+
}
71+
72+
function importSync({ babel, referencePath, state }) {
73+
let { types: t } = babel;
74+
let {
75+
file: {
76+
opts: { filename }
77+
}
78+
} = state;
79+
let documentFilename = referencePath.parentPath.parentPath.node.arguments[0].value;
80+
81+
let pathname = transform({ babel, filename, documentFilename });
82+
let id = referencePath.scope.generateUidIdentifier(pathname);
83+
84+
// Add an import statement
85+
let program = state.file.path;
86+
program.node.body.unshift(t.importDeclaration([t.importDefaultSpecifier(id)], t.stringLiteral(pathname)));
87+
88+
// Replace the `importMDX.sync()` call with the imported binding
89+
referencePath.parentPath.parentPath.replaceWith(id);
90+
}
91+
92+
// Find the import filename,
93+
function transform({ babel, filename, documentFilename }) {
94+
if (!filename) {
95+
throw new Error(`You must pass a filename to importMDX(). Please see the mdx.macro documentation`);
96+
}
97+
let documentPath = path.join(filename, '..', documentFilename);
98+
let imports = `import React from 'react'\nimport { MDXTag } from '@mdx-js/tag'\n`;
99+
100+
// In development mode, we want to import the original document so that
101+
// changes will be picked up and cause a re-build.
102+
// Note: this relies on files with macros *not* being cached by babel.
103+
if (process.env.NODE_ENV === 'development') {
104+
imports += `import '${documentPath.replace(/\\/g, '\\\\')}' // ${documentPath}\n`;
105+
}
106+
107+
let source = fs.readFileSync(documentPath, 'utf8');
108+
let transformedSource = babel.transformSync(imports + mdx.sync(source), {
109+
presets: [babelPresetReactApp],
110+
filename: documentPath
111+
}).code;
112+
113+
return writeTempFile(documentPath, transformedSource);
114+
}
115+
116+
function inlineMDX({ babel, referencePath, state }) {
117+
let {
118+
file: {
119+
opts: { filename }
120+
}
121+
} = state;
122+
let program = state.file.path;
123+
124+
let rawCode = referencePath.parent.quasi.quasis[0].value.raw;
125+
let transformedSource = mdx.sync(rawCode).replace('export default', '');
126+
127+
// Need to parse the transformed source this way instead of
128+
// with babel.parse or babel.transform, as otherwise the
129+
// generated code has errors. I'm not sure why.
130+
let ast = parse(transformedSource, {
131+
plugins: ['jsx', 'objectRestSpread'],
132+
sourceType: 'module',
133+
sourceFilename: filename
134+
});
135+
136+
function visitImport(path) {
137+
let name = path.node.local.name;
138+
var binding = path.scope.getBinding(name);
139+
if (!binding) {
140+
return;
141+
}
142+
if (binding[renamedSymbol]) {
143+
return;
144+
}
145+
146+
path.scope.rename(name, referencePath.scope.generateUidIdentifier(name).name);
147+
binding[renamedSymbol] = true;
148+
}
149+
150+
// Rename any imports to unique identifiers to prevent
151+
// collisions between import names across multiple mdx tags
152+
traverse(ast, {
153+
ImportNamespaceSpecifier: visitImport,
154+
ImportDefaultSpecifier: visitImport,
155+
ImportSpecifier: visitImport
156+
});
157+
158+
ast.program.body.slice(0, -1).forEach((node) => program.node.body.unshift(node));
159+
referencePath.parentPath.replaceWith(ast.program.body[ast.program.body.length - 1]);
160+
}
161+
162+
module.exports = createMacro(mdxMacro);

0 commit comments

Comments
 (0)