Storybook Markdown Docs
Supporting .md docs in Storybook — Part 2
This article is part of a series , feel free to look at the others below 👇👇.
- Addon-Kit is a developer’s dream come true
- Supporting .md docs in Storybook — Part 1
- Supporting .md docs in Storybook — Part 2
- Storybook Addon for Auto Markdown Import
- Supporting .html files in Storybook
So, in the last article I wrote about importing Markdown docs in MDX docs and using them with Storybook. But it still felt weird, I wanted a different solution without using .mdx files.
Now, my purpose is simple. I want Markdown docs to display in Storybook when they are imported in the main.js
configuration like so:
module.exports = {
stories: [
'../*.md',
'../src/**/*.stories.@(js|jsx|ts|tsx)'
]
}
By default if you start Storybook with that configuration, you will get an error, something like:
Error: No matching story indexer found for ../CHANGELOG.md
at StoryIndexGenerator.extractStories (..\node_modules\@storybook\core-server\dist\index.js:10:10133)
at ..\node_modules\@storybook\core-server\dist\index.js:10:8814
at ..\node_modules\@storybook\core-server\dist\index.js:10:8556
at Array.map (<anonymous>)
at ..\node_modules\@storybook\core-server\dist\index.js:10:8471
at Array.map (<anonymous>)
at StoryIndexGenerator.updateExtracted (..\node_modules\@storybook\core-server\dist\index.js:10:8364)
at StoryIndexGenerator.ensureExtracted (..\node_modules\@storybook\core-server\dist\index.js:10:8730)
at StoryIndexGenerator.initialize (..\node_modules\@storybook\core-server\dist\index.js:10:8268)
Well, there is a specific Github issue for that error specifically 👇
So, the whole idea resides in how Storybook reads the stories. It uses something called StoryIndexers
which is basically a method that loops over specific list of files, defined by a glob search, and create an index of these files, hence the name StoryIndexers
. This index will be used to populate the index.json
file ( stories.json
for users of older versions of Storybook).
Currently Storybook has only 2 indexers, one that reads .mdx files and the other reads files that contain csf stories like button.stories.ts
. So, the way indexer goes is like this (quoted from common-preset.ts
file in the Storybook core-server
repo):
import { loadCsf } from '@storybook/csf-tools';
import { readFile } from 'fs';
export const storyIndexers = async (indexers?: StoryIndexer[]) => {
const csfIndexer = async (fileName: string, opts: IndexerOptions) => {
const code = (await readFile(fileName, 'utf-8')).toString();
return loadCsf(code, { ...opts, fileName }).parse();
};
return [
{
test: /(stories|story)\.[tj]sx?$/,
indexer: csfIndexer,
},
...(indexers || []),
];
};
So, this is technically the way to go, if I actually wanted to import a CSF story, something like .storiestest.ts like mentioned in the issue I listed above. Just add a new indexer with a different glob, something like /(stories|story)(test)*\.[tj]sx?$/
to make it work.
While this will work for CSF stories, Markdown docs are totally different species. We need to convert them into MDX, then compile those MDX into CSF, and there is no guarantee it will work.
So, I will be using the same loadCsf
method from @storybook/csf-tools
to parse the CSF component along with another compile
method from @storybook/mdx2-csf
to compile the MDX into CSF.
import { loadCsf } from '@storybook/csf-tools';
import { compile } from '@storybook/mdx2-csf';
module.exports = async (fileName, opts) => {
const title = fileName.split('/').pop().replace('.md', '');
// Convert Markdown into MDX
const source = `
import { Meta, Description } from '@storybook/blocks';
<Meta title='${title}' />
<Description>{require('${fileName}')}</Description>
`;
// Compile MDX into CSF
const code = await compile(source, {});
// Parse CSF component
return loadCsf(code, { ...opts, fileName }).parse();
}
Looks familiar? That’s the same snippet I talked about in my last article 👇
Afterwards, we need to use that indexer in the main.js
file like so:
const mdIndexer = require('./indexer.js');
module.exports = {
storyIndexers: (indexers) => {
return [
{
test: /\.md$/,
indexer: mdIndexer
},
...(indexers || [])
];
}
}
At this point, everything should click into place. I wish it was this simple, we still have to deal with Storybook reading this Markdown as a Story instead of a Docs. I’ve create a teeny tiny PR for that purpose 👇
Once this PR is merged, I will publish the final article in this series, to include this code as part of an addon to be published and used by everyone who needs something like this, so stay tuned.