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} Image alt text - ``` ### 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 }