UNPKG

babel-plugin-transform-loadable-component

Version:

Transform react component to loadable component

273 lines (243 loc) 8.85 kB
const { getJSXAttr, createObjectExpression, createCalleeCallExpression, } = require('./ast') // eslint-disable-next-line max-lines-per-function module.exports = ({ types }) => { const defaultReactLoadableSpecifier = '__React$$Loadable__' const defaultReactLoadableSource = 'react-loadable' // 用于记录 import 和被引用 JSX 的访问器 const collectImportsVisitor = { /** * 遍历 import 声明 * @param {NodePath} p */ ImportDeclaration(p) { const specifiers = p.get('specifiers') const source = p.get('source') if ( // 如果没有 specifier, 如 import 'module' 则直接略过该次遍历 !specifiers || !specifiers.length || // 如果 source 非法则直接略过该次遍历 !source || !source.isStringLiteral() ) { return } // 目前只解析 default import 标识, 如果未发现 default import 特征, 则直接略过该次遍历 const firstSpecifier = specifiers[0] if (!firstSpecifier || !firstSpecifier.isImportDefaultSpecifier()) { return } // 获得当前的 default import 标识 const defaultImportIdentifier = firstSpecifier.get('local').node.name const importSource = source.node.value const importDeclarationMap = { source: importSource, path: p, } if (importSource === defaultReactLoadableSource) { // 如果找到了 react-loadable 的 import 引用, 则记录下来避免误删 // 并在后续的转换过程直接使用该 identifier 作为引用值 this.reactLoadableImport.set('path', p) this.reactLoadableImport.set('identifier', defaultImportIdentifier) } else { this.defaultImportsMap.set( defaultImportIdentifier, importDeclarationMap ) } }, } const transJSXElementVisitor = { /** * 遍历 JSX 元素 * @param {NodePath} p */ // eslint-disable-next-line max-len // eslint-disable-next-line max-lines-per-function, sonarjs/cognitive-complexity JSXElement(p) { let hasReferencedComponentProp = false let hasAsyncProp = false let hasLoadingProp = false let loadable = false const removeImports = [] const COMPONENT = Symbol('__COMPONENT__') const ASYNC = Symbol('__ASYNC__') const LOADING = Symbol('__LOADING__') const attrs = p.get('openingElement').get('attributes') attrs.forEach((attrPath) => { // 如果不是一般的 attr (譬如 JSXSpreadAttribute), 则直接跳过检查 if (!attrPath.isJSXAttribute()) { return } const { name, value, type } = getJSXAttr(attrPath) // 检查是否匹配 props.component: ReferencedIdentifier if ( name === 'component' && type === 'expression' && value.isReferencedIdentifier() && this.defaultImportsMap.has(value.node.name) ) { hasReferencedComponentProp = true // eslint-disable-next-line no-param-reassign attrPath[COMPONENT] = true } // 检查是否匹配 props.__async: true if (name === '__async') { hasAsyncProp = true // eslint-disable-next-line no-param-reassign attrPath[ASYNC] = true if (value === true) { loadable = true } else if (type === 'expression' && value.isBooleanLiteral()) { loadable = value.node.value } } if (name === '__loading' && type === 'expression') { hasLoadingProp = true // eslint-disable-next-line no-param-reassign attrPath[LOADING] = true } }) if (hasReferencedComponentProp || hasAsyncProp || hasLoadingProp) { let newAttrs = attrs const existingReactLoadableIdentifier = this.reactLoadableImport.get( 'identifier' ) const loadingAttrPath = newAttrs.filter( attrPath => attrPath[LOADING] )[0] if (loadable && hasReferencedComponentProp) { newAttrs = newAttrs.map((attrPath) => { if (!attrPath[COMPONENT]) { return attrPath } const { value: originComponentValue } = getJSXAttr(attrPath) const { source, path: importDeclarationPath, } = this.defaultImportsMap.get( originComponentValue.node.name ) this.matchedJSXSet.add(p) removeImports.push(importDeclarationPath) return { node: types.JSXAttribute( types.JSXIdentifier('component'), types.JSXExpressionContainer( createCalleeCallExpression( // eslint-disable-next-line max-len existingReactLoadableIdentifier || defaultReactLoadableSpecifier, [ createObjectExpression({ loader: types.arrowFunctionExpression( [], createCalleeCallExpression('import', [ types.stringLiteral(source), ]) ), loading: loadingAttrPath ? loadingAttrPath.get('value').get('expression').node : undefined, }), ] ) ) ), } }) } // eslint-disable-next-line no-param-reassign p.node.openingElement.attributes = newAttrs .filter(attrPath => (!attrPath[ASYNC] && !attrPath[LOADING])) .map(attrPath => attrPath.node) } removeImports.forEach((importPath) => { importPath && importPath.remove() }) }, } return { pre() { // 在开始遍历之前, 首先创建一个缓存对象, 用于存储 default import 声明 // // 存储的规则为: // - key {string} ImportDefaultSpecifier.Identifier // - value {Map} // - source: ImportSource.Literal // - path: NodePath // // 如: // import DefaultExports from 'source' // 会被记录为 // 'DefaultExports': { // 'path': NodePath, // 'source': String // } // this.defaultImportsMap = new Map() // 存储模式匹配的 jsx 声明 // // 匹配模式: // - 包含 props.__async 属性声明 // - 包含 props.component 属性声明, 其值必须是一个引用值 // - 可能包含 props.__loading 属性声明 // TODO(xingda.xd):目前先使用内置的模式, 将来考虑将这里做成配置项 // this.matchedJSXSet = new Set() // 记录 react-loadable 的 import 引用 // // 存储的规则为: // - identifier: Identifier // - path: NodePath // // 如: // import Loadable from 'react-loadable' // 会被记录为 // { identifier: 'Loadable', path: NodePath } // this.reactLoadableImport = new Map() }, post() { // 遍历结束之后释放对象引用以避免内存泄露 this.defaultImportsMap = null this.matchedJSXSet = null this.reactLoadableImport = null }, visitor: { Program: { enter(p) { // 在 AST 根节点进行遍历操作, 用来记录 AST 中所有的 default import 节点 p.traverse(collectImportsVisitor, { defaultImportsMap: this.defaultImportsMap, matchedJSXSet: this.matchedJSXSet, reactLoadableImport: this.reactLoadableImport, }) // 遍历 import 节点之后对 jsx 进行遍历, 对模式匹配的节点进行替换操作 p.traverse(transJSXElementVisitor, { defaultImportsMap: this.defaultImportsMap, matchedJSXSet: this.matchedJSXSet, reactLoadableImport: this.reactLoadableImport, }) }, exit(p) { // eslint-disable-next-line max-len const existingReactLoadableIdentifier = this.reactLoadableImport.get('identifier') const hasLoadableComponent = this.matchedJSXSet.size // 如果存在模式匹配的组件, 则插入 react-loadable 的引用 if (!existingReactLoadableIdentifier && hasLoadableComponent) { p.unshiftContainer('body', types.importDeclaration( [types.importDefaultSpecifier( types.identifier(defaultReactLoadableSpecifier) )], types.stringLiteral(defaultReactLoadableSource) )) } }, }, }, } }