mirror of
https://github.com/jlengrand/open-wc.git
synced 2026-03-10 08:31:19 +00:00
chore(demoing-storybook): inline MDJS plugin
This commit is contained in:
5
.changeset/silent-dryers-tell.md
Normal file
5
.changeset/silent-dryers-tell.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@open-wc/demoing-storybook': patch
|
||||
---
|
||||
|
||||
inline MDJS plugin
|
||||
@@ -70,7 +70,16 @@
|
||||
"rollup": "^2.7.2",
|
||||
"rollup-plugin-babel": "^5.0.0-alpha.1",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"storybook-addon-markdown-docs": "^0.4.4",
|
||||
"storybook-prebuilt": "^1.5.0"
|
||||
"@babel/code-frame": "^7.8.3",
|
||||
"@mdjs/core": "^0.4.1",
|
||||
"detab": "^2.0.3",
|
||||
"mdurl": "^1.0.1",
|
||||
"remark-html": "^10.0.0",
|
||||
"remark-parse": "^7.0.2",
|
||||
"remark-slug": "^5.1.2",
|
||||
"storybook-prebuilt": "^1.5.0",
|
||||
"unified": "^8.4.2",
|
||||
"unist-builder": "^2.0.2",
|
||||
"unist-util-visit-parents": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { mdjsToCsf } = require('storybook-addon-markdown-docs');
|
||||
const { mdjsToCsf } = require('../../storybook-addon-markdown-docs');
|
||||
const { transformMdxToCsf } = require('../../shared/transformMdxToCsf');
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { mdjsToCsf } = require('storybook-addon-markdown-docs');
|
||||
const { mdjsToCsf } = require('../../storybook-addon-markdown-docs');
|
||||
|
||||
function createMdjsToCsfTransformer(options) {
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
const { mdjsToCsf } = require('./src/mdjsToCsf');
|
||||
|
||||
module.exports = { mdjsToCsf };
|
||||
@@ -0,0 +1,26 @@
|
||||
/** @typedef {import('@mdjs/core').Story} Story */
|
||||
|
||||
/**
|
||||
* @param {Story[]} stories
|
||||
* @returns {string}
|
||||
*/
|
||||
function createStoriesCode(stories) {
|
||||
let allCode = '';
|
||||
for (const story of stories) {
|
||||
const { key, name, code } = story;
|
||||
if (!key) throw new Error(`Missing key in story`);
|
||||
if (!code) throw new Error(`Missing code in story`);
|
||||
|
||||
allCode += `${code}\n`;
|
||||
allCode += `${key}.story = ${key}.story || {};\n`;
|
||||
allCode += `${name !== key ? `${key}.story.name = ${JSON.stringify(name)};\n` : ''}`;
|
||||
allCode += `${key}.story.parameters = ${key}.story.parameters || {};\n`;
|
||||
allCode += `${key}.story.parameters.mdxSource = ${JSON.stringify(code.trim())};\n`;
|
||||
allCode += '\n';
|
||||
}
|
||||
return allCode;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createStoriesCode,
|
||||
};
|
||||
8
packages/demoing-storybook/src/storybook-addon-markdown-docs/src/global.d.ts
vendored
Normal file
8
packages/demoing-storybook/src/storybook-addon-markdown-docs/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
declare module '@mdx-js/mdx' {
|
||||
function mdx(markdown: string, options: { compilers: any[]; filepath: string }): Promise<string>;
|
||||
export = mdx;
|
||||
}
|
||||
|
||||
declare module '@mdx-js/mdx/mdx-hast-to-jsx' {
|
||||
export function toJSX(a: any, b: any, c: any): any;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
const { transformAsync } = require('@babel/core');
|
||||
|
||||
/**
|
||||
* Turns the JSX generated by MDX to JS.
|
||||
*
|
||||
* @param {string} docsJsx
|
||||
* @param {string} filename
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function jsxToJs(docsJsx, filename) {
|
||||
const result = await transformAsync(docsJsx, {
|
||||
filename,
|
||||
sourceMaps: true,
|
||||
babelrc: false,
|
||||
configFile: false,
|
||||
plugins: [
|
||||
require.resolve('@babel/plugin-syntax-import-meta'),
|
||||
[require.resolve('@babel/plugin-transform-react-jsx'), { useSpread: true }],
|
||||
],
|
||||
});
|
||||
if (!result || typeof result.code !== 'string') {
|
||||
throw new Error(`Something went wrong when compiling ${filename}`);
|
||||
}
|
||||
return result.code;
|
||||
}
|
||||
|
||||
module.exports = { jsxToJs };
|
||||
@@ -0,0 +1,97 @@
|
||||
/** @typedef {import('@mdjs/core').Story} Story */
|
||||
|
||||
const mdx = require('@mdx-js/mdx');
|
||||
const mdxToJsx = require('@mdx-js/mdx/mdx-hast-to-jsx');
|
||||
|
||||
/**
|
||||
* @param {string} markdown
|
||||
* @param {string} filepath
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function compileMdToJsx(markdown, filepath) {
|
||||
return mdx(
|
||||
`import { Story, Preview, Props } from 'storybook-prebuilt/addon-docs/blocks.js';\n\n${markdown}`,
|
||||
{
|
||||
compilers: [
|
||||
// custom mdx compiler which ensures mdx doesn't add a default export,
|
||||
// we don't need it because we are adding our own
|
||||
function mdxCompiler() {
|
||||
// @ts-ignore
|
||||
this.Compiler = tree => mdxToJsx.toJSX(tree, {}, { skipExport: true });
|
||||
},
|
||||
],
|
||||
filepath,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Story[]} [stories]
|
||||
*/
|
||||
function createDocsPage(stories) {
|
||||
/** @type {Record<string, string>} */
|
||||
const storyNameToKey = {};
|
||||
|
||||
if (stories) {
|
||||
for (const { key, name } of stories) {
|
||||
storyNameToKey[name || key] = key;
|
||||
}
|
||||
}
|
||||
|
||||
return `import * as React from 'storybook-prebuilt/react.js';
|
||||
import { mdx, AddContext } from 'storybook-prebuilt/addon-docs/blocks.js';
|
||||
|
||||
// Setup docs page
|
||||
const mdxStoryNameToKey = ${JSON.stringify(storyNameToKey)};
|
||||
__export_default__.parameters = __export_default__.parameters || {};
|
||||
__export_default__.parameters.docs = __export_default__.parameters.docs || {};
|
||||
__export_default__.parameters.docs.page = () => <AddContext
|
||||
mdxStoryNameToKey={mdxStoryNameToKey}
|
||||
mdxComponentMeta={__export_default__}><MDXContent
|
||||
/></AddContext>;
|
||||
${
|
||||
!stories || stories.length === 0
|
||||
? `
|
||||
export const __page = () => {
|
||||
throw new Error("Docs-only story");
|
||||
};
|
||||
__page.story = {
|
||||
parameters: {
|
||||
docsOnly: true
|
||||
}
|
||||
};
|
||||
`
|
||||
: ''
|
||||
}
|
||||
|
||||
export default __export_default__;`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns MD into JSX using the MDX compiler. This is necessary because most of the
|
||||
* regular storybook docs functionality relies on JSX and MDX specifics
|
||||
*
|
||||
* @param {string} markdown
|
||||
* @param {string} filepath
|
||||
* @param {Story[]} [stories]
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function mdToJsx(markdown, filepath, stories) {
|
||||
return `/**
|
||||
*
|
||||
* The code below is generated by storybook docs.
|
||||
*
|
||||
*/
|
||||
|
||||
${createDocsPage(stories)}
|
||||
|
||||
// The docs page, markdown turned into using jsx for storybook
|
||||
${await compileMdToJsx(markdown, filepath)}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mdToJsx,
|
||||
// export for testing
|
||||
createDocsPage,
|
||||
compileMdToJsx,
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
const { mdjsToMd } = require('./mdjsToMd');
|
||||
const { renameDefaultExport } = require('./renameDefaultExport');
|
||||
const { createStoriesCode } = require('./createStoriesCode');
|
||||
const { mdToJsx } = require('./mdToJsx');
|
||||
const { jsxToJs } = require('./jsxToJs');
|
||||
|
||||
/**
|
||||
* @param {string} markdown
|
||||
* @param {string} filePath
|
||||
* @param {object} options
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function mdjsToCsf(markdown, filePath, options = {}) {
|
||||
const markdownResult = await mdjsToMd(markdown, { ...options, filePath });
|
||||
|
||||
const jsCode = renameDefaultExport(markdownResult.jsCode, filePath);
|
||||
const storiesCode = createStoriesCode(markdownResult.stories);
|
||||
const docsJsx = await mdToJsx(markdownResult.html, filePath, markdownResult.stories);
|
||||
const docs = await jsxToJs(docsJsx, filePath);
|
||||
|
||||
return `${jsCode}\n${storiesCode}\n${docs}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mdjsToCsf,
|
||||
};
|
||||
@@ -0,0 +1,169 @@
|
||||
/** @typedef {import('@mdjs/core').Story} Story */
|
||||
/** @typedef {import('@mdjs/core').MarkdownResult} MarkdownResult */
|
||||
/** @typedef {import('@mdjs/core').ParseResult} ParseResult */
|
||||
/** @typedef {import('@mdjs/core').MdjsProcessPlugin} MdjsProcessPlugin */
|
||||
|
||||
const unified = require('unified');
|
||||
const markdown = require('remark-parse');
|
||||
// @ts-ignore
|
||||
const mdSlug = require('remark-slug');
|
||||
// @ts-ignore
|
||||
const mdStringify = require('remark-html');
|
||||
// @ts-ignore
|
||||
const detab = require('detab');
|
||||
const u = require('unist-builder');
|
||||
const visit = require('unist-util-visit-parents');
|
||||
// @ts-ignore
|
||||
const normalize = require('mdurl/encode');
|
||||
|
||||
const { mdjsParse, mdjsStoryParse } = require('@mdjs/core');
|
||||
|
||||
/**
|
||||
* Keep the code blocks as md source code so storybook will use it's special code block
|
||||
*
|
||||
* @param {*} h
|
||||
* @param {*} node
|
||||
*/
|
||||
function code(h, node) {
|
||||
const value = node.value ? detab(node.value) : '';
|
||||
const raw = ['', `\`\`\`${node.lang}`, value, '```'].join('\n');
|
||||
return h.augment(node, u('raw', raw));
|
||||
}
|
||||
|
||||
/**
|
||||
* Override image to make it closing `<img src="" />` as otherwise the jsx parser throws
|
||||
*
|
||||
* @param {*} h
|
||||
* @param {*} node
|
||||
*/
|
||||
function image(h, node) {
|
||||
const attributes = [];
|
||||
if (node.title !== null && node.title !== undefined) {
|
||||
attributes.push(`title="${node.title}"`);
|
||||
}
|
||||
if (node.alt !== null && node.alt !== undefined) {
|
||||
attributes.push(`alt="${node.alt}"`);
|
||||
}
|
||||
if (node.url !== null && node.url !== undefined) {
|
||||
attributes.push(`src="${normalize(node.url)}"`);
|
||||
}
|
||||
|
||||
const raw = `<img ${attributes.join(' ')} />`;
|
||||
return h.augment(node, u('raw', raw));
|
||||
}
|
||||
|
||||
/**
|
||||
* Override line break to make it closing `<br />` as otherwise the jsx parser throws
|
||||
* Can't use 'break' as a function name as it is a reserved javascript language name
|
||||
*
|
||||
* @param {*} h
|
||||
* @param {*} node
|
||||
*/
|
||||
function hardBreak(h, node) {
|
||||
return [h.augment(node, u('raw', '<br />')), u('text', '\n')];
|
||||
}
|
||||
|
||||
/**
|
||||
* Override hr to make it closing `<hr />` as otherwise the jsx parser throws
|
||||
*
|
||||
* @param {*} h
|
||||
* @param {*} node
|
||||
*/
|
||||
function thematicBreak(h, node) {
|
||||
return h.augment(node, u('raw', `<hr />`));
|
||||
}
|
||||
|
||||
function transformPropsHook() {
|
||||
// @ts-ignore
|
||||
return tree => {
|
||||
visit(tree, 'html', (node, ancestors) => {
|
||||
// @ts-ignore
|
||||
if (node.value.startsWith('<sb-props')) {
|
||||
/* eslint-disable no-param-reassign */
|
||||
ancestors[1].type = 'html';
|
||||
// @ts-ignore
|
||||
ancestors[1].value = node.value.replace('<sb-props', '<Props').replace('>', ' />');
|
||||
ancestors[1].children = [];
|
||||
/* eslint-enable no-param-reassign */
|
||||
}
|
||||
});
|
||||
return tree;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
function storyTag(name) {
|
||||
return `<Story name="${name}"></Story>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
function previewStoryTag(name) {
|
||||
return `<Preview><Story name="${name}"></Story></Preview>`;
|
||||
}
|
||||
|
||||
/** @type {MdjsProcessPlugin[]} */
|
||||
const mdjsToMdPlugins = [
|
||||
{ name: 'markdown', plugin: markdown },
|
||||
{ name: 'mdjsParse', plugin: mdjsParse },
|
||||
{
|
||||
name: 'mdjsStoryParse',
|
||||
plugin: mdjsStoryParse,
|
||||
options: {
|
||||
storyTag,
|
||||
previewStoryTag,
|
||||
},
|
||||
},
|
||||
{ name: 'transformPropsHook', plugin: transformPropsHook },
|
||||
{ name: 'mdSlug', plugin: mdSlug },
|
||||
{
|
||||
name: 'mdStringify',
|
||||
plugin: mdStringify,
|
||||
options: {
|
||||
handlers: {
|
||||
code,
|
||||
image,
|
||||
break: hardBreak,
|
||||
thematicBreak,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* @param {MdjsProcessPlugin[]} plugins
|
||||
* @param {string=} filePath
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function defaultSetupMdjsPlugins(plugins, filePath = '') {
|
||||
return plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} markdownText
|
||||
* @returns {Promise<MarkdownResult>}
|
||||
*/
|
||||
async function mdjsToMd(
|
||||
markdownText,
|
||||
{ filePath = '', setupMdjsPlugins = defaultSetupMdjsPlugins } = {},
|
||||
) {
|
||||
const plugins = setupMdjsPlugins(mdjsToMdPlugins, filePath);
|
||||
const parser = unified();
|
||||
for (const pluginObj of plugins) {
|
||||
parser.use(pluginObj.plugin, pluginObj.options);
|
||||
}
|
||||
/** @type {unknown} */
|
||||
const parseResult = await parser.process(markdownText);
|
||||
const result = /** @type {ParseResult} */ (parseResult);
|
||||
|
||||
return {
|
||||
html: result.contents,
|
||||
jsCode: result.data.jsCode,
|
||||
stories: result.data.stories,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { mdjsToMd };
|
||||
@@ -0,0 +1,75 @@
|
||||
/** @typedef {import('@babel/core').types.File} File */
|
||||
|
||||
const { parse } = require('@babel/parser');
|
||||
// @ts-ignore
|
||||
const { codeFrameColumns } = require('@babel/code-frame');
|
||||
const {
|
||||
isExportDefaultDeclaration,
|
||||
isExpression,
|
||||
isIdentifier,
|
||||
variableDeclaration,
|
||||
variableDeclarator,
|
||||
identifier,
|
||||
} = require('@babel/core').types;
|
||||
const { default: generate } = require('@babel/generator');
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
*/
|
||||
function createMissingExportError(path) {
|
||||
return new Error(
|
||||
`${path} does not contain a default export with the page title. This is required for Storybook.`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} code
|
||||
* @param {string} path
|
||||
* @param {boolean} [highlightError]
|
||||
* @returns {string}
|
||||
*/
|
||||
function renameDefaultExport(code, path, highlightError = true) {
|
||||
if (!code) {
|
||||
throw createMissingExportError(path);
|
||||
}
|
||||
|
||||
/** @type {File} */
|
||||
let file;
|
||||
try {
|
||||
// @ts-ignore
|
||||
file = parse(code, { sourceType: 'module', sourceFilename: path });
|
||||
} catch (error) {
|
||||
const codeFrame = codeFrameColumns(
|
||||
code,
|
||||
{ start: error.loc },
|
||||
{ highlightCode: highlightError },
|
||||
);
|
||||
throw new Error(`${error.message}\n\n${codeFrame}`);
|
||||
}
|
||||
if (!file) {
|
||||
throw createMissingExportError(path);
|
||||
}
|
||||
|
||||
const { body } = file.program;
|
||||
const [defaultExport] = body.filter(n => isExportDefaultDeclaration(n));
|
||||
if (!defaultExport || !isExportDefaultDeclaration(defaultExport)) {
|
||||
throw createMissingExportError(path);
|
||||
}
|
||||
|
||||
if (!isExpression(defaultExport.declaration) && !isIdentifier(defaultExport.declaration)) {
|
||||
throw createMissingExportError(path);
|
||||
}
|
||||
|
||||
// replace the user's default export with a variable, so that we can add it to the storybook
|
||||
// default export later
|
||||
const defaultExportReplacement = variableDeclaration('const', [
|
||||
variableDeclarator(identifier('__export_default__'), defaultExport.declaration),
|
||||
]);
|
||||
body.splice(body.indexOf(defaultExport), 1, defaultExportReplacement);
|
||||
|
||||
return generate(file).code;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
renameDefaultExport,
|
||||
};
|
||||
Reference in New Issue
Block a user