feat: analyze watch lifeCycle properties
This commit is contained in:
parent
fcc734e05f
commit
5427f13880
|
@ -1,13 +1,12 @@
|
||||||
import { NodePath } from '@babel/core';
|
import { type types as t, type NodePath } from '@babel/core';
|
||||||
import { jsxSlicesAnalyze } from './jsxSliceAnalyze';
|
import { propsAnalyze } from './propsAnalyze';
|
||||||
import { earlyReturnAnalyze } from './earlyReturnAnalyze';
|
|
||||||
import { AnalyzeContext, Analyzer, ComponentNode, CondNode, Visitor } from './types';
|
import { AnalyzeContext, Analyzer, ComponentNode, CondNode, Visitor } from './types';
|
||||||
import { createComponentNode } from './nodeFactory';
|
import { createComponentNode } from './nodeFactory';
|
||||||
import { propertiesAnalyze } from './propertiesAnalyze';
|
import { propertiesAnalyze } from './propertiesAnalyze';
|
||||||
import { isValidComponent } from './utils';
|
import { lifeCycleAnalyze } from './lifeCycleAnalyze';
|
||||||
import * as t from '@babel/types';
|
|
||||||
import { getFnBody } from '../utils';
|
import { getFnBody } from '../utils';
|
||||||
const builtinAnalyzers = [jsxSlicesAnalyze, earlyReturnAnalyze, propertiesAnalyze];
|
|
||||||
|
const builtinAnalyzers = [propsAnalyze, propertiesAnalyze, lifeCycleAnalyze];
|
||||||
let analyzers: Analyzer[] = builtinAnalyzers;
|
let analyzers: Analyzer[] = builtinAnalyzers;
|
||||||
|
|
||||||
export function isCondNode(node: any): node is CondNode {
|
export function isCondNode(node: any): node is CondNode {
|
||||||
|
@ -15,53 +14,73 @@ export function isCondNode(node: any): node is CondNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeVisitor(...visitors: Analyzer[]): Visitor {
|
function mergeVisitor(...visitors: Analyzer[]): Visitor {
|
||||||
return visitors.reduce((acc, cur) => {
|
return visitors.reduce<Visitor<AnalyzeContext>>((acc, cur) => {
|
||||||
return {
|
const visitor = cur();
|
||||||
...acc,
|
const visitorKeys = Object.keys(visitor) as (keyof Visitor)[];
|
||||||
...cur(),
|
for (const key of visitorKeys) {
|
||||||
};
|
if (acc[key]) {
|
||||||
|
// if already exist, merge the visitor function
|
||||||
|
const original = acc[key]!;
|
||||||
|
acc[key] = (path: any, ctx) => {
|
||||||
|
original(path, ctx);
|
||||||
|
visitor[key]?.(path, ctx);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// @ts-expect-error key is a valid key, no idea why it's not working
|
||||||
|
acc[key] = visitor[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// walk through the function component body
|
// walk through the function component body
|
||||||
export function iterateFCBody(bodyStatements: NodePath<t.Statement>[], componentNode: ComponentNode, level = 0) {
|
export function analyzeFnComp(
|
||||||
|
types: typeof t,
|
||||||
|
fnNode: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>,
|
||||||
|
componentNode: ComponentNode,
|
||||||
|
level = 0
|
||||||
|
) {
|
||||||
const visitor = mergeVisitor(...analyzers);
|
const visitor = mergeVisitor(...analyzers);
|
||||||
const visit = (p: NodePath<t.Statement>, ctx: AnalyzeContext) => {
|
const context: AnalyzeContext = {
|
||||||
|
level,
|
||||||
|
t: types,
|
||||||
|
current: componentNode,
|
||||||
|
traverse: (path: NodePath<t.Statement>, ctx: AnalyzeContext) => {
|
||||||
|
path.traverse(visitor, ctx);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// --- analyze the function props ---
|
||||||
|
const params = fnNode.get('params');
|
||||||
|
const props = params[0];
|
||||||
|
if (props) {
|
||||||
|
if (props.isObjectPattern()) {
|
||||||
|
props.get('properties').forEach(prop => {
|
||||||
|
visitor.Prop?.(prop, context);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Component ${componentNode.name}: The first parameter of the function component must be an object pattern`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- analyze the function body ---
|
||||||
|
const bodyStatements = getFnBody(fnNode).get('body');
|
||||||
|
for (let i = 0; i < bodyStatements.length; i++) {
|
||||||
|
const p = bodyStatements[i];
|
||||||
|
|
||||||
const type = p.node.type;
|
const type = p.node.type;
|
||||||
|
|
||||||
// TODO: More type safe way to handle this
|
// TODO: More type safe way to handle this
|
||||||
visitor[type]?.(p as unknown as any, ctx);
|
visitor[type]?.(p as unknown as any, context);
|
||||||
};
|
|
||||||
for (let i = 0; i < bodyStatements.length; i++) {
|
|
||||||
const p = bodyStatements[i];
|
|
||||||
let skipRest = false;
|
|
||||||
const context: AnalyzeContext = {
|
|
||||||
level,
|
|
||||||
index: i,
|
|
||||||
currentComponent: componentNode,
|
|
||||||
restStmt: bodyStatements.slice(i + 1),
|
|
||||||
skipRest() {
|
|
||||||
skipRest = true;
|
|
||||||
},
|
|
||||||
traverse: (path: NodePath<t.Statement>, ctx: AnalyzeContext) => {
|
|
||||||
// @ts-expect-error TODO: fix visitor type incompatibility
|
|
||||||
path.traverse(visitor, ctx);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
visit(p, context);
|
|
||||||
|
|
||||||
if (p.isReturnStatement()) {
|
if (p.isReturnStatement()) {
|
||||||
visitor.ReturnStatement?.(p, context);
|
visitor.ReturnStatement?.(p, context);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (skipRest) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The process of analyzing the component
|
* The process of analyzing the component
|
||||||
* 1. identify the component
|
* 1. identify the component
|
||||||
|
@ -69,10 +88,13 @@ export function iterateFCBody(bodyStatements: NodePath<t.Statement>[], component
|
||||||
* 2. identify the component's props, including children, alias, and default value
|
* 2. identify the component's props, including children, alias, and default value
|
||||||
* 3. analyze the early return of the component, build into the branch
|
* 3. analyze the early return of the component, build into the branch
|
||||||
*
|
*
|
||||||
|
* @param types
|
||||||
|
* @param fnName
|
||||||
* @param path
|
* @param path
|
||||||
* @param customAnalyzers
|
* @param customAnalyzers
|
||||||
*/
|
*/
|
||||||
export function analyze(
|
export function analyze(
|
||||||
|
types: typeof t,
|
||||||
fnName: string,
|
fnName: string,
|
||||||
path: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>,
|
path: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>,
|
||||||
customAnalyzers?: Analyzer[]
|
customAnalyzers?: Analyzer[]
|
||||||
|
@ -81,7 +103,8 @@ export function analyze(
|
||||||
analyzers = customAnalyzers;
|
analyzers = customAnalyzers;
|
||||||
}
|
}
|
||||||
|
|
||||||
const root = createComponentNode(fnName, getFnBody(path));
|
const root = createComponentNode(fnName, path);
|
||||||
|
analyzeFnComp(types, path, root);
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,13 +32,16 @@ function isLifeCycleName(name: string): name is LifeCycle {
|
||||||
*/
|
*/
|
||||||
export function lifeCycleAnalyze(): Visitor {
|
export function lifeCycleAnalyze(): Visitor {
|
||||||
return {
|
return {
|
||||||
CallExpression(path: NodePath<t.CallExpression>, ctx) {
|
ExpressionStatement(path: NodePath<t.ExpressionStatement>, ctx) {
|
||||||
const callee = path.get('callee');
|
const expression = path.get('expression');
|
||||||
if (callee.isIdentifier(path)) {
|
if (expression.isCallExpression()) {
|
||||||
const lifeCycleName = callee.node.name;
|
const callee = expression.get('callee');
|
||||||
if (isLifeCycleName(lifeCycleName)) {
|
if (callee.isIdentifier()) {
|
||||||
const fnNode = extractFnFromMacro(path, lifeCycleName);
|
const lifeCycleName = callee.node.name;
|
||||||
addLifecycle(ctx.currentComponent, lifeCycleName, getFnBody(fnNode));
|
if (isLifeCycleName(lifeCycleName)) {
|
||||||
|
const fnNode = extractFnFromMacro(expression, lifeCycleName);
|
||||||
|
addLifecycle(ctx.current, lifeCycleName, getFnBody(fnNode));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,44 +15,87 @@
|
||||||
|
|
||||||
import { NodePath, type types as t } from '@babel/core';
|
import { NodePath, type types as t } from '@babel/core';
|
||||||
import { Branch, ComponentNode, CondNode, InulaNode, JSX, JSXNode, LifeCycle, SubCompNode } from './types';
|
import { Branch, ComponentNode, CondNode, InulaNode, JSX, JSXNode, LifeCycle, SubCompNode } from './types';
|
||||||
import { iterateFCBody } from './index';
|
import { PropType } from '../constants';
|
||||||
|
|
||||||
export function createComponentNode(
|
export function createComponentNode(
|
||||||
name: string,
|
name: string,
|
||||||
fnBody: NodePath<t.Statement>[],
|
fnNode: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>,
|
||||||
parent?: ComponentNode
|
parent?: ComponentNode
|
||||||
): ComponentNode {
|
): ComponentNode {
|
||||||
const comp: ComponentNode = {
|
const comp: ComponentNode = {
|
||||||
type: 'comp',
|
type: 'comp',
|
||||||
name,
|
name,
|
||||||
props: {},
|
props: [],
|
||||||
child: undefined,
|
child: undefined,
|
||||||
subComponents: [],
|
subComponents: [],
|
||||||
methods: [],
|
properties: [],
|
||||||
state: [],
|
dependencyMap: {},
|
||||||
|
reactiveMap: {},
|
||||||
|
lifecycle: {},
|
||||||
parent,
|
parent,
|
||||||
fnBody,
|
// fnBody,
|
||||||
|
get availableProperties() {
|
||||||
|
return comp.properties
|
||||||
|
.filter(({ isMethod }) => !isMethod)
|
||||||
|
.map(({ name }) => name)
|
||||||
|
.concat(
|
||||||
|
comp.props
|
||||||
|
.map(({ name, nestedProps, alias }) => {
|
||||||
|
const nested = nestedProps ? nestedProps.map(name => name) : [];
|
||||||
|
return [alias ? alias : name, ...nested];
|
||||||
|
})
|
||||||
|
.flat()
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
iterateFCBody(fnBody, comp);
|
|
||||||
|
|
||||||
return comp;
|
return comp;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addState(comp: ComponentNode, name: string, value: t.Expression | null) {
|
export function addProperty(
|
||||||
comp.state.push({ name, value });
|
comp: ComponentNode,
|
||||||
|
name: string,
|
||||||
|
value: t.Expression | null,
|
||||||
|
isComputed: boolean,
|
||||||
|
isMethod = false
|
||||||
|
) {
|
||||||
|
comp.properties.push({ name, value, isComputed, isMethod });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addMethod(comp: ComponentNode, method: NodePath<t.FunctionDeclaration>) {
|
export function addMethod(comp: ComponentNode, name: string, value: t.Expression | null) {
|
||||||
comp.methods.push(method);
|
comp.properties.push({ name, value, isComputed: false, isMethod: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addLifecycle(comp: ComponentNode, lifeCycle: LifeCycle, stmts: NodePath<t.Statement>[]) {
|
export function addProp(
|
||||||
|
comp: ComponentNode,
|
||||||
|
type: PropType,
|
||||||
|
key: string,
|
||||||
|
defaultVal: t.Expression | null = null,
|
||||||
|
alias: string | null = null,
|
||||||
|
nestedProps: string[] | null = null,
|
||||||
|
nestedRelationship: t.ObjectPattern | t.ArrayPattern | null = null
|
||||||
|
) {
|
||||||
|
comp.props.push({ name: key, type, default: defaultVal, alias, nestedProps, nestedRelationship });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addLifecycle(comp: ComponentNode, lifeCycle: LifeCycle, block: NodePath<t.BlockStatement>) {
|
||||||
const compLifecycle = comp.lifecycle;
|
const compLifecycle = comp.lifecycle;
|
||||||
if (!compLifecycle[lifeCycle]) {
|
if (!compLifecycle[lifeCycle]) {
|
||||||
compLifecycle[lifeCycle] = [];
|
compLifecycle[lifeCycle] = [];
|
||||||
}
|
}
|
||||||
compLifecycle[lifeCycle].push(stmts);
|
compLifecycle[lifeCycle]!.push(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addWatch(
|
||||||
|
comp: ComponentNode,
|
||||||
|
callback: NodePath<t.ArrowFunctionExpression> | NodePath<t.FunctionExpression>,
|
||||||
|
deps: NodePath<t.ArrayExpression> | null
|
||||||
|
) {
|
||||||
|
// if watch not exist, create a new one
|
||||||
|
if (!comp.watch) {
|
||||||
|
comp.watch = [];
|
||||||
|
}
|
||||||
|
comp.watch.push({ callback, deps });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createJSXNode(parent: ComponentNode, content: NodePath<JSX>): JSXNode {
|
export function createJSXNode(parent: ComponentNode, content: NodePath<JSX>): JSXNode {
|
||||||
|
|
|
@ -13,40 +13,13 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NodePath } from '@babel/core';
|
import { AnalyzeContext, Visitor } from './types';
|
||||||
|
import { addLifecycle, addMethod, addProperty } from './nodeFactory';
|
||||||
|
import { isValidPath } from './utils';
|
||||||
|
import { type types as t, type NodePath } from '@babel/core';
|
||||||
|
import { reactivityFuncNames } from '../const';
|
||||||
|
import { types } from '../babelTypes';
|
||||||
|
|
||||||
import { Visitor } from './types';
|
|
||||||
import { addMethod, addState } from './nodeFactory';
|
|
||||||
import { hasJSX, isValidComponentName, isValidPath } from './utils';
|
|
||||||
import { jsxSlicesAnalyze } from './jsxSliceAnalyze';
|
|
||||||
import * as t from '@babel/types';
|
|
||||||
|
|
||||||
// Analyze the JSX slice in the function component, including:
|
|
||||||
// 1. VariableDeclaration, like `const a = <div />`
|
|
||||||
// 2. SubComponent, like `function Sub() { return <div /> }`
|
|
||||||
function handleFn(fnName: string, fnBody: NodePath<t.BlockStatement>) {
|
|
||||||
if (isValidComponentName(fnName)) {
|
|
||||||
// This is a subcomponent, treat it as a normal component
|
|
||||||
} else {
|
|
||||||
// This is jsx creation function
|
|
||||||
// function jsxFunc() {
|
|
||||||
// // This is a function that returns JSX
|
|
||||||
// // because the function name is smallCamelCased
|
|
||||||
// return <div>{count}</div>
|
|
||||||
// }
|
|
||||||
// =>
|
|
||||||
// function jsxFunc() {
|
|
||||||
// function Comp_$id4$() {
|
|
||||||
// return <div>{count}</div>
|
|
||||||
// }
|
|
||||||
// // This is a function that returns JSX
|
|
||||||
// // because the function name is smallCamelCased
|
|
||||||
// return <Comp_$id4$/>
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. jsx creation function, like `function create() { return <div /> }`
|
|
||||||
export function propertiesAnalyze(): Visitor {
|
export function propertiesAnalyze(): Visitor {
|
||||||
return {
|
return {
|
||||||
VariableDeclaration(path: NodePath<t.VariableDeclaration>, ctx) {
|
VariableDeclaration(path: NodePath<t.VariableDeclaration>, ctx) {
|
||||||
|
@ -61,43 +34,121 @@ export function propertiesAnalyze(): Visitor {
|
||||||
// TODO: handle array destructuring
|
// TODO: handle array destructuring
|
||||||
throw new Error('Array destructuring is not supported yet');
|
throw new Error('Array destructuring is not supported yet');
|
||||||
} else if (id.isIdentifier()) {
|
} else if (id.isIdentifier()) {
|
||||||
|
// --- properties: the state / computed / plain properties / methods---
|
||||||
const init = declaration.get('init');
|
const init = declaration.get('init');
|
||||||
if (isValidPath(init) && hasJSX(init)) {
|
let deps: string[] | null = null;
|
||||||
if (init.isArrowFunctionExpression()) {
|
if (isValidPath(init)) {
|
||||||
const fnName = id.node.name;
|
if (init.isArrowFunctionExpression() || init.isFunctionExpression()) {
|
||||||
const fnBody = init.get('body');
|
addMethod(ctx.current, id.node.name, init.node);
|
||||||
|
return;
|
||||||
// handle case like `const jsxFunc = () => <div />`
|
|
||||||
if (fnBody.isExpression()) {
|
|
||||||
// turn expression into block statement for consistency
|
|
||||||
fnBody.replaceWith(t.blockStatement([t.returnStatement(fnBody.node)]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// We switched to the block statement above, so we can safely call handleFn
|
|
||||||
handleFn(fnName, fnBody as NodePath<t.BlockStatement>);
|
|
||||||
}
|
}
|
||||||
// handle jsx slice
|
deps = getDependenciesFromNode(id.node.name, init, ctx);
|
||||||
ctx.traverse(path, ctx);
|
|
||||||
}
|
}
|
||||||
addState(ctx.currentComponent, id.node.name, declaration.node.init || null);
|
addProperty(ctx.current, id.node.name, init.node || null, !!deps?.length);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
FunctionDeclaration(path: NodePath<t.FunctionDeclaration>, ctx) {
|
FunctionDeclaration(path: NodePath<t.FunctionDeclaration>, { current }) {
|
||||||
const fnId = path.node.id;
|
const fnId = path.node.id;
|
||||||
if (!fnId) {
|
if (!fnId) {
|
||||||
// This is an anonymous function, collect into lifecycle
|
throw new Error('Function declaration must have an id');
|
||||||
//TODO
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasJSX(path)) {
|
const functionExpression = types.functionExpression(
|
||||||
// This is a normal function, collect into methods
|
path.node.id,
|
||||||
addMethod(ctx.currentComponent, path);
|
path.node.params,
|
||||||
return;
|
path.node.body,
|
||||||
}
|
path.node.generator,
|
||||||
|
path.node.async
|
||||||
handleFn(fnId.name, path.get('body'));
|
);
|
||||||
|
addMethod(current, fnId.name, functionExpression);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get all valid dependencies of a babel path
|
||||||
|
* @param propertyKey
|
||||||
|
* @param path
|
||||||
|
* @param ctx
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function getDependenciesFromNode(
|
||||||
|
propertyKey: string,
|
||||||
|
path: NodePath<t.Expression | t.ClassDeclaration>,
|
||||||
|
{ current }: AnalyzeContext
|
||||||
|
) {
|
||||||
|
// ---- Deps: console.log(this.count)
|
||||||
|
const deps = new Set<string>();
|
||||||
|
// ---- Assign deps: this.count = 1 / this.count++
|
||||||
|
const assignDeps = new Set<string>();
|
||||||
|
const visitor = (innerPath: NodePath<t.Identifier>) => {
|
||||||
|
const propertyKey = innerPath.node.name;
|
||||||
|
if (isAssignmentExpressionLeft(innerPath) || isAssignmentFunction(innerPath)) {
|
||||||
|
assignDeps.add(propertyKey);
|
||||||
|
} else if (current.availableProperties.includes(propertyKey)) {
|
||||||
|
deps.add(propertyKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (path.isIdentifier()) {
|
||||||
|
visitor(path);
|
||||||
|
}
|
||||||
|
path.traverse({
|
||||||
|
Identifier: visitor,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- Eliminate deps that are assigned in the same method
|
||||||
|
// e.g. { console.log(this.count); this.count = 1 }
|
||||||
|
// this will cause infinite loop
|
||||||
|
// so we eliminate "count" from deps
|
||||||
|
assignDeps.forEach(dep => {
|
||||||
|
deps.delete(dep);
|
||||||
|
});
|
||||||
|
|
||||||
|
const depArr = [...deps];
|
||||||
|
if (deps.size > 0) {
|
||||||
|
current.dependencyMap[propertyKey] = depArr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return depArr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if it's the left side of an assignment expression, e.g. count = 1
|
||||||
|
* @param innerPath
|
||||||
|
* @returns assignment expression
|
||||||
|
*/
|
||||||
|
function isAssignmentExpressionLeft(innerPath: NodePath): NodePath | null {
|
||||||
|
let parentPath = innerPath.parentPath;
|
||||||
|
while (parentPath && !parentPath.isStatement()) {
|
||||||
|
if (parentPath.isAssignmentExpression()) {
|
||||||
|
if (parentPath.node.left === innerPath.node) return parentPath;
|
||||||
|
const leftPath = parentPath.get('left') as NodePath;
|
||||||
|
if (innerPath.isDescendant(leftPath)) return parentPath;
|
||||||
|
} else if (parentPath.isUpdateExpression()) {
|
||||||
|
return parentPath;
|
||||||
|
}
|
||||||
|
parentPath = parentPath.parentPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if it's a reactivity function, e.g. arr.push
|
||||||
|
* @param innerPath
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function isAssignmentFunction(innerPath: NodePath): boolean {
|
||||||
|
let parentPath = innerPath.parentPath;
|
||||||
|
|
||||||
|
while (parentPath && parentPath.isMemberExpression()) {
|
||||||
|
parentPath = parentPath.parentPath;
|
||||||
|
}
|
||||||
|
if (!parentPath) return false;
|
||||||
|
return (
|
||||||
|
parentPath.isCallExpression() &&
|
||||||
|
parentPath.get('callee').isIdentifier() &&
|
||||||
|
reactivityFuncNames.includes((parentPath.get('callee').node as t.Identifier).name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { type types as t, type NodePath } from '@babel/core';
|
||||||
|
import { AnalyzeContext, Visitor } from './types';
|
||||||
|
import { addProp } from './nodeFactory';
|
||||||
|
import { PropType } from '../constants';
|
||||||
|
import { types } from '../babelTypes';
|
||||||
|
function analyzeSingleProp(
|
||||||
|
value: t.ObjectProperty['value'],
|
||||||
|
key: string,
|
||||||
|
path: NodePath<t.ObjectProperty>,
|
||||||
|
{ t, current }: AnalyzeContext
|
||||||
|
) {
|
||||||
|
let defaultVal: t.Expression | null = null;
|
||||||
|
let alias: string | null = null;
|
||||||
|
const nestedProps: string[] | null = [];
|
||||||
|
let nestedRelationship: t.ObjectPattern | t.ArrayPattern | null = null;
|
||||||
|
if (t.isIdentifier(value)) {
|
||||||
|
// 1. handle alias without default value
|
||||||
|
// handle alias without default value
|
||||||
|
if (key !== value.name) {
|
||||||
|
alias = value.name;
|
||||||
|
}
|
||||||
|
} else if (t.isAssignmentPattern(value)) {
|
||||||
|
// 2. handle default value case
|
||||||
|
const assignedName = value.left;
|
||||||
|
defaultVal = value.right;
|
||||||
|
if (t.isIdentifier(assignedName)) {
|
||||||
|
if (assignedName.name !== key) {
|
||||||
|
// handle alias in default value case
|
||||||
|
alias = assignedName.name;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Error(`Unsupported assignment type in object destructuring: ${assignedName.type}`);
|
||||||
|
}
|
||||||
|
} else if (t.isObjectPattern(value) || t.isArrayPattern(value)) {
|
||||||
|
// 3. nested destructuring
|
||||||
|
// we should collect the identifier that can be used in the function body as the prop
|
||||||
|
// e.g. function ({prop1, prop2: [p20X, {p211, p212: p212X}]}
|
||||||
|
// we should collect prop1, p20X, p211, p212X
|
||||||
|
path.get('value').traverse({
|
||||||
|
Identifier(path) {
|
||||||
|
// judge if the identifier is a prop
|
||||||
|
// 1. is the key of the object property and doesn't have alias
|
||||||
|
// 2. is the item of the array pattern and doesn't have alias
|
||||||
|
// 3. is alias of the object property
|
||||||
|
const parentPath = path.parentPath;
|
||||||
|
if (parentPath.isObjectProperty() && path.parentKey === 'value') {
|
||||||
|
// collect alias of the object property
|
||||||
|
nestedProps.push(path.node.name);
|
||||||
|
} else if (
|
||||||
|
parentPath.isArrayPattern() ||
|
||||||
|
parentPath.isObjectPattern() ||
|
||||||
|
parentPath.isRestElement() ||
|
||||||
|
(parentPath.isAssignmentPattern() && path.key === 'left')
|
||||||
|
) {
|
||||||
|
// collect the key of the object property or the item of the array pattern
|
||||||
|
nestedProps.push(path.node.name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
nestedRelationship = value;
|
||||||
|
}
|
||||||
|
addProp(current, PropType.SINGLE, key, defaultVal, alias, nestedProps, nestedRelationship);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyze the props deconstructing in the function component
|
||||||
|
* 1. meet identifier, just collect the name
|
||||||
|
* 2. has alias, collect the alias name
|
||||||
|
* 3. has default value, collect the default value
|
||||||
|
* 4. has rest element, collect the rest element
|
||||||
|
* 5. nested destructuring, the e2e goal:
|
||||||
|
* ```js
|
||||||
|
* function(prop1, prop2: [p20, p21]) {}
|
||||||
|
* // transform into
|
||||||
|
* function({ prop1, prop2: [p20, p21] }) {
|
||||||
|
* let p20, p21
|
||||||
|
* watch(() => {
|
||||||
|
* [p20, p21] = prop2
|
||||||
|
* })
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function propsAnalyze(): Visitor {
|
||||||
|
return {
|
||||||
|
Prop(path: NodePath<t.ObjectProperty | t.RestElement>, ctx) {
|
||||||
|
if (path.isObjectProperty()) {
|
||||||
|
// --- normal property ---
|
||||||
|
const key = path.node.key;
|
||||||
|
const value = path.node.value;
|
||||||
|
if (types.isIdentifier(key) || types.isStringLiteral(key)) {
|
||||||
|
const name = types.isIdentifier(key) ? key.name : key.value;
|
||||||
|
analyzeSingleProp(value, name, path, ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Error(`Unsupported key type in object destructuring: ${key.type}`);
|
||||||
|
} else {
|
||||||
|
// --- rest element ---
|
||||||
|
const arg = path.get('argument');
|
||||||
|
if (!Array.isArray(arg) && arg.isIdentifier()) {
|
||||||
|
addProp(ctx.current, PropType.REST, arg.node.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -13,9 +13,9 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NodePath, types as t } from '@babel/core';
|
import { type NodePath, types as t } from '@babel/core';
|
||||||
import { Node } from '@babel/traverse';
|
import { Node } from '@babel/traverse';
|
||||||
import { ON_MOUNT, ON_UNMOUNT, WILL_MOUNT, WILL_UNMOUNT } from '../constants';
|
import { ON_MOUNT, ON_UNMOUNT, PropType, WILL_MOUNT, WILL_UNMOUNT } from '../constants';
|
||||||
|
|
||||||
// --- Node shape ---
|
// --- Node shape ---
|
||||||
export type InulaNode = ComponentNode | CondNode | JSXNode;
|
export type InulaNode = ComponentNode | CondNode | JSXNode;
|
||||||
|
@ -23,35 +23,54 @@ export type JSX = t.JSXElement | t.JSXFragment;
|
||||||
export type LifeCycle = typeof WILL_MOUNT | typeof ON_MOUNT | typeof WILL_UNMOUNT | typeof ON_UNMOUNT;
|
export type LifeCycle = typeof WILL_MOUNT | typeof ON_MOUNT | typeof WILL_UNMOUNT | typeof ON_UNMOUNT;
|
||||||
type defaultVal = any | null;
|
type defaultVal = any | null;
|
||||||
type Bitmap = number;
|
type Bitmap = number;
|
||||||
interface Reactive {
|
interface Property {
|
||||||
name: string;
|
name: string;
|
||||||
value: t.Expression | null;
|
value: t.Expression | null;
|
||||||
// indicate the value is a state or computed or watch
|
// indicate the value is a state or computed or watch
|
||||||
listeners: string[];
|
listeners?: string[];
|
||||||
bitmap: Bitmap;
|
bitmap?: Bitmap;
|
||||||
// need a flag for computed to gen a getter
|
// need a flag for computed to gen a getter
|
||||||
// watch is a static computed
|
// watch is a static computed
|
||||||
isComputed: boolean;
|
isComputed: boolean;
|
||||||
|
isMethod: boolean;
|
||||||
|
}
|
||||||
|
interface Prop {
|
||||||
|
name: string;
|
||||||
|
type: PropType;
|
||||||
|
alias: string | null;
|
||||||
|
default: t.Expression | null;
|
||||||
|
nestedProps: string[] | null;
|
||||||
|
nestedRelationship: t.ObjectPattern | t.ArrayPattern | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComponentNode {
|
export interface ComponentNode {
|
||||||
type: 'comp';
|
type: 'comp';
|
||||||
name: string;
|
name: string;
|
||||||
props: Record<string, defaultVal>;
|
props: Prop[];
|
||||||
// A valuable could be a state or computed
|
// A properties could be a state or computed
|
||||||
valuable: Reactive;
|
properties: Property[];
|
||||||
methods: NodePath<t.FunctionDeclaration>[];
|
availableProperties: string[];
|
||||||
|
/**
|
||||||
|
* The map to find the dependencies
|
||||||
|
*/
|
||||||
|
dependencyMap: {
|
||||||
|
[key: string]: string[];
|
||||||
|
};
|
||||||
child?: InulaNode;
|
child?: InulaNode;
|
||||||
subComponents: ComponentNode[];
|
subComponents?: ComponentNode[];
|
||||||
parent?: ComponentNode;
|
parent?: ComponentNode;
|
||||||
/**
|
/**
|
||||||
* The function body of the fn component code
|
* The function body of the fn component code
|
||||||
*/
|
*/
|
||||||
// fnBody: NodePath<t.Statement>[];
|
fnBody: NodePath<t.Statement>[];
|
||||||
// a map to find the state
|
/**
|
||||||
|
* The map to find the state
|
||||||
|
*/
|
||||||
reactiveMap: Record<string, Bitmap>;
|
reactiveMap: Record<string, Bitmap>;
|
||||||
level: number;
|
lifecycle: Partial<Record<LifeCycle, NodePath<t.Statement>[]>>;
|
||||||
lifecycle: Record<LifeCycle, NodePath<t.Statement>[][]>;
|
watch?: {
|
||||||
|
deps: NodePath<t.ArrayExpression> | null;
|
||||||
|
callback: NodePath<t.ArrowFunctionExpression> | NodePath<t.FunctionExpression>;
|
||||||
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SubCompNode {
|
export interface SubCompNode {
|
||||||
|
@ -84,19 +103,15 @@ export interface Branch {
|
||||||
|
|
||||||
export interface AnalyzeContext {
|
export interface AnalyzeContext {
|
||||||
level: number;
|
level: number;
|
||||||
index: number;
|
t: typeof t;
|
||||||
currentComponent: ComponentNode;
|
current: ComponentNode;
|
||||||
restStmt: NodePath<t.Statement>[];
|
|
||||||
// --- flow control ---
|
|
||||||
/**
|
|
||||||
* ignore the rest of the statements
|
|
||||||
*/
|
|
||||||
skipRest: () => void;
|
|
||||||
traverse: (p: NodePath<t.Statement>, ctx: AnalyzeContext) => void;
|
traverse: (p: NodePath<t.Statement>, ctx: AnalyzeContext) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Visitor<S = AnalyzeContext> = {
|
export type Visitor<S = AnalyzeContext> = {
|
||||||
[Type in Node['type']]?: (path: NodePath<Extract<Node, { type: Type }>>, state: S) => void;
|
[Type in Node['type']]?: (path: NodePath<Extract<Node, { type: Type }>>, state: S) => void;
|
||||||
|
} & {
|
||||||
|
Prop?: (path: NodePath<t.ObjectProperty | t.RestElement>, state: S) => void;
|
||||||
};
|
};
|
||||||
export type Analyzer = () => Visitor;
|
export type Analyzer = () => Visitor;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NodePath } from '@babel/core';
|
||||||
|
import { Visitor } from './types';
|
||||||
|
import { addWatch } from './nodeFactory';
|
||||||
|
import * as t from '@babel/types';
|
||||||
|
import { WATCH } from '../constants';
|
||||||
|
import { extractFnFromMacro } from '../utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyze the watch in the function component
|
||||||
|
*/
|
||||||
|
export function watchAnalyze(): Visitor {
|
||||||
|
return {
|
||||||
|
ExpressionStatement(path: NodePath<t.ExpressionStatement>, ctx) {
|
||||||
|
const callExpression = path.get('expression');
|
||||||
|
if (callExpression.isCallExpression()) {
|
||||||
|
const callee = callExpression.get('callee');
|
||||||
|
if (callee.isIdentifier() && callee.node.name === WATCH) {
|
||||||
|
const fnNode = extractFnFromMacro(callExpression, WATCH);
|
||||||
|
const deps = getWatchDeps(callExpression);
|
||||||
|
addWatch(ctx.current, fnNode, deps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWatchDeps(callExpression: NodePath<t.CallExpression>) {
|
||||||
|
const args = callExpression.get('arguments');
|
||||||
|
if (!args[1]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let deps: null | NodePath<t.ArrayExpression> = null;
|
||||||
|
if (args[1].isArrayExpression()) {
|
||||||
|
deps = args[1];
|
||||||
|
} else {
|
||||||
|
console.error('watch deps should be an array expression');
|
||||||
|
}
|
||||||
|
return deps;
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { type types as t } from '@babel/core';
|
||||||
|
let _t: null | typeof types = null;
|
||||||
|
|
||||||
|
export const register = (types: typeof t) => {
|
||||||
|
_t = types;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const types = new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get: (_, p, receiver) => {
|
||||||
|
if (!_t) {
|
||||||
|
throw new Error('Please call register() before using the babel types');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p in _t) {
|
||||||
|
return Reflect.get(_t, p, receiver);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
) as typeof t;
|
|
@ -1,5 +1,29 @@
|
||||||
export const COMPONENT = 'Component';
|
export const COMPONENT = 'Component';
|
||||||
export const WILL_MOUNT = 'willMount';
|
export const WILL_MOUNT = 'willMount';
|
||||||
export const ON_MOUNT = 'onMount';
|
export const ON_MOUNT = 'onMount';
|
||||||
export const WILL_UNMOUNT = 'willUnMount';
|
export const WILL_UNMOUNT = 'willUnmount';
|
||||||
export const ON_UNMOUNT = 'onUnmount';
|
export const ON_UNMOUNT = 'onUnmount';
|
||||||
|
export const WATCH = 'watch';
|
||||||
|
export enum PropType {
|
||||||
|
REST = 'rest',
|
||||||
|
SINGLE = 'single',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reactivityFuncNames = [
|
||||||
|
// ---- Array
|
||||||
|
'push',
|
||||||
|
'pop',
|
||||||
|
'shift',
|
||||||
|
'unshift',
|
||||||
|
'splice',
|
||||||
|
'sort',
|
||||||
|
'reverse',
|
||||||
|
// ---- Set
|
||||||
|
'add',
|
||||||
|
'delete',
|
||||||
|
'clear',
|
||||||
|
// ---- Map
|
||||||
|
'set',
|
||||||
|
'delete',
|
||||||
|
'clear',
|
||||||
|
];
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import type babel from '@babel/core';
|
import type babel from '@babel/core';
|
||||||
import { type PluginObj } from '@babel/core';
|
import { type PluginObj } from '@babel/core';
|
||||||
import { PluginProviderClass } from './pluginProvider';
|
|
||||||
import { type DLightOption } from './types';
|
import { type DLightOption } from './types';
|
||||||
import { defaultAttributeMap } from './const';
|
import { defaultAttributeMap } from './const';
|
||||||
import { analyze } from './analyze';
|
import { analyze } from './analyze';
|
||||||
import { NodePath, type types as t } from '@babel/core';
|
import { NodePath, type types as t } from '@babel/core';
|
||||||
import { COMPONENT } from './constants';
|
import { COMPONENT } from './constants';
|
||||||
import { extractFnFromMacro } from './utils';
|
import { extractFnFromMacro } from './utils';
|
||||||
|
import { register } from './babelTypes';
|
||||||
|
|
||||||
export default function (api: typeof babel, options: DLightOption): PluginObj {
|
export default function (api: typeof babel, options: DLightOption): PluginObj {
|
||||||
const { types } = api;
|
const { types } = api;
|
||||||
|
@ -18,23 +18,16 @@ export default function (api: typeof babel, options: DLightOption): PluginObj {
|
||||||
attributeMap = defaultAttributeMap,
|
attributeMap = defaultAttributeMap,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const pluginProvider = new PluginProviderClass(
|
register(types);
|
||||||
api,
|
|
||||||
types,
|
|
||||||
Array.isArray(files) ? files : [files],
|
|
||||||
Array.isArray(excludeFiles) ? excludeFiles : [excludeFiles],
|
|
||||||
enableDevTools,
|
|
||||||
htmlTags,
|
|
||||||
attributeMap
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
visitor: {
|
visitor: {
|
||||||
Program: {
|
Program: {
|
||||||
enter(path, { filename }) {
|
enter(path, { filename }) {
|
||||||
return pluginProvider.programEnterVisitor(path, filename);
|
// return pluginProvider.programEnterVisitor(path, filename);
|
||||||
|
},
|
||||||
|
exit(path, { filename }) {
|
||||||
|
// pluginProvider.programExitVisitor.bind(pluginProvider);
|
||||||
},
|
},
|
||||||
exit: pluginProvider.programExitVisitor.bind(pluginProvider),
|
|
||||||
},
|
},
|
||||||
CallExpression(path: NodePath<t.CallExpression>) {
|
CallExpression(path: NodePath<t.CallExpression>) {
|
||||||
// find the component, like: Component(() => {})
|
// find the component, like: Component(() => {})
|
||||||
|
@ -52,7 +45,7 @@ export default function (api: typeof babel, options: DLightOption): PluginObj {
|
||||||
console.error('Component macro must be assigned to a variable');
|
console.error('Component macro must be assigned to a variable');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const root = analyze(name, componentNode);
|
const root = analyze(types, name, componentNode);
|
||||||
// The sub path has been visited, so we just skip
|
// The sub path has been visited, so we just skip
|
||||||
path.skip();
|
path.skip();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
// Auto Naming for Component and Hook
|
|
@ -14,14 +14,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NodePath, type types as t } from '@babel/core';
|
import { NodePath, type types as t } from '@babel/core';
|
||||||
import { createComponentNode, createCondNode, createJSXNode } from './nodeFactory';
|
import { createComponentNode, createCondNode, createJSXNode } from '../analyze/nodeFactory';
|
||||||
import { AnalyzeContext, Branch, Visitor } from './types';
|
import { AnalyzeContext, Branch, Visitor } from '../analyze/types';
|
||||||
import { isValidPath } from './utils';
|
import { isValidPath } from '../analyze/utils';
|
||||||
|
|
||||||
export function earlyReturnAnalyze(): Visitor {
|
export function earlyReturnPlugin(): Visitor {
|
||||||
return {
|
return {
|
||||||
ReturnStatement(path: NodePath<t.ReturnStatement>, context: AnalyzeContext) {
|
ReturnStatement(path: NodePath<t.ReturnStatement>, context: AnalyzeContext) {
|
||||||
const currentComp = context.currentComponent;
|
const currentComp = context.current;
|
||||||
|
|
||||||
const argument = path.get('argument');
|
const argument = path.get('argument');
|
||||||
if (argument.isJSXElement()) {
|
if (argument.isJSXElement()) {
|
||||||
|
@ -32,7 +32,7 @@ export function earlyReturnAnalyze(): Visitor {
|
||||||
if (!hasEarlyReturn(ifStmt)) {
|
if (!hasEarlyReturn(ifStmt)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const currentComp = context.currentComponent;
|
const currentComp = context.current;
|
||||||
|
|
||||||
const branches: Branch[] = [];
|
const branches: Branch[] = [];
|
||||||
let next: NodePath<t.Statement> | null = ifStmt;
|
let next: NodePath<t.Statement> | null = ifStmt;
|
|
@ -14,12 +14,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NodePath } from '@babel/core';
|
import { NodePath } from '@babel/core';
|
||||||
import { AnalyzeContext, Visitor } from './types';
|
import { AnalyzeContext, Visitor } from '../analyze/types';
|
||||||
import { createSubCompNode } from './nodeFactory';
|
import { createSubCompNode } from '../analyze/nodeFactory';
|
||||||
import * as t from '@babel/types';
|
import * as t from '@babel/types';
|
||||||
|
|
||||||
function genName(tagName: string, ctx: AnalyzeContext) {
|
function genName(tagName: string, ctx: AnalyzeContext) {
|
||||||
return `$$${tagName}-Sub${ctx.currentComponent.subComponents.length}`;
|
return `$$${tagName}-Sub${ctx.current.subComponents.length}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function genNameFromJSX(path: NodePath<t.JSXElement>, ctx: AnalyzeContext) {
|
function genNameFromJSX(path: NodePath<t.JSXElement>, ctx: AnalyzeContext) {
|
||||||
|
@ -33,8 +33,8 @@ function genNameFromJSX(path: NodePath<t.JSXElement>, ctx: AnalyzeContext) {
|
||||||
|
|
||||||
function replaceJSXSliceWithSubComp(name: string, ctx: AnalyzeContext, path: NodePath<t.JSXElement | t.JSXFragment>) {
|
function replaceJSXSliceWithSubComp(name: string, ctx: AnalyzeContext, path: NodePath<t.JSXElement | t.JSXFragment>) {
|
||||||
// create a subComponent node and add it to the current component
|
// create a subComponent node and add it to the current component
|
||||||
const subComp = createSubCompNode(name, ctx.currentComponent, path.node);
|
const subComp = createSubCompNode(name, ctx.current, path.node);
|
||||||
ctx.currentComponent.subComponents.push(subComp);
|
ctx.current.subComponents.push(subComp);
|
||||||
|
|
||||||
// replace with the subComp jsxElement
|
// replace with the subComp jsxElement
|
||||||
const subCompJSX = t.jsxElement(
|
const subCompJSX = t.jsxElement(
|
||||||
|
@ -73,3 +73,28 @@ export function jsxSlicesAnalyze(): Visitor {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Analyze the JSX slice in the function component, including:
|
||||||
|
// 1. VariableDeclaration, like `const a = <div />`
|
||||||
|
// 2. SubComponent, like `function Sub() { return <div /> }`
|
||||||
|
function handleFn(fnName: string, fnBody: NodePath<types.BlockStatement>) {
|
||||||
|
if (isValidComponentName(fnName)) {
|
||||||
|
// This is a subcomponent, treat it as a normal component
|
||||||
|
} else {
|
||||||
|
// This is jsx creation function
|
||||||
|
// function jsxFunc() {
|
||||||
|
// // This is a function that returns JSX
|
||||||
|
// // because the function name is smallCamelCased
|
||||||
|
// return <div>{count}</div>
|
||||||
|
// }
|
||||||
|
// =>
|
||||||
|
// function jsxFunc() {
|
||||||
|
// function Comp_$id4$() {
|
||||||
|
// return <div>{count}</div>
|
||||||
|
// }
|
||||||
|
// // This is a function that returns JSX
|
||||||
|
// // because the function name is smallCamelCased
|
||||||
|
// return <Comp_$id4$/>
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,5 +18,6 @@ export function getFnBody(path: NodePath<t.FunctionExpression | t.ArrowFunctionE
|
||||||
// turn expression into block statement for consistency
|
// turn expression into block statement for consistency
|
||||||
fnBody.replaceWith(t.blockStatement([t.returnStatement(fnBody.node)]));
|
fnBody.replaceWith(t.blockStatement([t.returnStatement(fnBody.node)]));
|
||||||
}
|
}
|
||||||
return (fnBody as NodePath<t.BlockStatement>).get('body');
|
|
||||||
|
return fnBody as unknown as NodePath<t.BlockStatement>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { genCode, mockAnalyze } from '../mock';
|
||||||
|
import { lifeCycleAnalyze } from '../../src/analyze/lifeCycleAnalyze';
|
||||||
|
import { types } from '../../src/babelTypes';
|
||||||
|
import { type NodePath, type types as t } from '@babel/core';
|
||||||
|
|
||||||
|
const analyze = (code: string) => mockAnalyze(code, [lifeCycleAnalyze]);
|
||||||
|
const combine = (body: NodePath<t.Statement>[]) => types.program(body.map(path => path.node));
|
||||||
|
|
||||||
|
describe('analyze lifeCycle', () => {
|
||||||
|
it('should collect will mount', () => {
|
||||||
|
const root = analyze(/*js*/ `
|
||||||
|
Component(() => {
|
||||||
|
willMount(() => {
|
||||||
|
console.log('test');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(genCode(combine(root.lifecycle.willMount!))).toMatchInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
console.log('test');
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should collect on mount', () => {
|
||||||
|
const root = analyze(/*js*/ `
|
||||||
|
Component(() => {
|
||||||
|
onMount(() => {
|
||||||
|
console.log('test');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(genCode(combine(root.lifecycle.onMount!))).toMatchInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
console.log('test');
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should collect willUnmount', () => {
|
||||||
|
const root = analyze(/*js*/ `
|
||||||
|
Component(() => {
|
||||||
|
willUnmount(() => {
|
||||||
|
console.log('test');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(genCode(combine(root.lifecycle.willUnmount!))).toMatchInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
console.log('test');
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should collect onUnmount', () => {
|
||||||
|
const root = analyze(/*js*/ `
|
||||||
|
Component(() => {
|
||||||
|
onUnmount(() => {
|
||||||
|
console.log('test');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(genCode(combine(root.lifecycle.onUnmount!))).toMatchInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
console.log('test');
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple lifecycle methods', () => {
|
||||||
|
const root = analyze(/*js*/ `
|
||||||
|
Component(() => {
|
||||||
|
willMount(() => {
|
||||||
|
console.log('willMount');
|
||||||
|
})
|
||||||
|
onMount(() => {
|
||||||
|
console.log('onMount');
|
||||||
|
})
|
||||||
|
willUnmount(() => {
|
||||||
|
console.log('willUnmount');
|
||||||
|
})
|
||||||
|
onUnmount(() => {
|
||||||
|
console.log('onUnmount');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(genCode(combine(root.lifecycle.willMount!))).toMatchInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
console.log('willMount');
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
expect(genCode(combine(root.lifecycle.onMount!))).toMatchInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
console.log('onMount');
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
expect(genCode(combine(root.lifecycle.willUnmount!))).toMatchInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
console.log('willUnmount');
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
expect(genCode(combine(root.lifecycle.onUnmount!))).toMatchInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
console.log('onUnmount');
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { genCode, mockAnalyze } from '../mock';
|
||||||
|
import { propertiesAnalyze } from '../../src/analyze/propertiesAnalyze';
|
||||||
|
import { propsAnalyze } from '../../src/analyze/propsAnalyze';
|
||||||
|
|
||||||
|
const analyze = (code: string) => mockAnalyze(code, [propsAnalyze, propertiesAnalyze]);
|
||||||
|
|
||||||
|
describe('analyze properties', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
const root = analyze(`
|
||||||
|
Component(() => {
|
||||||
|
let foo = 1;
|
||||||
|
let bar = 1;
|
||||||
|
})
|
||||||
|
`);
|
||||||
|
expect(root.properties.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('state dependency', () => {
|
||||||
|
it('should analyze dependency from state', () => {
|
||||||
|
const root = analyze(`
|
||||||
|
Component(() => {
|
||||||
|
let foo = 1;
|
||||||
|
let bar = foo;
|
||||||
|
})
|
||||||
|
`);
|
||||||
|
expect(root.properties.length).toBe(2);
|
||||||
|
expect(root.properties[0].isComputed).toBe(false);
|
||||||
|
expect(genCode(root.properties[0].value)).toBe('1');
|
||||||
|
expect(root.properties[1].isComputed).toBe(true);
|
||||||
|
expect(genCode(root.properties[1].value)).toBe('foo');
|
||||||
|
expect(root.dependencyMap).toEqual({ bar: ['foo'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should analyze dependency from state in different shape', () => {
|
||||||
|
const root = analyze(`
|
||||||
|
Component(() => {
|
||||||
|
let foo = 1;
|
||||||
|
let a = 1;
|
||||||
|
let b = 0;
|
||||||
|
let bar = { foo: foo ? a : b };
|
||||||
|
})
|
||||||
|
`);
|
||||||
|
expect(root.properties.length).toBe(4);
|
||||||
|
expect(root.properties[3].isComputed).toBe(true);
|
||||||
|
expect(genCode(root.properties[3].value)).toMatchInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
foo: foo ? a : b
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
expect(root.dependencyMap).toEqual({ bar: ['foo', 'a', 'b'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should analyze dependency from props', () => {
|
||||||
|
const root = analyze(`
|
||||||
|
Component(({ foo }) => {
|
||||||
|
let bar = foo;
|
||||||
|
})
|
||||||
|
`);
|
||||||
|
expect(root.properties.length).toBe(1);
|
||||||
|
expect(root.properties[0].isComputed).toBe(true);
|
||||||
|
expect(root.dependencyMap).toEqual({ bar: ['foo'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should analyze dependency from nested props', () => {
|
||||||
|
const root = analyze(`
|
||||||
|
Component(({ foo: foo1, name: [first, last] }) => {
|
||||||
|
let bar = [foo1, first, last];
|
||||||
|
})
|
||||||
|
`);
|
||||||
|
expect(root.properties.length).toBe(1);
|
||||||
|
expect(root.properties[0].isComputed).toBe(true);
|
||||||
|
expect(root.dependencyMap).toEqual({ bar: ['foo1', 'first', 'last'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not collect invalid dependency', () => {
|
||||||
|
const root = analyze(`
|
||||||
|
const cond = true
|
||||||
|
Component(() => {
|
||||||
|
let bar = cond ? count : window.innerWidth;
|
||||||
|
})
|
||||||
|
`);
|
||||||
|
expect(root.properties.length).toBe(1);
|
||||||
|
expect(root.properties[0].isComputed).toBe(false);
|
||||||
|
expect(root.dependencyMap).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should collect method', () => {
|
||||||
|
const root = analyze(`
|
||||||
|
Component(() => {
|
||||||
|
let foo = 1;
|
||||||
|
const onClick = () => {};
|
||||||
|
const onHover = function() {
|
||||||
|
onClick(foo)
|
||||||
|
};
|
||||||
|
function onInput() {}
|
||||||
|
})
|
||||||
|
`);
|
||||||
|
expect(root.properties.map(p => p.name)).toEqual(['foo', 'onClick', 'onHover', 'onInput']);
|
||||||
|
expect(root.properties[1].isMethod).toBe(true);
|
||||||
|
expect(root.properties[2].isMethod).toBe(true);
|
||||||
|
expect(root.dependencyMap).toMatchInlineSnapshot(`{}`);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { genCode, mockAnalyze } from '../mock';
|
||||||
|
import { PropType } from '../../src/constants';
|
||||||
|
import { propsAnalyze } from '../../src/analyze/propsAnalyze';
|
||||||
|
|
||||||
|
const analyze = (code: string) => mockAnalyze(code, [propsAnalyze]);
|
||||||
|
|
||||||
|
describe('analyze props', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
const root = analyze(/*js*/ `
|
||||||
|
Component(({foo, bar}) => {})
|
||||||
|
`);
|
||||||
|
expect(root.props.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support default value', () => {
|
||||||
|
const root = analyze(/*js*/ `
|
||||||
|
Component(({foo = 'default', bar = 123}) => {})
|
||||||
|
`);
|
||||||
|
expect(root.props.length).toBe(2);
|
||||||
|
expect(root.props[0].name).toBe('foo');
|
||||||
|
expect(root.props[1].name).toBe('bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support alias', () => {
|
||||||
|
const root = analyze(/*js*/ `
|
||||||
|
Component(({'foo': renamed, bar: anotherName}) => {})
|
||||||
|
`);
|
||||||
|
expect(root.props.length).toBe(2);
|
||||||
|
expect(root.props[0].name).toBe('foo');
|
||||||
|
expect(root.props[0].alias).toBe('renamed');
|
||||||
|
expect(root.props[1].name).toBe('bar');
|
||||||
|
expect(root.props[1].alias).toBe('anotherName');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support nested props', () => {
|
||||||
|
const root = analyze(/*js*/ `
|
||||||
|
Component(({foo: {nested1, nested2}, bar}) => {})
|
||||||
|
`);
|
||||||
|
expect(root.props.length).toBe(2);
|
||||||
|
expect(root.props[0].name).toBe('foo');
|
||||||
|
expect(root.props[0].nestedProps).toEqual(['nested1', 'nested2']);
|
||||||
|
expect(genCode(root.props[0].nestedRelationship)).toMatchInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
nested1,
|
||||||
|
nested2
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
expect(root.props[1].name).toBe('bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support complex nested props', () => {
|
||||||
|
// language=js
|
||||||
|
const root = analyze(/*js*/ `
|
||||||
|
Component(function ({
|
||||||
|
prop1, prop2: {p2: [p20X = defaultVal, {p211, p212: p212X = defaultVal}, ...restArr], p3, ...restObj}}
|
||||||
|
) {});
|
||||||
|
`);
|
||||||
|
// we should collect prop1, p20X, p211, p212X, p3
|
||||||
|
expect(root.props.length).toBe(2);
|
||||||
|
expect(root.props[0].name).toBe('prop1');
|
||||||
|
expect(root.props[1].name).toBe('prop2');
|
||||||
|
expect(root.props[1].nestedProps).toEqual(['p20X', 'p211', 'p212X', 'restArr', 'p3', 'restObj']);
|
||||||
|
expect(genCode(root.props[1].nestedRelationship)).toMatchInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
p2: [p20X = defaultVal, {
|
||||||
|
p211,
|
||||||
|
p212: p212X = defaultVal
|
||||||
|
}, ...restArr],
|
||||||
|
p3,
|
||||||
|
...restObj
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support rest element', () => {
|
||||||
|
const root = analyze(/*js*/ `
|
||||||
|
Component(({foo, ...rest}) => {})
|
||||||
|
`);
|
||||||
|
expect(root.props.length).toBe(2);
|
||||||
|
expect(root.props[0].name).toBe('foo');
|
||||||
|
expect(root.props[0].type).toBe(PropType.SINGLE);
|
||||||
|
expect(root.props[1].name).toBe('rest');
|
||||||
|
expect(root.props[1].type).toBe(PropType.REST);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support empty props', () => {
|
||||||
|
const root = analyze(/*js*/ `
|
||||||
|
Component(() => {})
|
||||||
|
`);
|
||||||
|
expect(root.props.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { propsAnalyze } from '../../src/analyze/propsAnalyze';
|
||||||
|
import { watchAnalyze } from '../../src/analyze/watchAnalyze';
|
||||||
|
import { genCode, mockAnalyze } from '../mock';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
const analyze = (code: string) => mockAnalyze(code, [watchAnalyze]);
|
||||||
|
|
||||||
|
describe('watchAnalyze', () => {
|
||||||
|
it('should analyze watch expressions', () => {
|
||||||
|
const root = analyze(/*js*/ `
|
||||||
|
Comp(() => {
|
||||||
|
watch(() => {
|
||||||
|
// watch expression
|
||||||
|
}, [a, b]);
|
||||||
|
})
|
||||||
|
`);
|
||||||
|
expect(root.watch).toHaveLength(1);
|
||||||
|
if (!root?.watch?.[0].callback) {
|
||||||
|
throw new Error('watch callback not found');
|
||||||
|
}
|
||||||
|
expect(genCode(root.watch[0].callback.node)).toMatchInlineSnapshot(`
|
||||||
|
"() => {
|
||||||
|
// watch expression
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
if (!root.watch[0].deps) {
|
||||||
|
throw new Error('watch deps not found');
|
||||||
|
}
|
||||||
|
expect(genCode(root.watch[0].deps.node)).toMatchInlineSnapshot('"[a, b]"');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,233 +0,0 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
import { transform } from './presets';
|
|
||||||
|
|
||||||
describe('fn2Class', () => {
|
|
||||||
it('should transform jsx', () => {
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
@View
|
|
||||||
class A {
|
|
||||||
Body() {
|
|
||||||
return <div></div>
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
|
|
||||||
class A extends View {
|
|
||||||
Body() {
|
|
||||||
let $node0;
|
|
||||||
$node0 = $$createElement("div");
|
|
||||||
return [$node0];
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should transform jsx with reactive', () => {
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
@Main
|
|
||||||
@View
|
|
||||||
class A {
|
|
||||||
count = 1
|
|
||||||
Body() {
|
|
||||||
return <div onClick={() => this.count++}>{this.count}</div>
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
|
|
||||||
class A extends View {
|
|
||||||
count = 1;
|
|
||||||
$$count = 1;
|
|
||||||
Body() {
|
|
||||||
let $node0, $node1;
|
|
||||||
this._$update = $changed => {
|
|
||||||
if ($changed & 1) {
|
|
||||||
$node1 && $node1.update(() => this.count, [this.count]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
$node0 = $$createElement("div");
|
|
||||||
$$delegateEvent($node0, "click", () => this._$ud(this.count++, "count"));
|
|
||||||
$node1 = new $$ExpNode(this.count, [this.count]);
|
|
||||||
$$insertNode($node0, $node1, 0);
|
|
||||||
$node0._$nodes = [$node1];
|
|
||||||
return [$node0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$$render("main", A);"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should transform fragment', () => {
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
@View
|
|
||||||
class A {
|
|
||||||
Body() {
|
|
||||||
return <>
|
|
||||||
<div></div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
|
|
||||||
class A extends View {
|
|
||||||
Body() {
|
|
||||||
let $node0;
|
|
||||||
$node0 = $$createElement("div");
|
|
||||||
return [$node0];
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should transform function component', () => {
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
function MyApp() {
|
|
||||||
let count = 0;
|
|
||||||
return <div onClick={() => count++}>{count}</div>
|
|
||||||
}`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
|
|
||||||
class MyApp extends View {
|
|
||||||
count = 0;
|
|
||||||
$$count = 1;
|
|
||||||
Body() {
|
|
||||||
let $node0, $node1;
|
|
||||||
this._$update = $changed => {
|
|
||||||
if ($changed & 1) {
|
|
||||||
$node1 && $node1.update(() => this.count, [this.count]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
$node0 = $$createElement("div");
|
|
||||||
$$delegateEvent($node0, "click", () => this._$ud(this.count++, "count"));
|
|
||||||
$node1 = new $$ExpNode(this.count, [this.count]);
|
|
||||||
$$insertNode($node0, $node1, 0);
|
|
||||||
$node0._$nodes = [$node1];
|
|
||||||
return [$node0];
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should transform function component reactively', () => {
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
function MyComp() {
|
|
||||||
let count = 0
|
|
||||||
return <>
|
|
||||||
<h1>Hello dlight fn, {count}</h1>
|
|
||||||
<button onClick={() => count +=1}>Add</button>
|
|
||||||
<Button />
|
|
||||||
</>
|
|
||||||
}`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
|
|
||||||
class MyComp extends View {
|
|
||||||
count = 0;
|
|
||||||
$$count = 1;
|
|
||||||
Body() {
|
|
||||||
let $node0, $node1, $node2, $node3, $node4;
|
|
||||||
this._$update = $changed => {
|
|
||||||
if ($changed & 1) {
|
|
||||||
$node2 && $node2.update(() => this.count, [this.count]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
$node0 = $$createElement("h1");
|
|
||||||
$node1 = $$createTextNode("Hello dlight fn, ", []);
|
|
||||||
$$insertNode($node0, $node1, 0);
|
|
||||||
$node2 = new $$ExpNode(this.count, [this.count]);
|
|
||||||
$$insertNode($node0, $node2, 1);
|
|
||||||
$node0._$nodes = [$node1, $node2];
|
|
||||||
$node3 = $$createElement("button");
|
|
||||||
$$delegateEvent($node3, "click", () => this._$ud(this.count += 1, "count"));
|
|
||||||
$node3.textContent = "Add";
|
|
||||||
$node4 = new Button();
|
|
||||||
$node4._$init(null, null, null, null);
|
|
||||||
return [$node0, $node3, $node4];
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should transform children props', () => {
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
function App({ children}) {
|
|
||||||
return <h1>{children}</h1>
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
|
|
||||||
class App extends View {
|
|
||||||
get children() {
|
|
||||||
return this._$children;
|
|
||||||
}
|
|
||||||
Body() {
|
|
||||||
let $node0, $node1;
|
|
||||||
$node0 = $$createElement("h1");
|
|
||||||
$node1 = new $$ExpNode(this.children, []);
|
|
||||||
$$insertNode($node0, $node1, 0);
|
|
||||||
$node0._$nodes = [$node1];
|
|
||||||
return [$node0];
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should transform component composition', () => {
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
function ArrayModification({name}) {
|
|
||||||
let arr = 1
|
|
||||||
return <section>
|
|
||||||
<div>{arr}</div>
|
|
||||||
</section>
|
|
||||||
}
|
|
||||||
|
|
||||||
function MyComp() {
|
|
||||||
return <>
|
|
||||||
<ArrayModification name="1" />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
|
|
||||||
class ArrayModification extends View {
|
|
||||||
$p$name;
|
|
||||||
name;
|
|
||||||
arr = 1;
|
|
||||||
$$arr = 2;
|
|
||||||
Body() {
|
|
||||||
let $node0, $node1, $node2;
|
|
||||||
this._$update = $changed => {
|
|
||||||
if ($changed & 2) {
|
|
||||||
$node2 && $node2.update(() => this.arr, [this.arr]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
$node0 = ArrayModification.$t0.cloneNode(true);
|
|
||||||
$node1 = $node0.firstChild;
|
|
||||||
$node2 = new $$ExpNode(this.arr, [this.arr]);
|
|
||||||
$$insertNode($node1, $node2, 0);
|
|
||||||
return [$node0];
|
|
||||||
}
|
|
||||||
static $t0 = (() => {
|
|
||||||
let $node0, $node1;
|
|
||||||
$node0 = $$createElement("section");
|
|
||||||
$node1 = $$createElement("div");
|
|
||||||
$node0.appendChild($node1);
|
|
||||||
return $node0;
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
class MyComp extends View {
|
|
||||||
Body() {
|
|
||||||
let $node0;
|
|
||||||
$node0 = new ArrayModification();
|
|
||||||
$node0._$init([["name", "1", []]], null, null, null);
|
|
||||||
return [$node0];
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -13,26 +13,34 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ComponentNode, InulaNode } from '../src/analyze/types';
|
import { Analyzer, ComponentNode, InulaNode } from '../src/analyze/types';
|
||||||
import babel, { type PluginObj, transform as transformWithBabel } from '@babel/core';
|
import babel, { type PluginObj, transform as transformWithBabel } from '@babel/core';
|
||||||
import syntaxJSX from '@babel/plugin-syntax-jsx';
|
import syntaxJSX from '@babel/plugin-syntax-jsx';
|
||||||
import { analyze } from '../src/analyze';
|
import { analyze } from '../src/analyze';
|
||||||
import generate from '@babel/generator';
|
import generate from '@babel/generator';
|
||||||
import * as t from '@babel/types';
|
import * as t from '@babel/types';
|
||||||
|
import { register } from '../src/babelTypes';
|
||||||
|
|
||||||
export function mockAnalyze(code: string): ComponentNode {
|
export function mockAnalyze(code: string, analyzers?: Analyzer[]): ComponentNode {
|
||||||
let root: ComponentNode | null = null;
|
let root: ComponentNode | null = null;
|
||||||
transformWithBabel(code, {
|
transformWithBabel(code, {
|
||||||
plugins: [
|
plugins: [
|
||||||
syntaxJSX.default ?? syntaxJSX,
|
syntaxJSX.default ?? syntaxJSX,
|
||||||
function (api: typeof babel): PluginObj {
|
function (api): PluginObj {
|
||||||
const { types } = api;
|
register(api.types);
|
||||||
return {
|
return {
|
||||||
visitor: {
|
visitor: {
|
||||||
FunctionDeclaration: {
|
FunctionExpression: path => {
|
||||||
enter: path => {
|
root = analyze(api.types, 'test', path, analyzers);
|
||||||
root = analyze(path);
|
if (root) {
|
||||||
},
|
path.skip();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ArrowFunctionExpression: path => {
|
||||||
|
root = analyze(api.types, 'test', path, analyzers);
|
||||||
|
if (root) {
|
||||||
|
path.skip();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,8 +14,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { isCondNode } from '../src/analyze';
|
import { isCondNode } from '../../src/analyze';
|
||||||
import { mockAnalyze } from './mock';
|
import { mockAnalyze } from '../mock';
|
||||||
|
|
||||||
describe('analyze early return', () => {
|
describe('analyze early return', () => {
|
||||||
it('should work', () => {
|
it('should work', () => {
|
|
@ -14,7 +14,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { genCode, mockAnalyze } from './mock';
|
import { genCode, mockAnalyze } from '../mock';
|
||||||
import generate from '@babel/generator';
|
import generate from '@babel/generator';
|
||||||
|
|
||||||
describe('propertiesAnalyze', () => {
|
describe('propertiesAnalyze', () => {
|
|
@ -0,0 +1 @@
|
||||||
|
export default ['packages/*', 'packages/**/*'];
|
Loading…
Reference in New Issue