Skip to content

Commit 1065373

Browse files
Arcathkentcdodds
andauthored
feat: allow files to be loaded directly (#29)
Co-authored-by: Kent C. Dodds <[email protected]>
1 parent 8303ada commit 1065373

File tree

7 files changed

+177
-65
lines changed

7 files changed

+177
-65
lines changed

README.md

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,52 @@ function MDXPage({code}: {code: string}) {
388388
}
389389
```
390390

391+
#### cwd
392+
393+
Setting `cwd` (_current working directory_) to a directory will allow esbuild to
394+
resolve imports. This directory could be the directory the mdx content was read
395+
from or a directory that off-disk mdx should be _run_ in.
396+
397+
_content/pages/demo.tsx_
398+
399+
```typescript
400+
import * as React from 'react'
401+
402+
function Demo() {
403+
return <div>Neat demo!</div>
404+
}
405+
406+
export default Demo
407+
```
408+
409+
_src/build.ts_
410+
411+
```typescript
412+
import {bundleMDX} from 'mdx-bundler'
413+
414+
const mdxSource = `
415+
---
416+
title: Example Post
417+
published: 2021-02-13
418+
description: This is some description
419+
---
420+
421+
# Wahoo
422+
423+
import Demo from './demo'
424+
425+
Here's a **neat** demo:
426+
427+
<Demo />
428+
`.trim()
429+
430+
const result = await bundleMDX(mdxSource, {
431+
cwd: '/users/you/site/_content/pages',
432+
})
433+
434+
const {code, frontmatter} = result
435+
```
436+
391437
### Component Substitution
392438

393439
MDX Bundler passes on
@@ -428,12 +474,11 @@ You can reference frontmatter meta or consts in the mdx content.
428474
title: Example Post
429475
---
430476

431-
export const exampleImage = 'https://example.com/image.jpg';
477+
export const exampleImage = 'https://example.com/image.jpg'
432478

433479
# {frontmatter.title}
434480

435481
<img src={exampleImage} alt="Image alt text" />
436-
437482
```
438483

439484
### Known Issues

other/150.png

373 Bytes
Loading

other/sample-component.jsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react'
2+
3+
import image from './150.png'
4+
5+
/** @type React.FC */
6+
export const Sample = () => {
7+
return (
8+
<div>
9+
<b>Sample!</b>
10+
<img src={image} />
11+
</div>
12+
)
13+
}

package.json

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,34 +40,36 @@
4040
"validate": "kcd-scripts validate"
4141
},
4242
"dependencies": {
43-
"@babel/runtime": "^7.13.10",
44-
"@esbuild-plugins/node-resolve": "0.1.4",
43+
"@babel/runtime": "^7.13.17",
44+
"@esbuild-plugins/node-resolve": "^0.1.4",
4545
"@fal-works/esbuild-plugin-global-externals": "^2.1.1",
46-
"esbuild": "^0.11.6",
47-
"gray-matter": "^4.0.2",
48-
"jsdom": "^16.5.2",
46+
"esbuild": "^0.11.15",
47+
"gray-matter": "^4.0.3",
48+
"jsdom": "^16.5.3",
4949
"remark-frontmatter": "^3.0.0",
5050
"remark-mdx-frontmatter": "^1.0.1",
5151
"uvu": "^0.5.1",
52-
"xdm": "^1.6.0"
52+
"xdm": "^1.8.0"
5353
},
5454
"devDependencies": {
5555
"@testing-library/react": "^11.2.6",
5656
"@types/jsdom": "^16.2.10",
57-
"@types/react": "^17.0.3",
57+
"@types/react": "^17.0.4",
5858
"@types/react-dom": "^17.0.3",
5959
"cross-env": "^7.0.3",
60-
"kcd-scripts": "^8.2.1",
60+
"kcd-scripts": "^10.0.0",
6161
"left-pad": "^1.3.0",
6262
"react": "^17.0.2",
6363
"react-dom": "^17.0.2",
64+
"remark-mdx-images": "^1.0.2",
6465
"typescript": "^4.2.4"
6566
},
6667
"eslintConfig": {
6768
"extends": "./node_modules/kcd-scripts/eslint.js",
6869
"rules": {
6970
"import/extensions": "off",
70-
"@typescript-eslint/no-unsafe-assignment": "off"
71+
"@typescript-eslint/no-unsafe-assignment": "off",
72+
"max-lines-per-function": "off"
7173
}
7274
},
7375
"eslintIgnore": [

src/__tests__/index.js

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import './setup-tests.js'
2-
import path from 'path'
32
import {test} from 'uvu'
43
import * as assert from 'uvu/assert'
54
import React from 'react'
65
import rtl from '@testing-library/react'
76
import leftPad from 'left-pad'
7+
import {remarkMdxImages} from 'remark-mdx-images'
88
import {bundleMDX} from '../index.js'
99
import {getMDXComponent} from '../client.js'
1010

@@ -148,7 +148,7 @@ import Demo from './demo'
148148
assert.equal(
149149
error.message,
150150
`Build failed with 1 error:
151-
__mdx_bundler_fake_dir__${path.sep}index.mdx:2:17: error: [inMemory] Could not resolve "./demo" from the entry MDX file.`,
151+
__mdx_bundler_fake_dir__/index.mdx:2:17: error: Could not resolve "./demo"`,
152152
)
153153
})
154154

@@ -168,7 +168,7 @@ import Demo from './demo'
168168
assert.equal(
169169
error.message,
170170
`Build failed with 1 error:
171-
__mdx_bundler_fake_dir__${path.sep}demo.tsx:1:7: error: [inMemory] Could not resolve "./blah-blah" from "./demo.tsx"`,
171+
__mdx_bundler_fake_dir__/demo.tsx:1:7: error: Could not resolve "./blah-blah"`,
172172
)
173173
})
174174

