preface
When we use a component library, the document page is the most direct window to obtain information. The document page generally contains the following information:
- Description of component
- Component Demo sample display, description and source code
- Parameter documentation for the component
We could certainly use a tool like Storybook for purely component sample debugging and presentation, but for aesthetics and the difficulty of restoring from blueprints, we should consider writing a more customizable and extensible tool of our own.
Analysis of the
Let’s comb through our requirements from the information contained in the document page above:
- The most concise syntax to write pages
- The most concise syntax to show Demo + source code + example description
- Minimum cost to maintain parameter documentation
Syntactically, we should have preferred Markdown, because the syntax is concise and powerful enough.
To show Demo and source code, we should put an example Demo + source code + example description in one file for more efficient and low cost maintenance, and reuse as much as possible to reduce the code needed to maintain. The example presentation is essentially the same as markDown -> HTML, but we need to extend the rules of translation.
There are many problems with maintaining parameter documents manually: it is expensive and not easy to synchronize with the code (you have to manually change parameter documents every time you change them), so we should consider automatically extracting information from TS declaration to form parameter documents.
For markdown -> HTML, we only need a WebPack loader.
implementation
Preprocessed entry file
Our main entry file is a Markdown file, which is the page we generate, and the entire page structure looks like this (the order of the structure can be adjusted, of course) :
To extract parameter names, descriptions, types, and defaults from TypeScript declarations, we can use the React-docgen-typescript tool, which we do a little bit of tweaking.
Custom compiler to filter parameters that react itself does not need to display
const parse = require('react-docgen-typescript').withDefaultConfig({
propFilter: (prop) => {
if (prop.parent == null) {
return true;
}
return prop.parent.fileName.indexOf('node_modules/@types/react') < 0;
},
}).parse;
Copy the code
Get the required information from the component TS
const params = parse(filePath);
const info = params[0];
Copy the code
We get info, which contains the component parameter name, description, type, default value, and so on. We just need to convert this information into markdown table and insert it into the main entry markdown file.
Webpack loader
The Webpack Loader itself handles the specified file type and outputs the actual packaged JS code. We now need to convert markdown into the actual running React JSX code.
In the previous step we get the main entry markdown file containing the parameter information, which we reserve a slot %%Content%% (of course we can specify at will) for later insert Demo examples.
For ease of maintenance, we put Demo examples for each component in the corresponding component directory in a folder named Demo.
Each Demo example is a markdown file. The contents of the markdown file are as follows:
-- Order: 0 Title: Different types of buttons -- There are six types of buttons: default button, main button, danger button, danger button, dotted button and text button. import { Button } from '@bytedesign/web-react'; ReactDOM.render( <Button type="primary"> Primary </Button>, CONTAINER );Copy the code
The file contains information such as title, presentation order, description, sample source code, and so on.
Using re or front-matter, we can get the configuration information such as title and presentation order. Using re, we can get the source code of description and examples. Now we have all the information we need to stitch together the information into the page we want.
AST tree processing
To handle the AST tree, we chose to use Babel, which is the package we need to use:
- @babel/core
- @babel/parser
- @babel/template
- @babel/traverse
- @babel/generator
- @babel/types
The AST tree of code is a very complex tree structure, and we can assist in generating and viewing the AST tree through this website astExplorer.net/.
For markDown content, we first convert it to HTML code with marked. Of course, the translated HTML is a string. We cannot generate the AST tree with Babel. We need to convert the HTML string to JSX first. As follows:
function htmlToJsx(html) { return `import React from 'react'; export default function() { return ( <span>${html .replace(/class=/g, 'className=') .replace(/{/g, '{"{"{') .replace(/}/g, '{"}"}') .replace(/{"{"{/g, '{"{"}')} </span> ); }; `; }Copy the code
Given the JSX code, we can happily generate the AST tree:
const parser = require('@babel/parser');
function parse(codeBlock) {
return parser.parse(codeBlock, {
sourceType: 'module',
plugins: ['jsx', 'classProperties'],
});
}
const ast = parse(htmlToJsx(marked(markdown)));
Copy the code
Build the AST for the demo example
Get the example source code through the regular AST tree:
// @arco-design/ arco-Components const ast = parse(' import {CodeBlockWrapper, CellCode, CellDemo, CellDescription, Browser } from "@arco-design/arco-components"; ${code} `);Copy the code
Traverse the AST tree and insert the sample AST, description AST, and source AST into the CodeBlockWrapper component:
const traverse = require('@babel/traverse').default; const t = require('@babel/types'); traverse(ast, { CallExpression(_path) { if ( _path.node.callee.object && _path.node.callee.object.name === 'ReactDOM' && _path.node.callee.property.name === 'render' ) { const demoCellElement = t.jsxElement( t.jsxOpeningElement(t.JSXIdentifier('CellDemo'), []), t.jsxClosingElement(t.JSXIdentifier('CellDemo')), [_path.node.arguments[0]] ); const codeCellElement = t.jsxElement( t.jsxOpeningElement(t.JSXIdentifier('CellCode'), codeAttrs), t.jsxClosingElement(t.JSXIdentifier('CellCode')), [codePreviewBlockAst] ); const descriptionCellElement = t.jsxElement( t.jsxOpeningElement(t.JSXIdentifier('CellDescription'), []), t.jsxClosingElement(t.JSXIdentifier('CellDescription')), [descriptionAst] ); const codeBlockElement = t.jsxElement( t.jsxOpeningElement(t.JSXIdentifier('CodeBlockWrapper'), []), t.jsxClosingElement(t.JSXIdentifier('CodeBlockWrapper')), [descriptionCellElement, demoCellElement, codeCellElement] ); const app = t.VariableDeclaration('const', [ t.VariableDeclarator(t.Identifier('__export'), codeBlockElement), ]); _path.insertBefore(app); _path.remove(); }}});Copy the code
Get the code from the above transformation:
const babel = require('@babel/core');
const { code } = babel.transformFromAstSync(ast, null, babelConfig);
Copy the code
The output code is in the following format:
const __export = <CodeBlockWrapper> <CellDescription>... <CellDescription> <CellDemo>... </CellDemo> <CellCode>... <CellCode> </CodeBlockWrapper>Copy the code
We will have several example Markdowns in our Demo folder, and for each example we will generate a function component and put it in an array:
const generate = require('@babel/generator').default;
const template = require('@babel/template').default;
const buildRequire = template(`
function NAME() {
AST
return __export;
}
`);
const finnalAst = buildRequire({
NAME: `Demo${index}`,
AST: code,
});
demoList.push(generate(finnalAst).code);
Copy the code
Now our demoList is actually an array of all the sample components of the component that we will put into a real component for display:
const buildRequire = template(` CODE class Component extends React.Component { render() { return React.createElement('span', { className: 'arco-components-wrapper' }, ${demoList .map((_, index) => `React.createElement(Demo${index}, { key: ${index} })`) .join(',')}); }} `); const finnalAst = buildRequire({ CODE: demoList.join('\n'), });Copy the code
OK, finnalAst is the AST that we finally inserted into the generated AST from the entry Markdown.
Replace the placeholder
Remember that we left a placeholder for %%Content%% above, we now need to replace the processed example with the placeholder.
traverse(contentAst, { JSXElement: (_path) => { if ( _path.node.openingElement.name.name === 'p' && _path.node.children[0].value === '%%Content%%' ) { const expresstion = t.jsxExpressionContainer( t.jsxElement( t.jsxOpeningElement(t.JSXIdentifier('Component'), [], true), null, [], true ) ); _path.replaceWith(expresstion); _path.stop(); }}}); Traverse (contentAst, {FunctionDeclaration: (_path) => {_path.insertbefore (finnalAst); // Traverse (contentAst, {FunctionDeclaration: (_path) => {_path.insertbefore (finnalAst); _path.stop(); }});Copy the code
The Webpack Loader finally processes the returned code:
return generate(contentAst).code;
Copy the code
use
Using this plugin, we can use a markDown loader that handles markDown files and demo examples as follows:
import ButtonPage from 'components/Button/README.md';
function Page() {
return <ButtonPage />;
}
Copy the code
conclusion
Above, we use a WebPack Loader to generate component documents based on Markdown. This process actually solves many pain points when we write component documents:
- Ensure that parameter documents are fully synchronized with source code, greatly reducing maintenance costs.
- The official website sample display, component debugging, and so on, while minimizing the cost of writing examples. The code is written once and can be used simultaneously to generate official site samples, official site sample source code, snapshot tests, and Github/Gitlab instructions pages. Moreover, the process is fully automated, so you can basically focus on writing component logic, greatly reducing development and maintenance costs.
- With markDown files being parsed natively by GitLab and Github, we now have a description page for each component directory, where you can see parameters and descriptions.
- Fully controlled website style.
If you are also developing the React component library, or need to show examples of React related components, this document may be helpful.