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';
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;
);

View File

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

View File

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

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 { 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,

View File

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

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 { 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}]}

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

View File

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

View File

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

View File

@ -8,7 +8,6 @@ export enum PropType {
REST = 'rest',
SINGLE = 'single',
}
export const reactivityFuncNames = [
// ---- Array
'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 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],
],
};
}

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

View File

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

View File

@ -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', () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 { 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 {

View File

@ -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', () => {

View File

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

View File

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

View File

@ -30,6 +30,7 @@
},
"dependencies": {
"@openinula/error-handler": "workspace:*",
"@openinula/jsx-view-parser": "workspace:*",
"@openinula/view-parser": "workspace:*"
},
"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 { 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';

View File

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

View File

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

View File

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

View File

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