diff --git a/.gitignore b/.gitignore index 8e0c70c..eac6254 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules coverage dist .DS_Store +output/ # these cause more harm than good # when working with contributors diff --git a/README.md b/README.md index 4ced99a..50c6c42 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,9 @@ should be installed as one of your project's `dependencies`: npm install --save mdx-bundler ``` +One of mdx-bundler's dependancies requires a working [node-gyp][node-gyp] setup +to be able to install correctly. + ## Usage ```typescript @@ -481,6 +484,68 @@ export const exampleImage = 'https://example.com/image.jpg' Image alt text ``` +### Image Bundling + +With the [cwd](#cwd) and the remark plugin +[remark-mdx-images](https://www.npmjs.com/package/remark-mdx-images) you can +bundle images in your mdx! + +There are two loaders in esbuild that can be used here. The easiest is `dataurl` +which outputs the images as inline data urls in the returned code. + +```js +import {remarkMdxImages} from 'remark-mdx-images' + +const {code} = await bundleMDX(mdxSource, { + cwd: '/users/you/site/_content/pages', + xdmOptions: (vFile, options) => { + options.remarkPlugins = [remarkMdxImages] + + return options + }, + esbuildOptions: options => { + options.loader = { + ...options.loader, + '.png': 'dataurl', + } + + return options + }, +}) +``` + +The `file` loader requires a little more configuration to get working. With the +`file` loader your images are copied to the output directory so esbuild needs to +be set to write files and needs to know where to put them plus the url of the +folder to be used in image sources. + +```js +const {code} = await bundleMDX(mdxSource, { + cwd: '/users/you/site/_content/pages', + xdmOptions: (vFile, options) => { + options.remarkPlugins = [remarkMdxImages] + + return options + }, + esbuildOptions: options => { + // Set the `outdir` to your public directory. + options.outdir = '/users/you/site/public/img' + options.loader = { + ...options.loader, + // Tell esbuild to use the `file` loader for pngs + '.png': 'file', + } + // Set the public path to /img/ so image sources start /img/ + options.publicPath = '/img/' + + // Set write to true so that esbuild will output the files. + options.write = true + + return options + }, +}) +``` + ### Known Issues #### Cloudflare Workers @@ -624,4 +689,5 @@ MIT [bugs]: https://github.com/kentcdodds/mdx-bundler/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Acreated-desc+label%3Abug [requests]: https://github.com/kentcdodds/mdx-bundler/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement [good-first-issue]: https://github.com/kentcdodds/mdx-bundler/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement+label%3A%22good+first+issue%22 +[node-gyp]: https://github.com/nodejs/node-gyp#installation diff --git a/package.json b/package.json index 5579329..e2fc753 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@babel/runtime": "^7.13.17", "@esbuild-plugins/node-resolve": "^0.1.4", "@fal-works/esbuild-plugin-global-externals": "^2.1.1", - "esbuild": "^0.11.15", + "esbuild": "^0.11.16", "gray-matter": "^4.0.3", "jsdom": "^16.5.3", "remark-frontmatter": "^3.0.0", diff --git a/src/__tests__/index.js b/src/__tests__/index.js index c19e59c..88e7129 100644 --- a/src/__tests__/index.js +++ b/src/__tests__/index.js @@ -5,6 +5,7 @@ import React from 'react' import rtl from '@testing-library/react' import leftPad from 'left-pad' import {remarkMdxImages} from 'remark-mdx-images' +import path from 'path' import {bundleMDX} from '../index.js' import {getMDXComponent} from '../client.js' @@ -293,4 +294,62 @@ import {Sample} from './other/sample-component' ) }) +test('should output assets', async () => { + const mdxSource = ` +# Sample Post + +![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.outdir = path.join(process.cwd(), 'output') + options.loader = { + ...options.loader, + '.png': 'file', + } + options.publicPath = '/img/' + options.write = true + + return options + }, + }) + + const Component = getMDXComponent(code) + + const {container} = render(React.createElement(Component)) + + assert.match(container.innerHTML, 'src="/img/150') + + const error = /** @type Error */ (await bundleMDX(mdxSource, { + cwd: process.cwd(), + xdmOptions: (vFile, options) => { + options.remarkPlugins = [remarkMdxImages] + + return options + }, + esbuildOptions: options => { + options.loader = { + ...options.loader, + // esbuild will throw its own error if we try to use `file` loader without `outdir` + '.png': 'dataurl', + } + options.write = true + + return options + }, + }).catch(e => e)) + + assert.equal( + error.message, + "You must either specify `write: false` or `write: true` and `outdir: '/path'` in your esbuild options", + ) +}) + test.run() diff --git a/src/index.js b/src/index.js index 95977ce..202565d 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,4 @@ +import fs from 'fs' import path from 'path' import {StringDecoder} from 'string_decoder' import remarkFrontmatter from 'remark-frontmatter' @@ -8,6 +9,8 @@ import {NodeResolvePlugin} from '@esbuild-plugins/node-resolve' import {globalExternals} from '@fal-works/esbuild-plugin-global-externals' import dirnameMessedUp from './dirname-messed-up.cjs' +const {readFile, unlink} = fs.promises + /** * * @param {string} mdxSource - A string of mdx source code @@ -149,14 +152,33 @@ async function bundleMDX( const bundled = await esbuild.build(buildOptions) - const decoder = new StringDecoder('utf8') + if (bundled.outputFiles) { + const decoder = new StringDecoder('utf8') - const code = decoder.write(Buffer.from(bundled.outputFiles[0].contents)) + const code = decoder.write(Buffer.from(bundled.outputFiles[0].contents)) - return { - code: `${code};return Component.default;`, - frontmatter, + return { + code: `${code};return Component.default;`, + frontmatter, + } } + + if (buildOptions.outdir && buildOptions.write) { + const code = await readFile( + path.join(buildOptions.outdir, '_mdx_bundler_entry_point.js'), + ) + + await unlink(path.join(buildOptions.outdir, '_mdx_bundler_entry_point.js')) + + return { + code: `${code};return Component.default;`, + frontmatter, + } + } + + throw new Error( + "You must either specify `write: false` or `write: true` and `outdir: '/path'` in your esbuild options", + ) } export {bundleMDX} diff --git a/src/types.d.ts b/src/types.d.ts index 173438e..521a0a5 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -8,7 +8,7 @@ import type {Plugin, BuildOptions, Loader} from 'esbuild' import type {ModuleInfo} from '@fal-works/esbuild-plugin-global-externals' import type {VFileCompatible, CompileOptions} from 'xdm/lib/compile' -type ESBuildOptions = BuildOptions & {write: false} +type ESBuildOptions = BuildOptions type BundleMDXOptions = { /**