refactor(proposal): use bitmap instead of dependency map
This commit is contained in:
parent
be4456f225
commit
f32da0e9c7
|
@ -0,0 +1,24 @@
|
|||
# @openinlua/babel-api
|
||||
A package that encapsulates the babel API for use in the transpiler.
|
||||
|
||||
To implement the dependency injection pattern, the package exports a function that registers the babel API in the
|
||||
transpiler.
|
||||
|
||||
```ts
|
||||
import { registerBabelAPI } from '@openinlua/babel-api';
|
||||
|
||||
function plugin(api: typeof babel) {
|
||||
registerBabelAPI(api);
|
||||
|
||||
// Your babel plugin code here.
|
||||
}
|
||||
```
|
||||
|
||||
And then you can import to use it.
|
||||
> types can use as a `type` or as a `namespace` for the babel API.
|
||||
|
||||
```ts
|
||||
import { types as t } from '@openinlua/babel-api';
|
||||
|
||||
t.isIdentifier(node as t.Node);
|
||||
```
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "@openinula/babel-api",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"main": "src/index.mjs",
|
||||
"typings": "src/index.d.ts",
|
||||
"files": [
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.23.3",
|
||||
"@babel/types": "^7.24.0",
|
||||
"@types/babel__core": "^7.20.5"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import type babel from '@babel/core';
|
||||
|
||||
// use .d.ts to satisfy the type check
|
||||
export * as types from '@babel/types';
|
||||
|
||||
export declare function register(api: typeof babel): void;
|
||||
export declare function getBabelApi(): typeof babel;
|
|
@ -1,13 +1,20 @@
|
|||
import { type types as t } from '@babel/core';
|
||||
import type babel from '@babel/core';
|
||||
let _t: null | typeof types = null;
|
||||
let babelApi: typeof babel | null = null;
|
||||
export const register = (api: typeof babel) => {
|
||||
/** @type {null | typeof import('@babel/core').types} */
|
||||
let _t = null;
|
||||
/** @type {null | typeof import('@babel/core')} */
|
||||
let babelApi = null;
|
||||
|
||||
/**
|
||||
* @param {import('@babel/core')} api
|
||||
*/
|
||||
export const register = api => {
|
||||
babelApi = api;
|
||||
_t = api.types;
|
||||
};
|
||||
|
||||
export const getBabelApi = (): typeof babel => {
|
||||
/**
|
||||
* @returns {typeof import('@babel/core')}
|
||||
*/
|
||||
export const getBabelApi = () => {
|
||||
if (!babelApi) {
|
||||
throw new Error('Please call register() before using the babel api');
|
||||
}
|
||||
|
@ -28,4 +35,4 @@ export const types = new Proxy(
|
|||
return undefined;
|
||||
},
|
||||
}
|
||||
) as typeof t;
|
||||
);
|
|
@ -44,7 +44,8 @@
|
|||
"@types/babel__generator": "^7.6.8",
|
||||
"@types/babel__parser": "^7.1.1",
|
||||
"@types/babel__traverse": "^7.6.8",
|
||||
"jsx-view-parser": "workspace:*",
|
||||
"@openinula/jsx-view-parser": "workspace:*",
|
||||
"@openinula/babel-api": "workspace:*",
|
||||
"minimatch": "^9.0.3",
|
||||
"vitest": "^1.4.0"
|
||||
},
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
import { NodePath } from '@babel/core';
|
||||
import { LifeCycle, Visitor } from './types';
|
||||
import { addLifecycle, addWatch } from './nodeFactory';
|
||||
import * as t from '@babel/types';
|
||||
import { types as t } from '@openinula/babel-api';
|
||||
import { ON_MOUNT, ON_UNMOUNT, WATCH, WILL_MOUNT, WILL_UNMOUNT } from '../constants';
|
||||
import { extractFnFromMacro, getFnBody } from '../utils';
|
||||
|
||||
|
@ -51,6 +51,9 @@ export function functionalMacroAnalyze(): Visitor {
|
|||
if (calleeName === WATCH) {
|
||||
const fnNode = extractFnFromMacro(expression, WATCH);
|
||||
const deps = getWatchDeps(expression);
|
||||
if (!deps) {
|
||||
// we auto collect the deps from the function body
|
||||
}
|
||||
addWatch(ctx.current, fnNode, deps);
|
||||
return;
|
||||
}
|
|
@ -1,18 +1,14 @@
|
|||
import { type types as t, type NodePath } from '@babel/core';
|
||||
import { type NodePath } from '@babel/core';
|
||||
import { propsAnalyze } from './propsAnalyze';
|
||||
import { AnalyzeContext, Analyzer, ComponentNode, CondNode, Visitor } from './types';
|
||||
import { AnalyzeContext, Analyzer, ComponentNode, Visitor } from './types';
|
||||
import { addLifecycle, createComponentNode } from './nodeFactory';
|
||||
import { propertiesAnalyze } from './propertiesAnalyze';
|
||||
import { variablesAnalyze } from './variablesAnalyze';
|
||||
import { functionalMacroAnalyze } from './functionalMacroAnalyze';
|
||||
import { getFnBody } from '../utils';
|
||||
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 {
|
||||
return node && node.type === 'cond';
|
||||
}
|
||||
import { types as t } from '@openinula/babel-api';
|
||||
const builtinAnalyzers = [propsAnalyze, variablesAnalyze, functionalMacroAnalyze, viewAnalyze];
|
||||
|
||||
function mergeVisitor(...visitors: Analyzer[]): Visitor {
|
||||
return visitors.reduce<Visitor<AnalyzeContext>>((acc, cur) => {
|
||||
|
@ -75,7 +71,7 @@ export function analyzeFnComp(
|
|||
}
|
||||
|
||||
if (context.unhandledNode.length) {
|
||||
addLifecycle(componentNode, WILL_MOUNT, types.blockStatement(context.unhandledNode));
|
||||
addLifecycle(componentNode, WILL_MOUNT, t.blockStatement(context.unhandledNode));
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
@ -85,10 +81,9 @@ export function analyzeFnComp(
|
|||
* 2. identify the component's props, including children, alias, and default value
|
||||
* 3. analyze the early return of the component, build into the branch
|
||||
*
|
||||
* @param types
|
||||
* @param fnName
|
||||
* @param path
|
||||
* @param customAnalyzers
|
||||
* @param options
|
||||
*/
|
||||
export function analyze(
|
||||
fnName: string,
|
|
@ -14,9 +14,9 @@
|
|||
*/
|
||||
|
||||
import { NodePath, type types as t } from '@babel/core';
|
||||
import { ComponentNode, FunctionalExpression, LifeCycle, ViewNode } from './types';
|
||||
import { ComponentNode, FunctionalExpression, LifeCycle, ReactiveVariable } from './types';
|
||||
import { PropType } from '../constants';
|
||||
import { ViewParticle } from '@openinula/reactivity-parser';
|
||||
import { ViewParticle, PrevMap } from '@openinula/reactivity-parser';
|
||||
|
||||
export function createComponentNode(
|
||||
name: string,
|
||||
|
@ -25,36 +25,32 @@ export function createComponentNode(
|
|||
): ComponentNode {
|
||||
const comp: ComponentNode = {
|
||||
type: 'comp',
|
||||
level: parent ? parent.level + 1 : 0,
|
||||
name,
|
||||
props: [],
|
||||
child: undefined,
|
||||
children: undefined,
|
||||
variables: [],
|
||||
dependencyMap: {},
|
||||
reactiveMap: {},
|
||||
dependencyMap: parent ? { [PrevMap]: parent.dependencyMap } : {},
|
||||
lifecycle: {},
|
||||
parent,
|
||||
fnNode,
|
||||
get availableProps() {
|
||||
return comp.props
|
||||
.map(({ name, nestedProps, alias }) => {
|
||||
const nested = nestedProps ? nestedProps.map(name => name) : [];
|
||||
return [alias ? alias : name, ...nested];
|
||||
})
|
||||
.flat();
|
||||
},
|
||||
get ownAvailableVariables() {
|
||||
return [...comp.variables.filter(p => p.type === 'reactive').map(({ name }) => name), ...comp.availableProps];
|
||||
return [...comp.variables.filter((p): p is ReactiveVariable => p.type === 'reactive')];
|
||||
},
|
||||
get availableVariables() {
|
||||
return [...comp.ownAvailableVariables, ...(comp.parent ? comp.parent.availableVariables : [])];
|
||||
// Here is critical for the dependency analysis, must put parent's availableVariables first
|
||||
// so the subcomponent can react to the parent's variables change
|
||||
return [...(comp.parent ? comp.parent.availableVariables : []), ...comp.ownAvailableVariables];
|
||||
},
|
||||
};
|
||||
|
||||
return comp;
|
||||
}
|
||||
|
||||
export function addProperty(comp: ComponentNode, name: string, value: t.Expression | null, isComputed: boolean) {
|
||||
comp.variables.push({ name, value, isComputed, type: 'reactive' });
|
||||
export function addProperty(comp: ComponentNode, name: string, value: t.Expression | null, deps: string[] | null) {
|
||||
comp.variables.push({ name, value, isComputed: !!deps?.length, type: 'reactive', deps });
|
||||
if (comp.dependencyMap[name] === undefined) {
|
||||
comp.dependencyMap[name] = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function addMethod(comp: ComponentNode, name: string, value: FunctionalExpression) {
|
||||
|
@ -98,9 +94,7 @@ export function addWatch(
|
|||
}
|
||||
|
||||
export function setViewChild(comp: ComponentNode, view: ViewParticle[], usedPropertySet: Set<string>) {
|
||||
const viewNode: ViewNode = {
|
||||
content: view,
|
||||
usedPropertySet,
|
||||
};
|
||||
comp.child = viewNode;
|
||||
// TODO: Maybe we should merge
|
||||
comp.usedPropertySet = usedPropertySet;
|
||||
comp.children = view;
|
||||
}
|
|
@ -1,8 +1,17 @@
|
|||
import { type types as t, type NodePath } from '@babel/core';
|
||||
import { type NodePath } from '@babel/core';
|
||||
import { AnalyzeContext, Visitor } from './types';
|
||||
import { addProp } from './nodeFactory';
|
||||
import { PropType } from '../constants';
|
||||
import { types } from '../babelTypes';
|
||||
import { types as t } from '@openinula/babel-api';
|
||||
|
||||
export interface Prop {
|
||||
name: string;
|
||||
type: PropType;
|
||||
alias: string | null;
|
||||
default: t.Expression | null;
|
||||
nestedProps: string[] | null;
|
||||
nestedRelationship: t.ObjectPattern | t.ArrayPattern | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze the props deconstructing in the function component
|
||||
|
@ -29,8 +38,8 @@ export function propsAnalyze(): Visitor {
|
|||
// --- 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;
|
||||
if (t.isIdentifier(key) || t.isStringLiteral(key)) {
|
||||
const name = t.isIdentifier(key) ? key.name : key.value;
|
||||
analyzeSingleProp(value, name, path, ctx);
|
||||
return;
|
||||
}
|
||||
|
@ -57,17 +66,17 @@ function analyzeSingleProp(
|
|||
let alias: string | null = null;
|
||||
const nestedProps: string[] | null = [];
|
||||
let nestedRelationship: t.ObjectPattern | t.ArrayPattern | null = null;
|
||||
if (types.isIdentifier(value)) {
|
||||
if (t.isIdentifier(value)) {
|
||||
// 1. handle alias without default value
|
||||
// handle alias without default value
|
||||
if (key !== value.name) {
|
||||
alias = value.name;
|
||||
}
|
||||
} else if (types.isAssignmentPattern(value)) {
|
||||
} else if (t.isAssignmentPattern(value)) {
|
||||
// 2. handle default value case
|
||||
const assignedName = value.left;
|
||||
defaultVal = value.right;
|
||||
if (types.isIdentifier(assignedName)) {
|
||||
if (t.isIdentifier(assignedName)) {
|
||||
if (assignedName.name !== key) {
|
||||
// handle alias in default value case
|
||||
alias = assignedName.name;
|
||||
|
@ -75,7 +84,7 @@ function analyzeSingleProp(
|
|||
} else {
|
||||
throw Error(`Unsupported assignment type in object destructuring: ${assignedName.type}`);
|
||||
}
|
||||
} else if (types.isObjectPattern(value) || types.isArrayPattern(value)) {
|
||||
} 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}]}
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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 type { NodePath } from '@babel/core';
|
||||
import { AnalyzeContext, DependencyMap } from '../types';
|
||||
import { types as t } from '@openinula/babel-api';
|
||||
import { reactivityFuncNames } from '../../const';
|
||||
import { PrevMap } from '@openinula/reactivity-parser';
|
||||
|
||||
/**
|
||||
* @brief Get all valid dependencies of a babel path
|
||||
* @param propertyKey
|
||||
* @param path
|
||||
* @param ctx
|
||||
* @returns
|
||||
*/
|
||||
export function getDependenciesFromNode(
|
||||
propertyKey: string,
|
||||
path: NodePath<t.Expression | t.ClassDeclaration>,
|
||||
{ current }: AnalyzeContext
|
||||
) {
|
||||
// ---- Deps: console.log(count)
|
||||
const deps = new Set<string>();
|
||||
// ---- Assign deps: count = 1 or count++
|
||||
const assignDeps = new Set<string>();
|
||||
const depNodes: Record<string, t.Expression[]> = {};
|
||||
|
||||
const visitor = (innerPath: NodePath<t.Identifier>) => {
|
||||
const propertyKey = innerPath.node.name;
|
||||
if (isAssignmentExpressionLeft(innerPath) || isAssignmentFunction(innerPath)) {
|
||||
assignDeps.add(propertyKey);
|
||||
} else if (current.availableVariables.includes(propertyKey)) {
|
||||
deps.add(propertyKey);
|
||||
findDependency(current.dependencyMap, propertyKey)?.forEach(deps.add.bind(deps));
|
||||
if (!depNodes[propertyKey]) depNodes[propertyKey] = [];
|
||||
depNodes[propertyKey].push(t.cloneNode(innerPath.node));
|
||||
}
|
||||
};
|
||||
if (path.isIdentifier()) {
|
||||
visitor(path);
|
||||
}
|
||||
path.traverse({
|
||||
Identifier: visitor,
|
||||
});
|
||||
|
||||
// ---- Eliminate deps that are assigned in the same method
|
||||
// e.g. { console.log(count); 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)
|
||||
);
|
||||
}
|
||||
|
||||
function findDependency(dependencyMap: DependencyMap, propertyKey: string) {
|
||||
let currentMap: DependencyMap | undefined = dependencyMap;
|
||||
do {
|
||||
if (currentMap[propertyKey] !== undefined) {
|
||||
return currentMap[propertyKey];
|
||||
}
|
||||
// trace back to the previous map
|
||||
currentMap = currentMap[PrevMap];
|
||||
} while (currentMap);
|
||||
return null;
|
||||
}
|
|
@ -15,20 +15,21 @@
|
|||
|
||||
import { type NodePath, types as t } from '@babel/core';
|
||||
import { ON_MOUNT, ON_UNMOUNT, PropType, WILL_MOUNT, WILL_UNMOUNT } from '../constants';
|
||||
import { ViewParticle } from '@openinula/reactivity-parser';
|
||||
import { ViewParticle, PrevMap } from '@openinula/reactivity-parser';
|
||||
|
||||
export type LifeCycle = typeof WILL_MOUNT | typeof ON_MOUNT | typeof WILL_UNMOUNT | typeof ON_UNMOUNT;
|
||||
type Bitmap = number;
|
||||
|
||||
export type FunctionalExpression = t.FunctionExpression | t.ArrowFunctionExpression;
|
||||
|
||||
interface BaseVariable<V> {
|
||||
name: string;
|
||||
value: V;
|
||||
}
|
||||
|
||||
export interface ReactiveVariable extends BaseVariable<t.Expression | null> {
|
||||
type: 'reactive';
|
||||
// indicate the value is a state or computed or watch
|
||||
listeners?: string[];
|
||||
level: number;
|
||||
bitmap?: Bitmap;
|
||||
// need a flag for computed to gen a getter
|
||||
// watch is a static computed
|
||||
|
@ -38,11 +39,13 @@ export interface ReactiveVariable extends BaseVariable<t.Expression | null> {
|
|||
export interface MethodVariable extends BaseVariable<FunctionalExpression> {
|
||||
type: 'method';
|
||||
}
|
||||
|
||||
export interface SubCompVariable extends BaseVariable<ComponentNode> {
|
||||
type: 'subComp';
|
||||
}
|
||||
|
||||
export type Variable = ReactiveVariable | MethodVariable | SubCompVariable;
|
||||
|
||||
export interface Prop {
|
||||
name: string;
|
||||
type: PropType;
|
||||
|
@ -51,12 +54,17 @@ export interface Prop {
|
|||
nestedProps: string[] | null;
|
||||
nestedRelationship: t.ObjectPattern | t.ArrayPattern | null;
|
||||
}
|
||||
|
||||
export interface ComponentNode {
|
||||
type: 'comp';
|
||||
name: string;
|
||||
props: Prop[];
|
||||
level: number;
|
||||
// The variables defined in the component
|
||||
variables: Variable[];
|
||||
/**
|
||||
* The used properties in the component
|
||||
*/
|
||||
usedPropertySet?: Set<string>;
|
||||
/**
|
||||
* The available props for the component, including the nested props
|
||||
*/
|
||||
|
@ -64,37 +72,27 @@ export interface ComponentNode {
|
|||
/**
|
||||
* The available variables and props owned by the component
|
||||
*/
|
||||
ownAvailableVariables: string[];
|
||||
ownAvailableVariables: ReactiveVariable[];
|
||||
/**
|
||||
* The available variables and props for the component and its parent
|
||||
*/
|
||||
availableVariables: string[];
|
||||
/**
|
||||
* The map to find the dependencies
|
||||
*/
|
||||
dependencyMap: {
|
||||
[key: string]: string[];
|
||||
};
|
||||
child?: ComponentNode | ViewNode;
|
||||
availableVariables: ReactiveVariable[];
|
||||
children?: (ComponentNode | ViewParticle)[];
|
||||
parent?: ComponentNode;
|
||||
/**
|
||||
* The function body of the fn component code
|
||||
*/
|
||||
fnNode: NodePath<FunctionalExpression>;
|
||||
/**
|
||||
* The map to find the state
|
||||
*/
|
||||
reactiveMap: Record<string, Bitmap>;
|
||||
lifecycle: Partial<Record<LifeCycle, t.Statement[]>>;
|
||||
/**
|
||||
* The watch fn in the component
|
||||
*/
|
||||
watch?: {
|
||||
bit: Bitmap;
|
||||
deps: NodePath<t.ArrayExpression> | null;
|
||||
callback: NodePath<t.ArrowFunctionExpression> | NodePath<t.FunctionExpression>;
|
||||
}[];
|
||||
}
|
||||
export interface ViewNode {
|
||||
content: ViewParticle[];
|
||||
usedPropertySet: Set<string>;
|
||||
}
|
||||
|
||||
export interface AnalyzeContext {
|
||||
level: number;
|
|
@ -13,21 +13,21 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { AnalyzeContext, Visitor } from './types';
|
||||
import { Visitor } from './types';
|
||||
import { addMethod, addProperty, addSubComponent, createComponentNode } 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 { type NodePath } from '@babel/core';
|
||||
import { COMPONENT } from '../constants';
|
||||
import { analyzeFnComp } from '.';
|
||||
import { getDependenciesFromNode } from './reactive/getDependencies';
|
||||
import { types as t } from '@openinula/babel-api';
|
||||
|
||||
/**
|
||||
* collect all properties and methods from the node
|
||||
* and analyze the dependencies of the properties
|
||||
* @returns
|
||||
*/
|
||||
export function propertiesAnalyze(): Visitor {
|
||||
export function variablesAnalyze(): Visitor {
|
||||
return {
|
||||
VariableDeclaration(path: NodePath<t.VariableDeclaration>, ctx) {
|
||||
const declarations = path.get('declarations');
|
||||
|
@ -41,7 +41,7 @@ export function propertiesAnalyze(): Visitor {
|
|||
// TODO: handle array destructuring
|
||||
throw new Error('Array destructuring is not supported yet');
|
||||
} else if (id.isIdentifier()) {
|
||||
// --- properties: the state / computed / plain properties / methods---
|
||||
// --- properties: the state / computed / plain properties / methods ---
|
||||
const init = declaration.get('init');
|
||||
let deps: string[] | null = null;
|
||||
if (isValidPath(init)) {
|
||||
|
@ -50,7 +50,7 @@ export function propertiesAnalyze(): Visitor {
|
|||
addMethod(ctx.current, id.node.name, init.node);
|
||||
return;
|
||||
}
|
||||
// handle the sub component
|
||||
// handle the subcomponent
|
||||
// Should like Component(() => {})
|
||||
if (
|
||||
init.isCallExpression() &&
|
||||
|
@ -64,14 +64,13 @@ export function propertiesAnalyze(): Visitor {
|
|||
const subComponent = createComponentNode(id.node.name, fnNode, ctx.current);
|
||||
|
||||
analyzeFnComp(fnNode, subComponent, ctx);
|
||||
deps = getDependenciesFromNode(id.node.name, init, ctx);
|
||||
addSubComponent(ctx.current, subComponent);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -81,7 +80,7 @@ export function propertiesAnalyze(): Visitor {
|
|||
throw new Error('Function declaration must have an id');
|
||||
}
|
||||
|
||||
const functionExpression = types.functionExpression(
|
||||
const functionExpression = t.functionExpression(
|
||||
path.node.id,
|
||||
path.node.params,
|
||||
path.node.body,
|
||||
|
@ -92,90 +91,3 @@ export function propertiesAnalyze(): Visitor {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.availableVariables.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)
|
||||
);
|
||||
}
|
|
@ -14,9 +14,9 @@
|
|||
*/
|
||||
|
||||
import { Visitor } from './types';
|
||||
import { type types as t, type NodePath } from '@babel/core';
|
||||
import { parseView as parseJSX } from 'jsx-view-parser';
|
||||
import { getBabelApi } from '../babelTypes';
|
||||
import { type NodePath } from '@babel/core';
|
||||
import { parseView as parseJSX } from '@openinula/jsx-view-parser';
|
||||
import { types as t, getBabelApi } from '@openinula/babel-api';
|
||||
import { parseReactivity } from '@openinula/reactivity-parser';
|
||||
import { reactivityFuncNames } from '../const';
|
||||
import { setViewChild } from './nodeFactory';
|
|
@ -8,7 +8,6 @@ export enum PropType {
|
|||
REST = 'rest',
|
||||
SINGLE = 'single',
|
||||
}
|
||||
|
||||
export const reactivityFuncNames = [
|
||||
// ---- Array
|
||||
'push',
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import { ViewParticle } from '@openinula/reactivity-parser';
|
||||
import { ComponentNode, Prop, Variable } from '../analyzer/types';
|
||||
import { type types as t, type NodePath } from '@babel/core';
|
||||
import { types } from '../babelTypes';
|
||||
|
||||
type Visitor = {
|
||||
[Type in (ViewParticle | ComponentNode)['type']]: (
|
||||
node: Extract<ViewParticle | ComponentNode, { type: Type }>,
|
||||
ctx: any
|
||||
) => void;
|
||||
};
|
||||
|
||||
interface GeneratorContext {
|
||||
classBodyNode: t.ClassBody;
|
||||
currentComp: ComponentNode;
|
||||
}
|
||||
|
||||
export function generateFnComp(compNode: ComponentNode) {
|
||||
const context = {
|
||||
classBodyNode: types.classBody([]),
|
||||
currentComp: compNode,
|
||||
};
|
||||
compNode.props.forEach(prop => {
|
||||
resolvePropDecorator(context, prop, 'Prop');
|
||||
});
|
||||
}
|
||||
|
||||
function reverseDependencyMap(dependencyMap: Record<string, Set<string>>) {
|
||||
const reversedMap: Record<string, Set<string>> = {};
|
||||
Object.entries(dependencyMap).forEach(([key, deps]) => {
|
||||
deps.forEach(dep => {
|
||||
if (!reversedMap[dep]) reversedMap[dep] = new Set();
|
||||
reversedMap[dep].add(key);
|
||||
});
|
||||
});
|
||||
|
||||
return reversedMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Decorator resolver: Prop/Env
|
||||
* Add:
|
||||
* $p/e$${key}
|
||||
* @param ctx
|
||||
* @param prop
|
||||
* @param decoratorName
|
||||
*/
|
||||
function resolvePropDecorator(ctx: GeneratorContext, prop: Prop, decoratorName: 'Prop' | 'Env' = 'Prop') {
|
||||
if (!ctx.classBodyNode) return;
|
||||
const key = prop.name;
|
||||
ctx.classBodyNode.body.push(types.classProperty(types.identifier(key), prop.default));
|
||||
|
||||
// Add tag to let the runtime know this property is a prop or env
|
||||
const tag = decoratorName.toLowerCase() === 'prop' ? 'p' : 'e';
|
||||
const derivedStatusKey = types.classProperty(types.identifier(`$${tag}$${key}`));
|
||||
ctx.classBodyNode.body.push(derivedStatusKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Decorator resolver: State
|
||||
* Add:
|
||||
* $$${key} = ${depIdx}
|
||||
* $sub$${key} = [${reversedDeps}]
|
||||
* @param ctx
|
||||
* @param varable
|
||||
* @param idx
|
||||
* @param reverseDeps
|
||||
*/
|
||||
function resolveStateDecorator(
|
||||
ctx: GeneratorContext,
|
||||
varable: Variable,
|
||||
idx: number,
|
||||
reverseDeps: Set<string> | undefined
|
||||
) {
|
||||
if (!ctx.classBodyNode) return;
|
||||
if (!types.isIdentifier(node.key)) return;
|
||||
const key = node.key.name;
|
||||
const idx = ctx.currentComp.variables.indexOf(node);
|
||||
|
||||
const idxNode = !ctx.dLightModel
|
||||
? [types.classProperty(types.identifier(`$$${key}`), types.numericLiteral(1 << idx))]
|
||||
: [];
|
||||
|
||||
const depsNode = reverseDeps
|
||||
? [
|
||||
types.classProperty(
|
||||
types.identifier(`$s$${key}`),
|
||||
types.arrayExpression([...reverseDeps].map(d => types.stringLiteral(d)))
|
||||
),
|
||||
]
|
||||
: [];
|
||||
|
||||
ctx.classBodyNode.body.splice(propertyIdx + 1, 0, ...idxNode, ...depsNode);
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import syntaxDecorators from '@babel/plugin-syntax-decorators';
|
||||
import syntaxJSX from '@babel/plugin-syntax-jsx';
|
||||
import syntaxTypescript from '@babel/plugin-syntax-typescript';
|
||||
import dlight from './plugin';
|
||||
import inulaNext from './plugin';
|
||||
import { type DLightOption } from './types';
|
||||
import { type ConfigAPI, type TransformOptions } from '@babel/core';
|
||||
import { plugin as fn2Class } from '@openinula/class-transformer';
|
||||
|
@ -14,7 +14,7 @@ export default function (_: ConfigAPI, options: DLightOption): TransformOptions
|
|||
[syntaxTypescript.default ?? syntaxTypescript, { isTSX: true }],
|
||||
[syntaxDecorators.default ?? syntaxDecorators, { legacy: true }],
|
||||
fn2Class,
|
||||
[dlight, options],
|
||||
[inulaNext, options],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
import type babel from '@babel/core';
|
||||
import { type PluginObj } from '@babel/core';
|
||||
import { type DLightOption } from './types';
|
||||
import { defaultAttributeMap, defaultHTMLTags } from './const';
|
||||
import { analyze } from './analyze';
|
||||
import { NodePath, type types as t } from '@babel/core';
|
||||
import { COMPONENT } from './constants';
|
||||
import { extractFnFromMacro } from './utils';
|
||||
import { register } from './babelTypes';
|
||||
|
||||
export default function (api: typeof babel, options: DLightOption): PluginObj {
|
||||
const { types } = api;
|
||||
const {
|
||||
files = '**/*.{js,ts,jsx,tsx}',
|
||||
excludeFiles = '**/{dist,node_modules,lib}/*',
|
||||
enableDevTools = false,
|
||||
customHtmlTags = defaultHtmlTags => defaultHtmlTags,
|
||||
attributeMap = defaultAttributeMap,
|
||||
} = options;
|
||||
|
||||
const htmlTags =
|
||||
typeof customHtmlTags === 'function'
|
||||
? customHtmlTags(defaultHTMLTags)
|
||||
: customHtmlTags.includes('*')
|
||||
? [...new Set([...defaultHTMLTags, ...customHtmlTags])].filter(tag => tag !== '*')
|
||||
: customHtmlTags;
|
||||
|
||||
register(api);
|
||||
return {
|
||||
visitor: {
|
||||
Program: {
|
||||
enter(path, { filename }) {
|
||||
// return pluginProvider.programEnterVisitor(path, filename);
|
||||
},
|
||||
exit(path, { filename }) {
|
||||
// pluginProvider.programExitVisitor.bind(pluginProvider);
|
||||
},
|
||||
},
|
||||
CallExpression(path: NodePath<t.CallExpression>) {
|
||||
// find the component, like: Component(() => {})
|
||||
const callee = path.get('callee');
|
||||
|
||||
if (callee.isIdentifier() && callee.node.name === COMPONENT) {
|
||||
const componentNode = extractFnFromMacro(path, COMPONENT);
|
||||
let name = '';
|
||||
// try to get the component name, when parent is a variable declarator
|
||||
if (path.parentPath.isVariableDeclarator()) {
|
||||
const lVal = path.parentPath.get('id');
|
||||
if (lVal.isIdentifier()) {
|
||||
name = lVal.node.name;
|
||||
} else {
|
||||
console.error('Component macro must be assigned to a variable');
|
||||
}
|
||||
}
|
||||
const root = analyze(name, componentNode, {
|
||||
htmlTags,
|
||||
});
|
||||
// The sub path has been visited, so we just skip
|
||||
path.skip();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,8 +1,12 @@
|
|||
import type babel from '@babel/core';
|
||||
import { type PluginObj } from '@babel/core';
|
||||
import { PluginProviderClass } from './pluginProvider';
|
||||
import { type DLightOption } from './types';
|
||||
import { defaultAttributeMap } from './const';
|
||||
import { defaultAttributeMap, defaultHTMLTags } from './const';
|
||||
import { analyze } from './analyzer';
|
||||
import { NodePath, type types as t } from '@babel/core';
|
||||
import { COMPONENT } from './constants';
|
||||
import { extractFnFromMacro } from './utils';
|
||||
import { register } from '@openinula/babel-api';
|
||||
|
||||
export default function (api: typeof babel, options: DLightOption): PluginObj {
|
||||
const { types } = api;
|
||||
|
@ -10,34 +14,51 @@ export default function (api: typeof babel, options: DLightOption): PluginObj {
|
|||
files = '**/*.{js,ts,jsx,tsx}',
|
||||
excludeFiles = '**/{dist,node_modules,lib}/*',
|
||||
enableDevTools = false,
|
||||
htmlTags = defaultHtmlTags => defaultHtmlTags,
|
||||
htmlTags: customHtmlTags = defaultHtmlTags => defaultHtmlTags,
|
||||
attributeMap = defaultAttributeMap,
|
||||
} = options;
|
||||
|
||||
const pluginProvider = new PluginProviderClass(
|
||||
api,
|
||||
types,
|
||||
Array.isArray(files) ? files : [files],
|
||||
Array.isArray(excludeFiles) ? excludeFiles : [excludeFiles],
|
||||
enableDevTools,
|
||||
htmlTags,
|
||||
attributeMap
|
||||
);
|
||||
const htmlTags =
|
||||
typeof customHtmlTags === 'function'
|
||||
? customHtmlTags(defaultHTMLTags)
|
||||
: customHtmlTags.includes('*')
|
||||
? [...new Set([...defaultHTMLTags, ...customHtmlTags])].filter(tag => tag !== '*')
|
||||
: customHtmlTags;
|
||||
|
||||
register(api);
|
||||
return {
|
||||
visitor: {
|
||||
Program: {
|
||||
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),
|
||||
},
|
||||
ClassDeclaration: {
|
||||
enter: pluginProvider.classEnter.bind(pluginProvider),
|
||||
exit: pluginProvider.classExit.bind(pluginProvider),
|
||||
CallExpression(path: NodePath<t.CallExpression>) {
|
||||
// find the component, like: Component(() => {})
|
||||
const callee = path.get('callee');
|
||||
|
||||
if (callee.isIdentifier() && callee.node.name === COMPONENT) {
|
||||
const componentNode = extractFnFromMacro(path, COMPONENT);
|
||||
let name = '';
|
||||
// try to get the component name, when parent is a variable declarator
|
||||
if (path.parentPath.isVariableDeclarator()) {
|
||||
const lVal = path.parentPath.get('id');
|
||||
if (lVal.isIdentifier()) {
|
||||
name = lVal.node.name;
|
||||
} else {
|
||||
console.error('Component macro must be assigned to a variable');
|
||||
}
|
||||
}
|
||||
const root = analyze(name, componentNode, {
|
||||
htmlTags,
|
||||
});
|
||||
// The sub path has been visited, so we just skip
|
||||
path.skip();
|
||||
}
|
||||
},
|
||||
ClassMethod: pluginProvider.classMethodVisitor.bind(pluginProvider),
|
||||
ClassProperty: pluginProvider.classPropertyVisitor.bind(pluginProvider),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -25,7 +25,7 @@ export function earlyReturnPlugin(): Visitor {
|
|||
|
||||
const argument = path.get('argument');
|
||||
if (argument.isJSXElement()) {
|
||||
currentComp.child = createJSXNode(currentComp, argument);
|
||||
currentComp.children = createJSXNode(currentComp, argument);
|
||||
}
|
||||
},
|
||||
IfStatement(ifStmt: NodePath<t.IfStatement>, context: AnalyzeContext) {
|
||||
|
@ -63,7 +63,7 @@ export function earlyReturnPlugin(): Visitor {
|
|||
);
|
||||
context.skipRest();
|
||||
|
||||
currentComp.child = createCondNode(currentComp, defaultComponent, branches);
|
||||
currentComp.children = createCondNode(currentComp, defaultComponent, branches);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -15,12 +15,11 @@
|
|||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { genCode, mockAnalyze } from '../mock';
|
||||
import { functionalMacroAnalyze } from '../../src/analyze/functionalMacroAnalyze';
|
||||
import { types } from '../../src/babelTypes';
|
||||
import { type NodePath, type types as t } from '@babel/core';
|
||||
import { functionalMacroAnalyze } from '../../src/analyzer/functionalMacroAnalyze';
|
||||
import { types as t } from '@openinula/babel-api';
|
||||
|
||||
const analyze = (code: string) => mockAnalyze(code, [functionalMacroAnalyze]);
|
||||
const combine = (body: t.Statement[]) => types.program(body);
|
||||
const combine = (body: t.Statement[]) => t.program(body);
|
||||
|
||||
describe('analyze lifeCycle', () => {
|
||||
it('should collect will mount', () => {
|
||||
|
|
|
@ -15,11 +15,11 @@
|
|||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { genCode, mockAnalyze } from '../mock';
|
||||
import { propertiesAnalyze } from '../../src/analyze/propertiesAnalyze';
|
||||
import { propsAnalyze } from '../../src/analyze/propsAnalyze';
|
||||
import { ComponentNode } from '../../src/analyze/types';
|
||||
import { variablesAnalyze } from '../../src/analyzer/variablesAnalyze';
|
||||
import { propsAnalyze } from '../../src/analyzer/propsAnalyze';
|
||||
import { ComponentNode, ReactiveVariable } from '../../src/analyzer/types';
|
||||
|
||||
const analyze = (code: string) => mockAnalyze(code, [propsAnalyze, propertiesAnalyze]);
|
||||
const analyze = (code: string) => mockAnalyze(code, [propsAnalyze, variablesAnalyze]);
|
||||
|
||||
describe('analyze properties', () => {
|
||||
it('should work', () => {
|
||||
|
@ -41,11 +41,14 @@ describe('analyze properties', () => {
|
|||
})
|
||||
`);
|
||||
expect(root.variables.length).toBe(2);
|
||||
expect(root.variables[0].isComputed).toBe(false);
|
||||
expect(genCode(root.variables[0].value)).toBe('1');
|
||||
expect(root.variables[1].isComputed).toBe(true);
|
||||
expect(genCode(root.variables[1].value)).toBe('foo');
|
||||
expect(root.dependencyMap).toEqual({ bar: ['foo'] });
|
||||
const fooVar = root.variables[0] as ReactiveVariable;
|
||||
expect(fooVar.isComputed).toBe(false);
|
||||
expect(genCode(fooVar.value)).toBe('1');
|
||||
|
||||
const barVar = root.variables[1] as ReactiveVariable;
|
||||
expect(barVar.isComputed).toBe(true);
|
||||
expect(genCode(barVar.value)).toBe('foo');
|
||||
expect(root.dependencyMap).toEqual({ bar: ['foo'], foo: null });
|
||||
});
|
||||
|
||||
it('should analyze dependency from state in different shape', () => {
|
||||
|
@ -58,13 +61,15 @@ describe('analyze properties', () => {
|
|||
})
|
||||
`);
|
||||
expect(root.variables.length).toBe(4);
|
||||
expect(root.variables[3].isComputed).toBe(true);
|
||||
expect(genCode(root.variables[3].value)).toMatchInlineSnapshot(`
|
||||
|
||||
const barVar = root.variables[3] as ReactiveVariable;
|
||||
expect(barVar.isComputed).toBe(true);
|
||||
expect(genCode(barVar.value)).toMatchInlineSnapshot(`
|
||||
"{
|
||||
foo: foo ? a : b
|
||||
}"
|
||||
`);
|
||||
expect(root.dependencyMap).toEqual({ bar: ['foo', 'a', 'b'] });
|
||||
expect(root.dependencyMap).toEqual({ bar: ['foo', 'a', 'b'], foo: null, a: null, b: null });
|
||||
});
|
||||
|
||||
it('should analyze dependency from props', () => {
|
||||
|
@ -74,7 +79,9 @@ describe('analyze properties', () => {
|
|||
})
|
||||
`);
|
||||
expect(root.variables.length).toBe(1);
|
||||
expect(root.variables[0].isComputed).toBe(true);
|
||||
|
||||
const barVar = root.variables[0] as ReactiveVariable;
|
||||
expect(barVar.isComputed).toBe(true);
|
||||
expect(root.dependencyMap).toEqual({ bar: ['foo'] });
|
||||
});
|
||||
|
||||
|
@ -85,7 +92,8 @@ describe('analyze properties', () => {
|
|||
})
|
||||
`);
|
||||
expect(root.variables.length).toBe(1);
|
||||
expect(root.variables[0].isComputed).toBe(true);
|
||||
const barVar = root.variables[0] as ReactiveVariable;
|
||||
expect(barVar.isComputed).toBe(true);
|
||||
expect(root.dependencyMap).toEqual({ bar: ['foo1', 'first', 'last'] });
|
||||
});
|
||||
|
||||
|
@ -97,8 +105,9 @@ describe('analyze properties', () => {
|
|||
})
|
||||
`);
|
||||
expect(root.variables.length).toBe(1);
|
||||
expect(root.variables[0].isComputed).toBe(false);
|
||||
expect(root.dependencyMap).toEqual({});
|
||||
const barVar = root.variables[0] as ReactiveVariable;
|
||||
expect(barVar.isComputed).toBe(false);
|
||||
expect(root.dependencyMap).toEqual({ bar: null });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -113,15 +122,90 @@ describe('analyze properties', () => {
|
|||
})
|
||||
`);
|
||||
expect(root.variables.length).toBe(2);
|
||||
expect(root.dependencyMap).toEqual({ Sub: ['foo'] });
|
||||
expect(root.dependencyMap).toEqual({ foo: null });
|
||||
expect((root.variables[1].value as ComponentNode).dependencyMap).toMatchInlineSnapshot(`
|
||||
{
|
||||
"bar": [
|
||||
"foo",
|
||||
],
|
||||
Symbol(prevMap): {
|
||||
"foo": null,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should analyze dependency in parent', () => {
|
||||
const root = analyze(`
|
||||
Component(({lastName}) => {
|
||||
let parentFirstName = 'sheldon';
|
||||
const parentName = parentFirstName + lastName;
|
||||
const Son = Component(() => {
|
||||
let middleName = parentName
|
||||
const name = 'shelly'+ middleName + lastName;
|
||||
const GrandSon = Component(() => {
|
||||
let grandSonName = 'bar' + lastName;
|
||||
});
|
||||
});
|
||||
})
|
||||
`);
|
||||
const sonNode = root.variables[2].value as ComponentNode;
|
||||
expect(sonNode.dependencyMap).toMatchInlineSnapshot(`
|
||||
{
|
||||
"middleName": [
|
||||
"parentName",
|
||||
"parentFirstName",
|
||||
"lastName",
|
||||
],
|
||||
"name": [
|
||||
"middleName",
|
||||
"parentName",
|
||||
"parentFirstName",
|
||||
"lastName",
|
||||
],
|
||||
Symbol(prevMap): {
|
||||
"parentFirstName": null,
|
||||
"parentName": [
|
||||
"parentFirstName",
|
||||
"lastName",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
const grandSonNode = sonNode.variables[2].value as ComponentNode;
|
||||
expect(grandSonNode.dependencyMap).toMatchInlineSnapshot(`
|
||||
{
|
||||
"grandSonName": [
|
||||
"lastName",
|
||||
],
|
||||
Symbol(prevMap): {
|
||||
"middleName": [
|
||||
"parentName",
|
||||
"parentFirstName",
|
||||
"lastName",
|
||||
],
|
||||
"name": [
|
||||
"middleName",
|
||||
"parentName",
|
||||
"parentFirstName",
|
||||
"lastName",
|
||||
],
|
||||
Symbol(prevMap): {
|
||||
"parentFirstName": null,
|
||||
"parentName": [
|
||||
"parentFirstName",
|
||||
"lastName",
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
// SubscriptionTree
|
||||
// const SubscriptionTree = {
|
||||
// lastName: ['parentName','son:middleName','son:name','son,grandSon:grandSonName'],
|
||||
//
|
||||
// }
|
||||
});
|
||||
|
||||
it('should collect method', () => {
|
||||
|
@ -138,6 +222,10 @@ describe('analyze properties', () => {
|
|||
expect(root.variables.map(p => p.name)).toEqual(['foo', 'onClick', 'onHover', 'onInput']);
|
||||
expect(root.variables[1].type).toBe('method');
|
||||
expect(root.variables[2].type).toBe('method');
|
||||
expect(root.dependencyMap).toMatchInlineSnapshot('{}');
|
||||
expect(root.dependencyMap).toMatchInlineSnapshot(`
|
||||
{
|
||||
"foo": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { genCode, mockAnalyze } from '../mock';
|
||||
import { PropType } from '../../src/constants';
|
||||
import { propsAnalyze } from '../../src/analyze/propsAnalyze';
|
||||
import { propsAnalyze } from '../../src/analyzer/propsAnalyze';
|
||||
|
||||
const analyze = (code: string) => mockAnalyze(code, [propsAnalyze]);
|
||||
|
||||
|
|
|
@ -1,19 +1,82 @@
|
|||
import { propertiesAnalyze } from '../../src/analyze/propertiesAnalyze';
|
||||
import { propsAnalyze } from '../../src/analyze/propsAnalyze';
|
||||
import { viewAnalyze } from '../../src/analyze/viewAnalyze';
|
||||
import { variablesAnalyze } from '../../src/analyzer/variablesAnalyze';
|
||||
import { propsAnalyze } from '../../src/analyzer/propsAnalyze';
|
||||
import { ComponentNode } from '../../src/analyzer/types';
|
||||
import { viewAnalyze } from '../../src/analyzer/viewAnalyze';
|
||||
import { genCode, mockAnalyze } from '../mock';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const analyze = (code: string) => mockAnalyze(code, [propsAnalyze, propertiesAnalyze, viewAnalyze]);
|
||||
|
||||
describe('watchAnalyze', () => {
|
||||
it('should analyze watch expressions', () => {
|
||||
const analyze = (code: string) => mockAnalyze(code, [propsAnalyze, variablesAnalyze, viewAnalyze]);
|
||||
describe('viewAnalyze', () => {
|
||||
it('should analyze view', () => {
|
||||
const root = analyze(/*js*/ `
|
||||
Comp(({name}) => {
|
||||
let count = 11
|
||||
return <div className={name}>{count}</div>
|
||||
})
|
||||
Component(({name ,className}) => {
|
||||
let count = name; // 1
|
||||
let doubleCount = count* 2; // 2
|
||||
let doubleCount2 = doubleCount* 2; // 4
|
||||
const Input = Component(() => {
|
||||
let count = 1;
|
||||
watch(() => {
|
||||
if (doubleCount2 > 10) {
|
||||
count++;
|
||||
}
|
||||
console.log(doubleCount2);
|
||||
});
|
||||
const update = changed => {
|
||||
if (changed & 0x1011) {
|
||||
node1.update(_$this0.count, _$this0.doubleCount);
|
||||
}
|
||||
};
|
||||
return <input>{count}{doubleCount}</input>;
|
||||
});
|
||||
return <div className={className + count}>{doubleCount2}</div>;
|
||||
});
|
||||
`);
|
||||
expect(true).toHaveLength(1);
|
||||
const div = root.children![0] as any;
|
||||
expect(div.children[0].content.dependencyIndexArr).toMatchInlineSnapshot(`
|
||||
[
|
||||
4,
|
||||
3,
|
||||
2,
|
||||
0,
|
||||
]
|
||||
`);
|
||||
expect(genCode(div.children[0].content.dependenciesNode)).toMatchInlineSnapshot('"[doubleCount2]"');
|
||||
expect(div.props.className.dependencyIndexArr).toMatchInlineSnapshot(`
|
||||
[
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
]
|
||||
`);
|
||||
expect(genCode(div.props.className.value)).toMatchInlineSnapshot('"className + count"');
|
||||
|
||||
// @ts-expect-error ignore ts here
|
||||
const InputCompNode = (root.variables[3] as ComponentNode).value;
|
||||
expect(InputCompNode.usedPropertySet).toMatchInlineSnapshot(`
|
||||
Set {
|
||||
"count",
|
||||
"doubleCount",
|
||||
"name",
|
||||
}
|
||||
`);
|
||||
// it's the {count}
|
||||
const inputFirstExp = InputCompNode.children[0].children[0];
|
||||
expect(inputFirstExp.content.dependencyIndexArr).toMatchInlineSnapshot(`
|
||||
[
|
||||
5,
|
||||
]
|
||||
`);
|
||||
expect(genCode(inputFirstExp.content.dependenciesNode)).toMatchInlineSnapshot('"[count]"');
|
||||
|
||||
// it's the {doubleCount}
|
||||
const inputSecondExp = InputCompNode.children[0].children[1];
|
||||
expect(inputSecondExp.content.dependencyIndexArr).toMatchInlineSnapshot(`
|
||||
[
|
||||
3,
|
||||
2,
|
||||
0,
|
||||
]
|
||||
`);
|
||||
expect(genCode(inputSecondExp.content.dependenciesNode)).toMatchInlineSnapshot('"[doubleCount]"');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { functionalMacroAnalyze } from '../../src/analyze/functionalMacroAnalyze';
|
||||
import { functionalMacroAnalyze } from '../../src/analyzer/functionalMacroAnalyze';
|
||||
import { genCode, mockAnalyze } from '../mock';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { transform } from './presets';
|
||||
import { transform } from '../presets';
|
||||
|
||||
describe('condition', () => {
|
||||
it('should transform jsx', () => {
|
|
@ -13,13 +13,13 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { Analyzer, ComponentNode, InulaNode } from '../src/analyze/types';
|
||||
import { Analyzer, ComponentNode } from '../src/analyzer/types';
|
||||
import { type PluginObj, transform as transformWithBabel } from '@babel/core';
|
||||
import syntaxJSX from '@babel/plugin-syntax-jsx';
|
||||
import { analyze } from '../src/analyze';
|
||||
import { analyze } from '../src/analyzer';
|
||||
import generate from '@babel/generator';
|
||||
import * as t from '@babel/types';
|
||||
import { register } from '../src/babelTypes';
|
||||
import { register } from '@openinula/babel-api';
|
||||
import { defaultHTMLTags } from '../src/const';
|
||||
|
||||
export function mockAnalyze(code: string, analyzers?: Analyzer[]): ComponentNode {
|
||||
|
@ -63,26 +63,3 @@ export function genCode(ast: t.Node | null) {
|
|||
}
|
||||
return generate(ast).code;
|
||||
}
|
||||
|
||||
export function printTree(node: InulaNode | undefined): any {
|
||||
if (!node) {
|
||||
return 'empty';
|
||||
}
|
||||
if (node.type === 'cond') {
|
||||
return {
|
||||
type: node.type,
|
||||
branch: node.branches.map(b => printTree(b.content)),
|
||||
children: printTree(node.child),
|
||||
};
|
||||
} else if (node.type === 'comp') {
|
||||
return {
|
||||
type: node.type,
|
||||
children: printTree(node.child),
|
||||
};
|
||||
} else if (node.type === 'jsx') {
|
||||
return {
|
||||
type: node.type,
|
||||
};
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { isCondNode } from '../../src/analyze';
|
||||
import { isCondNode } from '../../src/analyzer';
|
||||
import { mockAnalyze } from '../mock';
|
||||
|
||||
describe('analyze early return', () => {
|
||||
|
@ -30,7 +30,7 @@ describe('analyze early return', () => {
|
|||
</div>;
|
||||
}
|
||||
`);
|
||||
const branchNode = root?.child;
|
||||
const branchNode = root?.children;
|
||||
if (!isCondNode(branchNode)) {
|
||||
throw new Error('Should be branch node');
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ describe('analyze early return', () => {
|
|||
return <div></div>;
|
||||
}
|
||||
`);
|
||||
const branchNode = root?.child;
|
||||
const branchNode = root?.children;
|
||||
if (!isCondNode(branchNode)) {
|
||||
throw new Error('Should be branch node');
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ describe('analyze early return', () => {
|
|||
return <div></div>;
|
||||
}
|
||||
`);
|
||||
const branchNode = root?.child;
|
||||
const branchNode = root?.children;
|
||||
if (!isCondNode(branchNode)) {
|
||||
throw new Error('Should be branch node');
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
"@types/babel__generator": "^7.6.8",
|
||||
"@types/babel__parser": "^7.1.1",
|
||||
"@types/babel__traverse": "^7.6.8",
|
||||
"jsx-view-parser": "workspace:*",
|
||||
"@openinula/jsx-view-parser": "workspace:*",
|
||||
"minimatch": "^9.0.3",
|
||||
"vitest": "^1.4.0"
|
||||
},
|
||||
|
|
|
@ -3,7 +3,7 @@ import { type PluginObj } from '@babel/core';
|
|||
import { PluginProviderClass } from './pluginProvider';
|
||||
import { type DLightOption } from './types';
|
||||
import { defaultAttributeMap } from './const';
|
||||
import { analyze } from './analyze';
|
||||
import { analyze } from './analyzer';
|
||||
|
||||
export default function (api: typeof babel, options: DLightOption): PluginObj {
|
||||
const { types } = api;
|
||||
|
|
|
@ -3,7 +3,7 @@ import { type types as t, type NodePath } from '@babel/core';
|
|||
import { type PropertyContainer, type HTMLTags, type SnippetPropSubDepMap } from './types';
|
||||
import { minimatch } from 'minimatch';
|
||||
import { parseView, ViewUnit } from '@openinula/view-parser';
|
||||
import { parseView as parseJSX } from 'jsx-view-parser';
|
||||
import { parseView as parseJSX } from '@openinula/jsx-view-parser';
|
||||
import { parseReactivity } from '@openinula/reactivity-parser';
|
||||
import { generateSnippet, generateView } from '@openinula/view-generator';
|
||||
import {
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { isCondNode } from '../src/analyze';
|
||||
import { isCondNode } from '../src/analyzer';
|
||||
import { mockAnalyze } from './mock';
|
||||
|
||||
describe('analyze early return', () => {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
import { ComponentNode, InulaNode } from '../src/analyze/types';
|
||||
import babel, { type PluginObj, transform as transformWithBabel } from '@babel/core';
|
||||
import syntaxJSX from '@babel/plugin-syntax-jsx';
|
||||
import { analyze } from '../src/analyze';
|
||||
import { analyze } from '../src/analyzer';
|
||||
import generate from '@babel/generator';
|
||||
import * as t from '@babel/types';
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "jsx-view-parser",
|
||||
"name": "@openinula/jsx-view-parser",
|
||||
"version": "0.0.1",
|
||||
"description": "Inula jsx parser",
|
||||
"author": {
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@openinula/error-handler": "workspace:*",
|
||||
"@openinula/jsx-view-parser": "workspace:*",
|
||||
"@openinula/view-parser": "workspace:*"
|
||||
},
|
||||
"tsup": {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { type ViewUnit } from '@openinula/view-parser';
|
||||
import { type ViewUnit } from '@openinula/jsx-view-parser';
|
||||
import { ReactivityParser } from './parser';
|
||||
import { type ViewParticle, type ReactivityParserConfig } from './types';
|
||||
|
||||
|
@ -6,7 +6,6 @@ import { type ViewParticle, type ReactivityParserConfig } from './types';
|
|||
* @brief Parse view units to get used properties and view particles with reactivity
|
||||
* @param viewUnits
|
||||
* @param config
|
||||
* @param options
|
||||
* @returns [viewParticles, usedProperties]
|
||||
*/
|
||||
export function parseReactivity(viewUnits: ViewUnit[], config: ReactivityParserConfig): [ViewParticle[], Set<string>] {
|
||||
|
@ -21,5 +20,9 @@ export function parseReactivity(viewUnits: ViewUnit[], config: ReactivityParserC
|
|||
});
|
||||
return [dlParticles, usedProperties];
|
||||
}
|
||||
/**
|
||||
* The key to get the previous map in DependencyMap Chain
|
||||
*/
|
||||
export const PrevMap = Symbol('prevMap');
|
||||
|
||||
export type * from './types';
|
||||
|
|
|
@ -12,9 +12,7 @@ import {
|
|||
type ForParticle,
|
||||
type IfParticle,
|
||||
type EnvParticle,
|
||||
type SnippetParticle,
|
||||
SwitchParticle,
|
||||
TryParticle,
|
||||
DependencyMap,
|
||||
} from './types';
|
||||
import { type NodePath, type types as t, type traverse } from '@babel/core';
|
||||
import {
|
||||
|
@ -22,16 +20,14 @@ import {
|
|||
type HTMLUnit,
|
||||
type ViewUnit,
|
||||
type CompUnit,
|
||||
type ViewProp,
|
||||
type UnitProp,
|
||||
type ForUnit,
|
||||
type IfUnit,
|
||||
type EnvUnit,
|
||||
type ExpUnit,
|
||||
type SnippetUnit,
|
||||
SwitchUnit,
|
||||
TryUnit,
|
||||
} from '@openinula/view-parser';
|
||||
} from '@openinula/jsx-view-parser';
|
||||
import { DLError } from './error';
|
||||
import { PrevMap } from '.';
|
||||
|
||||
export class ReactivityParser {
|
||||
private readonly config: ReactivityParserConfig;
|
||||
|
@ -40,7 +36,7 @@ export class ReactivityParser {
|
|||
private readonly traverse: typeof traverse;
|
||||
private readonly availableProperties: string[];
|
||||
private readonly availableIdentifiers?: string[];
|
||||
private readonly dependencyMap: Record<string, string[]>;
|
||||
private readonly dependencyMap: DependencyMap;
|
||||
private readonly identifierDepMap: Record<string, string[]>;
|
||||
private readonly dependencyParseType;
|
||||
private readonly reactivityFuncNames;
|
||||
|
@ -99,12 +95,9 @@ export class ReactivityParser {
|
|||
if (viewUnit.type === 'html') return this.parseHTML(viewUnit);
|
||||
if (viewUnit.type === 'comp') return this.parseComp(viewUnit);
|
||||
if (viewUnit.type === 'for') return this.parseFor(viewUnit);
|
||||
if (viewUnit.type === 'try') return this.parseTry(viewUnit);
|
||||
if (viewUnit.type === 'if') return this.parseIf(viewUnit);
|
||||
if (viewUnit.type === 'env') return this.parseEnv(viewUnit);
|
||||
if (viewUnit.type === 'exp') return this.parseExp(viewUnit);
|
||||
if (viewUnit.type === 'switch') return this.parseSwitch(viewUnit);
|
||||
if (viewUnit.type === 'snippet') return this.parseSnippet(viewUnit);
|
||||
return DLError.throw1();
|
||||
}
|
||||
|
||||
|
@ -420,45 +413,6 @@ export class ReactivityParser {
|
|||
};
|
||||
}
|
||||
|
||||
// ---- @Switch ----
|
||||
/**
|
||||
* @brief Parse a SwitchUnit into an SwitchParticle with dependencies
|
||||
* @param switchUnit
|
||||
* @returns SwitchParticle
|
||||
*/
|
||||
private parseSwitch(switchUnit: SwitchUnit): SwitchParticle {
|
||||
return {
|
||||
type: 'switch',
|
||||
discriminant: {
|
||||
value: switchUnit.discriminant,
|
||||
...this.getDependencies(switchUnit.discriminant),
|
||||
},
|
||||
branches: switchUnit.branches.map(branch => ({
|
||||
case: {
|
||||
value: branch.case,
|
||||
...this.getDependencies(branch.case),
|
||||
},
|
||||
children: branch.children.map(this.parseViewParticle.bind(this)),
|
||||
break: branch.break,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// ---- @Try ----
|
||||
/**
|
||||
* @brief Parse a TryUnit into an TryParticle with dependencies
|
||||
* @param tryUnit
|
||||
* @returns TryParticle
|
||||
*/
|
||||
private parseTry(tryUnit: TryUnit): TryParticle {
|
||||
return {
|
||||
type: 'try',
|
||||
children: tryUnit.children.map(this.parseViewParticle.bind(this)),
|
||||
exception: tryUnit.exception,
|
||||
catchChildren: tryUnit.catchChildren.map(this.parseViewParticle.bind(this)),
|
||||
};
|
||||
}
|
||||
|
||||
// ---- @Env ----
|
||||
/**
|
||||
* @brief Parse an EnvUnit into an EnvParticle with dependencies
|
||||
|
@ -492,38 +446,13 @@ export class ReactivityParser {
|
|||
return expParticle;
|
||||
}
|
||||
|
||||
// ---- @Snippet ----
|
||||
/**
|
||||
* @brief Parse a SnippetUnit into a SnippetParticle with dependencies
|
||||
* @param snippetUnit
|
||||
* @returns SnippetParticle
|
||||
*/
|
||||
private parseSnippet(snippetUnit: SnippetUnit): SnippetParticle {
|
||||
const snippetParticle: SnippetParticle = {
|
||||
type: 'snippet',
|
||||
tag: snippetUnit.tag,
|
||||
props: {},
|
||||
children: [],
|
||||
};
|
||||
if (snippetUnit.props) {
|
||||
snippetParticle.props = Object.fromEntries(
|
||||
Object.entries(snippetUnit.props).map(([key, prop]) => [key, this.generateDependencyProp(prop)])
|
||||
);
|
||||
}
|
||||
if (snippetUnit.children) {
|
||||
snippetParticle.children = snippetUnit.children.map(this.parseViewParticle.bind(this));
|
||||
}
|
||||
|
||||
return snippetParticle;
|
||||
}
|
||||
|
||||
// ---- Dependencies ----
|
||||
/**
|
||||
* @brief Generate a dependency prop with dependencies
|
||||
* @param prop
|
||||
* @returns DependencyProp
|
||||
*/
|
||||
private generateDependencyProp(prop: ViewProp): DependencyProp {
|
||||
private generateDependencyProp(prop: UnitProp): DependencyProp {
|
||||
const dependencyProp: DependencyProp = {
|
||||
value: prop.value,
|
||||
...this.getDependencies(prop.value),
|
||||
|
@ -559,13 +488,12 @@ export class ReactivityParser {
|
|||
// ---- Both id and prop deps need to be calculated because
|
||||
// id is for snippet update, prop is normal update
|
||||
// in a snippet, the depsNode should be both id and prop
|
||||
const [directIdentifierDeps, identifierDepNodes] = this.getIdentifierDependencies(node);
|
||||
const [directPropertyDeps, propertyDepNodes] = this.getPropertyDependencies(node);
|
||||
const directDependencies = this.dependencyParseType === 'identifier' ? directIdentifierDeps : directPropertyDeps;
|
||||
const directDependencies = directPropertyDeps;
|
||||
const identifierMapDependencies = this.getIdentifierMapDependencies(node);
|
||||
const deps = [...new Set([...directDependencies, ...identifierMapDependencies])];
|
||||
|
||||
const depNodes = [...identifierDepNodes, ...propertyDepNodes] as t.Expression[];
|
||||
const depNodes = [...propertyDepNodes] as t.Expression[];
|
||||
|
||||
return {
|
||||
dynamic: depNodes.length > 0 || deps.length > 0,
|
||||
|
@ -625,7 +553,7 @@ export class ReactivityParser {
|
|||
});
|
||||
|
||||
deps.forEach(this.usedProperties.add.bind(this.usedProperties));
|
||||
return [[...deps].map(dep => this.availableProperties.indexOf(dep)), dependencyNodes];
|
||||
return [[...deps].map(dep => this.availableProperties.lastIndexOf(dep)), dependencyNodes];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -646,9 +574,9 @@ export class ReactivityParser {
|
|||
|
||||
const wrappedNode = this.valueWrapper(node);
|
||||
this.traverse(wrappedNode, {
|
||||
MemberExpression: innerPath => {
|
||||
if (!this.t.isIdentifier(innerPath.node.property) || !this.t.isThisExpression(innerPath.node.object)) return;
|
||||
const propertyKey = innerPath.node.property.name;
|
||||
Identifier: innerPath => {
|
||||
const propertyKey = innerPath.node.name;
|
||||
|
||||
if (this.isAssignmentExpressionLeft(innerPath) || this.isAssignmentFunction(innerPath)) {
|
||||
assignDeps.add(propertyKey);
|
||||
} else if (
|
||||
|
@ -657,13 +585,12 @@ export class ReactivityParser {
|
|||
!this.isMemberInManualFunction(innerPath)
|
||||
) {
|
||||
deps.add(propertyKey);
|
||||
this.dependencyMap[propertyKey]?.forEach(deps.add.bind(deps));
|
||||
if (!depNodes[propertyKey]) depNodes[propertyKey] = [];
|
||||
depNodes[propertyKey].push(this.geneDependencyNode(innerPath));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const dependencyIdxArr = deduplicate([...deps].map(this.calDependencyIndexArr).flat());
|
||||
assignDeps.forEach(dep => {
|
||||
deps.delete(dep);
|
||||
delete depNodes[dep];
|
||||
|
@ -676,7 +603,36 @@ export class ReactivityParser {
|
|||
});
|
||||
|
||||
deps.forEach(this.usedProperties.add.bind(this.usedProperties));
|
||||
return [[...deps].map(dep => this.availableProperties.indexOf(dep)), dependencyNodes];
|
||||
return [dependencyIdxArr, dependencyNodes];
|
||||
}
|
||||
|
||||
private calDependencyIndexArr = (directDepKey: string) => {
|
||||
// iterate the availableProperties reversely to find the index of the property
|
||||
// cause the availableProperties is in the order of the code
|
||||
const chainedDepKeys = this.findDependency(directDepKey);
|
||||
const depKeyQueue = chainedDepKeys ? [directDepKey, ...chainedDepKeys] : [directDepKey];
|
||||
depKeyQueue.forEach(this.usedProperties.add.bind(this.usedProperties));
|
||||
|
||||
let dep = depKeyQueue.shift();
|
||||
const result: number[] = [];
|
||||
for (let i = this.availableProperties.length - 1; i >= 0; i--) {
|
||||
if (this.availableProperties[i] === dep) {
|
||||
result.push(i);
|
||||
dep = depKeyQueue.shift();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
private findDependency(propertyKey: string) {
|
||||
let currentMap: DependencyMap | undefined = this.dependencyMap;
|
||||
do {
|
||||
if (currentMap[propertyKey] !== undefined) {
|
||||
return currentMap[propertyKey];
|
||||
}
|
||||
currentMap = currentMap[PrevMap];
|
||||
} while (currentMap);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -738,7 +694,7 @@ export class ReactivityParser {
|
|||
});
|
||||
|
||||
deps.forEach(this.usedProperties.add.bind(this.usedProperties));
|
||||
return [...deps].map(dep => this.availableProperties.indexOf(dep));
|
||||
return [...deps].map(dep => this.availableProperties.lastIndexOf(dep));
|
||||
}
|
||||
|
||||
// ---- Utils ----
|
||||
|
@ -781,7 +737,7 @@ export class ReactivityParser {
|
|||
* @param prop
|
||||
* @returns is a static prop
|
||||
*/
|
||||
private isStaticProp(prop: ViewProp): boolean {
|
||||
private isStaticProp(prop: UnitProp): boolean {
|
||||
const { value, viewPropMap } = prop;
|
||||
return (
|
||||
(!viewPropMap || Object.keys(viewPropMap).length === 0) &&
|
||||
|
@ -950,10 +906,9 @@ export class ReactivityParser {
|
|||
}
|
||||
if (!parentPath) return false;
|
||||
return (
|
||||
this.t.isCallExpression(parentPath.node) &&
|
||||
this.t.isMemberExpression(parentPath.node.callee) &&
|
||||
this.t.isIdentifier(parentPath.node.callee.property) &&
|
||||
this.reactivityFuncNames.includes(parentPath.node.callee.property.name)
|
||||
parentPath.isCallExpression() &&
|
||||
parentPath.get('callee').isIdentifier() &&
|
||||
this.reactivityFuncNames.includes((parentPath.get('callee').node as t.Identifier).name)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1021,3 +976,7 @@ export class ReactivityParser {
|
|||
return Math.random().toString(36).slice(2);
|
||||
}
|
||||
}
|
||||
|
||||
function deduplicate<T>(arr: T[]): T[] {
|
||||
return [...new Set(arr)];
|
||||
}
|
||||
|
|
|
@ -4,13 +4,13 @@ import { type CompParticle } from '../types';
|
|||
|
||||
describe('Dependency', () => {
|
||||
it('should parse the correct dependency', () => {
|
||||
const viewParticles = parse('Comp(this.flag)');
|
||||
const viewParticles = parse('Comp(flag)');
|
||||
const content = (viewParticles[0] as CompParticle).props._$content;
|
||||
expect(content?.dependencyIndexArr).toContain(0);
|
||||
});
|
||||
|
||||
it('should parse the correct dependency when interfacing the dependency chain', () => {
|
||||
const viewParticles = parse('Comp(this.doubleCount)');
|
||||
const viewParticles = parse('Comp(doubleCount)');
|
||||
const content = (viewParticles[0] as CompParticle).props._$content;
|
||||
const dependency = content?.dependencyIndexArr;
|
||||
// ---- doubleCount depends on count, count depends on flag
|
||||
|
@ -21,47 +21,41 @@ describe('Dependency', () => {
|
|||
});
|
||||
|
||||
it('should not parse the dependency if the property is not in the availableProperties', () => {
|
||||
const viewParticles = parse('Comp(this.notExist)');
|
||||
const content = (viewParticles[0] as CompParticle).props._$content;
|
||||
expect(content?.dependencyIndexArr).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not parse the dependency if the identifier is not an property of a ThisExpression', () => {
|
||||
const viewParticles = parse('Comp(count)');
|
||||
const viewParticles = parse('Comp(notExist)');
|
||||
const content = (viewParticles[0] as CompParticle).props._$content;
|
||||
expect(content?.dependencyIndexArr).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not parse the dependency if the member expression is in an escaped function', () => {
|
||||
let viewParticles = parse('Comp(escape(this.flag))');
|
||||
let viewParticles = parse('Comp(escape(flag))');
|
||||
let content = (viewParticles[0] as CompParticle).props._$content;
|
||||
expect(content?.dependencyIndexArr).toHaveLength(0);
|
||||
|
||||
viewParticles = parse('Comp($(this.flag))');
|
||||
viewParticles = parse('Comp($(flag))');
|
||||
content = (viewParticles[0] as CompParticle).props._$content;
|
||||
expect(content?.dependencyIndexArr).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not parse the dependency if the member expression is in a manual function', () => {
|
||||
const viewParticles = parse('Comp(manual(() => this.count, []))');
|
||||
const viewParticles = parse('Comp(manual(() => count, []))');
|
||||
const content = (viewParticles[0] as CompParticle).props._$content;
|
||||
expect(content?.dependencyIndexArr).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should parse the dependencies in manual function's second parameter", () => {
|
||||
const viewParticles = parse('Comp(manual(() => {let a = this.count}, [this.flag]))');
|
||||
const viewParticles = parse('Comp(manual(() => {let a = count}, [flag]))');
|
||||
const content = (viewParticles[0] as CompParticle).props._$content;
|
||||
expect(content?.dependencyIndexArr).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not parse the dependency if the member expression is the left side of an assignment expression', () => {
|
||||
const viewParticles = parse('Comp(this.flag = 1)');
|
||||
const viewParticles = parse('Comp(flag = 1)');
|
||||
const content = (viewParticles[0] as CompParticle).props._$content;
|
||||
expect(content?.dependencyIndexArr).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not parse the dependency if the member expression is right side of an assignment expression', () => {
|
||||
const viewParticles = parse('Comp(this.flag = this.flag + 1)');
|
||||
const viewParticles = parse('Comp(flag = flag + 1)');
|
||||
const content = (viewParticles[0] as CompParticle).props._$content;
|
||||
expect(content?.dependencyIndexArr).toHaveLength(0);
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@ import { type types as t } from '@babel/core';
|
|||
describe('MutableTagParticle', () => {
|
||||
// ---- HTML
|
||||
it('should parse an HTMLUnit with dynamic tag as an HTMLParticle', () => {
|
||||
const viewParticles = parse('tag(this.div)()');
|
||||
const viewParticles = parse('tag(div)()');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('html');
|
||||
});
|
||||
|
@ -18,7 +18,7 @@ describe('MutableTagParticle', () => {
|
|||
});
|
||||
|
||||
it('should parse an HTMLUnit with non-static-html children as an HTMLParticle', () => {
|
||||
const viewParticles = parse('div(); { Comp(); tag(this.div)(); }');
|
||||
const viewParticles = parse('div(); { Comp(); tag(div)(); }');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('html');
|
||||
});
|
||||
|
@ -30,7 +30,7 @@ describe('MutableTagParticle', () => {
|
|||
});
|
||||
|
||||
it('should parse an HTMLUnit with dynamic tag with dependencies as an ExpParticle', () => {
|
||||
const viewParticles = parse('tag(this.flag)()');
|
||||
const viewParticles = parse('tag(flag)()');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('exp');
|
||||
const content = (viewParticles[0] as ExpParticle).content;
|
||||
|
@ -48,7 +48,7 @@ describe('MutableTagParticle', () => {
|
|||
});
|
||||
|
||||
it('should parse a CompUnit with dynamic tag with dependencies as an ExpParticle', () => {
|
||||
const viewParticles = parse('comp(CompList[this.flag])()');
|
||||
const viewParticles = parse('comp(CompList[flag])()');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('exp');
|
||||
const content = (viewParticles[0] as ExpParticle).content;
|
||||
|
@ -61,7 +61,7 @@ describe('MutableTagParticle', () => {
|
|||
|
||||
// ---- Snippet
|
||||
it('should parse a SnippetUnit as an HTMLParticle', () => {
|
||||
const viewParticles = parse('this.MySnippet()');
|
||||
const viewParticles = parse('MySnippet()');
|
||||
expect(viewParticles.length).toBe(1);
|
||||
expect(viewParticles[0].type).toBe('snippet');
|
||||
});
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { type types as t } from '@babel/core';
|
||||
import type Babel from '@babel/core';
|
||||
import { PrevMap } from '.';
|
||||
|
||||
export interface DependencyValue<T> {
|
||||
value: T;
|
||||
dynamic: boolean;
|
||||
dependencyIndexArr: number[];
|
||||
dynamic: boolean; // to removed
|
||||
dependencyIndexArr: number[]; // -> bit
|
||||
dependenciesNode: t.ArrayExpression;
|
||||
}
|
||||
|
||||
|
@ -127,9 +128,25 @@ export interface ReactivityParserConfig {
|
|||
babelApi: typeof Babel;
|
||||
availableProperties: string[];
|
||||
availableIdentifiers?: string[];
|
||||
dependencyMap: Record<string, string[]>;
|
||||
dependencyMap: Record<string, string[] | null>;
|
||||
identifierDepMap?: Record<string, string[]>;
|
||||
dependencyParseType?: 'property' | 'identifier';
|
||||
parseTemplate?: boolean;
|
||||
reactivityFuncNames?: string[];
|
||||
}
|
||||
|
||||
export interface DependencyMap {
|
||||
/**
|
||||
* key is the variable name, value is the dependencies
|
||||
* i.e. {
|
||||
* count: ['flag'],
|
||||
* state1: ['count', 'flag'],
|
||||
* state2: ['count', 'flag', 'state1'],
|
||||
* state3: ['count', 'flag', 'state1', 'state2'],
|
||||
* state4: ['count', 'flag', 'state1', 'state2', 'state3'],
|
||||
* }
|
||||
*/
|
||||
[key: string]: string[] | null;
|
||||
|
||||
[PrevMap]?: DependencyMap;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue