refactor(proposal): use bitmap instead of dependency map
This commit is contained in:
parent
be4456f225
commit
f32da0e9c7
|
@ -0,0 +1,24 @@
|
||||||
|
# @openinlua/babel-api
|
||||||
|
A package that encapsulates the babel API for use in the transpiler.
|
||||||
|
|
||||||
|
To implement the dependency injection pattern, the package exports a function that registers the babel API in the
|
||||||
|
transpiler.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { registerBabelAPI } from '@openinlua/babel-api';
|
||||||
|
|
||||||
|
function plugin(api: typeof babel) {
|
||||||
|
registerBabelAPI(api);
|
||||||
|
|
||||||
|
// Your babel plugin code here.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And then you can import to use it.
|
||||||
|
> types can use as a `type` or as a `namespace` for the babel API.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { types as t } from '@openinlua/babel-api';
|
||||||
|
|
||||||
|
t.isIdentifier(node as t.Node);
|
||||||
|
```
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"name": "@openinula/babel-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"type": "module",
|
||||||
|
"main": "src/index.mjs",
|
||||||
|
"typings": "src/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core": "^7.23.3",
|
||||||
|
"@babel/types": "^7.24.0",
|
||||||
|
"@types/babel__core": "^7.20.5"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import type babel from '@babel/core';
|
||||||
|
|
||||||
|
// use .d.ts to satisfy the type check
|
||||||
|
export * as types from '@babel/types';
|
||||||
|
|
||||||
|
export declare function register(api: typeof babel): void;
|
||||||
|
export declare function getBabelApi(): typeof babel;
|
|
@ -1,13 +1,20 @@
|
||||||
import { type types as t } from '@babel/core';
|
/** @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;
|
);
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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,
|
|
@ -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;
|
|
||||||
}
|
}
|
|
@ -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}]}
|
|
@ -0,0 +1,124 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { NodePath } from '@babel/core';
|
||||||
|
import { AnalyzeContext, DependencyMap } from '../types';
|
||||||
|
import { types as t } from '@openinula/babel-api';
|
||||||
|
import { reactivityFuncNames } from '../../const';
|
||||||
|
import { PrevMap } from '@openinula/reactivity-parser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get all valid dependencies of a babel path
|
||||||
|
* @param propertyKey
|
||||||
|
* @param path
|
||||||
|
* @param ctx
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function getDependenciesFromNode(
|
||||||
|
propertyKey: string,
|
||||||
|
path: NodePath<t.Expression | t.ClassDeclaration>,
|
||||||
|
{ current }: AnalyzeContext
|
||||||
|
) {
|
||||||
|
// ---- Deps: console.log(count)
|
||||||
|
const deps = new Set<string>();
|
||||||
|
// ---- Assign deps: count = 1 or count++
|
||||||
|
const assignDeps = new Set<string>();
|
||||||
|
const depNodes: Record<string, t.Expression[]> = {};
|
||||||
|
|
||||||
|
const visitor = (innerPath: NodePath<t.Identifier>) => {
|
||||||
|
const propertyKey = innerPath.node.name;
|
||||||
|
if (isAssignmentExpressionLeft(innerPath) || isAssignmentFunction(innerPath)) {
|
||||||
|
assignDeps.add(propertyKey);
|
||||||
|
} else if (current.availableVariables.includes(propertyKey)) {
|
||||||
|
deps.add(propertyKey);
|
||||||
|
findDependency(current.dependencyMap, propertyKey)?.forEach(deps.add.bind(deps));
|
||||||
|
if (!depNodes[propertyKey]) depNodes[propertyKey] = [];
|
||||||
|
depNodes[propertyKey].push(t.cloneNode(innerPath.node));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (path.isIdentifier()) {
|
||||||
|
visitor(path);
|
||||||
|
}
|
||||||
|
path.traverse({
|
||||||
|
Identifier: visitor,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- Eliminate deps that are assigned in the same method
|
||||||
|
// e.g. { console.log(count); count = 1 }
|
||||||
|
// this will cause infinite loop
|
||||||
|
// so we eliminate "count" from deps
|
||||||
|
assignDeps.forEach(dep => {
|
||||||
|
deps.delete(dep);
|
||||||
|
});
|
||||||
|
|
||||||
|
const depArr = [...deps];
|
||||||
|
if (deps.size > 0) {
|
||||||
|
current.dependencyMap[propertyKey] = depArr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return depArr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if it's the left side of an assignment expression, e.g. count = 1
|
||||||
|
* @param innerPath
|
||||||
|
* @returns assignment expression
|
||||||
|
*/
|
||||||
|
function isAssignmentExpressionLeft(innerPath: NodePath): NodePath | null {
|
||||||
|
let parentPath = innerPath.parentPath;
|
||||||
|
while (parentPath && !parentPath.isStatement()) {
|
||||||
|
if (parentPath.isAssignmentExpression()) {
|
||||||
|
if (parentPath.node.left === innerPath.node) return parentPath;
|
||||||
|
const leftPath = parentPath.get('left') as NodePath;
|
||||||
|
if (innerPath.isDescendant(leftPath)) return parentPath;
|
||||||
|
} else if (parentPath.isUpdateExpression()) {
|
||||||
|
return parentPath;
|
||||||
|
}
|
||||||
|
parentPath = parentPath.parentPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if it's a reactivity function, e.g. arr.push
|
||||||
|
* @param innerPath
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function isAssignmentFunction(innerPath: NodePath): boolean {
|
||||||
|
let parentPath = innerPath.parentPath;
|
||||||
|
|
||||||
|
while (parentPath && parentPath.isMemberExpression()) {
|
||||||
|
parentPath = parentPath.parentPath;
|
||||||
|
}
|
||||||
|
if (!parentPath) return false;
|
||||||
|
return (
|
||||||
|
parentPath.isCallExpression() &&
|
||||||
|
parentPath.get('callee').isIdentifier() &&
|
||||||
|
reactivityFuncNames.includes((parentPath.get('callee').node as t.Identifier).name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findDependency(dependencyMap: DependencyMap, propertyKey: string) {
|
||||||
|
let currentMap: DependencyMap | undefined = dependencyMap;
|
||||||
|
do {
|
||||||
|
if (currentMap[propertyKey] !== undefined) {
|
||||||
|
return currentMap[propertyKey];
|
||||||
|
}
|
||||||
|
// trace back to the previous map
|
||||||
|
currentMap = currentMap[PrevMap];
|
||||||
|
} while (currentMap);
|
||||||
|
return null;
|
||||||
|
}
|
|
@ -15,20 +15,21 @@
|
||||||
|
|
||||||
import { type NodePath, types as t } from '@babel/core';
|
import { 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;
|
|
@ -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)
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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';
|
|
@ -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',
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { ViewParticle } from '@openinula/reactivity-parser';
|
||||||
|
import { ComponentNode, Prop, Variable } from '../analyzer/types';
|
||||||
|
import { type types as t, type NodePath } from '@babel/core';
|
||||||
|
import { types } from '../babelTypes';
|
||||||
|
|
||||||
|
type Visitor = {
|
||||||
|
[Type in (ViewParticle | ComponentNode)['type']]: (
|
||||||
|
node: Extract<ViewParticle | ComponentNode, { type: Type }>,
|
||||||
|
ctx: any
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface GeneratorContext {
|
||||||
|
classBodyNode: t.ClassBody;
|
||||||
|
currentComp: ComponentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateFnComp(compNode: ComponentNode) {
|
||||||
|
const context = {
|
||||||
|
classBodyNode: types.classBody([]),
|
||||||
|
currentComp: compNode,
|
||||||
|
};
|
||||||
|
compNode.props.forEach(prop => {
|
||||||
|
resolvePropDecorator(context, prop, 'Prop');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reverseDependencyMap(dependencyMap: Record<string, Set<string>>) {
|
||||||
|
const reversedMap: Record<string, Set<string>> = {};
|
||||||
|
Object.entries(dependencyMap).forEach(([key, deps]) => {
|
||||||
|
deps.forEach(dep => {
|
||||||
|
if (!reversedMap[dep]) reversedMap[dep] = new Set();
|
||||||
|
reversedMap[dep].add(key);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return reversedMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Decorator resolver: Prop/Env
|
||||||
|
* Add:
|
||||||
|
* $p/e$${key}
|
||||||
|
* @param ctx
|
||||||
|
* @param prop
|
||||||
|
* @param decoratorName
|
||||||
|
*/
|
||||||
|
function resolvePropDecorator(ctx: GeneratorContext, prop: Prop, decoratorName: 'Prop' | 'Env' = 'Prop') {
|
||||||
|
if (!ctx.classBodyNode) return;
|
||||||
|
const key = prop.name;
|
||||||
|
ctx.classBodyNode.body.push(types.classProperty(types.identifier(key), prop.default));
|
||||||
|
|
||||||
|
// Add tag to let the runtime know this property is a prop or env
|
||||||
|
const tag = decoratorName.toLowerCase() === 'prop' ? 'p' : 'e';
|
||||||
|
const derivedStatusKey = types.classProperty(types.identifier(`$${tag}$${key}`));
|
||||||
|
ctx.classBodyNode.body.push(derivedStatusKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Decorator resolver: State
|
||||||
|
* Add:
|
||||||
|
* $$${key} = ${depIdx}
|
||||||
|
* $sub$${key} = [${reversedDeps}]
|
||||||
|
* @param ctx
|
||||||
|
* @param varable
|
||||||
|
* @param idx
|
||||||
|
* @param reverseDeps
|
||||||
|
*/
|
||||||
|
function resolveStateDecorator(
|
||||||
|
ctx: GeneratorContext,
|
||||||
|
varable: Variable,
|
||||||
|
idx: number,
|
||||||
|
reverseDeps: Set<string> | undefined
|
||||||
|
) {
|
||||||
|
if (!ctx.classBodyNode) return;
|
||||||
|
if (!types.isIdentifier(node.key)) return;
|
||||||
|
const key = node.key.name;
|
||||||
|
const idx = ctx.currentComp.variables.indexOf(node);
|
||||||
|
|
||||||
|
const idxNode = !ctx.dLightModel
|
||||||
|
? [types.classProperty(types.identifier(`$$${key}`), types.numericLiteral(1 << idx))]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const depsNode = reverseDeps
|
||||||
|
? [
|
||||||
|
types.classProperty(
|
||||||
|
types.identifier(`$s$${key}`),
|
||||||
|
types.arrayExpression([...reverseDeps].map(d => types.stringLiteral(d)))
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
ctx.classBodyNode.body.splice(propertyIdx + 1, 0, ...idxNode, ...depsNode);
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import syntaxDecorators from '@babel/plugin-syntax-decorators';
|
import 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],
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
import type babel from '@babel/core';
|
|
||||||
import { type PluginObj } from '@babel/core';
|
|
||||||
import { type DLightOption } from './types';
|
|
||||||
import { defaultAttributeMap, defaultHTMLTags } from './const';
|
|
||||||
import { analyze } from './analyze';
|
|
||||||
import { NodePath, type types as t } from '@babel/core';
|
|
||||||
import { COMPONENT } from './constants';
|
|
||||||
import { extractFnFromMacro } from './utils';
|
|
||||||
import { register } from './babelTypes';
|
|
||||||
|
|
||||||
export default function (api: typeof babel, options: DLightOption): PluginObj {
|
|
||||||
const { types } = api;
|
|
||||||
const {
|
|
||||||
files = '**/*.{js,ts,jsx,tsx}',
|
|
||||||
excludeFiles = '**/{dist,node_modules,lib}/*',
|
|
||||||
enableDevTools = false,
|
|
||||||
customHtmlTags = defaultHtmlTags => defaultHtmlTags,
|
|
||||||
attributeMap = defaultAttributeMap,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const htmlTags =
|
|
||||||
typeof customHtmlTags === 'function'
|
|
||||||
? customHtmlTags(defaultHTMLTags)
|
|
||||||
: customHtmlTags.includes('*')
|
|
||||||
? [...new Set([...defaultHTMLTags, ...customHtmlTags])].filter(tag => tag !== '*')
|
|
||||||
: customHtmlTags;
|
|
||||||
|
|
||||||
register(api);
|
|
||||||
return {
|
|
||||||
visitor: {
|
|
||||||
Program: {
|
|
||||||
enter(path, { filename }) {
|
|
||||||
// return pluginProvider.programEnterVisitor(path, filename);
|
|
||||||
},
|
|
||||||
exit(path, { filename }) {
|
|
||||||
// pluginProvider.programExitVisitor.bind(pluginProvider);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
CallExpression(path: NodePath<t.CallExpression>) {
|
|
||||||
// find the component, like: Component(() => {})
|
|
||||||
const callee = path.get('callee');
|
|
||||||
|
|
||||||
if (callee.isIdentifier() && callee.node.name === COMPONENT) {
|
|
||||||
const componentNode = extractFnFromMacro(path, COMPONENT);
|
|
||||||
let name = '';
|
|
||||||
// try to get the component name, when parent is a variable declarator
|
|
||||||
if (path.parentPath.isVariableDeclarator()) {
|
|
||||||
const lVal = path.parentPath.get('id');
|
|
||||||
if (lVal.isIdentifier()) {
|
|
||||||
name = lVal.node.name;
|
|
||||||
} else {
|
|
||||||
console.error('Component macro must be assigned to a variable');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const root = analyze(name, componentNode, {
|
|
||||||
htmlTags,
|
|
||||||
});
|
|
||||||
// The sub path has been visited, so we just skip
|
|
||||||
path.skip();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,8 +1,12 @@
|
||||||
import type babel from '@babel/core';
|
import type 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
|
@ -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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
|
@ -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]"');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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', () => {
|
|
@ -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';
|
|
||||||
}
|
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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)];
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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');
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue