From 6a8b3a7307051eab22848351be24a7494de64764 Mon Sep 17 00:00:00 2001
From: Adam Laycock <adam@arcath.net>
Date: Thu, 18 Nov 2021 03:54:48 +0000
Subject: [PATCH] feat: merge `bundleMDX` and `bundleMDXFile` (#125)

BREAKING CHANGE: The first argument to `bundleMDX` has been removed in favor of a `source` option.
---
 README.md              | 138 +++++++++++++++++-----
 package.json           |  10 +-
 src/__tests__/index.js | 136 ++++++++++++----------
 src/index.js           | 251 +++++++++++++++++++----------------------
 src/types.d.ts         |  75 ++++++++++--
 5 files changed, 366 insertions(+), 244 deletions(-)

diff --git a/README.md b/README.md
index 423d87d..09f4373 100644
--- a/README.md
+++ b/README.md
@@ -187,11 +187,13 @@ the esbuild version mdx-bundler uses.
 - [Installation](#installation)
 - [Usage](#usage)
   - [Options](#options)
+  - [Returns](#returns)
+  - [Types](#types)
   - [Component Substitution](#component-substitution)
   - [Frontmatter and const](#frontmatter-and-const)
   - [Accessing named exports](#accessing-named-exports)
   - [Image Bundling](#image-bundling)
-  - [bundleMDXFile](#bundlemdxfile)
+  - [Bundling a file.](#bundling-a-file)
   - [Known Issues](#known-issues)
 - [Inspiration](#inspiration)
 - [Other Solutions](#other-solutions)
@@ -236,7 +238,8 @@ Here's a **neat** demo:
 <Demo />
 `.trim()
 
-const result = await bundleMDX(mdxSource, {
+const result = await bundleMDX({
+  source: mdxSource,
   files: {
     './demo.tsx': `
 import * as React from 'react'
@@ -297,6 +300,19 @@ Ultimately, this gets rendered (basically):
 
 ### Options
 
+#### source
+
+The `string` source of your MDX.
+
+_Can not be set if `file` is set_
+
+#### file
+
+The path to the file on your disk with the MDX in. You will probabbly want to
+set [cwd](#cwd) as well.
+
+_Can not be set if `source` is set_
+
 #### files
 
 The `files` config is an object of all the files you're bundling. The key is the
@@ -311,9 +327,12 @@ This allows you to modify the built-in xdm configuration (passed to the xdm
 esbuild plugin). This can be helpful for specifying your own
 remarkPlugins/rehypePlugins.
 
+The function is passed the default xdmOptions and the frontmatter.
+
 ```ts
-bundleMDX(mdxString, {
-  xdmOptions(options) {
+bundleMDX({
+  source: mdxSource,
+  xdmOptions(options, frontmatter) {
     // this is the recommended way to add custom remark/rehype plugins:
     // The syntax might look weird, but it protects you in case we add/remove
     // plugins in the future.
@@ -328,12 +347,13 @@ bundleMDX(mdxString, {
 #### esbuildOptions
 
 You can customize any of esbuild options with the option `esbuildOptions`. This
-takes a function which is passed the default esbuild options and expects an
-options object to be returned.
+takes a function which is passed the default esbuild options and the frontmatter
+and expects an options object to be returned.
 
 ```typescript
-bundleMDX(mdxSource, {
-  esbuildOptions(options) {
+bundleMDX({
+  source: mdxSource,
+  esbuildOptions(options, frontmatter) {
     options.minify = false
     options.target = [
       'es2020',
@@ -366,7 +386,8 @@ and once for this MDX component). This is wasteful and you'd be better off just
 telling esbuild to _not_ bundle `d3` and you can pass it to the component
 yourself when you call `getMDXComponent`.
 
-Global external configuration options: https://www.npmjs.com/package/@fal-works/esbuild-plugin-global-externals
+Global external configuration options:
+https://www.npmjs.com/package/@fal-works/esbuild-plugin-global-externals
 
 Here's an example:
 
@@ -382,7 +403,8 @@ import leftPad from 'left-pad'
 <div>{leftPad("Neat demo!", 12, '!')}</div>
 `.trim()
 
-const result = await bundleMDX(mdxSource, {
+const result = await bundleMDX({
+  source: mdxSource,
   // NOTE: this is *only* necessary if you want to share deps between your MDX
   // file bundle and the host app. Otherwise, all deps will just be bundled.
   // So it'll work either way, this is just an optimization to avoid sending
@@ -449,7 +471,8 @@ Here's a **neat** demo:
 <Demo />
 `.trim()
 
-const result = await bundleMDX(mdxSource, {
+const result = await bundleMDX({
+  source: mdxSource,
   cwd: '/users/you/site/_content/pages',
 })
 
@@ -465,7 +488,7 @@ Your function is passed the current gray-matter configuration for you to modify.
 Return your modified configuration object for gray matter.
 
 ```js
-bundleMDX(mdxString, {
+bundleMDX({
   grayMatterOptions: options => {
     options.excerpt = true
 
@@ -474,6 +497,46 @@ bundleMDX(mdxString, {
 })
 ```
 
+#### bundleDirectory & bundlePath
+
+This allows you to set the output directory for the bundle and the public URL to
+the directory. If one option is set the other must be aswell.
+
+_The Javascript bundle is not written to this directory and is still returned as
+a string from `bundleMDX`._
+
+This feature is best used with tweaks to `xdmOptions` and `esbuildOptions`. In
+the example below and `.png` files are written to the disk and then served from
+`/file/`.
+
+This allows you to store assets with your MDX and then have esbuild process them
+like anything else.
+
+_It is reccomended that each bundle has its own `bundleDirectory` so that
+multiple bundles don't overwrite each others assets._
+
+```ts
+const {code} = await bundleMDX({
+  file: '/path/to/site/content/file.mdx',
+  cwd: '/path/to/site/content',
+  bundleDirectory: '/path/to/site/public/file,
+  bundlePath: '/file/',
+  xdmOptions: options => {
+    options.remarkPlugins = [remarkMdxImages]
+
+    return options
+  },
+  esbuildOptions: options => {
+    options.loader = {
+      ...options.loader,
+      '.png': 'file',
+    }
+
+    return options
+  },
+})
+```
+
 ### Returns
 
 `bundleMDX` returns a promise for an object with the following properties.
@@ -483,6 +546,21 @@ bundleMDX(mdxString, {
 - `matter` - The whole
   [object returned by gray-matter](https://github.com/jonschlinkert/gray-matter#returned-object)
 
+### Types
+
+`mdx-bundler` supplies complete typings within its own package.
+
+`bundleMDX` has a single type parameter which is the type of your frontmatter.
+It defaults to `{[key: string]: any}` and must be an object. This is then used
+to type the returned `frontmatter` and the frontmatter passed to
+`esbuildOptions` and `xdmOptions`.
+
+```ts
+const {frontmatter} = bundleMDX<{title: string}>({source})
+
+frontmatter.title // has type string
+```
+
 ### Component Substitution
 
 MDX Bundler passes on
@@ -532,17 +610,16 @@ export const exampleImage = 'https://example.com/image.jpg'
 
 ### Accessing named exports
 
-You can use `getMDXExport` instead of `getMDXComponent` to treat the mdx file as a module instead of just a component.
-It takes the same arguments that `getMDXComponent` does.
+You can use `getMDXExport` instead of `getMDXComponent` to treat the mdx file as
+a module instead of just a component. It takes the same arguments that
+`getMDXComponent` does.
 
 ```mdx
 ---
 title: Example Post
 ---
 
-export const toc = [
-  { depth: 1, value: 'The title' }
-]
+export const toc = [{depth: 1, value: 'The title'}]
 
 # The title
 ```
@@ -560,6 +637,7 @@ function MDXPage({code}: {code: string}) {
   return <Component />
 }
 ```
+
 ### Image Bundling
 
 With the [cwd](#cwd) and the remark plugin
@@ -572,7 +650,8 @@ which outputs the images as inline data urls in the returned code.
 ```js
 import {remarkMdxImages} from 'remark-mdx-images'
 
-const {code} = await bundleMDX(mdxSource, {
+const {code} = await bundleMDX({
+  source: mdxSource,
   cwd: '/users/you/site/_content/pages',
   xdmOptions: options => {
     options.remarkPlugins = [...(options.remarkPlugins ?? []), remarkMdxImages]
@@ -602,7 +681,8 @@ folder to be used in image sources.
 ```js
 // For the file `_content/pages/about.mdx`
 
-const {code} = await bundleMDX(mdxSource, {
+const {code} = await bundleMDX({
+  source: mdxSource,
   cwd: '/users/you/site/_content/pages',
   xdmOptions: options => {
     options.remarkPlugins = [...(options.remarkPlugins ?? []), remarkMdxImages]
@@ -628,24 +708,22 @@ const {code} = await bundleMDX(mdxSource, {
 })
 ```
 
-### bundleMDXFile
+### Bundling a file.
 
 If your MDX file is on your disk you can save some time and code by having
-`esbuild` read the file for you. To do this mdx-bundler provides the function
-`bundleMDXFile` which works the same as `bundleMDX` except it's first option is
-the path to the mdx file instead of the mdx source.
+`mdx-bundler` read the file for you. Instead of supplying a `source` string you
+can set `file` to the path of the MDX on disk. Set `cwd` to it's folder so that
+relative imports work.
 
 ```js
-import {bundleMDXFile} from 'mdx-bundler'
+import {bundleMDX} from 'mdx-bundler'
 
-const {code, frontmatter} = await bundleMDXFile(
-  '/users/you/site/content/file.mdx',
-)
+const {code, frontmatter} = await bundleMDX({
+  file: '/users/you/site/content/file.mdx',
+  cwd: '/users/you/site/content/',
+})
 ```
 
-`cwd` will be automatically set to the `dirname` of the given file path, you can
-still override this. All other options work the same as they do for `bundleMDX`.
-
 ### Known Issues
 
 #### Cloudflare Workers
diff --git a/package.json b/package.json
index eca93b1..2e70444 100644
--- a/package.json
+++ b/package.json
@@ -40,14 +40,14 @@
     "validate": "kcd-scripts validate"
   },
   "dependencies": {
-    "@babel/runtime": "^7.15.4",
+    "@babel/runtime": "^7.16.3",
     "@esbuild-plugins/node-resolve": "^0.1.4",
     "@fal-works/esbuild-plugin-global-externals": "^2.1.2",
     "gray-matter": "^4.0.3",
     "remark-frontmatter": "^4.0.1",
-    "remark-mdx-frontmatter": "^1.0.1",
+    "remark-mdx-frontmatter": "^1.1.1",
     "uuid": "^8.3.2",
-    "xdm": "^3.2.0"
+    "xdm": "^3.3.0"
   },
   "peerDependencies": {
     "esbuild": "0.11.x || 0.12.x || 0.13.x"
@@ -61,8 +61,8 @@
     "@types/uuid": "^8.3.1",
     "c8": "^7.10.0",
     "cross-env": "^7.0.3",
-    "esbuild": "^0.13.12",
-    "jsdom": "^18.0.1",
+    "esbuild": "^0.13.13",
+    "jsdom": "^18.1.0",
     "kcd-scripts": "^11.2.2",
     "left-pad": "^1.3.0",
     "mdx-test-data": "^1.0.1",
diff --git a/src/__tests__/index.js b/src/__tests__/index.js
index bbfa9e2..6f78d09 100644
--- a/src/__tests__/index.js
+++ b/src/__tests__/index.js
@@ -1,13 +1,12 @@
 import './setup-tests.js'
 import path from 'path'
-import {fileURLToPath} from 'url'
 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, bundleMDXFile} from '../index.js'
+import {bundleMDX} from '../index.js'
 import {getMDXComponent, getMDXExport} from '../client.js'
 
 const {render} = rtl
@@ -29,7 +28,8 @@ Here's a **neat** demo:
 <Demo />
 `.trim()
 
-  const result = await bundleMDX(mdxSource, {
+  const result = await bundleMDX({
+    source: mdxSource,
     files: {
       './demo.tsx': `
 import * as React from 'react'
@@ -120,7 +120,8 @@ import Demo from './demo'
 <Demo />
   `.trim()
 
-  const result = await bundleMDX(mdxSource, {
+  const result = await bundleMDX({
+    source: mdxSource,
     files: {
       './demo.tsx': `
 import leftPad from 'left-pad'
@@ -144,7 +145,8 @@ import Demo from './demo'
   `.trim()
 
   const error = /** @type Error */ (
-    await bundleMDX(mdxSource, {
+    await bundleMDX({
+      source: mdxSource,
       files: {},
     }).catch(e => e)
   )
@@ -160,7 +162,8 @@ import Demo from './demo'
   `.trim()
 
   const error = /** @type Error */ (
-    await bundleMDX(mdxSource, {
+    await bundleMDX({
+      source: mdxSource,
       files: {
         './demo.tsx': `import './blah-blah'`,
       },
@@ -182,7 +185,8 @@ import Demo from './demo.blah'
   `.trim()
 
   const error = /** @type Error */ (
-    await bundleMDX(mdxSource, {
+    await bundleMDX({
+      source: mdxSource,
       files: {
         './demo.blah': `what even is this?`,
       },
@@ -196,7 +200,7 @@ import Demo from './demo.blah'
 })
 
 test('files is optional', async () => {
-  await bundleMDX('hello')
+  await bundleMDX({source: 'hello'})
 })
 
 test('uses the typescript loader where needed', async () => {
@@ -206,7 +210,8 @@ import Demo from './demo'
 <Demo />
   `.trim()
 
-  const {code} = await bundleMDX(mdxSource, {
+  const {code} = await bundleMDX({
+    source: mdxSource,
     files: {
       './demo.tsx': `
 import * as React from 'react'
@@ -241,7 +246,8 @@ import LeftPad from 'left-pad-js'
 <LeftPad padding={4} string="^">Hi</LeftPad>
   `.trim()
 
-  const {code} = await bundleMDX(mdxSource, {
+  const {code} = await bundleMDX({
+    source: mdxSource,
     files: {
       'left-pad-js': `export default () => <div>this is left pad</div>`,
     },
@@ -273,7 +279,8 @@ export const Demo: React.FC = () => {
     `.trim(),
   }
 
-  const {code} = await bundleMDX(mdxSource, {
+  const {code} = await bundleMDX({
+    source: mdxSource,
     files,
     esbuildOptions: options => {
       options.loader = {
@@ -303,7 +310,8 @@ import {Sample} from './sample-component'
 ![A Sample Image](./150.png)
 `.trim()
 
-  const {code} = await bundleMDX(mdxSource, {
+  const {code} = await bundleMDX({
+    source: mdxSource,
     cwd: path.join(process.cwd(), 'other'),
     xdmOptions: options => {
       options.remarkPlugins = [remarkMdxImages]
@@ -341,21 +349,21 @@ test('should output assets', async () => {
 ![Sample Image](./150.png)
   `.trim()
 
-  const {code} = await bundleMDX(mdxSource, {
+  const {code} = await bundleMDX({
+    source: mdxSource,
     cwd: path.join(process.cwd(), 'other'),
+    bundleDirectory: path.join(process.cwd(), 'output'),
+    bundlePath: '/img/',
     xdmOptions: options => {
       options.remarkPlugins = [remarkMdxImages]
 
       return options
     },
     esbuildOptions: options => {
-      options.outdir = path.join(process.cwd(), 'output')
       options.loader = {
         ...options.loader,
         '.png': 'file',
       }
-      options.publicPath = '/img/'
-      options.write = true
 
       return options
     },
@@ -367,8 +375,9 @@ test('should output assets', async () => {
 
   assert.match(container.innerHTML, 'src="/img/150')
 
-  const error = /** @type Error */ (
-    await bundleMDX(mdxSource, {
+  const writeError = /** @type Error */ (
+    await bundleMDX({
+      source: mdxSource,
       cwd: path.join(process.cwd(), 'other'),
       xdmOptions: options => {
         options.remarkPlugins = [remarkMdxImages]
@@ -389,9 +398,22 @@ test('should output assets', async () => {
   )
 
   assert.equal(
-    error.message,
+    writeError.message,
     "You must either specify `write: false` or `write: true` and `outdir: '/path'` in your esbuild options",
   )
+
+  const optionError = /** @type Error */ (
+    await bundleMDX({
+      source: mdxSource,
+      cwd: path.join(process.cwd(), 'other'),
+      bundleDirectory: path.join(process.cwd(), 'output'),
+    }).catch(e => e)
+  )
+
+  assert.equal(
+    optionError.message,
+    'When using `bundleDirectory` or `bundlePath` the other must be set.',
+  )
 })
 
 test('should support importing named exports', async () => {
@@ -407,7 +429,7 @@ export const uncle = 'Bob'
 # {uncle} was indeed the uncle
 `.trim()
 
-  const result = await bundleMDX(mdxSource)
+  const result = await bundleMDX({source: mdxSource})
 
   /** @type {import('../types').MDXExport<{uncle: string}, {title: string, published: Date, description: string}>} */
   const mdxExport = getMDXExport(result.code)
@@ -435,7 +457,7 @@ Local Content
 <MdxData />
   `.trim()
 
-  const {code} = await bundleMDX(mdxSource, {})
+  const {code} = await bundleMDX({source: mdxSource})
 
   const Component = getMDXComponent(code)
 
@@ -447,32 +469,6 @@ Local Content
   )
 })
 
-test('should support over-riding the entry point', async () => {
-  const {code} = await bundleMDX('', {
-    cwd: process.cwd(),
-    esbuildOptions: options => {
-      options.entryPoints = [
-        path.join(
-          path.dirname(fileURLToPath(import.meta.url)),
-          '..',
-          '..',
-          'CONTRIBUTING.md',
-        ),
-      ]
-      options.outdir = path.join(process.cwd(), 'output')
-      options.write = true
-
-      return options
-    },
-  })
-
-  const Component = getMDXComponent(code)
-
-  const {container} = render(React.createElement(Component))
-
-  assert.match(container.innerHTML, 'Thanks for being willing to contribute')
-})
-
 test('should work with react-dom api', async () => {
   const mdxSource = `
 import Demo from './demo'
@@ -480,7 +476,8 @@ import Demo from './demo'
 <Demo />
   `.trim()
 
-  const result = await bundleMDX(mdxSource, {
+  const result = await bundleMDX({
+    source: mdxSource,
     files: {
       './demo.tsx': `
 import * as ReactDOM from 'react-dom'
@@ -521,7 +518,8 @@ This is the rest of the content
 
   `.trim()
 
-  const {matter} = await bundleMDX(mdxSource, {
+  const {matter} = await bundleMDX({
+    source: mdxSource,
     grayMatterOptions: options => {
       options.excerpt = true
 
@@ -532,22 +530,38 @@ This is the rest of the content
   assert.equal((matter.excerpt ? matter.excerpt : '').trim(), 'Some excerpt')
 })
 
-test('specify a file using bundleMDXFile', async () => {
-  const {frontmatter} = await bundleMDXFile(
-    path.join(process.cwd(), 'other', 'sample.mdx'),
-    {
-      esbuildOptions: options => {
-        options.loader = {
-          ...options.loader,
-          '.png': 'dataurl',
-        }
+test('specify a file using bundleMDX', async () => {
+  const {frontmatter} = await bundleMDX({
+    file: path.join(process.cwd(), 'other', 'sample.mdx'),
+    cwd: path.join(process.cwd(), 'other'),
+    esbuildOptions: options => {
+      options.loader = {
+        ...options.loader,
+        '.png': 'dataurl',
+      }
 
-        return options
-      },
+      return options
     },
-  )
+  })
 
   assert.equal(frontmatter.title, 'Sample')
 })
 
+test('let you use the front matter in config', async () => {
+  await bundleMDX({
+    file: path.join(process.cwd(), 'other', 'sample.mdx'),
+    cwd: path.join(process.cwd(), 'other'),
+    esbuildOptions: (options, frontmatter) => {
+      assert.equal(frontmatter.title, 'Sample')
+
+      options.loader = {
+        ...options.loader,
+        '.png': 'dataurl',
+      }
+
+      return options
+    },
+  })
+})
+
 test.run()
diff --git a/src/index.js b/src/index.js
index 01bab54..0896559 100644
--- a/src/index.js
+++ b/src/index.js
@@ -12,22 +12,22 @@ import dirnameMessedUp from './dirname-messed-up.cjs'
 const {readFile, unlink} = fs.promises
 
 /**
- *
- * @param {string} mdxSource - A string of mdx source code
- * @param {import('./types').BundleMDXOptions} options
+ * @template {{[key: string]: any}} Frontmatter
+ * @param {import('./types').BundleMDX<Frontmatter>} options
  * @returns
  */
-async function bundleMDX(
-  mdxSource,
-  {
-    files = {},
-    xdmOptions = options => options,
-    esbuildOptions = options => options,
-    globals = {},
-    cwd = path.join(process.cwd(), `__mdx_bundler_fake_dir__`),
-    grayMatterOptions = options => options,
-  } = {},
-) {
+async function bundleMDX({
+  file,
+  source,
+  files = {},
+  xdmOptions = options => options,
+  esbuildOptions = options => options,
+  globals = {},
+  cwd = path.join(process.cwd(), `__mdx_bundler_fake_dir__`),
+  grayMatterOptions = options => options,
+  bundleDirectory,
+  bundlePath,
+}) {
   /* c8 ignore start */
   if (dirnameMessedUp && !process.env.ESBUILD_BINARY_PATH) {
     console.warn(
@@ -38,18 +38,45 @@ async function bundleMDX(
 
   // xdm is a native ESM, and we're running in a CJS context. This is the
   // only way to import ESM within CJS
-  const [
-    {default: xdmESBuild},
-    {default: remarkFrontmatter},
-  ] = await Promise.all([
-    import('xdm/esbuild.js'),
-    import('remark-frontmatter'),
-  ])
+  const [{default: xdmESBuild}, {default: remarkFrontmatter}] =
+    await Promise.all([import('xdm/esbuild.js'), import('remark-frontmatter')])
 
-  const entryPath = path.join(cwd, `./_mdx_bundler_entry_point-${uuid()}.mdx`)
+  let /** @type string */ code,
+    /** @type string */ entryPath,
+    /** @type Omit<grayMatter.GrayMatterFile<string>, "data"> & {data: Frontmatter} */ matter
 
   /** @type Record<string, string> */
-  const absoluteFiles = {[entryPath]: mdxSource}
+  const absoluteFiles = {}
+
+  const isWriting = typeof bundleDirectory === 'string'
+
+  if (typeof bundleDirectory !== typeof bundlePath) {
+    throw new Error(
+      'When using `bundleDirectory` or `bundlePath` the other must be set.',
+    )
+  }
+
+  if (typeof source === 'string') {
+    // The user has supplied MDX source.
+    /** @type any */ // Slight type hack to get the graymatter front matter typed correctly.
+    const gMatter = grayMatter(source, grayMatterOptions({}))
+    matter = gMatter
+    entryPath = path.join(cwd, `./_mdx_bundler_entry_point-${uuid()}.mdx`)
+    absoluteFiles[entryPath] = source
+  } else if (typeof file === 'string') {
+    // The user has supplied a file.
+    /** @type any */ // Slight type hack to get the graymatter front matter typed correctly.
+    const gMatter = grayMatter.read(file, grayMatterOptions({}))
+    matter = gMatter
+    entryPath = file
+    /* c8 ignore start */
+  } else {
+    // The user supplied neither file or source.
+    // The typings should prevent reaching this point.
+    // It is ignored from coverage as the tests wouldn't run in a way that can get here.
+    throw new Error('`source` or `file` must be defined')
+  }
+  /* c8 ignore end*/
 
   for (const [filepath, fileCode] of Object.entries(files)) {
     absoluteFiles[path.join(cwd, filepath)] = fileCode
@@ -125,136 +152,86 @@ async function bundleMDX(
     },
   }
 
-  const buildOptions = esbuildOptions({
-    entryPoints: [entryPath],
-    write: false,
-    absWorkingDir: cwd,
-    define: {
-      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
-    },
-    plugins: [
-      globalExternals({
-        ...globals,
-        react: {
-          varName: 'React',
-          type: 'cjs',
-        },
-        'react-dom': {
-          varName: 'ReactDOM',
-          type: 'cjs',
-        },
-        'react/jsx-runtime': {
-          varName: '_jsx_runtime',
-          type: 'cjs',
-        },
-      }),
-      // eslint-disable-next-line @babel/new-cap
-      NodeResolvePlugin({
-        extensions: ['.js', '.ts', '.jsx', '.tsx'],
-        resolveOptions: {basedir: cwd},
-      }),
-      inMemoryPlugin,
-      xdmESBuild(
-        xdmOptions({
-          remarkPlugins: [
-            remarkFrontmatter,
-            [remarkMdxFrontmatter, {name: 'frontmatter'}],
-          ],
+  const buildOptions = esbuildOptions(
+    {
+      entryPoints: [entryPath],
+      write: isWriting,
+      outdir: isWriting ? bundleDirectory : undefined,
+      publicPath: isWriting ? bundlePath : undefined,
+      absWorkingDir: cwd,
+      define: {
+        'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
+      },
+      plugins: [
+        globalExternals({
+          ...globals,
+          react: {
+            varName: 'React',
+            type: 'cjs',
+          },
+          'react-dom': {
+            varName: 'ReactDOM',
+            type: 'cjs',
+          },
+          'react/jsx-runtime': {
+            varName: '_jsx_runtime',
+            type: 'cjs',
+          },
         }),
-      ),
-    ],
-    bundle: true,
-    format: 'iife',
-    globalName: 'Component',
-    minify: true,
-  })
-
-  // Extract the front matter from the source or the entry point
-
-  /** @type grayMatter.GrayMatterFile<any> */
-  let matter
-
-  // We have to be a bit specific here to ensure type safety
-  if (
-    buildOptions.entryPoints &&
-    Array.isArray(buildOptions.entryPoints) &&
-    buildOptions.entryPoints[0] !== entryPath
-  ) {
-    //The user has replaced the entrypoint, we can assume this means `mdxSource` is empty
-
-    matter = grayMatter.read(buildOptions.entryPoints[0], grayMatterOptions({}))
-  } else {
-    matter = grayMatter(mdxSource, grayMatterOptions({}))
-  }
+        // eslint-disable-next-line @babel/new-cap
+        NodeResolvePlugin({
+          extensions: ['.js', '.ts', '.jsx', '.tsx'],
+          resolveOptions: {basedir: cwd},
+        }),
+        inMemoryPlugin,
+        xdmESBuild(
+          xdmOptions(
+            {
+              remarkPlugins: [
+                remarkFrontmatter,
+                [remarkMdxFrontmatter, {name: 'frontmatter'}],
+              ],
+            },
+            matter.data,
+          ),
+        ),
+      ],
+      bundle: true,
+      format: 'iife',
+      globalName: 'Component',
+      minify: true,
+    },
+    matter.data,
+  )
 
   const bundled = await esbuild.build(buildOptions)
 
   if (bundled.outputFiles) {
     const decoder = new StringDecoder('utf8')
 
-    const code = decoder.write(Buffer.from(bundled.outputFiles[0].contents))
-
-    return {
-      code: `${code};return Component;`,
-      frontmatter: matter.data,
-      errors: bundled.errors,
-      matter,
-    }
-  }
-
-  if (buildOptions.outdir && buildOptions.write) {
+    code = decoder.write(Buffer.from(bundled.outputFiles[0].contents))
+  } else if (buildOptions.outdir && buildOptions.write) {
     // We know that this has to be an array of entry point strings, with a single entry
     const entryFile = /** @type {{entryPoints: string[]}} */ (buildOptions)
       .entryPoints[0]
 
     const fileName = path.basename(entryFile).replace(/\.[^/.]+$/, '.js')
 
-    const code = await readFile(path.join(buildOptions.outdir, fileName))
+    code = (await readFile(path.join(buildOptions.outdir, fileName))).toString()
 
     await unlink(path.join(buildOptions.outdir, fileName))
-
-    return {
-      code: `${code};return Component`,
-      frontmatter: matter.data,
-      errors: bundled.errors,
-      matter,
-    }
+  } else {
+    throw new Error(
+      "You must either specify `write: false` or `write: true` and `outdir: '/path'` in your esbuild options",
+    )
   }
 
-  throw new Error(
-    "You must either specify `write: false` or `write: true` and `outdir: '/path'` in your esbuild options",
-  )
-}
-
-/**
- *
- * @param {string} mdxPath - The file path to bundle.
- * @param {import('./types').BundleMDXOptions} options
- * @returns
- */
-async function bundleMDXFile(
-  mdxPath,
-  {
-    files = {},
-    xdmOptions = options => options,
-    esbuildOptions = options => options,
-    globals = {},
-    cwd,
-    grayMatterOptions = options => options,
-  } = {},
-) {
-  return bundleMDX('', {
-    files,
-    xdmOptions,
-    esbuildOptions: options => {
-      options.entryPoints = [mdxPath]
-
-      return esbuildOptions(options)
-    },
-    globals,
-    cwd: cwd ? cwd : path.dirname(mdxPath),
-    grayMatterOptions,
-  })
+  return {
+    code: `${code};return Component;`,
+    frontmatter: matter.data,
+    errors: bundled.errors,
+    matter,
+  }
 }
 
-export {bundleMDX, bundleMDXFile}
+export {bundleMDX}
diff --git a/src/types.d.ts b/src/types.d.ts
index 867f946..011236d 100644
--- a/src/types.d.ts
+++ b/src/types.d.ts
@@ -7,17 +7,38 @@
 import type {Plugin, BuildOptions, Loader} from 'esbuild'
 import type {ModuleInfo} from '@fal-works/esbuild-plugin-global-externals'
 import type {CoreProcessorOptions} from 'xdm/lib/compile'
-import type {GrayMatterOption, Input} from 'gray-matter'
+import type {GrayMatterOption, Input, GrayMatterFile} from 'gray-matter'
 
 type ESBuildOptions = BuildOptions
 
-type BundleMDXOptions = {
+export type BundleMDX<Frontmatter extends {[key: string]: any}> =
+  | BundleMDXSource<Frontmatter>
+  | BundleMDXFile<Frontmatter>
+
+export type BundleMDXSource<Frontmatter> = {
+  /**
+   * Your MDX source.
+   */
+  source: string
+  file?: undefined
+} & BundleMDXOptions<Frontmatter>
+
+export type BundleMDXFile<Frontmatter> = {
+  /**
+   * The path to the mdx file on disk.
+   */
+  file: string
+  source?: undefined
+} & BundleMDXOptions<Frontmatter>
+
+type BundleMDXOptions<Frontmatter> = {
   /**
    * The dependencies of the MDX code to be bundled
    *
    * @example
    * ```
-   * bundleMDX(mdxString, {
+   * bundleMDX({
+   *   source: mdxString,
    *   files: {
    *     './components.tsx': `
    *       import * as React from 'react'
@@ -45,7 +66,8 @@ type BundleMDXOptions = {
    *
    * @example
    * ```
-   * bundleMDX(mdxString, {
+   * bundleMDX({
+   *   source: mdxString,
    *   xdmOptions(options) {
    *     // this is the recommended way to add custom remark/rehype plugins:
    *     // The syntax might look weird, but it protects you in case we add/remove
@@ -58,14 +80,18 @@ type BundleMDXOptions = {
    * })
    * ```
    */
-  xdmOptions?: (options: CoreProcessorOptions) => CoreProcessorOptions
+  xdmOptions?: (
+    options: CoreProcessorOptions,
+    frontmatter: Frontmatter,
+  ) => CoreProcessorOptions
   /**
    * This allows you to modify the built-in esbuild configuration. This can be
    * especially helpful for specifying the compilation target.
    *
    * @example
    * ```
-   * bundleMDX(mdxString, {
+   * bundleMDX({
+   *   source: mdxString,
    *   esbuildOptions(options) {
    *     options.target = [
    *       'es2020',
@@ -80,7 +106,10 @@ type BundleMDXOptions = {
    * })
    * ```
    */
-  esbuildOptions?: (options: ESBuildOptions) => ESBuildOptions
+  esbuildOptions?: (
+    options: ESBuildOptions,
+    frontmatter: Frontmatter,
+  ) => ESBuildOptions
   /**
    * Any variables you want treated as global variables in the bundling.
    *
@@ -91,11 +120,12 @@ type BundleMDXOptions = {
    *
    * @example
    * ```
-   * bundlMDX(mdxString, {
+   * bundlMDX({
+   *   source: mdxString,
    *   globals: {'left-pad': 'myLeftPad'},
    * })
    *
-   * // then later
+   * // on the client side
    *
    * import leftPad from 'left-pad'
    *
@@ -112,7 +142,8 @@ type BundleMDXOptions = {
    *
    * @example
    * ```
-   * bundleMDX(mdxString, {
+   * bundleMDX({
+   *  source: mdxString
    *  cwd: '/users/you/site/mdx_root'
    * })
    * ```
@@ -123,7 +154,8 @@ type BundleMDXOptions = {
    *
    * @example
    * ```
-   * bundleMDX(mdxString, {
+   * bundleMDX({
+   *   source: mdxString,
    *   grayMatterOptions: (options) => {
    *     options.excerpt = true
    *
@@ -135,6 +167,27 @@ type BundleMDXOptions = {
   grayMatterOptions?: <I extends Input>(
     options: GrayMatterOption<I, any>,
   ) => GrayMatterOption<I, any>
+  /**
+   * This allows you to set the output directory of the bundle. You will need
+   * to set `bundlePath` as well to give esbuild the public url to the folder.
+   *
+   * *Note, the javascrpt bundle will not be placed here, only assets
+   * that can't be part of the main bundle.*
+   *
+   * @example
+   * ```
+   * bundleMDX({
+   *   file: '/path/to/file.mdx',
+   *   bundleDirectory: '/path/to/bundle'
+   *   bundlePath: '/path/to/public/bundle'
+   * })
+   * ```
+   */
+  bundleDirectory?: string
+  /**
+   * @see bundleDirectory
+   */
+  bundlePath?: string
 }
 
 type MDXExport<