feat: viewNode as child
This commit is contained in:
parent
f15b7d1a14
commit
be4456f225
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* 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 { LifeCycle, Visitor } from './types';
|
||||||
|
import { addLifecycle, addWatch } from './nodeFactory';
|
||||||
|
import * as t from '@babel/types';
|
||||||
|
import { ON_MOUNT, ON_UNMOUNT, WATCH, WILL_MOUNT, WILL_UNMOUNT } from '../constants';
|
||||||
|
import { extractFnFromMacro, getFnBody } from '../utils';
|
||||||
|
|
||||||
|
function isLifeCycleName(name: string): name is LifeCycle {
|
||||||
|
return [WILL_MOUNT, ON_MOUNT, WILL_UNMOUNT, ON_UNMOUNT].includes(name);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Analyze the functional macro in the function component
|
||||||
|
* 1. lifecycle
|
||||||
|
* 1. willMount
|
||||||
|
* 2. onMount
|
||||||
|
* 3. willUnMount
|
||||||
|
* 4. onUnmount
|
||||||
|
* 2. watch
|
||||||
|
*/
|
||||||
|
export function functionalMacroAnalyze(): Visitor {
|
||||||
|
return {
|
||||||
|
ExpressionStatement(path: NodePath<t.ExpressionStatement>, ctx) {
|
||||||
|
const expression = path.get('expression');
|
||||||
|
if (expression.isCallExpression()) {
|
||||||
|
const callee = expression.get('callee');
|
||||||
|
if (callee.isIdentifier()) {
|
||||||
|
const calleeName = callee.node.name;
|
||||||
|
// lifecycle
|
||||||
|
if (isLifeCycleName(calleeName)) {
|
||||||
|
const fnNode = extractFnFromMacro(expression, calleeName);
|
||||||
|
addLifecycle(ctx.current, calleeName, getFnBody(fnNode).node);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// watch
|
||||||
|
if (calleeName === WATCH) {
|
||||||
|
const fnNode = extractFnFromMacro(expression, WATCH);
|
||||||
|
const deps = getWatchDeps(expression);
|
||||||
|
addWatch(ctx.current, fnNode, deps);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.unhandledNode.push(path.node);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
|
@ -1,11 +1,14 @@
|
||||||
import { type types as t, type NodePath } from '@babel/core';
|
import { type types as t, type NodePath } from '@babel/core';
|
||||||
import { propsAnalyze } from './propsAnalyze';
|
import { propsAnalyze } from './propsAnalyze';
|
||||||
import { AnalyzeContext, Analyzer, ComponentNode, CondNode, Visitor } from './types';
|
import { AnalyzeContext, Analyzer, ComponentNode, CondNode, Visitor } from './types';
|
||||||
import { createComponentNode } from './nodeFactory';
|
import { addLifecycle, createComponentNode } from './nodeFactory';
|
||||||
import { propertiesAnalyze } from './propertiesAnalyze';
|
import { propertiesAnalyze } from './propertiesAnalyze';
|
||||||
import { lifeCycleAnalyze } from './lifeCycleAnalyze';
|
import { functionalMacroAnalyze } from './functionalMacroAnalyze';
|
||||||
import { getFnBody } from '../utils';
|
import { getFnBody } from '../utils';
|
||||||
const builtinAnalyzers = [propsAnalyze, propertiesAnalyze, lifeCycleAnalyze];
|
import { viewAnalyze } from './viewAnalyze';
|
||||||
|
import { WILL_MOUNT } from '../constants';
|
||||||
|
import { types } from '../babelTypes';
|
||||||
|
const builtinAnalyzers = [propsAnalyze, propertiesAnalyze, functionalMacroAnalyze, viewAnalyze];
|
||||||
|
|
||||||
export function isCondNode(node: any): node is CondNode {
|
export function isCondNode(node: any): node is CondNode {
|
||||||
return node && node.type === 'cond';
|
return node && node.type === 'cond';
|
||||||
|
@ -13,22 +16,7 @@ export function isCondNode(node: any): node is CondNode {
|
||||||
|
|
||||||
function mergeVisitor(...visitors: Analyzer[]): Visitor {
|
function mergeVisitor(...visitors: Analyzer[]): Visitor {
|
||||||
return visitors.reduce<Visitor<AnalyzeContext>>((acc, cur) => {
|
return visitors.reduce<Visitor<AnalyzeContext>>((acc, cur) => {
|
||||||
const visitor = cur();
|
return { ...acc, ...cur() };
|
||||||
const visitorKeys = Object.keys(visitor) as (keyof Visitor)[];
|
|
||||||
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;
|
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +33,7 @@ export function analyzeFnComp(
|
||||||
current: componentNode,
|
current: componentNode,
|
||||||
htmlTags,
|
htmlTags,
|
||||||
analyzers,
|
analyzers,
|
||||||
|
unhandledNode: [],
|
||||||
traverse: (path: NodePath<t.Statement>, ctx: AnalyzeContext) => {
|
traverse: (path: NodePath<t.Statement>, ctx: AnalyzeContext) => {
|
||||||
path.traverse(visitor, ctx);
|
path.traverse(visitor, ctx);
|
||||||
},
|
},
|
||||||
|
@ -71,14 +60,23 @@ export function analyzeFnComp(
|
||||||
|
|
||||||
const type = p.node.type;
|
const type = p.node.type;
|
||||||
|
|
||||||
// TODO: More type safe way to handle this
|
const visit = visitor[type];
|
||||||
visitor[type]?.(p as unknown as any, context);
|
if (visit) {
|
||||||
|
// TODO: More type safe way to handle this
|
||||||
|
visit(p as unknown as any, context);
|
||||||
|
} else {
|
||||||
|
context.unhandledNode.push(p.node);
|
||||||
|
}
|
||||||
|
|
||||||
if (p.isReturnStatement()) {
|
if (p.isReturnStatement()) {
|
||||||
visitor.ReturnStatement?.(p, context);
|
visitor.ReturnStatement?.(p, context);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (context.unhandledNode.length) {
|
||||||
|
addLifecycle(componentNode, WILL_MOUNT, types.blockStatement(context.unhandledNode));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* The process of analyzing the component
|
* The process of analyzing the component
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 { LifeCycle, Visitor } from './types';
|
|
||||||
import { addLifecycle } from './nodeFactory';
|
|
||||||
import * as t from '@babel/types';
|
|
||||||
import { ON_MOUNT, ON_UNMOUNT, WILL_MOUNT, WILL_UNMOUNT } from '../constants';
|
|
||||||
import { extractFnFromMacro, getFnBody } from '../utils';
|
|
||||||
|
|
||||||
function isLifeCycleName(name: string): name is LifeCycle {
|
|
||||||
return [WILL_MOUNT, ON_MOUNT, WILL_UNMOUNT, ON_UNMOUNT].includes(name);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Analyze the lifeCycle in the function component
|
|
||||||
* 1. willMount
|
|
||||||
* 2. onMount
|
|
||||||
* 3. willUnMount
|
|
||||||
* 4. onUnmount
|
|
||||||
*/
|
|
||||||
export function lifeCycleAnalyze(): Visitor {
|
|
||||||
return {
|
|
||||||
ExpressionStatement(path: NodePath<t.ExpressionStatement>, ctx) {
|
|
||||||
const expression = path.get('expression');
|
|
||||||
if (expression.isCallExpression()) {
|
|
||||||
const callee = expression.get('callee');
|
|
||||||
if (callee.isIdentifier()) {
|
|
||||||
const lifeCycleName = callee.node.name;
|
|
||||||
if (isLifeCycleName(lifeCycleName)) {
|
|
||||||
const fnNode = extractFnFromMacro(expression, lifeCycleName);
|
|
||||||
addLifecycle(ctx.current, lifeCycleName, getFnBody(fnNode));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -14,12 +14,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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 { ComponentNode, FunctionalExpression, LifeCycle, ViewNode } from './types';
|
||||||
import { PropType } from '../constants';
|
import { PropType } from '../constants';
|
||||||
|
import { ViewParticle } from '@openinula/reactivity-parser';
|
||||||
|
|
||||||
export function createComponentNode(
|
export function createComponentNode(
|
||||||
name: string,
|
name: string,
|
||||||
fnNode: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>,
|
fnNode: NodePath<FunctionalExpression>,
|
||||||
parent?: ComponentNode
|
parent?: ComponentNode
|
||||||
): ComponentNode {
|
): ComponentNode {
|
||||||
const comp: ComponentNode = {
|
const comp: ComponentNode = {
|
||||||
|
@ -27,12 +28,12 @@ export function createComponentNode(
|
||||||
name,
|
name,
|
||||||
props: [],
|
props: [],
|
||||||
child: undefined,
|
child: undefined,
|
||||||
properties: [],
|
variables: [],
|
||||||
dependencyMap: {},
|
dependencyMap: {},
|
||||||
reactiveMap: {},
|
reactiveMap: {},
|
||||||
lifecycle: {},
|
lifecycle: {},
|
||||||
parent,
|
parent,
|
||||||
// fnBody,
|
fnNode,
|
||||||
get availableProps() {
|
get availableProps() {
|
||||||
return comp.props
|
return comp.props
|
||||||
.map(({ name, nestedProps, alias }) => {
|
.map(({ name, nestedProps, alias }) => {
|
||||||
|
@ -41,11 +42,11 @@ export function createComponentNode(
|
||||||
})
|
})
|
||||||
.flat();
|
.flat();
|
||||||
},
|
},
|
||||||
get ownAvailableProperties() {
|
get ownAvailableVariables() {
|
||||||
return [...comp.properties.filter(p => !p.isMethod).map(({ name }) => name), ...comp.availableProps];
|
return [...comp.variables.filter(p => p.type === 'reactive').map(({ name }) => name), ...comp.availableProps];
|
||||||
},
|
},
|
||||||
get availableProperties() {
|
get availableVariables() {
|
||||||
return [...comp.ownAvailableProperties, ...(comp.parent ? comp.parent.availableProperties : [])];
|
return [...comp.ownAvailableVariables, ...(comp.parent ? comp.parent.availableVariables : [])];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -53,15 +54,15 @@ export function createComponentNode(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addProperty(comp: ComponentNode, name: string, value: t.Expression | null, isComputed: boolean) {
|
export function addProperty(comp: ComponentNode, name: string, value: t.Expression | null, isComputed: boolean) {
|
||||||
comp.properties.push({ name, value, isComputed, isMethod: false });
|
comp.variables.push({ name, value, isComputed, type: 'reactive' });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addMethod(comp: ComponentNode, name: string, value: t.Expression | null) {
|
export function addMethod(comp: ComponentNode, name: string, value: FunctionalExpression) {
|
||||||
comp.properties.push({ name, value, isComputed: false, isMethod: true });
|
comp.variables.push({ name, value, type: 'method' });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addSubComponent(comp: ComponentNode, subComp: ComponentNode, isComputed: boolean) {
|
export function addSubComponent(comp: ComponentNode, subComp: ComponentNode) {
|
||||||
comp.properties.push({ name: subComp.name, value: subComp, isSubComp: true, isComputed, isMethod: false });
|
comp.variables.push({ name: subComp.name, value: subComp, type: 'subComp' });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addProp(
|
export function addProp(
|
||||||
|
@ -76,7 +77,7 @@ export function addProp(
|
||||||
comp.props.push({ name: key, type, default: defaultVal, alias, nestedProps, nestedRelationship });
|
comp.props.push({ name: key, type, default: defaultVal, alias, nestedProps, nestedRelationship });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addLifecycle(comp: ComponentNode, lifeCycle: LifeCycle, block: NodePath<t.BlockStatement>) {
|
export function addLifecycle(comp: ComponentNode, lifeCycle: LifeCycle, block: t.BlockStatement) {
|
||||||
const compLifecycle = comp.lifecycle;
|
const compLifecycle = comp.lifecycle;
|
||||||
if (!compLifecycle[lifeCycle]) {
|
if (!compLifecycle[lifeCycle]) {
|
||||||
compLifecycle[lifeCycle] = [];
|
compLifecycle[lifeCycle] = [];
|
||||||
|
@ -96,28 +97,10 @@ export function addWatch(
|
||||||
comp.watch.push({ callback, deps });
|
comp.watch.push({ callback, deps });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createJSXNode(parent: ComponentNode, content: NodePath<JSX>): JSXNode {
|
export function setViewChild(comp: ComponentNode, view: ViewParticle[], usedPropertySet: Set<string>) {
|
||||||
return {
|
const viewNode: ViewNode = {
|
||||||
type: 'jsx',
|
content: view,
|
||||||
parent,
|
usedPropertySet,
|
||||||
child: content,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createCondNode(parent: ComponentNode, child: InulaNode, branches: Branch[]): CondNode {
|
|
||||||
return {
|
|
||||||
type: 'cond',
|
|
||||||
branches,
|
|
||||||
child,
|
|
||||||
parent,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createSubCompNode(name: string, parent: ComponentNode, child: JSX): SubCompNode {
|
|
||||||
return {
|
|
||||||
type: 'subComp',
|
|
||||||
name,
|
|
||||||
parent,
|
|
||||||
child,
|
|
||||||
};
|
};
|
||||||
|
comp.child = viewNode;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AnalyzeContext, Visitor } from './types';
|
import { AnalyzeContext, Visitor } from './types';
|
||||||
import { addMethod, addProperty, createComponentNode } from './nodeFactory';
|
import { addMethod, addProperty, addSubComponent, createComponentNode } from './nodeFactory';
|
||||||
import { isValidPath } from './utils';
|
import { isValidPath } from './utils';
|
||||||
import { type types as t, type NodePath } from '@babel/core';
|
import { type types as t, type NodePath } from '@babel/core';
|
||||||
import { reactivityFuncNames } from '../const';
|
import { reactivityFuncNames } from '../const';
|
||||||
|
@ -45,11 +45,12 @@ export function propertiesAnalyze(): Visitor {
|
||||||
const init = declaration.get('init');
|
const init = declaration.get('init');
|
||||||
let deps: string[] | null = null;
|
let deps: string[] | null = null;
|
||||||
if (isValidPath(init)) {
|
if (isValidPath(init)) {
|
||||||
// the property is a method
|
// handle the method
|
||||||
if (init.isArrowFunctionExpression() || init.isFunctionExpression()) {
|
if (init.isArrowFunctionExpression() || init.isFunctionExpression()) {
|
||||||
addMethod(ctx.current, id.node.name, init.node);
|
addMethod(ctx.current, id.node.name, init.node);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// handle the sub component
|
||||||
// Should like Component(() => {})
|
// Should like Component(() => {})
|
||||||
if (
|
if (
|
||||||
init.isCallExpression() &&
|
init.isCallExpression() &&
|
||||||
|
@ -64,9 +65,10 @@ export function propertiesAnalyze(): Visitor {
|
||||||
|
|
||||||
analyzeFnComp(fnNode, subComponent, ctx);
|
analyzeFnComp(fnNode, subComponent, ctx);
|
||||||
deps = getDependenciesFromNode(id.node.name, init, ctx);
|
deps = getDependenciesFromNode(id.node.name, init, ctx);
|
||||||
addProperty(ctx.current, id.node.name, subComponent, !!deps?.length);
|
addSubComponent(ctx.current, subComponent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
deps = getDependenciesFromNode(id.node.name, init, ctx);
|
deps = getDependenciesFromNode(id.node.name, init, ctx);
|
||||||
}
|
}
|
||||||
addProperty(ctx.current, id.node.name, init.node || null, !!deps?.length);
|
addProperty(ctx.current, id.node.name, init.node || null, !!deps?.length);
|
||||||
|
@ -111,7 +113,7 @@ function getDependenciesFromNode(
|
||||||
const propertyKey = innerPath.node.name;
|
const propertyKey = innerPath.node.name;
|
||||||
if (isAssignmentExpressionLeft(innerPath) || isAssignmentFunction(innerPath)) {
|
if (isAssignmentExpressionLeft(innerPath) || isAssignmentFunction(innerPath)) {
|
||||||
assignDeps.add(propertyKey);
|
assignDeps.add(propertyKey);
|
||||||
} else if (current.availableProperties.includes(propertyKey)) {
|
} else if (current.availableVariables.includes(propertyKey)) {
|
||||||
deps.add(propertyKey);
|
deps.add(propertyKey);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,32 +14,36 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type NodePath, types as t } from '@babel/core';
|
import { type NodePath, types as t } from '@babel/core';
|
||||||
import { Node } from '@babel/traverse';
|
|
||||||
import { ON_MOUNT, ON_UNMOUNT, PropType, WILL_MOUNT, WILL_UNMOUNT } from '../constants';
|
import { ON_MOUNT, ON_UNMOUNT, PropType, WILL_MOUNT, WILL_UNMOUNT } from '../constants';
|
||||||
|
import { ViewParticle } from '@openinula/reactivity-parser';
|
||||||
|
|
||||||
// --- Node shape ---
|
|
||||||
export type InulaNode = ComponentNode | CondNode | JSXNode;
|
|
||||||
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 Bitmap = number;
|
type Bitmap = number;
|
||||||
interface BaseProperty<V> {
|
|
||||||
|
export type FunctionalExpression = t.FunctionExpression | t.ArrowFunctionExpression;
|
||||||
|
interface BaseVariable<V> {
|
||||||
name: string;
|
name: string;
|
||||||
value: V;
|
value: V;
|
||||||
// need a flag for computed to gen a getter
|
|
||||||
// watch is a static computed
|
|
||||||
isComputed: boolean;
|
|
||||||
isMethod: boolean;
|
|
||||||
}
|
}
|
||||||
interface Property extends BaseProperty<t.Expression | null> {
|
export interface ReactiveVariable extends BaseVariable<t.Expression | null> {
|
||||||
|
type: 'reactive';
|
||||||
// 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
|
||||||
|
// watch is a static computed
|
||||||
|
isComputed: boolean;
|
||||||
}
|
}
|
||||||
interface SubCompProperty extends BaseProperty<ComponentNode> {
|
|
||||||
isSubComp: true;
|
export interface MethodVariable extends BaseVariable<FunctionalExpression> {
|
||||||
|
type: 'method';
|
||||||
}
|
}
|
||||||
interface Prop {
|
export interface SubCompVariable extends BaseVariable<ComponentNode> {
|
||||||
|
type: 'subComp';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Variable = ReactiveVariable | MethodVariable | SubCompVariable;
|
||||||
|
export interface Prop {
|
||||||
name: string;
|
name: string;
|
||||||
type: PropType;
|
type: PropType;
|
||||||
alias: string | null;
|
alias: string | null;
|
||||||
|
@ -51,66 +55,45 @@ export interface ComponentNode {
|
||||||
type: 'comp';
|
type: 'comp';
|
||||||
name: string;
|
name: string;
|
||||||
props: Prop[];
|
props: Prop[];
|
||||||
// A properties could be a state or computed
|
// The variables defined in the component
|
||||||
properties: (Property | SubCompProperty)[];
|
variables: Variable[];
|
||||||
/**
|
/**
|
||||||
* The available props for the component, including the nested props
|
* The available props for the component, including the nested props
|
||||||
*/
|
*/
|
||||||
availableProps: string[];
|
availableProps: string[];
|
||||||
/**
|
/**
|
||||||
* The available properties for the component
|
* The available variables and props owned by the component
|
||||||
*/
|
*/
|
||||||
ownAvailableProperties: string[];
|
ownAvailableVariables: string[];
|
||||||
availableProperties: string[];
|
/**
|
||||||
|
* The available variables and props for the component and its parent
|
||||||
|
*/
|
||||||
|
availableVariables: string[];
|
||||||
/**
|
/**
|
||||||
* The map to find the dependencies
|
* The map to find the dependencies
|
||||||
*/
|
*/
|
||||||
dependencyMap: {
|
dependencyMap: {
|
||||||
[key: string]: string[];
|
[key: string]: string[];
|
||||||
};
|
};
|
||||||
child?: InulaNode;
|
child?: ComponentNode | ViewNode;
|
||||||
parent?: ComponentNode;
|
parent?: ComponentNode;
|
||||||
/**
|
/**
|
||||||
* The function body of the fn component code
|
* The function body of the fn component code
|
||||||
*/
|
*/
|
||||||
fnBody: NodePath<t.Statement>[];
|
fnNode: NodePath<FunctionalExpression>;
|
||||||
/**
|
/**
|
||||||
* The map to find the state
|
* The map to find the state
|
||||||
*/
|
*/
|
||||||
reactiveMap: Record<string, Bitmap>;
|
reactiveMap: Record<string, Bitmap>;
|
||||||
lifecycle: Partial<Record<LifeCycle, NodePath<t.Statement>[]>>;
|
lifecycle: Partial<Record<LifeCycle, t.Statement[]>>;
|
||||||
watch?: {
|
watch?: {
|
||||||
deps: NodePath<t.ArrayExpression> | null;
|
deps: NodePath<t.ArrayExpression> | null;
|
||||||
callback: NodePath<t.ArrowFunctionExpression> | NodePath<t.FunctionExpression>;
|
callback: NodePath<t.ArrowFunctionExpression> | NodePath<t.FunctionExpression>;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
export interface ViewNode {
|
||||||
export interface SubCompNode {
|
content: ViewParticle[];
|
||||||
type: 'subComp';
|
usedPropertySet: Set<string>;
|
||||||
name: string;
|
|
||||||
parent: ComponentNode;
|
|
||||||
child: JSX;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface JSXNode {
|
|
||||||
type: 'jsx';
|
|
||||||
parent: ComponentNode;
|
|
||||||
child: NodePath<JSX>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CondNode {
|
|
||||||
type: 'cond';
|
|
||||||
branches: Branch[];
|
|
||||||
parent: ComponentNode;
|
|
||||||
/**
|
|
||||||
* The default branch
|
|
||||||
*/
|
|
||||||
child: InulaNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Branch {
|
|
||||||
conditions: NodePath<t.Expression>[];
|
|
||||||
content: InulaNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnalyzeContext {
|
export interface AnalyzeContext {
|
||||||
|
@ -119,10 +102,11 @@ export interface AnalyzeContext {
|
||||||
analyzers: Analyzer[];
|
analyzers: Analyzer[];
|
||||||
htmlTags: string[];
|
htmlTags: string[];
|
||||||
traverse: (p: NodePath<t.Statement>, ctx: AnalyzeContext) => void;
|
traverse: (p: NodePath<t.Statement>, ctx: AnalyzeContext) => void;
|
||||||
|
unhandledNode: t.Statement[];
|
||||||
}
|
}
|
||||||
|
|
||||||
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 t.Statement['type']]?: (path: NodePath<Extract<t.Statement, { type: Type }>>, state: S) => void;
|
||||||
} & {
|
} & {
|
||||||
Prop?: (path: NodePath<t.ObjectProperty | t.RestElement>, state: S) => void;
|
Prop?: (path: NodePath<t.ObjectProperty | t.RestElement>, state: S) => void;
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { parseView as parseJSX } from 'jsx-view-parser';
|
||||||
import { getBabelApi } from '../babelTypes';
|
import { getBabelApi } from '../babelTypes';
|
||||||
import { parseReactivity } from '@openinula/reactivity-parser';
|
import { parseReactivity } from '@openinula/reactivity-parser';
|
||||||
import { reactivityFuncNames } from '../const';
|
import { reactivityFuncNames } from '../const';
|
||||||
|
import { setViewChild } from './nodeFactory';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Analyze the watch in the function component
|
* Analyze the watch in the function component
|
||||||
|
@ -35,10 +36,12 @@ export function viewAnalyze(): Visitor {
|
||||||
});
|
});
|
||||||
const [viewParticles, usedPropertySet] = parseReactivity(viewUnits, {
|
const [viewParticles, usedPropertySet] = parseReactivity(viewUnits, {
|
||||||
babelApi: getBabelApi(),
|
babelApi: getBabelApi(),
|
||||||
availableProperties: current.availableProperties,
|
availableProperties: current.availableVariables,
|
||||||
dependencyMap: current.dependencyMap,
|
dependencyMap: current.dependencyMap,
|
||||||
reactivityFuncNames,
|
reactivityFuncNames,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setViewChild(current, viewParticles, usedPropertySet);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
/*
|
|
||||||
* 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;
|
|
||||||
}
|
|
|
@ -15,12 +15,12 @@
|
||||||
|
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { genCode, mockAnalyze } from '../mock';
|
import { genCode, mockAnalyze } from '../mock';
|
||||||
import { lifeCycleAnalyze } from '../../src/analyze/lifeCycleAnalyze';
|
import { functionalMacroAnalyze } from '../../src/analyze/functionalMacroAnalyze';
|
||||||
import { types } from '../../src/babelTypes';
|
import { types } from '../../src/babelTypes';
|
||||||
import { type NodePath, type types as t } from '@babel/core';
|
import { type NodePath, type types as t } from '@babel/core';
|
||||||
|
|
||||||
const analyze = (code: string) => mockAnalyze(code, [lifeCycleAnalyze]);
|
const analyze = (code: string) => mockAnalyze(code, [functionalMacroAnalyze]);
|
||||||
const combine = (body: NodePath<t.Statement>[]) => types.program(body.map(path => path.node));
|
const combine = (body: t.Statement[]) => types.program(body);
|
||||||
|
|
||||||
describe('analyze lifeCycle', () => {
|
describe('analyze lifeCycle', () => {
|
||||||
it('should collect will mount', () => {
|
it('should collect will mount', () => {
|
||||||
|
|
|
@ -29,7 +29,7 @@ describe('analyze properties', () => {
|
||||||
let bar = 1;
|
let bar = 1;
|
||||||
})
|
})
|
||||||
`);
|
`);
|
||||||
expect(root.properties.length).toBe(2);
|
expect(root.variables.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('state dependency', () => {
|
describe('state dependency', () => {
|
||||||
|
@ -40,11 +40,11 @@ describe('analyze properties', () => {
|
||||||
let bar = foo;
|
let bar = foo;
|
||||||
})
|
})
|
||||||
`);
|
`);
|
||||||
expect(root.properties.length).toBe(2);
|
expect(root.variables.length).toBe(2);
|
||||||
expect(root.properties[0].isComputed).toBe(false);
|
expect(root.variables[0].isComputed).toBe(false);
|
||||||
expect(genCode(root.properties[0].value)).toBe('1');
|
expect(genCode(root.variables[0].value)).toBe('1');
|
||||||
expect(root.properties[1].isComputed).toBe(true);
|
expect(root.variables[1].isComputed).toBe(true);
|
||||||
expect(genCode(root.properties[1].value)).toBe('foo');
|
expect(genCode(root.variables[1].value)).toBe('foo');
|
||||||
expect(root.dependencyMap).toEqual({ bar: ['foo'] });
|
expect(root.dependencyMap).toEqual({ bar: ['foo'] });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -57,9 +57,9 @@ describe('analyze properties', () => {
|
||||||
let bar = { foo: foo ? a : b };
|
let bar = { foo: foo ? a : b };
|
||||||
})
|
})
|
||||||
`);
|
`);
|
||||||
expect(root.properties.length).toBe(4);
|
expect(root.variables.length).toBe(4);
|
||||||
expect(root.properties[3].isComputed).toBe(true);
|
expect(root.variables[3].isComputed).toBe(true);
|
||||||
expect(genCode(root.properties[3].value)).toMatchInlineSnapshot(`
|
expect(genCode(root.variables[3].value)).toMatchInlineSnapshot(`
|
||||||
"{
|
"{
|
||||||
foo: foo ? a : b
|
foo: foo ? a : b
|
||||||
}"
|
}"
|
||||||
|
@ -73,8 +73,8 @@ describe('analyze properties', () => {
|
||||||
let bar = foo;
|
let bar = foo;
|
||||||
})
|
})
|
||||||
`);
|
`);
|
||||||
expect(root.properties.length).toBe(1);
|
expect(root.variables.length).toBe(1);
|
||||||
expect(root.properties[0].isComputed).toBe(true);
|
expect(root.variables[0].isComputed).toBe(true);
|
||||||
expect(root.dependencyMap).toEqual({ bar: ['foo'] });
|
expect(root.dependencyMap).toEqual({ bar: ['foo'] });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -84,8 +84,8 @@ describe('analyze properties', () => {
|
||||||
let bar = [foo1, first, last];
|
let bar = [foo1, first, last];
|
||||||
})
|
})
|
||||||
`);
|
`);
|
||||||
expect(root.properties.length).toBe(1);
|
expect(root.variables.length).toBe(1);
|
||||||
expect(root.properties[0].isComputed).toBe(true);
|
expect(root.variables[0].isComputed).toBe(true);
|
||||||
expect(root.dependencyMap).toEqual({ bar: ['foo1', 'first', 'last'] });
|
expect(root.dependencyMap).toEqual({ bar: ['foo1', 'first', 'last'] });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -96,8 +96,8 @@ describe('analyze properties', () => {
|
||||||
let bar = cond ? count : window.innerWidth;
|
let bar = cond ? count : window.innerWidth;
|
||||||
})
|
})
|
||||||
`);
|
`);
|
||||||
expect(root.properties.length).toBe(1);
|
expect(root.variables.length).toBe(1);
|
||||||
expect(root.properties[0].isComputed).toBe(false);
|
expect(root.variables[0].isComputed).toBe(false);
|
||||||
expect(root.dependencyMap).toEqual({});
|
expect(root.dependencyMap).toEqual({});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -112,9 +112,9 @@ describe('analyze properties', () => {
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
`);
|
`);
|
||||||
expect(root.properties.length).toBe(2);
|
expect(root.variables.length).toBe(2);
|
||||||
expect(root.dependencyMap).toEqual({ Sub: ['foo'] });
|
expect(root.dependencyMap).toEqual({ Sub: ['foo'] });
|
||||||
expect((root.properties[1].value as ComponentNode).dependencyMap).toMatchInlineSnapshot(`
|
expect((root.variables[1].value as ComponentNode).dependencyMap).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"bar": [
|
"bar": [
|
||||||
"foo",
|
"foo",
|
||||||
|
@ -135,9 +135,9 @@ describe('analyze properties', () => {
|
||||||
function onInput() {}
|
function onInput() {}
|
||||||
})
|
})
|
||||||
`);
|
`);
|
||||||
expect(root.properties.map(p => p.name)).toEqual(['foo', 'onClick', 'onHover', 'onInput']);
|
expect(root.variables.map(p => p.name)).toEqual(['foo', 'onClick', 'onHover', 'onInput']);
|
||||||
expect(root.properties[1].isMethod).toBe(true);
|
expect(root.variables[1].type).toBe('method');
|
||||||
expect(root.properties[2].isMethod).toBe(true);
|
expect(root.variables[2].type).toBe('method');
|
||||||
expect(root.dependencyMap).toMatchInlineSnapshot('{}');
|
expect(root.dependencyMap).toMatchInlineSnapshot('{}');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { propsAnalyze } from '../../src/analyze/propsAnalyze';
|
import { functionalMacroAnalyze } from '../../src/analyze/functionalMacroAnalyze';
|
||||||
import { watchAnalyze } from '../../src/analyze/watchAnalyze';
|
|
||||||
import { genCode, mockAnalyze } from '../mock';
|
import { genCode, mockAnalyze } from '../mock';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
const analyze = (code: string) => mockAnalyze(code, [watchAnalyze]);
|
const analyze = (code: string) => mockAnalyze(code, [functionalMacroAnalyze]);
|
||||||
|
|
||||||
describe('watchAnalyze', () => {
|
describe('watchAnalyze', () => {
|
||||||
it('should analyze watch expressions', () => {
|
it('should analyze watch expressions', () => {
|
||||||
|
|
Loading…
Reference in New Issue