babel-plugin-transform-imports-api
Version:
Convert import default package API to modular reference to reduce package size and transforms member style imports.
240 lines (210 loc) • 9.12 kB
text/typescript
import { types as Types, PluginObj } from 'babel__core'
import transformImport from './transformImport'
import { ConvertPluginPass as PluginPass, packagesApis } from './types'
// 标识没有没有默认导出的包
const noDefault = '__noDefault'
const plugin = function (babel: {
types: typeof Types;
}): PluginObj {
const t = babel.types
// 需要一个数据格式管理 package 信息,这些变量需要在每个 program 里重置
/**
*
packagesApis: new Map([
['packageName1', new Set(['api'])],
['packageName2', new Set(['api'])],
]),
*/
let packagesApis: packagesApis = new Map()
let invokedApis: Map<string, string> = new Map()
const packageInvokedApis: Map<string, typeof invokedApis> = new Map()
const packageNeedDefault: Map<string, boolean> = new Map()
// packageName 和 defaultName 的映射
const packageNames: Map<string, string> = new Map()
// defaultName packageName 的映射
const packageDefaultNames: Map<string, string> = new Map()
const packageIsApiImported: Map<string, boolean> = new Map()
return {
name: 'babel-plugin-transform-imports-api',
visitor: {
ImportDeclaration(ast) {
const packageName = ast.node.source.value
const apis = packagesApis.get(packageName)
if (!apis) return
ast.node.specifiers.forEach(node => {
if (t.isImportDefaultSpecifier(node)) {
packageNames.set(packageName, node.local.name)
packageDefaultNames.set(node.local.name, packageName)
} else if (t.isImportSpecifier(node)) {
// @ts-ignore
const propertyName = node.imported.name
if (apis.has(propertyName)) { // 记录api名字
ast.scope.rename(node.local.name)
if (!packageInvokedApis.get(packageName)) {
packageInvokedApis.set(packageName, new Map())
}
invokedApis = packageInvokedApis.get(packageName) || new Map()
invokedApis.set(propertyName, node.local.name)
} else {
// 如果是未实现的 api 改成Taro.xxx
packageNeedDefault.set(packageName, true)
if (!packageNames.get(packageName)) {
// 假如没有默认导出变量,noDefault 占位,标识导出默认变量没有命名
packageNames.set(packageName, noDefault)
}
}
}
})
},
Identifier(ast) {
const packageName = packageDefaultNames.get(ast.node.name)
if (!packageName) return
// import Taro from '@tarojs/taro'
if (t.isImportDefaultSpecifier(ast.parent)) return
// obj.Taro MemberExpression
if (t.isMemberExpression(ast.parent) || t.isJSXMemberExpression(ast.parent)) {
// @ts-ignore
if (ast.parent.object.name !== ast.node.name) {
return
}
}
// 如果 import default 变量被引用,则需要默认导出变量
packageNeedDefault.set(packageName, true)
},
MemberExpression(ast) {
// @ts-ignore
const packageName = packageDefaultNames.get(ast.node.object.name)
if (!packageName) return
const apis = packagesApis.get(packageName)
if (!apis) return
let invokedApis = packageInvokedApis.get(packageName)
if (!invokedApis) {
invokedApis = new Map()
packageInvokedApis.set(packageName, invokedApis)
}
/* 处理Taro.xxx */
const property = ast.node.property
let propertyName: string | null = null
let propName = 'name'
// 兼容一下 Taro['xxx']
if (t.isStringLiteral(property)) {
propName = 'value'
}
propertyName = property[propName]
if (!propertyName) return
// 同一api使用多次, 读取变量名
if (apis.has(propertyName)) {
const parentNode = ast.parent
const isAssignment = t.isAssignmentExpression(parentNode) && parentNode.left === ast.node
if (!isAssignment) {
let identifier: Types.Identifier
if (invokedApis.has(propertyName)) {
identifier = t.identifier(invokedApis.get(propertyName)!)
} else {
const newPropertyName = ast.scope.generateUid(propertyName)
invokedApis.set(propertyName, newPropertyName)
/* 未绑定作用域 */
identifier = t.identifier(newPropertyName)
}
ast.replaceWith(identifier)
}
} else {
packageNeedDefault.set(packageName, true)
}
},
Program: {
enter(ast, state: PluginPass) {
if (!state.opts) return
packagesApis = state.opts.packagesApis
packageInvokedApis.clear()
packageNeedDefault.clear()
packageNames.clear()
packageDefaultNames.clear()
packageIsApiImported.clear()
},
exit(ast, state: PluginPass) {
if (!state.opts) return
ast.traverse({
ImportDeclaration(ast) {
const packageName = ast.node.source.value
if (!packageNames.get(packageName)) return
const apis = packagesApis.get(packageName)
if (!apis) return
// 未实现 api 没有默认导出的
ast.node.specifiers.forEach(node => {
if (t.isImportSpecifier(node)) {
if (packageNames.get(packageName) === noDefault) {
// 假如到最后都没有默认导出,则生成一个唯一的 iden
const defaultIden = ast.scope.generateUid(packageName)
packageNames.set(packageName, defaultIden)
}
// @ts-ignore
const propertyName = node.imported.name
// 如果是实现的 api,不去更改 Taro.api 这种引用
if (apis.has(propertyName)) {
return
}
const iden = t.identifier(packageNames.get(packageName) || '')
packageNeedDefault.set(packageName, true)
const localName = node.local.name
const binding = ast.scope.getBinding(localName)
binding && binding.referencePaths.forEach(reference => {
reference.replaceWith(
t.memberExpression(
iden,
t.identifier(propertyName)
)
)
})
}
})
// 调用了配置的 api,重复引入包会被删除
if (packageIsApiImported.get(packageName)) { return ast.remove() }
packageIsApiImported.set(packageName, true)
const invokedApis = packageInvokedApis.get(packageName) || new Map()
const namedImports = Array.from(invokedApis.entries()).map(([imported, local]) => t.importSpecifier(t.identifier(local), t.identifier(imported)))
const defaultImportName = packageNames.get(packageName) || ''
if (packageNeedDefault.get(packageName)) {
const defaultImport = t.importDefaultSpecifier(t.identifier(defaultImportName))
ast.node.specifiers = [
defaultImport,
...namedImports
]
packageNeedDefault.set(packageName, false)
} else if (namedImports.length){
ast.node.specifiers = namedImports
} else {
// 导出的声明变量如果没有被上下引用则删除该导出声明
ast.remove()
}
},
})
// usePackgesImport 是否对 import member 进行处理
// 这个包 fork 的是 https://bitbucket.org/amctheatres/babel-transform-imports/src/master/index.js 仓库
// 以下是修改后 opt 配置
/**
usePackgesImport: false, // 开关是否使用 packagesImport
packagesImport: {
'my-library': {
transform: (importName, matches) => `my-library/etc/${importName.toUpperCase()}`,
preventFullImport: true,
},
'date-fns': {
transform: importName => `date-fns/${camelCase(importName)}`,
preventFullImport: true,
},
}
*/
if (state.opts.usePackgesImport) {
ast.traverse({
ImportDeclaration(ast) {
transformImport(ast, state, babel.types)
},
})
}
}
}
}
}
}
export default plugin