inula/packages/transpiler/class-transformer/src/pluginProvider.ts

302 lines
9.1 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';
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);
// 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 watch
if (classTransformer.shouldTransformWatch(node)) {
// 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)[] = [];
private readonly babelApi: typeof babel;
private readonly t: typeof t;
private readonly functionScope: Scope;
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) {
return this.t.classDeclaration(
this.t.identifier(name),
this.t.identifier('View'),
this.t.classBody(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() {}
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 method with watch decorator
transformWatch(node: ToWatchNode) {
const id = this.functionScope.generateUidIdentifier('watch');
const method = this.t.classMethod('method', id, [], this.t.blockStatement([node]), false, false);
method.decorators = [this.t.decorator(this.t.identifier('Watch'))];
this.addProperty(method);
}
private isObjDestructuring(param: t.Identifier | t.RestElement | t.Pattern): param is t.ObjectPattern {
return this.t.isObjectPattern(param);
}
private transformPropsDestructuring(param: t.ObjectPattern) {
const propNames: t.Identifier[] = [];
param.properties.forEach(prop => {
if (this.t.isObjectProperty(prop)) {
const key = prop.key;
if (this.t.isIdentifier(key)) {
if (this.t.isAssignmentPattern(prop.value)) {
// handle default value
const defaultValue = prop.value.right;
this.addProp(key, defaultValue);
propNames.push(key);
return;
} else if (this.t.isIdentifier(prop.value)) {
// handle simple destructuring
this.addProp(key, undefined, prop.value.name === 'children');
propNames.push(key);
return;
} else if (this.t.isObjectPattern(prop.value)) {
// TODO: handle nested destructuring
this.transformPropsDestructuring(prop.value);
return;
}
return;
}
// handle default value
if (this.t.isAssignmentPattern(prop.value)) {
const defaultValue = prop.value.right;
const propName = prop.value.left;
if (this.t.isIdentifier(propName)) {
this.addProp(propName, 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;
}
// add prop to class, like @prop name = '';
private addProp(key: t.Identifier, defaultValue?: t.Expression, isChildren = false) {
// 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
[this.t.decorator(this.t.identifier(isChildren ? 'Children' : 'Prop'))],
undefined,
false
),
key.name
);
}
/**
* Check if the node should be transformed to watch method, including:
* 1. call expression.
* 2. for loop
* 3. while loop
* 4. if statement
* 5. switch statement
* 6. assignment expression
* 7. try statement
* 8. ++/-- expression
* @param node
*/
shouldTransformWatch(node: t.Node): node is ToWatchNode {
if (this.t.isExpressionStatement(node)) {
if (this.t.isCallExpression(node.expression)) {
return true;
}
if (this.t.isAssignmentExpression(node.expression)) {
return true;
}
if (this.t.isUpdateExpression(node.expression)) {
return true;
}
}
if (this.t.isForStatement(node)) {
return true;
}
if (this.t.isWhileStatement(node)) {
return true;
}
if (this.t.isIfStatement(node)) {
return true;
}
if (this.t.isSwitchStatement(node)) {
return true;
}
if (this.t.isTryStatement(node)) {
return true;
}
return false;
}
}