From 1d04e4a25e9b87e7ab6b2bbd09507352c5b0394e Mon Sep 17 00:00:00 2001 From: Michael Irwin Date: Tue, 5 Apr 2022 21:30:12 -0400 Subject: [PATCH] Add async processing support to support async plugins Resolves #680 Signed-off-by: Michael Irwin --- lib/react-markdown.js | 18 ++++++++++++++++-- package.json | 2 ++ test/test.jsx | 23 +++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/lib/react-markdown.js b/lib/react-markdown.js index 9291e7f3..6c18853f 100644 --- a/lib/react-markdown.js +++ b/lib/react-markdown.js @@ -13,6 +13,7 @@ * @property {PluggableList} [remarkPlugins=[]] * @property {PluggableList} [rehypePlugins=[]] * @property {import('remark-rehype').Options | undefined} [remarkRehypeOptions={}] + * @property {boolean} [async=false] * * @typedef LayoutOptions * @property {string} [className] @@ -69,9 +70,13 @@ const deprecated = { * React component to render markdown. * * @param {ReactMarkdownOptions} options - * @returns {ReactElement} + * @returns {ReactElement | null} */ export function ReactMarkdown(options) { + const [asyncHastNode, setAsyncHastNode] = React.useState( + /** @type {?Root} */ (null) + ) + for (const key in deprecated) { if (own.call(deprecated, key) && own.call(options, key)) { const deprecation = deprecated[key] @@ -104,7 +109,16 @@ export function ReactMarkdown(options) { ) } - const hastNode = processor.runSync(processor.parse(file), file) + if (options.async && !asyncHastNode) { + processor + .run(processor.parse(file), file) + .then((node) => setAsyncHastNode(node)) + return null + } + + const hastNode = options.async + ? /** @type Root */ (asyncHastNode) + : processor.runSync(processor.parse(file), file) if (hastNode.type !== 'root') { throw new TypeError('Expected a `root` node') diff --git a/package.json b/package.json index 88e4bbc2..9df9be1f 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "@types/react-is": "^17.0.0", + "@types/react-test-renderer": "^17.0.0", "c8": "^7.0.0", "esbuild": "^0.14.0", "eslint-config-xo-react": "^0.27.0", @@ -113,6 +114,7 @@ "prettier": "^2.0.0", "react": "^18.0.0", "react-dom": "^18.0.0", + "react-test-renderer": "^18.0.0", "rehype-raw": "^6.0.0", "remark-cli": "^10.0.0", "remark-gfm": "^3.0.0", diff --git a/test/test.jsx b/test/test.jsx index e904b8d1..77c2d64f 100644 --- a/test/test.jsx +++ b/test/test.jsx @@ -6,10 +6,12 @@ * @typedef {import('hast').Text} Text * @typedef {import('react').ReactNode} ReactNode * @typedef {import('../index.js').Components} Components + * @typedef {import('react-test-renderer').ReactTestRenderer} ReactTestRenderer */ import fs from 'node:fs' import path from 'node:path' +import {fail} from 'node:assert' import {test} from 'uvu' import * as assert from 'uvu/assert' import React from 'react' @@ -18,6 +20,7 @@ import {visit} from 'unist-util-visit' import raw from 'rehype-raw' import toc from 'remark-toc' import ReactDom from 'react-dom/server' +import renderer, {act} from 'react-test-renderer' import Markdown from '../index.js' const own = {}.hasOwnProperty @@ -27,6 +30,7 @@ const own = {}.hasOwnProperty * @returns {string} */ function asHtml(input) { + if (!input) return '' return ReactDom.renderToStaticMarkup(input) } @@ -1424,4 +1428,23 @@ test('should crash on a plugin replacing `root`', () => { }, /Expected a `root` node/) }) +test('should work correctly when executed asynchronously', async () => { + const input = '# Test' + + /** @type {ReactTestRenderer | undefined} */ + let component + await act(async () => { + component = renderer.create() + }) + + if (!component) fail('component not set') + + const renderedOutput = component.toJSON() + if (!renderedOutput) fail('No rendered output provided') + if (Array.isArray(renderedOutput)) fail('Not expecting multiple children') + + assert.equal(renderedOutput.type, 'h1') + assert.equal(renderedOutput.children, ['Test']) +}) + test.run()