@@ -188,7 +188,7 @@ import Demo from './demo.blah'
188188
assert.equal(
189189
error.message,
190190
`Build failed with 1 error:
191-
__mdx_bundler_fake_dir__${path.sep}index.mdx:2:17: error: [JavaScript plugins] Invalid loader: "blah" (valid: js, jsx, ts, tsx, css, json, text, base64, dataurl, file, binary)`,
191+
__mdx_bundler_fake_dir__/index.mdx:2:17: error: [plugin: JavaScript plugins] Invalid loader: "blah" (valid: js, jsx, ts, tsx, css, json, text, base64, dataurl, file, binary)`,
192192
)
193193
})
194194

@@ -251,4 +251,46 @@ import LeftPad from 'left-pad-js'
251251
assert.match(container.innerHTML, 'this is left pad')
252252
})
253253

254+
test('require from current directory', async () => {
255+
const mdxSource = `
256+
# Title
257+
258+
import {Sample} from './other/sample-component'
259+
260+
<Sample />
261+
262+
![A Sample Image](./other/150.png)
263+
`.trim()
264+
265+
const {code} = await bundleMDX(mdxSource, {
266+
cwd: process.cwd(),
267+
xdmOptions: (vFile, options) => {
268+
options.remarkPlugins = [remarkMdxImages]
269+
270+
return options
271+
},
272+
esbuildOptions: options => {
273+
options.loader = {
274+
...options.loader,
275+
'.png': 'dataurl',
276+
}
277+
278+
return options
279+
},
280+
})
281+
282+
const Component = getMDXComponent(code)
283+
284+
const {container} = render(React.createElement(Component))
285+
286+
assert.match(container.innerHTML, 'Sample!')
287+
// Test that the React components image is imported correctly.
288+
assert.match(container.innerHTML, 'img src="data:image/png')
289+
// Test that the markdowns image is imported correctly.
290+
assert.match(
291+
container.innerHTML,
292+
'img alt="A Sample Image" src="data:image/png',
293+
)
294+
})
295+
254296
test.run()

src/index.js

Lines changed: 45 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ async function bundleMDX(
2727
xdmOptions = (vfileCompatible, options) => options,
2828
esbuildOptions = options => options,
2929
globals = {},
30+
cwd = path.join(process.cwd(), `__mdx_bundler_fake_dir__`),
3031
} = {},
3132
) {
3233
// xdm is a native ESM, and we're running in a CJS context. This is the
@@ -38,81 +39,75 @@ async function bundleMDX(
3839
// extract the frontmatter
3940
const {data: frontmatter} = matter(mdxSource)
4041

41-
const dir = path.join(process.cwd(), `__mdx_bundler_fake_dir__`)
42-
const entryPath = path.join(dir, './index.mdx')
42+
const entryPath = path.join(cwd, './index.mdx')
4343

4444
/** @type Record<string, string> */
4545
const absoluteFiles = {[entryPath]: mdxSource}
4646

4747
for (const [filepath, fileCode] of Object.entries(files)) {
48-
absoluteFiles[path.join(dir, filepath)] = fileCode
48+
absoluteFiles[path.join(cwd, filepath)] = fileCode
4949
}
5050

5151
/** @type import('esbuild').Plugin */
5252
const inMemoryPlugin = {
5353
name: 'inMemory',
5454
setup(build) {
5555
build.onResolve({filter: /.*/}, ({path: filePath, importer}) => {
56-
if (filePath === entryPath) return {path: filePath}
56+
if (filePath === entryPath)
57+
return {path: filePath, pluginData: {inMemory: true}}
5758

5859
const modulePath = path.resolve(path.dirname(importer), filePath)
5960

60-
if (modulePath in absoluteFiles) return {path: modulePath}
61+
if (modulePath in absoluteFiles)
62+
return {path: modulePath, pluginData: {inMemory: true}}
6163

6264
for (const ext of ['.js', '.ts', '.jsx', '.tsx', '.json', '.mdx']) {
6365
const fullModulePath = `${modulePath}${ext}`
64-
if (fullModulePath in absoluteFiles) return {path: fullModulePath}
66+
if (fullModulePath in absoluteFiles)
67+
return {path: fullModulePath, pluginData: {inMemory: true}}
6568
}
6669

67-
return {
68-
errors: [
69-
{
70-
text: `Could not resolve "${filePath}" from ${
71-
importer === entryPath
72-
? 'the entry MDX file.'
73-
: `"${importer.replace(dir, '.')}"`
74-
}`,
75-
location: null,
76-
},
77-
],
78-
}
70+
// Return an empty object so that esbuild will handle resolving the file itself.
71+
return {}
7972
})
8073

81-
build.onLoad(
82-
{filter: /__mdx_bundler_fake_dir__/},
83-
async ({path: filePath}) => {
84-
// the || .js allows people to exclude a file extension
85-
const fileType = (path.extname(filePath) || '.jsx').slice(1)
86-
const contents = absoluteFiles[filePath]
87-
88-
switch (fileType) {
89-
case 'mdx': {
90-
/** @type import('xdm/lib/compile').VFileCompatible */
91-
const vFileCompatible = {
92-
path: filePath,
93-
contents,
94-
}
95-
const vfile = await compileMDX(
96-
vFileCompatible,
97-
xdmOptions(vFileCompatible, {
98-
jsx: true,
99-
remarkPlugins: [
100-
remarkFrontmatter,
101-
[remarkMdxFrontmatter, {name: 'frontmatter'}],
102-
],
103-
}),
104-
)
105-
return {contents: vfile.toString(), loader: 'jsx'}
74+
build.onLoad({filter: /.*/}, async ({path: filePath, pluginData}) => {
75+
if (pluginData === undefined || !pluginData.inMemory) {
76+
// Return an empty object so that esbuild will load & parse the file contents itself.
77+
return {}
78+
}
79+
80+
// the || .js allows people to exclude a file extension
81+
const fileType = (path.extname(filePath) || '.jsx').slice(1)
82+
const contents = absoluteFiles[filePath]
83+
84+
switch (fileType) {
85+
case 'mdx': {
86+
/** @type import('xdm/lib/compile').VFileCompatible */
87+
const vFileCompatible = {
88+
path: filePath,
89+
contents,
10690
}
107-
default: {
108-
return {
109-
contents,
110-
loader: /** @type import('esbuild').Loader */ (fileType),
111-
}
91+
const vfile = await compileMDX(
92+
vFileCompatible,
93+
xdmOptions(vFileCompatible, {
94+
jsx: true,
95+
remarkPlugins: [
96+
remarkFrontmatter,
97+
[remarkMdxFrontmatter, {name: 'frontmatter'}],
98+
],
99+
}),
100+
)
101+
return {contents: vfile.toString(), loader: 'jsx'}
102+
}
103+
default: {
104+
return {
105+
contents,
106+
loader: /** @type import('esbuild').Loader */ (fileType),
112107
}
113108
}
114-
},
115-
)
109+
}
110+
})
116111
},
117112
}
118113

src/types.d.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,19 @@ type BundleMDXOptions = {
105105
* ```
106106
*/
107107
globals?: Record<string, string | ModuleInfo>
108+
/**
109+
* The current working directory for the mdx bundle. Supplying this allows
110+
* esbuild to resolve paths itself instead of using `files`.
111+
*
112+
* This could be the directory the mdx content was read from or in the case
113+
* of off-disk content a common root directory.
114+
*
115+
* @example
116+
* ```
117+
* bundleMDX(mdxString, {
118+
* cwd: '/users/you/site/mdx_root'
119+
* })
120+
* ```
121+
*/
122+
cwd?: string
108123
}

0 commit comments

Comments
 (0)