scripts/documentation.js (245 lines of code) (raw):

// Copyright (c) 2020 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import documentation from 'documentation'; import fs from 'fs'; import {resolve, join} from 'path'; import {logSuccess, logProgress, logOk, logStep, logError} from './log'; import remark from 'remark'; import toc from 'remark-toc'; import unified from 'unified'; import markdown from 'remark-parse'; import actionTableMaker from './action-table-maker'; const INPUT_CONFIG = { shallow: true, access: ['public'], 'document-exported': true, sortOrder: 'alpha' // github: true }; const OUT_CONFIG = { markdownToc: true }; const PATHS = { src: resolve('./src'), api: resolve('./docs/api-reference') }; const BT = '`'; const TREE = { path: '', children: [ { path: 'actions', children: [ { input: [ '../constants/action-types.js', 'actions.js', 'action-wrapper.js', 'vis-state-actions.js', 'ui-state-actions.js', 'map-state-actions.js', 'map-style-actions.js', 'identity-actions.js' ], output: 'actions.md', config: {shallow: true} } ] }, { path: 'reducers', children: [ {input: ['root.js', 'core.js'], output: 'reducers.md', config: {shallow: true}}, {input: 'combined-updaters.js', output: 'combine.md', config: {shallow: true}}, {input: 'vis-state-updaters.js', output: 'vis-state.md', config: {shallow: true}}, {input: 'map-state-updaters.js', output: 'map-state.md', config: {shallow: true}}, {input: 'map-style-updaters.js', output: 'map-style.md', config: {shallow: true}}, {input: 'ui-state-updaters.js', output: 'ui-state.md', config: {shallow: true}} ] }, { path: 'processors', children: [{input: 'data-processor.js', output: 'processors.md', config: {shallow: true}}] }, { path: 'cloud-providers', children: [{input: 'provider.js', output: 'cloud-provider.md', config: {shallow: true}}] } ] }; function _overrideHeading(nodes) { const contents = ['Examples', 'Parameters']; const mdContents = contents.map(text => { return unified() .use(markdown) .parse(`__${text}__`); }); return nodes.map(node => { if ( node.type === 'heading' && Array.isArray(node.children) && contents.includes(node.children[0].value) ) { const value = node.children[0].value; const replacement = mdContents[contents.indexOf(value)].children[0]; return replacement; } return node; }); } function _appendActionTypesAndUpdatersToActions(node, actionMap) { // __Updaters__: [`visStateUpdaters.loadFilesUpdater`](../reducers/ui-state.md#uistateupdaterssetexportfilteredupdater) if (node.members && node.members.static.length) { node.members.static = node.members.static.map(nd => _appendActionTypesAndUpdatersToActions(nd, actionMap) ); } const action = node.name; if (!actionMap[action]) { return node; } const {actionType, updaters} = actionMap[action]; const updaterList = updaters .map( ({updater, name, path}) => `[${BT}${updater}.${name}${BT}](../reducers/${path.split('/')[2].replace('.js', '')}.md#${( updater + name ).toLowerCase()})` ) .join(', '); const mdContent = ` * __ActionTypes__: [${BT}ActionTypes.${actionType}${BT}](#actiontypes) * __Updaters__: ${updaterList} `; return _appendListToDescription(node, mdContent); } /** * Add action to linked updaters * @param {Object} node * @param {Object} actionMap */ function _appendActionToUpdaters(node, actionMap) { if (node.members && node.members.static.length) { node.members.static = node.members.static.map(nd => _appendActionToUpdaters(nd, actionMap)); } const updater = node.name; const action = Object.values(actionMap).find(action => action.updaters.find(up => up.name === updater) ); if (!action) { return node; } const actionName = action.name; const mdContent = ` * __Action__: [${BT}${actionName}${BT}](../actions/actions.md#${actionName.toLowerCase()}) `; return _appendListToDescription(node, mdContent); } function _appendListToDescription(node, mdContent) { const tree = unified() .use(markdown) .parse(mdContent); if (typeof node.description === 'object') { node.description.children = (node.description.children || []).concat(tree.children); } else { logError(`Missing Description for ${node.name}`); node.description = tree; } return node; } function _isParagraph(node) { return node.type === 'paragraph' && node.children.length === 1; } function _isLink(node) { return node.type === 'link' && node.children.length === 1; } function _isLinkReference(node) { return node.type === 'linkReference' && node.children.length === 1; } function _isExampleOrParam(node) { return node.type === 'text' && ['Parameters', 'Examples', 'Properties'].includes(node.value); } function _isExampleOrParameterLink(node) { return ( _isParagraph(node) && _isLinkReference(node.children[0]) && _isExampleOrParam(node.children[0].children[0]) ); } /** * Remove example and parameter link from TOC */ function _cleanUpTOCChildren(node) { if (!Array.isArray(node.children)) { return node; } if (_isExampleOrParameterLink(node)) { return null; } const filteredChildren = node.children .reduce((accu, nd) => { accu.push(_cleanUpTOCChildren(nd)); return accu; }, []) .filter(n => n); if (!filteredChildren.length) { return null; } return { ...node, children: filteredChildren }; } function buildChildDoc(inputPath, outputPath, actionMap, config) { return documentation .build(inputPath, {...INPUT_CONFIG, ...config}) .then(res => { // res is an array of parsed comments with inferred properties // and more: everything you need to build documentation or // any other kind of code data. let processed = res; if (outputPath.includes('actions.md')) { // add action type and updater links to action processed = res.map(node => _appendActionTypesAndUpdatersToActions(node, actionMap)); } else if (inputPath.some(p => p.includes('reducers'))) { // add action type and updater links to action processed = res.map(node => _appendActionToUpdaters(node, actionMap)); } return documentation.formats.remark(processed, OUT_CONFIG); }) .then(output => { // output is a string of remark json const ast = JSON.parse(output); ast.children = _overrideHeading(ast.children); if (ast.children.length < 3) { logError(inputPath, 'has less than 3 children'); } const tableOfContent = _cleanUpTOCChildren(ast.children[2]); ast.children[2] = tableOfContent; const mdOutput = remark().stringify(ast); fs.writeFileSync(outputPath, mdOutput); logOk(` ✓ build docs ${inputPath} -> ${outputPath}`); }) .catch(err => { logError(err); }); } function buildMdDocs(nodePath, node, actionMap, allTasks) { const {path, children} = node; const joinPath = nodePath ? `${nodePath}/${path}` : path; children.forEach(child => { if (!child.children) { const {input, output, config} = child; const inputPaths = (Array.isArray(input) ? input : [input]).reduce((accu, inp) => { const inputPath = join(PATHS.src, joinPath, inp); if (fs.existsSync(inputPath)) { // Do something accu.push(inputPath); } else { logError(`[Error] ${inputPath} doesn't exist!`); } return accu; }, []); if (!inputPaths.length) { return; } const outputPath = join(PATHS.api, joinPath, output); allTasks.push(buildChildDoc(inputPaths, outputPath, actionMap, config)); } else { buildMdDocs(joinPath, child, actionMap, allTasks); } }); return allTasks; } function buildDocs() { logProgress('\n================= Start Building API Documentation =================\n'); logStep(' ## 1. Gathering action and updater mapping'); const actionMap = actionTableMaker(); logStep(' ## 2. Build Markdown files from jsDoc'); const allTasks = buildMdDocs(null, TREE, actionMap, []); Promise.all(allTasks).then(() => { logSuccess('\n================= Building API Documentation Success! =================\n'); }); } buildDocs();