Skip to content

Commit c4ed24b

Browse files
authored
Merge pull request #2786 from modernweb-dev/feat/storybook-mdx-loads-md
feat(storybook-utils): add MDXFileLoader to load and render non-JS files in MDX
2 parents 6a4a4cf + 4b262db commit c4ed24b

File tree

12 files changed

+89
-4
lines changed

12 files changed

+89
-4
lines changed

.changeset/gentle-masks-film.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@web/storybook-utils': patch
3+
---
4+
5+
add MDXFileLoader to load and render non-JS files in MDX

.changeset/six-lions-sin.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@web/storybook-utils': patch
3+
---
4+
5+
fix exports

package-lock.json

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/storybook-framework-web-components/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@
7272
"@storybook/blocks": "^7.0.0",
7373
"@storybook/types": "^7.0.0",
7474
"@web/dev-server": "^0.4.0",
75+
"@web/rollup-plugin-import-meta-assets": "^2.2.1",
76+
"@web/storybook-utils": "^1.0.1",
7577
"storybook": "^7.0.0"
7678
}
7779
}

packages/storybook-framework-web-components/tests/all-in-one.spec.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,23 @@ test.describe('all in one', () => {
8888
await sbPage.waitUntilLoaded();
8989

9090
await expect(page).toHaveTitle(/^My page - Docs/);
91-
await expect(sbPage.docParent().locator('h1')).toContainText('My page header');
91+
await expect(sbPage.docParent().locator('h1').nth(0)).toContainText('My page header');
9292
await expect(
9393
sbPage.docParent().getByText('This is an MDX-based documentation page.'),
9494
).toBeAttached();
95-
await expect(sbPage.docParent().locator('h2')).toContainText('Story inside my page');
95+
await expect(sbPage.docParent().locator('h2').nth(0)).toContainText('Story inside my page');
9696
await expect(
9797
sbPage.docParent().getByText('Below is a story rendered in MDX.'),
9898
).toBeAttached();
9999
expect(await sbPage.docParent().locator('#root-inner').innerHTML()).toContain(
100100
'<div>My component works</div>',
101101
);
102+
await expect(sbPage.docParent().locator('h2').nth(1)).toContainText('MDXFileLoader');
103+
await expect(
104+
sbPage.docParent().getByText('Below is a content loaded from a separate Markdown file.'),
105+
).toBeAttached();
106+
await expect(sbPage.docParent().locator('h1').nth(1)).toContainText('My readme');
107+
await expect(sbPage.docParent().getByText('My readme works.')).toBeAttached();
102108
});
103109

104110
test('renders autodocs page', async ({ page }) => {

packages/storybook-framework-web-components/tests/fixtures/all-in-one/.storybook/main.js

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { importMetaAssets } from '@web/rollup-plugin-import-meta-assets';
2+
13
/** @type { import('../../../../index.d.ts').StorybookConfig } */
24
const config = {
35
stories: ['../stories/**/*.stories.js', '../stories/**/*.mdx'],
@@ -13,6 +15,7 @@ const config = {
1315
rollupFinal(config) {
1416
return {
1517
...config,
18+
plugins: [...config.plugins, importMetaAssets()],
1619
onLog(level, log, defaultHandler) {
1720
// we are only interested in warnings
1821
if (level !== 'warn') {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# My readme
2+
3+
My readme works.

packages/storybook-framework-web-components/tests/fixtures/all-in-one/stories/my-page.mdx

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Canvas, Meta } from '@storybook/blocks';
1+
import { Canvas, Markdown, Meta } from '@storybook/blocks';
2+
import { MDXFileLoader } from '@web/storybook-utils';
23

34
import * as MyComponentStories from './my-component.stories.js';
45

@@ -13,3 +14,14 @@ This is an MDX-based documentation page.
1314
Below is a story rendered in MDX.
1415

1516
<Canvas of={MyComponentStories.DefaultStory} />
17+
18+
## MDXFileLoader
19+
20+
Below is a content loaded from a separate Markdown file.
21+
22+
<MDXFileLoader
23+
url={new URL('../README.md', import.meta.url).href}
24+
render={content => {
25+
return <Markdown>{content}</Markdown>;
26+
}}
27+
/>

packages/storybook-utils/README.md

+29
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,35 @@
22

33
Utilities for Storybook.
44

5+
## MDXFileLoader
6+
7+
Loads a non-JS file content in MDX and allows to render it.
8+
9+
This is needed for 2 reasons:
10+
11+
- to workaround the limitation of MDX 2 where top-level await is not supported
12+
- to work in browsers where non-standard ESM imports (e.g. imports of text files, CSS and such) are not possible
13+
14+
> In MDX 3 you can just [use the top-level await.](https://mdxjs.com/blog/v3/#await-in-mdx)
15+
16+
### Use-case: render external Markdown file using Storybook `Markdown` block.
17+
18+
Given a Storybook MDX file `docs/my-page.mdx` and a Markdown file `README.md` in the root, use the following MDX code:
19+
20+
```mdx
21+
import { Markdown } from '@storybook/blocks';
22+
import { MDXFileLoader } from '@web/storybook-utils';
23+
24+
<MDXFileLoader
25+
url={new URL('../README.md', import.meta.url).href}
26+
render={content => {
27+
return <Markdown>{content}</Markdown>;
28+
}}
29+
/>
30+
```
31+
32+
> Make sure to use `@web/rollup-plugin-import-meta-assets` or another alternative to correctly bundle the assets resolved with the help of `import.meta.url`.
33+
534
## createAddon
635

736
Storybook addons are React components.

packages/storybook-utils/index.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export * from './dist/index.js';
1+
export * from './src/index.js';

packages/storybook-utils/src/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { createAddon } from './create-addon.js';
2+
export { MDXFileLoader } from './mdx-file-loader.js';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { useEffect, useState } from 'react';
2+
3+
/**
4+
* @param {{ url: string, render: (content: string) => any }} props
5+
*/
6+
export const MDXFileLoader = ({ url, render }) => {
7+
const [content, setContent] = useState('');
8+
useEffect(() => {
9+
fetch(url)
10+
.then(res => res.text())
11+
.then(text => setContent(text));
12+
}, []);
13+
if (!content) {
14+
return 'Loading...';
15+
}
16+
return render(content);
17+
};

0 commit comments

Comments
 (0)