diff --git a/README.md b/README.md
index 218c83b..4ced99a 100644
--- a/README.md
+++ b/README.md
@@ -388,6 +388,52 @@ function MDXPage({code}: {code: string}) {
}
```
+#### cwd
+
+Setting `cwd` (_current working directory_) to a directory will allow esbuild to
+resolve imports. This directory could be the directory the mdx content was read
+from or a directory that off-disk mdx should be _run_ in.
+
+_content/pages/demo.tsx_
+
+```typescript
+import * as React from 'react'
+
+function Demo() {
+ return
Neat demo!
+}
+
+export default Demo
+```
+
+_src/build.ts_
+
+```typescript
+import {bundleMDX} from 'mdx-bundler'
+
+const mdxSource = `
+---
+title: Example Post
+published: 2021-02-13
+description: This is some description
+---
+
+# Wahoo
+
+import Demo from './demo'
+
+Here's a **neat** demo:
+
+
+`.trim()
+
+const result = await bundleMDX(mdxSource, {
+ cwd: '/users/you/site/_content/pages',
+})
+
+const {code, frontmatter} = result
+```
+
### Component Substitution
MDX Bundler passes on
@@ -428,12 +474,11 @@ You can reference frontmatter meta or consts in the mdx content.
title: Example Post
---
-export const exampleImage = 'https://example.com/image.jpg';
+export const exampleImage = 'https://example.com/image.jpg'
# {frontmatter.title}
-
```
### Known Issues
diff --git a/other/150.png b/other/150.png
new file mode 100644
index 0000000..d90352c
Binary files /dev/null and b/other/150.png differ
diff --git a/other/sample-component.jsx b/other/sample-component.jsx
new file mode 100644
index 0000000..ffa245a
--- /dev/null
+++ b/other/sample-component.jsx
@@ -0,0 +1,13 @@
+import React from 'react'
+
+import image from './150.png'
+
+/** @type React.FC */
+export const Sample = () => {
+ return (
+
+
Sample!
+
+
+ )
+}
diff --git a/package.json b/package.json
index 99965f8..5579329 100644
--- a/package.json
+++ b/package.json
@@ -40,34 +40,36 @@
"validate": "kcd-scripts validate"
},
"dependencies": {
- "@babel/runtime": "^7.13.10",
- "@esbuild-plugins/node-resolve": "0.1.4",
+ "@babel/runtime": "^7.13.17",
+ "@esbuild-plugins/node-resolve": "^0.1.4",
"@fal-works/esbuild-plugin-global-externals": "^2.1.1",
- "esbuild": "^0.11.6",
- "gray-matter": "^4.0.2",
- "jsdom": "^16.5.2",
+ "esbuild": "^0.11.15",
+ "gray-matter": "^4.0.3",
+ "jsdom": "^16.5.3",
"remark-frontmatter": "^3.0.0",
"remark-mdx-frontmatter": "^1.0.1",
"uvu": "^0.5.1",
- "xdm": "^1.6.0"
+ "xdm": "^1.8.0"
},
"devDependencies": {
"@testing-library/react": "^11.2.6",
"@types/jsdom": "^16.2.10",
- "@types/react": "^17.0.3",
+ "@types/react": "^17.0.4",
"@types/react-dom": "^17.0.3",
"cross-env": "^7.0.3",
- "kcd-scripts": "^8.2.1",
+ "kcd-scripts": "^10.0.0",
"left-pad": "^1.3.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
+ "remark-mdx-images": "^1.0.2",
"typescript": "^4.2.4"
},
"eslintConfig": {
"extends": "./node_modules/kcd-scripts/eslint.js",
"rules": {
"import/extensions": "off",
- "@typescript-eslint/no-unsafe-assignment": "off"
+ "@typescript-eslint/no-unsafe-assignment": "off",
+ "max-lines-per-function": "off"
}
},
"eslintIgnore": [
diff --git a/src/__tests__/index.js b/src/__tests__/index.js
index 084a147..3df54f3 100644
--- a/src/__tests__/index.js
+++ b/src/__tests__/index.js
@@ -1,10 +1,10 @@
import './setup-tests.js'
-import path from 'path'
import {test} from 'uvu'
import * as assert from 'uvu/assert'
import React from 'react'
import rtl from '@testing-library/react'
import leftPad from 'left-pad'
+import {remarkMdxImages} from 'remark-mdx-images'
import {bundleMDX} from '../index.js'
import {getMDXComponent} from '../client.js'
@@ -148,7 +148,7 @@ import Demo from './demo'
assert.equal(
error.message,
`Build failed with 1 error:
-__mdx_bundler_fake_dir__${path.sep}index.mdx:2:17: error: [inMemory] Could not resolve "./demo" from the entry MDX file.`,
+__mdx_bundler_fake_dir__/index.mdx:2:17: error: Could not resolve "./demo"`,
)
})
@@ -168,7 +168,7 @@ import Demo from './demo'
assert.equal(
error.message,
`Build failed with 1 error:
-__mdx_bundler_fake_dir__${path.sep}demo.tsx:1:7: error: [inMemory] Could not resolve "./blah-blah" from "./demo.tsx"`,
+__mdx_bundler_fake_dir__/demo.tsx:1:7: error: Could not resolve "./blah-blah"`,
)
})
@@ -188,7 +188,7 @@ import Demo from './demo.blah'
assert.equal(
error.message,
`Build failed with 1 error:
-__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)`,
+__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)`,
)
})
@@ -251,4 +251,46 @@ import LeftPad from 'left-pad-js'
assert.match(container.innerHTML, 'this is left pad')
})
+test('require from current directory', async () => {
+ const mdxSource = `
+# Title
+
+import {Sample} from './other/sample-component'
+
+
+
+![A Sample Image](./other/150.png)
+`.trim()
+
+ const {code} = await bundleMDX(mdxSource, {
+ cwd: process.cwd(),
+ xdmOptions: (vFile, options) => {
+ options.remarkPlugins = [remarkMdxImages]
+
+ return options
+ },
+ esbuildOptions: options => {
+ options.loader = {
+ ...options.loader,
+ '.png': 'dataurl',
+ }
+
+ return options
+ },
+ })
+
+ const Component = getMDXComponent(code)
+
+ const {container} = render(React.createElement(Component))
+
+ assert.match(container.innerHTML, 'Sample!')
+ // Test that the React components image is imported correctly.
+ assert.match(container.innerHTML, 'img src="data:image/png')
+ // Test that the markdowns image is imported correctly.
+ assert.match(
+ container.innerHTML,
+ 'img alt="A Sample Image" src="data:image/png',
+ )
+})
+
test.run()
diff --git a/src/index.js b/src/index.js
index 84b3653..759f179 100644
--- a/src/index.js
+++ b/src/index.js
@@ -27,6 +27,7 @@ async function bundleMDX(
xdmOptions = (vfileCompatible, options) => options,
esbuildOptions = options => options,
globals = {},
+ cwd = path.join(process.cwd(), `__mdx_bundler_fake_dir__`),
} = {},
) {
// xdm is a native ESM, and we're running in a CJS context. This is the
@@ -38,14 +39,13 @@ async function bundleMDX(
// extract the frontmatter
const {data: frontmatter} = matter(mdxSource)
- const dir = path.join(process.cwd(), `__mdx_bundler_fake_dir__`)
- const entryPath = path.join(dir, './index.mdx')
+ const entryPath = path.join(cwd, './index.mdx')
/** @type Record */
const absoluteFiles = {[entryPath]: mdxSource}
for (const [filepath, fileCode] of Object.entries(files)) {
- absoluteFiles[path.join(dir, filepath)] = fileCode
+ absoluteFiles[path.join(cwd, filepath)] = fileCode
}
/** @type import('esbuild').Plugin */
@@ -53,66 +53,61 @@ async function bundleMDX(
name: 'inMemory',
setup(build) {
build.onResolve({filter: /.*/}, ({path: filePath, importer}) => {
- if (filePath === entryPath) return {path: filePath}
+ if (filePath === entryPath)
+ return {path: filePath, pluginData: {inMemory: true}}
const modulePath = path.resolve(path.dirname(importer), filePath)
- if (modulePath in absoluteFiles) return {path: modulePath}
+ if (modulePath in absoluteFiles)
+ return {path: modulePath, pluginData: {inMemory: true}}
for (const ext of ['.js', '.ts', '.jsx', '.tsx', '.json', '.mdx']) {
const fullModulePath = `${modulePath}${ext}`
- if (fullModulePath in absoluteFiles) return {path: fullModulePath}
+ if (fullModulePath in absoluteFiles)
+ return {path: fullModulePath, pluginData: {inMemory: true}}
}
- return {
- errors: [
- {
- text: `Could not resolve "${filePath}" from ${
- importer === entryPath
- ? 'the entry MDX file.'
- : `"${importer.replace(dir, '.')}"`
- }`,
- location: null,
- },
- ],
- }
+ // Return an empty object so that esbuild will handle resolving the file itself.
+ return {}
})
- build.onLoad(
- {filter: /__mdx_bundler_fake_dir__/},
- async ({path: filePath}) => {
- // the || .js allows people to exclude a file extension
- const fileType = (path.extname(filePath) || '.jsx').slice(1)
- const contents = absoluteFiles[filePath]
-
- switch (fileType) {
- case 'mdx': {
- /** @type import('xdm/lib/compile').VFileCompatible */
- const vFileCompatible = {
- path: filePath,
- contents,
- }
- const vfile = await compileMDX(
- vFileCompatible,
- xdmOptions(vFileCompatible, {
- jsx: true,
- remarkPlugins: [
- remarkFrontmatter,
- [remarkMdxFrontmatter, {name: 'frontmatter'}],
- ],
- }),
- )
- return {contents: vfile.toString(), loader: 'jsx'}
+ build.onLoad({filter: /.*/}, async ({path: filePath, pluginData}) => {
+ if (pluginData === undefined || !pluginData.inMemory) {
+ // Return an empty object so that esbuild will load & parse the file contents itself.
+ return {}
+ }
+
+ // the || .js allows people to exclude a file extension
+ const fileType = (path.extname(filePath) || '.jsx').slice(1)
+ const contents = absoluteFiles[filePath]
+
+ switch (fileType) {
+ case 'mdx': {
+ /** @type import('xdm/lib/compile').VFileCompatible */
+ const vFileCompatible = {
+ path: filePath,
+ contents,
}
- default: {
- return {
- contents,
- loader: /** @type import('esbuild').Loader */ (fileType),
- }
+ const vfile = await compileMDX(
+ vFileCompatible,
+ xdmOptions(vFileCompatible, {
+ jsx: true,
+ remarkPlugins: [
+ remarkFrontmatter,
+ [remarkMdxFrontmatter, {name: 'frontmatter'}],
+ ],
+ }),
+ )
+ return {contents: vfile.toString(), loader: 'jsx'}
+ }
+ default: {
+ return {
+ contents,
+ loader: /** @type import('esbuild').Loader */ (fileType),
}
}
- },
- )
+ }
+ })
},
}
diff --git a/src/types.d.ts b/src/types.d.ts
index efd09b6..173438e 100644
--- a/src/types.d.ts
+++ b/src/types.d.ts
@@ -105,4 +105,19 @@ type BundleMDXOptions = {
* ```
*/
globals?: Record
+ /**
+ * The current working directory for the mdx bundle. Supplying this allows
+ * esbuild to resolve paths itself instead of using `files`.
+ *
+ * This could be the directory the mdx content was read from or in the case
+ * of off-disk content a common root directory.
+ *
+ * @example
+ * ```
+ * bundleMDX(mdxString, {
+ * cwd: '/users/you/site/mdx_root'
+ * })
+ * ```
+ */
+ cwd?: string
}