refactor(proposal): use bitmap instead of dependency map

This commit is contained in:
Hoikan 2024-04-28 21:02:37 +08:00
parent be4456f225
commit f32da0e9c7
42 changed files with 699 additions and 1967 deletions

View File

@ -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);
```

View File

@ -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"
}
}

View File

@ -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;

View File

@ -1,13 +1,20 @@
import { type types as t } from '@babel/core'; /** @type {null | typeof import('@babel/core').types} */
import type babel from '@babel/core'; let _t = null;
let _t: null | typeof types = null; /** @type {null | typeof import('@babel/core')} */
let babelApi: typeof babel | null = null; let babelApi = null;
export const register = (api: typeof babel) => {
/**
* @param {import('@babel/core')} api
*/
export const register = api => {
babelApi = api; babelApi = api;
_t = api.types; _t = api.types;
}; };
export const getBabelApi = (): typeof babel => { /**
* @returns {typeof import('@babel/core')}
*/
export const getBabelApi = () => {
if (!babelApi) { if (!babelApi) {
throw new Error('Please call register() before using the babel api'); throw new Error('Please call register() before using the babel api');
} }
@ -28,4 +35,4 @@ export const types = new Proxy(
return undefined; return undefined;
}, },
} }
) as typeof t; );

View File

@ -44,7 +44,8 @@
"@types/babel__generator": "^7.6.8", "@types/babel__generator": "^7.6.8",
"@types/babel__parser": "^7.1.1", "@types/babel__parser": "^7.1.1",
"@types/babel__traverse": "^7.6.8", "@types/babel__traverse": "^7.6.8",
"jsx-view-parser": "workspace:*", "@openinula/jsx-view-parser": "workspace:*",
"@openinula/babel-api": "workspace:*",
"minimatch": "^9.0.3", "minimatch": "^9.0.3",
"vitest": "^1.4.0" "vitest": "^1.4.0"
}, },

View File

@ -16,7 +16,7 @@
import { NodePath } from '@babel/core'; import { NodePath } from '@babel/core';
import { LifeCycle, Visitor } from './types'; import { LifeCycle, Visitor } from './types';
import { addLifecycle, addWatch } from './nodeFactory'; 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 { ON_MOUNT, ON_UNMOUNT, WATCH, WILL_MOUNT, WILL_UNMOUNT } from '../constants';
import { extractFnFromMacro, getFnBody } from '../utils'; import { extractFnFromMacro, getFnBody } from '../utils';
@ -51,6 +51,9 @@ export function functionalMacroAnalyze(): Visitor {
if (calleeName === WATCH) { if (calleeName === WATCH) {
const fnNode = extractFnFromMacro(expression, WATCH); const fnNode = extractFnFromMacro(expression, WATCH);
const deps = getWatchDeps(expression); const deps = getWatchDeps(expression);
if (!deps) {
// we auto collect the deps from the function body
}
addWatch(ctx.current, fnNode, deps); addWatch(ctx.current, fnNode, deps);
return; return;
} }

View File

@ -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 { propsAnalyze } from './propsAnalyze';
import { AnalyzeContext, Analyzer, ComponentNode, CondNode, Visitor } from './types'; import { AnalyzeContext, Analyzer, ComponentNode, Visitor } from './types';
import { addLifecycle, createComponentNode } from './nodeFactory'; import { addLifecycle, createComponentNode } from './nodeFactory';
import { propertiesAnalyze } from './propertiesAnalyze'; import { variablesAnalyze } from './variablesAnalyze';
import { functionalMacroAnalyze } from './functionalMacroAnalyze'; import { functionalMacroAnalyze } from './functionalMacroAnalyze';
import { getFnBody } from '../utils'; import { getFnBody } from '../utils';
import { viewAnalyze } from './viewAnalyze'; import { viewAnalyze } from './viewAnalyze';
import { WILL_MOUNT } from '../constants'; import { WILL_MOUNT } from '../constants';
import { types } from '../babelTypes'; import { types as t } from '@openinula/babel-api';
const builtinAnalyzers = [propsAnalyze, propertiesAnalyze, functionalMacroAnalyze, viewAnalyze]; const builtinAnalyzers = [propsAnalyze, variablesAnalyze, functionalMacroAnalyze, viewAnalyze];
export function isCondNode(node: any): node is CondNode {
return node && node.type === 'cond';
}
function mergeVisitor(...visitors: Analyzer[]): Visitor { function mergeVisitor(...visitors: Analyzer[]): Visitor {
return visitors.reduce<Visitor<AnalyzeContext>>((acc, cur) => { return visitors.reduce<Visitor<AnalyzeContext>>((acc, cur) => {
@ -75,7 +71,7 @@ export function analyzeFnComp(
} }
if (context.unhandledNode.length) { 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 * 2. identify the component's props, including children, alias, and default value
* 3. analyze the early return of the component, build into the branch * 3. analyze the early return of the component, build into the branch
* *
* @param types
* @param fnName * @param fnName
* @param path * @param path
* @param customAnalyzers * @param options
*/ */
export function analyze( export function analyze(
fnName: string, fnName: string,

View File

@ -14,9 +14,9 @@
*/ */
import { NodePath, type types as t } from '@babel/core'; 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 { PropType } from '../constants';
import { ViewParticle } from '@openinula/reactivity-parser'; import { ViewParticle, PrevMap } from '@openinula/reactivity-parser';
export function createComponentNode( export function createComponentNode(
name: string, name: string,
@ -25,36 +25,32 @@ export function createComponentNode(
): ComponentNode { ): ComponentNode {
const comp: ComponentNode = { const comp: ComponentNode = {
type: 'comp', type: 'comp',
level: parent ? parent.level + 1 : 0,
name, name,
props: [], children: undefined,
child: undefined,
variables: [], variables: [],
dependencyMap: {}, dependencyMap: parent ? { [PrevMap]: parent.dependencyMap } : {},
reactiveMap: {},
lifecycle: {}, lifecycle: {},
parent, parent,
fnNode, 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() { 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() { 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; return comp;
} }
export function addProperty(comp: ComponentNode, name: string, value: t.Expression | null, isComputed: boolean) { export function addProperty(comp: ComponentNode, name: string, value: t.Expression | null, deps: string[] | null) {
comp.variables.push({ name, value, isComputed, type: 'reactive' }); 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) { 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>) { export function setViewChild(comp: ComponentNode, view: ViewParticle[], usedPropertySet: Set<string>) {
const viewNode: ViewNode = { // TODO: Maybe we should merge
content: view, comp.usedPropertySet = usedPropertySet;
usedPropertySet, comp.children = view;
};
comp.child = viewNode;
} }

View File

@ -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 { AnalyzeContext, Visitor } from './types';
import { addProp } from './nodeFactory'; import { addProp } from './nodeFactory';
import { PropType } from '../constants'; 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 * Analyze the props deconstructing in the function component
@ -29,8 +38,8 @@ export function propsAnalyze(): Visitor {
// --- normal property --- // --- normal property ---
const key = path.node.key; const key = path.node.key;
const value = path.node.value; const value = path.node.value;
if (types.isIdentifier(key) || types.isStringLiteral(key)) { if (t.isIdentifier(key) || t.isStringLiteral(key)) {
const name = types.isIdentifier(key) ? key.name : key.value; const name = t.isIdentifier(key) ? key.name : key.value;
analyzeSingleProp(value, name, path, ctx); analyzeSingleProp(value, name, path, ctx);
return; return;
} }
@ -57,17 +66,17 @@ function analyzeSingleProp(
let alias: string | null = null; let alias: string | null = null;
const nestedProps: string[] | null = []; const nestedProps: string[] | null = [];
let nestedRelationship: t.ObjectPattern | t.ArrayPattern | null = null; let nestedRelationship: t.ObjectPattern | t.ArrayPattern | null = null;
if (types.isIdentifier(value)) { if (t.isIdentifier(value)) {
// 1. handle alias without default value // 1. handle alias without default value
// handle alias without default value // handle alias without default value
if (key !== value.name) { if (key !== value.name) {
alias = value.name; alias = value.name;
} }
} else if (types.isAssignmentPattern(value)) { } else if (t.isAssignmentPattern(value)) {
// 2. handle default value case // 2. handle default value case
const assignedName = value.left; const assignedName = value.left;
defaultVal = value.right; defaultVal = value.right;
if (types.isIdentifier(assignedName)) { if (t.isIdentifier(assignedName)) {
if (assignedName.name !== key) { if (assignedName.name !== key) {
// handle alias in default value case // handle alias in default value case
alias = assignedName.name; alias = assignedName.name;
@ -75,7 +84,7 @@ function analyzeSingleProp(
} else { } else {
throw Error(`Unsupported assignment type in object destructuring: ${assignedName.type}`); 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 // 3. nested destructuring
// we should collect the identifier that can be used in the function body as the prop // 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}]} // e.g. function ({prop1, prop2: [p20X, {p211, p212: p212X}]}

View File

@ -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;
}

View File

@ -15,20 +15,21 @@
import { type NodePath, types as t } from '@babel/core'; import { type NodePath, types as t } from '@babel/core';
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'; import { ViewParticle, PrevMap } from '@openinula/reactivity-parser';
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 Bitmap = number; type Bitmap = number;
export type FunctionalExpression = t.FunctionExpression | t.ArrowFunctionExpression; export type FunctionalExpression = t.FunctionExpression | t.ArrowFunctionExpression;
interface BaseVariable<V> { interface BaseVariable<V> {
name: string; name: string;
value: V; value: V;
} }
export interface ReactiveVariable extends BaseVariable<t.Expression | null> { export interface ReactiveVariable extends BaseVariable<t.Expression | null> {
type: 'reactive'; type: 'reactive';
// indicate the value is a state or computed or watch level: number;
listeners?: string[];
bitmap?: Bitmap; bitmap?: Bitmap;
// need a flag for computed to gen a getter // need a flag for computed to gen a getter
// watch is a static computed // watch is a static computed
@ -38,11 +39,13 @@ export interface ReactiveVariable extends BaseVariable<t.Expression | null> {
export interface MethodVariable extends BaseVariable<FunctionalExpression> { export interface MethodVariable extends BaseVariable<FunctionalExpression> {
type: 'method'; type: 'method';
} }
export interface SubCompVariable extends BaseVariable<ComponentNode> { export interface SubCompVariable extends BaseVariable<ComponentNode> {
type: 'subComp'; type: 'subComp';
} }
export type Variable = ReactiveVariable | MethodVariable | SubCompVariable; export type Variable = ReactiveVariable | MethodVariable | SubCompVariable;
export interface Prop { export interface Prop {
name: string; name: string;
type: PropType; type: PropType;
@ -51,12 +54,17 @@ export interface Prop {
nestedProps: string[] | null; nestedProps: string[] | null;
nestedRelationship: t.ObjectPattern | t.ArrayPattern | null; nestedRelationship: t.ObjectPattern | t.ArrayPattern | null;
} }
export interface ComponentNode { export interface ComponentNode {
type: 'comp'; type: 'comp';
name: string; name: string;
props: Prop[]; level: number;
// The variables defined in the component // The variables defined in the component
variables: Variable[]; variables: Variable[];
/**
* The used properties in the component
*/
usedPropertySet?: Set<string>;
/** /**
* The available props for the component, including the nested props * 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 * The available variables and props owned by the component
*/ */
ownAvailableVariables: string[]; ownAvailableVariables: ReactiveVariable[];
/** /**
* The available variables and props for the component and its parent * The available variables and props for the component and its parent
*/ */
availableVariables: string[]; availableVariables: ReactiveVariable[];
/** children?: (ComponentNode | ViewParticle)[];
* The map to find the dependencies
*/
dependencyMap: {
[key: string]: string[];
};
child?: ComponentNode | ViewNode;
parent?: ComponentNode; parent?: ComponentNode;
/** /**
* The function body of the fn component code * The function body of the fn component code
*/ */
fnNode: NodePath<FunctionalExpression>; fnNode: NodePath<FunctionalExpression>;
/**
* The map to find the state
*/
reactiveMap: Record<string, Bitmap>;
lifecycle: Partial<Record<LifeCycle, t.Statement[]>>; lifecycle: Partial<Record<LifeCycle, t.Statement[]>>;
/**
* The watch fn in the component
*/
watch?: { watch?: {
bit: Bitmap;
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 {
content: ViewParticle[];
usedPropertySet: Set<string>;
}
export interface AnalyzeContext { export interface AnalyzeContext {
level: number; level: number;

View File

@ -13,21 +13,21 @@
* See the Mulan PSL v2 for more details. * 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 { 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 NodePath } from '@babel/core';
import { reactivityFuncNames } from '../const';
import { types } from '../babelTypes';
import { COMPONENT } from '../constants'; import { COMPONENT } from '../constants';
import { analyzeFnComp } from '.'; import { analyzeFnComp } from '.';
import { getDependenciesFromNode } from './reactive/getDependencies';
import { types as t } from '@openinula/babel-api';
/** /**
* collect all properties and methods from the node * collect all properties and methods from the node
* and analyze the dependencies of the properties * and analyze the dependencies of the properties
* @returns * @returns
*/ */
export function propertiesAnalyze(): Visitor { export function variablesAnalyze(): Visitor {
return { return {
VariableDeclaration(path: NodePath<t.VariableDeclaration>, ctx) { VariableDeclaration(path: NodePath<t.VariableDeclaration>, ctx) {
const declarations = path.get('declarations'); const declarations = path.get('declarations');
@ -41,7 +41,7 @@ export function propertiesAnalyze(): Visitor {
// TODO: handle array destructuring // TODO: handle array destructuring
throw new Error('Array destructuring is not supported yet'); throw new Error('Array destructuring is not supported yet');
} else if (id.isIdentifier()) { } else if (id.isIdentifier()) {
// --- properties: the state / computed / plain properties / methods--- // --- properties: the state / computed / plain properties / methods ---
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)) {
@ -50,7 +50,7 @@ export function propertiesAnalyze(): Visitor {
addMethod(ctx.current, id.node.name, init.node); addMethod(ctx.current, id.node.name, init.node);
return; return;
} }
// handle the sub component // handle the subcomponent
// Should like Component(() => {}) // Should like Component(() => {})
if ( if (
init.isCallExpression() && init.isCallExpression() &&
@ -64,14 +64,13 @@ export function propertiesAnalyze(): Visitor {
const subComponent = createComponentNode(id.node.name, fnNode, ctx.current); const subComponent = createComponentNode(id.node.name, fnNode, ctx.current);
analyzeFnComp(fnNode, subComponent, ctx); analyzeFnComp(fnNode, subComponent, ctx);
deps = getDependenciesFromNode(id.node.name, init, ctx);
addSubComponent(ctx.current, subComponent); 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);
} }
}); });
}, },
@ -81,7 +80,7 @@ export function propertiesAnalyze(): Visitor {
throw new Error('Function declaration must have an id'); throw new Error('Function declaration must have an id');
} }
const functionExpression = types.functionExpression( const functionExpression = t.functionExpression(
path.node.id, path.node.id,
path.node.params, path.node.params,
path.node.body, 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)
);
}

View File

@ -14,9 +14,9 @@
*/ */
import { Visitor } from './types'; import { Visitor } from './types';
import { type types as t, type NodePath } from '@babel/core'; import { type NodePath } from '@babel/core';
import { parseView as parseJSX } from 'jsx-view-parser'; import { parseView as parseJSX } from '@openinula/jsx-view-parser';
import { getBabelApi } from '../babelTypes'; import { types as t, getBabelApi } from '@openinula/babel-api';
import { parseReactivity } from '@openinula/reactivity-parser'; import { parseReactivity } from '@openinula/reactivity-parser';
import { reactivityFuncNames } from '../const'; import { reactivityFuncNames } from '../const';
import { setViewChild } from './nodeFactory'; import { setViewChild } from './nodeFactory';

View File

@ -8,7 +8,6 @@ export enum PropType {
REST = 'rest', REST = 'rest',
SINGLE = 'single', SINGLE = 'single',
} }
export const reactivityFuncNames = [ export const reactivityFuncNames = [
// ---- Array // ---- Array
'push', 'push',

View File

@ -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);
}

View File

@ -1,7 +1,7 @@
import syntaxDecorators from '@babel/plugin-syntax-decorators'; import syntaxDecorators from '@babel/plugin-syntax-decorators';
import syntaxJSX from '@babel/plugin-syntax-jsx'; import syntaxJSX from '@babel/plugin-syntax-jsx';
import syntaxTypescript from '@babel/plugin-syntax-typescript'; import syntaxTypescript from '@babel/plugin-syntax-typescript';
import dlight from './plugin'; import inulaNext from './plugin';
import { type DLightOption } from './types'; import { type DLightOption } from './types';
import { type ConfigAPI, type TransformOptions } from '@babel/core'; import { type ConfigAPI, type TransformOptions } from '@babel/core';
import { plugin as fn2Class } from '@openinula/class-transformer'; import { plugin as fn2Class } from '@openinula/class-transformer';
@ -14,7 +14,7 @@ export default function (_: ConfigAPI, options: DLightOption): TransformOptions
[syntaxTypescript.default ?? syntaxTypescript, { isTSX: true }], [syntaxTypescript.default ?? syntaxTypescript, { isTSX: true }],
[syntaxDecorators.default ?? syntaxDecorators, { legacy: true }], [syntaxDecorators.default ?? syntaxDecorators, { legacy: true }],
fn2Class, fn2Class,
[dlight, options], [inulaNext, options],
], ],
}; };
} }

View File

@ -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();
}
},
},
};
}

View File

@ -1,8 +1,12 @@
import type babel from '@babel/core'; import type babel from '@babel/core';
import { type PluginObj } from '@babel/core'; import { type PluginObj } from '@babel/core';
import { PluginProviderClass } from './pluginProvider';
import { type DLightOption } from './types'; import { type DLightOption } from './types';
import { defaultAttributeMap } from './const'; import { defaultAttributeMap, 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 { export default function (api: typeof babel, options: DLightOption): PluginObj {
const { types } = api; const { types } = api;
@ -10,34 +14,51 @@ export default function (api: typeof babel, options: DLightOption): PluginObj {
files = '**/*.{js,ts,jsx,tsx}', files = '**/*.{js,ts,jsx,tsx}',
excludeFiles = '**/{dist,node_modules,lib}/*', excludeFiles = '**/{dist,node_modules,lib}/*',
enableDevTools = false, enableDevTools = false,
htmlTags = defaultHtmlTags => defaultHtmlTags, htmlTags: customHtmlTags = defaultHtmlTags => defaultHtmlTags,
attributeMap = defaultAttributeMap, attributeMap = defaultAttributeMap,
} = options; } = options;
const pluginProvider = new PluginProviderClass( const htmlTags =
api, typeof customHtmlTags === 'function'
types, ? customHtmlTags(defaultHTMLTags)
Array.isArray(files) ? files : [files], : customHtmlTags.includes('*')
Array.isArray(excludeFiles) ? excludeFiles : [excludeFiles], ? [...new Set([...defaultHTMLTags, ...customHtmlTags])].filter(tag => tag !== '*')
enableDevTools, : customHtmlTags;
htmlTags,
attributeMap
);
register(api);
return { return {
visitor: { visitor: {
Program: { Program: {
enter(path, { filename }) { enter(path, { filename }) {
return pluginProvider.programEnterVisitor(path, filename); // return pluginProvider.programEnterVisitor(path, filename);
},
exit(path, { filename }) {
// pluginProvider.programExitVisitor.bind(pluginProvider);
}, },
exit: pluginProvider.programExitVisitor.bind(pluginProvider),
}, },
ClassDeclaration: { CallExpression(path: NodePath<t.CallExpression>) {
enter: pluginProvider.classEnter.bind(pluginProvider), // find the component, like: Component(() => {})
exit: pluginProvider.classExit.bind(pluginProvider), 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

View File

@ -25,7 +25,7 @@ export function earlyReturnPlugin(): Visitor {
const argument = path.get('argument'); const argument = path.get('argument');
if (argument.isJSXElement()) { if (argument.isJSXElement()) {
currentComp.child = createJSXNode(currentComp, argument); currentComp.children = createJSXNode(currentComp, argument);
} }
}, },
IfStatement(ifStmt: NodePath<t.IfStatement>, context: AnalyzeContext) { IfStatement(ifStmt: NodePath<t.IfStatement>, context: AnalyzeContext) {
@ -63,7 +63,7 @@ export function earlyReturnPlugin(): Visitor {
); );
context.skipRest(); context.skipRest();
currentComp.child = createCondNode(currentComp, defaultComponent, branches); currentComp.children = createCondNode(currentComp, defaultComponent, branches);
}, },
}; };
} }

View File

@ -15,12 +15,11 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { genCode, mockAnalyze } from '../mock'; import { genCode, mockAnalyze } from '../mock';
import { functionalMacroAnalyze } from '../../src/analyze/functionalMacroAnalyze'; import { functionalMacroAnalyze } from '../../src/analyzer/functionalMacroAnalyze';
import { types } from '../../src/babelTypes'; import { types as t } from '@openinula/babel-api';
import { type NodePath, type types as t } from '@babel/core';
const analyze = (code: string) => mockAnalyze(code, [functionalMacroAnalyze]); 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', () => { describe('analyze lifeCycle', () => {
it('should collect will mount', () => { it('should collect will mount', () => {

View File

@ -15,11 +15,11 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { genCode, mockAnalyze } from '../mock'; import { genCode, mockAnalyze } from '../mock';
import { propertiesAnalyze } from '../../src/analyze/propertiesAnalyze'; import { variablesAnalyze } from '../../src/analyzer/variablesAnalyze';
import { propsAnalyze } from '../../src/analyze/propsAnalyze'; import { propsAnalyze } from '../../src/analyzer/propsAnalyze';
import { ComponentNode } from '../../src/analyze/types'; 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', () => { describe('analyze properties', () => {
it('should work', () => { it('should work', () => {
@ -41,11 +41,14 @@ describe('analyze properties', () => {
}) })
`); `);
expect(root.variables.length).toBe(2); expect(root.variables.length).toBe(2);
expect(root.variables[0].isComputed).toBe(false); const fooVar = root.variables[0] as ReactiveVariable;
expect(genCode(root.variables[0].value)).toBe('1'); expect(fooVar.isComputed).toBe(false);
expect(root.variables[1].isComputed).toBe(true); expect(genCode(fooVar.value)).toBe('1');
expect(genCode(root.variables[1].value)).toBe('foo');
expect(root.dependencyMap).toEqual({ bar: ['foo'] }); 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', () => { 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.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 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', () => { it('should analyze dependency from props', () => {
@ -74,7 +79,9 @@ describe('analyze properties', () => {
}) })
`); `);
expect(root.variables.length).toBe(1); 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'] }); expect(root.dependencyMap).toEqual({ bar: ['foo'] });
}); });
@ -85,7 +92,8 @@ describe('analyze properties', () => {
}) })
`); `);
expect(root.variables.length).toBe(1); 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'] }); expect(root.dependencyMap).toEqual({ bar: ['foo1', 'first', 'last'] });
}); });
@ -97,8 +105,9 @@ describe('analyze properties', () => {
}) })
`); `);
expect(root.variables.length).toBe(1); expect(root.variables.length).toBe(1);
expect(root.variables[0].isComputed).toBe(false); const barVar = root.variables[0] as ReactiveVariable;
expect(root.dependencyMap).toEqual({}); 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.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(` expect((root.variables[1].value as ComponentNode).dependencyMap).toMatchInlineSnapshot(`
{ {
"bar": [ "bar": [
"foo", "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', () => { 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.map(p => p.name)).toEqual(['foo', 'onClick', 'onHover', 'onInput']);
expect(root.variables[1].type).toBe('method'); expect(root.variables[1].type).toBe('method');
expect(root.variables[2].type).toBe('method'); expect(root.variables[2].type).toBe('method');
expect(root.dependencyMap).toMatchInlineSnapshot('{}'); expect(root.dependencyMap).toMatchInlineSnapshot(`
{
"foo": null,
}
`);
}); });
}); });

View File

@ -16,7 +16,7 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { genCode, mockAnalyze } from '../mock'; import { genCode, mockAnalyze } from '../mock';
import { PropType } from '../../src/constants'; import { PropType } from '../../src/constants';
import { propsAnalyze } from '../../src/analyze/propsAnalyze'; import { propsAnalyze } from '../../src/analyzer/propsAnalyze';
const analyze = (code: string) => mockAnalyze(code, [propsAnalyze]); const analyze = (code: string) => mockAnalyze(code, [propsAnalyze]);

View File

@ -1,19 +1,82 @@
import { propertiesAnalyze } from '../../src/analyze/propertiesAnalyze'; import { variablesAnalyze } from '../../src/analyzer/variablesAnalyze';
import { propsAnalyze } from '../../src/analyze/propsAnalyze'; import { propsAnalyze } from '../../src/analyzer/propsAnalyze';
import { viewAnalyze } from '../../src/analyze/viewAnalyze'; import { ComponentNode } from '../../src/analyzer/types';
import { viewAnalyze } from '../../src/analyzer/viewAnalyze';
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, [propsAnalyze, propertiesAnalyze, viewAnalyze]); const analyze = (code: string) => mockAnalyze(code, [propsAnalyze, variablesAnalyze, viewAnalyze]);
describe('viewAnalyze', () => {
describe('watchAnalyze', () => { it('should analyze view', () => {
it('should analyze watch expressions', () => {
const root = analyze(/*js*/ ` const root = analyze(/*js*/ `
Comp(({name}) => { Component(({name ,className}) => {
let count = 11 let count = name; // 1
return <div className={name}>{count}</div> 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]"');
}); });
}); });

View File

@ -1,4 +1,4 @@
import { functionalMacroAnalyze } from '../../src/analyze/functionalMacroAnalyze'; import { functionalMacroAnalyze } from '../../src/analyzer/functionalMacroAnalyze';
import { genCode, mockAnalyze } from '../mock'; import { genCode, mockAnalyze } from '../mock';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';

View File

@ -14,7 +14,7 @@
*/ */
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { transform } from './presets'; import { transform } from '../presets';
describe('condition', () => { describe('condition', () => {
it('should transform jsx', () => { it('should transform jsx', () => {

View File

@ -13,13 +13,13 @@
* See the Mulan PSL v2 for more details. * 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 { type PluginObj, transform as transformWithBabel } from '@babel/core';
import syntaxJSX from '@babel/plugin-syntax-jsx'; import syntaxJSX from '@babel/plugin-syntax-jsx';
import { analyze } from '../src/analyze'; import { analyze } from '../src/analyzer';
import generate from '@babel/generator'; import generate from '@babel/generator';
import * as t from '@babel/types'; import * as t from '@babel/types';
import { register } from '../src/babelTypes'; import { register } from '@openinula/babel-api';
import { defaultHTMLTags } from '../src/const'; import { defaultHTMLTags } from '../src/const';
export function mockAnalyze(code: string, analyzers?: Analyzer[]): ComponentNode { export function mockAnalyze(code: string, analyzers?: Analyzer[]): ComponentNode {
@ -63,26 +63,3 @@ export function genCode(ast: t.Node | null) {
} }
return generate(ast).code; 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';
}

View File

@ -14,7 +14,7 @@
*/ */
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { isCondNode } from '../../src/analyze'; import { isCondNode } from '../../src/analyzer';
import { mockAnalyze } from '../mock'; import { mockAnalyze } from '../mock';
describe('analyze early return', () => { describe('analyze early return', () => {
@ -30,7 +30,7 @@ describe('analyze early return', () => {
</div>; </div>;
} }
`); `);
const branchNode = root?.child; const branchNode = root?.children;
if (!isCondNode(branchNode)) { if (!isCondNode(branchNode)) {
throw new Error('Should be branch node'); throw new Error('Should be branch node');
} }
@ -49,7 +49,7 @@ describe('analyze early return', () => {
return <div></div>; return <div></div>;
} }
`); `);
const branchNode = root?.child; const branchNode = root?.children;
if (!isCondNode(branchNode)) { if (!isCondNode(branchNode)) {
throw new Error('Should be branch node'); throw new Error('Should be branch node');
} }
@ -73,7 +73,7 @@ describe('analyze early return', () => {
return <div></div>; return <div></div>;
} }
`); `);
const branchNode = root?.child; const branchNode = root?.children;
if (!isCondNode(branchNode)) { if (!isCondNode(branchNode)) {
throw new Error('Should be branch node'); throw new Error('Should be branch node');
} }

View File

@ -44,7 +44,7 @@
"@types/babel__generator": "^7.6.8", "@types/babel__generator": "^7.6.8",
"@types/babel__parser": "^7.1.1", "@types/babel__parser": "^7.1.1",
"@types/babel__traverse": "^7.6.8", "@types/babel__traverse": "^7.6.8",
"jsx-view-parser": "workspace:*", "@openinula/jsx-view-parser": "workspace:*",
"minimatch": "^9.0.3", "minimatch": "^9.0.3",
"vitest": "^1.4.0" "vitest": "^1.4.0"
}, },

View File

@ -3,7 +3,7 @@ import { type PluginObj } from '@babel/core';
import { PluginProviderClass } from './pluginProvider'; import { PluginProviderClass } from './pluginProvider';
import { type DLightOption } from './types'; import { type DLightOption } from './types';
import { defaultAttributeMap } from './const'; import { defaultAttributeMap } from './const';
import { analyze } from './analyze'; import { analyze } from './analyzer';
export default function (api: typeof babel, options: DLightOption): PluginObj { export default function (api: typeof babel, options: DLightOption): PluginObj {
const { types } = api; const { types } = api;

View File

@ -3,7 +3,7 @@ import { type types as t, type NodePath } from '@babel/core';
import { type PropertyContainer, type HTMLTags, type SnippetPropSubDepMap } from './types'; import { type PropertyContainer, type HTMLTags, type SnippetPropSubDepMap } from './types';
import { minimatch } from 'minimatch'; import { minimatch } from 'minimatch';
import { parseView, ViewUnit } from '@openinula/view-parser'; 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 { parseReactivity } from '@openinula/reactivity-parser';
import { generateSnippet, generateView } from '@openinula/view-generator'; import { generateSnippet, generateView } from '@openinula/view-generator';
import { import {

View File

@ -14,7 +14,7 @@
*/ */
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { isCondNode } from '../src/analyze'; import { isCondNode } from '../src/analyzer';
import { mockAnalyze } from './mock'; import { mockAnalyze } from './mock';
describe('analyze early return', () => { describe('analyze early return', () => {

View File

@ -16,7 +16,7 @@
import { ComponentNode, InulaNode } from '../src/analyze/types'; import { ComponentNode, InulaNode } from '../src/analyze/types';
import babel, { type PluginObj, transform as transformWithBabel } from '@babel/core'; import babel, { type PluginObj, transform as transformWithBabel } from '@babel/core';
import syntaxJSX from '@babel/plugin-syntax-jsx'; import syntaxJSX from '@babel/plugin-syntax-jsx';
import { analyze } from '../src/analyze'; import { analyze } from '../src/analyzer';
import generate from '@babel/generator'; import generate from '@babel/generator';
import * as t from '@babel/types'; import * as t from '@babel/types';

View File

@ -1,5 +1,5 @@
{ {
"name": "jsx-view-parser", "name": "@openinula/jsx-view-parser",
"version": "0.0.1", "version": "0.0.1",
"description": "Inula jsx parser", "description": "Inula jsx parser",
"author": { "author": {

View File

@ -30,6 +30,7 @@
}, },
"dependencies": { "dependencies": {
"@openinula/error-handler": "workspace:*", "@openinula/error-handler": "workspace:*",
"@openinula/jsx-view-parser": "workspace:*",
"@openinula/view-parser": "workspace:*" "@openinula/view-parser": "workspace:*"
}, },
"tsup": { "tsup": {

View File

@ -1,4 +1,4 @@
import { type ViewUnit } from '@openinula/view-parser'; import { type ViewUnit } from '@openinula/jsx-view-parser';
import { ReactivityParser } from './parser'; import { ReactivityParser } from './parser';
import { type ViewParticle, type ReactivityParserConfig } from './types'; 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 * @brief Parse view units to get used properties and view particles with reactivity
* @param viewUnits * @param viewUnits
* @param config * @param config
* @param options
* @returns [viewParticles, usedProperties] * @returns [viewParticles, usedProperties]
*/ */
export function parseReactivity(viewUnits: ViewUnit[], config: ReactivityParserConfig): [ViewParticle[], Set<string>] { export function parseReactivity(viewUnits: ViewUnit[], config: ReactivityParserConfig): [ViewParticle[], Set<string>] {
@ -21,5 +20,9 @@ export function parseReactivity(viewUnits: ViewUnit[], config: ReactivityParserC
}); });
return [dlParticles, usedProperties]; return [dlParticles, usedProperties];
} }
/**
* The key to get the previous map in DependencyMap Chain
*/
export const PrevMap = Symbol('prevMap');
export type * from './types'; export type * from './types';

View File

@ -12,9 +12,7 @@ import {
type ForParticle, type ForParticle,
type IfParticle, type IfParticle,
type EnvParticle, type EnvParticle,
type SnippetParticle, DependencyMap,
SwitchParticle,
TryParticle,
} from './types'; } from './types';
import { type NodePath, type types as t, type traverse } from '@babel/core'; import { type NodePath, type types as t, type traverse } from '@babel/core';
import { import {
@ -22,16 +20,14 @@ import {
type HTMLUnit, type HTMLUnit,
type ViewUnit, type ViewUnit,
type CompUnit, type CompUnit,
type ViewProp, type UnitProp,
type ForUnit, type ForUnit,
type IfUnit, type IfUnit,
type EnvUnit, type EnvUnit,
type ExpUnit, type ExpUnit,
type SnippetUnit, } from '@openinula/jsx-view-parser';
SwitchUnit,
TryUnit,
} from '@openinula/view-parser';
import { DLError } from './error'; import { DLError } from './error';
import { PrevMap } from '.';
export class ReactivityParser { export class ReactivityParser {
private readonly config: ReactivityParserConfig; private readonly config: ReactivityParserConfig;
@ -40,7 +36,7 @@ export class ReactivityParser {
private readonly traverse: typeof traverse; private readonly traverse: typeof traverse;
private readonly availableProperties: string[]; private readonly availableProperties: string[];
private readonly availableIdentifiers?: string[]; private readonly availableIdentifiers?: string[];
private readonly dependencyMap: Record<string, string[]>; private readonly dependencyMap: DependencyMap;
private readonly identifierDepMap: Record<string, string[]>; private readonly identifierDepMap: Record<string, string[]>;
private readonly dependencyParseType; private readonly dependencyParseType;
private readonly reactivityFuncNames; private readonly reactivityFuncNames;
@ -99,12 +95,9 @@ export class ReactivityParser {
if (viewUnit.type === 'html') return this.parseHTML(viewUnit); if (viewUnit.type === 'html') return this.parseHTML(viewUnit);
if (viewUnit.type === 'comp') return this.parseComp(viewUnit); if (viewUnit.type === 'comp') return this.parseComp(viewUnit);
if (viewUnit.type === 'for') return this.parseFor(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 === 'if') return this.parseIf(viewUnit);
if (viewUnit.type === 'env') return this.parseEnv(viewUnit); if (viewUnit.type === 'env') return this.parseEnv(viewUnit);
if (viewUnit.type === 'exp') return this.parseExp(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(); 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 ---- // ---- @Env ----
/** /**
* @brief Parse an EnvUnit into an EnvParticle with dependencies * @brief Parse an EnvUnit into an EnvParticle with dependencies
@ -492,38 +446,13 @@ export class ReactivityParser {
return expParticle; 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 ---- // ---- Dependencies ----
/** /**
* @brief Generate a dependency prop with dependencies * @brief Generate a dependency prop with dependencies
* @param prop * @param prop
* @returns DependencyProp * @returns DependencyProp
*/ */
private generateDependencyProp(prop: ViewProp): DependencyProp { private generateDependencyProp(prop: UnitProp): DependencyProp {
const dependencyProp: DependencyProp = { const dependencyProp: DependencyProp = {
value: prop.value, value: prop.value,
...this.getDependencies(prop.value), ...this.getDependencies(prop.value),
@ -559,13 +488,12 @@ export class ReactivityParser {
// ---- Both id and prop deps need to be calculated because // ---- Both id and prop deps need to be calculated because
// id is for snippet update, prop is normal update // id is for snippet update, prop is normal update
// in a snippet, the depsNode should be both id and prop // in a snippet, the depsNode should be both id and prop
const [directIdentifierDeps, identifierDepNodes] = this.getIdentifierDependencies(node);
const [directPropertyDeps, propertyDepNodes] = this.getPropertyDependencies(node); const [directPropertyDeps, propertyDepNodes] = this.getPropertyDependencies(node);
const directDependencies = this.dependencyParseType === 'identifier' ? directIdentifierDeps : directPropertyDeps; const directDependencies = directPropertyDeps;
const identifierMapDependencies = this.getIdentifierMapDependencies(node); const identifierMapDependencies = this.getIdentifierMapDependencies(node);
const deps = [...new Set([...directDependencies, ...identifierMapDependencies])]; const deps = [...new Set([...directDependencies, ...identifierMapDependencies])];
const depNodes = [...identifierDepNodes, ...propertyDepNodes] as t.Expression[]; const depNodes = [...propertyDepNodes] as t.Expression[];
return { return {
dynamic: depNodes.length > 0 || deps.length > 0, dynamic: depNodes.length > 0 || deps.length > 0,
@ -625,7 +553,7 @@ export class ReactivityParser {
}); });
deps.forEach(this.usedProperties.add.bind(this.usedProperties)); 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); const wrappedNode = this.valueWrapper(node);
this.traverse(wrappedNode, { this.traverse(wrappedNode, {
MemberExpression: innerPath => { Identifier: innerPath => {
if (!this.t.isIdentifier(innerPath.node.property) || !this.t.isThisExpression(innerPath.node.object)) return; const propertyKey = innerPath.node.name;
const propertyKey = innerPath.node.property.name;
if (this.isAssignmentExpressionLeft(innerPath) || this.isAssignmentFunction(innerPath)) { if (this.isAssignmentExpressionLeft(innerPath) || this.isAssignmentFunction(innerPath)) {
assignDeps.add(propertyKey); assignDeps.add(propertyKey);
} else if ( } else if (
@ -657,13 +585,12 @@ export class ReactivityParser {
!this.isMemberInManualFunction(innerPath) !this.isMemberInManualFunction(innerPath)
) { ) {
deps.add(propertyKey); deps.add(propertyKey);
this.dependencyMap[propertyKey]?.forEach(deps.add.bind(deps));
if (!depNodes[propertyKey]) depNodes[propertyKey] = []; if (!depNodes[propertyKey]) depNodes[propertyKey] = [];
depNodes[propertyKey].push(this.geneDependencyNode(innerPath)); depNodes[propertyKey].push(this.geneDependencyNode(innerPath));
} }
}, },
}); });
const dependencyIdxArr = deduplicate([...deps].map(this.calDependencyIndexArr).flat());
assignDeps.forEach(dep => { assignDeps.forEach(dep => {
deps.delete(dep); deps.delete(dep);
delete depNodes[dep]; delete depNodes[dep];
@ -676,7 +603,36 @@ export class ReactivityParser {
}); });
deps.forEach(this.usedProperties.add.bind(this.usedProperties)); 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)); 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 ---- // ---- Utils ----
@ -781,7 +737,7 @@ export class ReactivityParser {
* @param prop * @param prop
* @returns is a static prop * @returns is a static prop
*/ */
private isStaticProp(prop: ViewProp): boolean { private isStaticProp(prop: UnitProp): boolean {
const { value, viewPropMap } = prop; const { value, viewPropMap } = prop;
return ( return (
(!viewPropMap || Object.keys(viewPropMap).length === 0) && (!viewPropMap || Object.keys(viewPropMap).length === 0) &&
@ -950,10 +906,9 @@ export class ReactivityParser {
} }
if (!parentPath) return false; if (!parentPath) return false;
return ( return (
this.t.isCallExpression(parentPath.node) && parentPath.isCallExpression() &&
this.t.isMemberExpression(parentPath.node.callee) && parentPath.get('callee').isIdentifier() &&
this.t.isIdentifier(parentPath.node.callee.property) && this.reactivityFuncNames.includes((parentPath.get('callee').node as t.Identifier).name)
this.reactivityFuncNames.includes(parentPath.node.callee.property.name)
); );
} }
@ -1021,3 +976,7 @@ export class ReactivityParser {
return Math.random().toString(36).slice(2); return Math.random().toString(36).slice(2);
} }
} }
function deduplicate<T>(arr: T[]): T[] {
return [...new Set(arr)];
}

View File

@ -4,13 +4,13 @@ import { type CompParticle } from '../types';
describe('Dependency', () => { describe('Dependency', () => {
it('should parse the correct 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; const content = (viewParticles[0] as CompParticle).props._$content;
expect(content?.dependencyIndexArr).toContain(0); expect(content?.dependencyIndexArr).toContain(0);
}); });
it('should parse the correct dependency when interfacing the dependency chain', () => { 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 content = (viewParticles[0] as CompParticle).props._$content;
const dependency = content?.dependencyIndexArr; const dependency = content?.dependencyIndexArr;
// ---- doubleCount depends on count, count depends on flag // ---- 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', () => { it('should not parse the dependency if the property is not in the availableProperties', () => {
const viewParticles = parse('Comp(this.notExist)'); 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 identifier is not an property of a ThisExpression', () => {
const viewParticles = parse('Comp(count)');
const content = (viewParticles[0] as CompParticle).props._$content; const content = (viewParticles[0] as CompParticle).props._$content;
expect(content?.dependencyIndexArr).toHaveLength(0); expect(content?.dependencyIndexArr).toHaveLength(0);
}); });
it('should not parse the dependency if the member expression is in an escaped function', () => { 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; let content = (viewParticles[0] as CompParticle).props._$content;
expect(content?.dependencyIndexArr).toHaveLength(0); expect(content?.dependencyIndexArr).toHaveLength(0);
viewParticles = parse('Comp($(this.flag))'); viewParticles = parse('Comp($(flag))');
content = (viewParticles[0] as CompParticle).props._$content; content = (viewParticles[0] as CompParticle).props._$content;
expect(content?.dependencyIndexArr).toHaveLength(0); expect(content?.dependencyIndexArr).toHaveLength(0);
}); });
it('should not parse the dependency if the member expression is in a manual function', () => { 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; const content = (viewParticles[0] as CompParticle).props._$content;
expect(content?.dependencyIndexArr).toHaveLength(0); expect(content?.dependencyIndexArr).toHaveLength(0);
}); });
it("should parse the dependencies in manual function's second parameter", () => { 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; const content = (viewParticles[0] as CompParticle).props._$content;
expect(content?.dependencyIndexArr).toHaveLength(1); expect(content?.dependencyIndexArr).toHaveLength(1);
}); });
it('should not parse the dependency if the member expression is the left side of an assignment expression', () => { 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; const content = (viewParticles[0] as CompParticle).props._$content;
expect(content?.dependencyIndexArr).toHaveLength(0); expect(content?.dependencyIndexArr).toHaveLength(0);
}); });
it('should not parse the dependency if the member expression is right side of an assignment expression', () => { 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; const content = (viewParticles[0] as CompParticle).props._$content;
expect(content?.dependencyIndexArr).toHaveLength(0); expect(content?.dependencyIndexArr).toHaveLength(0);
}); });

View File

@ -6,7 +6,7 @@ import { type types as t } from '@babel/core';
describe('MutableTagParticle', () => { describe('MutableTagParticle', () => {
// ---- HTML // ---- HTML
it('should parse an HTMLUnit with dynamic tag as an HTMLParticle', () => { 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.length).toBe(1);
expect(viewParticles[0].type).toBe('html'); 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', () => { 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.length).toBe(1);
expect(viewParticles[0].type).toBe('html'); 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', () => { 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.length).toBe(1);
expect(viewParticles[0].type).toBe('exp'); expect(viewParticles[0].type).toBe('exp');
const content = (viewParticles[0] as ExpParticle).content; 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', () => { 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.length).toBe(1);
expect(viewParticles[0].type).toBe('exp'); expect(viewParticles[0].type).toBe('exp');
const content = (viewParticles[0] as ExpParticle).content; const content = (viewParticles[0] as ExpParticle).content;
@ -61,7 +61,7 @@ describe('MutableTagParticle', () => {
// ---- Snippet // ---- Snippet
it('should parse a SnippetUnit as an HTMLParticle', () => { it('should parse a SnippetUnit as an HTMLParticle', () => {
const viewParticles = parse('this.MySnippet()'); const viewParticles = parse('MySnippet()');
expect(viewParticles.length).toBe(1); expect(viewParticles.length).toBe(1);
expect(viewParticles[0].type).toBe('snippet'); expect(viewParticles[0].type).toBe('snippet');
}); });

View File

@ -1,10 +1,11 @@
import { type types as t } from '@babel/core'; import { type types as t } from '@babel/core';
import type Babel from '@babel/core'; import type Babel from '@babel/core';
import { PrevMap } from '.';
export interface DependencyValue<T> { export interface DependencyValue<T> {
value: T; value: T;
dynamic: boolean; dynamic: boolean; // to removed
dependencyIndexArr: number[]; dependencyIndexArr: number[]; // -> bit
dependenciesNode: t.ArrayExpression; dependenciesNode: t.ArrayExpression;
} }
@ -127,9 +128,25 @@ export interface ReactivityParserConfig {
babelApi: typeof Babel; babelApi: typeof Babel;
availableProperties: string[]; availableProperties: string[];
availableIdentifiers?: string[]; availableIdentifiers?: string[];
dependencyMap: Record<string, string[]>; dependencyMap: Record<string, string[] | null>;
identifierDepMap?: Record<string, string[]>; identifierDepMap?: Record<string, string[]>;
dependencyParseType?: 'property' | 'identifier'; dependencyParseType?: 'property' | 'identifier';
parseTemplate?: boolean; parseTemplate?: boolean;
reactivityFuncNames?: string[]; 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;
}