scripts/action-table-maker.js (100 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 fs from 'fs'; import {resolve, join} from 'path'; import traverse from '@babel/traverse'; import * as parser from '@babel/parser'; import {walkSync} from './ast-helper'; const SRC_DIR = './src'; const entries = [ // action to type mapping join(SRC_DIR, 'actions'), // type to updater mapping join(SRC_DIR, 'reducers') ]; /** * Build action table * @public */ export function buildActionTable() { const allFiles = walkSync(entries, []); let actionTypeMap = {}; allFiles.forEach(file => { actionTypeMap = traverseTree(file, actionTypeMap); }); return Object.keys(actionTypeMap).reduce((accu, type) => { accu[actionTypeMap[type].action.name] = { ...actionTypeMap[type].action, updaters: actionTypeMap[type].updaters, actionType: type }; return accu; }, {}); } function createActionNode(actionType) { return {action: '', updaters: [], actionType}; } /** * Parse actionHandler declaration to map updater to action type * @param {Object} path - AST node * @param {Object} actionMap * @param {string} filePath */ function addActionHandler(path, actionMap, filePath) { const {init} = path.node; if (init && Array.isArray(init.properties)) { init.properties.forEach(property => { const {key, value} = property; if (key && value && key.property && value.property) { const actionType = key.property.name; const updater = value.name; actionMap[actionType] = actionMap[actionType] || createActionNode(actionType); actionMap[actionType].updaters.push({ updater: value.object.name, name: value.property.name, path: filePath }); } }) } } /** * Parse createAction function to add action to action type * @param {*} path * @param {*} actionMap * @param {*} filePath */ function addActionCreator(path, actionMap, filePath) { const {node, parentPath} = path; if (node.arguments.length && parentPath.node && parentPath.node.id) { const action = parentPath.node.id.name; const firstArg = node.arguments[0]; const actionType = firstArg.property ? firstArg.property.name : firstArg.name; const {loc} = parentPath.node actionMap[actionType] = actionMap[actionType] || createActionNode(actionType); actionMap[actionType].action = {name: action, path: `${filePath}#L${loc.start.line}-L${loc.end.line}`}; } } function addActionDeclaration(path, actionMap, filePath) { const {node} = path; const action = node.id.name; const returnStatement = node.body.body[0]; const returnValue = returnStatement.argument.properties[0].value; const actionType = returnValue.property ? returnValue.property.name : returnValue.name; const {loc} = path.node; actionMap[actionType] = actionMap[actionType] || createActionNode(actionType); actionMap[actionType].action = {name: action, path: `${filePath}#L${loc.start.line}-L${loc.end.line}`}; } function traverseTree(filePath, actionMap = {}) { const content = fs.readFileSync(filePath, 'utf8'); const ast = parser.parse(content, { sourceType: 'module', plugins: ['exportNamespaceFrom', 'exportDefaultFrom'] }); traverse(ast, { VariableDeclarator(path) { const {id} = path.node; if (id.name === 'actionHandler') { addActionHandler(path, actionMap, filePath); } }, CallExpression(path) { /** * If action is declared with createAction * export const togglePerspective = createAction( * ActionTypes.TOGGLE_PERSPECTIVE * ); */ const {callee} = path.node; if (callee.name === 'createAction') { addActionCreator(path, actionMap, filePath); } }, FunctionDeclaration(path) { /** * If action is declared with a function call * export function layerConfigChange(oldLayer, newConfig) { * return {type: ActionTypes.LAYER_CONFIG_CHANGE}; * } */ if (filePath.endsWith('actions.js')) { const {leadingComments} = path.parentPath.node; if ( Array.isArray(leadingComments) && leadingComments[0] && leadingComments[0].value.includes('@public') ) { addActionDeclaration(path, actionMap, filePath); } } } }); return actionMap; } export default buildActionTable;