Skip to content

Commit

Permalink
feat: allow files to be loaded directly (#29)
Browse files Browse the repository at this point in the history
Co-authored-by: Kent C. Dodds <[email protected]>
  • Loading branch information
Arcath and kentcdodds authored Apr 27, 2021
1 parent 8303ada commit 1065373
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 65 deletions.
49 changes: 47 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <div>Neat demo!</div>
}

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:
<Demo />
`.trim()

const result = await bundleMDX(mdxSource, {
cwd: '/users/you/site/_content/pages',
})

const {code, frontmatter} = result
```

### Component Substitution

MDX Bundler passes on
Expand Down Expand Up @@ -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}

<img src={exampleImage} alt="Image alt text" />

```

### Known Issues
Expand Down
Binary file added other/150.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions other/sample-component.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react'

import image from './150.png'

/** @type React.FC */
export const Sample = () => {
return (
<div>
<b>Sample!</b>
<img src={image} />
</div>
)
}
20 changes: 11 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
50 changes: 46 additions & 4 deletions src/__tests__/index.js
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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"`,
)
})

Expand All @@ -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"`,
)
})

Expand All @@ -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)`,
)
})

Expand Down Expand Up @@ -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'
<Sample />
![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()
95 changes: 45 additions & 50 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -38,81 +39,75 @@ 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<string, string> */
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 */
const inMemoryPlugin = {
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),
}
}
},
)
}
})
},
}

Expand Down
15 changes: 15 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,19 @@ type BundleMDXOptions = {
* ```
*/
globals?: Record<string, string | ModuleInfo>
/**
* 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
}

0 comments on commit 1065373

Please sign in to comment.