feat: add lifecycles and watch

This commit is contained in:
iandxssxx 2024-04-10 22:59:55 -04:00
parent 1f4b164952
commit 325f4c406a
1 changed files with 134 additions and 22 deletions

View File

@ -3,6 +3,10 @@ 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';
function replaceFnWithClass(path: NodePath<t.FunctionDeclaration>, classTransformer: ClassComponentTransformer) {
const originalName = path.node.id.name;
const tempName = path.node.id.name + 'Temp';
@ -49,8 +53,17 @@ export class PluginProvider {
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 (classTransformer.shouldTransformWatch(node)) {
if (this.t.isLabeledStatement(node) && node.label.name === 'watch') {
// transform the watch statement to watch method
classTransformer.transformWatch(node);
return;
@ -85,10 +98,16 @@ type ToWatchNode =
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);
}
@ -102,10 +121,23 @@ class ClassComponentTransformer {
// 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(this.properties),
this.t.classBody(
nestedDestructuringBindingsMethod ? [...this.properties, nestedDestructuringBindingsMethod] : this.properties
),
[]
);
}
@ -149,7 +181,19 @@ class ClassComponentTransformer {
this.addProperty(body, 'Body');
}
transformLifeCycle() {}
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() {}
@ -182,11 +226,21 @@ class ClassComponentTransformer {
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'))];
/**
* 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);
}
@ -194,36 +248,85 @@ class ClassComponentTransformer {
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)) {
const key = prop.key;
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)) {
// handle default value
const defaultValue = prop.value.right;
this.addProp(key, defaultValue);
propNames.push(key);
return;
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 simple destructuring
this.addProp(key, undefined, prop.value.name === 'children');
propNames.push(key);
return;
// 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);
return;
}
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.addProp(propName, defaultValue);
this.addClassProperty(propName, DECORATOR_PROPS, defaultValue);
propNames.push(propName);
}
// TODO: handle nested destructuring
@ -238,8 +341,17 @@ class ClassComponentTransformer {
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 addProp(key: t.Identifier, defaultValue?: t.Expression, isChildren = false) {
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(
@ -248,7 +360,7 @@ class ClassComponentTransformer {
defaultValue ?? undefined,
undefined,
// use prop decorator
[this.t.decorator(this.t.identifier(isChildren ? 'Children' : 'Prop'))],
decorator ? [this.t.decorator(this.t.identifier(decorator))] : undefined,
undefined,
false
),