babel-helper-decorate-react
Version:
Babel Helper for custom decorator for React Component
263 lines (227 loc) • 7.2 kB
text/typescript
import * as types from '@babel/types'
import tpl from '@babel/template'
import { addDefault, addNamespace } from '@babel/helper-module-imports'
import parseCommentsRanges, { CreateDisabledScopesOptions } from './parseCommentsRanges'
import { isScopeDepthPassed } from './utils'
export type TransformDataFn = (
data: any,
path: import('@babel/traverse').NodePath,
babelPluginPass: import('@babel/core').PluginPass,
helper: RangesHelper
) => any
export type StrictVisitorConfig = {
type: string
condition?: (
path: import('@babel/traverse').NodePath,
babelPluginPass: import('@babel/core').PluginPass,
helper: RangesHelper,
walkApi: WalkApi
) => boolean
transformData?: TransformDataFn
}
export type VisitorConfig = string | StrictVisitorConfig
export type CreateDecorateVisitorOpts = Partial<CreateDisabledScopesOptions> & {
visitorTypes?: VisitorConfig[]
deepVisitorTypes?: VisitorConfig[]
exportVisitorTypes?: string[]
transformData?: TransformDataFn
ExportDefaultDeclaration?: boolean
ExportNamedDeclaration?: boolean
decorateLibPath?: string
detectScopeDepth?: number
importType?: 'namespace' | 'default'
defaultEnable?: boolean
moduleInteropPath?: string
}
export class RangesHelper {
public ranges: ReturnType<typeof parseCommentsRanges>
importName: any
cache = new Map()
babelPass: import('@babel/core').PluginPass
constructor(public opts: any) {}
public getLocation(path: import('@babel/traverse').NodePath) {
let loc = path.node.loc
let tmp = path
while (!loc && tmp) {
// @ts-ignore
tmp = tmp.getPrevSibling()
if (!tmp?.node) {
tmp = tmp.parentPath
}
loc = tmp?.node?.loc
}
return loc
}
public eval(content: string) {
return `__$$EVAL%%${content}%%__`
}
public matched(path: import('@babel/traverse').NodePath) {
return this.getEnableOptions(this.getLocation(path).start.line)
}
public inject(path: import('@babel/traverse').NodePath, transformData?: any) {
let { matched, data } = this.matched(path)
if (!matched) {
return false
}
if (!this.cache.get(path.node)) {
this.cache.set(path.node, [])
}
const decorated = this.cache.get(path.node)
if (decorated.includes(this.opts.libPath)) {
return path.skip()
}
decorated.push(this.opts.libPath)
let importName = this.importName
if (!importName) {
const moduleInteropPath = this.opts.moduleInteropPath
if (this.opts.importType === 'namespace') {
this.importName = addNamespace(path, this.opts.libPath, { nameHint: 'decorate' })
} else {
this.importName = addDefault(path, this.opts.libPath, { nameHint: 'decorate' })
}
if (moduleInteropPath) {
const moduleInterop = addDefault(path, moduleInteropPath)
this.importName = types.callExpression(moduleInterop, [this.importName])
}
importName = this.importName
} else {
importName = types.cloneDeep(this.importName)
}
// this.importName
if (transformData) {
data = transformData(data, path, this)
}
let dataExp = tpl.expression(JSON.stringify(data || null).replace(/"__\$\$EVAL%%(.*?)%%__"/g, '$1'))()
if ('ClassDeclaration' === path.node.type) {
path.node.decorators = path.node.decorators || []
path.node.decorators.push(types.decorator(types.callExpression(importName, [dataExp])))
} else {
// @ts-ignore
path.replaceWith(types.callExpression(types.callExpression(importName, [dataExp]), [path.node]))
}
}
getEnableOptions(line: number) {
for (const r of this.ranges) {
if (r.type === 'disable') {
if (r.has(line)) {
return {
matched: false,
data: null
}
}
}
if (r.has(line)) {
return {
matched: true,
data: r.data
}
}
}
return {
matched: this.opts.defaultEnable,
data: null
}
}
}
type WalkStatus = 'noSkip' | 'wrap'
class WalkApi {
statusList: Set<WalkStatus> = new Set()
addStatus(s: WalkStatus) {
return this.statusList.add(s)
}
removeStatus(s: WalkStatus) {
return this.statusList.delete(s)
}
hasStatus(s: WalkStatus) {
return this.statusList.has(s)
}
noSkip(f: boolean = true) {
f ? this.addStatus('noSkip') : this.removeStatus('noSkip')
}
wrap(f: boolean = true) {
f ? this.addStatus('wrap') : this.removeStatus('wrap')
}
}
function createDecorateVisitor({
prefix = 'decorate',
decorateLibPath,
moduleInteropPath = require.resolve('module-interop'),
visitorTypes = ['FunctionExpression', 'ArrowFunctionExpression', 'ClassExpression', 'ClassDeclaration'],
deepVisitorTypes = visitorTypes,
exportVisitorTypes = ['ExportDefaultDeclaration', 'ExportNamedDeclaration'],
defaultEnable = true,
transformData,
importType = 'default',
detectScopeDepth = -1,
...opts
}: CreateDecorateVisitorOpts = {}) {
if (!prefix) {
throw new Error('`prefix` is required')
}
if (!decorateLibPath) {
throw new Error('`decorateLibPath` is required')
}
const reduceVisitors = (types: VisitorConfig[]) =>
types.reduce((acc: any, name) => {
if (typeof name === 'string') {
acc[name] = function (path, { helper }) {
if (isScopeDepthPassed(path, detectScopeDepth)) {
helper.inject(path)
}
path.skip()
}
} else {
acc[name.type] = function (path, { helper }) {
const transform = name.transformData || transformData
const walkApi = new WalkApi()
let rlt: boolean
if (isScopeDepthPassed(path, detectScopeDepth)) {
if (!name.condition) {
helper.inject(path, (data) => (transform ? transform(data, path, helper.babelPass, helper) : data))
} else if (
helper.matched(path)?.matched &&
(rlt = name.condition(path, helper.babelPass, helper, walkApi)) &&
rlt === true
) {
helper.inject(walkApi.hasStatus('wrap') ? path.parentPath : path, (data) =>
transform ? transform(data, path, helper.babelPass, helper) : data
)
}
}
if (!walkApi.hasStatus('noSkip')) {
path.skip()
}
}
}
return acc
}, {})
const deepVisitors = reduceVisitors(deepVisitorTypes)
const _visitors = reduceVisitors(visitorTypes)
let exportVisitors = exportVisitorTypes.reduce((acc: any, name) => {
acc[name] = function (path, state) {
path.traverse(deepVisitors, state)
path.skip()
}
return acc
}, {})
return {
Program(path) {
const helper = new RangesHelper({
importType,
moduleInteropPath,
libPath: decorateLibPath,
defaultEnable
})
helper.babelPass = this
helper.ranges = parseCommentsRanges(path.container.comments, { prefix, ...opts })
path.traverse(
{
..._visitors,
...exportVisitors
},
{ helper }
)
}
}
}
export default createDecorateVisitor