424 lines
14 KiB
TypeScript
424 lines
14 KiB
TypeScript
import { type types as t, NodePath } from '@babel/core';
|
|
import * as babel from '@babel/core';
|
|
import { Option } from './types';
|
|
import type { Scope } from '@babel/traverse';
|
|
|
|
const DECORATOR_PROPS = 'Prop';
|
|
const DECORATOR_CHILDREN = 'Children';
|
|
const DECORATOR_WATCH = 'Watch';
|
|
const DECORATOR_ENV = 'Env';
|
|
|
|
function replaceFnWithClass(path: NodePath<t.FunctionDeclaration>, classTransformer: ClassComponentTransformer) {
|
|
const originalName = path.node.id.name;
|
|
const tempName = path.node.id.name + 'Temp';
|
|
const classComp = classTransformer.genClassComponent(tempName);
|
|
path.replaceWith(classComp);
|
|
path.scope.rename(tempName, originalName);
|
|
}
|
|
|
|
export class PluginProvider {
|
|
// ---- Plugin Level ----
|
|
private readonly babelApi: typeof babel;
|
|
private readonly t: typeof t;
|
|
|
|
private programNode: t.Program | undefined;
|
|
|
|
constructor(babelApi: typeof babel, options: Option) {
|
|
this.babelApi = babelApi;
|
|
this.t = babelApi.types;
|
|
}
|
|
|
|
functionDeclarationVisitor(path: NodePath<t.FunctionDeclaration>): void {
|
|
// find Component function by:
|
|
// 1. has JSXElement as return value
|
|
// 2. name is capitalized
|
|
if (path.node.id?.name[0] !== path.node.id?.name[0].toUpperCase()) return;
|
|
const returnStatement = path.node.body.body.find(n => this.t.isReturnStatement(n)) as t.ReturnStatement;
|
|
if (!returnStatement) return;
|
|
if (!(this.t.isJSXElement(returnStatement.argument) || this.t.isJSXFragment(returnStatement.argument))) return;
|
|
const classTransformer = new ClassComponentTransformer(this.babelApi, path);
|
|
// transform the parameters to props
|
|
const params = path.node.params;
|
|
// ---
|
|
const props = params[0];
|
|
classTransformer.transformProps(props);
|
|
|
|
// --- env
|
|
const env = params[1];
|
|
classTransformer.transformEnv(env);
|
|
|
|
// iterate the function body orderly
|
|
const body = path.node.body.body;
|
|
body.forEach((node, idx) => {
|
|
if (this.t.isVariableDeclaration(node)) {
|
|
classTransformer.transformStateDeclaration(node);
|
|
return;
|
|
}
|
|
// handle method
|
|
if (this.t.isFunctionDeclaration(node)) {
|
|
classTransformer.transformMethods(node);
|
|
return;
|
|
}
|
|
|
|
// ---- handle lifecycles
|
|
const lifecycles = ['willMount', 'didMount', 'willUnmount', 'didUnmount'];
|
|
if (this.t.isLabeledStatement(node) && lifecycles.includes(node.label.name)) {
|
|
// transform the lifecycle statement to lifecycle method
|
|
classTransformer.transformLifeCycle(node);
|
|
return;
|
|
}
|
|
|
|
// handle watch
|
|
if (this.t.isLabeledStatement(node) && node.label.name === 'watch') {
|
|
// transform the watch statement to watch method
|
|
classTransformer.transformWatch(node);
|
|
return;
|
|
}
|
|
|
|
// handle return statement
|
|
if (this.t.isReturnStatement(node)) {
|
|
// handle early return
|
|
if (idx !== body.length - 1) {
|
|
// transform the return statement to render method
|
|
// TODO: handle early return
|
|
throw new Error('Early return is not supported yet.');
|
|
}
|
|
// transform the return statement to render method
|
|
classTransformer.transformRenderMethod(node);
|
|
return;
|
|
}
|
|
});
|
|
|
|
// replace the function declaration with class declaration
|
|
replaceFnWithClass(path, classTransformer);
|
|
}
|
|
}
|
|
|
|
type ToWatchNode =
|
|
| t.ExpressionStatement
|
|
| t.ForStatement
|
|
| t.WhileStatement
|
|
| t.IfStatement
|
|
| t.SwitchStatement
|
|
| t.TryStatement;
|
|
|
|
class ClassComponentTransformer {
|
|
properties: (t.ClassProperty | t.ClassMethod)[] = [];
|
|
// The expression to bind the nested destructuring props with prop
|
|
nestedDestructuringBindings: t.Expression[] = [];
|
|
private readonly babelApi: typeof babel;
|
|
private readonly t: typeof t;
|
|
private readonly functionScope: Scope;
|
|
|
|
valueWrapper(node) {
|
|
return this.t.file(this.t.program([this.t.isStatement(node) ? node : this.t.expressionStatement(node)]));
|
|
}
|
|
|
|
addProperty(prop: t.ClassProperty | t.ClassMethod, name?: string) {
|
|
this.properties.push(prop);
|
|
}
|
|
|
|
constructor(babelApi: typeof babel, fnNode: NodePath<t.FunctionDeclaration>) {
|
|
this.babelApi = babelApi;
|
|
this.t = babelApi.types;
|
|
// get the function body scope
|
|
this.functionScope = fnNode.scope;
|
|
}
|
|
|
|
// transform function component to class component extends View
|
|
genClassComponent(name: string) {
|
|
// generate ctor and push this.initExpressions to ctor
|
|
let nestedDestructuringBindingsMethod: t.ClassMethod | null = null;
|
|
if (this.nestedDestructuringBindings.length) {
|
|
nestedDestructuringBindingsMethod = this.t.classMethod(
|
|
'method',
|
|
this.t.identifier('$$bindNestDestructuring'),
|
|
[],
|
|
this.t.blockStatement([...this.nestedDestructuringBindings.map(exp => this.t.expressionStatement(exp))])
|
|
);
|
|
nestedDestructuringBindingsMethod.decorators = [this.t.decorator(this.t.identifier(DECORATOR_WATCH))];
|
|
}
|
|
return this.t.classDeclaration(
|
|
this.t.identifier(name),
|
|
this.t.identifier('View'),
|
|
this.t.classBody(
|
|
nestedDestructuringBindingsMethod ? [...this.properties, nestedDestructuringBindingsMethod] : this.properties
|
|
),
|
|
[]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Transform state declaration to class property
|
|
* if the variable is declared with `let` or `const`, it should be transformed to class property
|
|
* @param node
|
|
*/
|
|
transformStateDeclaration(node: t.VariableDeclaration) {
|
|
// iterate the declarations
|
|
node.declarations.forEach(declaration => {
|
|
const id = declaration.id;
|
|
// handle destructuring
|
|
if (this.t.isObjectPattern(id)) {
|
|
return this.transformPropsDestructuring(id);
|
|
} else if (this.t.isArrayPattern(id)) {
|
|
// TODO: handle array destructuring
|
|
} else if (this.t.isIdentifier(id)) {
|
|
// clone the id
|
|
const cloneId = this.t.cloneNode(id);
|
|
this.addProperty(this.t.classProperty(cloneId, declaration.init), id.name);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Transform render method to Body method
|
|
* The Body method should return the original return statement
|
|
* @param node
|
|
*/
|
|
transformRenderMethod(node: t.ReturnStatement) {
|
|
const body = this.t.classMethod(
|
|
'method',
|
|
this.t.identifier('Body'),
|
|
[],
|
|
this.t.blockStatement([node]),
|
|
false,
|
|
false
|
|
);
|
|
this.addProperty(body, 'Body');
|
|
}
|
|
|
|
transformLifeCycle(node: t.LabeledStatement) {
|
|
// transform the lifecycle statement to lifecycle method
|
|
const methodName = node.label.name;
|
|
const method = this.t.classMethod(
|
|
'method',
|
|
this.t.identifier(methodName),
|
|
[],
|
|
this.t.blockStatement(node.body.body),
|
|
false,
|
|
false
|
|
);
|
|
this.addProperty(method, methodName);
|
|
}
|
|
|
|
transformComputed() {}
|
|
|
|
transformMethods(node: t.FunctionDeclaration) {
|
|
// transform the function declaration to class method
|
|
const methodName = node.id?.name;
|
|
if (!methodName) return;
|
|
const method = this.t.classMethod(
|
|
'method',
|
|
this.t.identifier(methodName),
|
|
node.params,
|
|
node.body,
|
|
node.generator,
|
|
node.async
|
|
);
|
|
this.addProperty(method, methodName);
|
|
}
|
|
|
|
transformProps(param: t.Identifier | t.RestElement | t.Pattern) {
|
|
if (!param) return;
|
|
// handle destructuring
|
|
if (this.isObjDestructuring(param)) {
|
|
this.transformPropsDestructuring(param);
|
|
return;
|
|
}
|
|
if (this.t.isIdentifier(param)) {
|
|
// TODO: handle props identifier
|
|
return;
|
|
}
|
|
throw new Error('Unsupported props type, please use object destructuring or identifier.');
|
|
}
|
|
|
|
/**
|
|
* transform node to watch label to watch decorator
|
|
* e.g.
|
|
*
|
|
* watch: console.log(state)
|
|
* // transform into
|
|
* @Watch
|
|
* _watch() {
|
|
* console.log(state)
|
|
* }
|
|
*/
|
|
transformWatch(node: t.LabeledStatement) {
|
|
const id = this.functionScope.generateUidIdentifier(DECORATOR_WATCH.toLowerCase());
|
|
const method = this.t.classMethod('method', id, [], this.t.blockStatement([node.body]), false, false);
|
|
method.decorators = [this.t.decorator(this.t.identifier(DECORATOR_WATCH))];
|
|
this.addProperty(method);
|
|
}
|
|
|
|
private isObjDestructuring(param: t.Identifier | t.RestElement | t.Pattern): param is t.ObjectPattern {
|
|
return this.t.isObjectPattern(param);
|
|
}
|
|
|
|
/**
|
|
* how to handle default value
|
|
* ```js
|
|
* // 1. No alias
|
|
* function({name = 'defaultName'}) {}
|
|
* class A extends View {
|
|
* @Prop name = 'defaultName';
|
|
*
|
|
* // 2. Alias
|
|
* function({name: aliasName = 'defaultName'}) {}
|
|
* class A extends View {
|
|
* @Prop name = 'defaultName';
|
|
* aliasName
|
|
* @Watch
|
|
* bindAliasName() {
|
|
* this.aliasName = this.name;
|
|
* }
|
|
* }
|
|
*
|
|
* // 3. Children with default value and alias
|
|
* function({children: aliasName = 'defaultName'}) {}
|
|
* class A extends View {
|
|
* @Children aliasName = 'defaultName';
|
|
* }
|
|
* ```
|
|
*/
|
|
private transformPropsDestructuring(param: t.ObjectPattern) {
|
|
const propNames: t.Identifier[] = [];
|
|
param.properties.forEach(prop => {
|
|
if (this.t.isObjectProperty(prop)) {
|
|
let key = prop.key;
|
|
let defaultVal: t.Expression;
|
|
if (this.t.isIdentifier(key)) {
|
|
let alias: t.Identifier | null = null;
|
|
if (this.t.isAssignmentPattern(prop.value)) {
|
|
const propName = prop.value.left;
|
|
defaultVal = prop.value.right;
|
|
if (this.t.isIdentifier(propName)) {
|
|
// handle alias
|
|
if (propName.name !== key.name) {
|
|
alias = propName;
|
|
}
|
|
} else {
|
|
throw Error(`Unsupported assignment type in object destructuring: ${propName.type}`);
|
|
}
|
|
} else if (this.t.isIdentifier(prop.value)) {
|
|
// handle alias
|
|
if (key.name !== prop.value.name) {
|
|
alias = prop.value;
|
|
}
|
|
} else if (this.t.isObjectPattern(prop.value)) {
|
|
// TODO: handle nested destructuring
|
|
this.transformPropsDestructuring(prop.value);
|
|
}
|
|
|
|
const isChildren = key.name === 'children';
|
|
if (alias) {
|
|
if (isChildren) {
|
|
key = alias;
|
|
} else {
|
|
this.addClassPropertyForPropAlias(alias, key);
|
|
}
|
|
}
|
|
this.addClassProperty(key, isChildren ? DECORATOR_CHILDREN : DECORATOR_PROPS, defaultVal);
|
|
propNames.push(key);
|
|
return;
|
|
}
|
|
|
|
// handle default value
|
|
if (this.t.isAssignmentPattern(prop.value)) {
|
|
const defaultValue = prop.value.right;
|
|
const propName = prop.value.left;
|
|
//handle alias
|
|
if (this.t.isIdentifier(propName) && propName.name !== prop.key.name) {
|
|
this.addClassProperty(propName, null, undefined);
|
|
}
|
|
|
|
if (this.t.isIdentifier(propName)) {
|
|
this.addClassProperty(propName, DECORATOR_PROPS, defaultValue);
|
|
propNames.push(propName);
|
|
}
|
|
// TODO: handle nested destructuring
|
|
return;
|
|
}
|
|
throw new Error('Unsupported props destructuring, please use simple object destructuring.');
|
|
} else {
|
|
// TODO: handle rest element
|
|
}
|
|
});
|
|
|
|
return propNames;
|
|
}
|
|
|
|
private addClassPropertyForPropAlias(propName: t.Identifier, key: t.Identifier) {
|
|
// handle alias, like class A { foo: bar = 'default' }
|
|
this.addClassProperty(propName, null, undefined);
|
|
// push alias assignment in Watch , like this.bar = this.foo
|
|
this.nestedDestructuringBindings.push(
|
|
this.t.assignmentExpression('=', this.t.identifier(propName.name), this.t.identifier(key.name))
|
|
);
|
|
}
|
|
|
|
// add prop to class, like @prop name = '';
|
|
private addClassProperty(key: t.Identifier, decorator: string | null, defaultValue?: t.Expression) {
|
|
// clone the key to avoid reference issue
|
|
const id = this.t.cloneNode(key);
|
|
this.addProperty(
|
|
this.t.classProperty(
|
|
id,
|
|
defaultValue ?? undefined,
|
|
undefined,
|
|
// use prop decorator
|
|
decorator ? [this.t.decorator(this.t.identifier(decorator))] : undefined,
|
|
undefined,
|
|
false
|
|
),
|
|
key.name
|
|
);
|
|
}
|
|
|
|
// TODO: need refactor, maybe merge with props?
|
|
transformEnv(env: t.Identifier | t.Pattern | t.RestElement) {
|
|
if (!env) {
|
|
return;
|
|
}
|
|
if (!this.t.isObjectPattern(env)) {
|
|
throw Error('Unsupported env type, please use object destructuring.');
|
|
}
|
|
env.properties.forEach(property => {
|
|
if (this.t.isObjectProperty(property)) {
|
|
const key = property.key;
|
|
let defaultVal: t.Expression;
|
|
if (this.t.isIdentifier(key)) {
|
|
let alias: t.Identifier | null = null;
|
|
if (this.t.isAssignmentPattern(property.value)) {
|
|
const propName = property.value.left;
|
|
defaultVal = property.value.right;
|
|
if (this.t.isIdentifier(propName)) {
|
|
// handle alias
|
|
if (propName.name !== key.name) {
|
|
alias = propName;
|
|
}
|
|
} else {
|
|
throw Error(`Unsupported assignment type in object destructuring: ${propName.type}`);
|
|
}
|
|
} else if (this.t.isIdentifier(property.value)) {
|
|
// handle alias
|
|
if (key.name !== property.value.name) {
|
|
alias = property.value;
|
|
}
|
|
} else if (this.t.isObjectPattern(property.value)) {
|
|
throw Error('Unsupported nested env destructuring');
|
|
}
|
|
|
|
if (alias) {
|
|
this.addClassPropertyForPropAlias(alias, key);
|
|
}
|
|
this.addClassProperty(key, DECORATOR_ENV, defaultVal);
|
|
return;
|
|
}
|
|
throw new Error('Unsupported props destructuring, please use simple object destructuring.');
|
|
} else {
|
|
throw new Error('Unsupported env destructuring, please use plain object destructuring.');
|
|
}
|
|
});
|
|
}
|
|
}
|